441 lines
14 KiB
Vue
441 lines
14 KiB
Vue
<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>
|