Files
xk-client-wx/subPackages/product/product.vue
2026-06-01 09:52:26 +08:00

829 lines
22 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">
<MessageNotification />
<!-- 1. 顶部搜索栏 -->
<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>
<!-- 2. 顶部专区Tab -->
<view class="zone-tabs-wrapper">
<scroll-view scroll-x class="zone-tabs-scroll">
<view class="zone-tabs">
<view
v-for="(zone, index) in homeZones"
:key="index"
class="zone-tab-item"
:class="{ active: selectedZoneIndex === index }"
@click="onZoneChange(index)"
>
<text>{{ zone.title }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 3. 主体内容区:左侧分类 + 右侧商品 -->
<view
class="main-content"
@touchstart="onZoneTouchStart"
@touchend="onZoneTouchEnd"
>
<!-- 左侧分类栏 -->
<view class="category-sidebar">
<scroll-view scroll-y class="category-scroll">
<view
class="category-item"
:class="{ active: selectedCategoryId === 0 }"
@click="selectCategory(0)"
>
<text>全部</text>
</view>
<view
v-for="cat in categoryList"
:key="cat.id"
class="category-item"
:class="{ active: selectedCategoryId === cat.id }"
@click="selectCategory(cat.id)"
>
<text class="u-line-1">{{ cat.category_name }}</text>
</view>
</scroll-view>
</view>
<!-- 右侧商品列表区 -->
<view class="product-area">
<scroll-view scroll-y class="product-scroll" @scrolltolower="loadMore">
<!-- 商品列表 - 两列布局 -->
<view class="product-grid" v-if="products.length > 0">
<view
v-for="(item, index) in products"
:key="item.id"
class="product-card"
@click="handClickProduct(item)"
>
<!-- 商品图片 -->
<view class="product-img-box">
<image
class="product-img"
:class="{ 'prescription-blur': item.drug && item.drug.is_otc === 0 && item.drug.type == 2 }"
:src="(item.drug && item.drug.image) ? item.drug.image : 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/avatar_1.png'"
mode="aspectFill"
lazy-load
/>
</view>
<!-- 商品信息 -->
<view class="product-info">
<view class="product-title u-line-2">{{ item.drug ? item.drug.drug_name : '' }}</view>
<view class="product-tags" v-if="item.drug.type == 2">
<text
class="tag-label"
:class="getProductTag(item.drug).className"
>
{{ getProductTag(item.drug).label }}
</text>
<text class="tag-spec u-line-1">{{ (item.drug && item.drug.specification) ? item.drug.specification : '标准规格' }}</text>
</view>
<view class="product-bottom">
<view class="price-box">
<text class="price-symbol">¥</text>
<text class="price-val">{{ item.price }}</text>
</view>
<view class="cart-btn" @click.stop="showCartPopup(item)">
<u-icon name="plus" color="#ffffff" size="26"></u-icon>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="products.length === 0" class="empty-state">
<u-empty text="暂无相关药品" mode="search" margin-top="100"></u-empty>
</view>
</scroll-view>
</view>
</view>
<!-- 购物车悬浮球 -->
<view
class="cart-float"
:style="{ left: cartPosition.x + 'px', top: cartPosition.y + 'px' }"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
@click="goToCart"
>
<view class="cart-icon-inner">
<u-icon name="shopping-cart" color="#ffffff" size="44"></u-icon>
<view class="badge" v-if="cartCount > 0">{{ cartCount > 99 ? '99+' : cartCount }}</view>
</view>
</view>
<!-- 购物车弹窗 -->
<u-popup
v-model="showPopup"
mode="bottom"
border-radius="24"
:closeable="true"
@close="closePopup"
>
<view class="popup-content safe-area-inset-bottom">
<view class="product-header-row">
<image class="popup-img" :src="getProductImage" mode="aspectFill"></image>
<view class="popup-info">
<view class="price-row">¥<text class="big">{{ currentProduct.price || '' }}</text></view>
<view class="stock-row">库存充足</view>
<view class="selected-spec">已选: {{ getProductSpec }}</view>
</view>
</view>
<view class="quantity-section">
<text class="label">购买数量</text>
<u-number-box
v-model="quantity"
:min="1"
:max="999"
integer
@change="quantityChange"
></u-number-box>
</view>
<view class="popup-footer">
<button class="action-btn add-cart" @click="confirmAddToCart">加入购物车</button>
</view>
</view>
</u-popup>
<u-toast ref="uToast" />
</view>
</template>
<script>
import { getStoreDrugListBySalespersonApi, getHomeZonesApi, getDrugCategoriesApi } from "@/request/api/product";
import { getCartCountsApi, saveCartApi } from "@/request/api/cart";
export default {
data() {
return {
products: [],
searchKeyword: "",
// 专区相关
homeZones: [],
selectedZoneIndex: 0,
selectedZoneType: 'otc',
zoneTouch: { startX: 0, startY: 0 },
// 分类相关
categoryList: [],
selectedCategoryId: 0,
doctorId: 0,
r_type: 0,
delegateStoreId: '',
cartCount: 0,
cartPosition: { x: 0, y: 0 },
cartMove: { startX: 0, startY: 0, moving: false },
showPopup: false,
currentProduct: {},
quantity: 1
};
},
computed: {
getProductImage() {
return (this.currentProduct.drug && this.currentProduct.drug.image) ? this.currentProduct.drug.image : 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-client-wx/static/mine/avatar_1.png';
},
getProductName() {
return (this.currentProduct.drug && this.currentProduct.drug.drug_name) ? this.currentProduct.drug.drug_name : '';
},
getProductSpec() {
return (this.currentProduct.drug && this.currentProduct.drug.specification) ? this.currentProduct.drug.specification : '标准规格';
}
},
onLoad(e) {
this.doctorId = e.doctor_id;
this.r_type = e.register_type;
this.delegateStoreId = e.delegate_store_id || '';
// 根据首页传入的专区类型初始化
if (e.zone_type) {
this.selectedZoneType = e.zone_type;
} else {
this.selectedZoneType = 'otc';
}
// 获取专区列表
this.getHomeZones();
},
methods: {
// 获取专区列表
getHomeZones() {
getHomeZonesApi({
store_id: uni.getStorageSync('store_id') || '11001',
}).then((res) => {
if (res.data.code == 0) {
this.homeZones = res.data.result || res.data.data || [];
// 根据传入的zone_type找到对应的索引
if (this.homeZones.length > 0) {
const index = this.homeZones.findIndex(z => z.type === this.selectedZoneType);
this.selectedZoneIndex = index >= 0 ? index : 0;
this.selectedZoneType = this.homeZones[this.selectedZoneIndex]?.type || 'otc';
}
// 获取分类列表
this.getCategoryList();
}
}).catch(() => {
// 使用默认专区
this.homeZones = [
{ id: 1, title: 'OTC', type: 'otc' },
{ id: 2, title: '保健食品', type: 'health_food' }
];
this.getCategoryList();
});
},
// 获取分类列表
getCategoryList() {
getDrugCategoriesApi({
zone_type: this.selectedZoneType
}).then((res) => {
if (res.data.code == 0) {
// 将树形结构扁平化为一级分类列表
const categories = res.data.result || [];
this.categoryList = this.flattenCategories(categories);
}
}).catch(() => {
this.categoryList = [];
}).finally(() => {
// 重置分类选择并获取商品列表
this.selectedCategoryId = 0;
this.getList();
});
},
// 扁平化分类树(只取一级分类,或展开所有层级)
flattenCategories(categories, result = []) {
for (const cat of categories) {
result.push({
id: cat.id,
category_name: cat.category_name,
level: cat.level,
parent_id: cat.parent_id
});
if (cat.children && cat.children.length > 0) {
this.flattenCategories(cat.children, result);
}
}
return result;
},
// 专区切换
onZoneChange(index) {
this.selectedZoneIndex = index;
this.selectedZoneType = this.homeZones[index]?.type || 'otc';
this.selectedCategoryId = 0;
this.getCategoryList();
},
onZoneTouchStart(e) {
const touch = e.touches[0] || e.changedTouches[0];
if (!touch) return;
this.zoneTouch.startX = touch.clientX;
this.zoneTouch.startY = touch.clientY;
},
onZoneTouchEnd(e) {
const dx = e.changedTouches[0].clientX - this.zoneTouch.startX;
const dy = e.changedTouches[0].clientY - this.zoneTouch.startY;
if (Math.abs(dx) < 60 || Math.abs(dx) < Math.abs(dy)) return;
const next = dx < 0 ? this.selectedZoneIndex + 1 : this.selectedZoneIndex - 1;
if (next < 0 || next >= this.homeZones.length) return;
this.onZoneChange(next);
},
// 分类选择
selectCategory(categoryId) {
this.selectedCategoryId = categoryId;
this.getList();
},
// 核心标签判断逻辑
getProductTag(drug) {
if (!drug) return { label: 'OTC', className: 'tag-green' };
if (drug.type === 3) {
return { label: '保健食品', className: 'tag-orange' };
}
if (drug.type == 2) {
if (drug.is_otc) {
return { label: 'OTC', className: 'tag-green' };
} else {
return { label: 'Rx', className: 'tag-red' };
}
}
},
getList() {
const params = {
store_id: uni.getStorageSync('store_id') || '11001',
page: 1,
limit: 100,
category: this.selectedZoneType
};
if (this.selectedCategoryId > 0) {
params.category_id = this.selectedCategoryId;
}
if (this.searchKeyword) {
params.keyword = this.searchKeyword;
}
getStoreDrugListBySalespersonApi(params).then((res) => {
if (res.data.code == 0) {
this.products = res.data.result.records || [];
this.cartCount = res.data.result.cart_count || 0;
}
});
},
handleSearch() {
this.getList();
},
clearSearch() {
this.searchKeyword = "";
this.getList();
},
handClickProduct(item) {
let url = `/subPackages/product/symptoms?id=${item.drug.id}&drug_name=${item.drug.drug_name}&is_otc=${item.drug.is_otc}&image=${item.drug.image}&price=${item.price}`;
if (this.delegateStoreId) {
url += `&delegate_store_id=${this.delegateStoreId}`;
}
uni.navigateTo({ url });
},
// 悬浮球逻辑
initCartPosition() {
const systemInfo = uni.getSystemInfoSync();
this.cartPosition = {
x: systemInfo.windowWidth - 80,
y: systemInfo.windowHeight - 200
};
},
onTouchStart(e) {
this.cartMove.startX = e.touches[0].clientX;
this.cartMove.startY = e.touches[0].clientY;
this.cartMove.moving = false;
},
onTouchMove(e) {
this.cartMove.moving = true;
const moveX = e.touches[0].clientX - this.cartMove.startX;
const moveY = e.touches[0].clientY - this.cartMove.startY;
this.cartPosition.x += moveX;
this.cartPosition.y += moveY;
this.cartMove.startX = e.touches[0].clientX;
this.cartMove.startY = e.touches[0].clientY;
},
onTouchEnd() {
if (!this.cartMove.moving) return;
const systemInfo = uni.getSystemInfoSync();
const screenWidth = systemInfo.windowWidth;
const screenHeight = systemInfo.windowHeight;
let finalX = this.cartPosition.x;
let finalY = this.cartPosition.y;
finalX = Math.max(10, Math.min(finalX, screenWidth - 70));
finalY = Math.max(10, Math.min(finalY, screenHeight - 70));
if (finalX < screenWidth / 2) {
finalX = 20;
} else {
finalX = screenWidth - 70;
}
this.cartPosition = { x: finalX, y: finalY };
},
goToCart() {
uni.navigateTo({ url: '/subPackages/shop/shop-cate' });
},
showCartPopup(item) {
this.currentProduct = item;
this.quantity = 1;
this.showPopup = true;
},
closePopup() {
this.showPopup = false;
},
quantityChange(value) {},
confirmAddToCart() {
// 检查是否为处方药:is_otc === 0 && type == 2
if (this.currentProduct.drug && this.currentProduct.drug.is_otc === 0 && this.currentProduct.drug.type == 2) {
uni.showToast({
title: '处方药不能加入购物车,请通过提交预定购买',
icon: 'none',
duration: 2000
});
return;
}
saveCartApi({
store_id: uni.getStorageSync('store_id') || '11001',
drug_id: this.currentProduct.drug.id,
quantity: this.quantity
}).then(() => {
this.updateCartCount();
this.closePopup();
this.$refs.uToast.show({ title: '已加入购物车', type: 'success' });
});
},
updateCartCount() {
getCartCountsApi({
store_id: uni.getStorageSync('store_id') || '11001',
page: 1,
limit: 100
}).then((res) => {
if (res.data.code == 0) {
this.cartCount = res.data.result.cart_count;
}
});
},
loadMore() {
// 可扩展加载更多功能
}
},
onShow() {
if (this.homeZones.length === 0) {
this.getHomeZones();
} else {
this.getList();
}
this.initCartPosition();
this.updateCartCount();
},
onReady() {
uni.onWindowResize(() => {
this.initCartPosition();
});
},
mounted() {
this.updateCartCount();
}
};
</script>
<style lang="scss" scoped>
.container {
display: flex;
flex-direction: column;
height: 100vh;
background: #F5F7FA;
overflow: hidden;
}
/* 1. 顶部搜索栏 */
.header-section {
background: #fff;
padding: 16rpx 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.03);
.search-box {
width: 100%;
}
}
/* 2. 顶部专区Tab */
.zone-tabs-wrapper {
background: #fff;
border-bottom: 2rpx solid #f0f0f0;
.zone-tabs-scroll {
white-space: nowrap;
}
.zone-tabs {
display: inline-flex;
padding: 0 24rpx;
.zone-tab-item {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 24rpx 32rpx;
font-size: 28rpx;
color: #666;
position: relative;
transition: all 0.2s;
&.active {
color: #2B85E4;
font-weight: bold;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 48rpx;
height: 6rpx;
background: linear-gradient(90deg, #5CADFF, #2B85E4);
border-radius: 3rpx;
}
}
}
}
}
/* 3. 主体内容区 */
.main-content {
flex: 1;
display: flex;
overflow: hidden;
}
/* 左侧分类栏 */
.category-sidebar {
width: 180rpx;
background: #fff;
flex-shrink: 0;
.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: #2B85E4;
font-weight: bold;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 6rpx;
height: 48rpx;
background: #2B85E4;
border-radius: 0 3rpx 3rpx 0;
}
}
}
}
/* 右侧商品区域 */
.product-area {
flex: 1;
overflow: hidden;
.product-scroll {
height: 100%;
padding: 16rpx;
box-sizing: border-box;
}
}
/* 商品网格 - 两列布局 */
.product-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.product-card {
width: calc((100% - 16rpx) / 2);
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.04);
&:active {
transform: scale(0.98);
}
.product-img-box {
width: 100%;
height: 240rpx;
background: #f8f8f8;
.product-img {
width: 100%;
height: 100%;
&.prescription-blur {
/* 暂时禁用模糊
filter: blur(8px);
-webkit-filter: blur(8px);
*/
}
}
}
.product-info {
padding: 16rpx;
.product-title {
font-size: 26rpx;
font-weight: 600;
color: #333;
line-height: 1.4;
height: 72rpx;
margin-bottom: 12rpx;
}
.product-tags {
display: flex;
align-items: center;
margin-bottom: 12rpx;
gap: 8rpx;
.tag-label {
font-size: 18rpx;
padding: 2rpx 8rpx;
border-radius: 4rpx;
font-weight: 500;
border: 1px solid;
flex-shrink: 0;
&.tag-green { color: #10B981; border-color: #10B981; background: #ECFDF5; }
&.tag-red { color: #EF4444; border-color: #EF4444; background: #FEF2F2; }
&.tag-orange { color: #F59E0B; border-color: #F59E0B; background: #FFFBEB; }
}
.tag-spec {
font-size: 20rpx;
color: #999;
flex: 1;
min-width: 0;
}
}
.product-bottom {
display: flex;
justify-content: space-between;
align-items: center;
.price-box {
color: #EF4444;
font-weight: bold;
.price-symbol { font-size: 22rpx; }
.price-val { font-size: 32rpx; font-family: 'DIN Alternate', sans-serif; }
}
.cart-btn {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: linear-gradient(135deg, #4A90E2, #5B8FF9);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 8rpx rgba(74, 144, 226, 0.3);
}
}
}
}
/* 购物车悬浮球 */
.cart-float {
position: fixed;
z-index: 999;
.cart-icon-inner {
width: 100rpx;
height: 100rpx;
background: linear-gradient(135deg, #4A90E2, #5B8FF9);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 20rpx rgba(74, 144, 226, 0.4);
position: relative;
.badge {
position: absolute;
top: -6rpx;
right: -6rpx;
background: #EF4444;
color: #fff;
font-size: 20rpx;
padding: 2rpx 10rpx;
border-radius: 20rpx;
border: 2rpx solid #fff;
}
}
}
/* 弹窗内容 */
.popup-content {
padding: 30rpx 30rpx 40rpx;
background: #fff;
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
.product-header-row {
display: flex;
margin-bottom: 40rpx;
.popup-img {
width: 180rpx;
height: 180rpx;
border-radius: 12rpx;
margin-right: 24rpx;
background: #f8f8f8;
}
.popup-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
.price-row {
color: #EF4444;
font-size: 28rpx;
font-weight: bold;
.big { font-size: 44rpx; margin-left: 4rpx; }
}
.stock-row { font-size: 22rpx; color: #6B7280; margin: 8rpx 0; }
.selected-spec { font-size: 24rpx; color: #374151; }
}
}
.quantity-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 60rpx;
.label { font-size: 30rpx; color: #1F2937; font-weight: 500; }
}
.popup-footer {
.action-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
text-align: center;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 600;
&.add-cart {
background: linear-gradient(90deg, #4A90E2, #5B8FF9);
color: #fff;
}
&:active { opacity: 0.9; }
}
}
}
.empty-state {
margin-top: 100rpx;
}
.u-line-1 {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.u-line-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>