ykt-wxapp/pages/case/archive-detail.vue

1211 lines
30 KiB
Vue
Raw Normal View History

<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 }"
2026-01-27 16:46:36 +08:00
@click="switchTab(t.key)"
>
{{ t.title }}
</view>
</view>
<view class="content">
2026-01-22 15:54:15 +08:00
<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>
2026-01-27 17:38:40 +08:00
import { computed, getCurrentInstance, nextTick, ref } from 'vue';
2026-01-22 17:39:23 +08:00
import { onLoad, onPullDownRefresh, onReachBottom, onReady, onShow } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
2026-01-27 17:38:40 +08:00
import dayjs from 'dayjs';
2026-01-22 15:54:15 +08:00
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');
2026-01-22 15:54:15 +08:00
const reachBottomTime = ref(0);
const archiveId = ref('');
2026-01-22 17:39:23 +08:00
const tabsScrollTop = ref(0);
const instanceProxy = getCurrentInstance()?.proxy;
2026-01-27 16:46:36 +08:00
function switchTab(key) {
currentTab.value = key;
2026-01-27 17:38:40 +08:00
// 切换 tab 后,将 tab 滚动到页面顶部(隐藏头部信息区域)
nextTick(() => {
// tabs 高度可能随数据变化,先测量一次再滚动
measureTabsTop();
setTimeout(() => {
uni.pageScrollTo({ scrollTop: tabsScrollTop.value || 0, duration: 0 });
}, 0);
});
2026-01-27 16:46:36 +08:00
}
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();
2026-01-27 17:38:40 +08:00
loadTeamMembers();
await fetchTeamGroups(true);
2026-01-27 17:38:40 +08:00
// 档案信息刷新后tabs 的位置可能变化,重新测量
nextTick(() => setTimeout(measureTabsTop, 30));
} catch (e) {
toast('获取档案失败');
} finally {
hideLoading();
}
}
2026-01-27 17:38:40 +08:00
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();
}
}
2026-01-22 15:54:15 +08:00
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]);
}
2026-01-22 15:54:15 +08:00
if (!archiveId.value) {
archiveId.value = String(archive.value.medicalRecordNo || archive.value.outpatientNo || archive.value.inpatientNo || `mock_${Date.now()}`);
}
// 接口兜底:优先用接口刷新档案详情
fetchArchive();
2026-01-22 15:54:15 +08:00
});
2026-01-22 17:39:23 +08:00
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);
});
2026-01-22 15:54:15 +08:00
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,
2026-01-22 15:54:15 +08:00
};
}
2026-01-22 17:39:23 +08:00
setTimeout(measureTabsTop, 30);
fetchArchive();
2026-01-22 17:39:23 +08:00
});
onPullDownRefresh(() => {
uni.pageScrollTo({
scrollTop: tabsScrollTop.value || 0,
duration: 0,
});
setTimeout(() => uni.stopPullDownRefresh(), 150);
2026-01-22 15:54:15 +08:00
});
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(() => {
2026-01-27 17:38:40 +08:00
const time = formatCreateTime(archive.value.createTime);
2026-01-27 16:46:36 +08:00
const rawCreator = archive.value.creator ? String(archive.value.creator) : '';
2026-01-27 17:38:40 +08:00
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));
2026-01-22 15:54:15 +08:00
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 = () => {
2026-01-22 15:54:15 +08:00
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(80px + env(safe-area-inset-bottom));
}
.card {
background: #fff;
}
.header {
display: flex;
align-items: flex-start;
padding: 14px 14px 10px;
border-bottom: 1px solid #f2f2f2;
}
.avatar {
width: 56px;
height: 56px;
border-radius: 6px;
border: 1px solid #e8e8e8;
background: #fafafa;
overflow: hidden;
flex-shrink: 0;
}
.avatar-img {
width: 56px;
height: 56px;
}
.header-main {
flex: 1;
min-width: 0;
padding: 0 10px;
}
.name-row {
display: flex;
align-items: center;
gap: 8px;
padding-top: 2px;
}
.name {
font-size: 18px;
font-weight: 600;
color: #1f1f1f;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.meta {
font-size: 13px;
color: #666;
}
.sub-line {
margin-top: 6px;
font-size: 12px;
color: #666;
}
.id-rows {
margin-top: 6px;
}
.id-row {
display: flex;
align-items: center;
font-size: 12px;
color: #666;
line-height: 18px;
}
.id-label {
flex-shrink: 0;
}
.id-value {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.create-row {
margin-top: 6px;
}
.create-text {
font-size: 12px;
color: #999;
}
.header-right {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 6px;
}
.cells {
background: #fff;
padding: 0 14px;
}
.border-bottom {
border-bottom: 1px solid #f2f2f2;
}
.info-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0;
min-height: 24px;
}
.info-block {
padding: 16px 0;
}
.block-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.block-content {
min-height: 20px;
}
.input-label {
font-size: 15px;
color: #666;
}
.info-right {
display: flex;
align-items: center;
}
.phone-area {
display: flex;
align-items: center;
margin-right: 12px;
}
.mr-4 {
margin-right: 4px;
}
.phone-text {
font-size: 16px;
color: #4f6ef7;
font-weight: 500;
}
.edit-icon {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
}
.placeholder {
font-size: 14px;
color: #999;
}
.note-content {
font-size: 14px;
color: #333;
line-height: 1.5;
word-break: break-all;
}
.tags-wrap {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag-item {
height: 24px;
line-height: 22px;
padding: 0 10px;
border: 1px solid #4f6ef7;
border-radius: 12px;
font-size: 12px;
color: #4f6ef7;
box-sizing: border-box;
}
.empty-click {
/* To ensure empty area is clickable */
display: inline-block;
}
.tabs {
margin-top: 10px;
background: #fff;
display: flex;
border-top: 1px solid #f2f2f2;
border-bottom: 1px solid #f2f2f2;
2026-01-22 17:39:23 +08:00
position: sticky;
top: 0;
z-index: 30;
}
.tab {
flex: 1;
text-align: center;
height: 44px;
line-height: 44px;
font-size: 14px;
color: #333;
position: relative;
}
.tab.active {
color: #4f6ef7;
font-weight: 600;
}
.tab.active::after {
content: '';
position: absolute;
left: 50%;
bottom: 0;
width: 32px;
height: 3px;
background: #4f6ef7;
transform: translateX(-50%);
border-radius: 2px;
}
.content {
2026-01-22 15:54:15 +08:00
padding: 0;
}
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.empty-img {
width: 160px;
height: 160px;
opacity: 0.9;
}
.empty-text {
margin-top: 10px;
font-size: 13px;
color: #9aa0a6;
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
}
.bind-btn {
width: 100%;
height: 44px;
background: #4f6ef7;
color: #fff;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 15px;
}
.bind-text {
color: #fff;
}
/* ===== 弹窗样式(居中) ===== */
.modal {
width: 320px;
background: #fff;
border-radius: 8px;
overflow: hidden;
}
.modal-title {
font-size: 16px;
font-weight: 600;
text-align: center;
padding: 14px 12px;
color: #333;
border-bottom: 1px solid #f0f0f0;
}
.modal-body {
padding: 14px 14px 8px;
}
.modal-input {
width: 100%;
height: 40px;
border: 1px solid #e6e6e6;
border-radius: 4px;
padding: 0 10px;
font-size: 14px;
box-sizing: border-box;
}
.modal-actions {
display: flex;
gap: 12px;
padding: 12px 14px 14px;
}
.modal-btn {
flex: 1;
height: 40px;
line-height: 40px;
text-align: center;
border-radius: 4px;
font-size: 14px;
}
.modal-btn.cancel {
border: 1px solid #4f6ef7;
color: #4f6ef7;
background: #fff;
}
.modal-btn.save {
background: #4f6ef7;
color: #fff;
}
/* ===== 底部弹层样式 ===== */
.sheet {
background: #fff;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
overflow: hidden;
}
.sheet-header {
height: 48px;
display: flex;
align-items: center;
padding: 0 14px;
border-bottom: 1px solid #f0f0f0;
}
.sheet-title {
flex: 1;
text-align: center;
font-size: 16px;
font-weight: 600;
color: #333;
}
.sheet-close {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.sheet-header-left {
width: 24px;
}
.sheet-link {
min-width: 60px;
text-align: right;
font-size: 14px;
color: #4f6ef7;
}
.sheet-body {
padding: 14px;
}
.notes-textarea {
width: 100%;
height: 140px;
border: 1px solid #e6e6e6;
border-radius: 4px;
padding: 10px;
font-size: 14px;
box-sizing: border-box;
}
.counter {
text-align: right;
margin-top: 8px;
font-size: 12px;
color: #999;
}
.group-list {
padding: 8px 14px 14px;
max-height: 55vh;
overflow: auto;
}
.group-item {
display: flex;
align-items: center;
padding: 12px 0;
}
.group-name {
margin-left: 10px;
font-size: 14px;
color: #333;
}
.sheet-footer {
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
}
.primary-btn {
width: 100%;
height: 44px;
background: #4f6ef7;
color: #fff;
border-radius: 4px;
font-size: 15px;
line-height: 44px;
}
</style>