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

653 lines
20 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>
<business-page-layout title="商品订单" :show-back="true">
<view class="order-list-container">
<!-- 终极修复弃用会导致冲突的 page-sticky-header -->
<!-- 改用原生 native-sticky-header绑定动态计算的导航栏高度精准吸附在 u-navbar 下方 -->
<view class="native-sticky-header" :style="{ top: navTopOffset + 'px' }">
<view class="header-solid-bg">
<view class="quick-filter">
<view class="filter-row date-row">
<picker mode="date" :value="dateStart" @change="onStart">
<view class="filter-picker date-picker">{{ dateStart || '开始日期' }}</view>
</picker>
<text class="sep"></text>
<picker mode="date" :value="dateEnd" @change="onEnd">
<view class="filter-picker date-picker">{{ dateEnd || '结束日期' }}</view>
</picker>
<view class="btn-query" @click="reload">查询</view>
</view>
</view>
<collapsible-filter-panel title="订单筛选" @clear="resetFilter">
<view class="filter-row">
<text class="filter-label">订单号</text>
<input class="filter-input" v-model="orderNo" placeholder="输入订单号" placeholder-style="color:#BDBDBD" />
</view>
<view class="filter-row">
<text class="filter-label">订单状态</text>
<picker :range="statusLabels" @change="onStatus">
<view class="filter-picker">{{ statusLabels[statusIndex] || '全部' }}</view>
</picker>
</view>
<view class="filter-row">
<text class="filter-label">发货方式</text>
<picker :range="deliveryLabels" @change="onDelivery">
<view class="filter-picker">{{ deliveryLabels[deliveryIndex] }}</view>
</picker>
</view>
<view class="filter-row">
<text class="filter-label">订单类型</text>
<picker :range="prescriptionTypeLabels" @change="onPrescriptionType">
<view class="filter-picker">{{ prescriptionTypeLabels[prescriptionTypeIndex] }}</view>
</picker>
</view>
</collapsible-filter-panel>
</view>
</view>
<!-- 统计卡片区 -->
<scroll-view v-if="statItems.length" class="stats-scroll" scroll-x>
<view class="stats">
<view v-for="s in statItems" :key="s.key" class="stat-card">
<text class="stat-label">{{ s.label }}</text>
<text class="stat-value">{{ s.value }}</text>
</view>
</view>
</scroll-view>
<!-- 顶级悬浮卡片列表 -->
<view class="card-list-wrap">
<view
v-for="item in list"
:key="item._wxKey"
class="premium-card"
hover-class="card-hover"
:hover-stay-time="150"
@click="goDetail(item)"
>
<!-- 顶部业务归属与状态 -->
<view class="card-header">
<view class="header-left">
<!-- 诊所信息提权放在最醒目的左上角 -->
<view class="clinic-tag" v-if="rowDisplay(item).storeName !== '-'">
<text class="clinic-name">{{ rowDisplay(item).storeName }}</text>
</view>
<view class="order-no-wrap">
<text class="lbl">单号</text>
<text class="val">{{ rowDisplay(item).orderNo }}</text>
</view>
</view>
<view class="status-text" :class="['status-' + (rowDisplay(item).status || 'default')]">
{{ rowDisplay(item).statusText }}
</view>
</view>
<!-- 核心商品与收件信息无边框内联排版节省大量高度 -->
<view class="card-body">
<!-- 药品名称与标签同行排版 -->
<view class="product-row">
<text class="product-name">{{ rowDisplay(item).productSummary || '药品订单' }}</text>
</view>
<view class="tags-row">
<text class="modern-tag tag-type">{{ rowDisplay(item).prescriptionTypeText }}</text>
<text class="modern-tag tag-delivery">{{ rowDisplay(item).deliveryText }}</text>
</view>
<!-- 去掉臃肿的灰色岛采用精致的文本网格 -->
<view class="info-grid">
<view
class="info-item"
v-if="rowDisplay(item).patientName !== '-' || rowDisplay(item).userNickname !== '-'"
@click.stop="openPatient(item)"
>
<text class="info-lbl">就诊人</text>
<text class="info-val link">{{ rowDisplay(item).patientName !== '-' ? rowDisplay(item).patientName : rowDisplay(item).userNickname }}</text>
</view>
<view class="info-item">
<text class="info-lbl">收件人</text>
<text class="info-val">
{{ rowDisplay(item).receiverName }}
<text class="info-sub" v-if="rowDisplay(item).receiverMobile">{{ rowDisplay(item).receiverMobile }}</text>
</text>
</view>
</view>
</view>
<!-- 底部时间价格与操作水平对齐 -->
<view class="card-footer">
<text class="time">{{ rowDisplay(item).createdAt }}</text>
<view class="footer-actions">
<view class="price-block">
<text class="price-lbl">实付</text>
<text class="currency">¥</text>
<text class="amount">{{ rowDisplay(item).totalPayPrice.replace('¥', '').trim() }}</text>
</view>
<view
v-if="rowDisplay(item).canViewPrescription"
class="btn-outline"
@click.stop="openPrescription(rowDisplay(item).pId)"
>
查看处方
</view>
</view>
</view>
</view>
</view>
<view v-if="!loading && !list.length" class="empty-state">
<text>暂无订单记录</text>
</view>
</view>
</business-page-layout>
<prescription-detail-popup ref="prescriptionPopup" />
<user-patient-detail
v-if="patientDetailVisible"
:visible="patientDetailVisible"
:up-id="patientDetailUpId"
:patient-name="patientDetailName"
@close="patientDetailVisible = false"
/>
</view>
</template>
<script>
// 这里已经移除了引入产生冲突的 PageStickyHeader以保证代码的纯净性
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
import CollapsibleFilterPanel from '@/subPackages/sub_business_shared/components/CollapsibleFilterPanel.vue'
import PrescriptionDetailPopup from './components/PrescriptionDetailPopup.vue'
import UserPatientDetail from '@/subPackages/sub_business_shared/components/UserPatientDetail.vue'
import { withWxKey } from '@/utils/wxListKey.js'
import { mapOrderListRow } from '@/subPackages/sub_business_shared/common/display.js'
import { formatMoney } from '@/subPackages/sub_business_shared/common/format.js'
import {
buildOrderListParams,
buildOrderSaleAmountParams,
} from '@/subPackages/sub_business_shared/common/orderParams.js'
import {
loadOrderFilter,
saveOrderFilter,
clearOrderFilter,
defaultOrderFilter,
} from '@/subPackages/sub_business_shared/common/filterCache.js'
import { defaultTodayDate } from '@/subPackages/sub_business_shared/common/reconciliationParams.js'
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
const DELIVERY_OPTIONS = [
{ label: '全部', value: '' },
{ label: '快递到家', value: 0 },
{ label: '诊所自提', value: 1 },
]
const PRESCRIPTION_TYPE_OPTIONS = [
{ label: '全部', value: '' },
{ label: '中药', value: 1 },
{ label: '西(中成)药', value: 2 },
{ label: '中成药', value: 3 },
{ label: '产品服务包', value: 5 },
]
export default {
mixins: [businessMixin],
components: { BusinessPageLayout, CollapsibleFilterPanel, PrescriptionDetailPopup, UserPatientDetail },
data() {
return {
list: [],
page: 1,
loading: false,
orderNo: '',
statusOptions: [{ label: '全部', value: '' }],
statusIndex: 0,
deliveryIndex: 0,
prescriptionTypeIndex: 0,
dateStart: '',
dateEnd: '',
statItems: [],
navTopOffset: 0,
patientDetailVisible: false,
patientDetailUpId: 0,
patientDetailName: '',
}
},
computed: {
statusLabels() {
return this.statusOptions.map(s => s.label || s.name || '全部')
},
deliveryLabels() {
return DELIVERY_OPTIONS.map(o => o.label)
},
prescriptionTypeLabels() {
return PRESCRIPTION_TYPE_OPTIONS.map(o => o.label)
},
},
created() {
// 动态计算顶部 u-navbar 的总高度 (状态栏 + navbar 固定 44px 高度)
// 这一步与 business-page-layout 内的计算逻辑保持完全一致,保证完美对接
const sys = uni.getSystemInfoSync()
const statusBarHeight = this.$statusBarHeight || sys.statusBarHeight || 0
const navbarHeight = this.$navbarHeight || 44
this.navTopOffset = statusBarHeight + navbarHeight
},
onLoad() {
this.applyOrderFilter(loadOrderFilter())
this.bizApi.getOrderStatusOption().then(w => {
const opts = (w.data && (w.data.items || w.data)) || []
if (Array.isArray(opts) && opts.length) {
this.statusOptions = [{ label: '全部', value: '' }].concat(opts.map(o => ({
label: o.label || o.name,
value: o.value != null ? o.value : o.id,
})))
}
})
this.checkDateEndAndReload()
},
/** 离开/隐藏时关掉处方抽屉,避免 page-container 残留白屏 */
onHide() {
this.closePrescriptionPopup()
},
onUnload() {
this.closePrescriptionPopup()
},
onReachBottom() {
this.load(this.page + 1, true)
},
methods: {
/** 关闭处方抽屉 */
closePrescriptionPopup() {
const pop = this.$refs.prescriptionPopup
if (pop && typeof pop.onClose === 'function') {
pop.onClose()
}
},
applyOrderFilter(f) {
this.orderNo = f.orderNo
this.statusIndex = f.statusIndex
this.deliveryIndex = f.deliveryIndex
this.prescriptionTypeIndex = f.prescriptionTypeIndex
this.dateStart = f.dateStart
this.dateEnd = f.dateEnd
},
persistOrderFilter() {
saveOrderFilter({
orderNo: this.orderNo,
statusIndex: this.statusIndex,
deliveryIndex: this.deliveryIndex,
prescriptionTypeIndex: this.prescriptionTypeIndex,
dateStart: this.dateStart,
dateEnd: this.dateEnd,
})
},
resetFilter() {
this.applyOrderFilter(defaultOrderFilter())
clearOrderFilter()
this.reload()
},
checkDateEndAndReload() {
const today = defaultTodayDate()
if (this.dateEnd && this.dateEnd !== today) {
uni.showModal({
title: '提示',
content: '筛选结束日期不是今天,是否更新到今天?',
success: (r) => {
if (r.confirm) {
this.dateEnd = today
this.persistOrderFilter()
}
this.reload()
},
})
return
}
this.reload()
},
filterValues() {
const st = this.statusOptions[this.statusIndex]
const del = DELIVERY_OPTIONS[this.deliveryIndex]
const pt = PRESCRIPTION_TYPE_OPTIONS[this.prescriptionTypeIndex]
return {
orderNo: this.orderNo.trim(),
status: st && st.value !== '' && st.value != null ? st.value : undefined,
deliveryMethod: del.value !== '' ? del.value : undefined,
prescriptionType: pt.value !== '' ? pt.value : undefined,
dateStart: this.dateStart,
dateEnd: this.dateEnd,
}
},
rowDisplay(item) {
return mapOrderListRow(item)
},
reload() {
this.persistOrderFilter()
this.page = 1
this.load(1, false)
this.loadSaleAmount()
},
loadSaleAmount() {
const params = buildOrderSaleAmountParams(this.filterValues())
this.bizApi.getOrderSaleAmount(params).then(w => {
if (!w.ok || !w.data) {
this.statItems = []
return
}
const d = w.data
this.statItems = [
{ key: 'total', label: '销售金额', value: formatMoney(d.total) },
{ key: 'income', label: '我的收益', value: formatMoney(d.income) },
]
})
},
load(page, append) {
this.loading = true
const params = buildOrderListParams({
page,
pageSize: 15,
...this.filterValues(),
})
this.bizApi.getOrderList(params).then(w => {
const items = withWxKey((w.data && w.data.items) || [], 'id', 'ord')
this.list = append ? this.list.concat(items) : items
this.page = page
}).finally(() => { this.loading = false })
},
onStatus(e) {
this.statusIndex = Number(e.detail.value)
},
onDelivery(e) {
this.deliveryIndex = Number(e.detail.value)
},
onPrescriptionType(e) {
this.prescriptionTypeIndex = Number(e.detail.value)
},
onStart(e) { this.dateStart = e.detail.value },
onEnd(e) { this.dateEnd = e.detail.value },
goDetail(item) {
uni.navigateTo({ url: `${this.bizRoot}/order/detail?id=${item.id}` })
},
openPrescription(pId) {
if (!pId) return
this.$refs.prescriptionPopup.open(pId)
},
/** 打开患者视角详情抽屉 */
openPatient(item) {
const d = this.rowDisplay(item)
const upId = Number(d.upId || 0)
if (!upId) {
uni.showToast({ title: '缺少就诊人信息', icon: 'none' })
return
}
this.patientDetailUpId = upId
this.patientDetailName = d.patientName !== '-' ? d.patientName : ''
this.patientDetailVisible = true
},
},
}
</script>
<style lang="scss" scoped>
$theme-primary: #6acdbb;
$color-bg-base: #F2F4F7;
$color-text-main: #1D2129;
$color-text-body: #4E5969;
$color-text-muted: #86909C;
$color-border: #E5E6EB;
.order-list-container {
background-color: $color-bg-base;
min-height: 100vh;
padding-bottom: 60rpx;
}
/* ================= 核心:完美吸顶方案 ================= */
/* 原生 sticky 结合 Vue 计算出的精确导航栏高度(top),严丝合缝拦截在导航栏正下方 */
.native-sticky-header {
position: -webkit-sticky;
position: sticky;
z-index: 100; /* 高层级,保证滑动时覆盖在下方的卡片上 */
background-color: $color-bg-base; /* 必须有底色用于防穿透遮挡 */
}
.header-solid-bg {
/* 使用 padding 将内容向内挤,防止滑动过程的透明间隙穿透问题 */
padding: 16rpx 24rpx 16rpx;
}
/* 筛选项 UI 美化 */
.quick-filter {
background: #fff;
padding: 16rpx 20rpx;
border-radius: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.filter-row {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 24rpx;
&:last-child { margin-bottom: 0; }
}
.date-row {
margin-bottom: 0;
justify-content: space-between;
}
.filter-label {
font-size: 28rpx;
color: $color-text-body;
min-width: 130rpx;
}
.filter-input, .filter-picker {
flex: 1;
font-size: 26rpx;
padding: 14rpx 20rpx;
background: #F7F8FA;
border-radius: 12rpx;
color: $color-text-main;
}
.date-picker { text-align: center; }
.sep { color: $color-text-muted; font-size: 24rpx; }
/* 重新设计的紧凑型查询按钮 */
.btn-query {
flex-shrink: 0;
background: $theme-primary;
color: #fff;
padding: 0 32rpx;
height: 60rpx;
line-height: 60rpx;
border-radius: 30rpx; /* 胶囊圆角 */
font-size: 26rpx;
font-weight: 500;
box-shadow: 0 4rpx 12rpx rgba(106, 205, 187, 0.3);
transition: opacity 0.2s;
&:active { opacity: 0.8; }
}
/* 统计卡片 */
.stats-scroll { margin-bottom: 16rpx; white-space: nowrap; }
.stats { display: inline-flex; gap: 20rpx; padding: 0 24rpx; }
.stat-card {
width: 300rpx;
background: #fff;
padding: 24rpx;
border-radius: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
}
.stat-label { display: block; font-size: 24rpx; color: $color-text-muted; }
.stat-value { display: block; font-size: 38rpx; color: $color-text-main; font-weight: 700; margin-top: 8rpx; }
/* ================= 核心高级卡片样式 ================= */
.card-list-wrap {
padding: 0 24rpx;
}
.premium-card {
background: #ffffff;
border-radius: 24rpx;
margin-bottom: 24rpx;
padding: 28rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
border: 1px solid rgba(229, 230, 235, 0.5);
}
.card-hover {
transform: scale(0.99);
background-color: #FAFAFA;
}
/* 卡片头部 */
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20rpx;
}
.header-left {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.clinic-tag {
display: inline-flex;
}
.clinic-name {
font-size: 24rpx;
color: $color-text-main;
background: #F2F3F5;
padding: 4rpx 16rpx;
border-radius: 8rpx;
font-weight: 500;
}
.order-no-wrap {
font-size: 24rpx;
display: flex;
align-items: center;
gap: 12rpx;
}
.order-no-wrap .lbl { color: $color-text-muted; }
.order-no-wrap .val { color: $color-text-body; font-family: monospace; letter-spacing: 0.5rpx;}
.status-text {
font-size: 26rpx;
font-weight: 600;
color: $theme-primary;
margin-top: 4rpx;
}
.status-done { color: $color-text-muted; font-weight: 400; }
.status-cancel { color: #F53F3F; }
/* 卡片主体 */
.card-body {
margin-bottom: 24rpx;
}
.product-row {
margin-bottom: 12rpx;
}
.product-name {
font-size: 30rpx; /* 适中的药品名称 */
font-weight: 600;
color: $color-text-main;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.tags-row {
display: flex;
gap: 12rpx;
margin-bottom: 20rpx;
}
.modern-tag {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 6rpx;
font-weight: 500;
}
.tag-type { background: #FFF3E8; color: #FF7D00; }
.tag-delivery { background: #E8F3FF; color: #165DFF; }
/* 雅致的信息网格排版 */
.info-grid {
display: flex;
flex-direction: column;
gap: 8rpx;
background: transparent; /* 去除灰底 */
}
.info-item {
display: flex;
font-size: 26rpx;
line-height: 1.5;
}
.info-lbl {
color: $color-text-muted;
width: 110rpx;
flex-shrink: 0;
}
.info-val {
color: $color-text-body;
flex: 1;
&.link {
color: $theme-primary;
}
}
.info-sub {
color: $color-text-muted;
margin-left: 12rpx;
font-size: 24rpx;
}
/* 卡片底部 */
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1rpx dashed $color-border;
padding-top: 24rpx;
}
.time {
font-size: 24rpx;
color: $color-text-muted;
}
.footer-actions {
display: flex;
align-items: center;
gap: 24rpx;
}
.price-block {
display: flex;
align-items: baseline;
}
.price-lbl {
font-size: 22rpx;
color: $color-text-muted;
margin-right: 8rpx;
}
.currency {
font-size: 24rpx;
color: $color-text-main;
font-weight: 600;
}
.amount {
font-size: 36rpx;
font-weight: 700;
color: $color-text-main;
font-family: DIN Alternate, SF Pro Display, sans-serif;
}
/* 美化的边框按钮 */
.btn-outline {
font-size: 26rpx;
color: $theme-primary;
border: 1px solid $theme-primary;
padding: 8rpx 28rpx;
border-radius: 30rpx;
font-weight: 500;
background: transparent;
transition: all 0.2s;
&:active {
background: rgba(106, 205, 187, 0.08);
}
}
.empty-state { text-align: center; color: $color-text-muted; padding: 120rpx 0; font-size: 28rpx; }
</style>