1211 lines
30 KiB
Vue
1211 lines
30 KiB
Vue
<template>
|
||
<view class="page">
|
||
<view class="card">
|
||
<view class="header">
|
||
<view class="avatar">
|
||
<image v-if="archive.avatar" class="avatar-img" :src="archive.avatar" mode="aspectFill" />
|
||
</view>
|
||
|
||
<view class="header-main">
|
||
<view class="name-row">
|
||
<text class="name">{{ archive.name || '-' }}</text>
|
||
<text v-if="sexOrAge" class="meta">{{ sexOrAge }}</text>
|
||
</view>
|
||
|
||
<view v-if="archive.mobile" class="sub-line">{{ archive.mobile }}</view>
|
||
|
||
<view v-if="idRows.length" class="id-rows">
|
||
<view v-for="row in idRows" :key="row.label" class="id-row">
|
||
<text class="id-label">{{ row.label }}:</text>
|
||
<text class="id-value">{{ row.value }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="createText" class="create-row">
|
||
<text class="create-text">{{ createText }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="header-right" @click="goEdit">
|
||
<uni-icons type="right" size="18" color="#bbb" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="cells">
|
||
<!-- 联系电话 -->
|
||
<view class="info-row border-bottom">
|
||
<text class="input-label">联系电话</text>
|
||
<view class="info-right">
|
||
<template v-if="archive.mobile">
|
||
<view class="phone-area" @click="makeCall">
|
||
<uni-icons type="phone-filled" size="18" color="#4f6ef7" class="mr-4" />
|
||
<text class="phone-text">{{ archive.mobile }}</text>
|
||
</view>
|
||
<view class="edit-icon" @click.stop="openContact">
|
||
<uni-icons type="compose" size="20" color="#4f6ef7" />
|
||
</view>
|
||
</template>
|
||
<view v-else class="empty-click" @click="openContact">
|
||
<text class="placeholder">添加联系电话</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 备注 -->
|
||
<view class="info-block border-bottom">
|
||
<view class="block-header">
|
||
<text class="input-label">备注</text>
|
||
<view class="edit-icon" @click.stop="openNotes">
|
||
<uni-icons type="compose" size="20" color="#4f6ef7" />
|
||
</view>
|
||
</view>
|
||
<view class="block-content">
|
||
<text v-if="archive.notes" class="note-content">{{ archive.notes }}</text>
|
||
<view v-else class="empty-click" @click="openNotes">
|
||
<text class="placeholder">添加备注</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 分组 -->
|
||
<view class="info-block">
|
||
<view class="block-header">
|
||
<text class="input-label">分组</text>
|
||
<view class="edit-icon" @click.stop="openGroups">
|
||
<uni-icons type="compose" size="20" color="#4f6ef7" />
|
||
</view>
|
||
</view>
|
||
<view class="block-content">
|
||
<view v-if="archiveGroupNames.length" class="tags-wrap">
|
||
<view v-for="tag in archiveGroupNames" :key="tag" class="tag-item">
|
||
{{ tag }}
|
||
</view>
|
||
</view>
|
||
<view v-else class="empty-click" @click="openGroups">
|
||
<text class="placeholder">添加分组</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="tabs">
|
||
<view
|
||
v-for="t in tabs"
|
||
:key="t.key"
|
||
class="tab"
|
||
:class="{ active: currentTab === t.key }"
|
||
@click="switchTab(t.key)"
|
||
>
|
||
{{ t.title }}
|
||
</view>
|
||
</view>
|
||
|
||
<view class="content">
|
||
<HealthProfileTab
|
||
v-if="currentTab === 'visitRecord'"
|
||
:data="archive"
|
||
:archiveId="archiveId"
|
||
:floatingBottom="floatingBottom"
|
||
/>
|
||
<ServiceInfoTab
|
||
v-else-if="currentTab === 'serviceRecord'"
|
||
:data="archive"
|
||
:archiveId="archiveId"
|
||
:reachBottomTime="reachBottomTime"
|
||
:floatingBottom="floatingBottom"
|
||
/>
|
||
<FollowUpManageTab
|
||
v-else
|
||
:data="archive"
|
||
:archiveId="archiveId"
|
||
:reachBottomTime="reachBottomTime"
|
||
:floatingBottom="floatingBottom"
|
||
/>
|
||
</view>
|
||
|
||
<view v-if="showBindWechat" class="footer">
|
||
<button class="bind-btn" @click="bindWechat">
|
||
<uni-icons type="email" size="18" color="#fff" />
|
||
<text class="bind-text">关联患者微信</text>
|
||
</button>
|
||
</view>
|
||
|
||
<!-- 修改联系电话(居中弹窗) -->
|
||
<uni-popup ref="contactPopup" type="center">
|
||
<view class="modal">
|
||
<view class="modal-title">修改联系电话</view>
|
||
<view class="modal-body">
|
||
<input class="modal-input" v-model="contactInput" type="number" placeholder="请输入联系电话" />
|
||
</view>
|
||
<view class="modal-actions">
|
||
<view class="modal-btn cancel" @click="closeContact">取消</view>
|
||
<view class="modal-btn save" @click="saveContact">保存</view>
|
||
</view>
|
||
</view>
|
||
</uni-popup>
|
||
|
||
<!-- 修改备注(底部弹层) -->
|
||
<uni-popup ref="notesPopup" type="bottom">
|
||
<view class="sheet">
|
||
<view class="sheet-header">
|
||
<view class="sheet-header-left" />
|
||
<view class="sheet-title">修改备注</view>
|
||
<view class="sheet-close" @click="closeNotes">
|
||
<uni-icons type="closeempty" size="18" color="#333" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="sheet-body">
|
||
<textarea
|
||
v-model="notesInput"
|
||
class="notes-textarea"
|
||
placeholder="请输入备注"
|
||
:maxlength="100"
|
||
/>
|
||
<view class="counter">{{ notesInput.length }}/100</view>
|
||
</view>
|
||
|
||
<view class="sheet-footer">
|
||
<button class="primary-btn" @click="saveNotes">保存</button>
|
||
</view>
|
||
</view>
|
||
</uni-popup>
|
||
|
||
<!-- 选择分组(底部弹层) -->
|
||
<uni-popup ref="groupPopup" type="bottom">
|
||
<view class="sheet">
|
||
<view class="sheet-header">
|
||
<view class="sheet-close" @click="closeGroups">
|
||
<uni-icons type="closeempty" size="18" color="#333" />
|
||
</view>
|
||
<view class="sheet-title">选择分组</view>
|
||
<view class="sheet-link" @click="openAddGroup">添加分组</view>
|
||
</view>
|
||
|
||
<view class="group-list">
|
||
<view v-for="g in teamGroups" :key="g._id" class="group-item" @click="toggleGroup(String(g._id))">
|
||
<uni-icons
|
||
:type="selectedGroupMap[String(g._id)] ? 'checkbox-filled' : 'checkbox'"
|
||
size="20"
|
||
:color="selectedGroupMap[String(g._id)] ? '#4f6ef7' : '#c7c7c7'"
|
||
/>
|
||
<text class="group-name">{{ g.groupName }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="sheet-footer">
|
||
<button class="primary-btn" @click="saveGroups">保存</button>
|
||
</view>
|
||
</view>
|
||
</uni-popup>
|
||
|
||
<!-- 添加新分组(居中弹窗) -->
|
||
<uni-popup ref="addGroupPopup" type="center">
|
||
<view class="modal">
|
||
<view class="modal-title">添加新分组</view>
|
||
<view class="modal-body">
|
||
<input class="modal-input" v-model="newGroupName" placeholder="请输入分组名称" />
|
||
</view>
|
||
<view class="modal-actions">
|
||
<view class="modal-btn cancel" @click="closeAddGroup">取消</view>
|
||
<view class="modal-btn save" @click="saveAddGroup">保存</view>
|
||
</view>
|
||
</view>
|
||
</uni-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, getCurrentInstance, nextTick, ref } from 'vue';
|
||
import { onLoad, onPullDownRefresh, onReachBottom, onReady, onShow } from '@dcloudio/uni-app';
|
||
import { storeToRefs } from 'pinia';
|
||
import dayjs from 'dayjs';
|
||
|
||
import HealthProfileTab from '@/components/archive-detail/health-profile-tab.vue';
|
||
import ServiceInfoTab from '@/components/archive-detail/service-info-tab.vue';
|
||
import FollowUpManageTab from '@/components/archive-detail/follow-up-manage-tab.vue';
|
||
import api from '@/utils/api';
|
||
import useAccountStore from '@/store/account';
|
||
import { hideLoading, loading, toast } from '@/utils/widget';
|
||
|
||
const 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 tabs = [
|
||
{ key: 'visitRecord', title: '病历记录' },
|
||
{ key: 'followUp', title: '回访任务' },
|
||
{ key: 'serviceRecord', title: '服务记录' }
|
||
];
|
||
|
||
const currentTab = ref('visitRecord');
|
||
const reachBottomTime = ref(0);
|
||
const archiveId = ref('');
|
||
const tabsScrollTop = ref(0);
|
||
const instanceProxy = getCurrentInstance()?.proxy;
|
||
|
||
function switchTab(key) {
|
||
currentTab.value = key;
|
||
// 切换 tab 后,将 tab 滚动到页面顶部(隐藏头部信息区域)
|
||
nextTick(() => {
|
||
// tabs 高度可能随数据变化,先测量一次再滚动
|
||
measureTabsTop();
|
||
setTimeout(() => {
|
||
uni.pageScrollTo({ scrollTop: tabsScrollTop.value || 0, duration: 0 });
|
||
}, 0);
|
||
});
|
||
}
|
||
|
||
const archive = ref({
|
||
name: '',
|
||
sex: '',
|
||
age: '',
|
||
avatar: '',
|
||
mobile: '',
|
||
outpatientNo: '',
|
||
inpatientNo: '',
|
||
medicalRecordNo: '',
|
||
createTime: '',
|
||
creator: '',
|
||
createdByDoctor: true,
|
||
hasBindWechat: false,
|
||
notes: '',
|
||
groupIds: []
|
||
});
|
||
|
||
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 normalizeArchiveFromApi(raw) {
|
||
const r = raw && typeof raw === 'object' ? raw : {};
|
||
const next = {
|
||
_id: r._id || archiveId.value,
|
||
name: r.name || '',
|
||
sex: r.sex || r.gender || '',
|
||
age: r.age ?? '',
|
||
avatar: r.avatar || '',
|
||
mobile: r.mobile || r.phone1 || r.phone || '',
|
||
outpatientNo: r.outpatientNo || '',
|
||
inpatientNo: r.inpatientNo || '',
|
||
medicalRecordNo: r.medicalRecordNo || '',
|
||
createTime: r.createTime || '',
|
||
creator: r.creator || '',
|
||
notes: r.notes || r.remark || '',
|
||
groupIds: Array.isArray(r.groupIds) ? r.groupIds : [],
|
||
};
|
||
return next;
|
||
}
|
||
|
||
async function ensureDoctor() {
|
||
if (doctorInfo.value) return;
|
||
if (!account.value?.openid) return;
|
||
try {
|
||
await getDoctorInfo();
|
||
} catch (e) {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
async function fetchArchive() {
|
||
if (!archiveId.value) return;
|
||
await ensureDoctor();
|
||
loading('加载中...');
|
||
try {
|
||
const res = await api('getCustomerByCustomerId', { customerId: archiveId.value });
|
||
if (!res?.success) {
|
||
toast(res?.message || '获取档案失败');
|
||
return;
|
||
}
|
||
archive.value = { ...archive.value, ...normalizeArchiveFromApi(res.data) };
|
||
saveToStorage();
|
||
loadTeamMembers();
|
||
await fetchTeamGroups(true);
|
||
// 档案信息刷新后,tabs 的位置可能变化,重新测量
|
||
nextTick(() => setTimeout(measureTabsTop, 30));
|
||
} catch (e) {
|
||
toast('获取档案失败');
|
||
} finally {
|
||
hideLoading();
|
||
}
|
||
}
|
||
|
||
const userNameMap = ref({});
|
||
async function loadTeamMembers() {
|
||
const corpId = getCorpId();
|
||
const team = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || {};
|
||
const teamId = team?.teamId ? String(team.teamId) : '';
|
||
if (!corpId || !teamId) return;
|
||
if (Object.keys(userNameMap.value || {}).length > 0) return;
|
||
|
||
const res = await api('getTeamData', { corpId, teamId });
|
||
if (!res?.success) return;
|
||
const t = res?.data && typeof res.data === 'object' ? res.data : {};
|
||
const members = Array.isArray(t.memberList) ? t.memberList : [];
|
||
userNameMap.value = members.reduce((acc, m) => {
|
||
const uid = String(m?.userid || '');
|
||
if (!uid) return acc;
|
||
acc[uid] = String(m?.anotherName || m?.name || m?.userid || '') || uid;
|
||
return acc;
|
||
}, {});
|
||
}
|
||
|
||
function resolveUserName(userId) {
|
||
const id = String(userId || '');
|
||
if (!id) return '';
|
||
const map = userNameMap.value || {};
|
||
return String(map[id] || '') || '';
|
||
}
|
||
|
||
function formatCreateTime(v) {
|
||
if (v === null || v === undefined) return '';
|
||
const raw = typeof v === 'number' ? v : typeof v === 'string' ? v.trim() : '';
|
||
const ms =
|
||
typeof raw === 'number'
|
||
? raw
|
||
: raw && /^\d{10,13}$/.test(raw)
|
||
? Number(raw.length === 10 ? `${raw}000` : raw)
|
||
: null;
|
||
const d = ms !== null ? dayjs(ms) : dayjs(raw);
|
||
return d.isValid() ? d.format('YYYY-MM-DD HH:mm') : '';
|
||
}
|
||
|
||
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;
|
||
await ensureDoctor();
|
||
const userId = getUserId();
|
||
const corpId = getCorpId();
|
||
const team = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || {};
|
||
const createTeamId = team?.teamId ? String(team.teamId) : '';
|
||
const createTeamName = team?.name ? String(team.name) : '';
|
||
if (!userId || !corpId) {
|
||
toast('缺少登录信息,请先完成登录');
|
||
return false;
|
||
}
|
||
|
||
loading('保存中...');
|
||
try {
|
||
const res = await api('updateCustomer', {
|
||
id,
|
||
params: patch,
|
||
userId,
|
||
corpId,
|
||
createTeamId,
|
||
createTeamName,
|
||
});
|
||
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();
|
||
}
|
||
}
|
||
|
||
onLoad((options) => {
|
||
archiveId.value = options?.id ? String(options.id) : String(archive.value.medicalRecordNo || archive.value.outpatientNo || archive.value.inpatientNo || '');
|
||
|
||
const cached = uni.getStorageSync(STORAGE_KEY);
|
||
if (cached && typeof cached === 'object') {
|
||
archive.value = {
|
||
...archive.value,
|
||
...cached,
|
||
groupIds: Array.isArray(cached.groupIds) ? cached.groupIds : archive.value.groupIds
|
||
};
|
||
}
|
||
|
||
if (!archive.value.mobile) {
|
||
const mobiles = cached && Array.isArray(cached.mobiles) ? cached.mobiles : [];
|
||
if (mobiles.length) archive.value.mobile = String(mobiles[0]);
|
||
}
|
||
|
||
if (!archiveId.value) {
|
||
archiveId.value = String(archive.value.medicalRecordNo || archive.value.outpatientNo || archive.value.inpatientNo || `mock_${Date.now()}`);
|
||
}
|
||
|
||
// 接口兜底:优先用接口刷新档案详情
|
||
fetchArchive();
|
||
});
|
||
|
||
function measureTabsTop() {
|
||
const q = instanceProxy ? uni.createSelectorQuery().in(instanceProxy) : uni.createSelectorQuery();
|
||
q.select('.tabs').boundingClientRect();
|
||
q.selectViewport().scrollOffset();
|
||
q.exec((res) => {
|
||
const rect = res && res[0];
|
||
const viewport = res && res[1];
|
||
if (!rect || !viewport) return;
|
||
tabsScrollTop.value = (rect.top || 0) + (viewport.scrollTop || 0);
|
||
});
|
||
}
|
||
|
||
onReady(() => {
|
||
setTimeout(measureTabsTop, 30);
|
||
});
|
||
|
||
onShow(() => {
|
||
const cached = uni.getStorageSync(STORAGE_KEY);
|
||
if (cached && typeof cached === 'object') {
|
||
archive.value = {
|
||
...archive.value,
|
||
...cached,
|
||
groupIds: Array.isArray(cached.groupIds) ? cached.groupIds : archive.value.groupIds,
|
||
};
|
||
}
|
||
setTimeout(measureTabsTop, 30);
|
||
fetchArchive();
|
||
});
|
||
|
||
onPullDownRefresh(() => {
|
||
uni.pageScrollTo({
|
||
scrollTop: tabsScrollTop.value || 0,
|
||
duration: 0,
|
||
});
|
||
setTimeout(() => uni.stopPullDownRefresh(), 150);
|
||
});
|
||
|
||
onReachBottom(() => {
|
||
reachBottomTime.value = Date.now();
|
||
});
|
||
|
||
const sexOrAge = computed(() => {
|
||
const sex = archive.value.sex ? String(archive.value.sex) : '';
|
||
const age = archive.value.age || archive.value.age === 0 ? `${archive.value.age}岁` : '';
|
||
if (sex && age) return `${sex} ${age}`;
|
||
return sex || age;
|
||
});
|
||
|
||
const idRows = computed(() => {
|
||
const rows = [];
|
||
if (archive.value.outpatientNo) rows.push({ label: '门诊号', value: String(archive.value.outpatientNo) });
|
||
if (archive.value.inpatientNo) rows.push({ label: '住院号', value: String(archive.value.inpatientNo) });
|
||
if (archive.value.medicalRecordNo) rows.push({ label: '病案号', value: String(archive.value.medicalRecordNo) });
|
||
return rows;
|
||
});
|
||
|
||
const createText = computed(() => {
|
||
const time = formatCreateTime(archive.value.createTime);
|
||
const rawCreator = archive.value.creator ? String(archive.value.creator) : '';
|
||
const creatorId = ['-', '—', '--'].includes(rawCreator.trim()) ? '' : rawCreator.trim();
|
||
const creatorName = creatorId ? resolveUserName(creatorId) : '';
|
||
if (time && creatorName) return `${time} ${creatorName}创建`;
|
||
if (time) return `${time} 创建`;
|
||
return '';
|
||
});
|
||
|
||
const showBindWechat = computed(() => Boolean(archive.value.createdByDoctor && !archive.value.hasBindWechat));
|
||
const floatingBottom = computed(() => (showBindWechat.value ? 90 : 16));
|
||
|
||
// const contactTitle = computed(() => (archive.value.mobile ? '联系方式' : '添加联系电话'));
|
||
// const notesTitle = computed(() => (archive.value.notes ? '备注' : '添加备注'));
|
||
// const groupTitle = computed(() => (Array.isArray(archive.value.groups) && archive.value.groups.length ? '分组' : '添加分组'));
|
||
|
||
const saveToStorage = () => {
|
||
uni.setStorageSync(STORAGE_KEY, { ...archive.value });
|
||
};
|
||
|
||
const goEdit = () => {
|
||
uni.navigateTo({ url: `/pages/case/archive-edit?archiveId=${encodeURIComponent(archiveId.value || '')}` });
|
||
};
|
||
|
||
const bindWechat = () => {
|
||
uni.showToast({ title: '关联患者微信功能待接入', icon: 'none' });
|
||
};
|
||
|
||
const makeCall = () => {
|
||
if (archive.value.mobile) {
|
||
uni.makePhoneCall({ phoneNumber: archive.value.mobile });
|
||
}
|
||
};
|
||
|
||
// ===== 联系电话 =====
|
||
const contactPopup = ref(null);
|
||
const contactInput = ref('');
|
||
|
||
const openContact = () => {
|
||
contactInput.value = archive.value.mobile ? String(archive.value.mobile) : '';
|
||
contactPopup.value?.open();
|
||
};
|
||
|
||
const closeContact = () => {
|
||
contactPopup.value?.close();
|
||
};
|
||
|
||
const saveContact = () => {
|
||
const v = String(contactInput.value || '').trim();
|
||
if (!v) {
|
||
uni.showToast({ title: '请输入联系电话', icon: 'none' });
|
||
return;
|
||
}
|
||
updateArchive({ mobile: v }).then((ok) => ok && closeContact());
|
||
};
|
||
|
||
// ===== 备注 =====
|
||
const notesPopup = ref(null);
|
||
const notesInput = ref('');
|
||
|
||
const openNotes = () => {
|
||
notesInput.value = archive.value.notes ? String(archive.value.notes) : '';
|
||
notesPopup.value?.open();
|
||
};
|
||
|
||
const closeNotes = () => {
|
||
notesPopup.value?.close();
|
||
};
|
||
|
||
const saveNotes = () => {
|
||
const v = String(notesInput.value || '').trim();
|
||
updateArchive({ notes: v }).then((ok) => ok && closeNotes());
|
||
};
|
||
|
||
// ===== 分组 =====
|
||
const groupPopup = ref(null);
|
||
const addGroupPopup = ref(null);
|
||
|
||
const selectedGroupMap = ref({});
|
||
|
||
const syncSelectedMapFromArchive = () => {
|
||
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 = async () => {
|
||
await fetchTeamGroups(false);
|
||
syncSelectedMapFromArchive();
|
||
groupPopup.value?.open();
|
||
};
|
||
|
||
const closeGroups = () => {
|
||
groupPopup.value?.close();
|
||
};
|
||
|
||
const toggleGroup = (groupId) => {
|
||
const key = String(groupId || '');
|
||
if (!key) return;
|
||
selectedGroupMap.value[key] = !selectedGroupMap.value[key];
|
||
};
|
||
|
||
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();
|
||
};
|
||
|
||
// 添加新分组
|
||
const newGroupName = ref('');
|
||
|
||
const openAddGroup = () => {
|
||
newGroupName.value = '';
|
||
addGroupPopup.value?.open();
|
||
};
|
||
|
||
const closeAddGroup = () => {
|
||
addGroupPopup.value?.close();
|
||
};
|
||
|
||
const saveAddGroup = async () => {
|
||
const name = String(newGroupName.value || '').trim();
|
||
if (!name) {
|
||
uni.showToast({ title: '请输入分组名称', icon: 'none' });
|
||
return;
|
||
}
|
||
const exists = (Array.isArray(teamGroups.value) ? teamGroups.value : []).some((g) => String(g?.groupName || '').trim() === name);
|
||
if (exists) {
|
||
uni.showToast({ title: '该分组已存在', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
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>
|
||
|
||
<style lang="scss" scoped>
|
||
.page {
|
||
min-height: 100vh;
|
||
background: #f5f6f8;
|
||
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.card {
|
||
background: #fff;
|
||
}
|
||
|
||
.header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
padding: 28rpx 28rpx 20rpx;
|
||
border-bottom: 2rpx solid #f2f2f2;
|
||
}
|
||
|
||
.avatar {
|
||
width: 112rpx;
|
||
height: 112rpx;
|
||
border-radius: 12rpx;
|
||
border: 2rpx solid #e8e8e8;
|
||
background: #fafafa;
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.avatar-img {
|
||
width: 112rpx;
|
||
height: 112rpx;
|
||
}
|
||
|
||
.header-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
padding: 0 20rpx;
|
||
}
|
||
|
||
.name-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
padding-top: 4rpx;
|
||
}
|
||
|
||
.name {
|
||
font-size: 36rpx;
|
||
font-weight: 600;
|
||
color: #1f1f1f;
|
||
max-width: 440rpx;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.meta {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.sub-line {
|
||
margin-top: 12rpx;
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.id-rows {
|
||
margin-top: 12rpx;
|
||
}
|
||
|
||
.id-row {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
line-height: 36rpx;
|
||
}
|
||
|
||
.id-label {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.id-value {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.create-row {
|
||
margin-top: 12rpx;
|
||
}
|
||
|
||
.create-text {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.header-right {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
margin-top: 12rpx;
|
||
}
|
||
|
||
.cells {
|
||
background: #fff;
|
||
padding: 0 28rpx;
|
||
}
|
||
|
||
.border-bottom {
|
||
border-bottom: 2rpx solid #f2f2f2;
|
||
}
|
||
|
||
.info-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 32rpx 0;
|
||
min-height: 48rpx;
|
||
}
|
||
|
||
.info-block {
|
||
padding: 32rpx 0;
|
||
}
|
||
|
||
.block-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.block-content {
|
||
min-height: 40rpx;
|
||
}
|
||
|
||
.input-label {
|
||
font-size: 30rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.info-right {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.phone-area {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-right: 24rpx;
|
||
}
|
||
|
||
.mr-4 {
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
.phone-text {
|
||
font-size: 32rpx;
|
||
color: #4f6ef7;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.edit-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
}
|
||
|
||
.placeholder {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.note-content {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
line-height: 1.5;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.tags-wrap {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.tag-item {
|
||
height: 48rpx;
|
||
line-height: 44rpx;
|
||
padding: 0 20rpx;
|
||
border: 2rpx solid #4f6ef7;
|
||
border-radius: 24rpx;
|
||
font-size: 24rpx;
|
||
color: #4f6ef7;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.empty-click {
|
||
/* To ensure empty area is clickable */
|
||
display: inline-block;
|
||
}
|
||
|
||
.tabs {
|
||
margin-top: 20rpx;
|
||
background: #fff;
|
||
display: flex;
|
||
border-top: 2rpx solid #f2f2f2;
|
||
border-bottom: 2rpx solid #f2f2f2;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 30;
|
||
}
|
||
|
||
.tab {
|
||
flex: 1;
|
||
text-align: center;
|
||
height: 88rpx;
|
||
line-height: 88rpx;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
position: relative;
|
||
}
|
||
|
||
.tab.active {
|
||
color: #4f6ef7;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.tab.active::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 50%;
|
||
bottom: 0;
|
||
width: 64rpx;
|
||
height: 6rpx;
|
||
background: #4f6ef7;
|
||
transform: translateX(-50%);
|
||
border-radius: 4rpx;
|
||
}
|
||
|
||
.content {
|
||
padding: 0;
|
||
}
|
||
|
||
.empty {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.empty-img {
|
||
width: 320rpx;
|
||
height: 320rpx;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.empty-text {
|
||
margin-top: 20rpx;
|
||
font-size: 26rpx;
|
||
color: #9aa0a6;
|
||
}
|
||
|
||
.footer {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: #fff;
|
||
padding: 24rpx 28rpx calc(24rpx + env(safe-area-inset-bottom));
|
||
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.bind-btn {
|
||
width: 100%;
|
||
height: 88rpx;
|
||
background: #4f6ef7;
|
||
color: #fff;
|
||
border-radius: 12rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 16rpx;
|
||
font-size: 30rpx;
|
||
}
|
||
|
||
.bind-text {
|
||
color: #fff;
|
||
}
|
||
|
||
/* ===== 弹窗样式(居中) ===== */
|
||
.modal {
|
||
width: 640rpx;
|
||
background: #fff;
|
||
border-radius: 16rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
text-align: center;
|
||
padding: 28rpx 24rpx;
|
||
color: #333;
|
||
border-bottom: 2rpx solid #f0f0f0;
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 28rpx 28rpx 16rpx;
|
||
}
|
||
|
||
.modal-input {
|
||
width: 100%;
|
||
height: 80rpx;
|
||
border: 2rpx solid #e6e6e6;
|
||
border-radius: 8rpx;
|
||
padding: 0 20rpx;
|
||
font-size: 28rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.modal-actions {
|
||
display: flex;
|
||
gap: 24rpx;
|
||
padding: 24rpx 28rpx 28rpx;
|
||
}
|
||
|
||
.modal-btn {
|
||
flex: 1;
|
||
height: 80rpx;
|
||
line-height: 80rpx;
|
||
text-align: center;
|
||
border-radius: 8rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.modal-btn.cancel {
|
||
border: 2rpx solid #4f6ef7;
|
||
color: #4f6ef7;
|
||
background: #fff;
|
||
}
|
||
|
||
.modal-btn.save {
|
||
background: #4f6ef7;
|
||
color: #fff;
|
||
}
|
||
|
||
/* ===== 底部弹层样式 ===== */
|
||
.sheet {
|
||
background: #fff;
|
||
border-top-left-radius: 20rpx;
|
||
border-top-right-radius: 20rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.sheet-header {
|
||
height: 96rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 28rpx;
|
||
border-bottom: 2rpx solid #f0f0f0;
|
||
}
|
||
|
||
.sheet-title {
|
||
flex: 1;
|
||
text-align: center;
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.sheet-close {
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.sheet-header-left {
|
||
width: 48rpx;
|
||
}
|
||
|
||
.sheet-link {
|
||
min-width: 120rpx;
|
||
text-align: right;
|
||
font-size: 28rpx;
|
||
color: #4f6ef7;
|
||
}
|
||
|
||
.sheet-body {
|
||
padding: 28rpx;
|
||
}
|
||
|
||
.notes-textarea {
|
||
width: 100%;
|
||
height: 280rpx;
|
||
border: 2rpx solid #e6e6e6;
|
||
border-radius: 8rpx;
|
||
padding: 20rpx;
|
||
font-size: 28rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.counter {
|
||
text-align: right;
|
||
margin-top: 16rpx;
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.group-list {
|
||
padding: 16rpx 28rpx 28rpx;
|
||
max-height: 55vh;
|
||
overflow: auto;
|
||
}
|
||
|
||
.group-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 24rpx 0;
|
||
}
|
||
|
||
.group-name {
|
||
margin-left: 20rpx;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
|
||
.sheet-footer {
|
||
padding: 24rpx 28rpx calc(24rpx + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.primary-btn {
|
||
width: 100%;
|
||
height: 88rpx;
|
||
background: #4f6ef7;
|
||
color: #fff;
|
||
border-radius: 8rpx;
|
||
font-size: 30rpx;
|
||
line-height: 88rpx;
|
||
}
|
||
</style>
|