Files
李琦 cb290f3e95 fix(login): 密码登录补充密码非空前端校验
- doctor-login submitLogin 增加「请输入密码」前置校验,
  与 getCode 一致,避免空密码误触发登录请求
- 配合后端 service_user 空密码回退 PC 医生/药师密码校验
2026-07-19 16:08:23 +08:00

675 lines
19 KiB
Vue
Raw Permalink 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>
<clinic-page-layout title="订单详情" :show-back="true">
<view class="detail-container">
<!-- 头部面板通栏展示金额与状态 -->
<view v-if="info.id" class="detail-panel header-panel">
<view class="order-head">
<text class="order-no-main">订单号{{ info.order_no }}</text>
<text class="pay-total">{{ money(info.total_pay_price) }}</text>
</view>
<view class="tags">
<text class="modern-tag tag-status">{{ detailExtra.statusText }}</text>
<text class="modern-tag tag-type">{{ detailExtra.prescriptionTypeText }}</text>
<text class="modern-tag tag-delivery">{{ detailExtra.deliveryText }}</text>
</view>
<view class="sub-block">
<view class="sub-block-title">基本信息</view>
<view class="row"><text>订单类型</text><text>{{ detailExtra.orderTypeText }}</text></view>
<view class="row">
<text>患者</text>
<text
class="patient-link"
:class="{ disabled: !patientUpId }"
@click="openPatient"
>{{ info.patient || '-' }}</text>
</view>
<view class="row"><text>医生</text><text>{{ doctorName }}</text></view>
<view class="row"><text>处方来源</text><text>{{ storeName }}</text></view>
<view class="row"><text>订单来源</text><text>{{ onlineText(info.is_online) }}</text></view>
<view class="row" v-if="detailExtra.prescriptionStatusText"><text>处方状态</text><text>{{ detailExtra.prescriptionStatusText }}</text></view>
<view class="row"><text>支付时间</text><text>{{ info.pay_time || '-' }}</text></view>
<view class="row"><text>下单时间</text><text>{{ info.created_at || '-' }}</text></view>
<view class="row" v-if="detailExtra.cancelRemark"><text>发货备注</text><text>{{ detailExtra.cancelRemark }}</text></view>
</view>
<view class="sub-block">
<view class="sub-block-title">费用明细</view>
<view class="row">
<text>药品总价</text>
<text
v-if="canOrderPercentAdjust"
class="price-link"
@click="openOrderPriceAdjust"
>{{ money(info.items_price) }}</text>
<text v-else>{{ money(info.items_price) }}</text>
</view>
<view class="row"><text>供货价</text><text>{{ detailExtra.marketPrice }}</text></view>
<view class="row"><text>运费</text><text>{{ money(info.trans_expenses) }}</text></view>
<view class="row"><text>挂号费</text><text>{{ money(info.register_price) }}</text></view>
<view class="row"><text>是否包邮</text><text>{{ detailExtra.freeShippingText }}</text></view>
<view class="row" v-if="orderPriceDiscountLabel"><text>价格浮动</text><text>{{ orderPriceDiscountLabel }}</text></view>
</view>
</view>
<!-- 配送信息面板 -->
<view v-if="info.id" class="detail-panel">
<view class="section-title">{{ receiverBlockTitle }}</view>
<view class="row">
<text>{{ receiverLabel }}</text>
<text
class="patient-link"
:class="{ disabled: !patientUpId }"
@click="openPatient"
>{{ receiverName }}</text>
</view>
<view class="row"><text>联系电话</text><text>{{ receiverMobile }}</text></view>
<view class="row" v-if="info.delivery_method === 0">
<text>配送地址</text><text class="addr">{{ deliveryAddress }}</text>
</view>
<view class="row" v-else>
<text>自提地址</text><text class="addr">{{ pickupAddress }}</text>
</view>
</view>
<!-- 商品列表面板 -->
<view v-if="productItems.length" class="detail-panel">
<view class="section-title">订单商品</view>
<!-- 如果是中药高密度网格排列一行多个小且精致 -->
<view v-if="isTcmOrder" class="tcm-grid-wrap">
<view class="tcm-grid">
<view v-for="(p, idx) in productDisplayList" :key="idx" class="herb-box">
<text class="herb-name">{{ p.drugName }}</text>
<text class="herb-qty">{{ p.qty }}</text>
</view>
</view>
</view>
<!-- 非中药标准西药图文列表 -->
<view v-else>
<view
v-for="(p, idx) in productDisplayList"
:key="idx"
class="product-item"
:class="p.isTcm ? 'product-tcm' : 'product-west'"
>
<image
v-if="!p.isTcm"
class="product-img"
:src="p.imageUrl"
mode="aspectFill"
/>
<view class="product-body">
<view class="product-name">{{ p.drugName }}</view>
<view class="product-meta">
<text v-if="p.drugNumber">编号{{ p.drugNumber }}</text>
<text>数量{{ p.qty }}</text>
</view>
<view class="product-line" v-if="!p.isTcm">规格{{ p.specification }}</view>
<view class="product-line" v-if="!p.isTcm">厂家{{ p.manufacturer }}</view>
<view class="product-price-bar">
<text class="price-main">单价{{ p.price }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 处方信息内页入口 (保留作为备用) -->
<view
v-if="detailExtra.canViewPrescription"
class="detail-panel card-link"
hover-class="panel-hover"
@click="openPrescription"
>
<view class="section-head">
<text class="section-title">处方信息</text>
<text class="section-arrow">查看完整处方 </text>
</view>
<template v-if="detailExtra.showPrescription">
<view class="row"><text>处方编号</text><text>{{ detailExtra.showPrescription.prescriptionNo }}</text></view>
<view class="row"><text>诊断</text><text>{{ detailExtra.showPrescription.diagnose }}</text></view>
<view class="row"><text>医嘱</text><text>{{ detailExtra.showPrescription.doctorOrder }}</text></view>
<view class="row"><text>开药医生</text><text>{{ detailExtra.showPrescription.doctorName }}</text></view>
</template>
<view v-else class="prescription-hint">点击加载完整处方内容</view>
</view>
<!-- 物流时间轴 -->
<express-timeline
v-if="info.id && info.delivery_method === 0"
:express="expressInfo"
:show="true"
/>
<!-- 底部悬浮操作栏 -->
<view class="actions-bar" v-if="info.id">
<button v-if="canOrderPercentAdjust && Number(info.price_discount) !== 100" class="btn-outline" @click="clearOrderPriceDiscount({ adjustOrderPercent })">清除浮动</button>
<button class="btn-outline" @click="goTrace">订单追溯</button>
<button v-if="canRefund" class="btn-warn" @click="refund">申请退款</button>
<button v-if="detailExtra.canViewPrescription" class="btn-primary btn-full" @click="openPrescription">查看处方</button>
</view>
</view>
</clinic-page-layout>
<!-- page-container 抽屉挂在页面根级避免被 layout 裁切成页底内联 -->
<prescription-detail-popup ref="prescriptionPopup" />
<order-price-percent-adjust-popup
:visible="priceAdjustVisible"
:current-discount="info.price_discount || 100"
:quick-options="priceAdjustQuickOptions"
:submitting="priceAdjustSubmitting"
@close="closePriceAdjust"
@confirm="(p) => onPriceAdjustConfirm(p, { adjustOrderPercent })"
/>
<user-patient-detail
v-if="patientDetailVisible"
:visible="patientDetailVisible"
:up-id="patientDetailUpId"
:patient-name="patientDetailName"
@close="patientDetailVisible = false"
/>
</view>
</template>
<script>
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
import ExpressTimeline from './components/ExpressTimeline.vue'
import PrescriptionDetailPopup from './components/PrescriptionDetailPopup.vue'
import {
getOrderDetail,
refundOrder,
getExpressDetailByOrderId,
getPriceAdjustConfig,
adjustOrderPercent,
} from '@/api/clinicAdmin.js'
import { orderPriceAdjustMixin } from '@/subPackages/sub_business_shared/common/orderPriceAdjustMixin.js'
import OrderPricePercentAdjustPopup from '@/subPackages/sub_business_shared/components/OrderPricePercentAdjustPopup.vue'
import UserPatientDetail from '@/subPackages/sub_business_shared/components/UserPatientDetail.vue'
import {
orderStatusText,
deliveryMethodText,
orderOnlineText,
prescriptionTypeText,
erpSyncText,
mapOrderDetailDisplay,
mapOrderProductItem,
} from '@/subPackages/sub_clinic_admin/common/display.js'
import { formatMoney } from '@/subPackages/sub_clinic_admin/common/format.js'
export default {
mixins: [orderPriceAdjustMixin],
components: {
ClinicPageLayout,
ExpressTimeline,
PrescriptionDetailPopup,
OrderPricePercentAdjustPopup,
UserPatientDetail,
},
data() {
return {
id: 0,
info: {},
expressInfo: null,
detailExtra: {},
patientDetailVisible: false,
patientDetailUpId: 0,
patientDetailName: '',
}
},
computed: {
productItems() {
return this.info.product_order_items || []
},
productDisplayList() {
const pt = this.info.prescription_type
return this.productItems.map(p => mapOrderProductItem(p, pt))
},
// 判断是否为中药处方,决定是否使用网格布局
isTcmOrder() {
return this.info.prescription_type === 1 || (this.productDisplayList.length > 0 && this.productDisplayList[0].isTcm);
},
doctorName() {
const d = this.info.doctor
return (d && d.name) ? d.name : '-'
},
storeName() {
const s = this.info.store
return (s && s.name) ? s.name : '-'
},
/** 就诊人 ID订单 up_id 或关联 user_patient.id */
patientUpId() {
const up = Number(this.info.up_id || 0)
if (up) return up
const rel = this.info.user_patient
return Number((rel && rel.id) || 0)
},
receiverBlockTitle() {
return this.info.delivery_method === 0 ? '收货人信息' : '就诊人信息'
},
receiverLabel() {
return this.info.delivery_method === 0 ? '收货人' : '就诊人'
},
receiverName() {
if (this.info.delivery_method === 0) {
const a = this.info.address
return (a && a.name) ? a.name : '-'
}
return this.info.express_name || this.info.patient || '-'
},
receiverMobile() {
const a = this.info.address
if (a && a.mobile) return a.mobile
return this.info.patient_mobile || '-'
},
deliveryAddress() {
const a = this.info.address
if (!a || typeof a !== 'object') return '-'
return [a.province, a.region, a.detail_address].filter(Boolean).join(' ') || '-'
},
pickupAddress() {
const s = this.info.store
return (s && s.position) ? s.position : '-'
},
canRefund() {
return this.info.status === 2 || this.info.status === 3
}
},
onLoad(q) {
this.id = Number(q.id || 0)
this.load()
},
/** 离开页关闭处方抽屉,避免 page-container 残留遮罩导致列表白屏 */
onHide() {
this.closePrescriptionPopup()
this.patientDetailVisible = false
},
onUnload() {
this.closePrescriptionPopup()
this.patientDetailVisible = false
},
methods: {
/** 关闭处方抽屉 */
closePrescriptionPopup() {
const pop = this.$refs.prescriptionPopup
if (pop && typeof pop.onClose === 'function') {
pop.onClose()
}
},
/**
* 打开患者视角抽屉(与订单列表一致)
*/
openPatient() {
const upId = this.patientUpId
if (!upId) {
uni.showToast({ title: '缺少就诊人信息', icon: 'none' })
return
}
this.patientDetailUpId = upId
this.patientDetailName = this.info.patient || ''
this.patientDetailVisible = true
},
load() {
if (!this.id) {
uni.showToast({ title: '缺少订单信息', icon: 'none' })
return
}
getOrderDetail(this.id).then(w => {
if (!w.ok) {
uni.showToast({ title: '订单加载失败', icon: 'none' })
return
}
this.info = w.data || {}
this.detailExtra = mapOrderDetailDisplay(this.info)
this.loadPriceAdjustMeta(() => getPriceAdjustConfig(this.info.store_id))
if (this.info.delivery_method === 0) {
this.loadExpress()
}
}).catch(() => {
uni.showToast({ title: '订单加载失败', icon: 'none' })
})
},
loadExpress() {
getExpressDetailByOrderId(this.id).then(w => {
if (w.ok) this.expressInfo = w.data
})
},
money(v) {
return formatMoney(v)
},
statusText(s) {
return orderStatusText(s)
},
deliveryText(dm) {
return deliveryMethodText(dm)
},
onlineText(v) {
return orderOnlineText(v)
},
prescriptionText(pt) {
return prescriptionTypeText(pt)
},
erpSyncText(v) {
return erpSyncText(v)
},
refund() {
uni.showModal({
title: '确认退款',
confirmColor: '#F53F3F',
success: (r) => {
if (!r.confirm) return
refundOrder({ id: this.id }).then(w => {
if (w.ok) {
uni.showToast({ title: '已提交' })
this.load()
}
})
},
})
},
openPrescription() {
const pid = this.detailExtra.prescriptionId
if (!pid) return
this.$refs.prescriptionPopup.open(pid)
},
goTrace() {
uni.navigateTo({
url: `/subPackages/sub_clinic_admin/order/trace/index?order_id=${this.id}&scene=product`,
})
},
},
}
</script>
<style lang="scss" scoped>
$theme-primary: #6acdbb;
$color-page-bg: #F5F7F8;
$color-text-title: #222B2A;
$color-text-body: #4E5969;
$color-text-muted: #86909C;
$color-border: #F2F3F5;
$color-danger: #F53F3F;
.detail-container {
background-color: $color-page-bg;
min-height: 100vh;
/* 适配底部操作栏,留出极大的安全边距 */
padding-bottom: 180rpx;
}
/* 通栏面板 */
.detail-panel {
background: #ffffff;
padding: 32rpx 40rpx;
margin-bottom: 24rpx;
}
.header-panel {
padding-top: 40rpx;
}
.order-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.order-no-main {
font-size: 28rpx;
color: $color-text-muted;
}
.pay-total {
font-size: 40rpx;
color: $color-text-title;
font-weight: 700;
font-family: monospace, sans-serif;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-bottom: 32rpx;
}
.modern-tag {
font-size: 22rpx;
padding: 6rpx 16rpx;
border-radius: 8rpx;
font-weight: 500;
}
.tag-status { background: rgba(106, 205, 187, 0.1); color: $theme-primary; }
.tag-type { background: rgba(230, 162, 60, 0.1); color: #e6a23c; }
.tag-delivery { background: rgba(91, 127, 214, 0.1); color: #5b7fd6; }
.sub-block {
margin-top: 32rpx;
}
.sub-block-title {
font-size: 30rpx;
font-weight: 600;
color: $color-text-title;
margin-bottom: 8rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid $color-border;
}
.section-title {
font-size: 32rpx;
font-weight: 700;
color: $color-text-title;
margin-bottom: 16rpx;
}
.section-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.section-arrow {
font-size: 26rpx;
color: $theme-primary;
}
.panel-hover {
background-color: #FAFAFA;
}
.prescription-hint {
font-size: 26rpx;
color: $color-text-muted;
padding: 16rpx 0;
}
.row {
display: flex;
justify-content: space-between;
padding: 20rpx 0;
font-size: 28rpx;
border-bottom: 1rpx dashed $color-border;
gap: 24rpx;
&:last-child {
border-bottom: none;
}
text:first-child {
color: $color-text-muted;
}
text:last-child {
color: $color-text-title;
text-align: right;
flex: 1;
word-break: break-all;
}
}
.addr { max-width: 460rpx; line-height: 1.4; }
/* ================= 专属中药网格区 ================= */
.tcm-grid-wrap {
margin-top: 24rpx;
background: #F8FBFB;
border-radius: 16rpx;
padding: 24rpx;
}
.tcm-grid {
display: grid;
grid-template-columns: repeat(4, 1fr); /* 一排严格4个精致小巧 */
gap: 16rpx;
}
.herb-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #ffffff;
border-radius: 12rpx;
padding: 16rpx 8rpx;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.015);
border: 1rpx solid #EAEFEF;
}
.herb-name {
font-size: 26rpx;
color: $color-text-title;
font-weight: 600;
margin-bottom: 6rpx;
text-align: center;
}
.herb-qty {
font-size: 24rpx;
color: $theme-primary;
font-weight: 500;
}
/* 商品信息 (西药模式) */
.product-item {
display: flex;
padding: 32rpx 0;
border-bottom: 1rpx solid $color-border;
&:last-child {
border-bottom: none;
padding-bottom: 0;
}
}
.product-img {
width: 140rpx;
height: 140rpx;
border-radius: 20rpx;
flex-shrink: 0;
background: $color-page-bg;
border: 1rpx solid $color-border;
}
.product-body {
flex: 1;
margin-left: 24rpx;
display: flex;
flex-direction: column;
justify-content: center;
}
.product-name {
font-size: 30rpx;
color: $color-text-title;
font-weight: 600;
margin-bottom: 8rpx;
}
.product-meta {
display: flex;
gap: 24rpx;
font-size: 24rpx;
color: $color-text-muted;
margin-bottom: 8rpx;
}
.product-line {
font-size: 24rpx;
color: $color-text-muted;
margin-bottom: 6rpx;
}
.product-price-bar {
margin-top: auto;
display: flex;
justify-content: flex-end;
align-items: center;
}
.price-main { font-size: 28rpx; color: $color-text-title; font-weight: 600; }
.price-link { color: $theme-primary; text-decoration: underline; }
.patient-link {
color: $theme-primary;
text-align: right;
&.disabled { color: inherit; }
}
/* ================= 底部动作栏 ================= */
.actions-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #ffffff;
padding: 24rpx 40rpx;
/* 自动撑起 iOS 底部安全距离 */
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
box-shadow: 0 -4rpx 24rpx rgba(0,0,0,0.04);
display: flex;
gap: 24rpx;
z-index: 99;
}
.actions-bar button {
margin: 0;
height: 88rpx;
line-height: 88rpx;
font-size: 30rpx;
border-radius: 44rpx;
font-weight: 600;
}
.actions-bar button::after { display: none; }
/* 主按钮使用 flex: 1 铺满 */
.btn-full {
flex: 1;
}
.btn-primary {
background: $theme-primary;
color: #fff;
}
.btn-outline {
background: #fff;
color: $theme-primary;
border: 1rpx solid $theme-primary;
}
/* 次要操作(退款)给固定宽度 */
.btn-warn {
width: 220rpx;
background: #fff;
color: $color-danger;
border: 2rpx solid $color-danger;
}
</style>