feat:完善转移、共享、分组功能

This commit is contained in:
Jafeng 2026-01-23 17:59:24 +08:00
parent 7cb83882b8
commit da7397ce7e
6 changed files with 840 additions and 177 deletions

View File

@ -76,8 +76,8 @@
</view>
</view>
<view class="block-content">
<view v-if="archive.groups && archive.groups.length" class="tags-wrap">
<view v-for="tag in archive.groups" :key="tag" class="tag-item">
<view v-if="archiveGroupNames.length" class="tags-wrap">
<view v-for="tag in archiveGroupNames" :key="tag" class="tag-item">
{{ tag }}
</view>
</view>
@ -184,13 +184,13 @@
</view>
<view class="group-list">
<view v-for="g in groupOptions" :key="g" class="group-item" @click="toggleGroup(g)">
<view v-for="g in teamGroups" :key="g._id" class="group-item" @click="toggleGroup(String(g._id))">
<uni-icons
:type="selectedGroupMap[g] ? 'checkbox-filled' : 'checkbox'"
:type="selectedGroupMap[String(g._id)] ? 'checkbox-filled' : 'checkbox'"
size="20"
:color="selectedGroupMap[g] ? '#4f6ef7' : '#c7c7c7'"
:color="selectedGroupMap[String(g._id)] ? '#4f6ef7' : '#c7c7c7'"
/>
<text class="group-name">{{ g }}</text>
<text class="group-name">{{ g.groupName }}</text>
</view>
</view>
@ -259,8 +259,7 @@ const archive = ref({
createdByDoctor: true,
hasBindWechat: false,
notes: '',
groups: [],
groupOptions: ['高血压', '糖尿病', '高血脂']
groupIds: []
});
const accountStore = useAccountStore();
@ -295,6 +294,7 @@ function normalizeArchiveFromApi(raw) {
createTime: r.createTime || '',
creator: r.creator || '',
notes: r.notes || r.remark || '',
groupIds: Array.isArray(r.groupIds) ? r.groupIds : [],
};
return next;
}
@ -321,6 +321,7 @@ async function fetchArchive() {
}
archive.value = { ...archive.value, ...normalizeArchiveFromApi(res.data) };
saveToStorage();
await fetchTeamGroups(true);
} catch (e) {
toast('获取档案失败');
} finally {
@ -328,6 +329,56 @@ async function fetchArchive() {
}
}
const teamGroups = ref([]);
const groupNameMap = computed(() => {
const map = new Map();
(Array.isArray(teamGroups.value) ? teamGroups.value : []).forEach((g) => {
if (g && g._id && g.groupName) map.set(String(g._id), String(g.groupName));
});
return map;
});
const archiveGroupNames = computed(() => {
const ids = Array.isArray(archive.value.groupIds) ? archive.value.groupIds : [];
const map = groupNameMap.value;
return ids.map((id) => map.get(String(id))).filter(Boolean);
});
function sortGroupList(list) {
const { orderList, corpList, restList } = (Array.isArray(list) ? list : []).reduce(
(p, c) => {
if (typeof c?.sortOrder === 'number') p.orderList.push(c);
else if (c?.parentGroupId) p.corpList.push(c);
else p.restList.push(c);
return p;
},
{ orderList: [], corpList: [], restList: [] }
);
orderList.sort((a, b) => a.sortOrder - b.sortOrder);
return [...orderList, ...corpList, ...restList];
}
async function fetchTeamGroups(silent = false) {
const corpId = getCorpId();
const team = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || {};
const teamId = team?.teamId ? String(team.teamId) : '';
if (!corpId || !teamId) return;
try {
const projection = { _id: 1, groupName: 1, parentGroupId: 1, sortOrder: 1 };
const res = await api('getGroups', { corpId, teamId, page: 1, pageSize: 1000, projection, countGroupMember: false });
if (!res?.success) {
if (!silent) toast(res?.message || '获取分组失败');
teamGroups.value = [];
return;
}
const list = Array.isArray(res.data) ? res.data : Array.isArray(res.data?.data) ? res.data.data : [];
teamGroups.value = sortGroupList(list);
} catch (e) {
if (!silent) toast('获取分组失败');
teamGroups.value = [];
}
}
async function updateArchive(patch) {
const id = String(archiveId.value || '');
if (!id) return false;
@ -375,8 +426,7 @@ onLoad((options) => {
archive.value = {
...archive.value,
...cached,
groups: Array.isArray(cached.groups) ? cached.groups : archive.value.groups,
groupOptions: Array.isArray(cached.groupOptions) ? cached.groupOptions : archive.value.groupOptions
groupIds: Array.isArray(cached.groupIds) ? cached.groupIds : archive.value.groupIds
};
}
@ -416,8 +466,7 @@ onShow(() => {
archive.value = {
...archive.value,
...cached,
groups: Array.isArray(cached.groups) ? cached.groups : archive.value.groups,
groupOptions: Array.isArray(cached.groupOptions) ? cached.groupOptions : archive.value.groupOptions,
groupIds: Array.isArray(cached.groupIds) ? cached.groupIds : archive.value.groupIds,
};
}
setTimeout(measureTabsTop, 30);
@ -528,19 +577,18 @@ const saveNotes = () => {
const groupPopup = ref(null);
const addGroupPopup = ref(null);
const groupOptions = computed(() => (Array.isArray(archive.value.groupOptions) ? archive.value.groupOptions : []));
const selectedGroupMap = ref({});
const syncSelectedMapFromArchive = () => {
const current = Array.isArray(archive.value.groups) ? archive.value.groups : [];
selectedGroupMap.value = current.reduce((acc, name) => {
acc[name] = true;
const current = Array.isArray(archive.value.groupIds) ? archive.value.groupIds.map(String) : [];
selectedGroupMap.value = current.reduce((acc, id) => {
acc[String(id)] = true;
return acc;
}, {});
};
const openGroups = () => {
const openGroups = async () => {
await fetchTeamGroups(false);
syncSelectedMapFromArchive();
groupPopup.value?.open();
};
@ -549,14 +597,54 @@ const closeGroups = () => {
groupPopup.value?.close();
};
const toggleGroup = (name) => {
selectedGroupMap.value[name] = !selectedGroupMap.value[name];
const toggleGroup = (groupId) => {
const key = String(groupId || '');
if (!key) return;
selectedGroupMap.value[key] = !selectedGroupMap.value[key];
};
const saveGroups = () => {
archive.value.groups = Object.keys(selectedGroupMap.value).filter(k => selectedGroupMap.value[k]);
saveToStorage();
closeGroups();
async function applyGroupChange(nextIds) {
const memberId = String(archiveId.value || '');
if (!memberId) return false;
const prevIds = Array.isArray(archive.value.groupIds) ? archive.value.groupIds.map(String) : [];
const next = Array.isArray(nextIds) ? nextIds.map(String).filter(Boolean) : [];
const toAdd = next.filter((id) => !prevIds.includes(id));
const toRemove = prevIds.filter((id) => !next.includes(id));
if (toAdd.length === 0 && toRemove.length === 0) return true;
loading('保存中...');
try {
for (const id of toAdd) {
const res = await api('addGroupIdForMember', { memberId, toGroupId: id });
if (!res?.success) {
toast(res?.message || '分组保存失败');
return false;
}
}
for (const id of toRemove) {
const res = await api('addGroupIdForMember', { memberId, fromGroupId: id });
if (!res?.success) {
toast(res?.message || '分组保存失败');
return false;
}
}
uni.setStorageSync(NEED_RELOAD_STORAGE_KEY, 1);
await fetchArchive();
return true;
} catch (e) {
toast('分组保存失败');
return false;
} finally {
hideLoading();
}
}
const saveGroups = async () => {
const nextIds = Object.keys(selectedGroupMap.value).filter((k) => selectedGroupMap.value[k]);
const ok = await applyGroupChange(nextIds);
if (ok) closeGroups();
};
//
@ -571,22 +659,46 @@ const closeAddGroup = () => {
addGroupPopup.value?.close();
};
const saveAddGroup = () => {
const saveAddGroup = async () => {
const name = String(newGroupName.value || '').trim();
if (!name) {
uni.showToast({ title: '请输入分组名称', icon: 'none' });
return;
}
const exists = groupOptions.value.some(g => String(g).trim() === name);
const exists = (Array.isArray(teamGroups.value) ? teamGroups.value : []).some((g) => String(g?.groupName || '').trim() === name);
if (exists) {
uni.showToast({ title: '该分组已存在', icon: 'none' });
return;
}
archive.value.groupOptions = [...groupOptions.value, name];
selectedGroupMap.value[name] = true;
saveToStorage();
closeAddGroup();
const corpId = getCorpId();
const team = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || {};
const teamId = team?.teamId ? String(team.teamId) : '';
const creator = getUserId();
if (!corpId || !teamId || !creator) {
toast('缺少用户/团队信息');
return;
}
loading('保存中...');
try {
const params = { groupName: name, teamId, groupType: 'team', creator };
const res = await api('createGroup', { corpId, teamId, params });
if (!res?.success) {
toast(res?.message || '新增分组失败');
return;
}
await fetchTeamGroups(true);
// addGroupIdForMember
const newId = res.data ? String(res.data) : '';
if (newId) selectedGroupMap.value[newId] = true;
uni.setStorageSync('ykt_case_groups_need_reload', 1);
closeAddGroup();
} catch (e) {
toast('新增分组失败');
} finally {
hideLoading();
}
};
</script>

View File

@ -3,8 +3,8 @@
<view class="content">
<view class="section-title">选择共享团队</view>
<view class="selector-item" @click="selectTeam">
<text :class="team ? '' : 'placeholder'">{{ team ? team.name : "请选择团队" }}</text>
<uni-icons type="arrowdown" size="16" color="#999"></uni-icons>
<text :class="team ? '' : 'placeholder'">{{ team ? team.name : '请选择团队' }}</text>
<uni-icons type="arrowdown" size="16" color="#999" />
</view>
<view class="tips">共享客户表示客户档案共享多个团队可见多个团队可同时为该客户服务</view>
@ -12,8 +12,8 @@
<template v-if="team">
<view class="section-title">选择责任人</view>
<view class="selector-item" @click="selectUser">
<text :class="user ? '' : 'placeholder'">{{ user ? user.name : "请选择责任人" }}</text>
<uni-icons type="arrowdown" size="16" color="#999"></uni-icons>
<text :class="userId ? '' : 'placeholder'">{{ userLabel || '请选择责任人' }}</text>
<uni-icons type="arrowdown" size="16" color="#999" />
</view>
</template>
</view>
@ -26,39 +26,129 @@
</template>
<script setup>
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import api from '@/utils/api';
import useAccountStore from '@/store/account';
import { hideLoading, loading, toast } from '@/utils/widget';
const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team';
const NEED_RELOAD_STORAGE_KEY = 'ykt_case_need_reload';
const BATCH_CUSTOMER_IDS_KEY = 'ykt_case_batch_customer_ids';
const accountStore = useAccountStore();
const { account, doctorInfo } = storeToRefs(accountStore);
const { getDoctorInfo } = accountStore;
const customerIds = ref([]);
const currentTeam = ref(null);
const teams = ref([]);
const team = ref(null);
const user = ref(null);
const teamMembers = ref([]);
const userId = ref('');
// Mock Data
const teams = [
{ id: 1, name: '李医生团队' },
{ id: 2, name: '王医生团队' }
];
const userLabel = computed(() => {
const list = Array.isArray(teamMembers.value) ? teamMembers.value : [];
const found = list.find((m) => String(m?.userid || '') === String(userId.value));
return found ? String(found.anotherName || found.name || found.userid || '') : '';
});
const users = [
{ id: 101, name: '张医生' },
{ id: 102, name: '李医生' },
{ id: 103, name: '王医生' }
];
function getUserId() {
const d = doctorInfo.value || {};
const a = account.value || {};
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || '') || '';
}
const selectTeam = () => {
function getCorpId() {
const t = currentTeam.value || {};
const a = account.value || {};
const d = doctorInfo.value || {};
return String(t.corpId || d.corpId || a.corpId || '') || '';
}
function getCurrentTeamId() {
return String(currentTeam.value?.teamId || '') || '';
}
function normalizeTeam(raw) {
if (!raw || typeof raw !== 'object') return null;
const teamId = raw.teamId || raw.id || raw._id || '';
const name = raw.name || raw.teamName || raw.team || '';
const corpId = raw.corpId || raw.corpID || '';
if (!teamId || !name) return null;
return { teamId: String(teamId), name: String(name), corpId: corpId ? String(corpId) : '' };
}
async function ensureDoctor() {
if (doctorInfo.value) return;
if (!account.value?.openid) return;
try {
await getDoctorInfo();
} catch (e) {
// ignore
}
}
async function loadTeams() {
await ensureDoctor();
const corpId = getCorpId();
const userId = getUserId();
if (!corpId || !userId) return;
const res = await api('getTeamBymember', { corpId, corpUserId: userId });
if (!res?.success) {
toast(res?.message || '获取团队失败');
return;
}
const list = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.data) ? res.data.data : [];
teams.value = list.map(normalizeTeam).filter(Boolean);
}
async function loadTeamMembers(teamId) {
const corpId = getCorpId();
if (!teamId) return;
const res = await api('getTeamData', { corpId, teamId });
if (!res?.success) {
toast(res?.message || '获取团队成员失败');
teamMembers.value = [];
return;
}
const t = res?.data && typeof res.data === 'object' ? res.data : {};
teamMembers.value = Array.isArray(t.memberList) ? t.memberList : [];
}
const selectTeam = async () => {
if (!teams.value.length) await loadTeams();
const currentId = getCurrentTeamId();
const candidates = teams.value.filter((t) => t.teamId !== currentId);
if (!candidates.length) {
toast('暂无可选团队');
return;
}
uni.showActionSheet({
itemList: teams.map(t => t.name),
success: (res) => {
team.value = teams[res.tapIndex];
user.value = null;
}
itemList: candidates.map((t) => t.name),
success: async (res) => {
team.value = candidates[res.tapIndex] || null;
userId.value = '';
teamMembers.value = [];
if (team.value) await loadTeamMembers(team.value.teamId);
},
});
};
const selectUser = () => {
const list = Array.isArray(teamMembers.value) ? teamMembers.value : [];
if (!list.length) {
toast('当前团队暂无可选成员');
return;
}
const labels = list.map((m) => String(m?.anotherName || m?.name || m?.userid || ''));
uni.showActionSheet({
itemList: users.map(u => u.name),
itemList: labels,
success: (res) => {
user.value = users[res.tapIndex];
}
const picked = list[res.tapIndex];
userId.value = picked?.userid ? String(picked.userid) : '';
},
});
};
@ -66,25 +156,52 @@ const cancel = () => {
uni.navigateBack();
};
const save = () => {
if (!team.value) {
uni.showToast({ title: '请选择团队', icon: 'none' });
return;
const save = async () => {
if (!team.value) return toast('请选择团队');
if (!userId.value) return toast('请选择负责人');
const corpId = getCorpId();
const currentTeamId = getCurrentTeamId();
const creatorUserId = getUserId();
if (!corpId || !currentTeamId || !creatorUserId) return toast('缺少用户/团队信息');
loading('保存中...');
try {
const res = await api('transferCustomers', {
corpId,
customerIds: customerIds.value,
currentTeamId,
targetTeamId: team.value.teamId,
targetUserId: userId.value,
operationType: 'share',
creatorUserId,
});
if (!res?.success) {
toast(res?.message || '操作失败');
return;
}
toast('操作成功');
uni.removeStorageSync(BATCH_CUSTOMER_IDS_KEY);
uni.setStorageSync(NEED_RELOAD_STORAGE_KEY, 1);
uni.navigateBack();
} catch (e) {
toast('操作失败');
} finally {
hideLoading();
}
if (!user.value) {
uni.showToast({ title: '请选择负责人', icon: 'none' });
return;
}
uni.showLoading({ title: '保存中' });
setTimeout(() => {
uni.hideLoading();
uni.showToast({ title: '操作成功' });
setTimeout(() => {
uni.navigateBack();
}, 1500);
}, 1000);
};
onLoad(async () => {
customerIds.value = Array.isArray(uni.getStorageSync(BATCH_CUSTOMER_IDS_KEY))
? uni.getStorageSync(BATCH_CUSTOMER_IDS_KEY).map(String).filter(Boolean)
: [];
currentTeam.value = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || null;
if (!customerIds.value.length) {
toast('未选择客户');
setTimeout(() => uni.navigateBack(), 200);
return;
}
await loadTeams();
});
</script>
<style lang="scss" scoped>
@ -105,7 +222,7 @@ const save = () => {
color: #333;
margin-top: 20px;
margin-bottom: 10px;
&:first-child {
margin-top: 0;
}
@ -119,11 +236,11 @@ const save = () => {
align-items: center;
justify-content: space-between;
padding: 0 15px;
text {
font-size: 14px;
color: #333;
&.placeholder {
color: #999;
}
@ -143,7 +260,7 @@ const save = () => {
padding: 15px 20px 30px;
display: flex;
gap: 15px;
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
.btn {
flex: 1;
@ -164,10 +281,11 @@ const save = () => {
color: #fff;
border: none;
}
&::after {
border: none;
}
}
}
</style>

View File

@ -3,15 +3,15 @@
<view class="content">
<view class="section-title">选择新负责团队</view>
<view class="selector-item" @click="selectTeam">
<text :class="team ? '' : 'placeholder'">{{ team ? team.name : "请选择选择团队" }}</text>
<uni-icons type="arrowdown" size="16" color="#999"></uni-icons>
<text :class="team ? '' : 'placeholder'">{{ team ? team.name : '请选择团队' }}</text>
<uni-icons type="arrowdown" size="16" color="#999" />
</view>
<template v-if="team">
<view class="section-title">选择责任人</view>
<view class="selector-item" @click="selectUser">
<text :class="user ? '' : 'placeholder'">{{ user ? user.name : "请选择责任人" }}</text>
<uni-icons type="arrowdown" size="16" color="#999"></uni-icons>
<text :class="userId ? '' : 'placeholder'">{{ userLabel || '请选择责任人' }}</text>
<uni-icons type="arrowdown" size="16" color="#999" />
</view>
</template>
@ -26,40 +26,129 @@
</template>
<script setup>
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import api from '@/utils/api';
import useAccountStore from '@/store/account';
import { hideLoading, loading, toast } from '@/utils/widget';
const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team';
const NEED_RELOAD_STORAGE_KEY = 'ykt_case_need_reload';
const BATCH_CUSTOMER_IDS_KEY = 'ykt_case_batch_customer_ids';
const accountStore = useAccountStore();
const { account, doctorInfo } = storeToRefs(accountStore);
const { getDoctorInfo } = accountStore;
const customerIds = ref([]);
const currentTeam = ref(null);
const teams = ref([]);
const team = ref(null);
const user = ref(null);
const teamMembers = ref([]);
const userId = ref('');
// Mock Data
const teams = [
{ id: 1, name: '张敏西服务团队' },
{ id: 2, name: '李医生团队' },
{ id: 3, name: '王医生团队' }
];
const userLabel = computed(() => {
const list = Array.isArray(teamMembers.value) ? teamMembers.value : [];
const found = list.find((m) => String(m?.userid || '') === String(userId.value));
return found ? String(found.anotherName || found.name || found.userid || '') : '';
});
const users = [
{ id: 101, name: '张医生' },
{ id: 102, name: '李医生' },
{ id: 103, name: '王医生' }
];
function getUserId() {
const d = doctorInfo.value || {};
const a = account.value || {};
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || '') || '';
}
const selectTeam = () => {
function getCorpId() {
const t = currentTeam.value || {};
const a = account.value || {};
const d = doctorInfo.value || {};
return String(t.corpId || d.corpId || a.corpId || '') || '';
}
function getCurrentTeamId() {
return String(currentTeam.value?.teamId || '') || '';
}
function normalizeTeam(raw) {
if (!raw || typeof raw !== 'object') return null;
const teamId = raw.teamId || raw.id || raw._id || '';
const name = raw.name || raw.teamName || raw.team || '';
const corpId = raw.corpId || raw.corpID || '';
if (!teamId || !name) return null;
return { teamId: String(teamId), name: String(name), corpId: corpId ? String(corpId) : '' };
}
async function ensureDoctor() {
if (doctorInfo.value) return;
if (!account.value?.openid) return;
try {
await getDoctorInfo();
} catch (e) {
// ignore
}
}
async function loadTeams() {
await ensureDoctor();
const corpId = getCorpId();
const userId = getUserId();
if (!corpId || !userId) return;
const res = await api('getTeamBymember', { corpId, corpUserId: userId });
if (!res?.success) {
toast(res?.message || '获取团队失败');
return;
}
const list = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.data) ? res.data.data : [];
teams.value = list.map(normalizeTeam).filter(Boolean);
}
async function loadTeamMembers(teamId) {
const corpId = getCorpId();
if (!teamId) return;
const res = await api('getTeamData', { corpId, teamId });
if (!res?.success) {
toast(res?.message || '获取团队成员失败');
teamMembers.value = [];
return;
}
const t = res?.data && typeof res.data === 'object' ? res.data : {};
teamMembers.value = Array.isArray(t.memberList) ? t.memberList : [];
}
const selectTeam = async () => {
if (!teams.value.length) await loadTeams();
const currentId = getCurrentTeamId();
const candidates = teams.value.filter((t) => t.teamId !== currentId);
if (!candidates.length) {
toast('暂无可选团队');
return;
}
uni.showActionSheet({
itemList: teams.map(t => t.name),
success: (res) => {
team.value = teams[res.tapIndex];
user.value = null; // Reset user when team changes
}
itemList: candidates.map((t) => t.name),
success: async (res) => {
team.value = candidates[res.tapIndex] || null;
userId.value = '';
teamMembers.value = [];
if (team.value) await loadTeamMembers(team.value.teamId);
},
});
};
const selectUser = () => {
const list = Array.isArray(teamMembers.value) ? teamMembers.value : [];
if (!list.length) {
toast('当前团队暂无可选成员');
return;
}
const labels = list.map((m) => String(m?.anotherName || m?.name || m?.userid || ''));
uni.showActionSheet({
itemList: users.map(u => u.name),
itemList: labels,
success: (res) => {
user.value = users[res.tapIndex];
}
const picked = list[res.tapIndex];
userId.value = picked?.userid ? String(picked.userid) : '';
},
});
};
@ -67,25 +156,53 @@ const cancel = () => {
uni.navigateBack();
};
const save = () => {
if (!team.value) {
uni.showToast({ title: '请选择团队', icon: 'none' });
return;
const save = async () => {
if (!team.value) return toast('请选择团队');
if (!userId.value) return toast('请选择负责人');
const corpId = getCorpId();
const currentTeamId = getCurrentTeamId();
const creatorUserId = getUserId();
if (!corpId || !currentTeamId || !creatorUserId) return toast('缺少用户/团队信息');
loading('保存中...');
try {
const res = await api('transferCustomers', {
corpId,
customerIds: customerIds.value,
currentTeamId,
targetTeamId: team.value.teamId,
targetUserId: userId.value,
operationType: 'transferToOtherTeam',
creatorUserId,
});
if (!res?.success) {
toast(res?.message || '操作失败');
return;
}
toast('操作成功');
uni.removeStorageSync(BATCH_CUSTOMER_IDS_KEY);
uni.setStorageSync(NEED_RELOAD_STORAGE_KEY, 1);
uni.navigateBack();
} catch (e) {
toast('操作失败');
} finally {
hideLoading();
}
if (!user.value) {
uni.showToast({ title: '请选择责任人', icon: 'none' });
return;
}
uni.showLoading({ title: '保存中' });
setTimeout(() => {
uni.hideLoading();
uni.showToast({ title: '操作成功' });
setTimeout(() => {
uni.navigateBack();
}, 1500);
}, 1000);
};
onLoad(async () => {
customerIds.value = Array.isArray(uni.getStorageSync(BATCH_CUSTOMER_IDS_KEY))
? uni.getStorageSync(BATCH_CUSTOMER_IDS_KEY).map(String).filter(Boolean)
: [];
currentTeam.value = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || null;
if (!customerIds.value.length) {
toast('未选择客户');
setTimeout(() => uni.navigateBack(), 200);
return;
}
await loadTeams();
});
</script>
<style lang="scss" scoped>
@ -106,7 +223,7 @@ const save = () => {
color: #333;
margin-top: 20px;
margin-bottom: 10px;
&:first-child {
margin-top: 0;
}
@ -120,11 +237,11 @@ const save = () => {
align-items: center;
justify-content: space-between;
padding: 0 15px;
text {
font-size: 14px;
color: #333;
&.placeholder {
color: #999;
}
@ -144,7 +261,7 @@ const save = () => {
padding: 15px 20px 30px;
display: flex;
gap: 15px;
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
.btn {
flex: 1;
@ -165,10 +282,11 @@ const save = () => {
color: #fff;
border: none;
}
&::after {
border: none;
}
}
}
</style>

View File

@ -31,13 +31,13 @@
<scroll-view scroll-x class="tabs-scroll" :show-scrollbar="false">
<view class="tabs-container">
<view
v-for="(tab, index) in tabs"
:key="index"
v-for="tab in tabs"
:key="tab.key"
class="tab-item"
:class="{ active: currentTab === index }"
@click="currentTab = index"
:class="{ active: currentTabKey === tab.key }"
@click="onTabClick(tab)"
>
{{ tab }}
{{ tab.label }}
</view>
</view>
</scroll-view>
@ -83,7 +83,7 @@
<!-- Row 2 / 3 -->
<view class="card-row-bottom">
<template v-if="currentTab === 1"> <!-- New Patient Tab -->
<template v-if="currentTabKey === 'new'"> <!-- New Patient Tab -->
<text class="record-text">
{{ patient.createTime || '-' }} / {{ patient.creator || '-' }}
</text>
@ -147,9 +147,19 @@ import { toast } from '@/utils/widget';
// State
const teams = ref([]);
const currentTeam = ref(null);
const currentTab = ref(0);
const currentTabKey = ref('all');
const scrollIntoId = ref('');
const tabs = ['全部', '新患者', '糖尿病', '高血压', '冠心病', '慢阻肺'];
const teamGroups = ref([]);
const tabs = computed(() => {
const base = [
{ key: 'all', label: '全部', kind: 'all' },
{ key: 'new', label: '新患者', kind: 'new' },
];
const groupTabs = (Array.isArray(teamGroups.value) ? teamGroups.value : [])
.filter((g) => g && g._id && g.groupName)
.map((g) => ({ key: `group:${g._id}`, label: String(g.groupName), kind: 'group', groupId: String(g._id) }));
return [...base, ...groupTabs];
});
const isBatchMode = ref(false);
const selectedItems = ref([]); // Stores patient phone or unique ID
@ -162,6 +172,8 @@ const verifyFailedReason = ref('资料不完整,请补充营业执照/资质
const DETAIL_STORAGE_KEY = 'ykt_case_archive_detail';
const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team';
const NEED_RELOAD_STORAGE_KEY = 'ykt_case_need_reload';
const GROUPS_RELOAD_KEY = 'ykt_case_groups_need_reload';
const BATCH_CUSTOMER_IDS_KEY = 'ykt_case_batch_customer_ids';
const page = ref(1);
const pages = ref(0);
@ -170,6 +182,7 @@ const totalFromApi = ref(0);
const loading = ref(false);
const rawPatients = ref([]);
const more = computed(() => page.value < pages.value);
const currentTab = computed(() => tabs.value.find((t) => t.key === currentTabKey.value) || tabs.value[0]);
const accountStore = useAccountStore();
const { account, doctorInfo } = storeToRefs(accountStore);
@ -207,6 +220,41 @@ function getTeamId() {
return String(currentTeam.value?.teamId || '') || '';
}
function sortGroupList(list) {
const { orderList, corpList, restList } = (Array.isArray(list) ? list : []).reduce(
(p, c) => {
if (typeof c?.sortOrder === 'number') p.orderList.push(c);
else if (c?.parentGroupId) p.corpList.push(c);
else p.restList.push(c);
return p;
},
{ orderList: [], corpList: [], restList: [] }
);
orderList.sort((a, b) => a.sortOrder - b.sortOrder);
return [...orderList, ...corpList, ...restList];
}
async function loadGroups() {
if (!currentTeam.value) return;
const corpId = getCorpId();
const teamId = getTeamId();
if (!corpId || !teamId) return;
const projection = { _id: 1, groupName: 1, parentGroupId: 1, sortOrder: 1 };
const res = await api('getGroups', { corpId, teamId, page: 1, pageSize: 1000, projection, countGroupMember: false });
if (!res?.success) {
teamGroups.value = [];
return;
}
const list = Array.isArray(res.data) ? res.data : Array.isArray(res.data?.data) ? res.data.data : [];
teamGroups.value = sortGroupList(list);
// tab 退
if (currentTabKey.value.startsWith('group:')) {
const gid = currentTabKey.value.slice('group:'.length);
if (!teamGroups.value.some((g) => String(g._id) === String(gid))) currentTabKey.value = 'all';
}
}
function getLetter(patient) {
const raw = patient?.firstLetter || patient?.nameFirstLetter || patient?.pinyinFirstLetter || patient?.letter || '';
const candidate = String(raw || '').trim();
@ -342,14 +390,25 @@ async function reload(reset = true) {
totalFromApi.value = 0;
}
loading.value = true;
const res = await api('searchCorpCustomerWithFollowTime', {
const query = {
corpId,
userId,
teamId,
page: page.value,
pageSize: pageSize.value,
});
};
if (currentTab.value.kind === 'group' && currentTab.value.groupId) {
query.groupIds = [currentTab.value.groupId];
} else if (currentTab.value.kind === 'new') {
const start = dayjs().subtract(7, 'day').startOf('day').valueOf();
const end = dayjs().endOf('day').valueOf();
query.startCreateTime = start;
query.endCreateTime = end;
}
loading.value = true;
const res = await api('searchCorpCustomerWithFollowTime', query);
loading.value = false;
if (!res?.success) {
@ -409,7 +468,7 @@ const patientList = computed(() => {
const all = rawPatients.value || [];
// New Patient Filter (Last 7 days)
if (currentTab.value === 1) {
if (currentTab.value.kind === 'new') {
const now = dayjs();
const sevenDaysAgo = now.subtract(7, 'day').valueOf();
const flatList = all
@ -423,25 +482,18 @@ const patientList = computed(() => {
return [{ letter: '最近新增', data: flatList }];
}
// Tab Filtering (Mock logic for other tabs)
let filtered = all;
if (currentTab.value > 1) {
const tabName = tabs[currentTab.value];
filtered = filtered.filter((p) => Array.isArray(p.tags) && p.tags.includes(tabName));
}
return groupByLetter(filtered);
return groupByLetter(all);
});
const indexList = computed(() => {
if (currentTab.value === 1) return []; // No index bar for new patient
if (currentTab.value.kind === 'new') return []; // No index bar for new patient
return 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').filter(l => patientList.value.some(g => g.letter === l));
});
const totalPatients = computed(() => {
let count = 0;
patientList.value.forEach(g => count += g.data.length);
if (currentTab.value === 0 && totalFromApi.value) return totalFromApi.value;
return count;
return totalFromApi.value || count;
});
// Methods
@ -454,7 +506,7 @@ const checkBatchMode = () => {
};
const scrollToLetter = (letter) => {
if (currentTab.value === 1) return;
if (currentTab.value.kind === 'new') return;
scrollIntoId.value = letterToDomId(letter);
};
@ -469,6 +521,8 @@ const toggleTeamPopup = () => {
success: function (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();
reload(true);
}
});
@ -619,6 +673,7 @@ const handleTransfer = () => {
uni.showToast({ title: '请选择患者', icon: 'none' });
return;
}
uni.setStorageSync(BATCH_CUSTOMER_IDS_KEY, selectedItems.value.slice());
// Navigate to Transfer Page
uni.navigateTo({ url: '/pages/case/batch-transfer' });
};
@ -628,12 +683,12 @@ const handleShare = () => {
uni.showToast({ title: '请选择患者', icon: 'none' });
return;
}
uni.setStorageSync(BATCH_CUSTOMER_IDS_KEY, selectedItems.value.slice());
// Navigate to Share Page
uni.navigateTo({ url: '/pages/case/batch-share' });
};
function loadMore() {
if (currentTab.value === 1) return;
if (!more.value || loading.value) return;
page.value += 1;
reload(false);
@ -644,9 +699,24 @@ watch(currentTeam, (t) => {
uni.setStorageSync(CURRENT_TEAM_STORAGE_KEY, t);
});
watch(currentTabKey, () => {
if (!currentTeam.value) return;
reload(true);
});
function onTabClick(tab) {
if (checkBatchMode()) return;
if (!tab || !tab.key) return;
if (currentTabKey.value === tab.key) return;
currentTabKey.value = tab.key;
}
onLoad(async () => {
await loadTeams();
if (currentTeam.value) await reload(true);
if (currentTeam.value) {
await loadGroups();
await reload(true);
}
});
onShow(async () => {
@ -654,6 +724,18 @@ onShow(async () => {
if (need) {
uni.removeStorageSync(NEED_RELOAD_STORAGE_KEY);
await reload(true);
// 退
isBatchMode.value = false;
selectedItems.value = [];
}
const needGroups = uni.getStorageSync(GROUPS_RELOAD_KEY);
if (needGroups) {
uni.removeStorageSync(GROUPS_RELOAD_KEY);
await loadGroups();
await reload(true);
} else {
await loadGroups();
}
});

View File

@ -1,21 +1,35 @@
<template>
<view class="manage-container">
<view class="group-list">
<view v-for="(item, index) in groups" :key="index" class="group-item">
<view class="left-action" @click="handleDelete(index)">
<uni-icons type="minus-filled" size="24" color="#ff4d4f"></uni-icons>
<view v-for="(item, index) in groups" :key="item._id" class="group-item">
<view class="left-action" :class="{ disabled: Boolean(item.parentGroupId) || isSort }" @click="handleDelete(item, index)">
<uni-icons type="minus-filled" size="24" :color="Boolean(item.parentGroupId) || isSort ? '#ddd' : '#ff4d4f'"></uni-icons>
</view>
<view class="group-name">
<view class="name-row">
<text class="name-text">{{ item.groupName }}</text>
<text v-if="item.parentGroupId" class="corp-tag">机构</text>
</view>
<text v-if="item.description" class="desc">{{ item.description }}</text>
</view>
<view class="group-name">{{ item.name }}</view>
<view class="right-actions">
<uni-icons type="compose" size="24" color="#5d8aff" class="icon-edit" @click="handleEdit(item, index)"></uni-icons>
<uni-icons type="bars" size="24" color="#5d8aff" class="icon-drag"></uni-icons>
<uni-icons type="compose" size="24" :color="isSort ? '#ddd' : '#5d8aff'" class="icon-edit" @click="handleEdit(item, index)"></uni-icons>
<template v-if="isSort">
<uni-icons type="arrowup" size="20" color="#5d8aff" class="icon-sort" @click="moveUp(index)"></uni-icons>
<uni-icons type="arrowdown" size="20" color="#5d8aff" class="icon-sort" @click="moveDown(index)"></uni-icons>
</template>
<uni-icons v-else type="bars" size="24" color="#5d8aff" class="icon-drag" @click="enterSort"></uni-icons>
</view>
</view>
</view>
<!-- Bottom Button -->
<view class="footer">
<button class="add-btn" @click="handleAdd">添加新分组</button>
<template v-if="isSort">
<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>
<!-- Dialog -->
@ -36,13 +50,90 @@
<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import api from '@/utils/api';
import useAccountStore from '@/store/account';
import { hideLoading, loading, toast } from '@/utils/widget';
// State
const groups = ref([
{ id: 1, name: '糖尿病' },
{ id: 2, name: '高血压' },
{ id: 3, name: '高血脂' }
]);
const groups = ref([]);
const originalGroups = ref([]);
const isSort = ref(false);
const GROUPS_RELOAD_KEY = 'ykt_case_groups_need_reload';
const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team';
const accountStore = useAccountStore();
const { account, doctorInfo } = storeToRefs(accountStore);
const { getDoctorInfo } = accountStore;
function getUserId() {
const d = doctorInfo.value || {};
const a = account.value || {};
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || '') || '';
}
function getCorpId() {
const d = doctorInfo.value || {};
const a = account.value || {};
const team = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || {};
return String(d.corpId || a.corpId || team.corpId || '') || '';
}
function getTeamId() {
const team = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || {};
return String(team.teamId || '') || '';
}
function sortGroupList(list) {
const { orderList, corpList, restList } = (Array.isArray(list) ? list : []).reduce(
(p, c) => {
if (typeof c?.sortOrder === 'number') p.orderList.push(c);
else if (c?.parentGroupId) p.corpList.push(c);
else p.restList.push(c);
return p;
},
{ orderList: [], corpList: [], restList: [] }
);
orderList.sort((a, b) => a.sortOrder - b.sortOrder);
return [...orderList, ...corpList, ...restList];
}
async function ensureDoctor() {
if (doctorInfo.value) return;
if (!account.value?.openid) return;
try {
await getDoctorInfo();
} catch (e) {
// ignore
}
}
async function loadGroups() {
await ensureDoctor();
const corpId = getCorpId();
const teamId = getTeamId();
if (!corpId || !teamId) return;
loading('加载中...');
try {
const projection = { _id: 1, groupName: 1, parentGroupId: 1, description: 1, sortOrder: 1 };
const res = await api('getGroups', { corpId, teamId, page: 1, pageSize: 1000, projection, countGroupMember: false });
if (!res?.success) {
toast(res?.message || '获取分组失败');
return;
}
const list = Array.isArray(res.data) ? res.data : Array.isArray(res.data?.data) ? res.data.data : [];
const sorted = sortGroupList(list);
groups.value = sorted.map((i) => ({ ...i }));
originalGroups.value = sorted.map((i) => ({ ...i }));
} catch (e) {
toast('获取分组失败');
} finally {
hideLoading();
}
}
const showDialog = ref(false);
const dialogMode = ref('add'); // 'add' or 'edit'
@ -53,6 +144,7 @@ const dialogTitle = ref('添加新分组');
// Methods
const handleAdd = () => {
if (isSort.value) return;
dialogMode.value = 'add';
dialogTitle.value = '添加新分组';
inputValue.value = '';
@ -60,22 +152,42 @@ const handleAdd = () => {
};
const handleEdit = (item, index) => {
if (isSort.value) return;
dialogMode.value = 'edit';
dialogTitle.value = '编辑分组名称';
inputValue.value = item.name;
inputValue.value = item.groupName || '';
editingIndex.value = index;
showDialog.value = true;
};
const handleDelete = (index) => {
const handleDelete = (item, index) => {
if (isSort.value) return;
if (item?.parentGroupId) return;
uni.showModal({
title: '提示',
content: '确定要删除该分组吗?',
success: (res) => {
if (res.confirm) {
groups.value.splice(index, 1);
success: async (res) => {
if (!res.confirm) return;
const corpId = getCorpId();
const teamId = getTeamId();
if (!corpId || !teamId) {
toast('缺少团队信息');
return;
}
}
loading('');
try {
const delRes = await api('removeGroup', { corpId, id: item._id, teamId, groupType: 'team' });
if (!delRes?.success) {
toast(delRes?.message || '删除失败');
return;
}
toast('删除成功');
uni.setStorageSync(GROUPS_RELOAD_KEY, 1);
await loadGroups();
} finally {
hideLoading();
}
},
});
};
@ -83,23 +195,107 @@ const closeDialog = () => {
showDialog.value = false;
};
const handleSave = () => {
const handleSave = async () => {
if (!inputValue.value.trim()) {
uni.showToast({ title: '请输入分组名称', icon: 'none' });
return;
}
if (dialogMode.value === 'add') {
groups.value.push({
id: Date.now(),
name: inputValue.value
});
} else {
groups.value[editingIndex.value].name = inputValue.value;
await ensureDoctor();
const corpId = getCorpId();
const teamId = getTeamId();
const userId = getUserId();
if (!corpId || !teamId || !userId) {
toast('缺少用户/团队信息');
return;
}
closeDialog();
loading('保存中...');
try {
if (dialogMode.value === 'add') {
const params = {
groupName: inputValue.value.trim(),
teamId,
groupType: 'team',
creator: userId,
};
const res = await api('createGroup', { corpId, teamId, params });
if (!res?.success) {
toast(res?.message || '新增失败');
return;
}
toast('新增成功');
} else {
const item = groups.value[editingIndex.value];
if (!item?._id) return;
const res = await api('updateGroup', { corpId, id: item._id, params: { groupName: inputValue.value.trim() } });
if (!res?.success) {
toast(res?.message || '修改失败');
return;
}
toast('修改成功');
}
uni.setStorageSync(GROUPS_RELOAD_KEY, 1);
closeDialog();
await loadGroups();
} finally {
hideLoading();
}
};
function enterSort() {
if (!groups.value.length) return;
isSort.value = true;
}
function cancelSort() {
isSort.value = false;
groups.value = originalGroups.value.map((i) => ({ ...i }));
}
function moveUp(index) {
if (!isSort.value) return;
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) {
if (!isSort.value) return;
if (index >= groups.value.length - 1) return;
const next = groups.value.slice();
const tmp = next[index + 1];
next[index + 1] = next[index];
next[index] = tmp;
groups.value = next;
}
async function saveSort() {
const teamId = getTeamId();
if (!teamId) return;
loading('');
try {
const data = groups.value.map((i, idx) => ({ _id: i._id, sortOrder: idx }));
const res = await api('sortGroups', { teamId, data });
if (!res?.success) {
toast(res?.message || '保存失败');
return;
}
toast('保存成功');
uni.setStorageSync(GROUPS_RELOAD_KEY, 1);
isSort.value = false;
await loadGroups();
} finally {
hideLoading();
}
}
onLoad(() => {
loadGroups();
});
</script>
<style lang="scss" scoped>
@ -121,12 +317,33 @@ const handleSave = () => {
.left-action {
margin-right: 15px;
&.disabled {
opacity: 0.6;
}
}
.group-name {
flex: 1;
font-size: 16px;
color: #333;
.name-row {
display: flex;
align-items: center;
gap: 8px;
}
.corp-tag {
font-size: 12px;
padding: 2px 6px;
border-radius: 10px;
background: #f0f0f0;
color: #666;
}
.desc {
display: block;
margin-top: 4px;
font-size: 12px;
color: #999;
}
}
.right-actions {
@ -137,6 +354,9 @@ const handleSave = () => {
.icon-edit, .icon-drag {
padding: 5px; // Increase tap area
}
.icon-sort {
padding: 5px;
}
}
}
@ -161,6 +381,12 @@ const handleSave = () => {
&::after {
border: none;
}
&.plain {
background-color: #fff;
color: #666;
border: 1px solid #ddd;
}
}
}

View File

@ -21,6 +21,13 @@ const urlsConfig = {
member: {
addCustomer: 'add',
updateCustomer: 'update',
transferCustomers: 'transferCustomers',
getGroups: 'getGroups',
createGroup: 'createGroup',
updateGroup: 'updateGroup',
removeGroup: 'removeGroup',
sortGroups: 'sortGroups',
addGroupIdForMember: 'addGroupIdForMember',
bindMiniAppArchive: "bindMiniAppArchive",
getCustomerByCustomerId: 'getCustomerByCustomerId',
getMiniAppCustomers: 'getMiniAppCustomers',