1. 特色方

This commit is contained in:
李琦
2026-07-02 14:11:24 +08:00
parent 3f699fa2c6
commit 4ff6564ffb
16 changed files with 4241 additions and 2004 deletions

View File

@@ -194,6 +194,21 @@
"navigationBarTitleText": "用药信息",
"enablePullDownRefresh": false
}
},
{
"path": "special-prescription/list",
"style": {
"navigationBarTitleText": "特色药方",
"enablePullDownRefresh": false,
"disableScroll": true
}
},
{
"path": "special-prescription/detail",
"style": {
"navigationBarTitleText": "药方详情",
"enablePullDownRefresh": false
}
}
]
},

View File

@@ -43,10 +43,10 @@
<!-- 选择坐诊医生 -->
<view class="doctor_list">选择坐诊医生</view>
<!-- 列表 -->
<view class="list" v-for="(item,index) in doctorList" :key="item.id" @click="goLookDoctor(item.id)">
<view class="list" v-for="(item,index) in doctorList" :key="item.id">
<!-- 图片部分 -->
<view class="list_info">
<view class="list_left">
<view class="list_left" @click="goLookDoctor(item.id)">
<image :src="item.avatar || 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/avatar_1.png'" mode="医生头像"></image>
<!-- 医生 -->
<view class="list_title">
@@ -61,10 +61,10 @@
</view>
</view>
<view class="list_right">{{ typeMap[type] }}</view>
<view class="list_right" @click.stop="goRegister(item)">{{ typeMap[type] }}</view>
</view>
<!-- 文字部分 -->
<view class="card">
<view class="card" @click="goLookDoctor(item.id)">
<!-- 擅长 -->
<view class="list_going">
擅长{{item.good_at}}
@@ -129,7 +129,7 @@
import {
dDropdown
} from '@/components/d-dropdown/d-dropdown'
import { isGuestMode } from '@/common/utils/auth.js'
import { isGuestMode, isLoggedIn, saveRedirectInfo, buildUrlWithParams } from '@/common/utils/auth.js'
export default {
components: {
dDropdown
@@ -232,6 +232,11 @@
}
},
methods: {
resolveRegisterType(pageType) {
const t = String(pageType ?? '0');
if (['0', '1', '2', '3'].includes(t)) return t;
return '0';
},
closeDropdown() {
this.$refs.uDropdown.close();
this.getList()
@@ -309,6 +314,60 @@
})
},
async goRegister(item) {
if (!isLoggedIn()) {
const storeId = uni.getStorageSync('store_id') || '11001';
saveRedirectInfo('/subPackages/doctor/doctor-userinfo', {
id: item.id,
r_type: this.resolveRegisterType(this.type),
store_id: storeId,
});
uni.navigateTo({
url: buildUrlWithParams('/pages/login/login', {
redirect: 'doctor-userinfo',
doctor_id: item.id,
store_id: storeId,
})
});
return;
}
const rType = this.resolveRegisterType(this.type);
if (rType === '2') {
try {
const storeId = uni.getStorageSync('store_id') || '11001';
const { getClinicOnlineConsultationInfoApi } = await import('@/request/api/product');
const configRes = await getClinicOnlineConsultationInfoApi({ store_id: storeId });
if (configRes.data?.code === 0 || configRes.data?.code === '0') {
const result = configRes.data?.result;
if (result?.can_use && result?.delegate_doctor_id) {
uni.navigateTo({
url: `/subPackages/doctor/doctor-userinfo?id=${result.delegate_doctor_id}&r_type=2&delegate_store_id=${result.delegate_store_id || storeId}`
});
return;
}
uni.showToast({
title: result?.message || '该门店暂不支持在线问诊功能',
icon: 'none',
});
return;
}
uni.showToast({
title: configRes.data?.message || '获取在线问诊信息失败',
icon: 'none',
});
} catch (error) {
console.error('获取在线问诊信息失败:', error);
uni.showToast({ title: '获取在线问诊信息失败', icon: 'none' });
}
return;
}
uni.navigateTo({
url: `/subPackages/doctor/doctor-userinfo?id=${item.id}&r_type=${rType}`,
});
},
goProduct() {
uni.navigateTo({
url: '/subPackages/product/product?id=' + id

View File

@@ -3,7 +3,7 @@
<!-- 全局消息通知组件 -->
<MessageNotification />
<image-compress-popup ref="imageCompressPopup" />
<!-- 1. 自定义吸顶导航栏 -->
<view class="custom-nav"
:class="[
@@ -66,7 +66,7 @@
<view class="nav-grid">
<!-- 诊所模式2列布局 -->
<view v-if="storeType === '0' || storeType === 0" class="nav-clinic-grid">
<view class="nav-card" @click="goYard(1)">
<view class="nav-card" @click="goYard(0)">
<view class="nav-content">
<view class="nav-title" style="color:#4175EE">到店挂号</view>
<view class="nav-desc">预约专家 不排队</view>
@@ -94,11 +94,107 @@
</view>
</view>
<!-- 4. 诊所模式内容医生列表 -->
<!-- 待支付挂号提醒 -->
<view
v-if="unpaidRegisterCount > 0"
class="unpaid-banner unpaid-banner--register"
@click="onUnpaidRegisterBannerClick"
>
<text class="unpaid-banner-text">您有 {{ unpaidRegisterCount }} 个待支付挂号订单点击前往查看</text>
<text class="iconfont icon-jiantou1 unpaid-banner-arrow"></text>
</view>
<!-- 待支付商品订单提醒 -->
<view
v-if="unpaidProductOrderCount > 0"
class="unpaid-banner unpaid-banner--product"
@click="onUnpaidProductOrderBannerClick"
>
<text class="unpaid-banner-text">您有 {{ unpaidProductOrderCount }} 个待支付商品订单点击前往查看</text>
<text class="iconfont icon-jiantou1 unpaid-banner-arrow"></text>
</view>
<!-- 4. 诊所模式内容特色药方 -->
<view v-if="(storeType === '0' || storeType === 0) && specialPrescription.enabled" class="tea-section-wrap">
<!-- 特色药方异形卡片模块 -->
<view class="tea-section">
<view class="irregular-card">
<!-- 左侧凸起 Tab右侧带下坡过渡 -->
<view class="card-tab">
<view class="card-tab-slope"></view>
</view>
<!-- 悬浮艺术字标题位于凸起与卡片的衔接处 -->
<image
:src="specialPrescription.title_image"
class="main-title"
mode="widthFix"
></image>
<!-- 主体区域 -->
<view class="card-body">
<!-- 悬浮 IP 小熊 -->
<image
:src="specialPrescription.mascot_image"
class="main-mascot"
mode="widthFix"
></image>
<view class="tea-subtitle">
<view class="subtitle-icon"></view>
<text>{{ specialPrescription.subtitle || '精选时令养生茶饮' }}</text>
</view>
<!-- 横向滑动列表 -->
<scroll-view scroll-x class="tea-scroll" :show-scrollbar="false" :enable-passive="false">
<view class="tea-list">
<navigator
v-if="detailUrl(tea)"
class="tea-item"
v-for="(tea, tIndex) in specialPrescription.items"
:key="tea.id || tIndex"
:url="detailUrl(tea)"
hover-class="tea-item--active"
>
<view class="tea-image-wrap">
<image
v-if="tea.cover_image"
:src="tea.cover_image"
class="tea-image"
mode="aspectFill"
></image>
<text v-else class="tea-image-placeholder">特色方</text>
</view>
<view class="tea-info-wrap">
<view class="tea-name u-line-1">{{ tea.name }}</view>
<view class="tea-tags">
<text class="tea-tag" v-for="(tag, gIndex) in (tea.tags || []).slice(0, 2)" :key="gIndex">{{ tag }}</text>
</view>
<view class="tea-price" v-if="tea.price_available !== false">
<text class="tea-price-symbol">¥</text>{{ tea.price_per_dose }}
</view>
<view class="tea-price tea-price--unavailable" v-else>暂无报价</view>
</view>
</navigator>
</view>
</scroll-view>
<!-- 底部操作区 -->
<view class="tea-bottom-area">
<view class="days-left-tag">热卖养生茶</view>
<view class="tea-action-button" @click="goTeaList">查看全部药方</view>
</view>
</view>
</view>
</view>
</view>
<!-- 诊所模式医生列表 -->
<view v-if="storeType === '0' || storeType === 0">
<!-- 医生列表 -->
<view class="section-header">
<view class="section-title">医生推荐</view>
<view class="section-more" @click="goYard(1)">更多 <text class="iconfont icon-jiantou1" style="font-size: 20rpx;"></text></view>
<view class="section-more" @click="goYard(0)">更多 <text class="iconfont icon-jiantou1" style="font-size: 20rpx;"></text></view>
</view>
<view class="doctor-card" @click="goLookDoctor(item.id)" v-for="(item,index) in doctorList" :key="item.id">
@@ -118,8 +214,8 @@
{{item.good_at}}
</view>
</view>
<!-- <view class="consult-btn">咨询</view>-->
<!-- <view class="consult-btn">查看</view>-->
<!-- <view class="consult-btn">咨询</view>-->
<!-- <view class="consult-btn">查看</view>-->
</view>
<view class="doc-stats-bar">
<view class="stat-item">
@@ -209,10 +305,13 @@ import {
getToken,
storeChange,
storeInfo,
storeList
storeList,
fetchUnpaidProductOrderListApi,
} from '../../request/api/api'
import { getPlatformQualificationsApi } from '../../request/api/platform'
import { getStoreDrugListBySalespersonApi, getHomeTopProductsApi, getHomeZonesApi, checkOnlineConsultationConfigApi } from "@/request/api/product";
import { getSpecialPrescriptionHomeApi } from '@/request/api/specialPrescription';
import { fetchUnpaidRegisterListApi } from '@/request/api/register';
import request from "@/request/api/request";
import MessageNotification from "@/components/MessageNotification/MessageNotification.vue";
import ImageCompressPopup from '@/components/image-compress-popup/image-compress-popup.vue';
@@ -251,7 +350,22 @@ export default {
isSticky: false,
// 控制金刚区布局模式: 'scroll' (滑动) 或 'wrap' (换行)
zoneLayoutMode: 'scroll'
zoneLayoutMode: 'scroll',
unpaidRegisterCount: 0,
unpaidRegisterList: [],
unpaidProductOrderCount: 0,
unpaidProductOrderList: [],
// 特色药方API
specialPrescription: {
enabled: false,
doctor_id: 0,
title_image: '',
mascot_image: '',
subtitle: '',
items: [],
},
}
},
computed: {
@@ -265,7 +379,7 @@ export default {
}
},
onLoad() {
this.isShow_envVersion = uni.getStorageSync('isShow_envVersion')
if (this.isShow_envVersion === false) {
this.getList();
@@ -318,6 +432,55 @@ export default {
}
},
methods: {
async fetchUnpaidCounts() {
if (!isLoggedIn()) {
this.unpaidRegisterCount = 0;
this.unpaidRegisterList = [];
this.unpaidProductOrderCount = 0;
this.unpaidProductOrderList = [];
return;
}
try {
const [registerList, productList] = await Promise.all([
fetchUnpaidRegisterListApi(),
fetchUnpaidProductOrderListApi(),
]);
this.unpaidRegisterList = registerList;
this.unpaidRegisterCount = registerList.length;
this.unpaidProductOrderList = productList;
this.unpaidProductOrderCount = productList.length;
} catch (e) {
console.error('fetchUnpaidCounts', e);
this.unpaidRegisterCount = 0;
this.unpaidRegisterList = [];
this.unpaidProductOrderCount = 0;
this.unpaidProductOrderList = [];
}
},
onUnpaidRegisterBannerClick() {
if (this.unpaidRegisterCount === 1 && this.unpaidRegisterList[0]) {
const id = this.unpaidRegisterList[0].id;
uni.navigateTo({
url: `/subPackages/doctor/doctor-userinfo?step=2&register_id=${id}`,
});
return;
}
uni.navigateTo({
url: '/subPackages/my/myappiont',
});
},
onUnpaidProductOrderBannerClick() {
if (this.unpaidProductOrderCount === 1 && this.unpaidProductOrderList[0]) {
const id = this.unpaidProductOrderList[0].id;
uni.navigateTo({
url: `/subPackages/my/my-drug/drug-info?id=${id}`,
});
return;
}
uni.navigateTo({
url: '/subPackages/my/my-drug/drug-list?index=1',
});
},
// 处理购物车按钮点击
handleAddToCart(item) {
if(item && item.drug) {
@@ -331,10 +494,10 @@ export default {
const storeId = uni.getStorageSync('store_id') || '11001';
const { getClinicOnlineConsultationInfoApi } = await import('@/request/api/product');
const configRes = await getClinicOnlineConsultationInfoApi({ store_id: storeId });
if (configRes.data?.code === 0 || configRes.data?.code === '0') {
const result = configRes.data?.result;
if (result?.can_use && result?.delegate_doctor_id) {
// 直接跳转到就诊人选择页面传递医生ID和挂号类型
uni.navigateTo({
@@ -367,21 +530,21 @@ export default {
return;
}
}
// 当 type === 3 (在线复诊) 时,需要先检查门店是否支持在线问诊功能
if (type === 3) {
try {
const storeId = uni.getStorageSync('store_id') || '11001';
const configRes = await checkOnlineConsultationConfigApi({
const configRes = await checkOnlineConsultationConfigApi({
store_id: storeId,
guest: 1 // 使用 Guest 接口
});
// 直接使用新接口格式code + result
if (configRes.data?.code === 0 || configRes.data?.code === '0') {
// 获取结果数据,直接使用 result返回格式{ code: 0, result: { can_use: false, message: "..." } }
const result = configRes.data?.result;
// 检查是否可以使用在线问诊功能
if (!result?.can_use) {
// 显示后端返回的提示信息(使用 result 中的 message
@@ -411,7 +574,7 @@ export default {
return;
}
}
// 检查通过或非在线复诊类型,正常跳转
uni.navigateTo({
url: '/pages/Inquiries/Inquiries?type=' + type
@@ -476,18 +639,18 @@ export default {
const isGuest = isGuestMode();
const isNewFormat = isGuest && (res.data.code === 0 || res.data.code === '0');
const isOldFormat = !isGuest && (res.data.errcode === 0 || res.data.errcode === '0');
if (isNewFormat || isOldFormat) {
// 新接口格式:从 result 取值
// 老接口格式:从 data 取值
const responseData = isGuest && res.data.result ? res.data.result : res.data.data;
if (responseData && responseData.store) {
this.storeType = responseData.store['type']
uni.setStorageSync('store_type', responseData.store['type']) // 存储门店类型
this.infoList = responseData
this.headerTitle = responseData.store['name']
this.getBannered();
this.getBannered();
}
}
})
@@ -529,7 +692,7 @@ export default {
const isGuest = isGuestMode();
const isNewFormat = isGuest && (res.data.code === 0 || res.data.code === '0');
const isOldFormat = !isGuest && (res.data.errcode === 0 || res.data.errcode === '0');
if (isNewFormat || isOldFormat) {
// 新接口格式:从 result 取值
// 老接口格式:从 data 取值
@@ -555,12 +718,12 @@ export default {
const isGuest = isGuestMode();
const isNewFormat = isGuest && (res.data.code === 0 || res.data.code === '0');
const isOldFormat = !isGuest && (res.data.errcode === 0 || res.data.errcode === '0');
if (isNewFormat || isOldFormat) {
// 新接口格式:从 result 取值
// 老接口格式:从 data 取值
const responseData = isGuest && res.data.result ? res.data.result : res.data.data;
if (responseData) {
this.info = responseData
this.urlInfo = responseData.nav || []
@@ -628,8 +791,8 @@ export default {
previewQualification(item) {
// 获取所有有图片的资质,支持滑动预览
const allImages = this.qualifications
.filter(q => q.image)
.map(q => q.image);
.filter(q => q.image)
.map(q => q.image);
if (allImages.length > 0 && item.image) {
uni.previewImage({
urls: allImages,
@@ -650,6 +813,43 @@ export default {
goPlatformInfo() {
uni.navigateTo({ url: '/pages/index/platform-info' });
},
// 跳转到全部药方 / 养生茶饮列表
goTeaList() {
uni.navigateTo({
url: '/subPackages/special-prescription/list',
});
},
goTeaDetail(tea) {
const id = Number(tea?.id);
if (!id) return;
uni.navigateTo({
url: `/subPackages/special-prescription/detail?id=${id}`,
});
},
detailUrl(tea) {
const id = Number(tea?.id);
if (!id) return '';
return `/subPackages/special-prescription/detail?id=${id}`;
},
fetchSpecialPrescriptionHome() {
getSpecialPrescriptionHomeApi({
store_id: uni.getStorageSync('store_id') || '11001',
}).then((res) => {
if (res.data?.code === 0 || res.data?.code === '0') {
const result = res.data.result || {};
this.specialPrescription = {
enabled: !!result.enabled,
doctor_id: result.doctor_id || 0,
title_image: result.title_image || '',
mascot_image: result.mascot_image || '',
subtitle: result.subtitle || '',
items: result.items || [],
};
}
}).catch(() => {
this.specialPrescription.enabled = false;
});
},
async getDefaultDoctorId() {
try {
const storeId = uni.getStorageSync('store_id');
@@ -676,6 +876,8 @@ export default {
this.getHomeZones();
this.getPlatformQualifications(); // 已补回
this.getDefaultDoctorId();
this.fetchUnpaidCounts();
this.fetchSpecialPrescriptionHome();
},
}
</script>
@@ -789,12 +991,249 @@ $card-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.04);
}
}
.unpaid-banner {
display: flex;
align-items: center;
justify-content: space-between;
margin: 16rpx 32rpx 0;
padding: 24rpx 28rpx;
border-radius: 16rpx;
&--register {
background: linear-gradient(90deg, rgba(65, 117, 238, 0.08) 0%, rgba(65, 117, 238, 0.04) 100%);
border: 1rpx solid rgba(65, 117, 238, 0.2);
.unpaid-banner-text,
.unpaid-banner-arrow {
color: #4175EE;
}
}
&--product {
background: linear-gradient(90deg, rgba(0, 181, 120, 0.08) 0%, rgba(0, 181, 120, 0.04) 100%);
border: 1rpx solid rgba(0, 181, 120, 0.25);
.unpaid-banner-text,
.unpaid-banner-arrow {
color: #00B578;
}
}
.unpaid-banner-text {
flex: 1;
font-size: 26rpx;
line-height: 1.5;
}
.unpaid-banner-arrow {
font-size: 24rpx;
margin-left: 12rpx;
}
}
.section-header {
padding: 0 32rpx; margin: 20rpx 0 32rpx; display: flex; justify-content: space-between; align-items: center;
.section-title { font-size: 36rpx; font-weight: 800; color: #333; position: relative; padding-left: 24rpx; &::before { content: ''; position: absolute; left: 0; top: 8rpx; bottom: 8rpx; width: 8rpx; background: $primary-color; border-radius: 4rpx; } }
.section-more { font-size: 26rpx; color: #999; display: flex; align-items: center; }
}
/* --- 特色药方 / 养生茶饮异形卡片 --- */
.tea-section {
padding: 57rpx 32rpx 12rpx;
.irregular-card {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
}
/* 左侧凸起 Tab */
.card-tab {
width: 220rpx;
height: 40rpx;
background: #FFFFFF;
border-radius: 24rpx 24rpx 0 0;
position: relative;
z-index: 2;
margin-bottom: -1rpx;
}
/* 凸起右侧的下坡过渡(凹角) */
.card-tab-slope {
position: absolute;
right: -40rpx;
bottom: 0;
width: 40rpx;
height: 40rpx;
/* 使用径向渐变模拟内凹的下坡曲线 */
background-image: radial-gradient(circle at 100% 0%, transparent 40rpx, #FFFFFF 40rpx);
}
/* 悬浮艺术字标题:坐落在凸起与卡片衔接处 */
.main-title {
position: absolute;
left: 8rpx;
top: -120rpx;
width: 210rpx;
height: 128rpx;
z-index: 6;
pointer-events: none;
}
/* 主体区域 */
.card-body {
background: #FFFFFF;
width: 100%;
padding: 24rpx 24rpx 24rpx;
border-radius: 0 24rpx 24rpx 24rpx;
position: relative;
z-index: 1;
box-sizing: border-box;
box-shadow: 0 12rpx 32rpx rgba(65, 117, 238, 0.16);
}
/* 悬浮 IP 小熊 */
.main-mascot {
position: absolute;
right: 12rpx;
top: -140rpx;
width: 217rpx;
height: 168rpx;
z-index: 10;
pointer-events: none;
}
.tea-subtitle {
font-size: 28rpx;
color: #333;
font-weight: bold;
margin-bottom: 20rpx;
padding-right: 150rpx;
display: flex;
align-items: center;
.subtitle-icon {
width: 6rpx;
height: 26rpx;
background: #4175EE;
border-radius: 3rpx;
margin-right: 12rpx;
}
}
/* 横向滑动列表 */
.tea-scroll {
width: 100%;
white-space: nowrap;
margin: 0 -24rpx;
box-sizing: border-box;
position: relative;
z-index: 5;
}
.tea-list {
display: inline-flex;
padding: 4rpx 24rpx 12rpx;
position: relative;
z-index: 5;
}
.tea-item {
display: inline-flex;
flex-direction: column;
width: 176rpx;
margin-right: 16rpx;
background: #FFFFFF;
border-radius: 16rpx;
overflow: hidden;
border: 1rpx solid #F0F0F0;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
position: relative;
z-index: 5;
&:last-child { margin-right: 0; }
&:active,
&.tea-item--active { transform: scale(0.97); }
.tea-image-wrap {
width: 100%;
height: 132rpx;
background: linear-gradient(135deg, #F5F7FA 0%, rgba(65, 117, 238, 0.08) 100%);
display: flex;
align-items: center;
justify-content: center;
.tea-image { width: 100%; height: 100%; }
.tea-image-placeholder { font-size: 22rpx; color: #4175EE; }
}
.tea-info-wrap {
padding: 12rpx;
display: flex;
flex-direction: column;
}
.tea-name {
font-size: 24rpx;
color: #333;
font-weight: 600;
margin-bottom: 8rpx;
}
.tea-tags {
display: flex;
margin-bottom: 8rpx;
.tea-tag {
font-size: 18rpx;
color: #4175EE;
background: rgba(65, 117, 238, 0.1);
padding: 1rpx 8rpx;
border-radius: 4rpx;
margin-right: 6rpx;
}
}
.tea-price {
color: #FF5722;
font-size: 28rpx;
font-weight: 800;
.tea-price-symbol { font-size: 20rpx; margin-right: 2rpx; }
&--unavailable {
color: #999;
font-size: 22rpx;
font-weight: 500;
}
}
}
/* 底部操作区 */
.tea-bottom-area {
position: relative;
margin-top: 16rpx;
.days-left-tag {
position: absolute;
top: -20rpx;
right: 20rpx;
background: #FFC107;
color: #8C6A00;
font-size: 20rpx;
font-weight: bold;
padding: 6rpx 18rpx;
border-radius: 16rpx 16rpx 16rpx 0;
z-index: 2;
border: 2rpx solid #fff;
box-shadow: 0 4rpx 12rpx rgba(255, 193, 7, 0.3);
}
.tea-action-button {
width: 100%;
text-align: center;
padding: 20rpx 0;
border-radius: 40rpx;
background: linear-gradient(90deg, #4175EE 0%, #298DFF 100%);
color: #fff;
font-size: 28rpx;
font-weight: bold;
box-sizing: border-box;
box-shadow: 0 8rpx 24rpx rgba(65, 117, 238, 0.3);
&:active { transform: scale(0.98); }
}
}
}
.doctor-card {
background: #fff; margin: 0 32rpx 32rpx; border-radius: $card-radius; box-shadow: $card-shadow; overflow: hidden;
&:active { transform: scale(0.99); }
@@ -876,23 +1315,23 @@ $card-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.04);
.qualifications-section {
padding: 40rpx 32rpx 40rpx;
.qualifications-row {
display: flex;
display: flex;
flex-wrap: wrap;
gap: 20rpx;
.qualification-card {
background: #fff;
padding: 24rpx;
border-radius: 16rpx;
background: #fff;
padding: 24rpx;
border-radius: 16rpx;
width: calc((100% - 20rpx) / 2); /* 一行两个 */
text-align: center;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
display: flex;
flex-direction: column;
text-align: center;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
display: flex;
flex-direction: column;
align-items: center;
.qual-icon {
width: 200rpx;
height: 140rpx;
margin-bottom: 16rpx;
.qual-icon {
width: 200rpx;
height: 140rpx;
margin-bottom: 16rpx;
border-radius: 8rpx;
background: #f8f8f8;
}
@@ -904,4 +1343,4 @@ $card-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.04);
.u-line-1 { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.bg- { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.5); z-index: 999; display: flex; align-items: center; justify-content: center; }
}
</style>
</style>

View File

@@ -375,6 +375,29 @@ export async function getOrdlist(params) {
return normalizeXkApiResponse(data)
}
/**
* 获取待支付商品订单列表(首页提醒用)
*/
export async function fetchUnpaidProductOrderListApi(params = {}) {
const storeId = params.store_id || uni.getStorageSync('store_id') || '11001';
const res = await getOrdlist({
method: 'post',
data: {
store_id: storeId,
type: 'unpaid',
page: 1,
...params,
},
});
if (res.data?.errcode != 0) {
return [];
}
const list = res.data?.data?.list || [];
return list.filter(
(item) => item.status === 0 && item.is_pay === 0 && item.cancel_status === 0
);
}
// v1/product-order/list
export async function getToken(params) {
let data = await http('/oldApi/v1/user/login-platform', params)

View File

@@ -8,3 +8,22 @@ import {post} from './http'
export async function getRegisterListApi(params) {
return await post('/register/list', params, 3)
}
/**
* 获取待支付挂号列表(首页提醒用)
* @param params
* @returns {Promise<Array>}
*/
export async function fetchUnpaidRegisterListApi(params = {}) {
const storeId = params.store_id || uni.getStorageSync('store_id') || '11001';
const res = await post('/register/list?pageSize=100', {
store_id: storeId,
page: 1,
...params,
}, 3);
if (res.data?.code !== 0 && res.data?.code !== '0') {
return [];
}
const list = res.data?.result?.list || [];
return list.filter((item) => item.status === 0 && item.is_cancel !== 1);
}

View File

@@ -147,6 +147,9 @@ function request(url, params, method = 0) {
} else if (url.indexOf('/xkApi/store/list') !== -1) {
url = url.replace('/xkApi/store/list', '/guest/guest-home/store-list');
baseURL = envUrls.xkApi;
} else if (url.indexOf('/xkApi/special-prescription/') !== -1) {
url = url.replace('/xkApi/special-prescription/', '/guest/guest-special-prescription/');
baseURL = envUrls.xkApi;
}
}

View File

@@ -0,0 +1,92 @@
import { get, post } from './http'
export const SPECIAL_PRESCRIPTION_STORAGE_KEY = 'special_prescription_register_context'
/**
* 首页特色方配置
* @param params {{ store_id?: number|string }}
*/
export async function getSpecialPrescriptionHomeApi(params) {
return await get('/special-prescription/home', params, 3)
}
/**
* 特色方列表页配置
* @param params {{ store_id?: number|string }}
*/
export async function getSpecialPrescriptionListPageApi(params) {
return await get('/special-prescription/list-page', params, 3)
}
/**
* 特色方列表
* @param params {{ store_id?: number|string, category_id?: number, name?: string, page?: number, pageSize?: number }}
*/
export async function getSpecialPrescriptionListApi(params) {
return await get('/special-prescription/list', params, 3)
}
/**
* 特色方详情
* @param params {{ id: number|string, store_id?: number|string }}
*/
export async function getSpecialPrescriptionDetailApi(params) {
return await get('/special-prescription/detail', params, 3)
}
/**
* 特色方分类
*/
export async function getSpecialPrescriptionCategoriesApi(params = {}) {
return await get('/special-prescription/categories', params, 3)
}
/**
* 挂号成功后创建选方记录
* @param params {{ register_id, special_prescription_id, dose_count, store_id, doctor_id }}
*/
export async function createSpecialPrescriptionPatientRecordApi(params) {
return await post('/special-prescription/create-patient-record', params, 3)
}
export function saveSpecialPrescriptionRegisterContext(ctx) {
if (!ctx || !ctx.special_prescription_id) return
uni.setStorageSync(SPECIAL_PRESCRIPTION_STORAGE_KEY, {
special_prescription_id: ctx.special_prescription_id,
dose_count: ctx.dose_count || 1,
doctor_id: ctx.doctor_id || '',
})
}
export function getSpecialPrescriptionRegisterContext() {
return uni.getStorageSync(SPECIAL_PRESCRIPTION_STORAGE_KEY) || null
}
export function clearSpecialPrescriptionRegisterContext() {
uni.removeStorageSync(SPECIAL_PRESCRIPTION_STORAGE_KEY)
}
/**
* 挂号成功后绑定特色方选方记录(幂等,失败不阻断流程)
* @param registerId
*/
export async function tryCreateSpecialPrescriptionPatientRecord(registerId) {
if (!registerId) return
const ctx = getSpecialPrescriptionRegisterContext()
if (!ctx || !ctx.special_prescription_id) return
try {
const res = await createSpecialPrescriptionPatientRecordApi({
register_id: registerId,
special_prescription_id: ctx.special_prescription_id,
dose_count: ctx.dose_count || 1,
store_id: uni.getStorageSync('store_id') || '11001',
doctor_id: ctx.doctor_id,
})
if (res.data?.code === 0 || res.data?.code === '0') {
clearSpecialPrescriptionRegisterContext()
}
} catch (e) {
console.error('tryCreateSpecialPrescriptionPatientRecord', e)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,949 @@
<template>
<view class="container safe-area-inset-bottom">
<!-- #ifdef MP-WEIXIN -->
<page-container
:show="pageBackGuardShow"
:overlay="false"
@leave="onPageBackGuardLeave"
/>
<!-- #endif -->
<!-- 预约信息 -->
<view class="title">
预约信息
</view>
<view class="card">
<view class="item">
<view class="aapoint">
预约诊所
</view>
<view class="detail">
{{registList.store}}
</view>
</view>
<view class="item">
<view class="aapoint">
预约科室
</view>
<view class="detail">
{{registList.depart}}
</view>
</view>
<view class="item">
<view class="aapoint">
预约医生
</view>
<view class="detail">
{{registList.doctor}}
</view>
</view>
<view class="item">
<view class="aapoint">
预约序号
</view>
<view class="detail">
{{registList.order_number}}
</view>
</view>
<view class="item">
<view class="aapoint">
预计就诊时间
</view>
<view class="detail">
{{registList.created_at}}
</view>
</view>
<view class="item">
<view class="aapoint">
挂号金额
</view>
<view class="detail active">
{{registList.price}}
</view>
</view>
<view class="item">
<view class="aapoint">
挂号类型
</view>
<view class="detail">
{{registerType}}
</view>
</view>
</view>
<!-- 就诊人 -->
<view class="title">
就诊人
</view>
<view class="info">
<view class="name_edit">
<view class="name">
{{registList.patient}}
</view>
<view class="word_num">
{{registList.order_number}}
</view>
</view>
<view class="name_edit">
<view class="name">
身份证号
</view>
<view class="word_num" v-if="registList.idcard">
{{registList.idcard_tm || registList.idcard}}
</view>
</view>
<view class="name_edit">
<view class="name">
手机号
</view>
<view class="word_num" v-if="registList.mobile">
{{registList.mobile_tm || registList.mobile}}
</view>
</view>
</view>
<u-popup v-model="show" :safe-area-inset-bottom="true" mode="bottom" height="68%" border-radius="20"
:closeable="true" z-index="20000">
<view class="agreen">
<u-parse :html="content"></u-parse>
</view>
</u-popup>
<u-popup v-model="showConfirm" mode="center" border-radius="24" width="600" z-index="10075">
<view class="confirm-modal">
<view class="confirm-title">预约挂号须知</view>
<view class="confirm-desc">
请您在使用前仔细阅读并同意
<text class="link" @click="show=true">预约挂号须知</text>
</view>
<view class="confirm-footer">
<button class="cancel-btn" @click="showConfirm = false">不同意</button>
<button class="agree-btn" @click="agreeAndRegister">同意并挂号</button>
</view>
</view>
</u-popup>
<view class="bottom-spacer" aria-hidden="true"></view>
<view class="footer">
<block v-if="registList.is_pay==0">
<view class="allknow">
<u-icon size="40" :color="form['check']?'#2979FF':'#999999'" name="checkmark-circle-fill"
@click="form['check']=!form['check']"></u-icon>
<view class="isknow">我已阅读并同意 <text class="notice-link" @click="show=true">预约挂号须知</text> </view>
</view>
<view class="footer-btns">
<button class="btn-prev" @click="onPrevStep">上一步</button>
<button class="btn" @click="handleRegisterClick">确认挂号</button>
</view>
</block>
<view class="gh_" v-if="registList.is_pay==1" @click="toHome">
<button class="btn">您已挂号成功请等待医生接诊</button>
</view>
</view>
<u-toast ref="uToast" />
</view>
</template>
<script>
import {
registeInfo,
registePay,
service
} from '@/request/api/api';
import { createRoomByDoctorIdApi, sendToUserApi } from '@/request/api/im';
import { checkDev } from '@/utils/utils';
import { genOrderApi } from '@/request/api/order';
import request from '@/request/api/request';
import {
CLINIC_POST_REGISTER_STORAGE_KEY,
resolveFollowUpDrugsFromRInfo,
} from '@/utils/clinicFollowUpAssistantIm';
import { tryCreateSpecialPrescriptionPatientRecord } from '@/request/api/specialPrescription';
export default {
name: 'RegisterConfirmStep',
props: {
registerId: {
type: [String, Number],
default: '',
},
urlDoctorId: {
type: String,
default: '',
},
rInfoProp: {
type: Object,
default: () => ({}),
},
},
data() {
return {
show: false,
showConfirm: false,
form: { check: false },
register_id: '',
currentUserId: '',
currentDoctorId: '',
registList: {},
content: '预约挂号须知',
flag: true,
registerType: '线下就诊',
registerMap: {
0: '挂号',
1: '咨询',
2: '购药',
3: '复诊',
},
rInfo: {},
_urlDoctorId: '',
_getInfoPromise: null,
pageBackGuardShow: false,
_suppressPageBackGuardLeave: false,
};
},
computed: {
shouldEnablePageBackGuard() {
return !(this.registList && this.registList.is_pay == 1);
},
},
watch: {
'registList.is_pay'(val) {
if (val == 1) {
this.setPageBackGuardShow(false, { suppressLeave: true });
this.bindSpecialPrescriptionRecord();
} else {
this.syncPageBackGuard();
}
},
registerId: {
immediate: true,
handler(val) {
if (val) this.bootstrapFromProps();
},
},
},
mounted() {
this.toAbout();
const uid = uni.getStorageSync('user_id');
this.currentUserId = 'user-' + (uid !== '' && uid !== undefined && uid !== null ? uid : '2512');
},
methods: {
bootstrapFromProps() {
this.register_id = this.registerId;
this.rInfo = { ...(this.rInfoProp || {}) };
this._urlDoctorId = this.urlDoctorId ? this.stripDoctorPrefix(this.urlDoctorId) : '';
let initialDoc = '';
if (this._urlDoctorId) initialDoc = this._urlDoctorId;
else if (this.rInfo && (this.rInfo.doctor_id != null && this.rInfo.doctor_id !== '')) {
initialDoc = this.stripDoctorPrefix(this.rInfo.doctor_id);
} else if (this.rInfo && (this.rInfo.doctorId != null && this.rInfo.doctorId !== '')) {
initialDoc = this.stripDoctorPrefix(this.rInfo.doctorId);
}
this.currentDoctorId = initialDoc ? ('doctor-' + initialDoc) : '';
uni.setStorageSync('register_id', this.register_id);
if (this.register_id) {
this._getInfoPromise = this.getInfo();
this._getInfoPromise.then(() => this.ensureDoctorAfterLoad());
} else {
this.ensureDoctorAfterLoad();
this.syncPageBackGuard();
}
},
handleBackPress() {
if (this.closeTopOverlayIfAny()) {
return true;
}
if (this.registList && this.registList.is_pay == 1) {
return false;
}
this.promptExitBeforeLeave();
return true;
},
onPrevStep() {
this.$emit('prev-step');
},
handleRegisterClick() {
if (!this.form.check) {
this.showConfirm = true;
return;
}
this.next();
},
agreeAndRegister() {
this.form.check = true;
this.showConfirm = false;
this.next();
},
closeTopOverlayIfAny() {
if (this.show) {
this.show = false;
return true;
}
if (this.showConfirm) {
this.showConfirm = false;
return true;
}
return false;
},
setPageBackGuardShow(show, { suppressLeave = false } = {}) {
// #ifdef MP-WEIXIN
if (suppressLeave) this._suppressPageBackGuardLeave = true;
this.pageBackGuardShow = !!show;
// #endif
},
syncPageBackGuard() {
// #ifdef MP-WEIXIN
this.$nextTick(() => {
this.setPageBackGuardShow(this.shouldEnablePageBackGuard, {
suppressLeave: !this.shouldEnablePageBackGuard,
});
});
// #endif
},
safeNavigateBack() {
// #ifdef MP-WEIXIN
this.setPageBackGuardShow(false, { suppressLeave: true });
setTimeout(() => {
uni.navigateBack();
}, 16);
// #endif
// #ifndef MP-WEIXIN
uni.navigateBack();
// #endif
},
onPageBackGuardLeave() {
// #ifdef MP-WEIXIN
if (this._suppressPageBackGuardLeave) {
this._suppressPageBackGuardLeave = false;
return;
}
if (this.closeTopOverlayIfAny()) {
this.$nextTick(() => {
this.setPageBackGuardShow(this.shouldEnablePageBackGuard, { suppressLeave: true });
});
return;
}
if (this.registList && this.registList.is_pay == 1) {
this.setPageBackGuardShow(false, { suppressLeave: true });
setTimeout(() => {
uni.navigateBack();
}, 16);
return;
}
this.setPageBackGuardShow(false);
this.promptExitBeforeLeave({
onStay: () => {
this.syncPageBackGuard();
},
onExit: () => {
this.safeNavigateBack();
},
});
// #endif
},
disableBackGuardBeforeLeave() {
this.setPageBackGuardShow(false, { suppressLeave: true });
},
async bindSpecialPrescriptionRecord() {
if (!this.register_id) return;
await tryCreateSpecialPrescriptionPatientRecord(this.register_id);
},
promptExitBeforeLeave({ onStay, onExit } = {}) {
uni.showActionSheet({
itemList: ['返回上一步', '继续支付', '退出'],
success: (res) => {
if (res.tapIndex === 0) {
this.$emit('prev-step');
} else if (res.tapIndex === 1) {
if (onStay) {
onStay();
} else {
this.syncPageBackGuard();
}
} else if (res.tapIndex === 2) {
if (onExit) {
onExit();
} else {
this.safeNavigateBack();
}
}
},
fail: () => {
if (onStay) {
onStay();
} else {
this.syncPageBackGuard();
}
},
});
},
stripDoctorPrefix(raw) {
if (raw === null || raw === undefined || raw === '') return '';
return String(raw).replace(/^doctor-/, '').trim();
},
applyDoctorNumericId(id) {
const n = this.stripDoctorPrefix(id);
if (!n) return;
this.currentDoctorId = 'doctor-' + n;
},
getDoctorNumericIdFromLocalSync() {
const rl = this.registList;
if (rl && typeof rl === 'object' && !Array.isArray(rl) && rl.service_user_id) {
const s = this.stripDoctorPrefix(rl.service_user_id);
if (s) return s;
}
if (this.rInfo) {
const d = this.rInfo.doctor_id != null && this.rInfo.doctor_id !== '' ? this.rInfo.doctor_id : this.rInfo.doctorId;
if (d != null && d !== '') {
const s = this.stripDoctorPrefix(d);
if (s) return s;
}
}
if (this._urlDoctorId) {
const s = this.stripDoctorPrefix(this._urlDoctorId);
if (s) return s;
}
const cur = this.stripDoctorPrefix(this.currentDoctorId);
if (cur) return cur;
return '';
},
async fetchDefaultDoctorIdApi() {
const storeId = uni.getStorageSync('store_id') || '11001';
try {
const res = await request('/xkApi/online-consultation/get-default-doctor-id', {
method: 'GET',
data: { store_id: storeId }
}, 0);
const id = res.data?.result?.doctor_id ?? res.data?.doctor_id;
return id ? this.stripDoctorPrefix(id) : '';
} catch (err) {
console.error('get-default-doctor-id failed', err);
return '';
}
},
async resolveDoctorNumericIdForIm() {
let id = this.getDoctorNumericIdFromLocalSync();
if (!id) id = await this.fetchDefaultDoctorIdApi();
if (id) this.applyDoctorNumericId(id);
return id || '';
},
async ensureDoctorAfterLoad() {
const existing = this.getDoctorNumericIdFromLocalSync();
if (existing) {
this.applyDoctorNumericId(existing);
return;
}
const id = await this.fetchDefaultDoctorIdApi();
if (id) this.applyDoctorNumericId(id);
},
async waitForRegisterDetail() {
if (!this.register_id) return;
try {
if (this._getInfoPromise) await this._getInfoPromise;
else await this.getInfo();
} catch (err) {
console.error('waitForRegisterDetail', err);
}
},
needsClinicPostRegisterDrugPage() {
if (!this.registList || this.registList.register_type !== 2) return false;
const { registerInfoId, drugsPayload } = resolveFollowUpDrugsFromRInfo(this.rInfo);
return !!(registerInfoId && drugsPayload.length);
},
getInfo() {
const that = this;
if (!this.register_id) return Promise.resolve();
const p = registeInfo({
method: 'post',
data: {
store_id: uni.getStorageSync('store_id') || '11001',
register_id: that.register_id
}
}).then((res) => {
if (res.data.errcode == 0 && res.data.data.list && res.data.data.list[0]) {
that.registList = res.data.data.list[0];
that.registerType = that.registerMap[that.registList.register_type];
if (that.registList.service_user_id) {
that.applyDoctorNumericId(that.registList.service_user_id);
}
}
that.syncPageBackGuard();
});
this._getInfoPromise = p;
return p;
},
async handleOnlineConsultationAfterPayment() {
const that = this;
try {
const doctorId = await that.resolveDoctorNumericIdForIm();
if (!doctorId) {
uni.hideLoading();
uni.showToast({
title: '无法获取医生信息,请稍后重试',
icon: 'none'
});
return;
}
const doctorPrefixed = 'doctor-' + doctorId;
that.currentDoctorId = doctorPrefixed;
const patientId = that.currentUserId.replace('user-', '');
const roomType = (that.registList && that.registList.register_type === 2) ? 2 : 3;
const roomRes = await request('/xkApi/online-consultation/get-or-create-chat-room', {
method: 'POST',
data: {
doctor_id: doctorId,
patient_id: patientId,
type: roomType
}
}, 1);
const roomId = roomRes.data?.result?.room_id || roomRes.data?.room_id;
if (!roomId) {
uni.hideLoading();
uni.showToast({
title: '创建聊天房间失败',
icon: 'none'
});
return;
}
const storeType = uni.getStorageSync('store_type');
if (storeType === '1' || storeType === 1) {
const patientExperienceData = {
symptom_description: that.rInfo.illness_info || '',
has_visited: that.rInfo.has_visited || '0',
has_used_drug: that.rInfo.has_used_drug || '0',
drug_name: that.rInfo.drug_name || '',
illness_info: that.rInfo.illness_info || '',
doctor_id: doctorId,
user_id: patientId,
register_id: that.register_id,
created_at: new Date().toLocaleString('zh-CN')
};
await sendToUserApi({
room_id: roomId,
sender_user_id: `user-${patientId}`,
receiver_user_id: `doctor-${doctorId}`,
message_type: 11,
message_content: JSON.stringify(patientExperienceData),
duration: 0
});
} else {
await sendToUserApi({
room_id: roomId,
sender_user_id: `user-${patientId}`,
receiver_user_id: `doctor-${doctorId}`,
message_type: 10,
message_content: JSON.stringify({
id: that.register_id,
}),
duration: 0
});
}
const chatUrl = `/subPackages/chat/chat?user_id=${encodeURIComponent(doctorPrefixed)}&room_id=${roomId}&register_id=${that.register_id}`;
const goAfterGenOrder = () => {
if (that.needsClinicPostRegisterDrugPage()) {
const { registerInfoId, drugsPayload } = resolveFollowUpDrugsFromRInfo(that.rInfo);
uni.setStorageSync(
CLINIC_POST_REGISTER_STORAGE_KEY,
JSON.stringify({
register_info_id: registerInfoId,
register_id: that.register_id,
drugs: drugsPayload,
question: '您是否曾使用过此类药品?',
room_id: roomId,
doctor_id: doctorId,
patient_id: patientId,
doctor_prefixed: doctorPrefixed,
}),
);
uni.hideLoading();
that.disableBackGuardBeforeLeave();
uni.redirectTo({
url: '/subPackages/follow-up/medication-info?mode=clinic_post_register',
});
return;
}
uni.hideLoading();
that.disableBackGuardBeforeLeave();
uni.navigateTo({ url: chatUrl });
};
genOrderApi({
register_id: that.register_id,
register_info: typeof that.rInfo === 'string' ? that.rInfo : JSON.stringify(that.rInfo),
}).then(() => {
goAfterGenOrder();
}).catch((error) => {
console.error('生成订单失败:', error);
goAfterGenOrder();
});
} catch (error) {
uni.hideLoading();
console.error('处理在线复诊流程失败:', error);
uni.showToast({
title: '处理失败,请重试',
icon: 'none'
});
}
},
next() {
const that = this;
if (this.flag) {
this.flag = false;
registePay({
method: 'post',
data: {
register_id: this.register_id,
store_id: uni.getStorageSync('store_id') || '11001',
}
}).then(async (res) => {
uni.showLoading({ title: '加载中' });
if (checkDev('open-im') && that.registList && that.registList.register_type === 1) {
createRoomByDoctorIdApi({
doctor_id: that.currentDoctorId,
type: 1
}).then(result => {
sendToUserApi({
room_id: result.data.result.room_id,
sender_user_id: `user-${that.currentUserId}`,
receiver_user_id: `doctor-${that.currentDoctorId}`,
message_type: 10,
message_content: JSON.stringify({
id: that.register_id,
}),
duration: 0
});
});
}
if (res.data.data.is_paid == 1) {
await that.waitForRegisterDetail();
if (that.registList && (that.registList.register_type === 2 || that.registList.register_type === 3)) {
await that.handleOnlineConsultationAfterPayment();
return;
}
setTimeout(() => {
that.disableBackGuardBeforeLeave();
uni.navigateTo({
url: `/subPackages/register/register-success?id=${that.register_id}&register_type=${that.registList.register_type}`,
});
uni.hideLoading();
}, 800);
return;
}
uni.requestPayment({
provider: res.data.data.appId,
timeStamp: res.data.data.timestamp,
nonceStr: res.data.data.nonceStr,
package: res.data.data.package,
signType: res.data.data.signType,
paySign: res.data.data.paySign,
success: async () => {
await that.waitForRegisterDetail();
that.flag = false;
if (that.registList && (that.registList.register_type === 2 || that.registList.register_type === 3)) {
uni.showLoading({ title: '加载中' });
await that.handleOnlineConsultationAfterPayment();
return;
}
setTimeout(() => {
that.disableBackGuardBeforeLeave();
uni.navigateTo({
url: '/subPackages/register/register-success?id=' + that.register_id
});
uni.hideLoading();
}, 300);
},
fail: function(err) {
that.flag = false;
uni.hideLoading();
setTimeout(() => {
that.$refs.uToast.show({
title: (err && err.errMsg) || '支付失败',
type: 'default',
icon: false
});
}, 300);
}
});
}).catch((err) => {
console.error('registePay', err);
that.flag = true;
uni.hideLoading();
});
}
},
toHome() {
uni.switchTab({
url: '/pages/home/home'
});
},
toAbout() {
service({
method: 'get',
data: {
store_id: uni.getStorageSync('store_id') || '11001',
type: 3
}
}).then((res) => {
if (res.data.errcode == 0) {
this.content = res.data.data.content;
}
});
},
},
};
</script>
<style lang="scss" scoped>
.container {
width: 750rpx;
box-sizing: border-box;
padding-top: 32rpx;
padding-bottom: calc(320rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(320rpx + env(safe-area-inset-bottom));
background: #F8FAFC;
.bottom-spacer {
height: 120rpx;
flex-shrink: 0;
}
.title {
width: 750rpx;
height: 80rpx;
margin: 0 auto;
font-size: 32rpx;
font-family: PingFang SC-Bold, PingFang SC;
font-weight: bold;
color: #666666;
line-height: 80rpx;
padding-left: 32rpx;
}
.card {
width: 750rpx;
background: #FFFFFF;
border-radius: 8rpx;
padding: 32rpx;
margin: 24rpx auto 32rpx;
.item:first-child {
margin: 0 0 48rpx;
}
.item {
display: flex;
justify-content: space-between;
height: 40rpx;
font-family: PingFang SC-Regular, PingFang SC;
line-height: 33rpx;
margin: 48rpx 0 0;
.aapoint {
font-size: 28rpx;
font-weight: 400;
color: #666666;
}
.detail {
font-size: 28rpx;
font-weight: bold;
color: #232323;
}
.active {
color: #F44336;
}
}
}
.info {
width: 750rpx;
background: #FFFFFF;
padding: 32rpx;
border-radius: 8rpx;
margin: 10rpx auto 48rpx;
.name_edit:first-child {
margin-top: 0;
}
.name_edit {
margin-top: 48rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
line-height: 33rpx;
display: flex;
justify-content: space-between;
.name {
color: #475569;
}
.word_num {
font-size: 30rpx;
font-weight: 600;
color: #1E293B;
}
}
}
.footer {
width: 750rpx;
display: flex;
flex-direction: column;
padding: 0 32rpx;
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
background-color: #FFFFFF;
border-top: 2rpx solid #F1F5F9;
z-index: 100;
.allknow {
display: flex;
align-items: center;
width: 710rpx;
height: 40rpx;
padding: 0 32rpx;
margin: 10rpx auto;
.isknow {
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #666666;
margin-left: 8rpx;
.notice-link {
color: #4175EE;
}
}
}
.footer-btns {
display: flex;
align-items: center;
gap: 20rpx;
margin: 20rpx auto;
width: 686rpx;
}
.btn-prev {
flex-shrink: 0;
width: 180rpx;
height: 86rpx;
line-height: 86rpx;
text-align: center;
font-size: 28rpx;
color: #4175EE;
background: #FFFFFF;
border: 2rpx solid #4175EE;
border-radius: 64rpx;
padding: 0;
margin: 0;
&::after {
border: none;
}
}
.btn {
text-align: center;
flex: 1;
height: 86rpx;
line-height: 86rpx;
font-size: 32rpx;
font-family: PingFang SC-Medium, PingFang SC;
font-weight: 500;
color: #FFFFFF;
background: #4175EE;
border-radius: 64rpx;
margin: 0;
}
.gh_ {
margin-bottom: 30rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #333333;
}
}
.agreen {
padding: 60rpx 50rpx 20rpx;
}
.confirm-modal {
padding: 40rpx 30rpx;
background: #fff;
.confirm-title {
font-size: 34rpx;
font-weight: bold;
color: #333;
text-align: center;
margin-bottom: 30rpx;
}
.confirm-desc {
font-size: 28rpx;
color: #666;
line-height: 1.6;
margin-bottom: 40rpx;
text-align: center;
.link {
color: #2979FF;
}
}
.confirm-footer {
display: flex;
justify-content: space-between;
gap: 20rpx;
.cancel-btn,
.agree-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
font-size: 30rpx;
border: none;
&::after {
border: none;
}
}
.cancel-btn {
background: #F5F7FA;
color: #909399;
}
.agree-btn {
background: #4175EE;
color: #FFFFFF;
}
}
}
}
</style>

View File

@@ -0,0 +1,840 @@
<template>
<view class="container safe-area-inset-bottom">
<view class="header-section">
<view class="title-row">
<text class="main-title">您需要为谁{{ registerType === '3' || registerType === '2' ? '复诊' : '挂号' }}</text>
<view class="register-type-tag">
<text class="type-label">{{ registerTypeLabel }}</text>
</view>
</view>
<text v-if="showDelegateInfo" class="delegate-info">
委托诊所{{ delegateStoreName }} 委托医生{{ delegateDoctorName }}
</text>
<text class="sub-title">请选择患者信息以便医生给出更准确的诊疗建议患者信息仅医生可见</text>
</view>
<view class="detail">
<view class="one" :class="actived==item.id ? 'active' :''" @click="choose(item.id)"
v-for="item in setInfoList" :key="item.id">
<view class="name">
{{item.name}}
<text class="iconfont icon-bianji" v-if="actived==item.id" @click.stop="toEdit"></text>
</view>
<text class="patient-meta">{{(item.sex)% 2 == 1 ? "男" : "女"}} &nbsp; {{item.age}}</text>
<image class="choose-badge" src="https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/choose/choosed.png" mode=""></image>
</view>
<view class="other" @click="goAdd(0)">
<text class="add-plus">+</text>
</view>
</view>
<view class="card follow-up-card modern-card" v-if="showFollowUpForm">
<view class="section-header">
<text class="title-text">患者主诉</text>
<text class="req-tag">* 必填</text>
</view>
<view class="input-wrapper">
<textarea
class="chief-textarea"
v-model="chiefComplaint"
placeholder="请简要描述症状、持续时间等..."
placeholder-style="color:#94A3B8; font-size: 28rpx;"
maxlength="2000"
auto-height
/>
</view>
<view class="section-divider"></view>
<view class="section-header">
<text class="title-text">选择相关西药</text>
<text class="req-tag">* 必选</text>
</view>
<view class="hint-box">
<text class="sub-hint">请至少选择一种药品仅展示本门店与总后台均已上架的西药数量仅作展示提交按药品种类关联</text>
</view>
<view class="drug-container">
<FollowUpDrugDrawer
v-model="selectedFollowUpDrugs"
:store-id="resolveRegisterStoreId()"
/>
</view>
</view>
<view class="card" v-if="showHealthInfo" style="margin-top: 0;">
<view class="card_title">健康信息</view>
<view class="card_names">
<text class="health-label">肝功能</text>
<u-radio-group v-model="liver" size="44rpx">
<u-radio size="44rpx" active-color="#2979FF" :name="liver">{{liver}}</u-radio>
</u-radio-group>
</view>
<view class="card_names">
<text class="health-label">肾功能</text>
<u-radio-group v-model="renal" size="44rpx">
<u-radio :name="renal">{{renal}}</u-radio>
</u-radio-group>
</view>
<view class="card_history">
<view class="name">
<text class="health-label">过敏史</text>
<u-radio-group v-model="allergic" size="44rpx">
<u-radio :name="allergic">{{allergic}}</u-radio>
</u-radio-group>
</view>
<view class="tabs" v-if="allergicList[0]!='' && allergic === '有'">
<u-tag bg-color="#2979FF" color="#FFFFFF" v-for="(item,index) in allergicList" :key="index"
:text="item" mode="dark" />
</view>
</view>
<view class="card_history">
<view class="name">
<text class="health-label">个人病史</text>
<u-radio-group v-model="person" size="44rpx">
<u-radio :name="person">{{person}}</u-radio>
</u-radio-group>
</view>
<view class="tabs" v-if="personList[0]!='' && person === '有'">
<u-tag bg-color="#2979FF" color="#FFFFFF" v-for="(item,index) in personList" :key="index"
:text="item" mode="dark" />
</view>
</view>
<view class="card_history">
<view class="name">
<text class="health-label">家族病史</text>
<u-radio-group v-model="family" size="44rpx">
<u-radio :name="family">{{family}}</u-radio>
</u-radio-group>
</view>
<view class="tabs" v-if="familyList[0]!='' && family === '有'">
<u-tag bg-color="#2979FF" color="#FFFFFF" v-for="(item,index) in familyList" :key="index"
:text="item" mode="dark" />
</view>
</view>
</view>
<view class="footer">
<view class="infos" @click="subScriptionMessage">下一步</view>
</view>
<u-toast ref="uToast" />
</view>
</template>
<script>
import {
userList,
userEidtPatient,
registe,
storeInfo,
doctorDetail,
} from '@/request/api/api.js';
import { getClinicOnlineConsultationInfoApi } from '@/request/api/product';
import { createRegisterInfoApi, saveRegisterInfoApi } from '@/request/api/order';
import request from '@/request/api/request';
export default {
name: 'RegisterPatientStep',
props: {
doctorId: {
type: [String, Number],
default: '',
},
registerTypeProp: {
type: String,
default: '0',
},
initialRInfo: {
type: Object,
default: () => ({}),
},
delegateStoreIdProp: {
type: String,
default: '',
},
},
data() {
return {
Did: '',
actived: 1,
infoList: [],
register_id: '',
liver: '',
renal: '',
allergic: '',
allergicList: [],
person: '',
personList: [],
family: '',
familyList: [],
heathList: [],
rInfo: {},
registerType: '0',
delegateStoreId: '',
delegateStoreName: '',
delegateDoctorName: '',
chiefComplaint: '',
selectedFollowUpDrugs: [],
};
},
computed: {
setInfoList() {
return this.infoList.map((item) => ({ ...item }));
},
registerTypeLabel() {
const typeMap = {
'0': '线下就诊',
'1': '在线问诊',
'2': '在线复诊',
'3': '在线复诊'
};
return typeMap[this.registerType] || '未知类型';
},
hasPrefilledFollowUpInfo() {
const r = this.rInfo;
return !!(r && typeof r === 'object' && !Array.isArray(r) && r.info_id);
},
showFollowUpForm() {
const t = this.registerType;
return (t === '2' || t === '3') && !this.hasPrefilledFollowUpInfo;
},
showDelegateInfo() {
const isFollowUp = this.registerType === '2' || this.registerType === '3';
return isFollowUp && !!(this.delegateStoreName || this.delegateDoctorName);
},
showHealthInfo() {
return this.registerType === '0'
&& !!this.actived
&& this.infoList.some((p) => p.id === this.actived);
},
},
watch: {
doctorId: {
immediate: true,
handler(val) {
this.Did = val;
},
},
registerTypeProp: {
immediate: true,
handler(val) {
if (val) this.registerType = val;
},
},
initialRInfo: {
immediate: true,
deep: true,
handler(val) {
this.rInfo = val ? { ...val } : {};
},
},
delegateStoreIdProp: {
immediate: true,
handler(val) {
this.delegateStoreId = val || '';
this.loadDelegateDisplayInfo();
},
},
},
mounted() {
this.getInfolist();
},
methods: {
resolveRegisterStoreId() {
return (this.registerType !== '0' && this.delegateStoreId)
? this.delegateStoreId
: (uni.getStorageSync('store_id') || '11001');
},
async loadDelegateDisplayInfo() {
if (this.registerType !== '2' && this.registerType !== '3') return;
const currentStoreId = uni.getStorageSync('store_id') || '11001';
try {
if (this.registerType === '2') {
const res = await getClinicOnlineConsultationInfoApi({ store_id: currentStoreId });
const result = res.data?.result;
if (res.data?.code === 0 && result) {
this.delegateStoreName = result.delegate_store_name || result.store_name || '';
this.delegateDoctorName = result.doctor_name || '';
}
} else {
const storeId = this.delegateStoreId || this.resolveRegisterStoreId();
const [storeRes, doctorRes] = await Promise.all([
storeInfo({ method: 'post', data: { store_id: storeId } }),
doctorDetail({ method: 'post', data: { doctor_id: this.Did, store_id: currentStoreId } }),
]);
if (storeRes.data?.errcode == 0) {
this.delegateStoreName = storeRes.data.data?.store?.name || '';
}
const doctorData = doctorRes.data?.data || doctorRes.data?.result;
if (doctorRes.data?.errcode == 0 || doctorRes.data?.code === 0) {
this.delegateDoctorName = doctorData?.name || '';
}
}
} catch (e) {
console.warn('loadDelegateDisplayInfo', e);
}
},
followUpDrugIdsForApi() {
const ids = [];
const seen = new Set();
for (const it of this.selectedFollowUpDrugs || []) {
const id = Number(it.id);
if (id > 0 && !seen.has(id)) {
seen.add(id);
ids.push(id);
}
}
return ids;
},
validateFollowUpFields() {
const chief = String(this.chiefComplaint || '').trim();
if (!chief) return '请填写患者主诉';
if (this.followUpDrugIdsForApi().length === 0) return '请至少选择一种药品';
return '';
},
async prepareFollowUpRegisterInfo() {
let params = typeof this.rInfo === 'string' ? JSON.parse(this.rInfo) : { ...(this.rInfo || {}) };
if (params.info_id) {
return params;
}
const err = this.validateFollowUpFields();
if (err) {
throw new Error(err);
}
const chief = String(this.chiefComplaint || '').trim();
const storeId = this.resolveRegisterStoreId();
const westernDrugQty = {};
let qtySum = 0;
for (const d of this.selectedFollowUpDrugs || []) {
const id = Number(d.id);
const q = Math.min(99, Math.max(1, Number(d.quantity) || 1));
if (id > 0) {
westernDrugQty[String(id)] = q;
qtySum += q;
}
}
const createRes = await createRegisterInfoApi({
doctor_id: Number(this.Did),
drug_id: '',
has_visited: '',
has_used_drug: '',
illnessInfo: '',
type: 2,
number: qtySum >= 1 ? qtySum : 1,
store_id: Number(storeId) || storeId,
chief_complaint: chief,
drug_ids: this.followUpDrugIdsForApi(),
western_drug_qty: JSON.stringify(westernDrugQty),
});
const infoId = createRes.data?.result?.info_id;
if (!infoId) {
throw new Error('创建就诊信息失败');
}
const followUpDrugs = (this.selectedFollowUpDrugs || [])
.map((d) => ({
drug_id: Number(d.id),
name: d.drug_name || d.name || '',
quantity: Math.min(99, Math.max(1, Number(d.quantity) || 1)),
}))
.filter((d) => d.drug_id > 0);
const followUpNumber = followUpDrugs.reduce((s, r) => s + (Number(r.quantity) || 1), 0) || 1;
return { ...params, info_id: infoId, follow_up_drugs: followUpDrugs, follow_up_number: followUpNumber, number: followUpNumber };
},
goAdd() {
uni.navigateTo({ url: '/subPackages/my/myinfo-add?id=' });
},
toEdit() {
uni.navigateTo({ url: '/subPackages/my/myinfo-edit?id=' + this.actived });
},
choose(id) {
this.actived = id;
this.getHeath();
},
getInfolist() {
userList({
method: 'post',
data: {
store_id: uni.getStorageSync('store_id') || '11001',
}
}).then((res) => {
if (res.data.errcode == 0) {
this.infoList = res.data.data;
this.infoList.filter(item => {
if (item.is_default == 1) {
this.actived = item.id;
}
});
this.getHeath();
}
});
},
normalizeRInfo(followUpRInfo) {
if (typeof followUpRInfo === 'string') {
try {
return JSON.parse(followUpRInfo);
} catch (e) {
return {};
}
}
return followUpRInfo || {};
},
emitRegisterCreated(followUpRInfo, isHas) {
const rInfoObj = this.normalizeRInfo(followUpRInfo);
const payload = {
register_id: this.register_id,
doctor_id: this.Did,
rInfo: rInfoObj,
};
if (isHas) {
this.$refs.uToast.show({
title: '您已挂号该医生,请继续问诊服务',
type: 'default',
icon: false
});
setTimeout(() => {
this.$emit('register-created', payload);
}, 900);
} else {
this.$emit('register-created', payload);
}
},
async afterRegisterCreated(followUpRInfo) {
if (this.registerType === '3' || this.registerType === '2') {
const params = this.normalizeRInfo(followUpRInfo);
if (params.info_id) {
try {
await request('/xkApi/order/update-register-info-register-id', {
method: 'POST',
data: {
info_id: params.info_id,
register_id: this.register_id,
}
});
} catch (error) {
console.error('更新就诊信息关联失败:', error);
}
} else {
try {
await saveRegisterInfoApi({
register_id: this.register_id,
doctor_id: params.doctor_id || this.Did,
drug_id: params.drug_id || params.id,
has_visited: params.has_visited || params.hasVisited,
has_used_drug: params.has_used_drug || params.hasUsedDrug,
illnessInfo: params.illness_info || params.illnessInfo,
type: params.type,
number: params.number,
store_id: uni.getStorageSync('store_id') || '11001',
});
} catch (error) {
console.error('保存患者就诊信息失败:', error);
}
}
}
uni.setStorageSync('register_id', this.register_id);
this.emitRegisterCreated(followUpRInfo, false);
},
async getAsk() {
uni.showLoading({ title: '加载中...', mask: true });
const storeId = this.resolveRegisterStoreId();
let followUpRInfo = this.rInfo;
if (this.showFollowUpForm) {
try {
followUpRInfo = await this.prepareFollowUpRegisterInfo();
this.rInfo = followUpRInfo;
} catch (e) {
uni.hideLoading();
this.$refs.uToast.show({
title: e.message || '提交失败',
type: 'default',
icon: false
});
return;
}
}
const originStoreId = uni.getStorageSync('store_id') || '';
const shouldSendOriginStoreId =
this.registerType === '2' ||
this.registerType === '3' ||
(this.registerType !== '0' && !!this.delegateStoreId);
registe({
method: 'post',
data: {
user_id: uni.getStorageSync('user_id'),
user_patient_id: this.actived,
store_id: storeId,
origin_store_id: shouldSendOriginStoreId ? originStoreId : 0,
service_user_id: this.Did,
register_type: this.registerType
}
}).then(async (res) => {
if (res.data.errcode == 0) {
this.register_id = res.data.data.register_id;
if (this.registerType === '3' || this.registerType === '2') {
await this.afterRegisterCreated(followUpRInfo);
return;
}
uni.setStorageSync('register_id', this.register_id);
this.emitRegisterCreated(followUpRInfo, res.data.data.isHas == 1);
} else {
this.$refs.uToast.show({
title: res.data.msg,
type: 'default',
icon: false
});
}
}).finally(() => {
uni.hideLoading();
});
},
getHeath() {
userEidtPatient({
method: 'post',
data: {
user_patient_id: this.actived,
store_id: uni.getStorageSync('store_id') || '11001',
}
}).then((res) => {
if (res.data.errcode == 0) {
this.heathList = res.data.data;
this.liver = (this.heathList.liver_function) % 2 == 0 ? '正常' : '异常';
this.renal = (this.heathList.renal_function) % 2 == 0 ? '正常' : '异常';
this.allergic = (this.heathList.allergic_status) % 2 == 0 ? '无' : '有';
this.allergicList = (this.heathList.allergic_history) == 'Array' ? [] : (this.heathList.allergic_history);
this.person = (this.heathList.person_status) % 2 == 0 ? '无' : '有';
this.personList = (this.heathList.person_history) == 'Array' ? [] : (this.heathList.person_history);
this.family = (this.heathList.family_status) % 2 == 0 ? '无' : '有';
this.familyList = (this.heathList.family_history) == 'Array' ? [] : (this.heathList.family_history);
}
});
},
subScriptionMessage() {
if (this.showFollowUpForm) {
const err = this.validateFollowUpFields();
if (err) {
this.$refs.uToast.show({
title: err,
type: 'default',
icon: false
});
return;
}
}
uni.requestSubscribeMessage({
tmplIds: [
'O5P8ZZB6T01V5Z66tkbhmzlobSaSRoEfdoCLATuP2Mw',
'ddT3v-m_Sr4kXwGCBLf0O4P89mOS5q10LFMZeEuQm10'
],
complete: () => {
this.getAsk();
}
});
},
refreshPatientList() {
this.getInfolist();
},
},
};
</script>
<style lang="scss" scoped>
.container {
width: 750rpx;
max-height: 100%;
min-height: 100vh;
box-sizing: border-box;
background-color: #F8FAFC;
padding-bottom: 200rpx;
padding-bottom: calc(140rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
.header-section {
padding: 32rpx 32rpx 0;
.title-row {
display: flex;
align-items: center;
margin-bottom: 12rpx;
.main-title {
font-size: 38rpx;
font-family: PingFang SC-Medium, PingFang SC;
font-weight: 600;
color: #0F172A;
margin-right: 16rpx;
}
.register-type-tag {
background: linear-gradient(135deg, #4175EE 0%, #298dff 100%);
padding: 4rpx 16rpx;
border-radius: 8rpx;
box-shadow: 0 2rpx 8rpx rgba(65, 117, 238, 0.2);
.type-label {
color: #FFFFFF;
font-size: 24rpx;
font-weight: 500;
}
}
}
.sub-title {
display: block;
font-size: 24rpx;
font-weight: 400;
color: #64748B;
line-height: 1.5;
}
.delegate-info {
display: block;
font-size: 26rpx;
font-weight: 400;
color: #4175EE;
line-height: 1.5;
margin-bottom: 8rpx;
}
}
.detail {
width: 710rpx;
min-height: 124rpx;
margin: 32rpx;
padding-bottom: 8rpx;
display: flex;
flex-wrap: wrap;
align-items: flex-start;
.one {
width: 218rpx;
height: 124rpx;
border-radius: 8rpx;
margin: 0 16rpx 16rpx 0;
border: 2rpx solid #CBD5E1;
position: relative;
background: #FFFFFF;
.name {
margin: 16rpx 0 8rpx 32rpx;
height: 44rpx;
font-size: 32rpx;
font-weight: 400;
color: #0F172A;
display: flex;
justify-content: space-between;
align-items: center;
padding-right: 18rpx;
.iconfont {
font-size: 32rpx;
color: #94A3B8;
}
}
.patient-meta {
font-size: 28rpx;
color: #94A3B8;
margin-left: 32rpx;
}
.choose-badge {
width: 50rpx;
height: 50rpx;
position: absolute;
right: -2rpx;
bottom: -1rpx;
display: none;
}
&.active {
border: 2rpx solid #536DFE;
background: rgba(83, 109, 254, 0.04);
.choose-badge {
display: block;
}
}
}
.other {
width: 218rpx;
height: 124rpx;
line-height: 104rpx;
text-align: center;
border-radius: 8rpx;
border: 2rpx dashed #CBD5E1;
background: #FFFFFF;
.add-plus {
font-size: 68rpx;
color: #94A3B8;
}
}
}
.card {
width: 710rpx;
background: #FFFFFF;
border-radius: 16rpx;
margin: 20rpx auto 40rpx;
padding: 0 32rpx 40rpx;
box-shadow: 0 4rpx 24rpx rgba(15, 23, 42, 0.02);
.card_title {
height: 114rpx;
line-height: 114rpx;
font-size: 34rpx;
font-weight: 600;
color: #1E293B;
border-bottom: 1rpx solid #F1F5F9;
}
.card_names {
width: 646rpx;
padding: 34rpx 0 32rpx 0;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 30rpx;
border-bottom: 1rpx solid #F1F5F9;
.health-label {
color: #1E293B;
}
}
.card_history {
width: 646rpx;
padding: 34rpx 0 32rpx 0;
font-size: 30rpx;
border-bottom: 1rpx solid #F1F5F9;
.name {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10rpx;
.health-label {
color: #1E293B;
}
}
.tabs {
display: flex;
flex-wrap: wrap;
::v-deep .u-tag {
margin: 10rpx 20rpx 10rpx 0;
}
}
}
.card_history:last-child {
border-bottom: 0;
}
}
.modern-card {
padding: 32rpx;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
.title-text {
font-size: 32rpx;
font-weight: 600;
color: #0F172A;
position: relative;
padding-left: 18rpx;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 8rpx;
height: 28rpx;
background: #4175EE;
border-radius: 4rpx;
}
}
.req-tag {
color: #EF4444;
font-size: 24rpx;
background: rgba(239, 68, 68, 0.08);
padding: 4rpx 12rpx;
border-radius: 6rpx;
}
}
.section-divider {
height: 1rpx;
background-color: #F1F5F9;
margin: 40rpx 0 32rpx;
}
.input-wrapper {
background: #F8FAFC;
border-radius: 12rpx;
.chief-textarea {
width: 100%;
min-height: 160rpx;
padding: 24rpx;
box-sizing: border-box;
font-size: 28rpx;
color: #334155;
line-height: 1.5;
}
}
.hint-box {
background: #F1F5F9;
border-radius: 8rpx;
padding: 16rpx 24rpx;
margin-bottom: 24rpx;
.sub-hint {
font-size: 24rpx;
color: #64748B;
line-height: 1.6;
}
}
}
.footer {
width: 750rpx;
min-height: 104rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-top: 2rpx solid #F1F5F9;
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
background-color: #FFFFFF;
z-index: 100;
.infos {
width: 686rpx;
height: 90rpx;
line-height: 90rpx;
text-align: center;
margin: 16rpx 0;
font-size: 32rpx;
font-weight: 500;
background: linear-gradient(135deg, #4175EE 0%, #298dff 100%);
box-shadow: 0 8rpx 20rpx rgba(65, 117, 238, 0.3);
border-radius: 198rpx;
color: #FFFFFF;
}
}
}
</style>

View File

@@ -0,0 +1,148 @@
<template>
<view class="register-step-bar">
<view
class="step-item"
:class="{
active: currentStep === 1,
done: currentStep > 1,
clickable: currentStep === 2 && allowBackToStep1,
}"
@click="onStepClick(1)"
>
<view class="step-circle">
<text v-if="currentStep > 1" class="step-check"></text>
<text v-else class="step-num">1</text>
</view>
<text class="step-label">选择就诊人</text>
</view>
<view class="step-line" :class="{ done: currentStep > 1 }"></view>
<view class="step-item" :class="{ active: currentStep === 2 }" @click="onStepClick(2)">
<view class="step-circle">
<text class="step-num">2</text>
</view>
<text class="step-label">确认预约信息支付挂号费</text>
</view>
</view>
</template>
<script>
export default {
name: 'RegisterStepBar',
props: {
currentStep: {
type: Number,
default: 1,
},
allowBackToStep1: {
type: Boolean,
default: false,
},
},
methods: {
onStepClick(stepNum) {
if (stepNum === 1 && this.currentStep === 2 && this.allowBackToStep1) {
this.$emit('step-click', 1);
}
},
},
};
</script>
<style lang="scss" scoped>
.register-step-bar {
display: flex;
align-items: center;
justify-content: center;
padding: 24rpx 32rpx 16rpx;
background: #F8FAFC;
.step-item {
display: flex;
flex-direction: column;
align-items: center;
min-width: 140rpx;
&.clickable {
.step-circle,
.step-label {
cursor: pointer;
}
&:active {
opacity: 0.75;
}
}
.step-circle {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: #E2E8F0;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8rpx;
.step-num,
.step-check {
font-size: 24rpx;
font-weight: 600;
color: #94A3B8;
}
}
.step-label {
font-size: 24rpx;
color: #94A3B8;
white-space: nowrap;
}
&.active {
.step-circle {
background: #4175EE;
.step-num {
color: #FFFFFF;
}
}
.step-label {
color: #4175EE;
font-weight: 600;
}
}
&.done {
.step-circle {
background: rgba(65, 117, 238, 0.15);
.step-check {
color: #4175EE;
}
}
.step-label {
color: #64748B;
}
&.clickable .step-label {
color: #4175EE;
}
}
}
.step-line {
flex: 1;
max-width: 120rpx;
height: 4rpx;
background: #E2E8F0;
margin: 0 16rpx;
margin-bottom: 32rpx;
border-radius: 2rpx;
&.done {
background: #4175EE;
}
}
}
</style>

View File

@@ -18,6 +18,11 @@
</template>
<script>
import {
saveSpecialPrescriptionRegisterContext,
tryCreateSpecialPrescriptionPatientRecord,
} from '@/request/api/specialPrescription';
export default {
data() {
return {
@@ -26,8 +31,19 @@
},
onLoad(e) {
this.register_id = e.id
if (e.special_prescription_id) {
saveSpecialPrescriptionRegisterContext({
special_prescription_id: e.special_prescription_id,
dose_count: e.special_prescription_dose_count || 1,
doctor_id: e.doctor_id || '',
})
}
this.bindSpecialPrescriptionRecord()
},
methods: {
async bindSpecialPrescriptionRecord() {
await tryCreateSpecialPrescriptionPatientRecord(this.register_id)
},
// 确认
goOrder() {
uni.navigateTo({

View File

@@ -1,922 +1,39 @@
<template>
<view class="container safe-area-inset-bottom">
<!-- #ifdef MP-WEIXIN -->
<page-container
:show="pageBackGuardShow"
:overlay="false"
@leave="onPageBackGuardLeave"
/>
<!-- #endif -->
<MessageNotification />
<!-- 预约信息 -->
<view class="title">
预约信息
</view>
<view class="card">
<view class="item">
<view class="aapoint">
预约诊所
</view>
<view class="detail">
{{registList.store}}
</view>
</view>
<view class="item">
<view class="aapoint">
预约科室
</view>
<view class="detail">
{{registList.depart}}
</view>
</view>
<view class="item">
<view class="aapoint">
预约医生
</view>
<view class="detail">
{{registList.doctor}}
</view>
</view>
<view class="item">
<view class="aapoint">
预约序号
</view>
<view class="detail">
{{registList.order_number}}
</view>
</view>
<view class="item">
<view class="aapoint">
预计就诊时间
</view>
<view class="detail">
{{registList.created_at}}
</view>
</view>
<view class="item">
<view class="aapoint">
挂号金额
</view>
<view class="detail active">
{{registList.price}}
</view>
</view>
<view class="item">
<view class="aapoint">
挂号类型
</view>
<view class="detail">
{{registerType}}
</view>
</view>
</view>
<!-- 就诊人 -->
<view class="title">
就诊人
</view>
<view class="info">
<view class="name_edit">
<view class="name">
{{registList.patient}}
</view>
<view class="word_num">
{{registList.order_number}}
</view>
</view>
<!-- replace(/(\w{6})\w*(\w{4})/,'$1********$2') -->
<view class="name_edit">
<view class="name">
身份证号
</view>
<view class="word_num" v-if="registList.idcard">
{{registList.idcard_tm || registList.idcard}}
</view>
</view>
<view class="name_edit">
<view class="name">
手机号
</view>
<view class="word_num" v-if="registList.mobile">
{{registList.mobile_tm || registList.mobile}}
</view>
</view>
</view>
<!-- 须知 -->
<u-popup v-model="show" :safe-area-inset-bottom="true" mode="bottom" height="68%" border-radius="20"
:closeable="true" z-index="20000">
<view class="agreen">
<u-parse :html="content"></u-parse>
</view>
</u-popup>
<!-- 未勾选须知时的确认弹窗 -->
<u-popup v-model="showConfirm" mode="center" border-radius="24" width="600" z-index="10075">
<view class="confirm-modal">
<view class="confirm-title">预约挂号须知</view>
<view class="confirm-desc">
请您在使用前仔细阅读并同意
<text class="link" @click="show=true">预约挂号须知</text>
</view>
<view class="confirm-footer">
<button class="cancel-btn" @click="showConfirm = false">不同意</button>
<button class="agree-btn" @click="agreeAndRegister">同意并挂号</button>
</view>
</view>
</u-popup>
<!-- 底部 -->
<view class="footer">
<block v-if="registList.is_pay==0">
<!-- 须知 -->
<view class="allknow">
<u-icon size="40" :color="form['check']?'#2979FF':'#999999'" name="checkmark-circle-fill"
@click="form['check']=!form['check']"></u-icon>
<view class="isknow">我已阅读并同意 <text @click="show=true">预约挂号须知</text> </view>
</view>
<!-- 按钮 -->
<button class="btn" @click="handleRegisterClick">确认挂号
</button>
</block>
<view class="gh_" v-if="registList.is_pay==1" @click="toHome">
<button class="btn">您已挂号成功请等待医生接诊
</button>
</view>
</view>
<u-toast ref="uToast" />
</view>
<view class="register-redirect">
<u-loading mode="circle" />
</view>
</template>
<script>
import {
registeInfo,
registePay,
registe,
service
} from '../../request/api/api';
import {createRoomByDoctorIdApi, sendToUserApi} from "@/request/api/im";
import {checkDev} from "@/utils/utils";
import {genOrderApi, saveRegisterInfoApi} from "@/request/api/order";
import request from "@/request/api/request";
import {
CLINIC_POST_REGISTER_STORAGE_KEY,
resolveFollowUpDrugsFromRInfo,
} from '@/utils/clinicFollowUpAssistantIm';
export default {
data() {
return {
show: false,
showConfirm: false,
know: "",
form: {
check: false
},
register_id: "",
currentUserId: "",
currentDoctorId: "",
registList: [],
content: `预约挂号须知`,
flag: true,
registerType: '线下就诊',
registerMap: {
0: '挂号',
1: '咨询',
2: '购药',
3: '复诊',
},
rInfo: {},
_urlDoctorId: '',
_getInfoPromise: null,
pageBackGuardShow: false,
_suppressPageBackGuardLeave: false,
}
},
computed: {
shouldEnablePageBackGuard() {
return !(this.registList && this.registList.is_pay == 1);
},
},
watch: {
'registList.is_pay'(val) {
if (val == 1) {
this.setPageBackGuardShow(false, { suppressLeave: true });
} else {
this.syncPageBackGuard();
}
},
},
onLoad(e) {
const uid = uni.getStorageSync('user_id');
this.currentUserId = 'user-' + (uid !== '' && uid !== undefined && uid !== null ? uid : '2512');
this.register_id = e.id;
try {
this.rInfo = e.r_info ? (typeof e.r_info === 'string' ? JSON.parse(decodeURIComponent(e.r_info)) : e.r_info) : {};
} catch (err) {
console.error('register onLoad r_info parse', err);
this.rInfo = {};
}
const urlRaw = e.doctor_id && String(e.doctor_id).trim() !== '' ? String(e.doctor_id) : '';
this._urlDoctorId = urlRaw ? this.stripDoctorPrefix(urlRaw) : '';
let initialDoc = '';
if (this._urlDoctorId) initialDoc = this._urlDoctorId;
else if (this.rInfo && (this.rInfo.doctor_id != null && this.rInfo.doctor_id !== '')) {
initialDoc = this.stripDoctorPrefix(this.rInfo.doctor_id);
} else if (this.rInfo && (this.rInfo.doctorId != null && this.rInfo.doctorId !== '')) {
initialDoc = this.stripDoctorPrefix(this.rInfo.doctorId);
}
this.currentDoctorId = initialDoc ? ('doctor-' + initialDoc) : '';
uni.setStorageSync('register_id', this.register_id);
if (this.register_id) {
this._getInfoPromise = this.getInfo();
this._getInfoPromise.then(() => this.ensureDoctorAfterLoad());
} else {
this.ensureDoctorAfterLoad();
this.syncPageBackGuard();
}
},
onBackPress() {
if (this.closeTopOverlayIfAny()) {
return true;
}
if (this.registList && this.registList.is_pay == 1) {
return false;
}
this.promptExitBeforeLeave();
return true;
},
methods: {
handleRegisterClick() {
if (!this.form.check) {
this.showConfirm = true;
return;
}
this.next();
},
agreeAndRegister() {
this.form.check = true;
this.showConfirm = false;
this.next();
},
closeTopOverlayIfAny() {
if (this.show) {
this.show = false;
return true;
}
if (this.showConfirm) {
this.showConfirm = false;
return true;
}
return false;
},
setPageBackGuardShow(show, { suppressLeave = false } = {}) {
// #ifdef MP-WEIXIN
if (suppressLeave) this._suppressPageBackGuardLeave = true;
this.pageBackGuardShow = !!show;
// #endif
},
syncPageBackGuard() {
// #ifdef MP-WEIXIN
this.$nextTick(() => {
this.setPageBackGuardShow(this.shouldEnablePageBackGuard, {
suppressLeave: !this.shouldEnablePageBackGuard,
});
});
// #endif
},
safeNavigateBack() {
// #ifdef MP-WEIXIN
this.setPageBackGuardShow(false, { suppressLeave: true });
setTimeout(() => {
uni.navigateBack();
}, 16);
// #endif
// #ifndef MP-WEIXIN
uni.navigateBack();
// #endif
},
onPageBackGuardLeave() {
// #ifdef MP-WEIXIN
if (this._suppressPageBackGuardLeave) {
this._suppressPageBackGuardLeave = false;
return;
}
if (this.closeTopOverlayIfAny()) {
this.$nextTick(() => {
this.setPageBackGuardShow(this.shouldEnablePageBackGuard, { suppressLeave: true });
});
return;
}
if (this.registList && this.registList.is_pay == 1) {
this.setPageBackGuardShow(false, { suppressLeave: true });
setTimeout(() => {
uni.navigateBack();
}, 16);
return;
}
this.setPageBackGuardShow(false);
this.promptExitBeforeLeave({
onStay: () => {
this.syncPageBackGuard();
},
onExit: () => {
this.safeNavigateBack();
},
});
// #endif
},
disableBackGuardBeforeLeave() {
this.setPageBackGuardShow(false, { suppressLeave: true });
},
promptExitBeforeLeave({ onStay, onExit } = {}) {
uni.showModal({
title: '提示',
content: '当前未挂号成功,是否继续还是退出?',
cancelText: '继续',
confirmText: '退出',
success: (res) => {
if (res.confirm) {
if (onExit) {
onExit();
} else {
this.safeNavigateBack();
}
} else if (onStay) {
onStay();
} else {
this.syncPageBackGuard();
}
},
});
},
stripDoctorPrefix(raw) {
if (raw === null || raw === undefined || raw === '') return '';
return String(raw).replace(/^doctor-/, '').trim();
},
applyDoctorNumericId(id) {
const n = this.stripDoctorPrefix(id);
if (!n) return;
this.currentDoctorId = 'doctor-' + n;
},
/** 同步可用的医生数字 id详情 > rInfo > URL > 当前 currentDoctorId */
getDoctorNumericIdFromLocalSync() {
const rl = this.registList;
if (rl && typeof rl === 'object' && !Array.isArray(rl) && rl.service_user_id) {
const s = this.stripDoctorPrefix(rl.service_user_id);
if (s) return s;
}
if (this.rInfo) {
const d = this.rInfo.doctor_id != null && this.rInfo.doctor_id !== '' ? this.rInfo.doctor_id : this.rInfo.doctorId;
if (d != null && d !== '') {
const s = this.stripDoctorPrefix(d);
if (s) return s;
}
}
if (this._urlDoctorId) {
const s = this.stripDoctorPrefix(this._urlDoctorId);
if (s) return s;
}
const cur = this.stripDoctorPrefix(this.currentDoctorId);
if (cur) return cur;
return '';
},
async fetchDefaultDoctorIdApi() {
const storeId = uni.getStorageSync('store_id') || '11001';
try {
const res = await request('/xkApi/online-consultation/get-default-doctor-id', {
method: 'GET',
data: { store_id: storeId }
}, 0);
const id = res.data?.result?.doctor_id ?? res.data?.doctor_id;
return id ? this.stripDoctorPrefix(id) : '';
} catch (err) {
console.error('get-default-doctor-id failed', err);
return '';
}
},
async resolveDoctorNumericIdForIm() {
let id = this.getDoctorNumericIdFromLocalSync();
if (!id) id = await this.fetchDefaultDoctorIdApi();
if (id) this.applyDoctorNumericId(id);
return id || '';
},
async ensureDoctorAfterLoad() {
const existing = this.getDoctorNumericIdFromLocalSync();
if (existing) {
this.applyDoctorNumericId(existing);
return;
}
const id = await this.fetchDefaultDoctorIdApi();
if (id) this.applyDoctorNumericId(id);
},
async waitForRegisterDetail() {
if (!this.register_id) return;
try {
if (this._getInfoPromise) await this._getInfoPromise;
else await this.getInfo();
} catch (err) {
console.error('waitForRegisterDetail', err);
}
},
/** 诊所复诊(2)且存在复诊药:支付后先走独立用药确认页,再进聊天 */
needsClinicPostRegisterDrugPage() {
if (!this.registList || this.registList.register_type !== 2) return false;
const { registerInfoId, drugsPayload } = resolveFollowUpDrugsFromRInfo(this.rInfo);
return !!(registerInfoId && drugsPayload.length);
},
// 挂号信息
getInfo() {
const that = this;
if (!this.register_id) return Promise.resolve();
const p = registeInfo({
method: "post",
data: {
store_id: uni.getStorageSync('store_id') || '11001',
register_id: that.register_id
}
}).then((res) => {
if (res.data.errcode == 0 && res.data.data.list && res.data.data.list[0]) {
that.registList = res.data.data.list[0];
that.registerType = that.registerMap[that.registList.register_type];
if (that.registList.service_user_id) {
that.applyDoctorNumericId(that.registList.service_user_id);
}
}
that.syncPageBackGuard();
});
this._getInfoPromise = p;
return p;
},
// 处理在线复诊支付后的流程
async handleOnlineConsultationAfterPayment() {
const that = this;
try {
const doctorId = await that.resolveDoctorNumericIdForIm();
if (!doctorId) {
uni.hideLoading();
uni.showToast({
title: '无法获取医生信息,请稍后重试',
icon: 'none'
});
return;
}
const doctorPrefixed = 'doctor-' + doctorId;
that.currentDoctorId = doctorPrefixed;
const patientId = that.currentUserId.replace('user-', '');
// 3. 创建或获取聊天房间
// type=2 表示诊所在线复诊type=3 表示药店在线复诊/购药
const roomType = (that.registList && that.registList.register_type === 2) ? 2 : 3;
const roomRes = await request('/xkApi/online-consultation/get-or-create-chat-room', {
method: 'POST',
data: {
doctor_id: doctorId,
patient_id: patientId,
type: roomType
}
}, 1);
const roomId = roomRes.data?.result?.room_id || roomRes.data?.room_id;
if (!roomId) {
uni.hideLoading();
uni.showToast({
title: '创建聊天房间失败',
icon: 'none'
});
return;
}
// 4. 仅药店模式(store_type='1')发送患者就诊经历卡片
const storeType = uni.getStorageSync('store_type')
if (storeType === '1' || storeType === 1) {
const patientExperienceData = {
symptom_description: that.rInfo.illness_info || '',
has_visited: that.rInfo.has_visited || '0',
has_used_drug: that.rInfo.has_used_drug || '0',
drug_name: that.rInfo.drug_name || '',
illness_info: that.rInfo.illness_info || '',
doctor_id: doctorId,
user_id: patientId,
register_id: that.register_id,
created_at: new Date().toLocaleString('zh-CN')
};
// 5. 发送患者就诊经历消息消息类型11
await sendToUserApi({
room_id: roomId,
sender_user_id: `user-${patientId}`,
receiver_user_id: `doctor-${doctorId}`,
message_type: 11, // 患者就诊经历卡片
message_content: JSON.stringify(patientExperienceData),
duration: 0
});
} else {
// 诊所模式(store_type='0')发送挂号信息卡片message_type=10
await sendToUserApi({
room_id: roomId,
sender_user_id: `user-${patientId}`,
receiver_user_id: `doctor-${doctorId}`,
message_type: 10, // 挂号信息卡片
message_content: JSON.stringify({
id: that.register_id,
}),
duration: 0
});
}
// 6. 生成订单(如果需要);诊所复诊有药时先 redirect 用药确认页,助理卡改在页内提交后发
const chatUrl = `/subPackages/chat/chat?user_id=${encodeURIComponent(doctorPrefixed)}&room_id=${roomId}&register_id=${that.register_id}`;
const goAfterGenOrder = () => {
if (that.needsClinicPostRegisterDrugPage()) {
const { registerInfoId, drugsPayload } = resolveFollowUpDrugsFromRInfo(that.rInfo);
uni.setStorageSync(
CLINIC_POST_REGISTER_STORAGE_KEY,
JSON.stringify({
register_info_id: registerInfoId,
register_id: that.register_id,
drugs: drugsPayload,
question: '您是否曾使用过此类药品?',
room_id: roomId,
doctor_id: doctorId,
patient_id: patientId,
doctor_prefixed: doctorPrefixed,
}),
);
uni.hideLoading();
that.disableBackGuardBeforeLeave();
uni.redirectTo({
url: '/subPackages/follow-up/medication-info?mode=clinic_post_register',
});
return;
}
uni.hideLoading();
that.disableBackGuardBeforeLeave();
uni.navigateTo({ url: chatUrl });
};
genOrderApi({
register_id: that.register_id,
register_info: typeof that.rInfo === 'string' ? that.rInfo : JSON.stringify(that.rInfo),
}).then(() => {
goAfterGenOrder();
}).catch((error) => {
console.error('生成订单失败:', error);
goAfterGenOrder();
});
} catch (error) {
uni.hideLoading();
console.error('处理在线复诊流程失败:', error);
uni.showToast({
title: '处理失败,请重试',
icon: 'none'
});
}
},
// 确认挂号
next() {
let that = this
if (this.flag) {
this.flag = false
// 获取支付配置
registePay({
method: "post",
data: {
register_id: this.register_id,
store_id: uni.getStorageSync('store_id') || '11001',
}
}).then(async (res) => {
uni.showLoading({
title: "加载中"
});
if (checkDev('open-im') && that.registList && that.registList.register_type === 1) {
createRoomByDoctorIdApi({
doctor_id: that.currentDoctorId,
type: 1
}).then(result => {
sendToUserApi({
room_id: result.data.result.room_id,
sender_user_id: `user-${that.currentUserId}`,
receiver_user_id: `doctor-${that.currentDoctorId}`,
message_type: 10,
message_content: JSON.stringify({
id: that.register_id,
}),
duration: 0
})
})
}
if (res.data.data.is_paid == 1) {
await that.waitForRegisterDetail();
if (that.registList && (that.registList.register_type === 2 || that.registList.register_type === 3)) {
await that.handleOnlineConsultationAfterPayment();
return;
}
setTimeout(() => {
that.disableBackGuardBeforeLeave();
uni.navigateTo({
url: `/subPackages/register/register-success?id=${that.register_id}&register_type=${that.registList.register_type}`,
})
uni.hideLoading();
}, 800)
return
}
uni.requestPayment({
provider: res.data.data.appId,
timeStamp: res.data.data.timestamp,
nonceStr: res.data.data.nonceStr,
package: res.data.data.package,
signType: res.data.data.signType,
paySign: res.data.data.paySign,
success: async () => {
await that.waitForRegisterDetail();
that.flag = false;
if (that.registList && (that.registList.register_type === 2 || that.registList.register_type === 3)) {
uni.showLoading({ title: '加载中' });
await that.handleOnlineConsultationAfterPayment();
return;
}
setTimeout(() => {
that.disableBackGuardBeforeLeave();
uni.navigateTo({
url: '/subPackages/register/register-success?id=' + that.register_id
})
uni.hideLoading();
}, 300)
},
fail: function(err) {
that.flag = false
uni.hideLoading();
setTimeout(() => {
that.$refs.uToast.show({
title: (err && err.errMsg) || '支付失败',
type: 'default',
icon: false
})
}, 300)
}
})
}).catch((err) => {
console.error('registePay', err);
that.flag = true;
uni.hideLoading();
})
}
},
toHome() {
uni.switchTab({
url: '/pages/home/home'
})
},
// 预约挂号须知
toAbout() {
service({
method: "get",
data: {
store_id: uni.getStorageSync('store_id') || '11001',
type: 3
}
}).then((res) => {
// console.log(res, 'gh');
if (res.data.errcode == 0) {
this.content = res.data.data.content
}
})
}
},
mounted() {
this.toAbout()
}
}
/**
* 兼容壳:旧链接 /subPackages/register/register?id=register_id 重定向到合并后的挂号流程 Step 2
*/
export default {
onLoad(e) {
const registerId = e.id || e.register_id || '';
const doctorId = e.doctor_id || '';
let rInfoQuery = '';
if (e.r_info) {
const str = typeof e.r_info === 'string' ? e.r_info : JSON.stringify(e.r_info);
rInfoQuery = `&r_info=${encodeURIComponent(str)}`;
}
const doctorQuery = doctorId ? `&doctor_id=${doctorId}` : '';
uni.redirectTo({
url: `/subPackages/doctor/doctor-userinfo?step=2&register_id=${registerId}${doctorQuery}${rInfoQuery}`,
fail: () => {
uni.showToast({ title: '页面跳转失败', icon: 'none' });
},
});
},
};
</script>
<style lang="scss" scoped>
.container {
width: 750rpx;
max-height: 100%;
min-height: 100vh;
padding-top: 32rpx;
background: #F8FAFC;
.title {
width: 750rpx;
height: 80rpx;
margin: 0 auto;
font-size: 32rpx;
font-family: PingFang SC-Bold, PingFang SC;
font-weight: bold;
color: #666666;
line-height: 80rpx;
padding-left: 32rpx;
}
.card {
width: 750rpx;
background: #FFFFFF;
border-radius: 8rpx;
padding: 32rpx;
margin: 24rpx auto 32rpx;
.item:first-child {
margin: 0 0 48rpx;
}
.item {
display: flex;
justify-content: space-between;
height: 40rpx;
font-family: PingFang SC-Regular, PingFang SC;
line-height: 33rpx;
margin: 48rpx 0 0;
.aapoint {
font-size: 28rpx;
font-weight: 400;
color: #666666;
}
.detail {
font-size: 28rpx;
font-weight: bold;
color: #232323;
}
.active {
color: #F44336;
}
}
}
.info {
width: 750rpx;
background: #FFFFFF;
padding: 32rpx;
border-radius: 8rpx;
margin: 10rpx auto 40rpx;
.name_edit:first-child {
margin-top: 0;
}
.name_edit {
margin-top: 48rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
line-height: 33rpx;
display: flex;
justify-content: space-between;
.name {
color: #475569;
}
.word_num {
font-size: 30rpx;
font-weight: 600;
color: #1E293B;
}
}
}
.footer {
width: 750rpx;
display: flex;
flex-direction: column;
padding: 0 32rpx;
position: fixed;
left: 0;
bottom: env(safe-area-inset-bottom);
z-index: 10;
.allknow {
display: flex;
align-items: center;
width: 710rpx;
height: 40rpx;
padding: 0 32rpx;
margin: 10rpx auto;
.isknow {
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #666666;
margin-left: 8rpx;
text {
color: #4175EE;
}
}
}
.btn {
text-align: center;
width: 686rpx;
height: 86rpx;
line-height: 86rpx;
font-size: 32rpx;
font-family: PingFang SC-Medium, PingFang SC;
font-weight: 500;
color: #FFFFFF;
background: #4175EE;
border-radius: 64rpx;
margin: 20rpx auto;
}
.bg {
width: 688rpx;
height: 88rpx;
background: #8ABAFF;
line-height: 88rpx;
font-size: 32rpx;
text-align: center;
font-family: PingFang SC-Medium, PingFang SC;
font-weight: 400;
color: #FFFFFF;
border-radius: 64rpx;
}
.gh_ {
margin-bottom: 30rpx;
font-size: 28rpx;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #333333;
}
}
.agreen {
padding: 60rpx 50rpx 20rpx;
}
.confirm-modal {
padding: 40rpx 30rpx;
background: #fff;
.confirm-title {
font-size: 34rpx;
font-weight: bold;
color: #333;
text-align: center;
margin-bottom: 30rpx;
}
.confirm-desc {
font-size: 28rpx;
color: #666;
line-height: 1.6;
margin-bottom: 40rpx;
text-align: center;
.link {
color: #2979FF;
}
}
.confirm-footer {
display: flex;
justify-content: space-between;
gap: 20rpx;
button {
flex: 1;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
font-size: 30rpx;
border: none;
&::after {
border: none;
}
}
.cancel-btn {
background: #F5F7FA;
color: #909399;
}
.agree-btn {
background: #4175EE;
color: #FFFFFF;
}
}
}
}
</style>
.register-redirect {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #F8FAFC;
}
</style>

View File

@@ -0,0 +1,540 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<view v-if="loading" class="loading-wrap">
<u-loading mode="circle" size="48"></u-loading>
</view>
<block v-else-if="detail.id">
<!-- 顶部轮播 -->
<view class="swiper-section">
<swiper
class="swiper"
indicator-dots
autoplay
circular
indicator-color="rgba(0,0,0,0.1)"
indicator-active-color="#4175EE"
>
<swiper-item v-for="(img, index) in displayImages" :key="index">
<image
class="swiper-image"
:src="img"
mode="aspectFill"
@click="previewImage(index)"
/>
</swiper-item>
</swiper>
</view>
<!-- 基本信息 -->
<view class="basic-info">
<view class="price-row">
<view class="price-left" v-if="detail.price_available !== false">
<text class="symbol">¥</text>
<text class="amount">{{ detail.price_per_dose }}</text>
<text class="unit">/</text>
</view>
<view class="price-left price-left--unavailable" v-else>
<text class="amount">暂无报价</text>
</view>
<text class="sales" v-if="detail.sales_count">已售 {{ detail.sales_count }}</text>
</view>
<view class="title">{{ detail.name }}</view>
<view class="tag-row" v-if="detail.tags && detail.tags.length">
<text class="tag" v-for="(tag, idx) in detail.tags" :key="idx">{{ tag }}</text>
</view>
</view>
<!-- 图文介绍 -->
<view class="intro-section" v-if="detail.intro_text || (detail.introduction_images && detail.introduction_images.length)">
<view class="section-title">详情介绍</view>
<view class="intro-text" v-if="detail.intro_text">
<u-parse :html="detail.intro_text"></u-parse>
</view>
<view class="intro-images" v-if="detail.introduction_images && detail.introduction_images.length">
<image
v-for="(img, idx) in detail.introduction_images"
:key="idx"
:src="img"
mode="widthFix"
class="intro-img"
@click="previewIntroImage(idx)"
/>
</view>
</view>
<!-- 底部操作 -->
<view class="footer-spacer"></view>
<view class="footer safe-area-inset-bottom">
<button
class="register-btn"
:class="{ 'register-btn--disabled': detail.price_available === false }"
:disabled="detail.price_available === false"
@click="goRegister"
>去挂号选方</button>
</view>
</block>
<view v-else class="empty-wrap">
<u-empty text="特色方不存在或已下架" mode="page" margin-top="120"></u-empty>
</view>
<!-- #ifdef MP-WEIXIN -->
<page-container :show="registerDrawerShow" :overlay="false" @leave="onPageBackGuardLeave" />
<!-- #endif -->
<u-popup
v-model="registerDrawerShow"
mode="bottom"
border-radius="24"
safe-area-inset-bottom
@close="onRegisterDrawerClose"
>
<view class="register-drawer">
<view class="drawer-header">
<text class="drawer-title">确认贴数</text>
<view class="drawer-close" @tap="closeRegisterDrawer">
<u-icon name="close" size="32" color="#999"></u-icon>
</view>
</view>
<view class="drawer-dose-row">
<text class="dose-label">贴数</text>
<u-number-box
v-model="doseCount"
:min="1"
:max="99"
integer
></u-number-box>
</view>
<view class="drawer-price-row">
<text class="price-label">单价</text>
<text class="price-value">¥{{ detail.price_per_dose }}/</text>
</view>
<view class="drawer-total-row">
<text class="total-label">预估金额</text>
<text class="total-price">¥{{ totalPrice }}</text>
</view>
<button class="confirm-btn" @click="confirmRegister">确认并挂号</button>
</view>
</u-popup>
<u-toast ref="uToast" />
</view>
</template>
<script>
import {
getSpecialPrescriptionDetailApi,
saveSpecialPrescriptionRegisterContext,
} from '@/request/api/specialPrescription'
export default {
data() {
return {
id: '',
detail: {},
doseCount: 1,
loading: true,
registerDrawerShow: false,
defaultCover: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/avatar_1.png',
}
},
computed: {
displayImages() {
const images = []
if (this.detail.cover_image) {
images.push(this.detail.cover_image)
}
if (this.detail.introduction_images && this.detail.introduction_images.length) {
this.detail.introduction_images.forEach((img) => {
if (img && images.indexOf(img) === -1) {
images.push(img)
}
})
}
return images.length ? images : [this.defaultCover]
},
totalPrice() {
if (this.detail.price_available === false) {
return '0.00'
}
const price = parseFloat(this.detail.price_per_dose) || 0
return (price * this.doseCount).toFixed(2)
},
},
onLoad(e) {
this.id = e.id || ''
if (this.id) {
this.fetchDetail()
} else {
this.loading = false
}
},
methods: {
getStoreId() {
return uni.getStorageSync('store_id') || '11001'
},
async fetchDetail() {
this.loading = true
try {
const res = await getSpecialPrescriptionDetailApi({
id: this.id,
store_id: this.getStoreId(),
})
if (res.data?.code === 0 || res.data?.code === '0') {
this.detail = res.data.result || {}
} else {
this.$refs.uToast && this.$refs.uToast.show({
title: res.data?.message || '加载失败',
type: 'default',
icon: false,
})
}
} catch (e) {
console.error('fetchDetail', e)
} finally {
this.loading = false
}
},
previewImage(index) {
uni.previewImage({
urls: this.displayImages,
current: this.displayImages[index],
})
},
previewIntroImage(index) {
const urls = this.detail.introduction_images || []
if (!urls.length) return
uni.previewImage({
urls,
current: urls[index],
})
},
goRegister() {
if (this.detail.price_available === false) {
this.$refs.uToast.show({
title: '当前诊所暂无法报价,请联系诊所',
type: 'default',
icon: false,
})
return
}
const doctorId = this.detail.doctor_id
if (!doctorId) {
this.$refs.uToast.show({
title: '诊所未配置特色方挂号医生',
type: 'default',
icon: false,
})
return
}
this.registerDrawerShow = true
},
closeRegisterDrawer() {
this.registerDrawerShow = false
},
onRegisterDrawerClose() {
this.registerDrawerShow = false
},
onPageBackGuardLeave() {
if (this._suppressPageBackGuardLeave) {
this._suppressPageBackGuardLeave = false
return
}
if (this.registerDrawerShow) {
this._suppressPageBackGuardLeave = true
this.registerDrawerShow = false
}
},
confirmRegister() {
const doctorId = this.detail.doctor_id
if (!doctorId) {
this.$refs.uToast.show({
title: '诊所未配置特色方挂号医生',
type: 'default',
icon: false,
})
return
}
const ctx = {
special_prescription_id: this.detail.id,
dose_count: this.doseCount,
doctor_id: doctorId,
}
saveSpecialPrescriptionRegisterContext(ctx)
this._suppressPageBackGuardLeave = true
this.registerDrawerShow = false
uni.navigateTo({
url: `/subPackages/doctor/doctor-userinfo?id=${doctorId}&r_type=0&special_prescription_id=${ctx.special_prescription_id}&special_prescription_dose_count=${ctx.dose_count}`,
})
},
},
}
</script>
<style lang="scss" scoped>
$sp-primary: #4175EE;
$sp-primary-light: rgba(65, 117, 238, 0.1);
$sp-gradient: linear-gradient(90deg, #4175EE 0%, #298DFF 100%);
.container {
min-height: 100vh;
background: #F5F7FA;
padding-bottom: 140rpx;
}
.loading-wrap,
.empty-wrap {
display: flex;
align-items: center;
justify-content: center;
min-height: 60vh;
}
.swiper-section {
.swiper {
height: 750rpx;
width: 100%;
}
.swiper-image {
width: 100%;
height: 100%;
}
}
.basic-info {
background: #fff;
padding: 32rpx;
margin-bottom: 16rpx;
.price-row {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 16rpx;
}
.price-left {
color: #FF5722;
.symbol {
font-size: 28rpx;
font-weight: 600;
}
.amount {
font-size: 48rpx;
font-weight: 800;
}
.unit {
font-size: 24rpx;
color: #999;
margin-left: 4rpx;
}
&--unavailable .amount {
font-size: 32rpx;
color: #999;
font-weight: 500;
}
}
.sales {
font-size: 24rpx;
color: #999;
}
.title {
font-size: 36rpx;
font-weight: 700;
color: #333;
line-height: 1.4;
margin-bottom: 16rpx;
}
.tag-row {
display: flex;
flex-wrap: wrap;
.tag {
font-size: 22rpx;
color: $sp-primary;
background: $sp-primary-light;
padding: 4rpx 16rpx;
border-radius: 8rpx;
margin-right: 12rpx;
margin-bottom: 8rpx;
}
}
}
.intro-section {
background: #fff;
padding: 32rpx;
margin-bottom: 16rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 700;
color: #333;
margin-bottom: 24rpx;
padding-left: 16rpx;
border-left: 6rpx solid $sp-primary;
}
.intro-images {
font-size: 0;
line-height: 0;
.intro-img {
width: 100%;
display: block;
margin-bottom: 0;
vertical-align: top;
}
}
.intro-text {
margin-bottom: 24rpx;
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
.footer-spacer {
height: 120rpx;
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
padding: 16rpx 32rpx;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
z-index: 100;
.register-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: $sp-gradient;
color: #fff;
font-size: 32rpx;
font-weight: 600;
border-radius: 44rpx;
border: none;
&::after {
border: none;
}
&--disabled {
background: #e0e0e0;
color: #999;
}
}
}
.register-drawer {
padding: 32rpx;
background: #fff;
.drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 32rpx;
}
.drawer-title {
font-size: 32rpx;
font-weight: 700;
color: #333;
}
.drawer-close {
padding: 8rpx;
}
.drawer-dose-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
.dose-label {
font-size: 28rpx;
color: #666;
}
}
.drawer-price-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
.price-label {
font-size: 28rpx;
color: #666;
}
.price-value {
font-size: 28rpx;
color: #333;
}
}
.drawer-total-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0 32rpx;
border-top: 1rpx solid #f0f0f0;
margin-bottom: 16rpx;
.total-label {
font-size: 28rpx;
color: #666;
}
.total-price {
font-size: 40rpx;
font-weight: 800;
color: #FF5722;
}
}
.confirm-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: $sp-gradient;
color: #fff;
font-size: 32rpx;
font-weight: 600;
border-radius: 44rpx;
border: none;
&::after {
border: none;
}
}
}
</style>

View File

@@ -0,0 +1,759 @@
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<view class="list-fixed-header">
<!-- simple简洁横幅 -->
<view v-if="hasBanner && headerStyle === 'simple'" class="list-hero">
<view class="list-hero__inner">
<view class="list-hero__title-row">
<image
v-if="pageConfig.title_image"
:src="pageConfig.title_image"
class="list-hero__title"
mode="heightFix"
/>
<image
v-if="pageConfig.mascot_image"
:src="pageConfig.mascot_image"
class="list-hero__mascot"
mode="heightFix"
/>
</view>
<view v-if="pageConfig.subtitle" class="list-hero__subtitle">{{ pageConfig.subtitle }}</view>
</view>
</view>
<!-- card首页异形卡片仅头部无商品横滑 -->
<view v-else-if="hasBanner && headerStyle === 'card'" class="tea-section">
<view class="irregular-card">
<view class="card-tab">
<view class="card-tab-slope"></view>
</view>
<image
v-if="pageConfig.title_image"
:src="pageConfig.title_image"
class="main-title"
mode="widthFix"
/>
<view class="card-body">
<image
v-if="pageConfig.mascot_image"
:src="pageConfig.mascot_image"
class="main-mascot"
mode="widthFix"
/>
<view v-if="pageConfig.subtitle" class="tea-subtitle">
<view class="subtitle-icon"></view>
<text>{{ pageConfig.subtitle }}</text>
</view>
</view>
</view>
</view>
<!-- 顶部搜索 -->
<view class="header-section">
<view class="search-box">
<u-search
v-model="searchKeyword"
placeholder="搜索药方名称"
:show-action="true"
action-text="搜索"
bg-color="#F5F7FA"
placeholder-color="#9CA3AF"
shape="round"
height="64"
:input-style="{ fontSize: '26rpx' }"
@custom="handleSearch"
@search="handleSearch"
@clear="clearSearch"
></u-search>
</view>
</view>
<!-- tabs 布局顶部横向分类 -->
<view v-if="layout === 'tabs'" class="category-tabs-wrap">
<scroll-view scroll-x class="category-tabs-scroll" :show-scrollbar="false">
<view class="category-tabs">
<view
class="category-tab"
:class="{ active: selectedCategoryId === 0 }"
@tap="selectCategory(0)"
>
<text>全部</text>
</view>
<view
v-for="cat in categories"
:key="cat.id"
class="category-tab"
:class="{ active: selectedCategoryId === cat.id }"
@tap="selectCategory(cat.id)"
>
<text class="u-line-1">{{ cat.name }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
<!-- 主体左右独立滚动 -->
<view class="list-body">
<view v-if="layout === 'sidebar'" class="category-sidebar">
<scroll-view
scroll-y
class="category-scroll"
:show-scrollbar="false"
enable-flex
>
<view
class="category-item"
:class="{ active: selectedCategoryId === 0 }"
@tap="selectCategory(0)"
>
<text>全部</text>
</view>
<view
v-for="cat in categories"
:key="cat.id"
class="category-item"
:class="{ active: selectedCategoryId === cat.id }"
@tap="selectCategory(cat.id)"
>
<text class="u-line-1">{{ cat.name }}</text>
</view>
</scroll-view>
</view>
<view class="list-area" :class="{ 'list-area--full': layout === 'tabs' }">
<scroll-view
scroll-y
class="list-scroll"
:show-scrollbar="false"
enable-flex
refresher-enabled
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresherRefresh"
@scrolltolower="loadMore"
:lower-threshold="80"
>
<view class="item-grid" v-if="items.length > 0">
<view
class="item-card"
v-for="item in items"
:key="item.id"
@tap="goDetail(item)"
>
<view class="item-img-box">
<image
:src="item.cover_image || defaultCover"
class="item-img"
mode="aspectFill"
lazy-load
/>
</view>
<view class="item-info">
<view class="item-name u-line-2">{{ item.name }}</view>
<view class="item-tags" v-if="item.tags && item.tags.length">
<text class="item-tag" v-for="(tag, idx) in item.tags.slice(0, 2)" :key="idx">{{ tag }}</text>
</view>
<view class="item-bottom">
<view class="item-price" v-if="item.price_available !== false">
<text class="price-symbol">¥</text>
<text>{{ item.price_per_dose }}</text>
<text class="price-unit">/</text>
</view>
<text class="price-unavailable" v-else>暂无报价</text>
<text class="sales-text" v-if="item.sales_count">已售{{ item.sales_count }}</text>
</view>
</view>
</view>
</view>
<view v-if="items.length === 0 && !loading" class="empty-state">
<image :src="emptyImage" mode="aspectFit" class="empty-image"></image>
<text class="empty-text">暂无特色方</text>
</view>
<view v-if="loading" class="loading-tip">
<u-loading mode="circle" size="36"></u-loading>
<text>加载中...</text>
</view>
<view v-else-if="finished && items.length > 0" class="finished-tip">没有更多了</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script>
import {
getSpecialPrescriptionCategoriesApi,
getSpecialPrescriptionListApi,
getSpecialPrescriptionListPageApi,
} from '@/request/api/specialPrescription'
export default {
data() {
return {
layout: 'sidebar',
headerStyle: 'simple',
pageConfig: {
title_image: '',
mascot_image: '',
subtitle: '',
empty_image: '',
},
defaultEmptyImage: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20260701/eca70782-cdb5-4def-8eb0-542b147240a6_1782890269.png',
categories: [],
selectedCategoryId: 0,
searchKeyword: '',
items: [],
page: 1,
pageSize: 20,
pageCount: 1,
loading: false,
finished: false,
isRefreshing: false,
defaultCover: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/avatar_1.png',
}
},
computed: {
hasBanner() {
const cfg = this.pageConfig || {}
return !!(cfg.title_image || cfg.mascot_image || cfg.subtitle)
},
emptyImage() {
return this.pageConfig.empty_image || this.defaultEmptyImage
},
},
onLoad() {
this.fetchPageConfig()
this.fetchCategories()
this.fetchList(true)
},
methods: {
getStoreId() {
return uni.getStorageSync('store_id') || '11001'
},
async fetchPageConfig() {
try {
const res = await getSpecialPrescriptionListPageApi({ store_id: this.getStoreId() })
if (res.data?.code === 0 || res.data?.code === '0') {
const cfg = res.data.result || {}
this.layout = cfg.layout === 'tabs' ? 'tabs' : 'sidebar'
this.headerStyle = cfg.header_style === 'card' ? 'card' : 'simple'
this.pageConfig = {
title_image: cfg.title_image || '',
mascot_image: cfg.mascot_image || '',
subtitle: cfg.subtitle || '',
empty_image: cfg.empty_image || '',
}
}
} catch (e) {
console.error('fetchPageConfig', e)
}
},
async fetchCategories() {
try {
const res = await getSpecialPrescriptionCategoriesApi({ store_id: this.getStoreId() })
if (res.data?.code === 0 || res.data?.code === '0') {
this.categories = res.data.result || []
}
} catch (e) {
console.error('fetchCategories', e)
}
},
selectCategory(categoryId) {
if (this.selectedCategoryId === categoryId) return
this.selectedCategoryId = categoryId
this.fetchList(true)
},
handleSearch() {
this.fetchList(true)
},
clearSearch() {
this.searchKeyword = ''
this.fetchList(true)
},
async fetchList(reset = false, options = {}) {
const { skipLoadingCheck = false } = options
if (!skipLoadingCheck && this.loading) return
if (reset) {
this.page = 1
this.finished = false
this.items = []
}
if (this.finished && !reset) return
this.loading = true
try {
const params = {
store_id: this.getStoreId(),
page: this.page,
pageSize: this.pageSize,
}
if (this.selectedCategoryId > 0) {
params.category_id = this.selectedCategoryId
}
const keyword = (this.searchKeyword || '').trim()
if (keyword) {
params.name = keyword
}
const res = await getSpecialPrescriptionListApi(params)
if (res.data?.code === 0 || res.data?.code === '0') {
const result = res.data.result || {}
const list = result.items || []
if (result.layout === 'tabs' || result.layout === 'sidebar') {
this.layout = result.layout
}
if (result.header_style === 'card' || result.header_style === 'simple') {
this.headerStyle = result.header_style
}
this.items = reset ? list : this.items.concat(list)
this.pageCount = result.page_count || 1
this.finished = this.page >= this.pageCount || list.length === 0
if (!this.finished) {
this.page += 1
}
} else {
this.finished = true
}
} catch (e) {
console.error('fetchList', e)
this.finished = true
} finally {
this.loading = false
}
},
loadMore() {
this.fetchList(false)
},
async onRefresherRefresh() {
if (this.isRefreshing) return
this.isRefreshing = true
try {
await Promise.all([
this.fetchPageConfig(),
this.fetchCategories(),
this.fetchList(true, { skipLoadingCheck: true }),
])
} finally {
this.isRefreshing = false
}
},
goDetail(item) {
const id = Number(item?.id)
if (!id) return
uni.navigateTo({
url: `/subPackages/special-prescription/detail?id=${id}`,
})
},
},
}
</script>
<style lang="scss" scoped>
$sp-primary: #4175EE;
$sp-primary-light: rgba(65, 117, 238, 0.1);
$sp-primary-shadow: rgba(65, 117, 238, 0.16);
.container {
height: 100vh;
background: #F5F7FA;
display: flex;
flex-direction: column;
overflow: hidden;
}
.list-fixed-header {
flex-shrink: 0;
}
.list-body {
flex: 1;
min-height: 0;
display: flex;
overflow: hidden;
}
/* --- simple 简洁横幅 --- */
.list-hero {
background: #fff;
padding: 20rpx 24rpx 8rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.list-hero__inner {
position: relative;
}
.list-hero__title-row {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: 72rpx;
padding-right: 100rpx;
box-sizing: border-box;
}
.list-hero__title {
height: 56rpx;
display: block;
}
.list-hero__mascot {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
height: 72rpx;
pointer-events: none;
}
.list-hero__subtitle {
margin-top: 8rpx;
text-align: center;
font-size: 24rpx;
color: #666;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* --- card 异形卡片(复用首页结构) --- */
.tea-section {
padding: 87rpx 24rpx 0;
.irregular-card {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.card-tab {
width: 220rpx;
height: 40rpx;
background: #FFFFFF;
border-radius: 24rpx 24rpx 0 0;
position: relative;
z-index: 2;
margin-bottom: -1rpx;
}
.card-tab-slope {
position: absolute;
right: -40rpx;
bottom: 0;
width: 40rpx;
height: 40rpx;
background-image: radial-gradient(circle at 100% 0%, transparent 40rpx, #FFFFFF 40rpx);
}
.main-title {
position: absolute;
left: 8rpx;
top: -120rpx;
width: 210rpx;
height: 128rpx;
z-index: 6;
pointer-events: none;
}
.card-body {
background: #FFFFFF;
width: 100%;
padding: 24rpx;
border-radius: 0 24rpx 24rpx 24rpx;
position: relative;
z-index: 1;
box-sizing: border-box;
box-shadow: 0 12rpx 32rpx $sp-primary-shadow;
}
.main-mascot {
position: absolute;
right: 12rpx;
top: -140rpx;
width: 217rpx;
height: 168rpx;
z-index: 10;
pointer-events: none;
}
.tea-subtitle {
font-size: 28rpx;
color: #333;
font-weight: bold;
padding-right: 150rpx;
display: flex;
align-items: center;
.subtitle-icon {
width: 6rpx;
height: 26rpx;
background: $sp-primary;
border-radius: 3rpx;
margin-right: 12rpx;
}
}
}
.header-section {
background: #fff;
padding: 16rpx 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
.search-box {
width: 100%;
}
}
.category-tabs-wrap {
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.category-tabs-scroll {
white-space: nowrap;
width: 100%;
}
.category-tabs {
display: inline-flex;
padding: 0 16rpx;
}
.category-tab {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 20rpx 28rpx;
font-size: 26rpx;
color: #666;
position: relative;
flex-shrink: 0;
text {
max-width: 200rpx;
}
&.active {
color: $sp-primary;
font-weight: bold;
&::after {
content: '';
position: absolute;
left: 50%;
bottom: 8rpx;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background: $sp-primary;
border-radius: 2rpx;
}
}
}
.category-sidebar {
width: 180rpx;
height: 100%;
background: #fff;
flex-shrink: 0;
overflow: hidden;
.category-scroll {
height: 100%;
}
.category-item {
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
color: #666;
position: relative;
padding: 0 12rpx;
text {
text-align: center;
max-width: 100%;
}
&.active {
background: #F5F7FA;
color: $sp-primary;
font-weight: bold;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 6rpx;
height: 48rpx;
background: $sp-primary;
border-radius: 0 3rpx 3rpx 0;
}
}
}
}
.list-area {
flex: 1;
min-height: 0;
height: 100%;
overflow: hidden;
&--full {
width: 100%;
}
.list-scroll {
height: 100%;
padding: 16rpx;
box-sizing: border-box;
}
}
.item-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.item-card {
width: calc((100% - 16rpx) / 2);
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
&:active {
transform: scale(0.98);
}
}
.item-img-box {
width: 100%;
height: 240rpx;
background: #f8f8f8;
.item-img {
width: 100%;
height: 100%;
}
}
.item-info {
padding: 16rpx;
.item-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
line-height: 1.4;
min-height: 78rpx;
margin-bottom: 8rpx;
}
.item-tags {
display: flex;
flex-wrap: wrap;
margin-bottom: 12rpx;
.item-tag {
font-size: 20rpx;
color: $sp-primary;
background: $sp-primary-light;
padding: 2rpx 10rpx;
border-radius: 6rpx;
margin-right: 8rpx;
margin-bottom: 4rpx;
}
}
.item-bottom {
display: flex;
align-items: baseline;
justify-content: space-between;
}
.item-price {
color: #FF5722;
font-size: 32rpx;
font-weight: 800;
.price-symbol {
font-size: 22rpx;
}
.price-unit {
font-size: 22rpx;
color: #999;
font-weight: 400;
}
}
.price-unavailable {
font-size: 24rpx;
color: #999;
}
.sales-text {
font-size: 20rpx;
color: #999;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 0 40rpx;
.empty-image {
width: 440rpx;
height: 393rpx;
}
.empty-text {
margin-top: 24rpx;
font-size: 26rpx;
color: #999;
}
}
.loading-tip,
.finished-tip {
text-align: center;
padding: 40rpx 0;
color: #999;
font-size: 24rpx;
}
.loading-tip {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
}
.u-line-1 {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.u-line-2 {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
</style>