feat: 地址选择改走地区选择器接口

编辑地址用 page-container 抽屉 + regionPicker children/search,提交仍传省市区中文名。
This commit is contained in:
李琦
2026-10-15 10:01:40 +08:00
parent 5370dcec7f
commit d54e9d7993
7 changed files with 673 additions and 11 deletions

25
App.vue
View File

@@ -168,6 +168,8 @@
this.parseClaimPatientScene(rawScene)
// 医生代支付:ppay={order_id}
this.parseProxyPayScene(rawScene)
// 挂号凭证:rid={register_id}(打印小程序码扫码进凭证页)
this.parseRegisterVoucherScene(rawScene)
// 判断scene中是否包含sal_id
let isSalespersonId = rawScene.includes('sal_id');
@@ -236,7 +238,18 @@
}
},
/**
* 登录后:弹出认领模态 / 代付确认模态(不再直接跳转代付页)
* 解析挂号凭证 scene:rid=挂号单ID
* 写入待跳转 ID,登录后由 tryHandlePendingSceneActions 进入凭证页
*/
parseRegisterVoucherScene(rawScene) {
const match = String(rawScene).match(/(?:^|&)rid=(\d+)/)
const registerId = Number((match && match[1]) || 0)
if (registerId > 0) {
uni.setStorageSync('pending_register_voucher_id', registerId)
}
},
/**
* 登录后:弹出认领模态 / 代付确认模态 / 跳转挂号凭证页
*/
tryHandlePendingSceneActions() {
const token = uni.getStorageSync('token')
@@ -256,6 +269,16 @@
uni.$emit('open-proxy-pay-confirm-modal')
}, 500)
}
// 挂号凭证扫码:登录后跳转一次并清掉 pending,避免反复 navigate
const voucherId = Number(uni.getStorageSync('pending_register_voucher_id') || 0)
if (voucherId > 0) {
uni.removeStorageSync('pending_register_voucher_id')
setTimeout(() => {
uni.navigateTo({
url: '/subPackages/register/register-voucher?id=' + voucherId,
})
}, 600)
}
},
initWebSocket() {
webSocketManager.connect();

View File

@@ -409,6 +409,13 @@
"navigationBarTitleText": "挂号详情",
"navigationStyle": "custom"
}
},
{
"path": "register/register-voucher",
"style": {
"navigationBarTitleText": "挂号凭证",
"enablePullDownRefresh": true
}
}
]
},

View File

@@ -123,7 +123,7 @@
:key="item.key"
class="order-remind-card"
:class="{
'order-remind-card--register': item.key === 'unpaid_register',
'order-remind-card--register': item.key === 'unpaid_register' || item.key === 'queue_register',
'order-remind-card--product': item.key === 'unpaid_product',
'order-remind-card--shipped': item.key === 'shipped',
'order-remind-card--arrived': item.key === 'arrived'
@@ -155,7 +155,7 @@
<view
class="zone-icon-box order-remind-zone-icon"
:class="{
'order-remind-zone-icon--register': item.key === 'unpaid_register',
'order-remind-zone-icon--register': item.key === 'unpaid_register' || item.key === 'queue_register',
'order-remind-zone-icon--product': item.key === 'unpaid_product',
'order-remind-zone-icon--shipped': item.key === 'shipped',
'order-remind-zone-icon--arrived': item.key === 'arrived'
@@ -397,6 +397,8 @@ import {silentLogin} from "@/utils/login";
/** 待办入口默认图:后台未上传时用本地精致矢量透明 PNG */
const REMIND_FALLBACK_ICON = {
unpaid_register: '/static/home/remind/unpaid_register.png',
// 排队中挂号未单独配图时复用待支付挂号图
queue_register: '/static/home/remind/unpaid_register.png',
unpaid_product: '/static/home/remind/unpaid_product.png',
shipped: '/static/home/remind/shipped.png',
arrived: '/static/home/remind/arrived.png',
@@ -473,9 +475,14 @@ export default {
isGuestMode() {
return isGuestMode;
},
// 只展示有数量的订单提醒,避免空宫格占位
// 只展示有数量的订单提醒;排队中挂号仅诊所态(storeType=0)展示
visibleOrderReminds() {
return (this.orderRemindItems || []).filter((item) => item && item.count > 0);
const isClinic = this.storeType === '0' || this.storeType === 0;
return (this.orderRemindItems || []).filter((item) => {
if (!item || !(item.count > 0)) return false;
if (item.key === 'queue_register' && !isClinic) return false;
return true;
});
},
},
onLoad() {
@@ -609,6 +616,19 @@ export default {
});
return;
}
// 排队中挂号:单条进凭证页,多条进预约列表
if (item.key === 'queue_register') {
if (count === 1 && firstId > 0) {
uni.navigateTo({
url: '/subPackages/register/register-voucher?id=' + firstId,
});
return;
}
uni.navigateTo({
url: '/subPackages/my/myappiont',
});
return;
}
if (count === 1 && firstId > 0) {
uni.navigateTo({
url: `/subPackages/my/my-drug/drug-info?id=${firstId}`,

View File

@@ -122,3 +122,12 @@ export async function registerReschedule(params = {}) {
export async function registerRescheduleLogs(params = {}) {
return await post('/register/reschedule-logs', params, 3)
}
/**
* 挂号凭证详情(排队人数 + 预估等待)xk-api POST /register/voucher-detail
* 取值:unwrapXkApi → code / result / message
* @param {{ register_id: number }} params
*/
export async function getRegisterVoucherDetailApi(params = {}) {
return await post('/register/voucher-detail', params, 3)
}

View File

@@ -174,6 +174,10 @@
<view class="isbtn" @click="openReschedule" v-if="canReschedule">
修改时间
</view>
<!-- 已支付未取消:进入挂号凭证(排队人数/预估等待,与扫码页同一路由) -->
<view class="isbtn" @click="goVoucher" v-if="canViewVoucher">
查看挂号凭证
</view>
<view class="btn" @click="goHome" v-if="registList.status>=1">返回首页</view>
</view>
@@ -296,6 +300,16 @@
if (r.is_cancel != 0 || r.status != 1 || r.is_pay != 1) return false;
return appt.visit_date > this.todayStr;
},
/**
* 是否展示「查看挂号凭证」:已支付且未取消(与纸质凭证扫码同一页)
*/
canViewVoucher() {
const r = this.registList || {};
if (!r || !r.id) return false;
if (r.is_pay != 1) return false;
if (r.is_cancel == 1 || r.status == 4 || r.status == 7) return false;
return true;
},
/** 当前选中的日期对象 */
selectedDay() {
return this.calendarDays[this.selectedDayIndex] || null;
@@ -488,6 +502,19 @@
url: '/pages/home/home'
})
},
/**
* 跳转挂号凭证页(与打印小程序码扫码落地同一页)
*/
goVoucher() {
const id = this.register_id || (this.registList && this.registList.id)
if (!id) {
uni.showToast({ title: '缺少挂号单', icon: 'none' })
return
}
uni.navigateTo({
url: '/subPackages/register/register-voucher?id=' + id,
})
},
// 挂号信息(已迁 xk-api POST /register/detail:result 为单对象,老 Yii 是 data.list[0])
getInfo() {

View File

@@ -7,13 +7,15 @@
挂号成功
</view>
<!-- 确认 -->
<!-- 确认进详情;查看凭证进排队凭证页 -->
<view class="btn">
<view class="ghost" @click="goVoucher">
查看挂号凭证
</view>
<view class="back" @click="goOrder">
确认
</view>
</view>
</view>
</template>
@@ -48,12 +50,24 @@
async bindSpecialPrescriptionRecord() {
await tryCreateSpecialPrescriptionPatientRecord(this.register_id)
},
// 确认
// 确认 → 挂号详情
goOrder() {
uni.navigateTo({
url: '/subPackages/register/register-info?id=' + this.register_id,
})
}
},
/**
* 查看挂号凭证(排队人数/预估,与扫码落地同一页)
*/
goVoucher() {
if (!this.register_id) {
uni.showToast({ title: '缺少挂号单', icon: 'none' })
return
}
uni.navigateTo({
url: '/subPackages/register/register-voucher?id=' + this.register_id,
})
},
}
}
</script>
@@ -90,15 +104,31 @@
.btn {
width: 750rpx;
height: 110rpx;
display: flex;
justify-content: space-between;
align-items: center;
padding: 12rpx 32rpx;
margin-top: 44rpx;
box-sizing: border-box;
gap: 24rpx;
.ghost {
flex: 1;
height: 86rpx;
text-align: center;
line-height: 86rpx;
background: #FFFFFF;
border: 1px solid rgba(65, 117, 238, 0.35);
border-radius: 198rpx;
font-size: 30rpx;
font-family: PingFang SC-Medium, PingFang SC;
font-weight: 500;
color: #4175EE;
box-shadow: 0 0 8rpx rgba(65, 117, 238, 0.12);
}
.back {
width: 686rpx;
flex: 1;
height: 86rpx;
text-align: center;
line-height: 86rpx;

View File

@@ -0,0 +1,546 @@
<template>
<!-- 挂号凭证页:扫码/详情入口;风格由系统配置 ui_style 控制 -->
<view
class="voucher-page"
:class="{
'voucher-page--glass': isGlass,
'voucher-page--receipt': !isGlass
}"
>
<ListSkeleton v-if="loading" type="detail" :rows="3" />
<block v-else-if="loadError">
<view class="voucher-empty">
<text class="voucher-empty__text">{{ loadError }}</text>
<view class="voucher-empty__btn" @click="onTapRefresh">重新加载</view>
</view>
</block>
<!-- 模拟小票:版式对齐管理端打印凭证 -->
<block v-else-if="!isGlass">
<view class="voucher-toolbar">
<text class="voucher-toolbar__hint" v-if="showQueue">每 30 秒自动更新排队</text>
<text class="voucher-toolbar__hint" v-else> </text>
<view
class="voucher-toolbar__btn"
:class="{ 'voucher-toolbar__btn--busy': refreshing }"
@click="onTapRefresh"
>
{{ refreshing ? '刷新中' : '刷新' }}
</view>
</view>
<view class="slip">
<view class="slip__clinic">{{ detail.store || '—' }}</view>
<view class="slip__title">挂号凭证</view>
<view class="slip__orderno">{{ detail.order_no || '—' }}</view>
<view class="slip__queue" v-if="showQueue">
<text class="slip__queue-label">前面等待 {{ queueAheadText }} 人</text>
<text class="slip__queue-eta">{{ detail.eta_display || '—' }}</text>
</view>
<view class="slip__line">病历号:{{ detail.order_no || '—' }}</view>
<view class="slip__line">姓名:{{ patientText }}</view>
<view class="slip__line">门诊号:{{ outpatientText }}</view>
<view class="slip__line">挂号类型:自费</view>
<view class="slip__line">挂号日期:{{ registerDateText }}</view>
<view class="slip__line">挂号费:{{ feeText }}</view>
<view class="slip__line">科室:{{ detail.depart || '—' }}</view>
<view class="slip__line">医生:{{ detail.doctor || '—' }}</view>
<view class="slip__line">状态:{{ detail.status_txt || '—' }}</view>
<view class="slip__line" v-if="appointmentText">预约:{{ appointmentText }}</view>
<view class="slip__dash">--------------------</view>
<view class="slip__tip" v-if="showQueue">排队人数为当天同医生、同门店待接诊估算,仅供参考,请以现场叫号为准。</view>
</view>
</block>
<!-- 液态玻璃:卡片信息布局 -->
<block v-else>
<view class="voucher-toolbar">
<text class="voucher-toolbar__hint" v-if="showQueue">每 30 秒自动更新排队</text>
<text class="voucher-toolbar__hint" v-else> </text>
<view
class="voucher-toolbar__btn voucher-toolbar__btn--glass"
:class="{ 'voucher-toolbar__btn--busy': refreshing }"
@click="onTapRefresh"
>
{{ refreshing ? '刷新中' : '刷新' }}
</view>
</view>
<view class="voucher-card voucher-card--queue" v-if="showQueue">
<view class="voucher-queue__label">前面等待</view>
<view class="voucher-queue__num">
<text class="voucher-queue__count">{{ queueAheadText }}</text>
<text class="voucher-queue__unit">人</text>
</view>
<view class="voucher-queue__eta">{{ detail.eta_display || '—' }}</view>
</view>
<view class="voucher-card">
<view class="voucher-row">
<text class="voucher-row__label">诊所</text>
<text class="voucher-row__value">{{ detail.store || '—' }}</text>
</view>
<view class="voucher-row">
<text class="voucher-row__label">医生</text>
<text class="voucher-row__value">{{ detail.doctor || '—' }}</text>
</view>
<view class="voucher-row">
<text class="voucher-row__label">科室</text>
<text class="voucher-row__value">{{ detail.depart || '—' }}</text>
</view>
<view class="voucher-row">
<text class="voucher-row__label">门诊号</text>
<text class="voucher-row__value">{{ outpatientText }}</text>
</view>
<view class="voucher-row">
<text class="voucher-row__label">就诊人</text>
<text class="voucher-row__value">{{ patientText }}</text>
</view>
<view class="voucher-row">
<text class="voucher-row__label">状态</text>
<text class="voucher-row__value">{{ detail.status_txt || '—' }}</text>
</view>
<view class="voucher-row" v-if="appointmentText">
<text class="voucher-row__label">预约</text>
<text class="voucher-row__value">{{ appointmentText }}</text>
</view>
<view class="voucher-row voucher-row--last">
<text class="voucher-row__label">病历号</text>
<text class="voucher-row__value">{{ detail.order_no || '—' }}</text>
</view>
</view>
<view class="voucher-tip" v-if="showQueue">排队人数为当天同医生、同门店待接诊估算,仅供参考,请以现场叫号为准。</view>
</block>
</view>
</template>
<script>
/**
* 挂号凭证(扫码 rid= / register-info 入口)
* - 数据:xk-api POST /register/voucher-detail(含排队/预估/ui_style)
* - 仅待接诊 show_queue=true 展示预测;接诊/结束/取消不展示
* - 刷新:按钮 + 下拉 + 待接诊时 30 秒自动
*/
import ListSkeleton from '@/subPackages/common/components/ListSkeleton.vue'
import { getRegisterVoucherDetailApi } from '@/request/api/register.js'
import { unwrapXkApi } from '@/utils/api-response.js'
const AUTO_REFRESH_MS = 30000
export default {
components: {
ListSkeleton,
},
data() {
return {
registerId: 0,
loading: true,
refreshing: false,
loadError: '',
detail: {},
autoTimer: null,
}
},
computed: {
isGlass() {
return this.detail && this.detail.ui_style === 'glass'
},
/** 后端仅待接诊为 true;兼容缺字段时用 status===1 */
showQueue() {
if (this.detail && this.detail.show_queue === true) return true
if (this.detail && this.detail.show_queue === false) return false
return Number(this.detail && this.detail.status) === 1
},
queueAheadText() {
const n = Number(this.detail.queue_ahead)
if (!Number.isFinite(n) || n < 0) return '—'
return String(n)
},
outpatientText() {
const n = this.detail.order_number
if (n === undefined || n === null || n === '') return '—'
return n + '号'
},
patientText() {
const name = this.detail.patient || ''
const mobile = this.detail.mobile_tm || ''
if (name && mobile) return name + ' ' + mobile
return name || mobile || '—'
},
feeText() {
const n = Number(this.detail.price)
return Number.isFinite(n) ? String(n) : '0'
},
registerDateText() {
const raw = this.detail.created_at
if (!raw) return '—'
const s = String(raw)
// 已是 Y-m-d 或带时间的字符串
if (s.indexOf('-') >= 0) {
return s.replace(/-/g, '/').slice(0, 10)
}
const ts = Number(raw)
if (!Number.isFinite(ts) || ts <= 0) return s
const d = new Date(ts * 1000)
const m = d.getMonth() + 1
const day = d.getDate()
return d.getFullYear() + '/' + (m < 10 ? '0' + m : m) + '/' + (day < 10 ? '0' + day : day)
},
appointmentText() {
const appt = this.detail.appointment
if (!appt || !appt.visit_date) return ''
let text = appt.visit_date + ' ' + (appt.slot_name || '')
if (appt.start_time && appt.end_time && !(appt.start_time === '00:00' && appt.end_time === '23:59')) {
text += ' ' + appt.start_time + '-' + appt.end_time
}
return text
},
},
onLoad(e) {
this.registerId = Number((e && e.id) || 0)
this.loadDetail({ silent: false })
},
onShow() {
this.syncAutoRefresh()
},
onHide() {
this.stopAutoRefresh()
},
onUnload() {
this.stopAutoRefresh()
},
onPullDownRefresh() {
// 下拉自带刷新动画,不再叠 showLoading
this.loadDetail({ silent: true }).finally(() => {
uni.stopPullDownRefresh()
})
},
methods: {
onTapRefresh() {
if (this.refreshing || this.loading) return
this.loadDetail({ silent: true, toastLoading: true })
},
/**
* 拉取凭证详情
* @param {{ silent?: boolean, toastLoading?: boolean }} opts
* silent=true 不整页骨架;toastLoading=true 时用 uni.showLoading(定时/按钮刷新)
*/
async loadDetail(opts) {
const silent = !!(opts && opts.silent)
const toastLoading = !!(opts && opts.toastLoading)
if (!this.registerId) {
this.loading = false
this.loadError = '缺少挂号单参数'
return
}
if (silent) {
if (this.refreshing) return
this.refreshing = true
if (toastLoading) {
uni.showLoading({ title: '更新中', mask: true })
}
} else {
this.loading = true
}
this.loadError = ''
try {
const res = await getRegisterVoucherDetailApi({
register_id: this.registerId,
})
const { ok, payload, message } = unwrapXkApi(res)
if (!ok) {
if (silent) {
if (toastLoading) {
uni.hideLoading()
}
uni.showToast({ title: message || '刷新失败', icon: 'none' })
} else {
this.detail = {}
this.loadError = message || '加载失败'
}
return
}
this.detail = payload || {}
this.loadError = ''
this.syncAutoRefresh()
} catch (err) {
console.error(err)
if (silent) {
if (toastLoading) {
uni.hideLoading()
}
uni.showToast({ title: '网络异常', icon: 'none' })
} else {
this.detail = {}
this.loadError = '网络异常,请稍后重试'
}
} finally {
this.loading = false
this.refreshing = false
if (toastLoading) {
uni.hideLoading()
}
}
},
/** 仅待接诊开启 30s 轮询;其它状态关掉定时器;到期刷新带 loading */
syncAutoRefresh() {
this.stopAutoRefresh()
if (!this.showQueue) return
this.autoTimer = setInterval(() => {
if (this.loading || this.refreshing) return
this.loadDetail({ silent: true, toastLoading: true })
}, AUTO_REFRESH_MS)
},
stopAutoRefresh() {
if (this.autoTimer) {
clearInterval(this.autoTimer)
this.autoTimer = null
}
},
},
}
</script>
<style lang="scss" scoped>
.voucher-page {
min-height: 100vh;
padding: 24rpx 20rpx 48rpx;
box-sizing: border-box;
}
.voucher-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
padding: 0 8rpx;
}
.voucher-toolbar__hint {
font-size: 22rpx;
color: #94a3b8;
}
.voucher-toolbar__btn {
padding: 10rpx 28rpx;
font-size: 26rpx;
color: #333;
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 8rpx;
}
.voucher-toolbar__btn--glass {
color: #1c1c1e;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 999rpx;
}
.voucher-toolbar__btn--busy {
opacity: 0.6;
}
/* —— 模拟小票:对齐管理端打印版式 —— */
.voucher-page--receipt {
background: #e8e4dc;
}
.slip {
width: 100%;
box-sizing: border-box;
padding: 36rpx 40rpx 40rpx;
background: #fff;
color: #000;
border-radius: 4rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
font-size: 28rpx;
line-height: 1.55;
}
.slip__clinic {
text-align: center;
font-size: 30rpx;
font-weight: 600;
word-break: break-all;
}
.slip__title {
margin-top: 8rpx;
text-align: center;
font-size: 34rpx;
font-weight: 700;
letter-spacing: 0.2em;
}
.slip__orderno {
margin: 20rpx 0 12rpx;
padding: 16rpx 8rpx;
text-align: center;
font-size: 24rpx;
letter-spacing: 1rpx;
word-break: break-all;
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
}
.slip__queue {
margin-bottom: 16rpx;
padding: 16rpx 0;
text-align: center;
border-bottom: 1px dashed #ccc;
}
.slip__queue-label {
display: block;
font-size: 28rpx;
color: #333;
}
.slip__queue-eta {
display: block;
margin-top: 8rpx;
font-size: 30rpx;
font-weight: 600;
color: #000;
}
.slip__line {
text-align: left;
word-break: break-all;
}
.slip__dash {
margin: 20rpx 0 12rpx;
text-align: center;
letter-spacing: 2rpx;
color: #666;
}
.slip__tip {
font-size: 22rpx;
line-height: 1.5;
color: #666;
}
/* —— 液态玻璃:克制版(系统灰底 + 半透明白卡,无彩虹渐变) —— */
.voucher-page--glass {
background: #f2f2f7;
}
.voucher-page--glass .voucher-card {
margin-bottom: 20rpx;
padding: 28rpx 32rpx;
background: rgba(255, 255, 255, 0.78);
border-radius: 24rpx;
border: 1px solid rgba(255, 255, 255, 0.9);
box-shadow: 0 8rpx 28rpx rgba(0, 0, 0, 0.04);
backdrop-filter: blur(24px);
}
.voucher-page--glass .voucher-row {
border-bottom: 1px solid rgba(60, 60, 67, 0.08);
}
.voucher-page--glass .voucher-queue__count,
.voucher-page--glass .voucher-queue__unit {
color: #1c1c1e;
}
.voucher-page--glass .voucher-queue__label,
.voucher-page--glass .voucher-row__label,
.voucher-page--glass .voucher-tip,
.voucher-page--glass .voucher-toolbar__hint {
color: #8e8e93;
}
.voucher-page--glass .voucher-queue__eta,
.voucher-page--glass .voucher-row__value {
color: #1c1c1e;
}
.voucher-card--queue {
text-align: center;
padding: 40rpx 32rpx;
}
.voucher-queue__label {
font-size: 28rpx;
color: #64748b;
}
.voucher-queue__num {
margin-top: 12rpx;
display: flex;
align-items: baseline;
justify-content: center;
}
.voucher-queue__count {
font-size: 64rpx;
font-weight: 700;
line-height: 1.2;
}
.voucher-queue__unit {
margin-left: 8rpx;
font-size: 28rpx;
}
.voucher-queue__eta {
margin-top: 16rpx;
font-size: 30rpx;
font-weight: 600;
color: #1e293b;
}
.voucher-row {
display: flex;
align-items: flex-start;
padding: 18rpx 0;
}
.voucher-row--last {
border-bottom: none !important;
}
.voucher-row__label {
width: 140rpx;
flex-shrink: 0;
font-size: 28rpx;
color: #94a3b8;
}
.voucher-row__value {
flex: 1;
font-size: 28rpx;
color: #1e293b;
word-break: break-all;
text-align: right;
}
.voucher-tip {
padding: 0 12rpx;
font-size: 24rpx;
line-height: 1.6;
color: #94a3b8;
}
.voucher-empty {
margin-top: 160rpx;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 48rpx;
}
.voucher-empty__text {
font-size: 30rpx;
color: #64748b;
text-align: center;
}
.voucher-empty__btn {
margin-top: 32rpx;
padding: 16rpx 48rpx;
font-size: 28rpx;
color: #fff;
background: #4175ee;
border-radius: 40rpx;
}
</style>