feat:新增拖拽排序,修复bug
This commit is contained in:
parent
5d6a5a4a82
commit
959da07a05
@ -190,12 +190,6 @@
|
|||||||
"navigationBarTitleText": "共享客户"
|
"navigationBarTitleText": "共享客户"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "patient-invite",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "邀请患者"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "patient-create",
|
"path": "patient-create",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
1115
pages/case/case.vue
1115
pages/case/case.vue
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||||
|
|||||||
@ -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>
|
|
||||||
@ -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 = () => {
|
||||||
|
|||||||
@ -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 },
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user