Compare commits

..

4 Commits

13 changed files with 495 additions and 1469 deletions

View File

@ -21,7 +21,7 @@
{ {
"path": "pages/home/case-home", "path": "pages/home/case-home",
"style": { "style": {
"navigationBarTitleText": "病" "navigationBarTitleText": "病"
} }
}, },
{ {
@ -190,12 +190,6 @@
"navigationBarTitleText": "共享客户" "navigationBarTitleText": "共享客户"
} }
}, },
{
"path": "patient-invite",
"style": {
"navigationBarTitleText": "邀请患者"
}
},
{ {
"path": "patient-create", "path": "patient-create",
"style": { "style": {
@ -311,7 +305,7 @@
"pagePath": "pages/home/case-home", "pagePath": "pages/home/case-home",
"iconPath": "static/tabbar/cart.png", "iconPath": "static/tabbar/cart.png",
"selectedIconPath": "static/tabbar/cart_selected.png", "selectedIconPath": "static/tabbar/cart_selected.png",
"text": "病" "text": "病"
}, },
{ {
"pagePath": "pages/home/work-home", "pagePath": "pages/home/work-home",

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,32 @@
<template> <template>
<view class="manage-container"> <view class="manage-container">
<view class="group-list"> <view class="group-list">
<view v-for="(item, index) in groups" :key="item._id" class="group-item"> <scroll-view scroll-y class="sort-scroll">
<view class="left-action" :class="{ disabled: Boolean(item.parentGroupId) || isSort }" @click="handleDelete(item, index)"> <movable-area class="drag-area" :style="{ height: dragAreaHeight + 'px' }">
<uni-icons type="minus-filled" size="24" :color="Boolean(item.parentGroupId) || isSort ? '#ddd' : '#ff4d4f'"></uni-icons> <movable-view
v-for="(item, index) in groups"
:key="item._id"
class="drag-item"
direction="vertical"
:disabled="dragEnabledId !== String(item._id)"
:y="Number(item._y || 0)"
:animation="draggingId !== String(item._id)"
@change="(e) => onDragChange(e, item)"
@touchend="() => onTouchEnd(item)"
@touchcancel="() => onTouchEnd(item)"
>
<view class="group-item" :class="{ 'is-dragging': draggingId === String(item._id) }">
<view
class="left-action"
:class="{ disabled: Boolean(item.parentGroupId) || Boolean(dragEnabledId) }"
@click.stop="handleDelete(item, index)"
@longpress.stop
>
<uni-icons
type="minus-filled"
size="24"
:color="Boolean(item.parentGroupId) || Boolean(dragEnabledId) ? '#ddd' : '#ff4d4f'"
></uni-icons>
</view> </view>
<view class="group-name"> <view class="group-name">
<view class="name-row"> <view class="name-row">
@ -13,23 +36,32 @@
<text v-if="item.description" class="desc">{{ item.description }}</text> <text v-if="item.description" class="desc">{{ item.description }}</text>
</view> </view>
<view class="right-actions"> <view class="right-actions">
<uni-icons type="compose" size="24" :color="isSort ? '#ddd' : '#5d8aff'" class="icon-edit" @click="handleEdit(item, index)"></uni-icons> <uni-icons
<template v-if="isSort"> type="compose"
<uni-icons type="arrowup" size="20" color="#5d8aff" class="icon-sort" @click="moveUp(index)"></uni-icons> size="24"
<uni-icons type="arrowdown" size="20" color="#5d8aff" class="icon-sort" @click="moveDown(index)"></uni-icons> :color="Boolean(dragEnabledId) ? '#ddd' : '#5d8aff'"
</template> class="icon-edit"
<uni-icons v-else type="bars" size="24" color="#5d8aff" class="icon-drag" @click="enterSort"></uni-icons> @click.stop="handleEdit(item, index)"
@longpress.stop
></uni-icons>
<view
class="drag-handle"
@touchstart="() => startHoldToDrag(item)"
@touchend="() => cancelHoldToDrag()"
@touchcancel="() => cancelHoldToDrag()"
>
<uni-icons type="bars" size="24" color="#5d8aff" class="icon-drag"></uni-icons>
</view> </view>
</view> </view>
</view> </view>
</movable-view>
</movable-area>
</scroll-view>
</view>
<!-- Bottom Button --> <!-- Bottom Button -->
<view class="footer"> <view class="footer">
<template v-if="isSort"> <button class="add-btn" :disabled="Boolean(dragEnabledId)" @click="handleAdd">添加新分组</button>
<button class="add-btn plain" @click="cancelSort">取消</button>
<button class="add-btn" @click="saveSort">保存</button>
</template>
<button v-else class="add-btn" @click="handleAdd">添加新分组</button>
</view> </view>
<!-- Dialog --> <!-- Dialog -->
@ -49,7 +81,7 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { ref, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app'; import { onLoad } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import api from '@/utils/api'; import api from '@/utils/api';
@ -59,7 +91,15 @@ import { hideLoading, loading, toast } from '@/utils/widget';
// State // State
const groups = ref([]); const groups = ref([]);
const originalGroups = ref([]); const originalGroups = ref([]);
const isSort = ref(false);
const ITEM_HEIGHT = 74; // px
const draggingId = ref('');
const dragEnabledId = ref('');
const savingSort = ref(false);
const lastSavedOrderKey = ref('');
const holdTimer = ref(null);
const holdCandidateId = ref('');
const dragAreaHeight = computed(() => (groups.value.length || 0) * ITEM_HEIGHT);
const GROUPS_RELOAD_KEY = 'ykt_case_groups_need_reload'; const GROUPS_RELOAD_KEY = 'ykt_case_groups_need_reload';
const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team'; const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team';
@ -126,8 +166,9 @@ async function loadGroups() {
} }
const list = Array.isArray(res.data) ? res.data : Array.isArray(res.data?.data) ? res.data.data : []; const list = Array.isArray(res.data) ? res.data : Array.isArray(res.data?.data) ? res.data.data : [];
const sorted = sortGroupList(list); const sorted = sortGroupList(list);
groups.value = sorted.map((i) => ({ ...i })); groups.value = sorted.map((i, idx) => ({ ...i, _y: idx * ITEM_HEIGHT }));
originalGroups.value = sorted.map((i) => ({ ...i })); originalGroups.value = sorted.map((i, idx) => ({ ...i, _y: idx * ITEM_HEIGHT }));
lastSavedOrderKey.value = getOrderKey(groups.value);
} catch (e) { } catch (e) {
toast('获取分组失败'); toast('获取分组失败');
} finally { } finally {
@ -144,7 +185,7 @@ const dialogTitle = ref('添加新分组');
// Methods // Methods
const handleAdd = () => { const handleAdd = () => {
if (isSort.value) return; if (dragEnabledId.value) return;
dialogMode.value = 'add'; dialogMode.value = 'add';
dialogTitle.value = '添加新分组'; dialogTitle.value = '添加新分组';
inputValue.value = ''; inputValue.value = '';
@ -152,7 +193,7 @@ const handleAdd = () => {
}; };
const handleEdit = (item, index) => { const handleEdit = (item, index) => {
if (isSort.value) return; if (dragEnabledId.value) return;
dialogMode.value = 'edit'; dialogMode.value = 'edit';
dialogTitle.value = '编辑分组名称'; dialogTitle.value = '编辑分组名称';
inputValue.value = item.groupName || ''; inputValue.value = item.groupName || '';
@ -161,7 +202,7 @@ const handleEdit = (item, index) => {
}; };
const handleDelete = (item, index) => { const handleDelete = (item, index) => {
if (isSort.value) return; if (dragEnabledId.value) return;
if (item?.parentGroupId) return; if (item?.parentGroupId) return;
uni.showModal({ uni.showModal({
title: '提示', title: '提示',
@ -243,56 +284,114 @@ const handleSave = async () => {
} }
}; };
function enterSort() { function initDragPositions() {
if (!groups.value.length) return; groups.value = groups.value.map((i, idx) => ({ ...i, _y: idx * ITEM_HEIGHT }));
isSort.value = true;
} }
function cancelSort() { function getOrderKey(list) {
isSort.value = false; return (Array.isArray(list) ? list : []).map((i) => String(i?._id || '')).join('|');
groups.value = originalGroups.value.map((i) => ({ ...i }));
} }
function moveUp(index) { function clamp(n, min, max) {
if (!isSort.value) return; return Math.max(min, Math.min(max, n));
if (index <= 0) return;
const next = groups.value.slice();
const tmp = next[index - 1];
next[index - 1] = next[index];
next[index] = tmp;
groups.value = next;
} }
function moveDown(index) { function findIndexById(id) {
if (!isSort.value) return; const sid = String(id || '');
if (index >= groups.value.length - 1) return; return groups.value.findIndex((g) => String(g?._id || '') === sid);
const next = groups.value.slice();
const tmp = next[index + 1];
next[index + 1] = next[index];
next[index] = tmp;
groups.value = next;
} }
async function saveSort() { function setYByIndex(activeId, activeY) {
const sid = String(activeId || '');
groups.value.forEach((g, idx) => {
if (sid && String(g?._id || '') === sid && typeof activeY === 'number') g._y = activeY;
else g._y = idx * ITEM_HEIGHT;
});
}
function clearHoldTimer() {
if (holdTimer.value) {
clearTimeout(holdTimer.value);
holdTimer.value = null;
}
holdCandidateId.value = '';
}
async function onTouchEnd(item) {
const id = String(item?._id || '');
clearHoldTimer();
if (!id) return;
//
if (dragEnabledId.value === id) {
draggingId.value = '';
setYByIndex('', null);
dragEnabledId.value = '';
await maybeAutoSaveSort();
}
}
function startHoldToDrag(item) {
const id = String(item?._id || '');
if (!id) return;
if (savingSort.value) return;
if (dragEnabledId.value) return;
clearHoldTimer();
holdCandidateId.value = id;
holdTimer.value = setTimeout(() => {
if (holdCandidateId.value !== id) return;
initDragPositions();
dragEnabledId.value = id;
draggingId.value = id;
}, 350);
}
function cancelHoldToDrag() {
clearHoldTimer();
}
async function maybeAutoSaveSort() {
if (savingSort.value) return;
const key = getOrderKey(groups.value);
if (!key || key === lastSavedOrderKey.value) return;
savingSort.value = true;
try {
const teamId = getTeamId(); const teamId = getTeamId();
if (!teamId) return; if (!teamId) return;
loading('');
try {
const data = groups.value.map((i, idx) => ({ _id: i._id, sortOrder: idx })); const data = groups.value.map((i, idx) => ({ _id: i._id, sortOrder: idx }));
const res = await api('sortGroups', { teamId, data }); const res = await api('sortGroups', { teamId, data });
if (!res?.success) { if (!res?.success) {
toast(res?.message || '保存失败'); toast(res?.message || '保存失败');
return; return;
} }
toast('保存成功'); lastSavedOrderKey.value = key;
originalGroups.value = groups.value.map((i, idx) => ({ ...i, _y: idx * ITEM_HEIGHT }));
uni.setStorageSync(GROUPS_RELOAD_KEY, 1); uni.setStorageSync(GROUPS_RELOAD_KEY, 1);
isSort.value = false; toast('已保存');
await loadGroups();
} finally { } finally {
hideLoading(); savingSort.value = false;
} }
} }
function onDragChange(e, item) {
const detail = e?.detail || {};
if (detail.source && detail.source !== 'touch') return;
const id = String(item?._id || '');
if (!id) return;
if (dragEnabledId.value !== id) return;
const from = findIndexById(id);
if (from < 0) return;
const y = Number(detail.y || 0);
const len = groups.value.length;
const to = clamp(Math.round(y / ITEM_HEIGHT), 0, Math.max(0, len - 1));
if (to !== from) {
const moved = groups.value.splice(from, 1)[0];
groups.value.splice(to, 0, moved);
}
setYByIndex(id, y);
}
onLoad(() => { onLoad(() => {
loadGroups(); loadGroups();
}); });
@ -309,12 +408,29 @@ onLoad(() => {
padding: 0 15px; padding: 0 15px;
} }
.sort-scroll {
max-height: calc(100vh - 120px);
}
.drag-area {
width: 100%;
}
.drag-item {
width: 100%;
height: 74px;
}
.group-item { .group-item {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 15px 0; padding: 15px 0;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
&.is-dragging {
background: #f7fbff;
}
.left-action { .left-action {
margin-right: 15px; margin-right: 15px;
&.disabled { &.disabled {
@ -368,20 +484,29 @@ onLoad(() => {
background-color: #fff; background-color: #fff;
padding: 15px 20px 30px; // Safe area padding padding: 15px 20px 30px; // Safe area padding
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
justify-content: center;
.add-btn { .add-btn {
background-color: #0877F1; background-color: #0877F1;
color: #fff; color: #fff;
border-radius: 4px; border-radius: 8px;
font-size: 16px; font-size: 16px;
height: 44px; height: 44px;
line-height: 44px; line-height: 44px;
border: none; border: none;
width: 100%;
max-width: 520px;
&::after { &::after {
border: none; border: none;
} }
&:disabled {
opacity: 0.6;
}
&.plain { &.plain {
background-color: #fff; background-color: #fff;
color: #666; color: #666;
@ -390,6 +515,10 @@ onLoad(() => {
} }
} }
.drag-handle {
padding: 5px;
}
// Custom Dialog Styles // Custom Dialog Styles
.dialog-mask { .dialog-mask {
position: fixed; position: fixed;

View File

@ -46,6 +46,33 @@
<view class="counter">{{ (form.taskContent || '').length }}/200</view> <view class="counter">{{ (form.taskContent || '').length }}/200</view>
</view> </view>
</view> </view>
<view class="block">
<view class="block-title">向患者发送</view>
<view class="textarea-box">
<textarea v-model="form.sendContent" class="textarea" placeholder="请输入要发送给患者的内容" maxlength="200" />
<view class="counter">{{ (form.sendContent || '').length }}/200</view>
</view>
</view>
<view class="row clickable" @click="chooseFile">
<view class="left">
<view class="label">添加附件</view>
</view>
<view class="right">
<uni-icons type="plusempty" size="16" color="#0877F1" />
</view>
</view>
<view v-if="showFileList.length" class="file-list">
<view v-for="(i, index) in showFileList" :key="String(i._k || index)" class="file-item">
<view class="file-main">
<view v-if="i.typeStr" class="file-type">{{ i.typeStr }}</view>
<view class="file-name">{{ i.fileName }}</view>
</view>
<uni-icons type="closeempty" size="18" color="#999" @click="removeFile(index)" />
</view>
</view>
</view> </view>
<view class="footer"> <view class="footer">
@ -106,6 +133,7 @@ import api from '@/utils/api';
import useAccountStore from '@/store/account'; import useAccountStore from '@/store/account';
import { toast } from '@/utils/widget'; import { toast } from '@/utils/widget';
import { getTodoEventTypeLabel, getTodoEventTypeOptions } from '@/utils/todo-const'; import { getTodoEventTypeLabel, getTodoEventTypeOptions } from '@/utils/todo-const';
import { chooseAndUploadImage } from '@/utils/file';
const archiveId = ref(''); const archiveId = ref('');
const archiveName = ref(''); const archiveName = ref('');
@ -127,6 +155,8 @@ const form = reactive({
executorName: '', // anotherName executorName: '', // anotherName
eventType: '', eventType: '',
taskContent: '', taskContent: '',
sendContent: '',
fileList: [],
}); });
const eventTypeLabel = computed(() => getTodoEventTypeLabel(form.eventType)); const eventTypeLabel = computed(() => getTodoEventTypeLabel(form.eventType));
@ -165,6 +195,8 @@ function resetForm(keepCustomer = false) {
form.executorName = ''; form.executorName = '';
form.eventType = ''; form.eventType = '';
form.taskContent = ''; form.taskContent = '';
form.sendContent = '';
form.fileList = [];
if (!keepCustomer) { if (!keepCustomer) {
archiveId.value = ''; archiveId.value = '';
archiveName.value = ''; archiveName.value = '';
@ -174,6 +206,35 @@ function resetForm(keepCustomer = false) {
uni.setStorageSync('select-mamagement-plan', ''); uni.setStorageSync('select-mamagement-plan', '');
} }
const QUESTIONNAIRE_ICON =
'https://796f-youcan-clouddev-1-8ewcqf31dbb2b5-1317294507.tcb.qcloud.la/other/19-%E9%97%AE%E5%8D%B7.png?sign=55a4cd77c418b2c548b65792a2cf6bce&t=1701328694';
const ARTICLE_ICON =
'https://796f-youcan-clouddev-1-8ewcqf31dbb2b5-1317294507.tcb.qcloud.la/other/18-%E5%AE%A3%E6%95%99.png?sign=26f221d14fd57a2ff0a106dfb01a5e7a&t=1701328694';
const showFileList = computed(() =>
(Array.isArray(form.fileList) ? form.fileList : []).map((i, idx) => {
const type = i?.type;
const fileType = i?.file && typeof i.file.type === 'string' ? i.file.type : '';
let typeStr = '';
if (type === 'image' || fileType.includes('image')) typeStr = '【图片】';
else if (type === 'video' || fileType.includes('video')) typeStr = '【视频】';
else if (fileType === 'article') typeStr = '【文章】';
else if (fileType === 'questionnaire') typeStr = '【问卷】';
else if (type === 'link') typeStr = '【链接】';
const fileName =
String(i?.file?.name || i?.file?.title || i?.name || i?.URL || '').trim() ||
`附件${idx + 1}`;
return {
...i,
_k: `${idx}_${String(i?.type || '')}_${String(i?.URL || '')}`,
typeStr,
fileName,
};
})
);
async function ensureDoctor() { async function ensureDoctor() {
if (doctorInfo.value) return; if (doctorInfo.value) return;
if (!account.value?.openid) return; if (!account.value?.openid) return;
@ -272,6 +333,9 @@ async function save() {
const customerName = String(customer.name || archiveName.value || ''); const customerName = String(customer.name || archiveName.value || '');
const customerUserId = String(customer.externalUserId || customer.customerUserId || '') || ''; const customerUserId = String(customer.externalUserId || customer.customerUserId || '') || '';
const enableSend = !!(String(form.sendContent || '').trim() || (Array.isArray(form.fileList) && form.fileList.length));
const fileList = Array.isArray(form.fileList) ? form.fileList : [];
const params = { const params = {
corpId, corpId,
customerId, customerId,
@ -283,15 +347,15 @@ async function save() {
userId: form.executorUserId || userId, userId: form.executorUserId || userId,
taskList: [ taskList: [
{ {
enableSend: false, enableSend,
eventType: form.eventType, eventType: form.eventType,
executeMethod: 'todo', executeMethod: 'todo',
executorUserId: form.executorUserId || userId, executorUserId: form.executorUserId || userId,
planExecutionTime: form.planExecutionTime, planExecutionTime: form.planExecutionTime,
sendContent: '', sendContent: String(form.sendContent || ''),
taskContent: form.taskContent, taskContent: form.taskContent,
taskId: `${Date.now()}_${Math.random().toString(16).slice(2)}`, taskId: `${Date.now()}_${Math.random().toString(16).slice(2)}`,
fileList: [], fileList,
}, },
], ],
}; };
@ -324,6 +388,81 @@ function pickExecutor(m) {
function closeExecutorPicker() { function closeExecutorPicker() {
executorPopup.value?.close?.(); executorPopup.value?.close?.();
} }
function removeFile(index) {
if (!Array.isArray(form.fileList)) form.fileList = [];
form.fileList.splice(index, 1);
}
function chooseFile() {
uni.showActionSheet({
itemList: ['图片', '文章', '问卷'],
success: ({ tapIndex }) => {
if (tapIndex === 0) chooseImage();
else if (tapIndex === 1) chooseArticle();
else if (tapIndex === 2) chooseQuestionnaire();
},
});
}
async function chooseImage() {
const url = await chooseAndUploadImage({ count: 1 });
if (!url) return;
form.fileList.push({
type: 'image',
URL: url,
file: {
type: 'image',
name: `图片_${dayjs().format('MMDD_HHmmss')}.jpg`,
},
});
}
function chooseArticle() {
const eventName = `on-select-article_${Date.now()}`;
uni.navigateTo({
url: `/pages/message/article-list?select=1&eventName=${eventName}`,
});
uni.$once(eventName, (data) => {
const corpId = getCorpId();
const articleId = String(data?._id || data?.id || '');
if (!articleId) return;
const url = `${__VITE_ENV__?.MP_PATIENT_PAGE_BASE_URL || ''}pages/article/index?id=${articleId}&corpId=${corpId}`;
form.fileList.push({
type: 'link',
URL: String(data?.cover || data?.imgUrl || '') || ARTICLE_ICON,
file: {
type: 'article',
name: String(data?.title || '宣教文章'),
subtitle: String(data?.summary || data?.desc || ''),
url,
},
});
});
}
function chooseQuestionnaire() {
const eventName = `on-select-survey_${Date.now()}`;
uni.navigateTo({
url: `/pages/message/survey-list?select=1&eventName=${eventName}&patientId=${archiveId.value}&customerName=${archiveName.value || ''}`,
});
uni.$once(eventName, (data) => {
const corpId = getCorpId();
const surveryId = String(data?._id || data?.surveryId || '');
if (!surveryId) return;
const url = `${__VITE_ENV__?.MP_PATIENT_PAGE_BASE_URL || ''}pages/survery/fill?corpId=${corpId}&surveryId=${surveryId}`;
form.fileList.push({
type: 'link',
URL: QUESTIONNAIRE_ICON,
file: {
type: 'questionnaire',
name: String(data?.name || '问卷'),
surveryId,
url,
},
});
});
}
</script> </script>
<style scoped> <style scoped>
@ -440,6 +579,39 @@ function closeExecutorPicker() {
font-size: 12px; font-size: 12px;
color: #999; color: #999;
} }
.file-list {
padding: 0 14px 14px;
}
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-top: 10px;
padding: 12px 12px;
background: #f5f6f8;
border-radius: 8px;
}
.file-main {
display: flex;
align-items: center;
min-width: 0;
gap: 8px;
}
.file-type {
flex-shrink: 0;
font-size: 12px;
color: #666;
}
.file-name {
font-size: 14px;
color: #333;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 240px;
}
.toggle-row { .toggle-row {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -1,225 +0,0 @@
<template>
<view class="page">
<view class="content">
<view class="code-row">
<text class="code-text">{{ displayTeamCode }}</text>
<view class="help" @click="showHelp">
<uni-icons type="help-filled" size="18" color="#0877F1"></uni-icons>
</view>
</view>
<view class="qr-card">
<image class="qr-image" :src="qrImageUrl" mode="aspectFit" @click="previewQr" />
</view>
<view class="tips">
<text class="tips-title">微信扫一扫上面的二维码</text>
<text class="tips-sub">进入团队首页即可发起线上咨询建档授权等服务</text>
</view>
</view>
<view class="footer">
<button class="footer-btn outline" @click="saveQr">保存图片</button>
<!-- #ifdef MP-WEIXIN -->
<button class="footer-btn primary" open-type="share">分享到微信</button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<button class="footer-btn primary" @click="shareFallback">分享到微信</button>
<!-- #endif -->
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import { onLoad, onShareAppMessage } from '@dcloudio/uni-app';
const teamCode = ref('133****3356');
const qrImageUrl = ref('');
const displayTeamCode = computed(() => `${teamCode.value}服务团队码`);
const buildQrUrl = (text) => {
// UI
const encoded = encodeURIComponent(text);
return `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encoded}`;
};
onLoad((query) => {
if (query?.teamCode) teamCode.value = String(query.teamCode);
if (query?.qrUrl) {
qrImageUrl.value = String(query.qrUrl);
} else {
qrImageUrl.value = buildQrUrl(`TEAM_CODE:${teamCode.value}`);
}
});
const showHelp = () => {
uni.showModal({
title: '服务团队码',
content: '患者微信扫一扫二维码进入团队首页,可发起线上咨询、建档授权等服务。',
showCancel: false
});
};
const previewQr = () => {
if (!qrImageUrl.value) return;
uni.previewImage({ urls: [qrImageUrl.value] });
};
const saveQr = () => {
if (!qrImageUrl.value) {
uni.showToast({ title: '二维码生成中,请稍后', icon: 'none' });
return;
}
uni.showLoading({ title: '保存中...' });
uni.downloadFile({
url: qrImageUrl.value,
success: (res) => {
const filePath = res.tempFilePath;
uni.saveImageToPhotosAlbum({
filePath,
success: () => {
uni.hideLoading();
uni.showToast({ title: '已保存到相册', icon: 'success' });
},
fail: () => {
uni.hideLoading();
uni.showModal({
title: '保存失败',
content: '请在系统设置中允许保存到相册后重试。',
confirmText: '去设置',
cancelText: '取消',
success: (r) => {
if (r.confirm) {
uni.openSetting?.({});
}
}
});
}
});
},
fail: () => {
uni.hideLoading();
uni.showToast({ title: '下载二维码失败', icon: 'none' });
}
});
};
const shareFallback = () => {
uni.showToast({ title: '请使用右上角菜单分享', icon: 'none' });
};
onShareAppMessage(() => {
return {
title: '邀请您加入服务团队',
path: `/pages/case/patient-invite?teamCode=${encodeURIComponent(teamCode.value)}`
};
});
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: #ffffff;
padding-bottom: calc(76px + env(safe-area-inset-bottom));
}
.content {
padding: 22px 16px 0;
}
.code-row {
display: flex;
align-items: center;
justify-content: center;
margin-top: 8px;
}
.code-text {
font-size: 16px;
color: #333;
font-weight: 600;
}
.help {
margin-left: 8px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.qr-card {
margin: 18px auto 0;
width: 280px;
height: 280px;
display: flex;
align-items: center;
justify-content: center;
}
.qr-image {
width: 260px;
height: 260px;
}
.tips {
margin-top: 16px;
text-align: center;
padding: 0 16px;
}
.tips-title {
display: block;
font-size: 14px;
color: #3a3a3a;
margin-bottom: 6px;
}
.tips-sub {
display: block;
font-size: 12px;
color: #999;
line-height: 18px;
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
display: flex;
gap: 12px;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
}
.footer-btn {
flex: 1;
height: 44px;
line-height: 44px;
border-radius: 6px;
font-size: 15px;
}
.footer-btn::after {
border: none;
}
.footer-btn.outline {
background: #fff;
color: #0877F1;
border: 1px solid #0877F1;
}
.footer-btn.primary {
background: #0877F1;
color: #fff;
}
</style>

View File

@ -75,7 +75,7 @@
<text class="patient-meta">{{ patient.gender }}/{{ patient.age }}</text> <text class="patient-meta">{{ patient.gender }}/{{ patient.age }}</text>
</view> </view>
<view class="patient-tags"> <view class="patient-tags">
<view v-for="(tag, tIndex) in patient.tags" :key="tIndex" class="tag"> <view v-for="(tag, tIndex) in resolveGroupTags(patient)" :key="tIndex" class="tag">
{{ tag }} {{ tag }}
</view> </view>
</view> </view>
@ -163,6 +163,16 @@ const tabs = computed(() => {
const isBatchMode = ref(false); const isBatchMode = ref(false);
const selectedItems = ref([]); // Stores patient phone or unique ID const selectedItems = ref([]); // Stores patient phone or unique ID
const groupNameMap = computed(() => {
const map = new Map();
(Array.isArray(teamGroups.value) ? teamGroups.value : []).forEach((g) => {
const id = g && g._id ? String(g._id) : '';
const name = g && g.groupName ? String(g.groupName) : '';
if (id && name) map.set(id, name);
});
return map;
});
// Team Members Map // Team Members Map
const userNameMap = ref({}); const userNameMap = ref({});
@ -329,6 +339,20 @@ function getSelectId(patient) {
return patient?._id || patient?.id || patient?.phone || patient?.mobile || ''; return patient?._id || patient?.id || patient?.phone || patient?.mobile || '';
} }
function getPatientGroupIds(patient) {
const raw = patient?.groupIds ?? patient?.groupIdList ?? patient?.groupId ?? patient?.groups;
if (Array.isArray(raw)) return raw.map(String).filter(Boolean);
if (typeof raw === 'string' || typeof raw === 'number') return [String(raw)].filter(Boolean);
return [];
}
function resolveGroupTags(patient) {
const ids = getPatientGroupIds(patient);
if (!ids.length) return [];
const map = groupNameMap.value;
return ids.map((id) => map.get(String(id))).filter(Boolean);
}
function parseCreateTime(value) { function parseCreateTime(value) {
if (!value) return null; if (!value) return null;
if (typeof value === 'number') return dayjs(value); if (typeof value === 'number') return dayjs(value);
@ -354,6 +378,7 @@ function formatPatient(raw) {
const createTime = parseCreateTime(raw?.createTime); const createTime = parseCreateTime(raw?.createTime);
const createTimeStr = createTime ? createTime.format('YYYY-MM-DD HH:mm') : ''; const createTimeStr = createTime ? createTime.format('YYYY-MM-DD HH:mm') : '';
const createTimeTs = createTime ? createTime.valueOf() : 0;
// 使 tagNames // 使 tagNames
const rawTagNames = asArray(raw?.tagNames).filter((i) => typeof i === 'string' && i.trim()); const rawTagNames = asArray(raw?.tagNames).filter((i) => typeof i === 'string' && i.trim());
@ -392,6 +417,7 @@ function formatPatient(raw) {
mobiles, mobiles,
mobile, mobile,
createTime: createTimeStr, createTime: createTimeStr,
createTimeTs,
creator: raw?.creatorName || raw?.creator || '', creator: raw?.creatorName || raw?.creator || '',
hospitalId: raw?.customerNumber || raw?.hospitalId || '', hospitalId: raw?.customerNumber || raw?.hospitalId || '',
record, record,
@ -548,16 +574,11 @@ const patientList = computed(() => {
// New Patient Filter (Last 7 days) // New Patient Filter (Last 7 days)
if (currentTab.value.kind === 'new') { if (currentTab.value.kind === 'new') {
const now = dayjs(); const sevenDaysAgo = dayjs().subtract(7, 'day').startOf('day').valueOf();
const sevenDaysAgo = now.subtract(7, 'day').valueOf();
const flatList = all const flatList = all
.map((p) => { .filter((p) => Number(p?.createTimeTs || 0) >= sevenDaysAgo)
const t = parseCreateTime(p.createTime)?.valueOf(); .slice()
return t ? { ...p, _ts: t } : null; .sort((a, b) => Number(b?.createTimeTs || 0) - Number(a?.createTimeTs || 0));
})
.filter(Boolean)
.filter((p) => p._ts >= sevenDaysAgo)
.sort((a, b) => b._ts - a._ts);
return [{ letter: '最近新增', data: flatList }]; return [{ letter: '最近新增', data: flatList }];
} }
@ -717,11 +738,11 @@ const openVerifyEntry = () => {
}; };
const openAddCustomerServiceEntry = () => { const openAddCustomerServiceEntry = () => {
uni.showToast({ title: '添加客服功能待接入', icon: 'none' }); uni.navigateTo({ url: '/pages/work/service/contact-service' });
}; };
const openInvitePatientEntry = () => { const openInvitePatientEntry = () => {
uni.navigateTo({ url: '/pages/case/patient-invite' }); uni.navigateTo({ url: '/pages/work/team/invite/invite-patient' });
}; };
const openCreatePatientEntry = () => { const openCreatePatientEntry = () => {

View File

@ -60,9 +60,9 @@
class="send-btn" class="send-btn"
size="mini" size="mini"
type="primary" type="primary"
@click.stop="sendArticle(article)" @click.stop="handlePrimaryAction(article)"
> >
发送 {{ isSelectMode ? '选择' : '发送' }}
</button> </button>
</view> </view>
</view> </view>
@ -125,6 +125,9 @@ const pageParams = ref({
corpId: "", corpId: "",
}); });
const isSelectMode = ref(false);
const selectEventName = ref("");
// //
const searchTitle = ref(""); const searchTitle = ref("");
let searchTimer = null; let searchTimer = null;
@ -312,6 +315,20 @@ const closePreview = () => {
previewPopup.value?.close(); previewPopup.value?.close();
}; };
const selectArticle = (article) => {
if (!selectEventName.value) {
uni.showToast({ title: "缺少 eventName", icon: "none" });
return;
}
uni.$emit(selectEventName.value, article);
uni.navigateBack();
};
const handlePrimaryAction = (article) => {
if (isSelectMode.value) return selectArticle(article);
return sendArticle(article);
};
// //
const sendArticle = async (article) => { const sendArticle = async (article) => {
try { try {
@ -355,6 +372,8 @@ const goBack = () => {
// //
onLoad((options) => { onLoad((options) => {
isSelectMode.value = String(options?.select || '') === '1';
selectEventName.value = String(options?.eventName || '');
if (options.groupId) { if (options.groupId) {
pageParams.value.groupId = options.groupId; pageParams.value.groupId = options.groupId;
} }

View File

@ -78,6 +78,7 @@ const props = defineProps({
userId: { type: String, default: "" }, userId: { type: String, default: "" },
patientId: { type: String, default: "" }, patientId: { type: String, default: "" },
corpId: { type: String, default: "" }, corpId: { type: String, default: "" },
orderStatus: { type: String, default: "" },
}); });
// Emits // Emits
@ -422,7 +423,8 @@ const handleOpenConsult = () => {
}); });
}; };
const morePanelButtons = [ const morePanelButtons = computed(() => {
const buttons = [
{ text: "照片", icon: "/static/icon/zhaopian.png", action: showImagePicker }, { text: "照片", icon: "/static/icon/zhaopian.png", action: showImagePicker },
{ {
text: "回访任务", text: "回访任务",
@ -444,17 +446,27 @@ const morePanelButtons = [
icon: "/static/icon/wenjuan.png", icon: "/static/icon/wenjuan.png",
action: goToSurveyList, action: goToSurveyList,
}, },
{ ];
text: "结束问诊",
icon: "/static/icon/jieshuzixun.png", //
action: handleEndConsult, if (props.orderStatus === "finished") {
}, // ""
{ buttons.push({
text: "开启会话", text: "开启会话",
icon: "/static/icon/kaiqihuihua.png", icon: "/static/icon/kaiqihuihua.png",
action: handleOpenConsult, action: handleOpenConsult,
}, });
]; } else {
// ""
buttons.push({
text: "结束问诊",
icon: "/static/icon/jieshuzixun.png",
action: handleEndConsult,
});
}
return buttons;
});
function handleInputFocus() { function handleInputFocus() {
console.log("handleInputFocus"); console.log("handleInputFocus");

View File

@ -75,6 +75,8 @@ const text = computed(() => {
return '问诊已结束'; return '问诊已结束';
case 'consult_timeout': case 'consult_timeout':
return '问诊已超时'; return '问诊已超时';
case 'consult_reopened':
return '会话已重新开启';
default: default:
return systemMessageData.value.content || '[系统消息]'; return systemMessageData.value.content || '[系统消息]';
} }

View File

@ -163,6 +163,7 @@
:patientId="patientId" :patientId="patientId"
:corpId="corpId" :corpId="corpId"
:patientInfo="patientInfo" :patientInfo="patientInfo"
:orderStatus="orderStatus"
@scrollToBottom="() => scrollToBottom(true)" @scrollToBottom="() => scrollToBottom(true)"
@messageSent="() => scrollToBottom(true)" @messageSent="() => scrollToBottom(true)"
@endConsult="handleEndConsult" @endConsult="handleEndConsult"

View File

@ -63,9 +63,9 @@
class="send-btn" class="send-btn"
size="mini" size="mini"
type="primary" type="primary"
@click="sendSurvey(survey)" @click="handlePrimaryAction(survey)"
> >
发送 {{ isSelectMode ? '选择' : '发送' }}
</button> </button>
</view> </view>
</view> </view>
@ -119,14 +119,33 @@ const pageSize = 30;
const total = ref(0); const total = ref(0);
const emptyText = ref(""); const emptyText = ref("");
const isSelectMode = ref(false);
const selectEventName = ref("");
// //
onLoad((options) => { onLoad((options) => {
isSelectMode.value = String(options?.select || '') === '1';
selectEventName.value = String(options?.eventName || '');
customerId.value = options?.patientId || ""; customerId.value = options?.patientId || "";
customerName.value = options?.customerName || ""; customerName.value = options?.customerName || "";
getCategoryList(); getCategoryList();
loadSurveyList(); loadSurveyList();
}); });
const selectSurvey = (survey) => {
if (!selectEventName.value) {
uni.showToast({ title: "缺少 eventName", icon: "none" });
return;
}
uni.$emit(selectEventName.value, survey);
uni.navigateBack();
};
const handlePrimaryAction = (survey) => {
if (isSelectMode.value) return selectSurvey(survey);
return sendSurvey(survey);
};
// //
const getCategoryList = async () => { const getCategoryList = async () => {
try { try {

View File

@ -47,7 +47,7 @@ export default [
}, },
{ {
path: 'pages/home/case-home', path: 'pages/home/case-home',
meta: { title: '病', login: false }, meta: { title: '病', login: false },
}, },
{ {
path: 'pages/case/search', path: 'pages/case/search',
@ -65,10 +65,6 @@ export default [
path: 'pages/case/batch-share', path: 'pages/case/batch-share',
meta: { title: '共享客户', login: false }, meta: { title: '共享客户', login: false },
}, },
{
path: 'pages/case/patient-invite',
meta: { title: '邀请患者', login: false },
},
{ {
path: 'pages/case/patient-create', path: 'pages/case/patient-create',
meta: { title: '新增患者', login: false }, meta: { title: '新增患者', login: false },

View File

@ -94,6 +94,7 @@ const urlsConfig = {
getChatRecordsByGroupId: "getChatRecordsByGroupId", getChatRecordsByGroupId: "getChatRecordsByGroupId",
sendConsultRejectedMessage: "sendConsultRejectedMessage", sendConsultRejectedMessage: "sendConsultRejectedMessage",
endConsultation: "endConsultation", endConsultation: "endConsultation",
openConsultation: "openConsultation",
getGroupListByGroupId: "getGroupListByGroupId", getGroupListByGroupId: "getGroupListByGroupId",
acceptConsultation: "acceptConsultation", acceptConsultation: "acceptConsultation",
sendArticleMessage: "sendArticleMessage", sendArticleMessage: "sendArticleMessage",