2026-02-02 18:22:22 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<view class="container">
|
|
|
|
|
|
<!-- Header -->
|
|
|
|
|
|
<view class="header">
|
2026-02-10 13:40:49 +08:00
|
|
|
|
<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">
|
2026-02-02 18:22:22 +08:00
|
|
|
|
<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">
|
2026-02-06 14:12:56 +08:00
|
|
|
|
<view v-for="tab in tabs" :key="tab.key" class="tab-item" :class="{ active: currentTabKey === tab.key }"
|
|
|
|
|
|
@click="onTabClick(tab)">
|
2026-02-02 18:22:22 +08:00
|
|
|
|
{{ tab.label }}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</scroll-view>
|
|
|
|
|
|
<view class="total-count-inline">共{{ totalPatients }}条</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Main Content -->
|
|
|
|
|
|
<view class="content-body">
|
|
|
|
|
|
<!-- Patient List -->
|
2026-02-06 14:12:56 +08:00
|
|
|
|
<scroll-view scroll-y class="patient-list" :scroll-into-view="scrollIntoId" :scroll-with-animation="true"
|
|
|
|
|
|
lower-threshold="80" @scrolltolower="loadMore">
|
2026-02-02 18:22:22 +08:00
|
|
|
|
<view v-for="group in patientList" :key="group.letter" :id="letterToDomId(group.letter)">
|
|
|
|
|
|
<view class="group-title">{{ group.letter }}</view>
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
|
|
|
|
|
<view v-for="(patient, pIndex) in group.data" :key="pIndex" class="patient-card"
|
|
|
|
|
|
@click="handlePatientClick(patient)">
|
2026-02-02 18:22:22 +08:00
|
|
|
|
<!-- Checkbox for Batch Mode -->
|
|
|
|
|
|
<view v-if="isBatchMode" class="checkbox-area">
|
2026-02-06 14:12:56 +08:00
|
|
|
|
<uni-icons :type="selectedItems.includes(getSelectId(patient)) ? 'checkbox-filled' : 'checkbox'" size="24"
|
|
|
|
|
|
:color="selectedItems.includes(getSelectId(patient)) ? '#007aff' : '#ccc'"></uni-icons>
|
2026-02-02 18:22:22 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<view class="card-content">
|
|
|
|
|
|
<!-- Row 1 -->
|
|
|
|
|
|
<view class="card-row-top">
|
|
|
|
|
|
<view class="patient-info">
|
|
|
|
|
|
<text class="patient-name">{{ patient.name }}</text>
|
2026-02-06 14:12:56 +08:00
|
|
|
|
<text class="patient-meta">{{ patient.gender }}{{ patient.age ? '/' + patient.age + '岁' : '' }}</text>
|
2026-02-02 18:22:22 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<view class="patient-tags">
|
2026-02-04 15:48:42 +08:00
|
|
|
|
<view v-for="(tag, tIndex) in resolveGroupTags(patient)" :key="tIndex" class="tag">
|
2026-02-02 18:22:22 +08:00
|
|
|
|
{{ tag }}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 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 -->
|
2026-02-02 18:22:22 +08:00
|
|
|
|
<view class="card-row-bottom">
|
|
|
|
|
|
<template v-if="currentTabKey === 'new'"> <!-- New Patient Tab -->
|
|
|
|
|
|
<text class="record-text">
|
2026-02-09 14:20:42 +08:00
|
|
|
|
{{ resolveRecentAddTime(patient) }} {{ resolveRecentAddMeta(patient) }}
|
2026-02-02 18:22:22 +08:00
|
|
|
|
</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">
|
2026-02-06 14:12:56 +08:00
|
|
|
|
<view v-for="letter in indexList" :key="letter" class="index-item" @click="scrollToLetter(letter)">
|
2026-02-02 18:22:22 +08:00
|
|
|
|
{{ letter }}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Batch Actions Footer -->
|
|
|
|
|
|
<view v-if="isBatchMode" class="batch-footer">
|
|
|
|
|
|
<view class="left-action" @click="handleSelectAll">
|
2026-02-06 14:12:56 +08:00
|
|
|
|
<uni-icons
|
|
|
|
|
|
:type="selectedItems.length > 0 && selectedItems.length === patientList.flatMap(g => g.data).length ? 'checkbox-filled' : 'checkbox'"
|
|
|
|
|
|
size="24" color="#666"></uni-icons>
|
2026-02-02 18:22:22 +08:00
|
|
|
|
<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';
|
2026-02-09 18:57:22 +08:00
|
|
|
|
import useInfoCheck from '@/hooks/useInfoCheck';
|
2026-02-06 15:38:54 +08:00
|
|
|
|
import { confirm as uniConfirm, hideLoading, loading as showLoading, toast } from '@/utils/widget';
|
2026-02-02 18:22:22 +08:00
|
|
|
|
|
|
|
|
|
|
// State
|
|
|
|
|
|
const teams = ref([]);
|
|
|
|
|
|
const currentTeam = ref(null);
|
|
|
|
|
|
const currentTabKey = ref('all');
|
|
|
|
|
|
const scrollIntoId = ref('');
|
|
|
|
|
|
const teamGroups = ref([]);
|
2026-02-10 13:40:49 +08:00
|
|
|
|
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);
|
2026-02-02 18:22:22 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-02-04 15:48:42 +08:00
|
|
|
|
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;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
// Team Members Map
|
|
|
|
|
|
const userNameMap = ref({});
|
2026-02-09 16:29:46 +08:00
|
|
|
|
const loadedMembersTeamId = ref('');
|
|
|
|
|
|
const corpMemberNameInflight = new Map(); // userId -> Promise
|
|
|
|
|
|
const corpMemberNameTried = new Set(); // avoid retry storms on failures
|
|
|
|
|
|
let corpMemberBatchInflight = null; // Promise | null
|
2026-02-02 18:22:22 +08:00
|
|
|
|
|
|
|
|
|
|
// 新增流程所需状态(认证相关)
|
|
|
|
|
|
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);
|
2026-02-10 13:40:49 +08:00
|
|
|
|
const pageSize = ref(200);
|
2026-02-02 18:22:22 +08:00
|
|
|
|
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]);
|
2026-02-10 13:40:49 +08:00
|
|
|
|
const lastTeamsLoadOk = ref(true);
|
|
|
|
|
|
const lastGroupsLoadOk = ref(true);
|
|
|
|
|
|
const lastPatientsLoadOk = ref(true);
|
|
|
|
|
|
const loadedGroupsTeamId = ref('');
|
|
|
|
|
|
let enterRefreshInflight = null;
|
|
|
|
|
|
const hasEnteredOnce = ref(false);
|
2026-02-10 15:47:35 +08:00
|
|
|
|
let lastReloadStartedAt = 0;
|
2026-02-02 18:22:22 +08:00
|
|
|
|
|
|
|
|
|
|
const accountStore = useAccountStore();
|
|
|
|
|
|
const { account, doctorInfo } = storeToRefs(accountStore);
|
|
|
|
|
|
const { getDoctorInfo } = accountStore;
|
2026-02-09 18:57:22 +08:00
|
|
|
|
const { withInfo } = useInfoCheck();
|
2026-02-02 18:22:22 +08:00
|
|
|
|
|
2026-02-06 14:12:56 +08:00
|
|
|
|
const teamDisplay = computed(() => `${currentTeam.value?.name || ''}`);
|
2026-02-02 18:22:22 +08:00
|
|
|
|
|
2026-02-10 13:40:49 +08:00
|
|
|
|
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 }
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
function asArray(value) {
|
|
|
|
|
|
return Array.isArray(value) ? value : [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 13:40:49 +08:00
|
|
|
|
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: '请求失败' };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 16:29:46 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 17:17:34 +08:00
|
|
|
|
// 2) 兜底:成员列表接口(支持 memberList)
|
2026-02-09 16:29:46 +08:00
|
|
|
|
try {
|
2026-02-27 17:17:34 +08:00
|
|
|
|
const res = await api(
|
|
|
|
|
|
'getCorpMember',
|
|
|
|
|
|
{
|
|
|
|
|
|
page: 1,
|
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
|
params: {
|
|
|
|
|
|
corpId,
|
|
|
|
|
|
memberList: [id],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
false
|
|
|
|
|
|
);
|
2026-02-09 16:29:46 +08:00
|
|
|
|
if (res?.success) {
|
2026-02-27 17:17:34 +08:00
|
|
|
|
const rows = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.data) ? res.data.data : [];
|
|
|
|
|
|
const row =
|
|
|
|
|
|
rows.find((m) => normalizeUserId(m?.userid || m?.userId || m?.corpUserId || '') === id) || rows[0] || null;
|
|
|
|
|
|
const name = extractDisplayNameFromCorpMember(row) || '';
|
2026-02-09 16:29:46 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
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 || '') || '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 11:21:52 +08:00
|
|
|
|
function hasCompleteUserInfo() {
|
|
|
|
|
|
const userId = getUserId();
|
|
|
|
|
|
const corpId = String(account.value?.corpId || doctorInfo.value?.corpId || '') || '';
|
2026-02-08 13:47:47 +08:00
|
|
|
|
const anotherName = String(doctorInfo.value?.anotherName || '').trim();
|
|
|
|
|
|
return Boolean(corpId && userId && anotherName);
|
2026-02-08 11:21:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ensureUserInfoForFeature() {
|
|
|
|
|
|
if (hasCompleteUserInfo()) return true;
|
|
|
|
|
|
toast('请先完善个人信息再使用该功能');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 16:29:46 +08:00
|
|
|
|
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());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
async function loadTeamMembers() {
|
|
|
|
|
|
const corpId = getCorpId();
|
|
|
|
|
|
const teamId = getTeamId();
|
|
|
|
|
|
if (!corpId || !teamId) return;
|
2026-02-09 16:29:46 +08:00
|
|
|
|
if (loadedMembersTeamId.value === teamId && Object.keys(userNameMap.value || {}).length > 0) return;
|
2026-02-02 18:22:22 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-02-09 16:29:46 +08:00
|
|
|
|
const nextMap = { ...(userNameMap.value || {}) };
|
|
|
|
|
|
|
|
|
|
|
|
// 以团队详情为准(getTeamMemberAvatarsAndName 存在不全/不准的情况)
|
2026-02-02 18:22:22 +08:00
|
|
|
|
const res = await api('getTeamData', { corpId, teamId });
|
2026-02-09 16:29:46 +08:00
|
|
|
|
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;
|
2026-02-02 18:22:22 +08:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('获取团队成员失败', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveCreatorName(patient) {
|
|
|
|
|
|
const val = patient.creator;
|
|
|
|
|
|
if (!val) return '';
|
2026-02-09 16:29:46 +08:00
|
|
|
|
return resolveUserName(val);
|
2026-02-02 18:22:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 14:20:42 +08:00
|
|
|
|
function resolveRecentAddTime(patient) {
|
|
|
|
|
|
return patient?.recentAddTime || patient?.createTime || '-';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveRecentAddOperatorName(patient) {
|
2026-02-09 16:29:46 +08:00
|
|
|
|
const nameFromApi = String(patient?.recentAddOperatorName || '').trim();
|
|
|
|
|
|
// 后端部分场景会把 userid 填到 name 字段里,此时仍需通过 userid 解析姓名
|
|
|
|
|
|
if (nameFromApi && !isLikelyUserId(nameFromApi)) return nameFromApi;
|
|
|
|
|
|
const uid = patient?.recentAddOperatorUserId || nameFromApi || patient?.creator || '';
|
2026-02-09 14:20:42 +08:00
|
|
|
|
if (!uid) return '';
|
2026-02-09 16:29:46 +08:00
|
|
|
|
return resolveUserName(uid);
|
2026-02-09 14:20:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 '-';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
|
|
|
|
|
|
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 };
|
2026-02-10 13:40:49 +08:00
|
|
|
|
let res;
|
|
|
|
|
|
try {
|
|
|
|
|
|
res = await apiWithRetry(
|
|
|
|
|
|
'getGroups',
|
|
|
|
|
|
{ corpId, teamId, page: 1, pageSize: 1000, projection, countGroupMember: false },
|
|
|
|
|
|
false,
|
|
|
|
|
|
{ retries: 1 }
|
|
|
|
|
|
);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
res = null;
|
|
|
|
|
|
}
|
2026-02-02 18:22:22 +08:00
|
|
|
|
if (!res?.success) {
|
2026-02-10 13:40:49 +08:00
|
|
|
|
lastGroupsLoadOk.value = false;
|
|
|
|
|
|
if (!loadedGroupsTeamId.value || loadedGroupsTeamId.value !== teamId) teamGroups.value = [];
|
2026-02-02 18:22:22 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-10 13:40:49 +08:00
|
|
|
|
lastGroupsLoadOk.value = true;
|
|
|
|
|
|
loadedGroupsTeamId.value = teamId;
|
2026-02-02 18:22:22 +08:00
|
|
|
|
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 || '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 15:48:42 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 16:29:46 +08:00
|
|
|
|
function normalizeMedicalType(raw) {
|
|
|
|
|
|
const s = String(raw || '').trim();
|
|
|
|
|
|
if (!s) return '';
|
|
|
|
|
|
const lower = s.toLowerCase();
|
2026-02-09 18:57:22 +08:00
|
|
|
|
// 常见后缀:xxxRecord(后端/历史数据可能返回此形式)
|
|
|
|
|
|
if (lower.endsWith('record') && s.length > 6) {
|
|
|
|
|
|
return normalizeMedicalType(s.slice(0, -6));
|
|
|
|
|
|
}
|
2026-02-09 16:29:46 +08:00
|
|
|
|
// 中文兜底(部分接口返回展示名)
|
|
|
|
|
|
if (s.includes('门诊')) return 'outpatient';
|
|
|
|
|
|
if (s.includes('住院') || s.includes('入院')) return 'inhospital';
|
2026-02-10 15:47:35 +08:00
|
|
|
|
if (s.includes('预问诊') || s.includes('问诊')) return 'preConsultationRecord';
|
2026-02-09 16:29:46 +08:00
|
|
|
|
if (s.includes('体检')) return 'physicalExaminationTemplate';
|
2026-02-10 15:47:35 +08:00
|
|
|
|
if (lower.includes('preconsult') || (lower.includes('pre') && lower.includes('consult'))) return 'preConsultationRecord';
|
2026-02-09 16:29:46 +08:00
|
|
|
|
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';
|
2026-02-12 15:54:50 +08:00
|
|
|
|
if (s === 'preConsultationRecord') return 'preConsultationRecord';
|
2026-02-10 15:47:35 +08:00
|
|
|
|
if (s === 'preConsultationRecord') return 'preConsultationRecord';
|
2026-02-09 16:29:46 +08:00
|
|
|
|
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;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-09 17:18:32 +08:00
|
|
|
|
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',
|
2026-02-10 15:47:35 +08:00
|
|
|
|
'consultationDate',
|
2026-02-09 17:18:32 +08:00
|
|
|
|
'consultDate',
|
|
|
|
|
|
'inspectDate',
|
|
|
|
|
|
'sortTime',
|
|
|
|
|
|
'createTime',
|
|
|
|
|
|
'createdAt',
|
|
|
|
|
|
'updateTime',
|
|
|
|
|
|
'updatedAt',
|
|
|
|
|
|
'time'
|
|
|
|
|
|
);
|
2026-02-09 16:29:46 +08:00
|
|
|
|
const rawDateStr = String(rawDate ?? '').trim();
|
|
|
|
|
|
const date = (/^\d{10,13}$/.test(rawDateStr) ? (formatAnyDate(rawDateStr, 'YYYY-MM-DD') || rawDateStr) : rawDateStr)
|
2026-02-10 15:47:35 +08:00
|
|
|
|
|| formatAnyDate(pick('visitTime', 'inhosDate', 'consultationDate', 'consultDate', 'inspectDate', 'sortTime', 'createTime', 'updateTime'), 'YYYY-MM-DD')
|
2026-02-09 16:29:46 +08:00
|
|
|
|
|| '-';
|
|
|
|
|
|
|
|
|
|
|
|
let third = '';
|
2026-02-10 15:47:35 +08:00
|
|
|
|
// searchCorpCustomerForCaseList 接口存在问题,按类型优先展示:
|
|
|
|
|
|
// - 预问诊:chiefComplaint
|
|
|
|
|
|
// - 体检:inspectSummary
|
|
|
|
|
|
// - 门诊/住院:diagnosisName
|
|
|
|
|
|
if (uiType === 'outpatient' || uiType === 'inhospital') {
|
|
|
|
|
|
third = normalizeText(pick('diagnosisName', 'diagnosis'));
|
2026-02-09 16:29:46 +08:00
|
|
|
|
} else if (uiType === 'physicalExaminationTemplate') {
|
2026-02-10 15:47:35 +08:00
|
|
|
|
third = normalizeText(pick('inspectSummary', 'summary', 'summaryText', 'inspectConclusion', 'inspectResult'));
|
|
|
|
|
|
} else if (uiType === 'preConsultationRecord') {
|
|
|
|
|
|
third = normalizeText(pick('chiefComplaint', 'chiefComplain', 'chiefComplaintText', 'chiefComplaintContent'));
|
2026-02-09 16:29:46 +08:00
|
|
|
|
} else {
|
2026-02-10 15:47:35 +08:00
|
|
|
|
third = normalizeText(pick('diagnosisName', 'diagnosis', 'inspectSummary', 'summary', 'chiefComplaint'));
|
2026-02-09 16:29:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
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' ? '住院记录'
|
2026-02-10 15:47:35 +08:00
|
|
|
|
: uiType === 'preConsultationRecord' ? '预问诊记录'
|
2026-02-09 16:29:46 +08:00
|
|
|
|
: uiType === 'physicalExaminationTemplate' ? '体检档案'
|
|
|
|
|
|
: '-');
|
|
|
|
|
|
|
|
|
|
|
|
return { type, date, diagnosis: third };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
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') : '';
|
2026-02-04 15:48:42 +08:00
|
|
|
|
const createTimeTs = createTime ? createTime.valueOf() : 0;
|
2026-02-02 18:22:22 +08:00
|
|
|
|
|
2026-02-09 14:20:42 +08:00
|
|
|
|
// 最近一次“新增到当前团队”的时间(后端计算:创建/转移/共享),没有则退化为 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 || '');
|
|
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
// 优先使用后端返回的 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);
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
// 解析标签:优先 tagNames > tags(字符串) > tagIds
|
|
|
|
|
|
const displayTags = rawTagNames.length ? rawTagNames : (rawTags.length ? rawTags : []);
|
|
|
|
|
|
|
|
|
|
|
|
// 解析病历信息
|
|
|
|
|
|
let record = null;
|
2026-02-09 16:29:46 +08:00
|
|
|
|
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);
|
2026-02-02 18:22:22 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...raw,
|
|
|
|
|
|
_id: raw?._id || raw?.id || '',
|
|
|
|
|
|
name: String(name || ''),
|
|
|
|
|
|
gender: String(sex || ''),
|
|
|
|
|
|
age,
|
|
|
|
|
|
tags: displayTags,
|
|
|
|
|
|
mobiles,
|
|
|
|
|
|
mobile,
|
|
|
|
|
|
createTime: createTimeStr,
|
2026-02-04 15:48:42 +08:00
|
|
|
|
createTimeTs,
|
2026-02-09 14:20:42 +08:00
|
|
|
|
recentAddTime: recentAddTimeStr,
|
|
|
|
|
|
recentAddTimeTs,
|
|
|
|
|
|
recentAddType,
|
|
|
|
|
|
recentAddOperatorUserId,
|
2026-02-09 16:29:46 +08:00
|
|
|
|
recentAddOperatorName: String(
|
|
|
|
|
|
raw?.recentAddOperatorName
|
|
|
|
|
|
|| raw?.recentAddOperatorAnotherName
|
|
|
|
|
|
|| raw?.recentAddOperatorUserName
|
|
|
|
|
|
|| raw?.recentAddOperatorRealName
|
|
|
|
|
|
|| raw?.recentAddOperatorDisplayName
|
|
|
|
|
|
|| ''
|
|
|
|
|
|
).trim(),
|
2026-02-02 18:22:22 +08:00
|
|
|
|
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) || [] }));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 13:40:49 +08:00
|
|
|
|
async function loadTeams(opts = {}) {
|
2026-02-02 18:22:22 +08:00
|
|
|
|
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) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 13:40:49 +08:00
|
|
|
|
let res;
|
|
|
|
|
|
try {
|
|
|
|
|
|
res = await apiWithRetry('getTeamBymember', { corpId, corpUserId: userId }, !opts?.silent, { retries: 2 });
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
res = null;
|
|
|
|
|
|
}
|
2026-02-02 18:22:22 +08:00
|
|
|
|
if (!res?.success) {
|
2026-02-10 13:40:49 +08:00
|
|
|
|
lastTeamsLoadOk.value = false;
|
|
|
|
|
|
if (!opts?.silent) toast(res?.message || '获取团队失败');
|
2026-02-02 18:22:22 +08:00
|
|
|
|
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;
|
2026-02-10 13:40:49 +08:00
|
|
|
|
lastTeamsLoadOk.value = true;
|
2026-02-02 18:22:22 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 13:40:49 +08:00
|
|
|
|
async function reload(reset = true, opts = {}) {
|
|
|
|
|
|
if (!currentTeam.value) return false;
|
|
|
|
|
|
if (loading.value) return false;
|
2026-02-02 18:22:22 +08:00
|
|
|
|
|
2026-02-09 16:29:46 +08:00
|
|
|
|
await ensureDoctorForQuery();
|
2026-02-02 18:22:22 +08:00
|
|
|
|
const userId = getUserId();
|
|
|
|
|
|
const corpId = getCorpId();
|
|
|
|
|
|
const teamId = getTeamId();
|
|
|
|
|
|
if (!corpId || !teamId || !userId) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 13:40:49 +08:00
|
|
|
|
const keepOnFail = Boolean(opts?.keepOnFail);
|
|
|
|
|
|
const targetPage = reset ? 1 : page.value;
|
|
|
|
|
|
|
|
|
|
|
|
if (reset && !keepOnFail) {
|
2026-02-02 18:22:22 +08:00
|
|
|
|
page.value = 1;
|
|
|
|
|
|
rawPatients.value = [];
|
|
|
|
|
|
pages.value = 0;
|
|
|
|
|
|
totalFromApi.value = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 13:40:49 +08:00
|
|
|
|
if (reset) {
|
|
|
|
|
|
const net = await getNetworkTypeSafe();
|
|
|
|
|
|
pageSize.value = pickPageSizeByNetwork(net);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
const query = {
|
|
|
|
|
|
corpId,
|
|
|
|
|
|
userId,
|
|
|
|
|
|
teamId,
|
2026-02-10 13:40:49 +08:00
|
|
|
|
page: targetPage,
|
|
|
|
|
|
pageSize: pageSize.value, // 按首字母排序时,按网络自适应加载数量
|
2026-02-02 18:22:22 +08:00
|
|
|
|
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();
|
2026-02-09 14:20:42 +08:00
|
|
|
|
// “新患者”= 最近7天新增到当前团队:创建 + 转移/共享(时间来自服务记录)
|
|
|
|
|
|
query.startRecentTime = start;
|
|
|
|
|
|
query.endRecentTime = end;
|
|
|
|
|
|
query.includeRecentAddTime = true;
|
2026-02-02 18:22:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 15:47:35 +08:00
|
|
|
|
lastReloadStartedAt = Date.now();
|
2026-02-02 18:22:22 +08:00
|
|
|
|
loading.value = true;
|
2026-02-10 13:40:49 +08:00
|
|
|
|
let res;
|
|
|
|
|
|
try {
|
|
|
|
|
|
res = await apiWithRetry('searchCorpCustomerForCaseList', query, !opts?.silent, { retries: 1 });
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
res = null;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
}
|
2026-02-02 18:22:22 +08:00
|
|
|
|
|
|
|
|
|
|
if (!res?.success) {
|
2026-02-10 13:40:49 +08:00
|
|
|
|
lastPatientsLoadOk.value = false;
|
|
|
|
|
|
if (!opts?.silent || (rawPatients.value || []).length === 0) toast(res?.message || '获取患者列表失败');
|
|
|
|
|
|
return false;
|
2026-02-02 18:22:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 13:40:49 +08:00
|
|
|
|
if (reset) page.value = 1;
|
|
|
|
|
|
lastPatientsLoadOk.value = true;
|
|
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
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);
|
2026-02-10 13:40:49 +08:00
|
|
|
|
rawPatients.value = targetPage === 1 ? next : [...rawPatients.value, ...next];
|
2026-02-09 16:29:46 +08:00
|
|
|
|
// 补齐创建人/新增人姓名(部分创建人不在当前团队成员列表中)
|
|
|
|
|
|
void prefetchUserNamesFromPatients(next).catch(() => {});
|
2026-02-02 18:22:22 +08:00
|
|
|
|
pages.value = Number(payload.pages || 0) || 0;
|
|
|
|
|
|
totalFromApi.value = Number(payload.total || 0) || rawPatients.value.length;
|
|
|
|
|
|
managedArchiveCountAllTeams.value =
|
|
|
|
|
|
Number(
|
|
|
|
|
|
payload.totalAllTeams ||
|
2026-02-06 14:12:56 +08:00
|
|
|
|
payload.totalAllTeam ||
|
|
|
|
|
|
payload.totalAllTeamsCount ||
|
|
|
|
|
|
managedArchiveCountAllTeams.value ||
|
|
|
|
|
|
totalFromApi.value ||
|
|
|
|
|
|
0
|
2026-02-02 18:22:22 +08:00
|
|
|
|
) || (totalFromApi.value || 0);
|
2026-02-10 13:40:49 +08:00
|
|
|
|
|
|
|
|
|
|
return true;
|
2026-02-02 18:22:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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') {
|
2026-02-04 15:48:42 +08:00
|
|
|
|
const sevenDaysAgo = dayjs().subtract(7, 'day').startOf('day').valueOf();
|
2026-02-02 18:22:22 +08:00
|
|
|
|
const flatList = all
|
2026-02-09 14:20:42 +08:00
|
|
|
|
.filter((p) => Number(p?.recentAddTimeTs || p?.createTimeTs || 0) >= sevenDaysAgo)
|
2026-02-04 15:48:42 +08:00
|
|
|
|
.slice()
|
2026-02-09 14:20:42 +08:00
|
|
|
|
.sort(
|
|
|
|
|
|
(a, b) =>
|
|
|
|
|
|
Number(b?.recentAddTimeTs || b?.createTimeTs || 0) -
|
|
|
|
|
|
Number(a?.recentAddTimeTs || a?.createTimeTs || 0)
|
|
|
|
|
|
);
|
2026-02-06 14:12:56 +08:00
|
|
|
|
return [{ letter: '最近7天新增', data: flatList }];
|
2026-02-02 18:22:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return groupByLetter(all);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const indexList = computed(() => {
|
|
|
|
|
|
if (currentTab.value.kind === 'new') return []; // No index bar for new patient
|
2026-02-06 14:12:56 +08:00
|
|
|
|
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'.split('').filter(l => patientList.value.some(g => g.letter === l));
|
|
|
|
|
|
return letters;
|
2026-02-02 18:22:22 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const totalPatients = computed(() => {
|
|
|
|
|
|
let count = 0;
|
|
|
|
|
|
patientList.value.forEach(g => count += g.data.length);
|
2026-02-09 14:20:42 +08:00
|
|
|
|
if (currentTab.value.kind === 'new') return count;
|
2026-02-02 18:22:22 +08:00
|
|
|
|
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);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-10 13:40:49 +08:00
|
|
|
|
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]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
const toggleTeamPopup = () => {
|
|
|
|
|
|
if (checkBatchMode()) return;
|
2026-02-10 13:40:49 +08:00
|
|
|
|
if (useTeamPicker.value) return;
|
2026-02-02 18:22:22 +08:00
|
|
|
|
if (!teams.value.length) {
|
|
|
|
|
|
toast('暂无可选团队');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
uni.showActionSheet({
|
|
|
|
|
|
itemList: teams.value.map((i) => i.name),
|
2026-02-09 16:29:46 +08:00
|
|
|
|
success: async (res) => {
|
2026-02-10 13:40:49 +08:00
|
|
|
|
await applyTeamSelection(teams.value[res.tapIndex] || teams.value[0] || null);
|
2026-02-02 18:22:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const goToSearch = () => {
|
|
|
|
|
|
if (checkBatchMode()) return;
|
|
|
|
|
|
uni.navigateTo({
|
|
|
|
|
|
url: '/pages/case/search'
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-09 18:57:22 +08:00
|
|
|
|
const goToGroupManage = withInfo(() => {
|
2026-02-02 18:22:22 +08:00
|
|
|
|
if (checkBatchMode()) return;
|
|
|
|
|
|
uni.navigateTo({
|
|
|
|
|
|
url: '/pages/case/group-manage'
|
|
|
|
|
|
});
|
2026-02-09 18:57:22 +08:00
|
|
|
|
});
|
2026-02-02 18:22:22 +08:00
|
|
|
|
|
2026-02-09 18:57:22 +08:00
|
|
|
|
const toggleBatchMode = withInfo(() => {
|
2026-02-02 18:22:22 +08:00
|
|
|
|
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 = [];
|
2026-02-09 18:57:22 +08:00
|
|
|
|
});
|
2026-02-02 18:22:22 +08:00
|
|
|
|
|
2026-02-11 16:48:26 +08:00
|
|
|
|
const handleCreate = withInfo(async () => {
|
2026-02-02 18:22:22 +08:00
|
|
|
|
if (checkBatchMode()) return;
|
2026-02-11 16:48:26 +08:00
|
|
|
|
|
|
|
|
|
|
// 规则沿用原前端逻辑:
|
|
|
|
|
|
// - 认证中按未认证控制(上限10,且直接toast阻止)
|
|
|
|
|
|
// - 未认证:达到10提示去认证
|
|
|
|
|
|
// - 已认证:达到100提示联系客服
|
|
|
|
|
|
// 只是把“在管档案数”来源改为新接口(医生创建的所有团队汇总)。
|
|
|
|
|
|
let total = null;
|
|
|
|
|
|
let limit = null;
|
|
|
|
|
|
let unlimited = false;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const corpId = getCorpId();
|
|
|
|
|
|
const userId = getUserId();
|
|
|
|
|
|
if (corpId && userId) {
|
|
|
|
|
|
const res = await api('doctorCreatedTeamsCustomerLimitation', { corpId, userId }, false);
|
|
|
|
|
|
if (res?.success) {
|
|
|
|
|
|
const count = Number(res?.count ?? res?.data?.count);
|
|
|
|
|
|
if (Number.isFinite(count)) total = count;
|
|
|
|
|
|
|
|
|
|
|
|
const rawLimit = Number(res?.limit ?? res?.data?.limit);
|
|
|
|
|
|
if (Number.isFinite(rawLimit)) {
|
|
|
|
|
|
limit = rawLimit;
|
|
|
|
|
|
unlimited = rawLimit === -1;
|
2026-02-02 18:22:22 +08:00
|
|
|
|
}
|
2026-02-11 16:48:26 +08:00
|
|
|
|
}
|
2026-02-09 15:40:03 +08:00
|
|
|
|
}
|
2026-02-11 16:48:26 +08:00
|
|
|
|
} 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;
|
|
|
|
|
|
}
|
2026-02-09 15:40:03 +08:00
|
|
|
|
|
2026-02-11 16:48:26 +08:00
|
|
|
|
// 无限:直接放行
|
|
|
|
|
|
if (unlimited) {
|
|
|
|
|
|
uni.showActionSheet({
|
|
|
|
|
|
itemList: ['邀请患者建档', '我帮患者建档'],
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
if (res.tapIndex === 0) {
|
|
|
|
|
|
openInvitePatientEntry();
|
|
|
|
|
|
} else if (res.tapIndex === 1) {
|
|
|
|
|
|
openCreatePatientEntry();
|
|
|
|
|
|
}
|
2026-02-02 18:22:22 +08:00
|
|
|
|
}
|
2026-02-11 16:48:26 +08:00
|
|
|
|
});
|
|
|
|
|
|
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();
|
2026-02-09 15:40:03 +08:00
|
|
|
|
}
|
2026-02-11 16:48:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 未认证/认证中:达到上限 -> 提示去认证(认证中直接toast阻止)
|
|
|
|
|
|
if (!isVerified.value && total >= limit) {
|
|
|
|
|
|
if (verifyStatus.value === 'verifying') {
|
|
|
|
|
|
toast('信息认证中,请耐心等待!');
|
2026-02-09 15:40:03 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-11 16:48:26 +08:00
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '提示',
|
2026-03-06 15:02:56 +08:00
|
|
|
|
content: '当前管理档案数已达上限10个,完成认证后可提升档案管理数至100个。',
|
2026-02-11 16:48:26 +08:00
|
|
|
|
cancelText: '暂不认证',
|
|
|
|
|
|
confirmText: '去认证',
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
if (res.confirm) {
|
|
|
|
|
|
startVerifyFlow();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
2026-02-02 18:22:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 未达上限:显示新增入口
|
|
|
|
|
|
uni.showActionSheet({
|
|
|
|
|
|
itemList: ['邀请患者建档', '我帮患者建档'],
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
if (res.tapIndex === 0) {
|
|
|
|
|
|
openInvitePatientEntry();
|
|
|
|
|
|
} else if (res.tapIndex === 1) {
|
|
|
|
|
|
openCreatePatientEntry();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-02-09 18:57:22 +08:00
|
|
|
|
});
|
2026-02-02 18:22:22 +08:00
|
|
|
|
|
|
|
|
|
|
// 新增流程:认证分支
|
|
|
|
|
|
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 = () => {
|
2026-02-04 15:48:42 +08:00
|
|
|
|
uni.navigateTo({ url: '/pages/work/service/contact-service' });
|
2026-02-02 18:22:22 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const openInvitePatientEntry = () => {
|
2026-02-04 15:48:42 +08:00
|
|
|
|
uni.navigateTo({ url: '/pages/work/team/invite/invite-patient' });
|
2026-02-02 18:22:22 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
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 = [];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-06 15:38:54 +08:00
|
|
|
|
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();
|
|
|
|
|
|
} 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();
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
toast('操作失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
hideLoading();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleTransfer = async () => {
|
2026-02-02 18:22:22 +08:00
|
|
|
|
if (selectedItems.value.length === 0) {
|
|
|
|
|
|
uni.showToast({ title: '请选择患者', icon: 'none' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-06 15:38:54 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-02-02 18:22:22 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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' });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-10 13:40:49 +08:00
|
|
|
|
async function loadMore() {
|
2026-02-02 18:22:22 +08:00
|
|
|
|
if (!more.value || loading.value) return;
|
2026-02-10 13:40:49 +08:00
|
|
|
|
const nextPage = page.value + 1;
|
|
|
|
|
|
page.value = nextPage;
|
|
|
|
|
|
const ok = await reload(false, { silent: true });
|
|
|
|
|
|
if (!ok) page.value = nextPage - 1;
|
2026-02-02 18:22:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(currentTeam, (t) => {
|
|
|
|
|
|
if (!t) return;
|
|
|
|
|
|
uni.setStorageSync(CURRENT_TEAM_STORAGE_KEY, t);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
watch(currentTabKey, () => {
|
|
|
|
|
|
if (!currentTeam.value) return;
|
2026-02-10 13:40:49 +08:00
|
|
|
|
if (suppressTabReload.value) return;
|
2026-02-09 16:29:46 +08:00
|
|
|
|
loadTeamMembers();
|
2026-02-02 18:22:22 +08:00
|
|
|
|
reload(true);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function onTabClick(tab) {
|
|
|
|
|
|
if (checkBatchMode()) return;
|
|
|
|
|
|
if (!tab || !tab.key) return;
|
|
|
|
|
|
if (currentTabKey.value === tab.key) return;
|
2026-02-08 11:21:52 +08:00
|
|
|
|
|
|
|
|
|
|
if (tab.kind === 'group' && !ensureUserInfoForFeature()) return;
|
2026-02-02 18:22:22 +08:00
|
|
|
|
currentTabKey.value = tab.key;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 13:40:49 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
onLoad(async () => {
|
2026-02-10 13:40:49 +08:00
|
|
|
|
await refreshOnEnter();
|
2026-02-02 18:22:22 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
onShow(async () => {
|
2026-02-10 15:47:35 +08:00
|
|
|
|
const showAt = Date.now();
|
|
|
|
|
|
const wasEnteredOnce = hasEnteredOnce.value;
|
2026-02-02 18:22:22 +08:00
|
|
|
|
const need = uni.getStorageSync(NEED_RELOAD_STORAGE_KEY);
|
|
|
|
|
|
if (need) {
|
|
|
|
|
|
uni.removeStorageSync(NEED_RELOAD_STORAGE_KEY);
|
|
|
|
|
|
// 批量操作完成后回到列表,默认退出批量态
|
|
|
|
|
|
isBatchMode.value = false;
|
|
|
|
|
|
selectedItems.value = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const needGroups = uni.getStorageSync(GROUPS_RELOAD_KEY);
|
2026-02-10 13:40:49 +08:00
|
|
|
|
if (needGroups) uni.removeStorageSync(GROUPS_RELOAD_KEY);
|
|
|
|
|
|
|
|
|
|
|
|
await refreshOnEnter();
|
2026-02-10 15:47:35 +08:00
|
|
|
|
|
|
|
|
|
|
// 避免“复用旧的 inflight 请求”导致回到本页仍然展示旧数据:
|
|
|
|
|
|
// 只有在非首次进入且本次 onShow 没有发起新的 reload 时,才补一次 reload。
|
|
|
|
|
|
if (wasEnteredOnce && lastReloadStartedAt && lastReloadStartedAt < showAt) {
|
|
|
|
|
|
await reload(true, { silent: true, keepOnFail: true });
|
|
|
|
|
|
}
|
2026-02-02 18:22:22 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
height: 100vh;
|
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
padding-bottom: 0; // Default
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
// Padding for batch footer
|
|
|
|
|
|
/* &.is-batch {
|
|
|
|
|
|
padding-bottom: 100rpx;
|
2026-02-06 14:12:56 +08:00
|
|
|
|
} */
|
2026-02-02 18:22:22 +08:00
|
|
|
|
// 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;
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
.team-name {
|
|
|
|
|
|
margin-right: 10rpx;
|
|
|
|
|
|
}
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
.team-icon {
|
|
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 30rpx;
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
.action-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
.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;
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
.tabs-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
padding: 20rpx 30rpx;
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
.tab-item {
|
|
|
|
|
|
padding: 10rpx 30rpx;
|
|
|
|
|
|
margin-right: 20rpx;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
border-radius: 8rpx;
|
|
|
|
|
|
flex-shrink: 0;
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
&.active {
|
|
|
|
|
|
color: #5d8aff;
|
|
|
|
|
|
background-color: #e6f0ff;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.total-count-inline {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
flex-shrink: 0;
|
2026-02-06 14:12:56 +08:00
|
|
|
|
min-width: 70rpx;
|
2026-02-02 18:22:22 +08:00
|
|
|
|
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;
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
.tag {
|
|
|
|
|
|
font-size: 20rpx;
|
|
|
|
|
|
color: #5d8aff;
|
|
|
|
|
|
border: 2rpx solid #5d8aff;
|
|
|
|
|
|
padding: 0 8rpx;
|
|
|
|
|
|
border-radius: 16rpx;
|
|
|
|
|
|
height: 32rpx;
|
|
|
|
|
|
line-height: 28rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 14:12:56 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
.card-row-bottom {
|
|
|
|
|
|
font-size: 28rpx;
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
.record-text {
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
.record-ellipsis {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
}
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
.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;
|
2026-02-06 14:12:56 +08:00
|
|
|
|
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
|
2026-02-02 18:22:22 +08:00
|
|
|
|
z-index: 99;
|
|
|
|
|
|
|
|
|
|
|
|
.left-action {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
.footer-text {
|
|
|
|
|
|
margin-left: 10rpx;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.right-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 20rpx;
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
.footer-btn {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
padding: 0 30rpx;
|
|
|
|
|
|
height: 64rpx;
|
|
|
|
|
|
line-height: 64rpx;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
border-radius: 8rpx;
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
&.plain {
|
|
|
|
|
|
border: 2rpx solid #ddd;
|
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
&.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;
|
2026-02-06 14:12:56 +08:00
|
|
|
|
|
2026-02-02 18:22:22 +08:00
|
|
|
|
.index-item {
|
|
|
|
|
|
font-size: 20rpx;
|
|
|
|
|
|
color: #555;
|
|
|
|
|
|
padding: 4rpx 0;
|
|
|
|
|
|
width: 40rpx;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|