Files
xk-client-wx/subPackages/special-prescription/detail.vue
2026-07-03 16:48:46 +08:00

907 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="container safe-area-inset-bottom">
<MessageNotification />
<view v-if="loading" class="loading-wrap">
<u-loading mode="circle" size="48" color="#4175EE"></u-loading>
</view>
<block v-else-if="detail.id">
<!-- STREAMING_CHUNK:渲染顶部轮播图 -->
<view class="swiper-section">
<swiper
class="swiper"
indicator-dots
autoplay
circular
indicator-color="rgba(255,255,255,0.6)"
indicator-active-color="#ffffff"
>
<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>
<!-- STREAMING_CHUNK:渲染上浮卡片式基本信息 -->
<view class="info-container-price">
<view class="basic-info">
<!-- 价格区渐变背景装饰 -->
<view class="price-panel">
<view class="price-bg-decor"></view>
<view class="price-content">
<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>
<view class="sales-box" v-if="detail.sales_count">
<u-icon name="shopping-cart" size="24" color="#FF8D1A"></u-icon>
<text class="sales-text">已售 {{ detail.sales_count }}</text>
</view>
</view>
</view>
<!-- 标题与标签区 -->
<view class="title-panel">
<view class="title">
<text class="title-text">{{ detail.name }}</text>
</view>
<view class="tag-row" v-if="detail.tags && detail.tags.length">
<view class="tag-item" v-for="(tag, idx) in detail.tags" :key="idx">
<text class="tag-text">{{ tag }}</text>
</view>
</view>
</view>
</view>
</view>
<view class="info-container">
<!-- STREAMING_CHUNK:渲染图文介绍 -->
<view class="intro-section" v-if="detail.intro_text || (detail.introduction_images && detail.introduction_images.length)">
<view class="section-title">
<view class="title-decor-line"></view>
<text class="title-text">详情介绍</text>
</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>
<!-- STREAMING_CHUNK:渲染适度抬高的悬浮操作按钮 -->
<view class="footer-spacer"></view>
<view class="floating-action-bar safe-area-inset-bottom">
<button
class="register-btn-float"
:class="{ 'register-btn-float--disabled': detail.price_available === false }"
:disabled="detail.price_available === false"
@click="goRegister"
>
<text>去挂号选方</text>
</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 :v-if="registerDrawerShow" :overlay="false" @leave="onPageBackGuardLeave" />
<!-- #endif -->
<!-- STREAMING_CHUNK:渲染包含自定义项的SKU抽屉 -->
<u-popup
v-model="registerDrawerShow"
mode="bottom"
border-radius="32"
safe-area-inset-bottom
closeable
close-icon-color="#999"
@close="onRegisterDrawerClose"
>
<view class="register-drawer">
<view class="drawer-header">
<image class="header-img" :src="displayImages[0]" mode="aspectFill"></image>
<view class="header-info">
<view class="header-price">
<text class="symbol">¥</text>
<text class="amount">{{ unitPrice }}</text>
<text class="unit">/</text>
</view>
<text class="header-stock">
已选{{ selectedSkuId === 0 ? '自定义贴数' : (selectedSku ? selectedSku.sku_name : '') }} {{ doseCount }}
</text>
</view>
</view>
<scroll-view scroll-y class="drawer-scroll-area">
<view class="drawer-section">
<text class="section-label">规格选择</text>
<view class="sku-list">
<!-- STREAMING_CHUNK:自定义贴数 SKU 胶囊 (带动态价格) -->
<view
class="sku-item"
:class="{ 'sku-item--active': Number(selectedSkuId) === 0 }"
@tap="selectCustomSku"
>
<text class="sku-name">自定义贴数</text>
<text class="sku-price" v-if="detail.price_available !== false && customSkuDisplayPrice > 0">
¥{{ customSkuDisplayPrice }}
</text>
<view class="active-triangle" v-if="Number(selectedSkuId) === 0">
<u-icon name="checkbox-mark" size="16" color="#fff" class="triangle-icon"></u-icon>
</view>
</view>
<!-- 真实的 SKU 列表 -->
<view
v-for="sku in detail.skus"
:key="sku.id"
class="sku-item"
:class="{ 'sku-item--active': Number(selectedSkuId) === Number(sku.id) }"
@tap="selectSku(sku)"
>
<text class="sku-name">{{ sku.sku_name }}</text>
<text class="sku-price" v-if="sku.sale_price > 0">¥{{ sku.sale_price }}</text>
<view class="active-triangle" v-if="Number(selectedSkuId) === Number(sku.id)">
<u-icon name="checkbox-mark" size="16" color="#fff" class="triangle-icon"></u-icon>
</view>
</view>
</view>
</view>
<!-- 步进器部分 -->
<view class="drawer-section dose-section">
<text class="section-label">贴数</text>
<u-number-box
v-model="doseCount"
:min="1"
:max="99"
integer
:input-width="90"
:input-height="56"
bg-color="#F5F7FA"
@change="onDoseCountChange"
></u-number-box>
</view>
</scroll-view>
<view class="drawer-footer">
<view class="total-info">
<text class="total-label">预估金额</text>
<view class="total-price-box">
<text class="total-symbol">¥</text>
<text class="total-price">{{ totalPrice }}</text>
</view>
</view>
<button class="confirm-btn" @click="confirmRegister">确认挂号</button>
</view>
</view>
</u-popup>
<u-toast ref="uToast" />
</view>
</template>
<script>
/* STREAMING_CHUNK:引入依赖文件 */
import {
getSpecialPrescriptionDetailApi,
saveSpecialPrescriptionRegisterContext,
} from '@/request/api/specialPrescription'
import { calculateSpecialPrescriptionEstimatedTotal } from '@/utils/formatPrice'
export default {
data() {
return {
id: '',
detail: {},
doseCount: 1,
selectedSkuId: 0,
loading: true,
registerDrawerShow: false,
defaultCover: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/avatar_1.png',
}
},
computed: {
/* STREAMING_CHUNK:计算属性 */
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]
},
hasSkus() {
return Array.isArray(this.detail.skus) && this.detail.skus.length > 0
},
selectedSku() {
if (!this.hasSkus || Number(this.selectedSkuId) === 0) return null
return this.detail.skus.find((item) => Number(item.id) === Number(this.selectedSkuId)) || null
},
unitPrice() {
return parseFloat(this.detail.price_per_dose || 0).toFixed(2)
},
// 为自定义贴数单独提供展示价格
customSkuDisplayPrice() {
return parseFloat(this.detail.price_per_dose || 0).toFixed(2)
},
totalPrice() {
if (this.detail.price_available === false) {
return '0.00'
}
if (this.selectedSku && Number(this.selectedSku.sale_price) > 0) {
return parseFloat(this.selectedSku.sale_price || 0).toFixed(2)
}
return calculateSpecialPrescriptionEstimatedTotal(
this.detail.price_per_dose,
this.doseCount,
)
},
},
watch: {
/* STREAMING_CHUNK:监听器处理 */
doseCount(newVal) {
if (this._suppressDoseSkuClear) {
return
}
this.selectedSkuId = 0
},
},
onLoad(e) {
this.id = e.id || ''
if (this.id) {
this.fetchDetail()
} else {
this.loading = false
}
},
methods: {
/* STREAMING_CHUNK:核心方法与生命周期 */
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 || {}
this.initSkuSelection()
} 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
}
},
initSkuSelection() {
const skus = this.detail.skus || []
if (skus.length > 0 && this.detail.default_sku_id) {
const defaultId = Number(this.detail.default_sku_id || 0)
const defaultSku = skus.find((item) => Number(item.is_default) === 1)
|| skus.find((item) => Number(item.id) === defaultId)
if (defaultSku) {
this._suppressDoseSkuClear = true
this.selectedSkuId = Number(defaultSku.id)
this.doseCount = defaultSku.dose_count || 1
this.$nextTick(() => {
this._suppressDoseSkuClear = false
})
return
}
}
this.selectedSkuId = 0
this.doseCount = 1
},
selectCustomSku() {
this.selectedSkuId = 0
},
selectSku(sku) {
this._suppressDoseSkuClear = true
if (Number(this.selectedSkuId) === Number(sku.id)) {
this.selectedSkuId = 0
} else {
this.selectedSkuId = Number(sku.id)
this.doseCount = sku.dose_count || 1
}
this.$nextTick(() => {
this._suppressDoseSkuClear = false
})
},
onDoseCountChange(e) {
if (this._suppressDoseSkuClear) return
this.selectedSkuId = 0
},
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 skuId = this.selectedSku
? Number(this.selectedSku.id)
: 0
const ctx = {
special_prescription_id: this.detail.id,
dose_count: this.doseCount,
sku_id: skuId,
doctor_id: doctorId,
}
saveSpecialPrescriptionRegisterContext(ctx)
this._suppressPageBackGuardLeave = true
this.registerDrawerShow = false
let url = `/subPackages/doctor/doctor-userinfo?id=${doctorId}&r_type=0&special_prescription_id=${ctx.special_prescription_id}&special_prescription_dose_count=${ctx.dose_count}`
if (ctx.sku_id) {
url += `&special_prescription_sku_id=${ctx.sku_id}`
}
uni.navigateTo({ url })
},
},
}
</script>
<style lang="scss" scoped>
/* STREAMING_CHUNK:定义全局样式与主题变量 */
$sp-primary: #4175EE;
$sp-primary-light: #EBF1FF;
$sp-price: #FF4F23;
$sp-price-bg: linear-gradient(135deg, #FFF1EB 0%, #FFFAFA 100%);
$sp-text-main: #222222;
$sp-text-sub: #666666;
$sp-text-muted: #999999;
$sp-bg: #F4F5F7;
$sp-gradient-btn: linear-gradient(90deg, #5C8CFF 0%, #3567E8 100%);
view, text, scroll-view {
box-sizing: border-box;
}
.container {
min-height: 100vh;
background: $sp-bg;
/* 调整底部留白,适应稍微降低的悬浮按钮 */
padding-bottom: calc(150rpx + env(safe-area-inset-bottom));
//padding-bottom: 50rpx;
}
.loading-wrap,
.empty-wrap {
display: flex;
align-items: center;
justify-content: center;
min-height: 70vh;
}
/* 轮播图样式 */
.swiper-section {
.swiper {
height: 750rpx;
width: 100%;
background-color: #f8f8f8;
}
.swiper-image {
width: 100%;
height: 100%;
}
}
/* 详情页面装饰增强 */
.info-container-price {
position: relative;
margin-top: -32rpx;
z-index: 10;
padding: 0 24rpx;
}
/* 详情页面装饰增强 */
.info-container {
position: relative;
margin-top: -32rpx;
z-index: 10;
}
.basic-info {
background: #ffffff;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
margin-bottom: 24rpx;
.price-panel {
position: relative;
padding: 32rpx 24rpx;
background: $sp-price-bg;
.price-bg-decor {
position: absolute;
right: 0;
top: 0;
width: 200rpx;
height: 100%;
background: radial-gradient(circle at 100% 0%, rgba(255, 79, 35, 0.08) 0%, transparent 70%);
pointer-events: none;
}
.price-content {
position: relative;
display: flex;
align-items: flex-end;
justify-content: space-between;
z-index: 2;
}
.price-left {
color: $sp-price;
display: flex;
align-items: baseline;
.symbol {
font-size: 32rpx;
font-weight: 700;
margin-right: 4rpx;
}
.amount {
font-size: 56rpx;
font-weight: 800;
font-family: 'DIN Alternate', sans-serif;
line-height: 1;
}
.unit {
font-size: 24rpx;
margin-left: 8rpx;
}
&--unavailable .amount {
font-size: 36rpx;
color: $sp-text-muted;
}
}
.sales-box {
display: flex;
align-items: center;
background: rgba(255, 141, 26, 0.1);
padding: 6rpx 16rpx;
border-radius: 24rpx;
.sales-text {
font-size: 22rpx;
color: #E67A15;
margin-left: 6rpx;
font-weight: 500;
}
}
}
.title-panel {
padding: 24rpx;
}
.title {
font-size: 34rpx;
font-weight: 700;
color: $sp-text-main;
line-height: 1.5;
margin-bottom: 16rpx;
}
.tag-row {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
.tag-item {
position: relative;
background: #FAFAFA;
border: 1rpx solid #EBEBEB;
padding: 6rpx 16rpx;
border-radius: 8rpx;
.tag-text {
font-size: 22rpx;
color: $sp-text-sub;
}
}
}
}
.intro-section {
background: #ffffff;
border-radius: 24rpx;
padding: 32rpx 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
.section-title {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 32rpx;
position: relative;
.title-decor-line {
position: absolute;
bottom: 4rpx;
width: 120rpx;
height: 12rpx;
background: linear-gradient(90deg, rgba(65, 117, 238, 0.3) 0%, transparent 100%);
border-radius: 6rpx;
z-index: 1;
}
.title-text {
font-size: 32rpx;
font-weight: 700;
color: $sp-text-main;
z-index: 2;
}
}
.intro-images {
font-size: 0;
line-height: 0;
border-radius: 12rpx;
overflow: hidden;
.intro-img {
width: 100%;
display: block;
}
}
.intro-text {
font-size: 28rpx;
color: $sp-text-sub;
line-height: 1.7;
margin-bottom: 20rpx;
}
}
/* STREAMING_CHUNK:底部适度抬高悬浮操作按钮 */
.footer-spacer {
height: 20rpx;
}
.floating-action-bar {
position: fixed;
left: 32rpx;
right: 32rpx;
/* 调整悬浮高度:降低到 16rpx更加稳重 */
//bottom: calc(16rpx + env(safe-area-inset-bottom));
bottom: 50rpx;
z-index: 100;
display: flex;
justify-content: center;
.register-btn-float {
width: 100%;
height: 96rpx;
background: $sp-gradient-btn;
color: #fff;
font-size: 32rpx;
font-weight: 600;
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
border: none;
box-shadow: 0 12rpx 24rpx rgba(65, 117, 238, 0.3);
transition: all 0.3s;
&::after { border: none; }
&:active {
transform: scale(0.98);
box-shadow: 0 6rpx 12rpx rgba(65, 117, 238, 0.2);
}
&--disabled {
background: #E2E4E8;
color: #B0B5C1;
box-shadow: none;
}
}
}
/* 抽屉与包含自定义的精简版SKU样式 */
.register-drawer {
width: 100%;
background: #fff;
border-radius: 32rpx 32rpx 0 0;
display: flex;
flex-direction: column;
max-height: 75vh;
.drawer-header {
width: 100%;
padding: 32rpx;
border-bottom: 1rpx solid #F2F3F5;
display: flex;
position: relative;
padding-right: 80rpx;
.header-img {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
background: $sp-bg;
margin-right: 24rpx;
border: 1rpx solid #EBEBEB;
}
.header-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding-bottom: 12rpx;
.header-price {
color: $sp-price;
display: flex;
align-items: baseline;
margin-bottom: 12rpx;
.symbol { font-size: 28rpx; font-weight: 600; }
.amount { font-size: 48rpx; font-weight: 800; font-family: 'DIN Alternate', sans-serif;}
.unit { font-size: 24rpx; color: $sp-text-muted; margin-left: 4rpx;}
}
.header-stock {
font-size: 24rpx;
color: $sp-text-sub;
background: #F7F7F7;
padding: 4rpx 12rpx;
border-radius: 8rpx;
display: inline-block;
align-self: flex-start;
}
}
}
.drawer-scroll-area {
width: 100%;
max-height: 50vh;
padding: 0 32rpx 40rpx;
}
.drawer-section {
margin-top: 32rpx;
.section-label {
font-size: 28rpx;
font-weight: 600;
color: $sp-text-main;
margin-bottom: 20rpx;
display: block;
}
}
.sku-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.sku-item {
position: relative;
display: inline-flex;
align-items: center;
background: #F5F6F8;
border: 2rpx solid transparent;
padding: 14rpx 28rpx;
border-radius: 40rpx;
transition: all 0.2s;
overflow: hidden;
.sku-name {
font-size: 26rpx;
color: $sp-text-main;
}
.sku-name + .sku-price {
margin-left: 8rpx;
}
.sku-price {
font-size: 24rpx;
color: $sp-text-sub;
font-weight: 500;
}
&--active {
background: $sp-primary-light;
border-color: $sp-primary;
.sku-name {
color: $sp-primary;
font-weight: 600;
}
.sku-price {
color: $sp-primary;
}
}
.active-triangle {
position: absolute;
right: -2rpx;
bottom: -2rpx;
width: 0;
height: 0;
border-bottom: 32rpx solid $sp-primary;
border-left: 32rpx solid transparent;
.triangle-icon {
position: absolute;
right: 2rpx;
bottom: -28rpx;
transform: scale(0.6);
}
}
}
.dose-section {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 16rpx;
.section-label {
margin-bottom: 0;
}
}
.drawer-footer {
width: 100%;
padding: 20rpx 32rpx;
background: #fff;
border-top: 1rpx solid #F2F3F5;
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
.total-info {
display: flex;
flex-direction: column;
.total-label {
font-size: 24rpx;
color: $sp-text-sub;
}
.total-price-box {
display: flex;
align-items: baseline;
color: $sp-price;
}
.total-symbol {
font-size: 28rpx;
font-weight: 600;
}
.total-price {
font-size: 44rpx;
font-weight: 800;
font-family: 'DIN Alternate', sans-serif;
}
}
.confirm-btn {
margin: 0;
width: 280rpx;
height: 80rpx;
line-height: 80rpx;
background: $sp-gradient-btn;
color: #fff;
font-size: 30rpx;
font-weight: 600;
border-radius: 40rpx;
border: none;
&::after { border: none; }
}
}
}
</style>