Files
xk-doctor-wx/subPackages/sub_business_shared/components/UserPatientDetail.vue

868 lines
27 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>
<!-- 患者视角详情page-container 抽屉 v-if 控制挂载 -->
<!-- 微信同一页只能有一个 page-container打开回访表单前必须先卸掉本实例再挂 CallLogDrawer -->
<page-container
v-if="visible && !patientSheetHiddenForCallLog"
:show="true"
:overlay="true"
position="bottom"
:round="true"
@beforeleave="onBeforeLeave"
@clickoverlay="onClickOverlay"
>
<view class="drawer">
<view class="drawer-head">
<text class="title">患者视角 · {{ patientName || '就诊人' }}</text>
<text class="close" @click="close">关闭</text>
</view>
<view v-if="loading" class="empty">加载中...</view>
<template v-else>
<!-- 资料卡对齐就诊人详情页 -->
<view class="profile-card">
<view class="profile-row">
<image
class="avatar"
:src="profile.user.avatarurl || '/static/image/default-avatar.png'"
mode="aspectFill"
/>
<view class="meta">
<text class="name">{{ profile.user.nickname || '微信用户' }}</text>
<text class="sub">
ID {{ profile.user.id || '—' }}
<template v-if="profile.user.mobile"> · {{ profile.user.mobile }}</template>
</text>
</view>
</view>
<view class="patient-block">
<view class="patient-head">
<view class="patient-head-left">
<text class="p-name">{{ profile.patient.name || '—' }}</text>
<text v-if="sexText !== '—'" class="sex-tag" :class="sexTagClass">{{ sexText }}</text>
</view>
<view class="patient-actions">
<!-- 回访拨打就诊人/用户手机 -->
<view class="callback-btn" @click.stop="onCallbackCall">
<text class="callback-btn-text">回访</text>
</view>
<!-- 记录本次回访手动打开回访记录表单抽屉 -->
<view class="callback-link" @click.stop="openCallLogForm">
<text class="callback-link-text">记录本次回访</text>
</view>
</view>
</view>
<text class="p-sub">
{{ profile.patient.age || '—' }}
<template v-if="profile.patient.mobile"> · {{ profile.patient.mobile }}</template>
</text>
<text v-if="profile.patient.id_card" class="p-sub">身份证 {{ profile.patient.id_card }}</text>
</view>
</view>
<!-- Tab下划线选中态 -->
<view class="tabs">
<view
v-for="(t, idx) in tabs"
:key="t.key"
class="tab"
:class="{ on: tabIndex === idx }"
@click="selectTab(idx)"
>
<text class="tab-text">{{ t.label }}</text>
<view v-if="tabIndex === idx" class="tab-bar" />
</view>
</view>
<!-- 五页左右滑资料 / 回访 / 挂号 / 处方 / 订单 -->
<swiper
class="tab-swiper"
:style="{ height: swiperHeightPx + 'px' }"
:current="tabIndex"
:duration="200"
@change="onTabSwiperChange"
>
<swiper-item v-for="t in tabs" :key="t.key">
<scroll-view
scroll-y
class="tab-scroll"
:style="{ height: swiperHeightPx + 'px' }"
>
<!-- 资料不绑 tabLoading避免切列表时闪加载中 -->
<view v-if="t.key === 'info'" class="health-card">
<text class="section-title">健康信息</text>
<template v-if="health">
<view class="info-row">
<text class="info-label">既往史</text>
<text class="info-value">{{ historyText(health.person_status, health.person_history) }}</text>
</view>
<view class="info-row">
<text class="info-label">过敏史</text>
<text
class="info-value"
:class="{ danger: Number(health.allergic_status) !== 0 }"
>{{ historyText(health.allergic_status, health.allergic_history) }}</text>
</view>
<view class="info-row">
<text class="info-label">家族遗传史</text>
<text class="info-value">{{ historyText(health.family_status, health.family_history) }}</text>
</view>
<view class="info-row">
<text class="info-label">肝功能异常</text>
<text
class="info-value"
:class="{ danger: Number(health.liver_function) !== 0 }"
>{{ functionText(health.liver_function, health.liver_index) }}</text>
</view>
<view class="info-row">
<text class="info-label">肾功能异常</text>
<text
class="info-value"
:class="{ danger: Number(health.renal_function) !== 0 }"
>{{ functionText(health.renal_function, health.renal_index) }}</text>
</view>
</template>
<view v-else class="empty-inline">暂无健康问诊记录</view>
</view>
<!-- 回访历史回访记录列表懒加载 -->
<view v-else-if="t.key === 'calllog'" class="call-log-card in-tab">
<view class="call-log-head">
<text class="call-log-head-title">回访记录</text>
<text v-if="callLogList.length" class="call-log-head-count">{{ callLogTotal }} </text>
</view>
<view v-if="callLogLoading" class="empty-inline">加载中...</view>
<view v-else-if="!callLogList.length" class="empty-inline">暂无回访记录</view>
<view v-else>
<view v-for="log in callLogList" :key="log.id" class="call-log-item">
<view class="call-log-item-head">
<text
class="call-log-result-tag"
:style="{ color: log.result_color || '#6b7280', background: log.result_bg, borderColor: log.result_color || '#e5e7eb' }"
>{{ log.result_text }}</text>
<text class="call-log-item-time">{{ log.call_at }}</text>
</view>
<view v-if="log.tags.length" class="call-log-tags">
<text
v-for="tg in log.tags"
:key="tg.value"
class="call-log-tag-text"
:style="{ color: tg.color || '#6b7280' }"
>#{{ tg.name }}</text>
</view>
<text v-if="log.conclusion" class="call-log-line">结论{{ log.conclusion }}</text>
<text v-if="log.remark" class="call-log-line">备注{{ log.remark }}</text>
<text v-if="log.next_at" class="call-log-line call-log-next">下次回访{{ log.next_at_text }}</text>
</view>
</view>
</view>
<template v-else>
<view v-if="tabLoading && activeTab === t.key" class="empty">加载中...</view>
<template v-else>
<view v-if="!listForTab(t.key).length" class="empty">暂无记录</view>
<view
v-for="item in listForTab(t.key)"
:key="item.id"
class="record-card"
@click="onRecordClick(item, t.key)"
>
<view class="record-main">
<template v-if="t.key === 'register'">
<text class="r-title">{{ item.order_no || item.order_number || '—' }}</text>
<text class="r-sub">{{ item.store || '—' }} · {{ item.doctor || '—' }}</text>
<text class="r-sub">{{ item.status_text || '—' }} · {{ item.created_at || '' }}</text>
</template>
<template v-else-if="t.key === 'prescription'">
<text class="r-title">{{ item.prescription_no || '—' }}</text>
<text class="r-sub">{{ item.store || '—' }} · {{ item.doctor || '—' }}</text>
<text class="r-sub">{{ item.clinical_diagnose || '—' }} · {{ item.created_at || '' }}</text>
</template>
<template v-else>
<text class="r-title">{{ item.order_no || '—' }}</text>
<text class="r-sub">{{ item.store || '—' }} · ¥{{ item.total_pay_price || '—' }}</text>
<text class="r-sub">{{ item.created_at || '' }}</text>
</template>
</view>
<text v-if="t.key === 'register'" class="arrow"></text>
</view>
</template>
</template>
</scroll-view>
</swiper-item>
</swiper>
</template>
</view>
</page-container>
<!-- 回访记录表单抽屉独立 page-container避免与详情抽屉嵌套 -->
<CallLogDrawer
v-if="callLogDrawerVisible"
:visible="callLogDrawerVisible"
:up-id="upId"
:prefill="callLogPrefill"
@update:visible="onCallLogDrawerVisibleChange"
@close="onCallLogDrawerClose"
@success="onCallLogSuccess"
/>
</view>
</template>
<script>
/**
* 患者视角详情抽屉:微信用户/就诊人信息 + 挂号/处方/订单
* 对齐详情页展示与回访;五 Tab 支持点击与左右滑动(懒加载)
*/
import {
getUserPatientDetail,
getUserPatientRegisterList,
getUserPatientPrescriptionList,
getUserPatientOrderList,
getPatientCallLogList,
} from '@/api/userPatientProfile.js';
import CallLogDrawer from './CallLogDrawer.vue';
/** 抽屉左右滑提示只展示一次 */
const SWIPE_TIP_STORAGE_KEY = 'user_patient_drawer_swipe_tip';
/** 拨号返回事件名:与 App.vue 中的 uni.$emit 配合使用 */
const CALLBACK_RETURNED_EVENT = 'patient:callbackReturned';
/** pendingCallback 存储键写入拨号上下文App.onShow 读取后广播 */
const PENDING_CALLBACK_KEY = 'pendingCallback';
/** 拨号返回判定时间窗5 分钟内的 onAppShow 才认为是本次拨号的返回 */
const CALLBACK_RETURN_WINDOW_MS = 5 * 60 * 1000;
/** 回访标签 code -> 中文,用于列表展示 */
const TAG_LABEL_MAP = {
care: '关怀',
condition: '病情',
medication: '用药提醒',
review: '复诊提醒',
complaint: '投诉处理',
};
export default {
name: 'UserPatientDetail',
components: { CallLogDrawer },
props: {
visible: { type: Boolean, default: false },
upId: { type: [Number, String], default: 0 },
patientName: { type: String, default: '' },
},
emits: ['close'],
data() {
return {
loading: false,
tabLoading: false,
activeTab: 'info',
tabIndex: 0,
tabSwiperChanging: false,
swiperHeightPx: 320,
tabs: [
{ key: 'info', label: '资料' },
{ key: 'calllog', label: '回访' },
{ key: 'register', label: '挂号' },
{ key: 'prescription', label: '处方' },
{ key: 'order', label: '订单' },
],
profile: {
user: {},
patient: {},
},
health: null,
registerList: [],
prescriptionList: [],
orderList: [],
// 回访记录相关状态
callLogList: [],
callLogTotal: 0,
callLogLoading: false,
callLogDrawerVisible: false,
// 传给 CallLogDrawer 的预填字段(拨号后自动填 phone + call_at
callLogPrefill: {},
/**
* 打开回访表单时同步置 true拦截外层卸载触发的 beforeleave/clickoverlay
* 避免误 emit close 导致父页卸载本组件
*/
suppressPatientClose: false,
/**
* true 时卸载外层 page-container尚未或不再挂 CallLogDrawer
* 与 callLogDrawerVisible 配合两阶段互斥,保证页面上最多一个 page-container
*/
patientSheetHiddenForCallLog: false,
};
},
computed: {
sexText() {
const sex = this.profile.patient && this.profile.patient.sex;
if (Number(sex) === 1) return '男';
if (Number(sex) === 2) return '女';
return '—';
},
sexTagClass() {
const sex = this.profile.patient && this.profile.patient.sex;
if (Number(sex) === 1) return 'male';
if (Number(sex) === 2) return 'female';
return '';
},
/**
* 回访拨号:只用就诊人 call_mobile拦截器只解密不脱敏
* 不用脱敏后的 mobile也不回退微信用户号
*/
callbackPhone() {
const patient = this.profile.patient || {};
return String(patient.call_mobile || '').trim();
},
},
watch: {
/**
* v-if 挂载时 visible 已是 true必须 immediate否则 loadDetail 永不触发
*/
visible: {
immediate: true,
handler(val) {
if (val && this.upId) {
this.openAndLoad();
}
// 抽屉挂载时注册"拨号返回"监听;卸载时取消
if (val) {
uni.$on(CALLBACK_RETURNED_EVENT, this.handleCallbackReturned);
} else {
uni.$off(CALLBACK_RETURNED_EVENT, this.handleCallbackReturned);
}
},
},
/** 抽屉打开时切换不同就诊人,重新拉详情 */
upId(val) {
if (this.visible && val) {
this.openAndLoad();
}
},
},
/** 兜底:组件销毁前移除事件监听,避免重复触发 */
beforeDestroy() {
uni.$off(CALLBACK_RETURNED_EVENT, this.handleCallbackReturned);
},
methods: {
close() {
this.$emit('close');
},
/**
* 遮罩点击:为打开回访而卸载本抽屉时勿关
*/
onClickOverlay() {
if (this.suppressPatientClose || this.callLogDrawerVisible || this.patientSheetHiddenForCallLog) return;
this.close();
},
/**
* page-container 即将关闭:为打开回访而卸载本抽屉时勿当成用户关闭
*/
onBeforeLeave() {
if (this.suppressPatientClose || this.callLogDrawerVisible || this.patientSheetHiddenForCallLog) return;
this.close();
},
/**
* 打开抽屉:重置 Tab/列表并请求就诊人详情
*/
openAndLoad() {
this.activeTab = 'info';
this.tabIndex = 0;
this.health = null;
this.registerList = [];
this.prescriptionList = [];
this.orderList = [];
this.callLogList = [];
this.callLogTotal = 0;
this.callLogDrawerVisible = false;
this.callLogPrefill = {};
this.suppressPatientClose = false;
this.patientSheetHiddenForCallLog = false;
this.profile = { user: {}, patient: {} };
this.calcSwiperHeight();
this.loadDetail();
// 回访列表改为切到「回访」Tab 时懒加载,不再一进页就拉
},
/**
* 抽屉内 swiper 高度80vh 资料卡 Tab保证横向滑动可用
*/
calcSwiperHeight() {
try {
const sys = uni.getSystemInfoSync() || {};
const winH = Number(sys.windowHeight) || 600;
const drawerH = Math.floor(winH * 0.8);
const rpx = (Number(sys.windowWidth) || 375) / 750;
const headH = Math.ceil(100 * rpx);
const profileH = Math.ceil(280 * rpx);
const tabsH = Math.ceil(90 * rpx);
const pad = Math.ceil(24 * rpx);
const h = drawerH - headH - profileH - tabsH - pad;
this.swiperHeightPx = h > 160 ? h : 160;
} catch (e) {
this.swiperHeightPx = 320;
}
},
/** 按 Tab key 取对应列表swiper 多页同时渲染时不能只用 activeTab */
listForTab(key) {
if (key === 'register') return this.registerList;
if (key === 'prescription') return this.prescriptionList;
if (key === 'order') return this.orderList;
return [];
},
/** 既往/过敏/家族0 无,否则展示 history */
historyText(status, history) {
if (Number(status) === 0) return '无';
const t = String(history || '').trim();
return t || '有';
},
/** 肝/肾功能0 正常,异常时附带指标文案 */
functionText(flag, indexText) {
if (Number(flag) === 0) return '正常';
const t = String(indexText || '').trim();
return t ? `异常 · ${t}` : '异常';
},
/**
* 回访:调起系统拨号;无号 toast按钮始终显示
* 同时把拨号上下文写入 storageApp.onShow 检测到返回时
* 通过 uni.$emit 广播事件,触发本组件自动打开回访表单抽屉
*/
onCallbackCall() {
const phone = this.callbackPhone;
if (!phone) {
uni.showToast({ title: '暂无手机号', icon: 'none' });
return;
}
try {
uni.setStorageSync(PENDING_CALLBACK_KEY, {
upId: Number(this.upId),
phone,
ts: Date.now(),
});
} catch (e) { /* ignore */ }
uni.makePhoneCall({
phoneNumber: phone,
});
},
/**
* App.onShow 广播的"拨号返回"事件处理
* 校验是当前就诊人后,两阶段打开回访表单(先卸外层再挂表单)
*/
handleCallbackReturned(payload) {
if (!this.visible) return;
if (!payload || Number(payload.upId) !== Number(this.upId)) return;
this.suppressPatientClose = true;
this.callLogPrefill = {
phone: payload.phone || '',
call_at: Math.floor((payload.ts || Date.now()) / 1000),
};
this.beginOpenCallLogDrawer();
},
/** 手动点击"记录本次回访":两阶段打开,避免双 page-container */
openCallLogForm() {
this.suppressPatientClose = true;
this.callLogPrefill = {
phone: this.callbackPhone || '',
call_at: Math.floor(Date.now() / 1000),
};
this.beginOpenCallLogDrawer();
},
/**
* 两阶段打开回访表单:先卸外层 page-containernextTick 后再挂 CallLogDrawer
* 微信同一页只能存在一个 page-container 实例
*/
beginOpenCallLogDrawer() {
this.patientSheetHiddenForCallLog = true;
this.$nextTick(() => {
this.callLogDrawerVisible = true;
});
},
/**
* 关闭回访表单后恢复就诊人抽屉:先卸 CallLogDrawernextTick 再挂外层
*/
restorePatientSheetAfterCallLog() {
this.callLogDrawerVisible = false;
this.$nextTick(() => {
this.patientSheetHiddenForCallLog = false;
this.suppressPatientClose = false;
});
},
/** CallLogDrawer 通过 .sync 更新 visible */
onCallLogDrawerVisibleChange(val) {
if (val) {
this.callLogDrawerVisible = true;
return;
}
this.restorePatientSheetAfterCallLog();
},
/** 抽屉关闭 */
onCallLogDrawerClose() {
this.restorePatientSheetAfterCallLog();
},
/** 提交成功回调刷新列表并切到「回访」Tab方便看到刚写入的记录 */
onCallLogSuccess() {
this.loadCallLogList();
const idx = this.tabs.findIndex((t) => t.key === 'calllog');
if (idx >= 0) {
this.selectTab(idx);
}
},
/** 拉取就诊人回访记录列表 */
async loadCallLogList() {
if (!this.upId) return;
this.callLogLoading = true;
try {
const wrap = await getPatientCallLogList(this.upId);
const body = wrap && wrap.res ? wrap.res : null;
const list = (body && body.result && Array.isArray(body.result.items)) ? body.result.items : [];
// 预算每条记录的 result 浅色背景,避免模板 :style 调函数(小程序禁止)
this.callLogList = list.map((log) => Object.assign({}, log, {
result_bg: this.hexToRgba(log.result_color || '#6b7280', 0.12),
}));
this.callLogTotal = (body && body.result && Number(body.result.total)) || list.length;
} catch (e) {
this.callLogList = [];
this.callLogTotal = 0;
} finally {
this.callLogLoading = false;
}
},
/** hex -> rgba用于结果标签浅色背景 */
hexToRgba(hex, alpha) {
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex || '');
if (!m) return `rgba(106,205,187,${alpha})`;
const r = parseInt(m[1], 16);
const g = parseInt(m[2], 16);
const b = parseInt(m[3], 16);
return `rgba(${r},${g},${b},${alpha})`;
},
/** 模板渲染用tag code 转中文(已废弃:后端列表直接返回 name/color 对象,保留兜底用) */
tagLabelOf(code) {
return TAG_LABEL_MAP[code] || code;
},
/**
* 首次打开抽屉短暂提示可左右滑动
*/
maybeShowSwipeTip() {
let shown = '';
try {
shown = uni.getStorageSync(SWIPE_TIP_STORAGE_KEY);
} catch (e) {
shown = '';
}
if (shown) return;
uni.showToast({
title: '左右滑动可切换标签',
icon: 'none',
duration: 2000,
});
try {
uni.setStorageSync(SWIPE_TIP_STORAGE_KEY, '1');
} catch (e) {
// ignore
}
},
async loadDetail() {
this.loading = true;
try {
const wrap = await getUserPatientDetail(this.upId);
if (wrap && wrap.ok && wrap.data) {
this.profile = {
user: wrap.data.user || {},
patient: wrap.data.patient || {},
};
this.health = wrap.data.health_inquiry || null;
}
this.calcSwiperHeight();
this.$nextTick(() => {
this.maybeShowSwipeTip();
});
} finally {
this.loading = false;
}
},
/** 点击顶部 Tab */
selectTab(index) {
if (this.tabSwiperChanging) return;
const tab = this.tabs[index];
if (!tab) return;
this.tabIndex = index;
this.switchTab(tab.key);
},
/** swiper 滑动切换 */
onTabSwiperChange(e) {
const index = e && e.detail ? e.detail.current : 0;
const tab = this.tabs[index];
if (!tab || tab.key === this.activeTab) {
this.tabIndex = index;
return;
}
this.tabSwiperChanging = true;
this.tabIndex = index;
this.switchTab(tab.key);
this.$nextTick(() => {
this.tabSwiperChanging = false;
});
},
/** 切换 Tab 并懒加载对应列表 */
async switchTab(key) {
this.activeTab = key;
const idx = this.tabs.findIndex((t) => t.key === key);
if (idx >= 0 && this.tabIndex !== idx) {
this.tabIndex = idx;
}
if (key === 'info') return;
if (key === 'calllog' && !this.callLogList.length) {
await this.loadCallLogList();
} else if (key === 'register' && !this.registerList.length) {
await this.loadRegisters();
} else if (key === 'prescription' && !this.prescriptionList.length) {
await this.loadPrescriptions();
} else if (key === 'order' && !this.orderList.length) {
await this.loadOrders();
}
},
async loadRegisters() {
this.tabLoading = true;
try {
const wrap = await getUserPatientRegisterList(this.upId);
this.registerList = (wrap && wrap.ok && wrap.data && wrap.data.items) || [];
} finally {
this.tabLoading = false;
}
},
async loadPrescriptions() {
this.tabLoading = true;
try {
const wrap = await getUserPatientPrescriptionList(this.upId);
this.prescriptionList = (wrap && wrap.ok && wrap.data && wrap.data.items) || [];
} finally {
this.tabLoading = false;
}
},
async loadOrders() {
this.tabLoading = true;
try {
const wrap = await getUserPatientOrderList(this.upId);
this.orderList = (wrap && wrap.ok && wrap.data && wrap.data.items) || [];
} finally {
this.tabLoading = false;
}
},
/**
* 挂号行:关抽屉后进挂号详情(与详情页一致)
*/
onRecordClick(item, tabKey) {
if (tabKey !== 'register') return;
const rid = Number((item && item.id) || 0);
if (!rid) {
uni.showToast({ title: '缺少挂号信息', icon: 'none' });
return;
}
try {
uni.setStorageSync('user_patient_register_detail', item || {});
} catch (e) { /* ignore */ }
this.close();
uni.navigateTo({
url: `/subPackages/sub_business_shared/user-patient/register-detail?id=${rid}`,
});
},
},
};
</script>
<style lang="scss" scoped>
.drawer {
height: 80vh;
background: #f9fafb;
display: flex;
flex-direction: column;
padding-bottom: env(safe-area-inset-bottom);
box-sizing: border-box;
}
.drawer-head {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28rpx 32rpx;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
flex-shrink: 0;
}
.title { font-size: 30rpx; font-weight: 600; color: #111827; }
.close { font-size: 26rpx; color: #6acdbb; }
.profile-card {
margin: 20rpx 24rpx 0;
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
flex-shrink: 0;
}
.profile-row { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
.avatar { width: 80rpx; height: 80rpx; border-radius: 50%; background: #e5e7eb; }
.meta { display: flex; flex-direction: column; min-width: 0; }
.name { font-size: 28rpx; font-weight: 600; color: #1f2937; }
.sub { font-size: 22rpx; color: #9ca3af; margin-top: 4rpx; }
.patient-block { padding-top: 12rpx; border-top: 1rpx solid #f3f4f6; }
.patient-head {
display: flex; align-items: center; justify-content: space-between; gap: 16rpx;
}
.patient-head-left { display: flex; align-items: center; gap: 12rpx; min-width: 0; flex: 1; }
.p-name { font-size: 28rpx; font-weight: 600; color: #111827; }
.sex-tag {
font-size: 20rpx; padding: 2rpx 12rpx; border-radius: 8rpx;
background: #e8f5f2; color: #2a9d8f;
}
.sex-tag.male { background: #e8f0fe; color: #3b82f6; }
.sex-tag.female { background: #fce7f3; color: #db2777; }
.callback-btn {
flex-shrink: 0;
padding: 8rpx 22rpx;
border-radius: 24rpx;
border: 1rpx solid #6acdbb;
background: #e8f5f2;
}
.callback-btn-text { font-size: 24rpx; color: #1a9b88; font-weight: 500; }
/* 回访/记录按钮区 */
.patient-actions {
display: flex;
align-items: center;
gap: 12rpx;
flex-shrink: 0;
}
.p-sub { display: block; font-size: 22rpx; color: #6b7280; margin-top: 6rpx; }
.tabs {
display: flex;
background: #fff;
margin: 16rpx 24rpx 0;
border-radius: 16rpx 16rpx 0 0;
padding: 0 8rpx;
border-bottom: 1rpx solid #eef2f1;
flex-shrink: 0;
}
.tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 0 14rpx;
position: relative;
}
.tab-text { font-size: 26rpx; color: #6b7280; }
.tab.on .tab-text { color: #1a9b88; font-weight: 600; }
.tab-bar {
position: absolute; bottom: 0; width: 48rpx; height: 4rpx;
border-radius: 4rpx; background: #6acdbb;
}
.tab-swiper {
width: 100%;
padding: 0 24rpx;
box-sizing: border-box;
}
.tab-scroll { width: 100%; box-sizing: border-box; }
.health-card {
background: #fff; border-radius: 0 0 16rpx 16rpx; padding: 8rpx 20rpx 20rpx;
}
.section-title {
display: block; font-size: 26rpx; font-weight: 600; color: #1f2937;
padding: 16rpx 0 8rpx;
}
.info-row {
display: flex; justify-content: space-between; align-items: flex-start;
padding: 16rpx 0; border-bottom: 1rpx solid #f3f4f6; gap: 24rpx;
}
.info-row:last-child { border-bottom: none; }
.info-label { font-size: 24rpx; color: #9ca3af; flex-shrink: 0; }
.info-value { font-size: 24rpx; color: #374151; text-align: right; flex: 1; word-break: break-all; }
.info-value.danger { color: #dc2626; font-weight: 600; }
.empty-inline { text-align: center; color: #9ca3af; padding: 40rpx 0; font-size: 24rpx; }
.record-card {
background: #fff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 12rpx;
display: flex;
align-items: center;
gap: 12rpx;
}
.record-main { flex: 1; min-width: 0; }
.r-title { display: block; font-size: 26rpx; font-weight: 600; color: #111827; }
.r-sub { display: block; font-size: 22rpx; color: #6b7280; margin-top: 6rpx; }
.arrow { font-size: 36rpx; color: #d1d5db; line-height: 1; }
.empty { text-align: center; color: #9ca3af; padding: 48rpx 0; font-size: 26rpx; }
/* "记录本次回访" link 按钮 */
.callback-link {
flex-shrink: 0;
padding: 8rpx 16rpx;
margin-left: 12rpx;
}
.callback-link-text {
font-size: 22rpx;
color: #1a9b88;
text-decoration: underline;
}
/* 回访记录卡片Tab 内用 in-tab 去掉外边距,与 health-card 对齐) */
.call-log-card {
margin: 16rpx 24rpx 0;
padding: 20rpx 24rpx;
background: #fff;
border-radius: 16rpx;
flex-shrink: 0;
}
.call-log-card.in-tab {
margin: 0;
border-radius: 0;
box-shadow: none;
}
.call-log-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8rpx;
}
.call-log-head-title {
font-size: 26rpx;
font-weight: 600;
color: #1f2937;
}
.call-log-head-count {
font-size: 22rpx;
color: #9ca3af;
}
.call-log-item {
padding: 16rpx 0;
border-bottom: 1rpx solid #f3f4f6;
}
.call-log-item:last-child { border-bottom: none; }
.call-log-item-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8rpx;
}
.call-log-result-tag {
font-size: 20rpx;
padding: 2rpx 12rpx;
border-radius: 8rpx;
background: #e5e7eb;
color: #6b7280;
}
.call-log-result-tag.connected { background: #e8f5f2; color: #1a9b88; }
.call-log-result-tag.no_answer { background: #fef3c7; color: #b45309; }
.call-log-result-tag.hangup { background: #fee2e2; color: #b91c1c; }
.call-log-result-tag.invalid { background: #f3f4f6; color: #6b7280; }
.call-log-item-time {
font-size: 22rpx;
color: #9ca3af;
}
.call-log-tags {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
margin: 4rpx 0;
}
.call-log-tag-text {
font-size: 20rpx;
color: #6acdbb;
}
.call-log-line {
display: block;
font-size: 24rpx;
color: #374151;
margin-top: 4rpx;
word-break: break-all;
}
.call-log-next { color: #1a9b88; }
</style>