Files
xk-doctor-wx/subPackages/sub_business_shared/user-patient/register-detail.vue

441 lines
14 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>
<business-page-layout title="挂号详情" :show-back="true">
<view class="page">
<view v-if="loading" class="empty">加载中...</view>
<template v-else-if="!detail.id">
<view class="empty">暂无挂号信息</view>
</template>
<template v-else>
<!-- 有药品/回答时才出 Tab仅基本信息时不展示 Tab -->
<view v-if="visibleTabs.length > 1" class="tabs">
<view
v-for="(t, idx) in visibleTabs"
: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>
<!-- Tab固定高度 swiper避免与外层页面滚动抢手势 -->
<swiper
v-if="visibleTabs.length > 1"
class="tab-swiper"
:style="{ height: swiperHeightPx + 'px' }"
:current="tabIndex"
:duration="200"
@change="onTabSwiperChange"
>
<swiper-item v-for="t in visibleTabs" :key="t.key">
<scroll-view
scroll-y
class="tab-scroll"
:style="{ height: swiperHeightPx + 'px' }"
>
<view v-if="t.key === 'basic'" class="card">
<view class="row">
<text class="label">订单号</text>
<text class="value">{{ detail.order_no || detail.order_number || '—' }}</text>
</view>
<view class="row">
<text class="label">诊所</text>
<text class="value">{{ detail.store || '—' }}</text>
</view>
<view class="row">
<text class="label">医生</text>
<text class="value">{{ detail.doctor || '—' }}</text>
</view>
<view class="row">
<text class="label">挂号类型</text>
<text class="value">{{ detail.type_text || '—' }}</text>
</view>
<view class="row">
<text class="label">状态</text>
<text class="value accent">{{ detail.status_text || '—' }}</text>
</view>
<view class="row">
<text class="label">金额</text>
<text class="value">¥{{ formatPrice(detail.price) }}</text>
</view>
<view class="row">
<text class="label">支付</text>
<text class="value">{{ Number(detail.is_pay) === 1 ? '已支付' : '未支付' }}</text>
</view>
<view class="row">
<text class="label">创建时间</text>
<text class="value">{{ detail.created_at || '—' }}</text>
</view>
<view v-if="detail.pay_time" class="row">
<text class="label">支付时间</text>
<text class="value">{{ detail.pay_time }}</text>
</view>
</view>
<view v-else-if="t.key === 'drugs'" class="card">
<view v-if="!drugRows.length" class="empty-inline">暂无选药</view>
<view v-for="row in drugRows" :key="row._wxKey" class="drug-row">
<image
v-if="row.image"
class="drug-thumb"
:src="row.image"
mode="aspectFill"
@click="previewDrug(row.image)"
/>
<view v-else class="drug-thumb drug-thumb-placeholder">
<text class="drug-thumb-letter">{{ drugThumbLetter(row.name) }}</text>
</view>
<view class="drug-meta">
<text class="drug-name">{{ row.name || '药品' }}</text>
<text class="drug-spec">{{ row.specification || '暂无规格' }}</text>
</view>
<text class="drug-qty">×{{ row.quantity }}</text>
</view>
</view>
<view v-else-if="t.key === 'answers'" class="card">
<view v-if="!answerRecords.length" class="empty-inline">暂无回答</view>
<view v-for="(a, aidx) in answerRecords" :key="aidx" class="row answer-row">
<text class="label">{{ a.question || '问题' }}</text>
<text class="value">{{ a.answer || '—' }}</text>
</view>
</view>
</scroll-view>
</swiper-item>
</swiper>
<!-- 仅基本信息无滑动 -->
<view v-else class="card">
<view class="row">
<text class="label">订单号</text>
<text class="value">{{ detail.order_no || detail.order_number || '—' }}</text>
</view>
<view class="row">
<text class="label">诊所</text>
<text class="value">{{ detail.store || '—' }}</text>
</view>
<view class="row">
<text class="label">医生</text>
<text class="value">{{ detail.doctor || '—' }}</text>
</view>
<view class="row">
<text class="label">挂号类型</text>
<text class="value">{{ detail.type_text || '—' }}</text>
</view>
<view class="row">
<text class="label">状态</text>
<text class="value accent">{{ detail.status_text || '—' }}</text>
</view>
<view class="row">
<text class="label">金额</text>
<text class="value">¥{{ formatPrice(detail.price) }}</text>
</view>
<view class="row">
<text class="label">支付</text>
<text class="value">{{ Number(detail.is_pay) === 1 ? '已支付' : '未支付' }}</text>
</view>
<view class="row">
<text class="label">创建时间</text>
<text class="value">{{ detail.created_at || '—' }}</text>
</view>
<view v-if="detail.pay_time" class="row">
<text class="label">支付时间</text>
<text class="value">{{ detail.pay_time }}</text>
</view>
</view>
</template>
</view>
</business-page-layout>
</template>
<script>
/**
* 挂号详情:基本信息 + 条件 Tab本次选药 / 回答记录)
* 多 Tab 时用固定高度 swiper 左右滑切换,并在首次进入时短暂提示
*/
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue';
import { getUserPatientRegisterDetail } from '@/api/userPatientProfile.js';
/** 左右滑切换提示只展示一次,避免每次进入打扰 */
const SWIPE_TIP_STORAGE_KEY = 'user_patient_register_detail_swipe_tip';
export default {
components: { BusinessPageLayout },
data() {
return {
loading: false,
registerId: 0,
activeTab: 'basic',
/** swiper 当前下标,与 activeTab / visibleTabs 同步 */
tabIndex: 0,
/** 点击 Tab 与 swiper change 互斥,避免回环 */
tabSwiperChanging: false,
/** 按窗口算出的 swiper 像素高度,保证横向滑动可用 */
swiperHeightPx: 400,
detail: {},
registerInfo: null,
answerRecords: [],
};
},
computed: {
/** 药品行:多选西药优先,否则单药 */
drugRows() {
const info = this.registerInfo;
if (!info || typeof info !== 'object') return [];
const arr = info.selected_western_drugs;
const fallbackQty = info.number != null && info.number !== '' ? Number(info.number) : 1;
if (Array.isArray(arr) && arr.length) {
return arr.map((d, idx) => {
const q = d && d.quantity != null ? Number(d.quantity) : fallbackQty;
const id = d && (d.drug_id || d.id);
return {
_wxKey: 'd-' + (id != null ? id : idx),
name: (d && d.name) || '',
specification: (d && (d.specification || d.spec)) || '',
image: (d && (d.image || d.drug_image)) || '',
quantity: q >= 1 ? q : 1,
};
});
}
if (info.drug && info.drug.name) {
return [{
_wxKey: 'd-' + (info.drug.drug_id || info.drug.id || 0),
name: info.drug.name,
specification: info.drug.specification || info.drug.spec || '',
image: info.drug.image || info.drug.drug_image || '',
quantity: fallbackQty >= 1 ? fallbackQty : 1,
}];
}
return [];
},
hasDrugs() {
return this.drugRows.length > 0;
},
hasAnswers() {
return Array.isArray(this.answerRecords) && this.answerRecords.length > 0;
},
/** 仅展示有数据的 Tab基本信息始终保留 */
visibleTabs() {
const all = [
{ key: 'basic', label: '基本信息' },
{ key: 'drugs', label: '本次挂号选择药品', show: this.hasDrugs },
{ key: 'answers', label: '回答记录', show: this.hasAnswers },
];
return all.filter((t) => t.show !== false);
},
},
onLoad(options) {
this.calcSwiperHeight();
const id = Number((options && options.id) || 0);
this.registerId = id;
let cached = {};
try {
cached = uni.getStorageSync('user_patient_register_detail') || {};
} catch (e) {
cached = {};
}
// 缓存作乐观展示id 不一致则只用 URL 中的 id
if (cached && Number(cached.id) === id) {
this.detail = cached;
} else {
this.detail = { id };
}
if (id) {
this.loadDetail(id);
}
},
onReady() {
// 布局就绪后再算一次,避免状态栏/导航高度未就绪
this.calcSwiperHeight();
},
methods: {
/**
* 按窗口高度算死 swiper 高度:窗口 状态栏 导航 Tab 页边距
* 微信小程序 swiper 无明确高度时横向滑动常失效
*/
calcSwiperHeight() {
try {
const sys = uni.getSystemInfoSync() || {};
const winH = Number(sys.windowHeight) || 600;
const statusBar = Number(sys.statusBarHeight) || 0;
const navbar = Number(this.$navbarHeight) || 44;
const rpx = (Number(sys.windowWidth) || 375) / 750;
const pagePad = Math.ceil(24 * rpx);
const tabsH = Math.ceil(88 * rpx);
const h = winH - statusBar - navbar - pagePad * 2 - tabsH;
this.swiperHeightPx = h > 200 ? h : 200;
} catch (e) {
this.swiperHeightPx = 400;
}
},
formatPrice(v) {
const n = Number(v);
return Number.isFinite(n) ? n.toFixed(2) : '0.00';
},
/** 无图时取药名首字作占位,与接诊面板一致 */
drugThumbLetter(name) {
const s = String(name || '').trim();
return s ? s.charAt(0) : '药';
},
previewDrug(url) {
if (!url) return;
uni.previewImage({ urls: [url], current: url });
},
/**
* 点击顶部 Tab同步 activeTab + tabIndex与工作台 swiper 防抖一致)
*/
selectTab(index) {
if (this.tabSwiperChanging) return;
const tab = this.visibleTabs[index];
if (!tab) return;
this.tabIndex = index;
this.activeTab = tab.key;
},
/**
* 左右滑动 swiper同步 Tab 下标与 activeTab
*/
onTabSwiperChange(e) {
const index = e && e.detail ? e.detail.current : 0;
const tab = this.visibleTabs[index];
if (!tab || tab.key === this.activeTab) return;
this.tabSwiperChanging = true;
this.tabIndex = index;
this.activeTab = tab.key;
this.$nextTick(() => {
this.tabSwiperChanging = false;
});
},
/**
* 按 activeTab 校正 tabIndex数据加载后 Tab 集合可能变化)
*/
syncTabIndexFromActive() {
const keys = this.visibleTabs.map((t) => t.key);
let idx = keys.indexOf(this.activeTab);
if (idx < 0) {
this.activeTab = 'basic';
idx = 0;
}
this.tabIndex = idx;
},
/**
* 多 Tab 时首次进入短暂提示「可左右滑动」;本地标记只提示一次
*/
maybeShowSwipeTip() {
if (this.visibleTabs.length <= 1) return;
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) {
// 写缓存失败不影响主流程
}
},
/** 拉取挂号详情并同步 Tab 可见性 */
async loadDetail(id) {
this.loading = true;
try {
const wrap = await getUserPatientRegisterDetail(id);
if (!wrap || !wrap.ok || !wrap.data) {
uni.showToast({ title: '挂号详情加载失败', icon: 'none' });
return;
}
const data = wrap.data;
this.detail = {
id: data.id,
order_no: data.order_no,
order_number: data.order_number,
store: data.store,
doctor: data.doctor,
type: data.type,
type_text: data.type_text,
status_text: data.status_text,
price: data.price,
is_pay: data.is_pay,
created_at: data.created_at,
pay_time: data.pay_time,
};
this.registerInfo = data.register_info || null;
this.answerRecords = Array.isArray(data.answer_records) ? data.answer_records : [];
this.syncTabIndexFromActive();
this.calcSwiperHeight();
this.$nextTick(() => {
this.maybeShowSwipeTip();
});
} finally {
this.loading = false;
}
},
},
};
</script>
<style lang="scss" scoped>
.page {
padding: 24rpx;
box-sizing: border-box;
}
.tabs {
display: flex;
flex-wrap: wrap;
gap: 8rpx 24rpx;
margin-bottom: 16rpx;
padding: 0 4rpx;
}
.tab {
position: relative;
padding: 8rpx 0 12rpx;
}
.tab-text { font-size: 28rpx; color: #6b7280; }
.tab.on .tab-text { color: #1a9b88; font-weight: 600; }
.tab-bar {
position: absolute; left: 20%; right: 20%; bottom: 0;
height: 6rpx; border-radius: 6rpx; background: #6acdbb;
}
.tab-swiper {
width: 100%;
}
.tab-scroll {
width: 100%;
box-sizing: border-box;
}
.card {
background: #fff; border-radius: 20rpx; padding: 8rpx 24rpx;
}
.row {
display: flex; justify-content: space-between; align-items: flex-start;
padding: 24rpx 0; border-bottom: 1rpx solid #f3f4f6; gap: 24rpx;
}
.row:last-child { border-bottom: none; }
.answer-row .label { max-width: 46%; }
.label { font-size: 26rpx; color: #9ca3af; flex-shrink: 0; }
.value { font-size: 26rpx; color: #1f2937; text-align: right; flex: 1; word-break: break-all; }
.value.accent { color: #1a9b88; font-weight: 600; }
.drug-row {
display: flex; align-items: flex-start; gap: 16rpx;
padding: 24rpx 0; border-bottom: 1rpx solid #f3f4f6;
}
.drug-row:last-child { border-bottom: none; }
.drug-thumb {
width: 72rpx; height: 72rpx; border-radius: 10rpx; background: #f3f4f6; flex-shrink: 0;
}
.drug-thumb-placeholder {
display: flex; align-items: center; justify-content: center;
background: #e5e7eb;
}
.drug-thumb-letter { font-size: 28rpx; color: #6b7280; font-weight: 600; }
.drug-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6rpx; }
.drug-name { font-size: 28rpx; color: #1f2937; font-weight: 500; }
.drug-spec { font-size: 22rpx; color: #9ca3af; }
.drug-qty { font-size: 28rpx; color: #1f2937; font-weight: 600; flex-shrink: 0; }
.empty { text-align: center; color: #9ca3af; padding: 80rpx 0; font-size: 28rpx; }
.empty-inline { text-align: center; color: #9ca3af; padding: 48rpx 0; font-size: 26rpx; }
</style>