1. 诊所管理

This commit is contained in:
李琦
2026-06-18 10:52:42 +08:00
parent 73d535378e
commit 66a2243042
8 changed files with 558 additions and 472 deletions

View File

@@ -1,45 +1,50 @@
<template>
<business-page-layout title="商品订单" :show-back="true">
<view class="order-list-container">
<!-- 顶部筛选 -->
<page-sticky-header>
<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>
<!-- 终极修复弃用会导致冲突的 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>
<view class="filter-query-btn" @click="reload">查询</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>
<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>
</page-sticky-header>
</view>
<!-- 统计卡片区 -->
<scroll-view v-if="statItems.length" class="stats-scroll" scroll-x>
@@ -61,63 +66,62 @@
: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 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-badge"
:class="['status-' + (rowDisplay(item).status || 'default')]"
>
<view class="status-text" :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 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>
<!-- 信息岛 (Info Island)聚合次要信息的高级灰底区块 -->
<view class="info-island">
<view class="island-row" v-if="rowDisplay(item).userNickname !== '-'">
<text class="island-label">就诊人</text>
<text class="island-value">{{ rowDisplay(item).userNickname }}</text>
<!-- 去掉臃肿的灰色岛采用精致的文本网格 -->
<view class="info-grid">
<view class="info-item" v-if="rowDisplay(item).userNickname !== '-'">
<text class="info-lbl">就诊人</text>
<text class="info-val">{{ rowDisplay(item).userNickname }}</text>
</view>
<view class="island-row">
<text class="island-label">收件人</text>
<text class="island-value">
<view class="info-item">
<text class="info-lbl">收件人</text>
<text class="info-val">
{{ rowDisplay(item).receiverName }}
<text class="muted" v-if="rowDisplay(item).receiverMobile"> · {{ rowDisplay(item).receiverMobile }}</text>
<text class="info-sub" 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">
<!-- 底部时间价格与操作水平对齐 -->
<view class="card-footer">
<text class="time">{{ rowDisplay(item).createdAt }}</text>
<view class="actions">
<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-ghost"
class="btn-outline"
@click.stop="openPrescription(rowDisplay(item).pId)"
>
查看处方
@@ -137,8 +141,8 @@
</template>
<script>
// 这里已经移除了引入产生冲突的 PageStickyHeader以保证代码的纯净性
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
import PageStickyHeader from '@/subPackages/sub_business_shared/components/PageStickyHeader.vue'
import CollapsibleFilterPanel from '@/subPackages/sub_business_shared/components/CollapsibleFilterPanel.vue'
import PrescriptionDetailPopup from './components/PrescriptionDetailPopup.vue'
import { withWxKey } from '@/utils/wxListKey.js'
@@ -172,7 +176,7 @@ const PRESCRIPTION_TYPE_OPTIONS = [
export default {
mixins: [businessMixin],
components: { BusinessPageLayout, PageStickyHeader, CollapsibleFilterPanel, PrescriptionDetailPopup },
components: { BusinessPageLayout, CollapsibleFilterPanel, PrescriptionDetailPopup },
data() {
return {
list: [],
@@ -186,6 +190,7 @@ export default {
dateStart: '',
dateEnd: '',
statItems: [],
navTopOffset: 0, // 新增:用于存放导航栏计算后的确切高度
}
},
computed: {
@@ -199,6 +204,14 @@ export default {
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 => {
@@ -330,10 +343,11 @@ export default {
<style lang="scss" scoped>
$theme-primary: #6acdbb;
$color-bg-base: #F2F4F7; /* Apple 风格的冷调高级灰底色 */
$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;
@@ -341,271 +355,253 @@ $color-text-muted: #86909C;
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: 24rpx;
margin-bottom: 16rpx;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
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: 140rpx;
min-width: 130rpx;
}
.filter-input, .filter-picker {
flex: 1;
font-size: 28rpx;
padding: 16rpx 20rpx;
font-size: 26rpx;
padding: 14rpx 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;
.sep { color: $color-text-muted; font-size: 24rpx; }
/* 重新设计的紧凑型查询按钮 */
.btn-query {
flex-shrink: 0;
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);
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: 20rpx;
white-space: nowrap;
}
.stats {
display: inline-flex;
gap: 20rpx;
padding: 16rpx 32rpx;
}
.stats-scroll { margin-bottom: 16rpx; white-space: nowrap; }
.stats { display: inline-flex; gap: 20rpx; padding: 0 24rpx; }
.stat-card {
display: inline-block;
width: 320rpx;
width: 300rpx;
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;
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;
padding: 0 24rpx;
}
.premium-card {
background: #ffffff;
border-radius: 32rpx; /* 超大圆角带来前沿视觉享受 */
border-radius: 24rpx;
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;
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.98);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.01);
transform: scale(0.99);
background-color: #FAFAFA;
}
/* 卡片头部 */
.card-head {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 28rpx;
align-items: flex-start;
margin-bottom: 20rpx;
}
.order-id {
.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;}
.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 {
.status-text {
font-size: 26rpx;
font-weight: 600;
color: $theme-primary;
margin-top: 4rpx;
}
/* 可根据实际的状态值(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;
.product-row {
margin-bottom: 12rpx;
}
.title-wrap {
flex: 1;
}
.goods-title {
font-size: 32rpx;
.product-name {
font-size: 30rpx; /* 适中的药品名称 */
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 {
.tags-row {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-bottom: 20rpx;
}
.modern-tag {
font-size: 22rpx;
padding: 4rpx 14rpx;
border-radius: 8rpx;
padding: 4rpx 12rpx;
border-radius: 6rpx;
font-weight: 500;
}
.tag-type { background: #FFF3E8; color: #FF7D00; } /* 柔和爱马仕橙 */
.tag-delivery { background: #E8F3FF; color: #165DFF; } /* 科技蓝 */
.tag-type { background: #FFF3E8; color: #FF7D00; }
.tag-delivery { background: #E8F3FF; color: #165DFF; }
.price-wrap {
/* 雅致的信息网格排版 */
.info-grid {
display: flex;
align-items: baseline;
color: $color-text-main;
flex-direction: column;
gap: 8rpx;
background: transparent; /* 去除灰底 */
}
.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 {
.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;
}
.info-sub {
color: $color-text-muted;
margin-left: 12rpx;
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;
}
.muted {
color: $color-text-muted;
}
}
/* 卡片底部 */
.card-foot {
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1rpx solid #F2F3F5;
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-ghost {
/* 美化的边框按钮 */
.btn-outline {
font-size: 26rpx;
color: $theme-primary;
background: rgba(106, 205, 187, 0.08);
padding: 10rpx 32rpx;
border: 1px solid $theme-primary;
padding: 8rpx 28rpx;
border-radius: 30rpx;
font-weight: 500;
transition: background-color 0.2s;
background: transparent;
transition: all 0.2s;
&:active {
background: rgba(106, 205, 187, 0.15);
background: rgba(106, 205, 187, 0.08);
}
}
.empty-state {
text-align: center;
color: $color-text-muted;
padding: 120rpx 0;
font-size: 28rpx;
}
.empty-state { text-align: center; color: $color-text-muted; padding: 120rpx 0; font-size: 28rpx; }
</style>

View File

@@ -1,15 +1,15 @@
<template>
<page-layout :is-platform="true" :title="title" :show-back="showBack" :show-tabbar="showTabbar" :tab-index="tabIndex">
<business-page-layout :title="title" :show-back="showBack" :show-tabbar="showTabbar" :tab-index="tabIndex">
<slot />
</page-layout>
</business-page-layout>
</template>
<script>
import PageLayout from '@/subPackages/sub_salesperson_manage/components/PageLayout.vue';
import BusinessPageLayout from '@/subPackages/sub_platform_admin/components/business-page-layout.vue';
export default {
name: 'StorePageLayout',
components: { PageLayout },
components: { BusinessPageLayout },
props: {
title: { type: String, default: '' },
showBack: { type: Boolean, default: true },

View File

@@ -139,7 +139,7 @@ export function createDefaultStoreInputForm() {
position: '',
province_id: null,
city_id: null,
clinic_type: 2,
clinic_type: 1,
type: 0,
is_shipping_free: 0,
subscribe_price_change: 0,

View File

@@ -12,6 +12,7 @@
:value="show"
mode="bottom"
border-radius="24"
:z-index="10090"
:safe-area-inset-bottom="true"
:mask-close-able="maskCloseable"
@input="onPopupInput"

View File

@@ -74,8 +74,6 @@ export default {
<style scoped>
.picker-wrap {
width: 100%;
position: relative;
z-index: 1;
}
.promoter-picker {
display: flex;

View File

@@ -1,196 +1,244 @@
<template>
<view class="picker-wrap">
<view class="region-trigger" @click="openDrawer">
<text :class="displayText ? 'region-text' : 'region-placeholder'">{{ displayText || placeholder }}</text>
<u-icon name="arrow-right" color="#c0c4cc" size="28"></u-icon>
</view>
<drawer-page-container v-model="show" title="选择省市区">
<view class="drawer-body">
<view class="search-box">
<u-input v-model="keyword" placeholder="搜索省/市/区" clearable />
</view>
<scroll-view scroll-x class="crumb-scroll">
<view class="crumbs">
<text
v-for="(item, idx) in breadcrumbs"
:key="item.rowKey"
class="crumb"
@click="onCrumb(item)"
>{{ item.label }}<text v-if="idx < breadcrumbs.length - 1" class="crumb-sep"> / </text></text>
<text v-if="!breadcrumbs.length" class="crumb muted">请选择</text>
</view>
</scroll-view>
<scroll-view scroll-y class="list-scroll">
<view
v-for="row in listRows"
:key="row.rowKey"
class="list-row"
@click="onPickRow(row)"
>
<text class="row-label">{{ isSearching ? row.label : row.node.label }}</text>
<text v-if="!isSearching && rowHasNextLevel(row, ctx)" class="row-arrow"></text>
</view>
<view v-if="!listRows.length" class="empty">无匹配结果</view>
</scroll-view>
</view>
</drawer-page-container>
</view>
</template>
<script>
import DrawerPageContainer from '@/subPackages/sub_salesperson/components/DrawerPageContainer.vue'
import {
formatAddressDisplay,
resolveAddressValue,
buildAddressOutput,
buildBreadcrumbFromContext,
getInitialPickerLevel,
levelOptionsToRows,
searchAddressHits,
searchHitsToRows,
applySearchHit,
rowHasNextLevel,
truncateContextToBreadcrumb,
getNextLevel,
canCompleteAfterSelect,
} from '@/subPackages/sub_clinic_salesperson/utils/address-index.js'
export default {
name: 'RegionAddressPicker',
components: { DrawerPageContainer },
props: {
value: { type: Array, default: () => [] },
placeholder: { type: String, default: '请选择省市区' },
},
data() {
return {
show: false,
keyword: '',
currentLevel: 'province',
ctx: { provinceId: undefined, cityId: undefined, districtId: undefined },
}
},
computed: {
displayText() {
return formatAddressDisplay(this.value)
},
isSearching() {
return !!(this.keyword || '').trim()
},
breadcrumbs() {
return buildBreadcrumbFromContext(this.ctx).map((item) => ({
...item,
rowKey: item.level + '-' + item.id,
}))
},
listRows() {
const rows = this.isSearching
? searchHitsToRows(searchAddressHits(this.keyword, this.ctx))
: levelOptionsToRows(this.currentLevel, this.ctx)
return rows.map((row) => ({
...row,
rowKey: row.level + '-' + row.node.value,
}))
},
},
methods: {
rowHasNextLevel,
syncFromValue() {
const resolved = resolveAddressValue(this.value)
this.ctx = {
provinceId: resolved.provinceId,
cityId: resolved.cityId,
districtId: resolved.districtId,
}
this.currentLevel = getInitialPickerLevel(resolved)
this.keyword = ''
},
openDrawer() {
this.syncFromValue()
this.show = true
},
emitValue() {
const out = buildAddressOutput(this.ctx.provinceId, this.ctx.cityId, this.ctx.districtId)
if (out) this.$emit('change', out)
},
onCrumb(item) {
this.ctx = truncateContextToBreadcrumb(this.ctx, item)
this.currentLevel = item.level
this.keyword = ''
},
onPickRow(row) {
if (this.isSearching) {
const hit = searchAddressHits(this.keyword, this.ctx).find(
(h) => h.level === row.level && h.node.value === row.node.value
)
if (!hit) return
this.ctx = applySearchHit(hit, this.ctx)
if (hit.complete) {
this.emitValue()
this.show = false
return
}
const next = getNextLevel(hit.level, this.ctx)
this.currentLevel = next || hit.level
this.keyword = ''
return
}
const pickCtx = row.level === 'city'
? { provinceId: this.ctx.provinceId, cityId: row.node.value }
: row.level === 'district'
? { provinceId: this.ctx.provinceId, cityId: this.ctx.cityId, districtId: row.node.value }
: { provinceId: row.node.value, cityId: undefined, districtId: undefined }
if (row.level === 'province') {
this.ctx = { provinceId: row.node.value, cityId: undefined, districtId: undefined }
this.currentLevel = getNextLevel('province', this.ctx) || 'province'
} else if (row.level === 'city') {
this.ctx = { ...this.ctx, cityId: row.node.value, districtId: undefined }
if (canCompleteAfterSelect('city', pickCtx, row.node)) {
this.emitValue()
this.show = false
return
}
this.currentLevel = getNextLevel('city', this.ctx) || 'city'
} else if (row.level === 'district') {
this.ctx = { ...this.ctx, districtId: row.node.value }
this.emitValue()
this.show = false
}
},
},
}
</script>
<style scoped>
.picker-wrap { width: 100%; }
.region-trigger {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
min-height: 72rpx;
}
.region-text { font-size: 28rpx; color: #303133; flex: 1; }
.region-placeholder { font-size: 28rpx; color: #c0c4cc; flex: 1; }
.drawer-body { display: flex; flex-direction: column; max-height: 70vh; }
.search-box { padding: 16rpx 24rpx; flex-shrink: 0; }
.crumb-scroll { white-space: nowrap; padding: 0 24rpx 16rpx; flex-shrink: 0; }
.crumbs { display: inline-flex; flex-wrap: nowrap; }
.crumb { font-size: 26rpx; color: #6acdbb; }
.crumb.muted { color: #86909c; }
.crumb-sep { color: #c0c4cc; }
.list-scroll { flex: 1; max-height: 52vh; }
.list-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 32rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.row-label { font-size: 28rpx; color: #1d2129; flex: 1; }
.row-arrow { font-size: 36rpx; color: #c0c4cc; }
.empty { text-align: center; color: #86909c; padding: 80rpx 0; font-size: 28rpx; }
</style>
<template>
<view class="picker-wrap">
<view class="region-trigger" @click="openDrawer">
<text :class="displayText ? 'region-text' : 'region-placeholder'">{{ displayText || placeholder }}</text>
<u-icon name="arrow-right" color="#c0c4cc" size="28"></u-icon>
</view>
<drawer-page-container v-model="show" title="选择省市区">
<view class="drawer-body" :style="drawerBodyStyle">
<view class="search-box">
<u-input v-model="keyword" placeholder="搜索省/市/区" clearable :adjust-position="false" />
</view>
<scroll-view scroll-x class="crumb-scroll">
<view class="crumbs">
<text
v-for="(item, idx) in breadcrumbs"
:key="item.rowKey"
class="crumb"
@click="onCrumb(item)"
>{{ item.label }}<text v-if="idx < breadcrumbs.length - 1" class="crumb-sep"> / </text></text>
<text v-if="!breadcrumbs.length" class="crumb muted">请选择</text>
</view>
</scroll-view>
<scroll-view scroll-y class="list-scroll" :style="listScrollStyle">
<view
v-for="row in listRows"
:key="row.rowKey"
class="list-row"
@click="onPickRow(row)"
>
<text class="row-label">{{ isSearching ? row.label : row.node.label }}</text>
<text v-if="!isSearching && rowHasNextLevel(row, ctx)" class="row-arrow"></text>
</view>
<view v-if="!listRows.length" class="empty">无匹配结果</view>
</scroll-view>
</view>
</drawer-page-container>
</view>
</template>
<script>
import DrawerPageContainer from '@/subPackages/sub_salesperson/components/DrawerPageContainer.vue'
import {
formatAddressDisplay,
resolveAddressValue,
buildAddressOutput,
buildBreadcrumbFromContext,
getInitialPickerLevel,
levelOptionsToRows,
searchAddressHits,
searchHitsToRows,
applySearchHit,
rowHasNextLevel,
truncateContextToBreadcrumb,
getNextLevel,
canCompleteAfterSelect,
} from '@/subPackages/sub_clinic_salesperson/utils/address-index.js'
export default {
name: 'RegionAddressPicker',
components: { DrawerPageContainer },
props: {
value: { type: Array, default: () => [] },
placeholder: { type: String, default: '请选择省市区' },
},
data() {
return {
show: false,
keyword: '',
currentLevel: 'province',
ctx: { provinceId: undefined, cityId: undefined, districtId: undefined },
keyboardHeight: 0,
}
},
computed: {
displayText() {
return formatAddressDisplay(this.value)
},
isSearching() {
return !!(this.keyword || '').trim()
},
breadcrumbs() {
return buildBreadcrumbFromContext(this.ctx).map((item) => ({
...item,
rowKey: item.level + '-' + item.id,
}))
},
listRows() {
const rows = this.isSearching
? searchHitsToRows(searchAddressHits(this.keyword, this.ctx))
: levelOptionsToRows(this.currentLevel, this.ctx)
return rows.map((row) => ({
...row,
rowKey: row.level + '-' + row.node.value,
}))
},
drawerBodyStyle() {
if (!this.keyboardHeight) {
return {}
}
return {
paddingBottom: this.keyboardHeight + 'px',
maxHeight: `calc(70vh - ${this.keyboardHeight}px)`,
}
},
listScrollStyle() {
if (!this.keyboardHeight) {
return {}
}
return {
maxHeight: `calc(52vh - ${this.keyboardHeight}px)`,
minHeight: '240rpx',
}
},
},
watch: {
show(val) {
if (val) {
this.bindKeyboardListener()
return
}
this.keyboardHeight = 0

View File

@@ -35,19 +35,40 @@
/>
</u-form-item>
<u-form-item label="诊所类型" right-icon="arrow-right">
<u-input :value="clinicTypes[clinicTypeIndex].label" type="select" @click="showClinicType = true" placeholder="请选择" />
<u-select v-model="showClinicType" :list="clinicTypes" @confirm="onClinicTypeConfirm"></u-select>
<u-form-item label="诊所类型" :border-bottom="false">
<view class="clinic-type-cards">
<view
v-for="(item, idx) in clinicTypes"
:key="item.value"
class="clinic-type-card"
:class="{ active: clinicTypeIndex === idx }"
@click="selectClinicType(idx)"
>
<text class="clinic-type-label">{{ item.label }}</text>
</view>
</view>
</u-form-item>
<u-form-item label="是否包邮" right-icon="arrow-right">
<u-input :value="yesNoLabel(form.is_shipping_free)" type="select" @click="showYesNo = true" placeholder="请选择" />
<u-select v-model="showYesNo" :list="yesNo" @confirm="e => onPickConfirm('is_shipping_free', e)"></u-select>
<u-form-item label="是否包邮">
<view class="switch-row">
<text class="switch-hint">{{ Number(form.is_shipping_free) === 1 ? '包邮' : '不包邮' }}</text>
<switch
:checked="Number(form.is_shipping_free) === 1"
color="#6acdbb"
@change="onShippingFreeChange"
/>
</view>
</u-form-item>
<u-form-item label="订阅价格波动" right-icon="arrow-right">
<u-input :value="subscribeOpts[form.subscribe_price_change === 1 ? 1 : 0].label" type="select" @click="showSub = true" placeholder="请选择" />
<u-select v-model="showSub" :list="subscribeOpts" @confirm="e => onPickConfirm('subscribe_price_change', e)"></u-select>
<u-form-item label="订阅价格波动">
<view class="switch-row">
<text class="switch-hint">{{ Number(form.subscribe_price_change) === 0 ? '订阅' : '不订阅' }}</text>
<switch
:checked="Number(form.subscribe_price_change) === 0"
color="#6acdbb"
@change="onSubscribeChange"
/>
</view>
</u-form-item>
<u-form-item label="营业时间" right-icon="arrow-right">
@@ -211,22 +232,17 @@ export default {
// uView select/picker 显示状态控制
showStoreType: false,
showClinicType: false,
showYesNo: false,
showSub: false,
showStartTime: false,
showEndTime: false,
showBankType: false,
clinicTypeIndex: 1,
clinicTypeIndex: 0,
bankTypeIndex: 0,
// uView 格式的下拉数据
storeTypes: [{ label: '诊所', value: 0 }, { label: '药店', value: 1 }],
storeTypeIndex: 0,
clinicTypes: [{ label: '西医诊所', value: 1 }, { label: '中医诊所', value: 2 }],
yesNo: [{ label: '不包邮', value: 0 }, { label: '包邮', value: 1 }],
subscribeOpts: [{ label: '订阅价格波动', value: 0 }, { label: '不订阅价格波动', value: 1 }],
bankTypes: [{ label: '对公', value: 1 }, { label: '对私', value: 2 }, { label: '存折', value: 5 }],
form: createDefaultStoreInputForm(),
@@ -306,7 +322,17 @@ export default {
this.bankTypeIndex = Math.max(0, this.bankTypes.findIndex((b) => b.value === Number(d.bank_account_type)))
})
},
yesNoLabel(v) { return Number(v) === 1 ? '包邮' : '不包邮' },
selectClinicType(idx) {
this.clinicTypeIndex = idx
this.form.clinic_type = this.clinicTypes[idx].value
},
onShippingFreeChange(e) {
this.form.is_shipping_free = e.detail.value ? 1 : 0
},
onSubscribeChange(e) {
this.form.subscribe_price_change = e.detail.value ? 0 : 1
},
// --- 下拉及选择器确认事件 ---
onStoreTypeConfirm(e) {
@@ -314,19 +340,11 @@ export default {
this.form.type = selected.value
this.storeTypeIndex = this.storeTypes.findIndex((item) => item.value === selected.value)
},
onClinicTypeConfirm(e) {
const selected = e[0];
this.form.clinic_type = selected.value;
this.clinicTypeIndex = this.clinicTypes.findIndex(item => item.value === selected.value);
},
onBankTypeConfirm(e) {
const selected = e[0];
this.form.bank_account_type = selected.value;
this.bankTypeIndex = this.bankTypes.findIndex(item => item.value === selected.value);
},
onPickConfirm(field, e) {
this.form[field] = e[0].value;
},
onTimeConfirm(field, e) {
this.form[field] = `${e.hour}:${e.minute}`;
},
@@ -490,6 +508,31 @@ export default {
.time-box { flex: 1; text-align: center; background: #f5f7fa; padding: 12rpx 0; border-radius: 8rpx; font-size: 28rpx; color: #606266; }
.time-sep { margin: 0 20rpx; color: #999; }
.clinic-type-cards { display: flex; gap: 20rpx; width: 100%; }
.clinic-type-card {
flex: 1;
padding: 24rpx 16rpx;
text-align: center;
background: #f5f7fa;
border: 2rpx solid #e4e7ed;
border-radius: 12rpx;
box-sizing: border-box;
}
.clinic-type-card.active {
background: rgba(106, 205, 187, 0.08);
border-color: #6acdbb;
}
.clinic-type-label { font-size: 28rpx; color: #303133; }
.clinic-type-card.active .clinic-type-label { color: #6acdbb; font-weight: 600; }
.switch-row {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
gap: 16rpx;
}
.switch-hint { font-size: 26rpx; color: #86909c; }
/* 图片资料区域 */
.upload-group { margin-bottom: 30rpx; padding-bottom: 20rpx; border-bottom: 1rpx solid #f5f5f5; }
.upload-group:last-child { border-bottom: none; }

View File

@@ -6,8 +6,8 @@ export function checkDev(key = '') {
// 判断某些模块是否开启
switch (key) {
case 'dev':
return false;
// return true;
// return false;
return true;
case 'open-im':
// return false;
return true;