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

723 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>
<clinic-page-layout title="商品订单" :show-back="true">
<view class="order-list-container">
<!-- 顶部筛选 -->
<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-head">
<view class="order-id">
<text class="id-icon"></text>
<text class="id-text">{{ rowDisplay(item).orderNo }}</text>
</view>
<view
class="status-badge"
:class="['status-' + (rowDisplay(item).status || 'default')]"
>
{{ rowDisplay(item).statusText }}
</view>
</view>
<view class="card-body">
<!-- 核心视觉区商品与金额 -->
<view class="main-info">
<view class="title-wrap">
<text class="goods-title">{{ rowDisplay(item).productSummary || '药品订单' }}</text>
<view class="goods-tags">
<text class="modern-tag tag-type">{{ rowDisplay(item).prescriptionTypeText }}</text>
<text class="modern-tag tag-delivery">{{ rowDisplay(item).deliveryText }}</text>
</view>
</view>
<view class="price-wrap">
<text class="currency">¥</text>
<text class="amount">{{ rowDisplay(item).totalPayPrice.replace('¥', '').trim() }}</text>
</view>
</view>
<!-- 信息岛 (Info Island)聚合次要信息的高级灰底区块 -->
<view class="info-island">
<view
class="island-row"
v-if="rowDisplay(item).patientName !== '-' || rowDisplay(item).userNickname !== '-'"
@click.stop="openPatient(item)"
>
<text class="island-label">就诊人</text>
<text class="island-value link">{{ rowDisplay(item).patientName !== '-' ? rowDisplay(item).patientName : rowDisplay(item).userNickname }}</text>
</view>
<view class="island-row">
<text class="island-label">收件人</text>
<text class="island-value">
{{ rowDisplay(item).receiverName }}
<text class="muted" v-if="rowDisplay(item).receiverMobile"> · {{ rowDisplay(item).receiverMobile }}</text>
</text>
</view>
<view class="island-row" v-if="rowDisplay(item).storeName !== '-'">
<text class="island-label">开方诊所</text>
<text class="island-value">{{ rowDisplay(item).storeName }}</text>
</view>
</view>
</view>
<!-- 底部时间与操作按键 -->
<view class="card-foot">
<text class="time">{{ rowDisplay(item).createdAt }}</text>
<view class="actions">
<view
v-if="rowDisplay(item).canViewPrescription"
class="btn-ghost"
@click.stop="openPrescription(rowDisplay(item).pId)"
>
查看处方
</view>
</view>
</view>
</view>
</view>
<view v-if="!loading && !list.length" class="empty-state">
<text>暂无订单记录</text>
</view>
</view>
</clinic-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>
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
import PageStickyHeader from '@/subPackages/sub_clinic_admin/components/PageStickyHeader.vue'
import CollapsibleFilterPanel from '@/subPackages/sub_clinic_admin/components/CollapsibleFilterPanel.vue'
import PrescriptionDetailPopup from './components/PrescriptionDetailPopup.vue'
import UserPatientDetail from '@/subPackages/sub_business_shared/components/UserPatientDetail.vue'
import { getOrderList, getOrderStatusOption, getOrderSaleAmount } from '@/api/clinicAdmin.js'
import { withWxKey } from '@/utils/wxListKey.js'
import { mapOrderListRow } from '@/subPackages/sub_clinic_admin/common/display.js'
import { formatMoney } from '@/subPackages/sub_clinic_admin/common/format.js'
import {
buildOrderListParams,
buildOrderSaleAmountParams,
} from '@/subPackages/sub_clinic_admin/common/orderParams.js'
import {
loadOrderFilter,
saveOrderFilter,
clearOrderFilter,
defaultOrderFilter,
} from '@/subPackages/sub_clinic_admin/common/filterCache.js'
import { defaultTodayDate } from '@/subPackages/sub_clinic_admin/common/reconciliationParams.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 {
components: { ClinicPageLayout, PageStickyHeader, CollapsibleFilterPanel, PrescriptionDetailPopup, UserPatientDetail },
data() {
return {
list: [],
page: 1,
loading: false,
orderNo: '',
statusOptions: [{ label: '全部', value: '' }],
statusIndex: 0,
deliveryIndex: 0,
prescriptionTypeIndex: 0,
dateStart: '',
dateEnd: '',
statItems: [],
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)
},
},
onLoad() {
this.applyOrderFilter(loadOrderFilter())
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())
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(),
})
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: '/subPackages/sub_clinic_admin/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; /* Apple 风格的冷调高级灰底色 */
$color-text-main: #1D2129;
$color-text-body: #4E5969;
$color-text-muted: #86909C;
.order-list-container {
background-color: $color-bg-base;
min-height: 100vh;
padding-bottom: 60rpx;
}
/* 筛选区 */
.quick-filter {
background: #fff;
padding: 24rpx;
margin-bottom: 16rpx;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.filter-row {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 24rpx;
}
.filter-label {
font-size: 28rpx;
color: $color-text-body;
min-width: 140rpx;
}
.filter-input, .filter-picker {
flex: 1;
font-size: 28rpx;
padding: 16rpx 20rpx;
background: #F7F8FA;
border-radius: 12rpx;
color: $color-text-main;
}
.date-row { flex-wrap: nowrap; }
.date-picker { text-align: center; }
.sep { color: $color-text-muted; margin: 0 8rpx; font-size: 24rpx; }
.filter-query-btn {
text-align: center;
padding: 24rpx;
background: $theme-primary;
color: #fff;
border-radius: 16rpx;
font-size: 30rpx;
font-weight: 600;
margin-top: 32rpx;
box-shadow: 0 8rpx 24rpx rgba(106, 205, 187, 0.2);
}
/* 统计卡片 */
.stats-scroll {
margin-bottom: 20rpx;
white-space: nowrap;
}
.stats {
display: inline-flex;
gap: 20rpx;
padding: 16rpx 32rpx;
}
.stat-card {
display: inline-block;
width: 320rpx;
background: #fff;
padding: 32rpx;
border-radius: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
vertical-align: top;
white-space: normal;
}
.stat-label {
display: block;
font-size: 26rpx;
color: $color-text-muted;
}
.stat-value {
display: block;
font-size: 42rpx;
color: $color-text-main;
font-weight: 700;
margin-top: 12rpx;
}
/* ================= 核心高级卡片样式 ================= */
.card-list-wrap {
//padding: 0 24rpx;
}
.premium-card {
background: #ffffff;
border-radius: 32rpx; /* 超大圆角带来前沿视觉享受 */
margin-bottom: 24rpx;
padding: 32rpx;
border: 1rpx solid #E5E6EB;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.015); /* 极其克制的阴影 */
transition: all 0.2s ease;
}
.card-hover {
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.01);
background-color: #FAFAFA;
}
/* 卡片头部 */
.card-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 28rpx;
}
.order-id {
display: flex;
align-items: center;
gap: 12rpx;
}
.id-icon {
font-size: 20rpx;
color: #fff;
background-color: #C9CDD4;
padding: 4rpx 12rpx;
border-radius: 6rpx;
font-weight: 600;
}
.id-text {
font-size: 26rpx;
color: $color-text-muted;
font-family: SF Pro Text, monospace, sans-serif;
letter-spacing: 0.5rpx;
}
/* 状态标签配色优化 */
.status-badge {
font-size: 26rpx;
font-weight: 600;
color: $theme-primary;
}
/* 可根据实际的状态值(status)配置置灰 */
.status-done { color: $color-text-muted; font-weight: 400; }
.status-cancel { color: #F53F3F; }
/* 核心内容区 */
.card-body {
margin-bottom: 24rpx;
}
.main-info {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24rpx;
gap: 20rpx;
}
.title-wrap {
flex: 1;
}
.goods-title {
font-size: 32rpx;
font-weight: 600;
color: $color-text-main;
line-height: 1.4;
margin-bottom: 16rpx;
/* 多行截断 */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.goods-tags {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.modern-tag {
font-size: 22rpx;
padding: 4rpx 14rpx;
border-radius: 8rpx;
font-weight: 500;
}
.tag-type { background: #FFF3E8; color: #FF7D00; } /* 柔和爱马仕橙 */
.tag-delivery { background: #E8F3FF; color: #165DFF; } /* 科技蓝 */
.price-wrap {
display: flex;
align-items: baseline;
color: $color-text-main;
}
.currency {
font-size: 28rpx;
font-weight: 600;
margin-right: 4rpx;
}
.amount {
font-size: 44rpx;
font-weight: 700;
font-family: DIN Alternate, SF Pro Display, sans-serif;
}
/* 高级信息岛 (Inset Info Box) */
.info-island {
background-color: #F7F8FA;
border-radius: 16rpx;
padding: 24rpx;
}
.island-row {
display: flex;
font-size: 24rpx;
line-height: 1.6;
margin-bottom: 12rpx;
&:last-child {
margin-bottom: 0;
}
.island-label {
color: $color-text-muted;
width: 110rpx;
flex-shrink: 0;
}
.island-value {
color: $color-text-body;
flex: 1;
word-break: break-all;
&.link {
color: $theme-primary;
}
}
.muted {
color: $color-text-muted;
}
}
/* 卡片底部 */
.card-foot {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1rpx solid #F2F3F5;
padding-top: 24rpx;
}
.time {
font-size: 24rpx;
color: $color-text-muted;
}
.btn-ghost {
font-size: 26rpx;
color: $theme-primary;
background: rgba(106, 205, 187, 0.08);
padding: 10rpx 32rpx;
border-radius: 30rpx;
font-weight: 500;
transition: background-color 0.2s;
&:active {
background: rgba(106, 205, 187, 0.15);
}
}
.empty-state {
text-align: center;
color: $color-text-muted;
padding: 120rpx 0;
font-size: 28rpx;
}
/* ================= 核心:完美吸顶方案 ================= */
/* 原生 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; }
}
</style>