ykt-wxapp/pages/case/archive-detail.vue
2026-01-28 20:01:28 +08:00

1211 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>