Merge branch 'dev-wdb' of http://175.27.226.205:3000/huxuejian/ykt-wxapp into dev-wdb
This commit is contained in:
commit
89f460d5c1
@ -22,7 +22,7 @@
|
||||
<slot name="footer"></slot>
|
||||
</view>
|
||||
<!-- #ifdef MP-->
|
||||
<!-- <view v-if="showSafeArea" class="safeareaBottom"></view> -->
|
||||
<view v-if="showSafeArea" class="safeareaBottom"></view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -9,7 +9,11 @@ export default function useInfoCheck() {
|
||||
function withInfo(fn) {
|
||||
return async (...args) => {
|
||||
if (!doctorInfo.value || !doctorInfo.value.anotherName) {
|
||||
await confirm('请先完善您的个人信息,方可使用该功能!', { cancelText: '再等等', confirmText: '去完善' })
|
||||
try {
|
||||
await confirm('请先完善您的个人信息,方可使用该功能!', { cancelText: '再等等', confirmText: '去完善' })
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
return uni.navigateTo({ url: '/pages/work/profile' });
|
||||
}
|
||||
return fn(...args);
|
||||
|
||||
@ -334,18 +334,26 @@ async function fetchArchive() {
|
||||
await ensureDoctor();
|
||||
loading('加载中...');
|
||||
try {
|
||||
const prevLocalChatGroupId = normalizeGroupId(chatGroupId.value || archive.value.chatGroupId || '');
|
||||
const res = await api('getCustomerByCustomerId', { customerId: archiveId.value });
|
||||
if (!res?.success) {
|
||||
toast(res?.message || '获取档案失败');
|
||||
return;
|
||||
}
|
||||
archive.value = { ...archive.value, ...normalizeArchiveFromApi(res.data) };
|
||||
|
||||
const normalized = normalizeArchiveFromApi(res.data);
|
||||
// 后端经常不返回 chatGroupId(或为空),但本地已探测出可用群聊时不能被覆盖掉,否则返回页面会导致“去聊天”按钮短暂/持续消失
|
||||
if (!normalized.chatGroupId && prevLocalChatGroupId) {
|
||||
normalized.chatGroupId = prevLocalChatGroupId;
|
||||
}
|
||||
|
||||
archive.value = { ...archive.value, ...normalized };
|
||||
saveToStorage();
|
||||
loadTeamMembers();
|
||||
await fetchTeamGroups(true);
|
||||
chatGroupId.value = normalizeGroupId(archive.value.chatGroupId || '');
|
||||
if (!chatGroupId.value) {
|
||||
refreshChatRoom();
|
||||
await refreshChatRoom({ force: true });
|
||||
} else {
|
||||
const meta = await getChatRoomMeta(chatGroupId.value);
|
||||
const ok = isChatRoomForArchive(meta, archiveId.value);
|
||||
@ -354,7 +362,7 @@ async function fetchArchive() {
|
||||
if (!ok || (currentTeamId && metaTeamId && metaTeamId !== currentTeamId)) {
|
||||
chatGroupId.value = '';
|
||||
archive.value.chatGroupId = '';
|
||||
refreshChatRoom();
|
||||
await refreshChatRoom({ force: true });
|
||||
} else {
|
||||
archive.value.chatGroupId = chatGroupId.value;
|
||||
}
|
||||
@ -618,7 +626,7 @@ const goEdit = () => {
|
||||
const goChat = async () => {
|
||||
let gid = normalizeGroupId(currentChatGroupId.value || '');
|
||||
if (!gid) {
|
||||
await refreshChatRoom();
|
||||
await refreshChatRoom({ force: true });
|
||||
gid = normalizeGroupId(currentChatGroupId.value || '');
|
||||
}
|
||||
if (!gid) {
|
||||
@ -634,7 +642,7 @@ const goChat = async () => {
|
||||
chatGroupId.value = '';
|
||||
archive.value.chatGroupId = '';
|
||||
saveToStorage();
|
||||
await refreshChatRoom();
|
||||
await refreshChatRoom({ force: true });
|
||||
gid = normalizeGroupId(currentChatGroupId.value || '');
|
||||
}
|
||||
if (!gid) {
|
||||
@ -650,6 +658,7 @@ const goChat = async () => {
|
||||
|
||||
const isRefreshingChatRoom = ref(false);
|
||||
let lastRefreshChatRoomAt = 0;
|
||||
let refreshChatRoomPromise = null;
|
||||
|
||||
function isChatRoomForArchive(meta, customerId) {
|
||||
const cid = String(meta?.customerId || '');
|
||||
@ -681,16 +690,24 @@ function parseAnyTimeMs(v) {
|
||||
return d.isValid() ? d.valueOf() : 0;
|
||||
}
|
||||
|
||||
async function refreshChatRoom() {
|
||||
async function refreshChatRoom(options = {}) {
|
||||
const force = Boolean(options?.force);
|
||||
const customerId = String(archiveId.value || '');
|
||||
if (!customerId) return;
|
||||
if (isRefreshingChatRoom.value) return;
|
||||
|
||||
// 如果正在刷新,复用同一个 promise,确保调用方可以 await 到结果
|
||||
if (isRefreshingChatRoom.value && refreshChatRoomPromise) {
|
||||
await refreshChatRoomPromise;
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (now - lastRefreshChatRoomAt < 5000) return;
|
||||
if (!force && now - lastRefreshChatRoomAt < 5000) return;
|
||||
lastRefreshChatRoomAt = now;
|
||||
|
||||
isRefreshingChatRoom.value = true;
|
||||
try {
|
||||
refreshChatRoomPromise = (async () => {
|
||||
try {
|
||||
const corpId = getCorpId();
|
||||
const teamId = getCurrentTeamId();
|
||||
|
||||
@ -742,11 +759,15 @@ async function refreshChatRoom() {
|
||||
chatGroupId.value = '';
|
||||
archive.value.chatGroupId = '';
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} finally {
|
||||
} finally {
|
||||
isRefreshingChatRoom.value = false;
|
||||
}
|
||||
refreshChatRoomPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
await refreshChatRoomPromise;
|
||||
}
|
||||
|
||||
const makeCall = () => {
|
||||
|
||||
@ -75,6 +75,7 @@
|
||||
<view
|
||||
v-if="i.fileList && i.fileList.length > 0"
|
||||
class="file-list"
|
||||
:class="{ 'no-send-text': !i.sendContent }"
|
||||
>
|
||||
<view
|
||||
v-for="(file, idx) in i.fileList"
|
||||
@ -218,7 +219,6 @@ import dayjs from "dayjs";
|
||||
import api from "@/utils/api";
|
||||
import useAccountStore from "@/store/account";
|
||||
import { toast } from "@/utils/widget";
|
||||
import { handleFollowUpMessages } from "@/utils/send-message-helper";
|
||||
import {
|
||||
getTodoEventTypeLabel,
|
||||
getTodoEventTypeOptions,
|
||||
@ -237,7 +237,9 @@ const { account, doctorInfo } = storeToRefs(accountStore);
|
||||
const { getDoctorInfo } = accountStore;
|
||||
|
||||
function getUserId() {
|
||||
return doctorInfo.value?.userid || "";
|
||||
const d = doctorInfo.value || {};
|
||||
const a = account.value || {};
|
||||
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || "") || "";
|
||||
}
|
||||
|
||||
function isExecutor(todo) {
|
||||
@ -248,7 +250,20 @@ function isExecutor(todo) {
|
||||
|
||||
function getCorpId() {
|
||||
const team = uni.getStorageSync("ykt_case_current_team") || {};
|
||||
return team.corpId || doctorInfo.value?.corpId || "";
|
||||
const d = doctorInfo.value || {};
|
||||
const a = account.value || {};
|
||||
return String(d.corpId || a.corpId || team.corpId || "") || "";
|
||||
}
|
||||
|
||||
function getCurrentTeamId() {
|
||||
const team = uni.getStorageSync("ykt_case_current_team") || {};
|
||||
return String(team?.teamId || team?._id || team?.id || "") || "";
|
||||
}
|
||||
|
||||
function normalizeGroupId(v) {
|
||||
const s = String(v || "").trim();
|
||||
if (!s) return "";
|
||||
return s.startsWith("GROUP") ? s.slice(5) : s;
|
||||
}
|
||||
|
||||
const statusTabs = [
|
||||
@ -278,6 +293,11 @@ const query = reactive({
|
||||
const list = ref([]);
|
||||
const total = ref(0);
|
||||
|
||||
const chatGroupId = ref("");
|
||||
const currentChatGroupId = computed(() => normalizeGroupId(chatGroupId.value || ""));
|
||||
|
||||
const PENDING_FOLLOWUP_SEND_STORAGE_KEY = "ykt_followup_pending_send";
|
||||
|
||||
const page = ref(1);
|
||||
const pageSize = 10;
|
||||
const pages = ref(1);
|
||||
@ -733,6 +753,7 @@ onMounted(() => {
|
||||
if (userId && name)
|
||||
userNameMap.value = { ...(userNameMap.value || {}), [userId]: name };
|
||||
loadTeams();
|
||||
refreshChatRoom();
|
||||
resetList();
|
||||
uni.$on("archive-detail:followup-changed", resetList);
|
||||
});
|
||||
@ -926,12 +947,19 @@ watch(
|
||||
.file-list {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.file-list.no-send-text {
|
||||
margin-top: 0;
|
||||
}
|
||||
.file-item {
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.file-item + .file-item {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.file-icon {
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
<view class="head">
|
||||
<view class="dot"></view>
|
||||
<view class="time">{{ i.timeStr }}</view>
|
||||
<view v-if="i.hasFile" class="file-link" @click.stop="viewFile(i)">
|
||||
<view v-if="showFileEntry && i.hasFile" class="file-link" @click.stop="viewFile(i)">
|
||||
{{ i.fileType === 'article' ? '查看文章' : '查看问卷' }}
|
||||
</view>
|
||||
</view>
|
||||
@ -137,6 +137,11 @@ const loading = ref(false);
|
||||
const list = ref([]);
|
||||
const expandMap = ref({});
|
||||
const userNameMap = ref({});
|
||||
const teamNameMap = ref({});
|
||||
const loadedTeamNameIds = new Set();
|
||||
|
||||
// 先隐藏“查看问卷/文章”入口(保留相关代码,后续可随时打开)
|
||||
const showFileEntry = ref(false);
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||
@ -183,6 +188,7 @@ function getExecutorId(r) {
|
||||
const row = r && typeof r === 'object' ? r : {};
|
||||
return String(
|
||||
row.executorUserId ||
|
||||
row.executorUserID ||
|
||||
row.executorId ||
|
||||
row.executor ||
|
||||
row.creatorUserId ||
|
||||
@ -194,11 +200,11 @@ function getExecutorId(r) {
|
||||
|
||||
function executorText(r) {
|
||||
const row = r && typeof r === 'object' ? r : {};
|
||||
const fromRow = normalizeName(row.executorName || row.executorUserName || row.creatorName || row.updateUserName || '');
|
||||
if (fromRow) return fromRow;
|
||||
const uid = getExecutorId(row);
|
||||
const mapped = normalizeName(resolveUserName(uid));
|
||||
return mapped || (uid ? uid : '--');
|
||||
const fromRow = normalizeName(row.executorName || row.executorUserName || row.creatorName || row.updateUserName || '');
|
||||
if (mapped && uid && mapped !== uid && (!fromRow || fromRow === uid)) return mapped;
|
||||
return fromRow || mapped || (uid ? uid : '--');
|
||||
}
|
||||
|
||||
function getExecuteTeamId(r) {
|
||||
@ -209,9 +215,86 @@ function getExecuteTeamId(r) {
|
||||
function resolveTeamName(teamId) {
|
||||
const tid = String(teamId || '') || '';
|
||||
if (!tid) return '';
|
||||
const cached = teamNameMap.value?.[tid];
|
||||
if (cached) return String(cached);
|
||||
const list = teamList.value || [];
|
||||
const hit = list.find((i) => i && i.value === tid);
|
||||
return hit?.label ? String(hit.label) : '';
|
||||
if (hit?.label) return String(hit.label);
|
||||
// 不阻塞渲染:后台补齐团队名
|
||||
void batchLoadTeamNames([tid]);
|
||||
return '';
|
||||
}
|
||||
|
||||
let teamNameBatchInflight = null; // Promise | null
|
||||
async function batchLoadTeamNames(teamIds) {
|
||||
const ids = Array.isArray(teamIds) ? teamIds.map((v) => String(v || '').trim()).filter(Boolean) : [];
|
||||
if (!ids.length) return;
|
||||
const uniq = Array.from(new Set(ids));
|
||||
|
||||
const unknown = uniq.filter((tid) => {
|
||||
if (loadedTeamNameIds.has(tid)) return false;
|
||||
const cached = teamNameMap.value?.[tid];
|
||||
if (cached) return false;
|
||||
const list = teamList.value || [];
|
||||
const hit = list.find((i) => i && i.value === tid);
|
||||
return !hit?.label;
|
||||
});
|
||||
if (!unknown.length) return;
|
||||
|
||||
if (teamNameBatchInflight) return teamNameBatchInflight;
|
||||
|
||||
const corpId = getCorpId();
|
||||
if (!corpId) return;
|
||||
unknown.forEach((tid) => loadedTeamNameIds.add(tid));
|
||||
|
||||
teamNameBatchInflight = (async () => {
|
||||
// 现成接口:getTeamById 支持 teamIds 批量查询,返回 team 列表(含 teamId/name)
|
||||
try {
|
||||
const res = await api('getTeamById', { corpId, teamIds: unknown }, false);
|
||||
if (res?.success) {
|
||||
const rows = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.data) ? res.data.data : [];
|
||||
const patch = rows.reduce((acc, t) => {
|
||||
const id = String(t?.teamId || t?.id || t?._id || '').trim();
|
||||
if (!id) return acc;
|
||||
const name = String(t?.name || t?.teamName || t?.team || '').trim();
|
||||
if (!name) return acc;
|
||||
// 只补缺,不覆盖已有映射
|
||||
const existing = teamNameMap.value?.[id];
|
||||
if (existing) return acc;
|
||||
acc[id] = name;
|
||||
return acc;
|
||||
}, {});
|
||||
if (Object.keys(patch).length) teamNameMap.value = { ...(teamNameMap.value || {}), ...patch };
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// 兜底:逐个 getTeamData(并发不高,避免卡顿)
|
||||
const limit = 4;
|
||||
let idx = 0;
|
||||
const workers = Array.from({ length: Math.min(limit, unknown.length) }, async () => {
|
||||
while (idx < unknown.length) {
|
||||
const tid = unknown[idx++];
|
||||
try {
|
||||
const res = await api('getTeamData', { corpId, teamId: tid }, false);
|
||||
if (!res?.success) continue;
|
||||
const data = res?.data && typeof res.data === 'object' ? res.data : {};
|
||||
const name = String(data?.name || data?.teamName || data?.team || '').trim();
|
||||
if (!name) continue;
|
||||
if (!teamNameMap.value?.[tid]) teamNameMap.value = { ...(teamNameMap.value || {}), [tid]: name };
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
await Promise.allSettled(workers);
|
||||
})().finally(() => {
|
||||
teamNameBatchInflight = null;
|
||||
});
|
||||
|
||||
return teamNameBatchInflight;
|
||||
}
|
||||
|
||||
function executeTeamText(r) {
|
||||
@ -264,21 +347,110 @@ async function loadTeamMembers(teamId) {
|
||||
const tid = String(teamId || '') || '';
|
||||
if (!tid) return;
|
||||
if (loadedTeamMemberIds.has(tid)) return;
|
||||
loadedTeamMemberIds.add(tid);
|
||||
await ensureDoctor();
|
||||
const corpId = getCorpId();
|
||||
if (!corpId) return;
|
||||
const res = await api('getTeamData', { corpId, teamId: tid });
|
||||
if (!res?.success) return;
|
||||
const t = res?.data && typeof res.data === 'object' ? res.data : {};
|
||||
loadedTeamMemberIds.add(tid);
|
||||
|
||||
// 以 getTeamData 为准(getTeamMemberAvatarsAndName 存在不全/不准的情况)
|
||||
const fallback = await api('getTeamData', { corpId, teamId: tid });
|
||||
if (!fallback?.success) {
|
||||
loadedTeamMemberIds.delete(tid);
|
||||
return;
|
||||
}
|
||||
const t = fallback?.data && typeof fallback.data === 'object' ? fallback.data : {};
|
||||
const members = Array.isArray(t.memberList) ? t.memberList : [];
|
||||
const map = members.reduce((acc, m) => {
|
||||
const uid = String(m?.userid || '');
|
||||
if (typeof m === 'string') {
|
||||
const id = String(m || '');
|
||||
if (id) acc[id] = id;
|
||||
return acc;
|
||||
}
|
||||
const uid = String(m?.userid || m?.userId || m?.corpUserId || m?._id || m?.id || '');
|
||||
if (!uid) return acc;
|
||||
acc[uid] = String(m?.anotherName || m?.name || m?.userid || '') || uid;
|
||||
acc[uid] = String(m?.anotherName || m?.name || m?.userid || m?.userId || '') || uid;
|
||||
return acc;
|
||||
}, {});
|
||||
userNameMap.value = { ...(userNameMap.value || {}), ...map };
|
||||
|
||||
// 补缺:仅当当前没有映射时才用 avatars 接口补齐,避免覆盖正确姓名
|
||||
try {
|
||||
const res = await api('getTeamMemberAvatarsAndName', { corpId, teamId: tid });
|
||||
if (res?.success && res?.data && typeof res.data === 'object') {
|
||||
const raw = res.data;
|
||||
const patch = Object.keys(raw).reduce((acc, uid) => {
|
||||
const id = String(uid || '');
|
||||
if (!id) return acc;
|
||||
const existing = userNameMap.value?.[id];
|
||||
if (existing && existing !== id) return acc;
|
||||
const name = String(raw?.[uid]?.name || raw?.[uid]?.anotherName || '').trim();
|
||||
if (!name || name === id) return acc;
|
||||
acc[id] = name;
|
||||
return acc;
|
||||
}, {});
|
||||
if (Object.keys(patch).length) userNameMap.value = { ...(userNameMap.value || {}), ...patch };
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
let corpMemberBatchInflight = null; // Promise | null
|
||||
async function batchLoadCorpMembers(userIds) {
|
||||
const ids = Array.isArray(userIds) ? userIds.map((v) => String(v || '').trim()).filter(Boolean) : [];
|
||||
if (!ids.length) return;
|
||||
const uniq = Array.from(new Set(ids));
|
||||
const unknown = uniq.filter((id) => {
|
||||
const existing = userNameMap.value?.[id];
|
||||
return !existing || existing === id;
|
||||
});
|
||||
if (!unknown.length) return;
|
||||
|
||||
if (corpMemberBatchInflight) return corpMemberBatchInflight;
|
||||
|
||||
await ensureDoctor();
|
||||
const corpId = getCorpId();
|
||||
if (!corpId) return;
|
||||
|
||||
corpMemberBatchInflight = (async () => {
|
||||
try {
|
||||
const res = await api(
|
||||
'getCorpMember',
|
||||
{
|
||||
page: 1,
|
||||
pageSize: Math.min(Math.max(unknown.length, 10), 500),
|
||||
params: {
|
||||
corpId,
|
||||
memberList: unknown,
|
||||
},
|
||||
},
|
||||
false
|
||||
);
|
||||
if (!res?.success) return;
|
||||
|
||||
const rows = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.data) ? res.data.data : [];
|
||||
if (!rows.length) return;
|
||||
|
||||
const patch = rows.reduce((acc, m) => {
|
||||
const id = String(m?.userid || m?.userId || m?.corpUserId || '').trim();
|
||||
if (!id) return acc;
|
||||
const existing = userNameMap.value?.[id];
|
||||
if (existing && existing !== id) return acc;
|
||||
const display = String(m?.anotherName || m?.name || '').trim();
|
||||
if (!display || display === id) return acc;
|
||||
acc[id] = display;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (Object.keys(patch).length) userNameMap.value = { ...(userNameMap.value || {}), ...patch };
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
})().finally(() => {
|
||||
corpMemberBatchInflight = null;
|
||||
});
|
||||
|
||||
return corpMemberBatchInflight;
|
||||
}
|
||||
|
||||
const moreStatus = computed(() => {
|
||||
@ -375,6 +547,13 @@ async function getMore() {
|
||||
// 尽量加载记录所属团队成员,用于执行人展示
|
||||
const teamIds = mapped.map((i) => i.executeTeamId).filter(Boolean);
|
||||
Array.from(new Set(teamIds)).forEach((tid) => loadTeamMembers(tid));
|
||||
|
||||
// 批量补齐团队名(与 userid 补齐策略一致:缓存 + 只补缺,不阻塞渲染)
|
||||
void batchLoadTeamNames(teamIds);
|
||||
|
||||
// 补齐非团队成员执行人姓名(例如其他团队创建/操作)
|
||||
const executorIds = mapped.map((i) => i.executorUserId).filter(Boolean);
|
||||
void batchLoadCorpMembers(executorIds);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@ -669,6 +848,7 @@ watch(
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
.pen {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<uni-icons type="search" size="18" color="#999" class="search-icon"></uni-icons>
|
||||
<input
|
||||
class="search-input"
|
||||
placeholder="搜索患者名称/手机号/院内ID号"
|
||||
placeholder="搜索患者名称/手机号/病案号"
|
||||
v-model="searchQuery"
|
||||
confirm-type="search"
|
||||
focus
|
||||
@ -55,7 +55,7 @@
|
||||
|
||||
<!-- History or Suggestions (when no search) -->
|
||||
<view v-else class="search-tips">
|
||||
<text class="tips-text">输入患者名称、手机号或院内ID号进行搜索</text>
|
||||
<text class="tips-text">输入患者名称、手机号或病案号进行搜索</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@ -73,6 +73,7 @@ import { toast } from '@/utils/widget';
|
||||
const searchQuery = ref('');
|
||||
const searchResultsRaw = ref([]);
|
||||
const searching = ref(false);
|
||||
const searchSeq = ref(0);
|
||||
|
||||
const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team';
|
||||
const DETAIL_STORAGE_KEY = 'ykt_case_archive_detail';
|
||||
@ -111,11 +112,29 @@ function formatPatient(raw) {
|
||||
const mobiles = asArray(raw?.mobiles).map(String).filter(Boolean);
|
||||
const mobile = raw?.mobile ? String(raw.mobile) : (mobiles[0] || '');
|
||||
|
||||
const customerNumber = raw?.customerNumber || raw?.hospitalId || '';
|
||||
const customerProfileNo2 = raw?.customerProfileNo2 || '';
|
||||
const customerProfileNo3 = raw?.customerProfileNo3 || '';
|
||||
|
||||
function normalizeRecordTypeLabel(value) {
|
||||
const s = String(value || '').trim();
|
||||
if (!s) return '';
|
||||
const lower = s.toLowerCase();
|
||||
const base = lower.endsWith('record') && s.length > 6 ? s.slice(0, -6) : s;
|
||||
const baseLower = String(base).toLowerCase();
|
||||
if (baseLower === 'outpatient' || baseLower === 'out_patient' || baseLower === 'out-patient') return '门诊记录';
|
||||
if (baseLower === 'inhospital' || baseLower === 'in_hospital' || baseLower === 'in-hospital' || baseLower === 'inpatient') return '住院记录';
|
||||
if (baseLower === 'preconsultation' || baseLower === 'pre_consultation' || baseLower === 'pre-consultation' || baseLower === 'preconsultationtemplate') return '预问诊记录';
|
||||
if (baseLower === 'physicalexaminationtemplate' || baseLower === 'physicalexamination' || baseLower === 'physical_examination') return '体检记录';
|
||||
// 如果已经是中文(或后端已给展示名),直接返回
|
||||
return s;
|
||||
}
|
||||
|
||||
// 解析病历信息
|
||||
let record = null;
|
||||
if (raw?.latestRecord && typeof raw.latestRecord === 'object') {
|
||||
const lr = raw.latestRecord;
|
||||
const type = lr.type || '';
|
||||
const type = normalizeRecordTypeLabel(lr.type || lr.medicalTypeName || lr.medicalType || '');
|
||||
const date = lr.date || '';
|
||||
const diagnosis = lr.diagnosis || '';
|
||||
if (type || date || diagnosis) {
|
||||
@ -139,10 +158,79 @@ function formatPatient(raw) {
|
||||
record,
|
||||
createTime: raw?.createTime || '',
|
||||
creator: raw?.creatorName || raw?.creator || '',
|
||||
hospitalId: raw?.customerNumber || raw?.hospitalId || '',
|
||||
hospitalId: customerNumber,
|
||||
customerNumber,
|
||||
customerProfileNo2,
|
||||
customerProfileNo3,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function normalizeDigits(value) {
|
||||
const s = normalizeText(value);
|
||||
return s.replace(/\D/g, '');
|
||||
}
|
||||
|
||||
function isChinaMobile(value) {
|
||||
const digits = normalizeDigits(value);
|
||||
return /^1[3-9]\d{9}$/.test(digits);
|
||||
}
|
||||
|
||||
function isLikelyArchiveNo(value) {
|
||||
const s = normalizeText(value);
|
||||
if (!s) return false;
|
||||
if (/[\u4e00-\u9fa5]/.test(s)) return false; // 中文一般是姓名
|
||||
if (isChinaMobile(s)) return false;
|
||||
// 病案号一般为数字/字母组合(或纯数字)
|
||||
return /^[0-9A-Za-z-]{3,}$/.test(s);
|
||||
}
|
||||
|
||||
function matchesAnyArchiveNo(patient, q) {
|
||||
const needle = normalizeText(q).toLowerCase();
|
||||
if (!needle) return false;
|
||||
const values = [
|
||||
patient?.customerNumber,
|
||||
patient?.customerProfileNo2,
|
||||
patient?.customerProfileNo3,
|
||||
patient?.hospitalId,
|
||||
].map((v) => normalizeText(v).toLowerCase()).filter(Boolean);
|
||||
|
||||
// 优先精确匹配
|
||||
if (values.some((v) => v === needle)) return true;
|
||||
// 兜底:包含匹配(部分医院病案号前后可能带前缀/后缀)
|
||||
return values.some((v) => v.includes(needle));
|
||||
}
|
||||
|
||||
function matchesAnyMobile(patient, q) {
|
||||
const needle = normalizeDigits(q);
|
||||
if (!needle) return false;
|
||||
const mobiles = [
|
||||
patient?.mobile,
|
||||
...(Array.isArray(patient?.mobiles) ? patient.mobiles : []),
|
||||
patient?.phone,
|
||||
patient?.phone1,
|
||||
].map(normalizeDigits).filter(Boolean);
|
||||
return mobiles.some((m) => m === needle);
|
||||
}
|
||||
|
||||
async function fetchList(params) {
|
||||
const res = await api('searchCorpCustomerForCaseList', params);
|
||||
if (!res?.success) {
|
||||
throw new Error(res?.message || '搜索失败');
|
||||
}
|
||||
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 : [];
|
||||
return list.map(formatPatient);
|
||||
}
|
||||
|
||||
// Computed
|
||||
const searchResults = computed(() => {
|
||||
return searchResultsRaw.value || [];
|
||||
@ -163,29 +251,58 @@ const doSearch = useDebounce(async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const seq = (searchSeq.value += 1);
|
||||
searching.value = true;
|
||||
const res = await api('searchCorpCustomerForCaseList', {
|
||||
corpId,
|
||||
userId,
|
||||
teamId,
|
||||
name: q,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
});
|
||||
searching.value = false;
|
||||
try {
|
||||
if (isChinaMobile(q)) {
|
||||
const digits = normalizeDigits(q);
|
||||
const list = await fetchList({
|
||||
corpId,
|
||||
userId,
|
||||
teamId,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
mobile: digits,
|
||||
});
|
||||
if (seq !== searchSeq.value) return;
|
||||
|
||||
if (!res?.success) {
|
||||
toast(res?.message || '搜索失败');
|
||||
return;
|
||||
// 后端为包含匹配,这里做一次精确过滤,避免“部分号段”带来误命中
|
||||
searchResultsRaw.value = list.filter((p) => matchesAnyMobile(p, digits));
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLikelyArchiveNo(q)) {
|
||||
const list = await fetchList({
|
||||
corpId,
|
||||
userId,
|
||||
teamId,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
archiveNo: q,
|
||||
});
|
||||
if (seq !== searchSeq.value) return;
|
||||
|
||||
// 兜底:后端字段不全时,仍然做一次本地匹配(包含 customerNumber/2/3)
|
||||
searchResultsRaw.value = list.filter((p) => matchesAnyArchiveNo(p, q));
|
||||
return;
|
||||
}
|
||||
|
||||
const list = await fetchList({
|
||||
corpId,
|
||||
userId,
|
||||
teamId,
|
||||
name: q,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
});
|
||||
if (seq !== searchSeq.value) return;
|
||||
searchResultsRaw.value = list;
|
||||
} catch (e) {
|
||||
if (seq !== searchSeq.value) return;
|
||||
toast(e?.message || '搜索失败');
|
||||
} finally {
|
||||
if (seq === searchSeq.value) searching.value = false;
|
||||
}
|
||||
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 : [];
|
||||
searchResultsRaw.value = list.map(formatPatient);
|
||||
}, 600);
|
||||
|
||||
const handleSearch = () => doSearch();
|
||||
|
||||
@ -130,6 +130,7 @@ 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
|
||||
@ -163,6 +164,10 @@ const groupNameMap = computed(() => {
|
||||
|
||||
// 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); // 在管档案数(所有团队)
|
||||
@ -189,6 +194,7 @@ const currentTab = computed(() => tabs.value.find((t) => t.key === currentTabKey
|
||||
const accountStore = useAccountStore();
|
||||
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||
const { getDoctorInfo } = accountStore;
|
||||
const { withInfo } = useInfoCheck();
|
||||
|
||||
const teamDisplay = computed(() => `${currentTeam.value?.name || ''}`);
|
||||
|
||||
@ -196,6 +202,272 @@ 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 || '';
|
||||
@ -235,23 +507,76 @@ function ensureUserInfoForFeature() {
|
||||
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) return;
|
||||
const t = res?.data && typeof res.data === 'object' ? res.data : {};
|
||||
const members = Array.isArray(t.memberList) ? t.memberList : [];
|
||||
// Update map
|
||||
members.forEach(m => {
|
||||
const uid = String(m?.userid || '');
|
||||
if (uid) {
|
||||
userNameMap.value[uid] = String(m?.anotherName || m?.name || m?.userid || '') || uid;
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
@ -260,7 +585,7 @@ async function loadTeamMembers() {
|
||||
function resolveCreatorName(patient) {
|
||||
const val = patient.creator;
|
||||
if (!val) return '';
|
||||
return userNameMap.value[val] || val;
|
||||
return resolveUserName(val);
|
||||
}
|
||||
|
||||
function resolveRecentAddTime(patient) {
|
||||
@ -268,9 +593,12 @@ function resolveRecentAddTime(patient) {
|
||||
}
|
||||
|
||||
function resolveRecentAddOperatorName(patient) {
|
||||
const uid = patient?.recentAddOperatorUserId || patient?.creator || '';
|
||||
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 userNameMap.value[uid] || uid;
|
||||
return resolveUserName(uid);
|
||||
}
|
||||
|
||||
function resolveRecentAddAction(patient) {
|
||||
@ -396,6 +724,205 @@ function parseCreateTime(value) {
|
||||
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 || '';
|
||||
@ -427,20 +954,16 @@ function formatPatient(raw) {
|
||||
|
||||
// 解析病历信息
|
||||
let record = null;
|
||||
if (raw?.latestRecord && typeof raw.latestRecord === 'object') {
|
||||
const lr = raw.latestRecord;
|
||||
const type = lr.type || '';
|
||||
const date = lr.date || '';
|
||||
const diagnosis = lr.diagnosis || '';
|
||||
// 只有存在有效信息时才设置 record
|
||||
if (type || date || diagnosis) {
|
||||
record = {
|
||||
type: type || '-',
|
||||
date: date || '-',
|
||||
diagnosis: diagnosis || '-'
|
||||
};
|
||||
}
|
||||
}
|
||||
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,
|
||||
@ -457,6 +980,14 @@ function formatPatient(raw) {
|
||||
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,
|
||||
@ -518,6 +1049,7 @@ async function reload(reset = true) {
|
||||
if (!currentTeam.value) return;
|
||||
if (loading.value) return;
|
||||
|
||||
await ensureDoctorForQuery();
|
||||
const userId = getUserId();
|
||||
const corpId = getCorpId();
|
||||
const teamId = getTeamId();
|
||||
@ -571,6 +1103,8 @@ async function reload(reset = true) {
|
||||
const list = Array.isArray(payload.list) ? payload.list : Array.isArray(payload.data) ? payload.data : [];
|
||||
const next = list.map(formatPatient);
|
||||
rawPatients.value = page.value === 1 ? next : [...rawPatients.value, ...next];
|
||||
// 补齐创建人/新增人姓名(部分创建人不在当前团队成员列表中)
|
||||
void prefetchUserNamesFromPatients(next).catch(() => {});
|
||||
pages.value = Number(payload.pages || 0) || 0;
|
||||
totalFromApi.value = Number(payload.total || 0) || rawPatients.value.length;
|
||||
managedArchiveCountAllTeams.value =
|
||||
@ -665,13 +1199,13 @@ const toggleTeamPopup = () => {
|
||||
}
|
||||
uni.showActionSheet({
|
||||
itemList: teams.value.map((i) => i.name),
|
||||
success: function (res) {
|
||||
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';
|
||||
loadGroups();
|
||||
loadTeamMembers();
|
||||
reload(true);
|
||||
await loadGroups();
|
||||
await loadTeamMembers();
|
||||
await reload(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -683,15 +1217,14 @@ const goToSearch = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const goToGroupManage = () => {
|
||||
const goToGroupManage = withInfo(() => {
|
||||
if (checkBatchMode()) return;
|
||||
if (!ensureUserInfoForFeature()) return;
|
||||
uni.navigateTo({
|
||||
url: '/pages/case/group-manage'
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
const toggleBatchMode = () => {
|
||||
const toggleBatchMode = withInfo(() => {
|
||||
if (isBatchMode.value) {
|
||||
// Already in batch mode, do nothing or prompt?
|
||||
// Prompt says "click Other operations... prompt please finish".
|
||||
@ -701,9 +1234,9 @@ const toggleBatchMode = () => {
|
||||
}
|
||||
isBatchMode.value = true;
|
||||
selectedItems.value = [];
|
||||
};
|
||||
});
|
||||
|
||||
const handleCreate = () => {
|
||||
const handleCreate = withInfo(() => {
|
||||
if (checkBatchMode()) return;
|
||||
const rawMax = doctorInfo.value?.maxCustomerArchive;
|
||||
const hasMaxField = rawMax !== undefined && rawMax !== null && String(rawMax).trim() !== '';
|
||||
@ -775,7 +1308,7 @@ const handleCreate = () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// 新增流程:认证分支
|
||||
const startVerifyFlow = () => {
|
||||
@ -1016,6 +1549,7 @@ watch(currentTeam, (t) => {
|
||||
|
||||
watch(currentTabKey, () => {
|
||||
if (!currentTeam.value) return;
|
||||
loadTeamMembers();
|
||||
reload(true);
|
||||
});
|
||||
|
||||
@ -1032,7 +1566,7 @@ onLoad(async () => {
|
||||
await loadTeams();
|
||||
if (currentTeam.value) {
|
||||
await loadGroups();
|
||||
loadTeamMembers();
|
||||
await loadTeamMembers();
|
||||
await reload(true);
|
||||
}
|
||||
await refreshVerifyStatus();
|
||||
@ -1056,7 +1590,7 @@ onShow(async () => {
|
||||
} else {
|
||||
await loadGroups();
|
||||
}
|
||||
loadTeamMembers();
|
||||
await loadTeamMembers();
|
||||
await refreshVerifyStatus();
|
||||
});
|
||||
|
||||
|
||||
@ -182,6 +182,7 @@ import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
||||
import { storeToRefs } from "pinia";
|
||||
import useAccountStore from "@/store/account.js";
|
||||
import { globalTimChatManager } from "@/utils/tim-chat.js";
|
||||
import { handleFollowUpMessages } from "@/utils/send-message-helper";
|
||||
import {
|
||||
startIMMonitoring,
|
||||
stopIMMonitoring,
|
||||
@ -210,6 +211,63 @@ import AIAssistantButtons from "./components/ai-assistant-buttons.vue";
|
||||
|
||||
const timChatManager = globalTimChatManager;
|
||||
|
||||
const PENDING_FOLLOWUP_SEND_STORAGE_KEY = "ykt_followup_pending_send";
|
||||
const pendingFollowUpSendConsumed = ref(false);
|
||||
const initialMessageListLoaded = ref(false);
|
||||
|
||||
function normalizeGroupId(v) {
|
||||
const s = String(v || "").trim();
|
||||
if (!s) return "";
|
||||
return s.startsWith("GROUP") ? s.slice(5) : s;
|
||||
}
|
||||
|
||||
async function tryConsumePendingFollowUpSend() {
|
||||
if (pendingFollowUpSendConsumed.value) return;
|
||||
|
||||
// 等待 IM 就绪与首屏消息加载完成,避免发送的本地消息被列表回调覆盖
|
||||
if (!timChatManager?.isLoggedIn) return;
|
||||
if (!initialMessageListLoaded.value) return;
|
||||
|
||||
const raw = uni.getStorageSync(PENDING_FOLLOWUP_SEND_STORAGE_KEY);
|
||||
const payload = raw && typeof raw === "object" ? raw : null;
|
||||
if (!payload) return;
|
||||
|
||||
const createdAt = Number(payload.createdAt || 0) || 0;
|
||||
// 过期就清理,避免误发送
|
||||
if (createdAt && Date.now() - createdAt > 5 * 60 * 1000) {
|
||||
uni.removeStorageSync(PENDING_FOLLOWUP_SEND_STORAGE_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
const payloadConversationID = String(payload.conversationID || "");
|
||||
const payloadGroupId = normalizeGroupId(payload.groupId || "");
|
||||
const currentConversationID = String(chatInfo.value.conversationID || "");
|
||||
const currentGroupId = normalizeGroupId(groupId.value || "");
|
||||
|
||||
// 必须匹配当前会话,才允许发送
|
||||
if (!payloadConversationID || payloadConversationID !== currentConversationID) return;
|
||||
if (payloadGroupId && currentGroupId && payloadGroupId !== currentGroupId) return;
|
||||
|
||||
const messages = Array.isArray(payload.messages) ? payload.messages : [];
|
||||
const context = payload.context && typeof payload.context === "object" ? payload.context : {};
|
||||
if (!messages.length) {
|
||||
uni.removeStorageSync(PENDING_FOLLOWUP_SEND_STORAGE_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
pendingFollowUpSendConsumed.value = true;
|
||||
// 先清理再发,避免页面重复初始化导致二次发送
|
||||
uni.removeStorageSync(PENDING_FOLLOWUP_SEND_STORAGE_KEY);
|
||||
|
||||
await nextTick();
|
||||
const ok = await handleFollowUpMessages(messages, context);
|
||||
if (ok) {
|
||||
uni.showToast({ title: "消息已发送", icon: "success" });
|
||||
} else {
|
||||
uni.showToast({ title: "部分发送失败", icon: "none" });
|
||||
}
|
||||
}
|
||||
|
||||
// 获取环境变量
|
||||
const env = __VITE_ENV__;
|
||||
const corpId = env.MP_CORP_ID || "";
|
||||
@ -527,15 +585,45 @@ const initTIMCallbacks = async () => {
|
||||
}
|
||||
});
|
||||
|
||||
messageList.value = uniqueMessages;
|
||||
// 合并现有 messageList,避免首屏加载覆盖刚发送的本地消息
|
||||
const merged = [];
|
||||
const mergedSeen = new Set();
|
||||
const existing = Array.isArray(messageList.value) ? messageList.value : [];
|
||||
|
||||
for (const m of existing) {
|
||||
if (!m || !m.ID) continue;
|
||||
if (m.conversationID !== chatInfo.value.conversationID) continue;
|
||||
if (mergedSeen.has(m.ID)) continue;
|
||||
mergedSeen.add(m.ID);
|
||||
merged.push(m);
|
||||
}
|
||||
for (const m of uniqueMessages) {
|
||||
if (!m || !m.ID) continue;
|
||||
if (mergedSeen.has(m.ID)) continue;
|
||||
mergedSeen.add(m.ID);
|
||||
merged.push(m);
|
||||
}
|
||||
merged.sort((a, b) => {
|
||||
const ta = Number(a?.lastTime || a?.time || 0) || 0;
|
||||
const tb = Number(b?.lastTime || b?.time || 0) || 0;
|
||||
return ta - tb;
|
||||
});
|
||||
|
||||
messageList.value = merged;
|
||||
console.log(
|
||||
"消息列表已更新,原始",
|
||||
messages.length,
|
||||
"条,过滤后",
|
||||
uniqueMessages.length,
|
||||
messageList.value.length,
|
||||
"条消息"
|
||||
);
|
||||
|
||||
if (!data.isPullUp && !data.isRefresh) {
|
||||
initialMessageListLoaded.value = true;
|
||||
// 首屏加载完成后再尝试消费待发送 payload
|
||||
tryConsumePendingFollowUpSend();
|
||||
}
|
||||
|
||||
isCompleted.value = data.isCompleted || false;
|
||||
isLoadingMore.value = false;
|
||||
|
||||
@ -620,6 +708,9 @@ const loadMessageList = async () => {
|
||||
|
||||
timChatManager.enterConversation(chatInfo.value.conversationID || "test1");
|
||||
|
||||
// 若从病历回访记录带入待发送内容,则进入会话后自动发送
|
||||
tryConsumePendingFollowUpSend();
|
||||
|
||||
// 标记会话为已读 - 确保清空未读数
|
||||
if (
|
||||
timChatManager.tim &&
|
||||
|
||||
@ -3,11 +3,14 @@ import request from "./http";
|
||||
const urlsConfig = {
|
||||
corp: {
|
||||
getCorpMemberHomepageInfo: 'getCorpMemberHomepageInfo',
|
||||
getCorpMember: 'getCorpMember',
|
||||
getCorpMemberOptions: 'getCorpMemberOptions',
|
||||
// 企业信息/标签
|
||||
getCorpInfo: 'getCorpInfo',
|
||||
getCorpTags: 'getCorpTags',
|
||||
getTeamBaseInfo: 'getTeamBaseInfo',
|
||||
getTeamData: 'getTeamData',
|
||||
getTeamById: 'getTeamById',
|
||||
getTeamBymember: 'getTeamBymember',
|
||||
getCurrentTemplate: 'getCurrentTemplate',
|
||||
getTemplateGroup: 'getTemplateGroup',
|
||||
|
||||
@ -6,6 +6,38 @@ import { globalTimChatManager } from './tim-chat.js';
|
||||
import api from './api.js';
|
||||
import { toast } from './widget.js';
|
||||
const env = __VITE_ENV__;
|
||||
|
||||
function nowTs() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
async function tryAddServiceRecord(payload) {
|
||||
try {
|
||||
const res = await api('addServiceRecord', payload);
|
||||
if (!res?.success) {
|
||||
console.warn('写入服务记录失败:', res?.message || res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('写入服务记录异常:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function canWriteServiceRecord(options = {}) {
|
||||
return Boolean(options?.corpId && options?.userId && options?.customerId);
|
||||
}
|
||||
|
||||
function normalizeServiceRecordBase(options = {}) {
|
||||
return {
|
||||
corpId: options.corpId,
|
||||
executorUserId: String(options.userId || ''),
|
||||
creatorUserId: String(options.userId || ''),
|
||||
customerId: String(options.customerId || ''),
|
||||
customerName: String(options.customerName || ''),
|
||||
executeTeamId: String(options.teamId || options.executeTeamId || ''),
|
||||
executionTime: nowTs(),
|
||||
externalUserId: String(options.externalUserId || options.customerUserId || ''),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 发送文字消息
|
||||
* @param {string} content - 文字内容
|
||||
@ -168,6 +200,24 @@ export async function sendArticleMessage(article, options = {}) {
|
||||
console.error('记录文章发送失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
// 写入服务记录留痕(异步,不阻塞)
|
||||
if (canWriteServiceRecord(options)) {
|
||||
const base = normalizeServiceRecordBase(options);
|
||||
tryAddServiceRecord({
|
||||
...base,
|
||||
eventType: 'ContentReminder',
|
||||
taskContent: `推送文章:${article.title || '宣教文章'}`,
|
||||
pannedEventSendFile: {
|
||||
type: 'article',
|
||||
articleId: article._id || options.articleId || '',
|
||||
title: article.title || '',
|
||||
url: article.url || '',
|
||||
},
|
||||
pannedEventName: '宣教发送',
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
toast(result?.error || '发送文章消息失败');
|
||||
@ -243,6 +293,22 @@ export async function sendSurveyMessage(survey, options = {}) {
|
||||
const result = await globalTimChatManager.sendCustomMessage(customMessageData);
|
||||
|
||||
if (result?.success) {
|
||||
// 写入服务记录留痕(异步,不阻塞)
|
||||
if (canWriteServiceRecord(options)) {
|
||||
const base = normalizeServiceRecordBase(options);
|
||||
tryAddServiceRecord({
|
||||
...base,
|
||||
eventType: 'questionnaire',
|
||||
taskContent: `推送问卷:${survey.name || '问卷'}`,
|
||||
pannedEventSendFile: {
|
||||
type: 'questionnaire',
|
||||
surveryId: survey._id || survey.surveryId || '',
|
||||
name: survey.name || '',
|
||||
url: surveyLink || '',
|
||||
},
|
||||
pannedEventName: '问卷调查',
|
||||
});
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
toast(result?.error || '发送问卷消息失败');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user