- 新增分包组件 ListSkeleton(doctor/message/product/article/category/zone) - 首页/消息/资讯首次加载改用骨架屏,刷新不再清空旧数据避免闪空 · 去掉 home getList、消息 onShow 中的数组清空 · 三页 pages.json 增加 easycom 与 componentPlaceholder 主包异步引用 - 聊天室己方头像改为读取本地 userinfo.avatarurl(登录后微信头像), mycollection 上传头像成功后同步写回 storage - 修复协议抽屉点不开:page-container 改为 v-if + :show; 协议名单独可点节点,匹配放宽 name/desc 双向 includes - 医生气泡头像可点击进入 doctor-detail?readonly=1 只读医生详情, 隐藏预约挂号区
507 lines
13 KiB
Vue
507 lines
13 KiB
Vue
<template>
|
||
<view class="follow-up-drug-drawer">
|
||
<!-- 触发器部分:优化阴影与圆角,使用 hover-class 提升微信小程序点击反馈 -->
|
||
<view class="trigger" hover-class="trigger-hover" :hover-stay-time="150" @click="openPopup">
|
||
<view class="trigger-main">
|
||
<text v-if="!displaySummary" class="placeholder">请选择药品</text>
|
||
<text v-else class="summary">{{ displaySummary }}</text>
|
||
</view>
|
||
<u-icon name="arrow-right" color="#94A3B8" size="28"></u-icon>
|
||
</view>
|
||
|
||
<!-- 弹窗部分:增加圆角弧度(40),视觉更加柔和 -->
|
||
<u-popup
|
||
v-model="popupVisible"
|
||
mode="bottom"
|
||
border-radius="40"
|
||
height="78%"
|
||
:safe-area-inset-bottom="true"
|
||
:closeable="true"
|
||
close-icon-color="#94A3B8"
|
||
@close="onPopupClose"
|
||
>
|
||
<view class="drawer-inner">
|
||
<!-- 头部区域 -->
|
||
<view class="drawer-header">
|
||
<view class="drawer-title">选择相关西药</view>
|
||
</view>
|
||
|
||
<!-- 搜索栏:优化高度和背景色,让输入框更圆润 -->
|
||
<view v-if="storeId" class="drawer-search">
|
||
<u-search
|
||
v-model="searchKeyword"
|
||
placeholder="输入药品名称或拼音首字母"
|
||
bg-color="#F8FAFC"
|
||
border-color="transparent"
|
||
search-icon-color="#94A3B8"
|
||
color="#0F172A"
|
||
:show-action="false"
|
||
:clearabled="true"
|
||
shape="round"
|
||
height="76"
|
||
@clear="onSearchClear"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 状态展示:居中、增加图标、使用更柔和的提示文本 -->
|
||
<view v-if="searchLoading" class="drawer-state">
|
||
<u-loading mode="circle" size="64" color="#4175ee"></u-loading>
|
||
<text class="state-text mt-20">正在努力加载中...</text>
|
||
</view>
|
||
<view v-else-if="!remoteDrugList.length" class="drawer-state empty-state">
|
||
<!-- 这里使用内置icon做空状态,也可以替换为你自己的空状态插画 -->
|
||
<u-icon name="info-circle" size="96" color="#CBD5E1"></u-icon>
|
||
<text class="state-text mt-20">{{ emptyText }}</text>
|
||
</view>
|
||
|
||
<!-- 列表区域:精美的卡片式 UI -->
|
||
<scroll-view v-else scroll-y class="drawer-scroll" scroll-with-animation>
|
||
<view class="drug-list">
|
||
<view
|
||
v-for="d in remoteDrugList"
|
||
:key="d.id"
|
||
class="drawer-row card-ui"
|
||
:class="{ 'is-active': getQty(d.id) > 0 }"
|
||
hover-class="card-ui-hover"
|
||
:hover-stay-time="100"
|
||
>
|
||
<!-- 勾选框 -->
|
||
<view class="drug-check" :class="{ on: getQty(d.id) > 0 }" @click.stop="toggleRow(d)">
|
||
<u-icon v-if="getQty(d.id) > 0" name="checkbox-mark" color="#fff" size="24"></u-icon>
|
||
</view>
|
||
|
||
<!-- 药品图片 -->
|
||
<image class="drug-thumb" :src="drugImage(d)" mode="aspectFill" @click.stop="toggleRow(d)" />
|
||
|
||
<!-- 药品信息 -->
|
||
<view class="drug-meta" @click.stop="toggleRow(d)">
|
||
<text class="dn">{{ d.drug_name }}</text>
|
||
<text class="ds" v-if="d.specification">{{ d.specification }}</text>
|
||
</view>
|
||
|
||
<!-- 数量操作 (阻止冒泡防止触发卡片点击) -->
|
||
<view class="drawer-row-actions" @click.stop>
|
||
<u-number-box
|
||
:value="getQty(d.id)"
|
||
:min="0"
|
||
:max="99"
|
||
:size="26"
|
||
:input-width="72"
|
||
:input-height="52"
|
||
:index="d.id"
|
||
bg-color="#F1F5F9"
|
||
@input="onQtyInput(d, $event)"
|
||
/>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 底部操作栏:增强主次按钮对比度,增大点击热区,适配底部安全区 -->
|
||
<view class="drawer-footer safe-area-inset-bottom">
|
||
<view class="btn-text" hover-class="btn-text-hover" :hover-stay-time="100" @click="clearDraft">
|
||
清空已选
|
||
</view>
|
||
<view class="btn-primary" hover-class="btn-primary-hover" :hover-stay-time="100" @click="confirm">
|
||
确定保存
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</u-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
// 逻辑部分完全保持不变
|
||
import { getWesternDrugsForFollowUpApi } from '@/request/api/order';
|
||
|
||
export default {
|
||
name: 'FollowUpDrugDrawer',
|
||
props: {
|
||
storeId: {
|
||
type: [String, Number],
|
||
default: '',
|
||
},
|
||
value: {
|
||
type: Array,
|
||
default: () => [],
|
||
},
|
||
},
|
||
data() {
|
||
return {
|
||
popupVisible: false,
|
||
draftQty: {},
|
||
searchKeyword: '',
|
||
remoteDrugList: [],
|
||
searchLoading: false,
|
||
drugMetaCache: {},
|
||
searchTimer: null,
|
||
};
|
||
},
|
||
computed: {
|
||
displaySummary() {
|
||
const v = this.value || [];
|
||
if (!v.length) return '';
|
||
return v.map((i) => i.drug_name).filter(Boolean).join(',');
|
||
},
|
||
emptyText() {
|
||
return (this.searchKeyword || '').trim() ? '未找到相关药品' : '暂无可选西药';
|
||
},
|
||
},
|
||
watch: {
|
||
searchKeyword(val) {
|
||
if (!this.popupVisible) return;
|
||
if (this.searchTimer) {
|
||
clearTimeout(this.searchTimer);
|
||
}
|
||
this.searchTimer = setTimeout(() => {
|
||
this.fetchRemoteDrugs(val);
|
||
}, 300);
|
||
},
|
||
},
|
||
beforeDestroy() {
|
||
if (this.searchTimer) {
|
||
clearTimeout(this.searchTimer);
|
||
}
|
||
},
|
||
methods: {
|
||
drugImage(d) {
|
||
const u = d && d.image;
|
||
return u && String(u).trim() ? u : 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/avatar_1.png';
|
||
},
|
||
getQty(id) {
|
||
const q = this.draftQty[id];
|
||
return q != null && q > 0 ? Number(q) : 0;
|
||
},
|
||
cacheDrugMeta(d) {
|
||
if (d && d.id) {
|
||
this.$set(this.drugMetaCache, d.id, d);
|
||
}
|
||
},
|
||
initDraftFromValue() {
|
||
const next = {};
|
||
for (const it of this.value || []) {
|
||
const id = it.id;
|
||
const q = Number(it.quantity);
|
||
if (id != null && q > 0) {
|
||
next[id] = q;
|
||
this.cacheDrugMeta(it);
|
||
}
|
||
}
|
||
this.draftQty = next;
|
||
},
|
||
async fetchRemoteDrugs(keyword = '') {
|
||
if (!this.storeId) {
|
||
this.remoteDrugList = [];
|
||
return;
|
||
}
|
||
this.searchLoading = true;
|
||
try {
|
||
const params = { store_id: this.storeId };
|
||
const kw = (keyword || '').trim();
|
||
if (kw) params.keyword = kw;
|
||
const res = await getWesternDrugsForFollowUpApi(params);
|
||
const list = Array.isArray(res.data?.result) ? res.data.result : [];
|
||
this.remoteDrugList = list;
|
||
list.forEach((d) => this.cacheDrugMeta(d));
|
||
} catch (e) {
|
||
console.warn('fetchRemoteDrugs', e);
|
||
this.remoteDrugList = [];
|
||
} finally {
|
||
this.searchLoading = false;
|
||
}
|
||
},
|
||
openPopup() {
|
||
if (this.searchTimer) {
|
||
clearTimeout(this.searchTimer);
|
||
this.searchTimer = null;
|
||
}
|
||
this.searchKeyword = '';
|
||
this.initDraftFromValue();
|
||
this.popupVisible = true;
|
||
this.fetchRemoteDrugs('');
|
||
},
|
||
onPopupClose() {
|
||
this.popupVisible = false;
|
||
},
|
||
onSearchClear() {
|
||
this.searchKeyword = '';
|
||
this.fetchRemoteDrugs('');
|
||
},
|
||
toggleRow(d) {
|
||
const id = d.id;
|
||
this.cacheDrugMeta(d);
|
||
if (this.getQty(id) > 0) {
|
||
this.$delete(this.draftQty, id);
|
||
} else {
|
||
this.$set(this.draftQty, id, 1);
|
||
}
|
||
},
|
||
onQtyInput(d, val) {
|
||
this.cacheDrugMeta(d);
|
||
const n = Number(val);
|
||
if (!n || n < 1) {
|
||
this.$delete(this.draftQty, d.id);
|
||
} else {
|
||
this.$set(this.draftQty, d.id, Math.min(99, n));
|
||
}
|
||
},
|
||
clearDraft() {
|
||
this.draftQty = {};
|
||
},
|
||
confirm() {
|
||
const out = [];
|
||
for (const idStr of Object.keys(this.draftQty)) {
|
||
const id = Number(idStr);
|
||
const q = this.getQty(id);
|
||
if (q > 0) {
|
||
const meta = this.drugMetaCache[id] || {};
|
||
out.push({
|
||
id,
|
||
drug_name: meta.drug_name || '',
|
||
quantity: q,
|
||
});
|
||
}
|
||
}
|
||
this.$emit('input', out);
|
||
this.$emit('confirm', out);
|
||
this.popupVisible = false;
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.follow-up-drug-drawer {
|
||
width: 100%;
|
||
}
|
||
|
||
/* ================= 外部触发器 ================= */
|
||
.trigger {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 28rpx 30rpx;
|
||
margin-top: 12rpx;
|
||
background: #ffffff;
|
||
border-radius: 20rpx;
|
||
border: 1rpx solid #F1F5F9;
|
||
box-shadow: 0 4rpx 12rpx rgba(148, 163, 184, 0.08);
|
||
transition: all 0.2s ease;
|
||
}
|
||
/* 微信小程序推荐使用 hover-class 替代 :active */
|
||
.trigger-hover {
|
||
background: #F8FAFC;
|
||
transform: scale(0.995);
|
||
box-shadow: 0 2rpx 6rpx rgba(148, 163, 184, 0.04);
|
||
}
|
||
|
||
.trigger-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
margin-right: 16rpx;
|
||
}
|
||
.placeholder {
|
||
font-size: 30rpx;
|
||
color: #94A3B8;
|
||
}
|
||
.summary {
|
||
font-size: 30rpx;
|
||
color: #1E293B;
|
||
line-height: 1.5;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* ================= 弹窗内部容器 ================= */
|
||
.drawer-inner {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
box-sizing: border-box;
|
||
padding: 40rpx 28rpx 0;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.drawer-header {
|
||
padding-bottom: 30rpx;
|
||
}
|
||
.drawer-title {
|
||
font-size: 36rpx;
|
||
font-weight: 600;
|
||
color: #0F172A;
|
||
text-align: center;
|
||
letter-spacing: 1rpx;
|
||
}
|
||
|
||
.drawer-search {
|
||
padding-bottom: 24rpx;
|
||
}
|
||
|
||
/* ================= 空状态/加载状态 ================= */
|
||
.drawer-state {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 100rpx 0;
|
||
|
||
.mt-20 {
|
||
margin-top: 24rpx;
|
||
}
|
||
.state-text {
|
||
color: #94A3B8;
|
||
font-size: 28rpx;
|
||
letter-spacing: 2rpx;
|
||
}
|
||
}
|
||
.empty-state {
|
||
opacity: 0.8;
|
||
}
|
||
|
||
/* ================= 滚动列表 ================= */
|
||
.drawer-scroll {
|
||
flex: 1;
|
||
max-height: calc(100vh * 0.78 - 320rpx);
|
||
min-height: 200rpx;
|
||
}
|
||
.drug-list {
|
||
padding: 8rpx 4rpx; /* 为阴影留出空间 */
|
||
}
|
||
|
||
/* ================= 卡片式 UI ================= */
|
||
.card-ui {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 24rpx;
|
||
margin-bottom: 24rpx;
|
||
background: #ffffff;
|
||
border-radius: 24rpx;
|
||
border: 2rpx solid #F1F5F9;
|
||
box-shadow: 0 4rpx 16rpx rgba(148, 163, 184, 0.05);
|
||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||
|
||
/* 选中态全局高亮边框和背景 */
|
||
&.is-active {
|
||
border-color: #4175EE;
|
||
background: #F5F8FF;
|
||
box-shadow: 0 6rpx 20rpx rgba(65, 117, 238, 0.08);
|
||
}
|
||
}
|
||
|
||
/* 卡片点击反馈 */
|
||
.card-ui-hover {
|
||
background: #F8FAFC;
|
||
transform: scale(0.99);
|
||
}
|
||
|
||
/* ================= 自定义复选框 ================= */
|
||
.drug-check {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
border: 3rpx solid #CBD5E1;
|
||
border-radius: 50%;
|
||
margin-right: 24rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||
background: #ffffff;
|
||
|
||
&.on {
|
||
background: #4175EE;
|
||
border-color: #4175EE;
|
||
transform: scale(1.05); /* 选中时有微小的回弹感 */
|
||
}
|
||
}
|
||
|
||
/* ================= 药品缩略图 ================= */
|
||
.drug-thumb {
|
||
width: 112rpx;
|
||
height: 112rpx;
|
||
border-radius: 16rpx;
|
||
margin-right: 24rpx;
|
||
flex-shrink: 0;
|
||
background: #F8FAFC;
|
||
border: 1rpx solid #F1F5F9;
|
||
}
|
||
|
||
/* ================= 药品文字信息 ================= */
|
||
.drug-meta {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
|
||
.dn {
|
||
font-size: 32rpx;
|
||
color: #0F172A;
|
||
font-weight: 500;
|
||
margin-bottom: 10rpx;
|
||
/* 单行省略 */
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.ds {
|
||
font-size: 24rpx;
|
||
color: #64748B;
|
||
}
|
||
}
|
||
|
||
.drawer-row-actions {
|
||
flex-shrink: 0;
|
||
margin-left: 16rpx;
|
||
}
|
||
|
||
/* ================= 底部操作区 ================= */
|
||
.drawer-footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 24rpx 0 32rpx;
|
||
gap: 30rpx;
|
||
background: #ffffff;
|
||
/* 去掉生硬的顶边框,改用轻微的上阴影 */
|
||
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.02);
|
||
z-index: 10;
|
||
}
|
||
|
||
/* 次要按钮 */
|
||
.btn-text {
|
||
flex: 1;
|
||
text-align: center;
|
||
font-size: 32rpx;
|
||
color: #475569;
|
||
padding: 26rpx 0;
|
||
border-radius: 20rpx;
|
||
background: #F1F5F9;
|
||
font-weight: 500;
|
||
transition: all 0.2s ease;
|
||
}
|
||
.btn-text-hover {
|
||
background: #E2E8F0;
|
||
color: #334155;
|
||
}
|
||
|
||
/* 主要按钮 */
|
||
.btn-primary {
|
||
flex: 2;
|
||
text-align: center;
|
||
font-size: 32rpx;
|
||
color: #ffffff;
|
||
background: linear-gradient(135deg, #5B8AFF 0%, #3B6BE0 100%);
|
||
border-radius: 20rpx;
|
||
padding: 26rpx 0;
|
||
font-weight: 500;
|
||
box-shadow: 0 8rpx 20rpx rgba(65, 117, 238, 0.25);
|
||
transition: all 0.2s ease;
|
||
}
|
||
.btn-primary-hover {
|
||
transform: translateY(2rpx);
|
||
box-shadow: 0 4rpx 10rpx rgba(65, 117, 238, 0.15);
|
||
background: linear-gradient(135deg, #4A7AFF 0%, #325BCE 100%);
|
||
}
|
||
</style> |