feat: 电子药箱小程序 - 4Tab药品管理
- 电子药箱: 药品库存管理 + 手动录入 + 今日用药打卡 - 药品百科: 搜索 + 分类筛选 + 20种药品静态数据 - 惠教中心: 4条药品使用指南课程 - 我的: 家庭成员信息管理 - 自定义TabBar + navigation-bar组件 - SVG药品分类插图
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"popup": "/components/popup/popup",
|
||||
"tag":"/components/tag/tag"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/* components/buy-record/buy-record.wxss */
|
||||
@import "variables";
|
||||
|
||||
.order {
|
||||
margin-top: $primary-gap;
|
||||
border-radius: $primary-border-radius;
|
||||
background-color: white;
|
||||
overflow: hidden;
|
||||
|
||||
.order-header {
|
||||
border-bottom: 2rpx solid rgba(0, 0, 0, 0.1);
|
||||
line-height: $primary-gap;
|
||||
height: $primary-gap;
|
||||
padding: $primary-gap;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.order-footer {
|
||||
border-top: 2rpx solid rgba(0, 0, 0, 0.1);
|
||||
// height: $primary-gap;
|
||||
padding: $primary-gap;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $primary-gap/2;
|
||||
|
||||
.order-price {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.order-total-price {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.order-trade-amount {
|
||||
color: $primary-color;
|
||||
font-size: 38rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.orderNo {
|
||||
// font-size: $large-font-size;
|
||||
}
|
||||
|
||||
.payTime {
|
||||
// font-size: $large-font-size;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.item+.item {
|
||||
border-top: 2rpx solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.item {
|
||||
width: 702rpx;
|
||||
background-color: white;
|
||||
height: 300rpx;
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: $primary-gap;
|
||||
padding-bottom: 0;
|
||||
height: 200rpx;
|
||||
|
||||
.left {
|
||||
border-radius: $primary-border-radius;
|
||||
overflow: hidden;
|
||||
min-width: 200rpx;
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
margin-right: $primary-gap;
|
||||
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
|
||||
.spu {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
font-size: $large-font-size;
|
||||
font-weight: 700;
|
||||
|
||||
.spuName {
|
||||
max-width: 300rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.sku {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
color: $gray-color;
|
||||
|
||||
.skuName {
|
||||
max-width: 300rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.history {
|
||||
width: 100%;
|
||||
border-radius: $primary-border-radius;
|
||||
text-align: end;
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.order:first-child {
|
||||
margin-top: 0rpx;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 40rpx;
|
||||
justify-content: space-between;
|
||||
|
||||
|
||||
|
||||
.totalPrice {
|
||||
font-size: $large-font-size;
|
||||
font-weight: 700;
|
||||
margin-right: $primary-gap;
|
||||
margin-top: $primary-gap/2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// .popup-header {
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// font-weight: 700;
|
||||
// font-size: $large-font-size;
|
||||
// margin-bottom: $primary-gap * 2;
|
||||
// }
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Orders } from '../../../pages/tab-bar/user/user.d'
|
||||
export const OrderStateMap: Record<number, any> = {
|
||||
10: { id: 10, name: "未付款", color: "#ff0056" },
|
||||
20: { id: 20, name: "已驳回", color: "#ff0056" },
|
||||
30: { id: 30, name: "待发货", color: "#FA6905" },
|
||||
40: { id: 40, name: "待审核", color: "#ff7500" },
|
||||
50: { id: 50, name: "发货中", color: "#1a64d3" },
|
||||
60: { id: 60, name: "已签收", color: "#00bc12" },
|
||||
70: { id: 70, name: "售后中", color: "#00b1dd" },
|
||||
80: { id: 80, name: "全退款", color: "#ff0000" },
|
||||
90: { id: 90, name: "部分退款", color: "#d90000" },
|
||||
100: { id: 100, name: "已取消", color: "#ffba00" },
|
||||
};
|
||||
|
||||
export const DeliveryStateMap: Record<number, any> = {
|
||||
10: { id: 10, name: "待确认", color: "#ff4c3e" },
|
||||
15: { id: 15, name: "定时发货", color: "#ff4c3e" },
|
||||
20: { id: 20, name: "待发送ERP", color: "gold" },
|
||||
30: { id: 30, name: "仓库待发货", color: "#ff7500" },
|
||||
40: { id: 40, name: "仓库已发货", color: "#FABD14" },
|
||||
50: { id: 50, name: "物流已揽收", color: "#057748" },
|
||||
60: { id: 60, name: "客户签收", color: "#00bc12" },
|
||||
70: { id: 70, name: "客户半签", color: "#52c41a" },
|
||||
80: { id: 80, name: "客户拒收", color: "#ff0000" },
|
||||
100: { id: 100, name: "已取消", color: "#ffba00" },
|
||||
110: { id: 110, name: "售后中", color: "#00B4F4" },
|
||||
120: { id: 120, name: "售后完成", color: "#156AA8" },
|
||||
};
|
||||
|
||||
//不能查看物流信息
|
||||
Component({
|
||||
properties: {
|
||||
order: {
|
||||
type: Object,
|
||||
value: {} as Orders,
|
||||
},
|
||||
isDetail: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
},
|
||||
data: {
|
||||
item: [] as any[],
|
||||
state: {},
|
||||
orderTime: "",
|
||||
payTime: "",
|
||||
showHistoryPopup: false,
|
||||
deliveryState: null
|
||||
},
|
||||
observers: {
|
||||
'order': function (order: Orders) {
|
||||
if (order && order.good) {
|
||||
// @ts-ignore
|
||||
this.setData({
|
||||
item: order.good,
|
||||
state: OrderStateMap[order.state],
|
||||
orderTime: order.orderTime.replace("T", " "),
|
||||
payTime: order.payTime ? order.payTime.replace("T", " ") : ""
|
||||
})
|
||||
}
|
||||
if (order && order.deliveryState) {
|
||||
// @ts-ignore
|
||||
this.setData({
|
||||
deliveryState: DeliveryStateMap[order.deliveryState]
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// openPopup() {
|
||||
// this.setData({ showHistoryPopup: true })
|
||||
// },
|
||||
// closePopup() {
|
||||
// this.setData({ showHistoryPopup: false })
|
||||
// }
|
||||
handleDetail(this: WechatMiniprogram.Component.TrivialInstance) {
|
||||
if (this.data.isDetail) return;
|
||||
wx.navigateTo({ url: `/pages/common/order-detail/order-detail?id=${this.data.order.orderNo}` })
|
||||
}
|
||||
}
|
||||
})
|
||||
export { }
|
||||
@@ -0,0 +1,68 @@
|
||||
<!--components/buy-record/buy-record.wxml-->
|
||||
<view class="order">
|
||||
<view class="order-header">订单创建时间 {{orderTime}}</view>
|
||||
|
||||
<view class="item" wx:for="{{item}}" wx:key="skuName" bind:tap="handleDetail">
|
||||
<view class="info">
|
||||
<view class="left">
|
||||
<image src="{{item.picUrl}}" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="right">
|
||||
<view class="spu">
|
||||
<view class="spuName">{{item.spuName}}</view>
|
||||
<view class="price">{{item.price?'¥'+item.price / 100:"暂无报价"}}</view>
|
||||
</view>
|
||||
<view class="sku">
|
||||
<view class="skuName">{{item.skuName}}</view>
|
||||
<view class="count">x{{item.count}}</view>
|
||||
</view>
|
||||
<tag class="history" color="{{state.color}}" propStyle="margin-right: 0rpx;">{{state.name}}</tag>
|
||||
<view class="history" style="color:{{deliveryState.color}}">{{deliveryState.name}}</view>
|
||||
<!-- <view class="history">
|
||||
查看物流
|
||||
</view> -->
|
||||
<view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="footer">
|
||||
<view></view>
|
||||
<view class="totalPrice">
|
||||
应付款:¥{{item.totalPrice / 100}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="order-footer">
|
||||
<view class="order-price">
|
||||
<view class="order-total-price">订单总金额 ¥{{order.totalAmount/100}}</view>
|
||||
<view class="order-total-price">
|
||||
优惠金额:¥{{order.discountAmount/100 || 0.00}}
|
||||
</view>
|
||||
</view>
|
||||
<view class="order-price">
|
||||
<view class="order-trade-amount">
|
||||
实付金额 ¥{{order.advancePrice/100 || 0.00}}
|
||||
</view>
|
||||
<view class="order-trade-amount" >
|
||||
{{state>=70?'退款金额':'支付金额'}}
|
||||
¥{{order.tradeAmount/100}}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="orderNo" wx:if="{{order.orderNo}}">订单号 {{order.orderNo}}</view>
|
||||
<view class="orderNo" wx:if="{{order.transactionId}}">微信订单号 {{order.transactionId}}</view>
|
||||
<view class="payTime" wx:if="{{order.payTime}}">订单支付时间 {{payTime||'未付款'}}</view>
|
||||
<view class="payTime" wx:if="{{order.sendTime}}">订单发货时间 {{sendTime||'未付款'}}</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<!-- <popup visible="{{showHistoryPopup}}" bind:close="closePopup">
|
||||
<view slot="header" class="popup-header">
|
||||
<text>物流信息</text>
|
||||
</view>
|
||||
<view>
|
||||
这里显示物流的信息
|
||||
</view>
|
||||
</popup> -->
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"van-stepper": "@vant/weapp/stepper/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/* components/cart-item/cart-item.wxss */
|
||||
@import "variables";
|
||||
|
||||
.item {
|
||||
background-color: white;
|
||||
// background-color: red;
|
||||
height: 300rpx;
|
||||
margin-bottom: $primary-gap;
|
||||
border-radius: $primary-border-radius;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: $primary-gap;
|
||||
|
||||
.left {
|
||||
height: 300rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
|
||||
.pic {
|
||||
border-radius: $primary-border-radius;
|
||||
overflow: hidden;
|
||||
min-width: 200rpx;
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
margin-right: $primary-gap;
|
||||
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.delete {
|
||||
height: 100rpx;
|
||||
line-height: 120rpx;
|
||||
margin-left: $primary-gap/2;
|
||||
|
||||
.delete-button {
|
||||
color: $danger-color;
|
||||
font-size: $large-font-size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.right {
|
||||
height: 200rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
height: 300rpx;
|
||||
max-width: 440rpx;
|
||||
|
||||
.name {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
max-width: 500rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.price {
|
||||
font-size: $large-font-size;
|
||||
font-weight: 700;
|
||||
text-align: end;
|
||||
color: $primary-color;
|
||||
}
|
||||
|
||||
.count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
text-align: end;
|
||||
justify-content: flex-end;
|
||||
float: right;
|
||||
|
||||
.stepper {
|
||||
margin-right: $primary-gap/2;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: $large-font-size;
|
||||
text-align: end;
|
||||
}
|
||||
}
|
||||
|
||||
.stock {
|
||||
color: $gray-color;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
font-size: $large-font-size;
|
||||
font-weight: 700;
|
||||
text-align: end;
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// @ts-nocheck
|
||||
// components/cart-item/cart-item.ts
|
||||
export { }
|
||||
const app = getApp<IAppOption>();
|
||||
Component({
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
item: {},
|
||||
shoppingCart: {},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
totalPrice: 0,
|
||||
showStock: app.globalData.showStock
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
calcTotalPrice() {
|
||||
if (!this.data.shoppingCart || !this.data.shoppingCart.data) return 0;
|
||||
const total = this.data.shoppingCart.data.reduce((sum: number, item: any) => {
|
||||
return sum + item.price * item.count;
|
||||
}, 0);
|
||||
this.setData({ totalPrice: total });
|
||||
},
|
||||
handleCountChange(e: any) {
|
||||
this.setData({ item: { ...this.data.item, count: e.detail } })
|
||||
|
||||
const target = this.data.shoppingCart.data.find(
|
||||
(item: any) => item.skuId == this.data.item.skuId
|
||||
)
|
||||
if (target) target.count = e.detail;
|
||||
let storage = wx.getStorageSync("shoppingCart") || [];
|
||||
storage = storage.map((cart: any) => {
|
||||
if (cart.mbuId == this.data.shoppingCart.mbuId) {
|
||||
return this.data.shoppingCart;
|
||||
}
|
||||
return cart;
|
||||
});
|
||||
wx.setStorageSync("shoppingCart", storage);
|
||||
this.calcTotalPrice();
|
||||
this.triggerEvent('handleCountChange', {
|
||||
totalPrice: this.data.totalPrice
|
||||
})
|
||||
},
|
||||
handleDelete() {
|
||||
this.triggerEvent('delete', {
|
||||
skuId: this.data.item.skuId
|
||||
})
|
||||
}
|
||||
},
|
||||
lifetimes: {
|
||||
created: function () {
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
<!--components/cart-item/cart-item.wxml-->
|
||||
<view class="item">
|
||||
<view class="left">
|
||||
<view class="pic">
|
||||
<image src="{{item.picUrl}}"></image>
|
||||
</view>
|
||||
<view class="delete">
|
||||
<view bind:tap="handleDelete" class="delete-button">
|
||||
删除
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="right">
|
||||
<view class="name">{{item.name}}</view>
|
||||
<view class="price">¥ {{item.price/100}} </view>
|
||||
<!-- 这里用计数器 -->
|
||||
<view class="count">
|
||||
<!-- max="{{showStock?checkItem ? checkItem.stock : skus[0].stock :99999}}" -->
|
||||
<van-stepper integer min="1" bind:change="handleCountChange" max="{{showStock?item.stock:99999}}" value="{{item.count}}"
|
||||
class="stepper"></van-stepper>
|
||||
<view class="unit">{{item.unit}}</view>
|
||||
</view>
|
||||
|
||||
<view class="total-price">总金额:¥ {{item.price*item.count/100}}</view>
|
||||
|
||||
<view class="stock" wx:if="{{showStock}}">库存:{{item.stock}}</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"guide-popup": "/components/static/guide-popup/guide-popup"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
@import "variables";
|
||||
|
||||
.comment {
|
||||
padding: 0rpx $primary-gap;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 100rpx;
|
||||
z-index: 9;
|
||||
overflow: visible;
|
||||
|
||||
.top-notice-layer {
|
||||
position: absolute;
|
||||
left: $primary-gap;
|
||||
right: 190rpx;
|
||||
top: -86rpx;
|
||||
height: 64rpx;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.top-notice-pill {
|
||||
height: 64rpx;
|
||||
width: 100%;
|
||||
padding: 0 20rpx;
|
||||
border-radius: 999rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
color: #f6fbff;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(12, 24, 52, 0.92) 0%,
|
||||
rgba(30, 78, 132, 0.78) 100%
|
||||
);
|
||||
border: 1rpx solid rgba(117, 207, 255, 0.24);
|
||||
box-shadow: 0 14rpx 36rpx rgba(0, 0, 0, 0.22);
|
||||
animation: top-notice-slide 3.4s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
.top-notice-tag {
|
||||
flex-shrink: 0;
|
||||
height: 36rpx;
|
||||
padding: 0 16rpx;
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #62f0ff 0%, #3cb6ff 100%);
|
||||
color: #07253d;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.top-notice-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.comment-container {
|
||||
width: 470rpx;
|
||||
max-width: calc(100% - 260rpx);
|
||||
overflow: hidden;
|
||||
|
||||
.list-wrapper {
|
||||
will-change: transform;
|
||||
|
||||
.msg-item {
|
||||
line-height: 50rpx;
|
||||
border-radius: 30rpx;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
min-height: 50rpx;
|
||||
margin-top: $primary-gap / 2;
|
||||
font-weight: 700;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
word-break: break-all;
|
||||
padding: 0rpx $primary-gap;
|
||||
|
||||
&.is-system {
|
||||
background: rgba(30, 65, 117, 0.48);
|
||||
border: 1rpx solid rgba(104, 196, 255, 0.18);
|
||||
}
|
||||
|
||||
.msg-item-name {
|
||||
color: #76dbfd;
|
||||
margin-right: $primary-gap;
|
||||
}
|
||||
|
||||
.msg-item-text {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unread-reminder {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 116rpx;
|
||||
transform: translateX(-50%);
|
||||
min-width: 180rpx;
|
||||
height: 56rpx;
|
||||
padding: 0 22rpx;
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10rpx;
|
||||
box-sizing: border-box;
|
||||
color: #ffffff;
|
||||
background: rgba(14, 24, 44, 0.9);
|
||||
box-shadow: 0 10rpx 24rpx rgba(0, 0, 0, 0.18);
|
||||
animation: unread-reminder-pop 0.24s ease-out;
|
||||
}
|
||||
|
||||
.unread-reminder-dot {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
border-radius: 50%;
|
||||
background: #ff6a88;
|
||||
box-shadow: 0 0 0 8rpx rgba(255, 106, 136, 0.12);
|
||||
}
|
||||
|
||||
.unread-reminder-text {
|
||||
font-size: 24rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.input-shell {
|
||||
margin-top: $primary-gap * 1.5;
|
||||
width: calc(100% - 260rpx);
|
||||
max-width: 470rpx;
|
||||
height: 90rpx;
|
||||
padding: 0rpx $primary-gap;
|
||||
border-radius: 999rpx;
|
||||
box-sizing: border-box;
|
||||
background: rgba(0, 0, 0, 0.56);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: background 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||
|
||||
&.is-enhanced {
|
||||
padding-left: 18rpx;
|
||||
padding-right: 10rpx;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(12, 19, 30, 0.88) 0%,
|
||||
rgba(20, 36, 59, 0.84) 100%
|
||||
);
|
||||
border: 1rpx solid rgba(135, 195, 255, 0.16);
|
||||
box-shadow: 0 18rpx 40rpx rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
&.is-focused {
|
||||
transform: translateY(-2rpx);
|
||||
box-shadow: 0 18rpx 42rpx rgba(48, 137, 255, 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
.input-leading {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
margin-right: 14rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input-leading-dot {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #5dfff0 0%, #4ab7ff 100%);
|
||||
box-shadow: 0 0 0 10rpx rgba(73, 207, 255, 0.1);
|
||||
animation: input-pulse 1.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.msg-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
line-height: 90rpx;
|
||||
color: white;
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input-send {
|
||||
min-width: 108rpx;
|
||||
height: 68rpx;
|
||||
padding: 0 24rpx;
|
||||
border-radius: 999rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.64);
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2rpx;
|
||||
transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease;
|
||||
|
||||
&.is-ready {
|
||||
background: linear-gradient(135deg, #53d6ff 0%, #2f8cff 100%);
|
||||
color: #ffffff;
|
||||
transform: translateY(-1rpx);
|
||||
box-shadow: 0 10rpx 26rpx rgba(47, 140, 255, 0.26);
|
||||
}
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
position: absolute;
|
||||
right: $primary-gap;
|
||||
bottom: 7rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(17, 17, 17, 0.58);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 12rpx 28rpx rgba(0, 0, 0, 0.14);
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.action-btn-hover {
|
||||
transform: scale(0.94);
|
||||
background: rgba(17, 17, 17, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.action-share {
|
||||
border: none;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn-heart {
|
||||
background: rgba(255, 61, 104, 0.18);
|
||||
border: 2rpx solid rgba(255, 108, 143, 0.45);
|
||||
|
||||
&.is-active {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 12rpx 30rpx rgba(255, 61, 104, 0.24);
|
||||
}
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 38rpx;
|
||||
height: 38rpx;
|
||||
}
|
||||
|
||||
.action-icon-heart {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.heart-layer {
|
||||
position: absolute;
|
||||
right: $primary-gap + 12rpx;
|
||||
bottom: 84rpx;
|
||||
width: 160rpx;
|
||||
height: 340rpx;
|
||||
pointer-events: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.heart-particle {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
animation-fill-mode: forwards;
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
.heart-glyph {
|
||||
display: none;
|
||||
line-height: 1;
|
||||
text-shadow: 0 8rpx 20rpx rgba(255, 77, 109, 0.2);
|
||||
}
|
||||
|
||||
.heart-fly-icon {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pulse-small {
|
||||
animation: heart-pop 0.72s ease-out;
|
||||
}
|
||||
|
||||
.pulse-medium {
|
||||
animation: heart-pop 0.84s ease-out;
|
||||
}
|
||||
|
||||
.pulse-large {
|
||||
animation: heart-pop 0.96s ease-out;
|
||||
}
|
||||
|
||||
.path-left {
|
||||
animation-name: float-heart-left;
|
||||
}
|
||||
|
||||
.path-center {
|
||||
animation-name: float-heart-center;
|
||||
}
|
||||
|
||||
.path-right {
|
||||
animation-name: float-heart-right;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes input-pulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(0.9);
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.08);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes unread-reminder-pop {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(12rpx);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes top-notice-slide {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(120%, 0, 0) scale(0.96);
|
||||
}
|
||||
|
||||
12% {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
}
|
||||
|
||||
86% {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate3d(-16%, 0, 0) scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes heart-pop {
|
||||
0% {
|
||||
transform: scale(0.6);
|
||||
}
|
||||
|
||||
45% {
|
||||
transform: scale(1.18);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float-heart-left {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 0, 0) scale(0.45);
|
||||
}
|
||||
|
||||
18% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate3d(-48rpx, -240rpx, 0) scale(1.18);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float-heart-center {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 0, 0) scale(0.45);
|
||||
}
|
||||
|
||||
18% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate3d(8rpx, -260rpx, 0) scale(1.24);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float-heart-right {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 0, 0) scale(0.45);
|
||||
}
|
||||
|
||||
18% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate3d(54rpx, -228rpx, 0) scale(1.12);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,681 @@
|
||||
import { request } from "../../../utils/request";
|
||||
import { shopId } from "../../../env";
|
||||
import {
|
||||
join,
|
||||
leave,
|
||||
addCustomEventListener,
|
||||
removeCustomEventListener,
|
||||
} from "../../../utils/server-sent-events";
|
||||
|
||||
type CommentMessage = {
|
||||
id: string;
|
||||
name: string;
|
||||
msg: string;
|
||||
kind?: "system" | "chat";
|
||||
};
|
||||
|
||||
type CommentFeatureState = {
|
||||
approved: boolean;
|
||||
unreadReminder: boolean;
|
||||
optimizedInput: boolean;
|
||||
topTicker: boolean;
|
||||
};
|
||||
|
||||
interface CommentProperties {
|
||||
enhancementApproved: boolean;
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
interface CommentData {
|
||||
list: CommentMessage[];
|
||||
displayList: CommentMessage[];
|
||||
scrollTop: number;
|
||||
isAtBottom: boolean;
|
||||
maxVisible: number;
|
||||
itemHeight: number;
|
||||
MAX_HISTORY: number;
|
||||
joined: boolean;
|
||||
listener: boolean;
|
||||
lastMsgKey: string;
|
||||
inputValue: string;
|
||||
canSubmit: boolean;
|
||||
inputFocused: boolean;
|
||||
inputPlaceholderStyle: string;
|
||||
showGuidePopup: boolean;
|
||||
guideTitle: string;
|
||||
guideSubtitle: string;
|
||||
guideImages: string[];
|
||||
heartParticles: any[];
|
||||
heartButtonActive: boolean;
|
||||
unreadMessageCount: number;
|
||||
topNoticeText: string;
|
||||
topNoticeVisible: boolean;
|
||||
featureState: CommentFeatureState;
|
||||
}
|
||||
|
||||
interface CommentInstance extends WechatMiniprogram.Component.TrivialInstance {
|
||||
data: CommentData;
|
||||
properties: CommentProperties;
|
||||
_sseListener?: (payload: any) => void;
|
||||
_heartButtonTimer?: number | null;
|
||||
_heartTimers?: number[];
|
||||
_topNoticeTimer?: number | null;
|
||||
_topNoticeQueue?: string[];
|
||||
safeLeave: () => Promise<void>;
|
||||
bindSseListener: () => void;
|
||||
unbindSseListener: () => void;
|
||||
appendMsgFromSse: (
|
||||
payload: { name: string; msg: string },
|
||||
options?: { kind?: "system" | "chat"; skipTopNotice?: boolean },
|
||||
) => void;
|
||||
handleSubmit: (e: any) => Promise<void>;
|
||||
appendMsgText: (text: string) => void;
|
||||
createHeartParticle: (index: number) => any;
|
||||
removeHeartParticle: (id: string) => void;
|
||||
clearHeartEffects: () => void;
|
||||
safeVibrate: () => void;
|
||||
}
|
||||
|
||||
const ITEM_HEIGHT = 60;
|
||||
const MAX_VISIBLE = 5;
|
||||
const MAX_HISTORY = 50;
|
||||
const SSE_CHANNEL = "12";
|
||||
const SYSTEM_NOTICE = "欢迎来到直播间,请文明发言,禁止发布违规信息。";
|
||||
const DEFAULT_GUIDE_SUBTITLE = "个人中心";
|
||||
const DEFAULT_GUIDE_IMAGES = Object.freeze(["/assets/img/600.jpg"]);
|
||||
const DEFAULT_PLACEHOLDER_STYLE = "color:white;font-size:32rpx";
|
||||
const ENHANCED_PLACEHOLDER_STYLE =
|
||||
"color:rgba(255,255,255,0.62);font-size:28rpx";
|
||||
const TOP_NOTICE_DURATION = 3400;
|
||||
const REVIEW_READY_FEATURES = Object.freeze({
|
||||
unreadReminder: true,
|
||||
optimizedInput: true,
|
||||
topTicker: true,
|
||||
});
|
||||
|
||||
function getChatChannelName() {
|
||||
return `${shopId}-${SSE_CHANNEL}`;
|
||||
}
|
||||
|
||||
function createMessage(
|
||||
name: string,
|
||||
msg: string,
|
||||
kind: "system" | "chat" = "chat",
|
||||
): CommentMessage {
|
||||
return {
|
||||
id: `msg_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
|
||||
name,
|
||||
msg,
|
||||
kind,
|
||||
};
|
||||
}
|
||||
|
||||
function buildWelcomeMessage() {
|
||||
return createMessage("系统", SYSTEM_NOTICE, "system");
|
||||
}
|
||||
|
||||
function buildFeatureState(approved: boolean): CommentFeatureState {
|
||||
return {
|
||||
approved,
|
||||
unreadReminder: approved && REVIEW_READY_FEATURES.unreadReminder,
|
||||
optimizedInput: approved && REVIEW_READY_FEATURES.optimizedInput,
|
||||
topTicker: approved && REVIEW_READY_FEATURES.topTicker,
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeInput(value: unknown): string {
|
||||
return String(value ?? "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function buildGuideImages(avatarUrl: unknown): string[] {
|
||||
const normalizedAvatarUrl = sanitizeInput(avatarUrl);
|
||||
|
||||
return normalizedAvatarUrl
|
||||
? [normalizedAvatarUrl]
|
||||
: [...DEFAULT_GUIDE_IMAGES];
|
||||
}
|
||||
|
||||
function syncGuideImages(instance: CommentInstance, avatarUrl: unknown) {
|
||||
const nextGuideImages = buildGuideImages(avatarUrl);
|
||||
const currentGuideImages = instance.data.guideImages || [];
|
||||
const unchanged =
|
||||
currentGuideImages.length === nextGuideImages.length &&
|
||||
currentGuideImages.every((item, index) => item === nextGuideImages[index]);
|
||||
|
||||
if (unchanged) return;
|
||||
|
||||
instance.setData({
|
||||
guideImages: nextGuideImages,
|
||||
});
|
||||
}
|
||||
|
||||
function trimMessages(messages: CommentMessage[], maxHistory: number) {
|
||||
if (messages.length <= maxHistory) return messages;
|
||||
|
||||
const pinnedMessage = messages[0]?.kind === "system" ? messages[0] : null;
|
||||
if (!pinnedMessage) {
|
||||
return messages.slice(-maxHistory);
|
||||
}
|
||||
|
||||
const normalMessages = messages.slice(1);
|
||||
return [pinnedMessage, ...normalMessages.slice(-(maxHistory - 1))];
|
||||
}
|
||||
|
||||
function formatTopNoticeText(name: string, msg: string) {
|
||||
const safeName = sanitizeInput(name) || "新消息";
|
||||
const preview = sanitizeInput(msg);
|
||||
const clippedPreview =
|
||||
preview.length > 18 ? `${preview.slice(0, 18)}...` : preview;
|
||||
|
||||
return clippedPreview
|
||||
? `${safeName}:${clippedPreview}`
|
||||
: `${safeName} 发来了一条新消息`;
|
||||
}
|
||||
|
||||
function clearTopNotice(instance: CommentInstance) {
|
||||
if (instance._topNoticeTimer) {
|
||||
clearTimeout(instance._topNoticeTimer);
|
||||
instance._topNoticeTimer = null;
|
||||
}
|
||||
|
||||
instance._topNoticeQueue = [];
|
||||
|
||||
if (instance.data.topNoticeVisible || instance.data.topNoticeText) {
|
||||
instance.setData({
|
||||
topNoticeVisible: false,
|
||||
topNoticeText: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function playTopNoticeQueue(instance: CommentInstance) {
|
||||
if (!instance.data.featureState.topTicker || instance.data.topNoticeVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
instance._topNoticeQueue = instance._topNoticeQueue || [];
|
||||
const nextNotice = instance._topNoticeQueue.shift();
|
||||
if (!nextNotice) return;
|
||||
|
||||
instance.setData(
|
||||
{
|
||||
topNoticeText: nextNotice,
|
||||
topNoticeVisible: false,
|
||||
},
|
||||
() => {
|
||||
wx.nextTick(() => {
|
||||
instance.setData({ topNoticeVisible: true });
|
||||
instance._topNoticeTimer = setTimeout(() => {
|
||||
instance.setData({ topNoticeVisible: false }, () => {
|
||||
instance._topNoticeTimer = null;
|
||||
playTopNoticeQueue(instance);
|
||||
});
|
||||
}, TOP_NOTICE_DURATION);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function enqueueTopNotice(instance: CommentInstance, text: string) {
|
||||
if (!instance.data.featureState.topTicker || !text) return;
|
||||
|
||||
instance._topNoticeQueue = instance._topNoticeQueue || [];
|
||||
instance._topNoticeQueue.push(text);
|
||||
|
||||
if (!instance.data.topNoticeVisible) {
|
||||
playTopNoticeQueue(instance);
|
||||
}
|
||||
}
|
||||
|
||||
function clearUnreadReminder(instance: CommentInstance) {
|
||||
if (!instance.data.unreadMessageCount) return;
|
||||
|
||||
instance.setData({ unreadMessageCount: 0 });
|
||||
}
|
||||
|
||||
function scrollToLatest(instance: CommentInstance, clearUnread = false) {
|
||||
if (clearUnread) {
|
||||
clearUnreadReminder(instance);
|
||||
}
|
||||
|
||||
instance.setData({ scrollTop: Date.now() });
|
||||
}
|
||||
|
||||
function syncFeatureApproval(instance: CommentInstance, approved: boolean) {
|
||||
const justEnabledTopTicker =
|
||||
!instance.data.featureState.topTicker &&
|
||||
approved &&
|
||||
REVIEW_READY_FEATURES.topTicker;
|
||||
const featureState = buildFeatureState(approved);
|
||||
|
||||
instance.setData(
|
||||
{
|
||||
featureState,
|
||||
inputPlaceholderStyle: featureState.optimizedInput
|
||||
? ENHANCED_PLACEHOLDER_STYLE
|
||||
: DEFAULT_PLACEHOLDER_STYLE,
|
||||
inputFocused: featureState.optimizedInput
|
||||
? instance.data.inputFocused
|
||||
: false,
|
||||
unreadMessageCount: featureState.unreadReminder
|
||||
? instance.data.unreadMessageCount
|
||||
: 0,
|
||||
topNoticeText: featureState.topTicker ? instance.data.topNoticeText : "",
|
||||
topNoticeVisible: featureState.topTicker
|
||||
? instance.data.topNoticeVisible
|
||||
: false,
|
||||
},
|
||||
() => {
|
||||
if (justEnabledTopTicker) {
|
||||
enqueueTopNotice(
|
||||
instance,
|
||||
formatTopNoticeText("直播提醒", SYSTEM_NOTICE),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!featureState.topTicker) {
|
||||
clearTopNotice(instance);
|
||||
}
|
||||
}
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
enhancementApproved: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer(this: CommentInstance, nextValue: boolean) {
|
||||
syncFeatureApproval(this, !!nextValue);
|
||||
},
|
||||
},
|
||||
avatarUrl: {
|
||||
type: String,
|
||||
value: "",
|
||||
observer(this: CommentInstance, nextValue: string) {
|
||||
syncGuideImages(this, nextValue);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
list: [] as CommentMessage[],
|
||||
displayList: [] as CommentMessage[],
|
||||
scrollTop: 999999,
|
||||
isAtBottom: true,
|
||||
maxVisible: MAX_VISIBLE,
|
||||
itemHeight: ITEM_HEIGHT,
|
||||
MAX_HISTORY,
|
||||
joined: false,
|
||||
listener: false,
|
||||
lastMsgKey: "",
|
||||
inputValue: "",
|
||||
canSubmit: false,
|
||||
inputFocused: false,
|
||||
inputPlaceholderStyle: DEFAULT_PLACEHOLDER_STYLE,
|
||||
showGuidePopup: false,
|
||||
guideTitle: "",
|
||||
guideSubtitle: DEFAULT_GUIDE_SUBTITLE,
|
||||
guideImages: [...DEFAULT_GUIDE_IMAGES],
|
||||
heartParticles: [] as any[],
|
||||
heartButtonActive: false,
|
||||
unreadMessageCount: 0,
|
||||
topNoticeText: "",
|
||||
topNoticeVisible: false,
|
||||
featureState: buildFeatureState(false),
|
||||
} as CommentData,
|
||||
|
||||
lifetimes: {
|
||||
attached(this: CommentInstance) {
|
||||
const initialMessages = [buildWelcomeMessage()];
|
||||
this._topNoticeQueue = [];
|
||||
|
||||
this.setData(
|
||||
{
|
||||
list: initialMessages,
|
||||
displayList: initialMessages,
|
||||
},
|
||||
() => {
|
||||
syncGuideImages(this, this.properties.avatarUrl);
|
||||
syncFeatureApproval(this, !!this.properties.enhancementApproved);
|
||||
this.bindSseListener();
|
||||
|
||||
setTimeout(() => {
|
||||
scrollToLatest(this);
|
||||
}, 50);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
detached(this: CommentInstance) {
|
||||
this.unbindSseListener();
|
||||
this.safeLeave();
|
||||
this.clearHeartEffects();
|
||||
clearTopNotice(this);
|
||||
},
|
||||
},
|
||||
|
||||
pageLifetimes: {
|
||||
show(this: CommentInstance) {
|
||||
this.bindSseListener();
|
||||
|
||||
wx.nextTick(async () => {
|
||||
if (this.data.joined) return;
|
||||
|
||||
try {
|
||||
await join(getChatChannelName());
|
||||
this.setData({ joined: true });
|
||||
} catch (error) {
|
||||
console.warn("SSE join failed", error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
hide(this: CommentInstance) {
|
||||
this.unbindSseListener();
|
||||
this.safeLeave();
|
||||
this.clearHeartEffects();
|
||||
clearTopNotice(this);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async safeLeave(this: CommentInstance) {
|
||||
if (!this.data.joined) return;
|
||||
|
||||
this.setData({ joined: false });
|
||||
try {
|
||||
await leave(getChatChannelName());
|
||||
} catch (error) {
|
||||
console.warn("SSE leave failed", error);
|
||||
}
|
||||
},
|
||||
|
||||
bindSseListener(this: CommentInstance) {
|
||||
if (this.data.listener) return;
|
||||
|
||||
this.setData({ listener: true });
|
||||
|
||||
this._sseListener = (payload: any) => {
|
||||
if (!payload) return;
|
||||
|
||||
const { name, msg } = payload;
|
||||
if (!msg && msg !== "") return;
|
||||
|
||||
const key = `${name ?? ""}:${msg}`;
|
||||
if (key === this.data.lastMsgKey) return;
|
||||
|
||||
this.setData({ lastMsgKey: key });
|
||||
this.appendMsgFromSse({
|
||||
name: name ?? "用户",
|
||||
msg,
|
||||
});
|
||||
};
|
||||
|
||||
addCustomEventListener("pushMsg", this._sseListener);
|
||||
},
|
||||
|
||||
unbindSseListener(this: CommentInstance) {
|
||||
if (!this.data.listener) return;
|
||||
|
||||
this.setData({ listener: false });
|
||||
|
||||
if (!this._sseListener) return;
|
||||
|
||||
removeCustomEventListener("pushMsg", this._sseListener);
|
||||
this._sseListener = undefined;
|
||||
},
|
||||
|
||||
onScroll(this: CommentInstance, e: any) {
|
||||
const { scrollTop, scrollHeight } = e.detail;
|
||||
|
||||
wx.createSelectorQuery()
|
||||
.in(this)
|
||||
.select("#scrollView")
|
||||
.boundingClientRect((rect) => {
|
||||
const clientHeight = rect?.height || 0;
|
||||
const isBottom = scrollTop + clientHeight >= scrollHeight - 20;
|
||||
if (isBottom !== this.data.isAtBottom) {
|
||||
this.setData({ isAtBottom: isBottom });
|
||||
}
|
||||
|
||||
if (isBottom) {
|
||||
clearUnreadReminder(this);
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
},
|
||||
|
||||
appendMsgFromSse(
|
||||
this: CommentInstance,
|
||||
payload: { name: string; msg: string },
|
||||
options: { kind?: "system" | "chat"; skipTopNotice?: boolean } = {},
|
||||
) {
|
||||
const messageName =
|
||||
sanitizeInput(payload.name) ||
|
||||
(options.kind === "system" ? "系统" : "用户");
|
||||
const messageText = String(payload.msg ?? "");
|
||||
const nextMessage = createMessage(
|
||||
messageName,
|
||||
messageText,
|
||||
options.kind || "chat",
|
||||
);
|
||||
const nextList = trimMessages(
|
||||
[...this.data.list, nextMessage],
|
||||
this.data.MAX_HISTORY,
|
||||
);
|
||||
const shouldAutoScroll = this.data.isAtBottom;
|
||||
const shouldShowUnreadReminder =
|
||||
this.data.featureState.unreadReminder &&
|
||||
!shouldAutoScroll &&
|
||||
(options.kind || "chat") !== "system";
|
||||
|
||||
this.setData(
|
||||
{
|
||||
list: nextList,
|
||||
displayList: nextList,
|
||||
unreadMessageCount: shouldShowUnreadReminder
|
||||
? this.data.unreadMessageCount + 1
|
||||
: this.data.unreadMessageCount,
|
||||
},
|
||||
() => {
|
||||
if (shouldAutoScroll) {
|
||||
scrollToLatest(this);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (this.data.featureState.topTicker && !options.skipTopNotice) {
|
||||
enqueueTopNotice(this, formatTopNoticeText(messageName, messageText));
|
||||
}
|
||||
},
|
||||
|
||||
handleInput(this: CommentInstance, e: any) {
|
||||
const value = e.detail.value || "";
|
||||
|
||||
this.setData({
|
||||
inputValue: value,
|
||||
canSubmit: !!sanitizeInput(value),
|
||||
});
|
||||
},
|
||||
|
||||
handleInputFocus(this: CommentInstance) {
|
||||
if (!this.data.featureState.optimizedInput) return;
|
||||
|
||||
this.setData({ inputFocused: true });
|
||||
},
|
||||
|
||||
handleInputBlur(this: CommentInstance) {
|
||||
if (!this.data.inputFocused) return;
|
||||
|
||||
this.setData({ inputFocused: false });
|
||||
},
|
||||
|
||||
submitCurrentInput(this: CommentInstance) {
|
||||
if (!this.data.canSubmit) return;
|
||||
|
||||
this.handleSubmit({
|
||||
detail: {
|
||||
value: this.data.inputValue,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
handleUnreadReminderTap(this: CommentInstance) {
|
||||
scrollToLatest(this, true);
|
||||
},
|
||||
|
||||
openGuidePopup(this: CommentInstance) {
|
||||
this.setData({
|
||||
showGuidePopup: true,
|
||||
guideTitle: wx.getStorageSync("userName") || "当前用户",
|
||||
guideSubtitle: DEFAULT_GUIDE_SUBTITLE,
|
||||
});
|
||||
},
|
||||
|
||||
closeGuidePopup(this: CommentInstance) {
|
||||
this.setData({
|
||||
showGuidePopup: false,
|
||||
});
|
||||
},
|
||||
|
||||
async handleSubmit(this: CommentInstance, e: any) {
|
||||
const value = sanitizeInput(e?.detail?.value ?? this.data.inputValue);
|
||||
|
||||
if (!value) {
|
||||
wx.showToast({
|
||||
title: "发送内容不能为空",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await request({
|
||||
options: {
|
||||
url: "/app/mini-app/push-chat-msg",
|
||||
data: {
|
||||
shopId,
|
||||
id: Number(SSE_CHANNEL),
|
||||
name: wx.getStorageSync("userName") || "微信用户",
|
||||
msg: value,
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error, "push chat message failed");
|
||||
this.appendMsgText(value);
|
||||
} finally {
|
||||
this.setData({
|
||||
inputValue: "",
|
||||
canSubmit: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
appendMsgText(this: CommentInstance, text: string) {
|
||||
this.appendMsgFromSse({
|
||||
name: wx.getStorageSync("userName") || "微信用户",
|
||||
msg: text,
|
||||
});
|
||||
},
|
||||
|
||||
handleHeartTap(this: CommentInstance) {
|
||||
const particles = Array.from({ length: 6 }, (_, index) =>
|
||||
this.createHeartParticle(index),
|
||||
);
|
||||
|
||||
this.setData({
|
||||
heartParticles: [...this.data.heartParticles, ...particles],
|
||||
heartButtonActive: true,
|
||||
});
|
||||
|
||||
this.safeVibrate();
|
||||
|
||||
if (this._heartButtonTimer) {
|
||||
clearTimeout(this._heartButtonTimer);
|
||||
}
|
||||
|
||||
this._heartButtonTimer = setTimeout(() => {
|
||||
this.setData({ heartButtonActive: false });
|
||||
}, 220);
|
||||
|
||||
this._heartTimers = this._heartTimers || [];
|
||||
particles.forEach((particle: any) => {
|
||||
const timer = setTimeout(
|
||||
() => {
|
||||
this.removeHeartParticle(particle.id);
|
||||
},
|
||||
particle.duration + particle.delay + 260,
|
||||
);
|
||||
this._heartTimers?.push(timer);
|
||||
});
|
||||
},
|
||||
|
||||
createHeartParticle(index: number) {
|
||||
const palette = ["#ff4d6d", "#ff6b81", "#ff8399", "#ffb3c1"];
|
||||
const pathClasses = ["path-left", "path-center", "path-right"];
|
||||
const pulseClasses = ["pulse-small", "pulse-medium", "pulse-large"];
|
||||
|
||||
return {
|
||||
id: `heart_${Date.now()}_${index}_${Math.floor(Math.random() * 1000)}`,
|
||||
right: 16 + Math.floor(Math.random() * 28),
|
||||
bottom: 10 + Math.floor(Math.random() * 18),
|
||||
size: 34 + Math.floor(Math.random() * 10),
|
||||
color:
|
||||
palette[
|
||||
(index + Math.floor(Math.random() * palette.length)) %
|
||||
palette.length
|
||||
],
|
||||
duration: 920 + Math.floor(Math.random() * 320),
|
||||
delay: index * 40,
|
||||
rotate: -16 + Math.floor(Math.random() * 32),
|
||||
pathClass:
|
||||
pathClasses[
|
||||
(index + Math.floor(Math.random() * pathClasses.length)) %
|
||||
pathClasses.length
|
||||
],
|
||||
pulseClass: pulseClasses[index % pulseClasses.length],
|
||||
};
|
||||
},
|
||||
|
||||
removeHeartParticle(this: CommentInstance, id: string) {
|
||||
this.setData({
|
||||
heartParticles: this.data.heartParticles.filter(
|
||||
(item: any) => item.id !== id,
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
clearHeartEffects(this: CommentInstance) {
|
||||
if (this._heartButtonTimer) {
|
||||
clearTimeout(this._heartButtonTimer);
|
||||
this._heartButtonTimer = null;
|
||||
}
|
||||
|
||||
if (Array.isArray(this._heartTimers)) {
|
||||
this._heartTimers.forEach((timer) => clearTimeout(timer));
|
||||
this._heartTimers = [];
|
||||
}
|
||||
|
||||
this.setData({
|
||||
heartParticles: [],
|
||||
heartButtonActive: false,
|
||||
});
|
||||
},
|
||||
|
||||
safeVibrate() {
|
||||
if (typeof wx.vibrateShort !== "function") return;
|
||||
|
||||
try {
|
||||
wx.vibrateShort();
|
||||
} catch (error) {
|
||||
console.warn("vibrateShort failed", error);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
<view class="comment">
|
||||
<view wx:if="{{featureState.topTicker && topNoticeVisible}}" class="top-notice-layer">
|
||||
<view class="top-notice-pill">
|
||||
<text class="top-notice-tag">LIVE</text>
|
||||
<text class="top-notice-text">{{topNoticeText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
scroll-y="true"
|
||||
class="comment-container"
|
||||
id="scrollView"
|
||||
style="height: {{maxVisible * itemHeight + 24 * (maxVisible - 1)}}rpx"
|
||||
bindscroll="onScroll"
|
||||
scroll-with-animation="true"
|
||||
scroll-top="{{scrollTop}}"
|
||||
>
|
||||
<view id="list" class="list-wrapper">
|
||||
<view wx:for="{{displayList}}" wx:key="id">
|
||||
<view class="msg-item {{item.kind === 'system' ? 'is-system' : ''}}">
|
||||
<text class="msg-item-name" wx:if="{{item.name}}">{{item.name}}</text>
|
||||
<text class="msg-item-text" wx:if="{{item.msg || item.msg === ''}}">{{item.msg}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view
|
||||
wx:if="{{featureState.unreadReminder && unreadMessageCount > 0}}"
|
||||
class="unread-reminder"
|
||||
bindtap="handleUnreadReminderTap">
|
||||
<view class="unread-reminder-dot"></view>
|
||||
<text class="unread-reminder-text">{{unreadMessageCount}} 条新消息</text>
|
||||
</view>
|
||||
|
||||
<view class="input-shell {{featureState.optimizedInput ? 'is-enhanced' : ''}} {{inputFocused ? 'is-focused' : ''}}">
|
||||
<view wx:if="{{featureState.optimizedInput}}" class="input-leading">
|
||||
<view class="input-leading-dot"></view>
|
||||
</view>
|
||||
|
||||
<input
|
||||
value="{{inputValue}}"
|
||||
confirm-type="send"
|
||||
bindinput="handleInput"
|
||||
bindconfirm="handleSubmit"
|
||||
bindfocus="handleInputFocus"
|
||||
bindblur="handleInputBlur"
|
||||
class="msg-input"
|
||||
cursor-spacing="18"
|
||||
placeholder="说点什么吧~"
|
||||
placeholder-style="{{inputPlaceholderStyle}}"
|
||||
/>
|
||||
|
||||
<view
|
||||
wx:if="{{featureState.optimizedInput}}"
|
||||
class="input-send {{canSubmit ? 'is-ready' : ''}}"
|
||||
bindtap="submitCurrentInput"
|
||||
>
|
||||
发送
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="action-bar">
|
||||
<view class="action-btn" hover-class="action-btn-hover" bindtap="openGuidePopup">
|
||||
<image class="action-icon" src="./icons/gallery.svg" mode="aspectFit" />
|
||||
</view>
|
||||
|
||||
<button class="action-btn action-share" hover-class="action-btn-hover" open-type="share">
|
||||
<image class="action-icon" src="./icons/share.svg" mode="aspectFit" />
|
||||
</button>
|
||||
|
||||
<view
|
||||
class="action-btn action-btn-heart {{heartButtonActive ? 'is-active' : ''}}"
|
||||
hover-class="action-btn-hover"
|
||||
bindtap="handleHeartTap"
|
||||
>
|
||||
<image class="action-icon action-icon-heart" src="./icons/heart.svg" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="heart-layer">
|
||||
<view
|
||||
wx:for="{{heartParticles}}"
|
||||
wx:key="id"
|
||||
class="heart-particle {{item.pathClass}}"
|
||||
style="right: {{item.right}}rpx; bottom: {{item.bottom}}rpx; animation-duration: {{item.duration}}ms; animation-delay: {{item.delay}}ms;"
|
||||
>
|
||||
<image class="heart-fly-icon {{item.pulseClass}}" src="./icons/heart.svg" mode="aspectFit" />
|
||||
<text
|
||||
class="heart-glyph {{item.pulseClass}}"
|
||||
style="font-size: {{item.size}}rpx; color: {{item.color}}; transform: rotate({{item.rotate}}deg);"
|
||||
>❤</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<guide-popup
|
||||
show="{{showGuidePopup}}"
|
||||
title="{{guideTitle}}"
|
||||
subtitle="{{guideSubtitle}}"
|
||||
images="{{guideImages}}"
|
||||
bind:close="closeGuidePopup"
|
||||
/>
|
||||
</view>
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="17" y="22" width="62" height="52" rx="12" stroke="#FFFFFF" stroke-width="6"/>
|
||||
<circle cx="63" cy="38" r="6" fill="#FFFFFF"/>
|
||||
<path d="M27 62L41 48L52 58L61 49L69 58V66H27V62Z" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 311 B |
@@ -0,0 +1,9 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M48 80C46.1 80 44.3 79.3 42.8 78L22.3 59.2C10.2 48.1 9.1 29.4 19.8 17.9C30.4 6.5 48 5.8 59.4 16.3L62 18.7L64.6 16.3C76 5.8 93.6 6.5 104.2 17.9C114.9 29.4 113.8 48.1 101.7 59.2L81.2 78C79.7 79.3 77.9 80 76 80C74.1 80 72.3 79.3 70.8 78L62 69.9L53.2 78C51.7 79.3 49.9 80 48 80Z" fill="url(#paint0_linear_420_7)" transform="translate(-14 0)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_420_7" x1="48" y1="10" x2="48" y2="80" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF8AA1"/>
|
||||
<stop offset="1" stop-color="#FF325C"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 685 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="28" cy="50" r="10" fill="#FFFFFF"/>
|
||||
<circle cx="66" cy="30" r="10" fill="#FFFFFF"/>
|
||||
<circle cx="68" cy="68" r="10" fill="#FFFFFF"/>
|
||||
<path d="M37 45L57 34" stroke="#FFFFFF" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M37 55L58 64" stroke="#FFFFFF" stroke-width="6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 421 B |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"popup": "/components/popup/popup"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/* components/static/coupon-popup/coupon-popup.wxss */
|
||||
.popup-header {
|
||||
font-size: 36rpx;
|
||||
color: #FA6905;
|
||||
font-weight: 600;
|
||||
padding-bottom: 12rpx;
|
||||
border-bottom: 1rpx solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.list-container {
|
||||
margin-top: 24rpx;
|
||||
position: relative;
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 2px 8px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
gap: 24rpx;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
|
||||
.default {
|
||||
background: linear-gradient(135deg, rgb(255, 107, 107) 0%, rgb(255, 142, 83) 100%);
|
||||
padding: 32rpx;
|
||||
border-radius: 24rpx;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
min-height: 240rpx;
|
||||
|
||||
.default-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
|
||||
.amount {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.min-price {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
height: 76rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.85;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.default-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.id {
|
||||
opacity: 0;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.large {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
gap: 32rpx;
|
||||
color: #fff;
|
||||
// height: 160rpx;
|
||||
background: linear-gradient(135deg, rgb(255, 107, 107) 0%, rgb(255, 142, 83) 100%);
|
||||
padding: 32rpx;
|
||||
border-radius: 24rpx;
|
||||
|
||||
.large-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
|
||||
.amount {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.min-price {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.large-center {
|
||||
width: 1px;
|
||||
background: white;
|
||||
margin: 0rpx;
|
||||
}
|
||||
|
||||
.large-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
gap: 8rpx;
|
||||
width: 100%;
|
||||
|
||||
.large-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
font-size: 30rpx;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.85;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.large-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.id {
|
||||
opacity: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.large-button {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: none;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
min-width: 140rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 24rpx;
|
||||
line-height: 48rpx;
|
||||
font-size: 28rpx;
|
||||
margin: 0;
|
||||
color: #FA6905;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.no-use{
|
||||
width: 100%;
|
||||
background-color:rgb(255, 125, 95);
|
||||
border-radius:24rpx;
|
||||
margin-top:24rpx;
|
||||
height: 70rpx;
|
||||
line-height: 70rpx;
|
||||
text-align: center;
|
||||
font-weight: 800;
|
||||
font-size: 38rpx;
|
||||
color: #fff;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// components/static/coupon-popup/coupon-popup.ts
|
||||
import { request } from "../../../utils/request";
|
||||
const app = getApp<IAppOption>();
|
||||
|
||||
Component({
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
hasTabBar: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
visible: false,
|
||||
couponList: [],
|
||||
safeBottom: app.globalData.safeBottom,
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
async handleSwitch(amount?: number) {
|
||||
const that = this as any;
|
||||
if (amount) {
|
||||
|
||||
try {
|
||||
const res = await request({
|
||||
options: {
|
||||
url: "/app/mini-app/user-coupon",
|
||||
data: {
|
||||
amount,
|
||||
mbuId: wx.getStorageSync('mbuId')
|
||||
}
|
||||
},
|
||||
needLogin: true
|
||||
})
|
||||
if (!res || !res.data || res.data.length == 0) {
|
||||
// wx.showToast({
|
||||
// title: "没有可使用的优惠券",
|
||||
// icon: "none",
|
||||
// duration: 2000
|
||||
// })
|
||||
that.triggerEvent('select', {
|
||||
coupon: null
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
if (res.data.length > 0) res.data.forEach((item: any) => {
|
||||
item.discountPrice = (item.discountPrice / 100).toFixed(2);
|
||||
item.usePrice = item.usePrice / 100;
|
||||
item.validStartTime = item.validStartTime.replace("T", " ");
|
||||
item.validEndTime = item.validEndTime.replace("T", " ");
|
||||
}); //金额单位为分
|
||||
that.setData({ couponList: res.data, visible: true })
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e, 'error');
|
||||
wx.showToast({
|
||||
title: "查询优惠券信息出错了!",
|
||||
icon: "none",
|
||||
duration: 2000
|
||||
})
|
||||
that.setData({
|
||||
visible: false,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
that.setData({
|
||||
visible: false,
|
||||
})
|
||||
}
|
||||
},
|
||||
//选择优惠券
|
||||
handleSelect(e: any) {
|
||||
const that = this as any;
|
||||
const index = e.currentTarget.dataset.index;
|
||||
const coupon = that.data.couponList[index];
|
||||
|
||||
that.setData({ visible: false });
|
||||
|
||||
that.triggerEvent('select', {
|
||||
couponId: coupon?.couponIds?.[0] ?? coupon?.id ?? null,
|
||||
coupon
|
||||
});
|
||||
},
|
||||
|
||||
handleClose(this: WechatMiniprogram.Component.TrivialInstance) {
|
||||
this.setData({ visible: false });
|
||||
this.triggerEvent("select", { couponId: null });
|
||||
},
|
||||
closePopup(this: WechatMiniprogram.Component.TrivialInstance) {
|
||||
this.setData({ visible: false });
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
<!--components/static/coupon-popup/coupon-popup.wxml-->
|
||||
<popup visible="{{visible}}" bind:close="closePopup">
|
||||
<view slot="header" class="popup-header">
|
||||
选择优惠券
|
||||
</view>
|
||||
|
||||
<view class="list-container">
|
||||
<view wx:for="{{couponList}}" wx:key="name" class="large" data-index="{{index}}">
|
||||
<view class="large-left">
|
||||
<view class="amount">{{'¥'+item.discountPrice}}</view>
|
||||
<view class="min-price">满{{item.usePrice}}元可用</view>
|
||||
<view class="min-price">剩余{{item.count}}张</view>
|
||||
</view>
|
||||
<view class="large-center"></view>
|
||||
<view class="large-right">
|
||||
<view class="large-title">
|
||||
<tag propStyle="background: rgba(0, 0, 0, 0.2);border:none;color:white;">
|
||||
{{item.discountType===1?'代金券':'折扣券'}}
|
||||
</tag>
|
||||
<view class="name">{{item.name}}</view>
|
||||
</view>
|
||||
<view class="desc">优惠开始时间:{{item.validStartTime}}</view>
|
||||
<view class="desc">优惠结束时间:{{item.validEndTime}}</view>
|
||||
<div class="large-bottom">
|
||||
<div class="id">ID:{{item.couponIds}}</div>
|
||||
<button class="button large-button" data-index="{{index}}" bind:tap="handleSelect">选择</button>
|
||||
</div>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="no-use" bind:tap="handleSelect">
|
||||
不使用优惠券
|
||||
</view>
|
||||
<view style="height:{{hasTabBar? safeBottom/2 + 90 : safeBottom}}rpx;"></view>
|
||||
</popup>
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"tag":"/components/tag/tag",
|
||||
"empty": "/components/empty/empty"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/* components/static/coupon-record/coupon-record.wxss */
|
||||
.list-container {
|
||||
position: relative;
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 2px 8px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
gap: 24rpx;
|
||||
background: rgba(0,0,0,0);
|
||||
.default {
|
||||
background: linear-gradient(135deg, rgb(255, 107, 107) 0%, rgb(255, 142, 83) 100%);
|
||||
padding: 32rpx;
|
||||
border-radius: 24rpx;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
min-height: 240rpx;
|
||||
|
||||
.default-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
|
||||
.amount {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.min-price {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
height: 76rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.85;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.default-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.id {
|
||||
opacity: 0;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.large {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
gap: 32rpx;
|
||||
color: #fff;
|
||||
// height: 160rpx;
|
||||
background: linear-gradient(135deg, rgb(255, 107, 107) 0%, rgb(255, 142, 83) 100%);
|
||||
padding: 32rpx;
|
||||
border-radius: 24rpx;
|
||||
|
||||
.large-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap:8rpx;
|
||||
.amount {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.min-price {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.large-center {
|
||||
width: 1px;
|
||||
background: white;
|
||||
margin: 0rpx;
|
||||
}
|
||||
|
||||
.large-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
gap: 8rpx;
|
||||
width: 100%;
|
||||
|
||||
.large-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
font-size: 30rpx;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.85;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.large-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.id {
|
||||
opacity: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.large-button {
|
||||
margin-top:8rpx;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: none;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
min-width: 140rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 24rpx;
|
||||
line-height: 48rpx;
|
||||
font-size: 28rpx;
|
||||
margin: 0;
|
||||
color:#FA6905;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { request } from "../../../utils/request";
|
||||
import { shopId } from '../../../env';
|
||||
// components/static/coupon-record/coupon-record.ts
|
||||
Component({
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
couponList: []
|
||||
},
|
||||
pageLifetimes: {
|
||||
show(this: WechatMiniprogram.Component.TrivialInstance) {
|
||||
this.search();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
async search(this: WechatMiniprogram.Component.TrivialInstance) {
|
||||
try {
|
||||
const res = await request({
|
||||
options: {
|
||||
url: "/app/mini-app/user-coupon",
|
||||
data: {
|
||||
mbuId: wx.getStorageSync("mbuId"),
|
||||
shopId: shopId
|
||||
}
|
||||
},
|
||||
needLogin: true
|
||||
})
|
||||
if (res.data.length > 0) res.data.forEach((item: any) => {
|
||||
item.discountPrice = (item.discountPrice / 100).toFixed(2);
|
||||
item.usePrice = item.usePrice / 100;
|
||||
item.validStartTime = item.validStartTime.replace("T", " ");
|
||||
item.validEndTime = item.validEndTime.replace("T", " ");
|
||||
}); //金额单位为分
|
||||
this.setData({ couponList: res.data })
|
||||
} catch (e) {
|
||||
console.log(e, 'error');
|
||||
wx.showToast({
|
||||
title: "获取优惠券列表失败",
|
||||
icon: "none",
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
},
|
||||
handleClick(this: WechatMiniprogram.Component.TrivialInstance) {
|
||||
wx.switchTab({
|
||||
url: "/pages/tab-bar/medicine-box/index"
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
<!--components/static/coupon-record/coupon-record.wxml-->
|
||||
<view class="list-container">
|
||||
<!-- <view wx:for="{{couponList}}" wx:key="name" class="default">
|
||||
<tag propStyle="background: rgba(0, 0, 0, 0.2);border:none;color:white;">
|
||||
{{item.discountType===1?'代金券':'折扣券'}}
|
||||
</tag>
|
||||
<view class="default-content">
|
||||
<view class="amount">{{'¥'+item.discountPrice}}</view>
|
||||
<view class="min-price">满{{item.usePrice}}元可用</view>
|
||||
<view class="min-price">拥有{{item.count}}张</view>
|
||||
<view class="name">{{item.name}}</view>
|
||||
<view class="min-price">优惠开始时间:{{item.validStartTime}}</view>
|
||||
<view class="min-price">优惠结束时间:{{item.validEndTime}}</view>
|
||||
|
||||
</view>
|
||||
<view class="default-bottom">
|
||||
<view class="id">ID:{{item.id}}</view>
|
||||
<button class="button" data-id="{{item.id}}" bind:tap="handleClick">去使用</button>
|
||||
</view>
|
||||
</view> -->
|
||||
<view wx:for="{{couponList}}" wx:key="name" class="large">
|
||||
<view class="large-left">
|
||||
<view class="amount">{{'¥'+item.discountPrice}}</view>
|
||||
<view class="min-price">满{{item.usePrice}}元可用</view>
|
||||
<view class="min-price">剩余{{item.count}}张</view>
|
||||
</view>
|
||||
<view class="large-center"></view>
|
||||
<view class="large-right">
|
||||
<view class="large-title">
|
||||
<tag propStyle="background: rgba(0, 0, 0, 0.2);border:none;color:white;">
|
||||
{{item.discountType===1?'代金券':'折扣券'}}
|
||||
</tag>
|
||||
<view class="name">{{item.name}}</view>
|
||||
</view>
|
||||
<view class="desc">优惠开始时间:{{item.validStartTime}}</view>
|
||||
<view class="desc">优惠结束时间:{{item.validEndTime}}</view>
|
||||
<div class="large-bottom">
|
||||
<div class="id">ID:{{item.id}}</div>
|
||||
<button class="button large-button" data-id="{{item.id}}" bind:tap="handleClick">去使用</button>
|
||||
</div>
|
||||
</view>
|
||||
</view>
|
||||
<empty wx:if="{{!couponList||couponList.length===0}}"></empty>
|
||||
</view>
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"popup": "/components/popup/popup",
|
||||
"van-stepper": "@vant/weapp/stepper/index",
|
||||
"sku-item":"/components/static/sku-item/sku-item",
|
||||
"coupon-popup":"../coupon-popup/coupon-popup"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/* components/static/good-detail-popup/good-detail-popup.wxss */
|
||||
@import "variables";
|
||||
|
||||
.popup-header {
|
||||
.location {
|
||||
min-height: 60rpx;
|
||||
width: 100%;
|
||||
color: $primary-color;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
// align-items:end;
|
||||
border-bottom: 1rpx solid rgba(0, 0, 0, 0.1);
|
||||
.icon {
|
||||
min-width: 40rpx;
|
||||
line-height: 60rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
.address{
|
||||
padding-bottom: $primary-gap;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
margin-top: $primary-gap;
|
||||
|
||||
.left {
|
||||
border-radius: $primary-border-radius;
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
overflow: hidden;
|
||||
margin-right: $primary-gap;
|
||||
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
;
|
||||
|
||||
.price {
|
||||
color: $primary-color;
|
||||
font-size: $x-large-font-size;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.count {
|
||||
width: 400rpx;
|
||||
}
|
||||
|
||||
.stock {
|
||||
font-size: $large-font-size;
|
||||
color: $gray-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sku-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// max-height: 500rpx;
|
||||
// margin-bottom: 54rpx;
|
||||
.skuTitle {
|
||||
font-size: $large-font-size;
|
||||
font-weight: 600;
|
||||
margin: $primary-gap 0rpx;
|
||||
}
|
||||
.scroll {
|
||||
max-height: 500rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
// gap: $primary-gap;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 702rpx;
|
||||
|
||||
.submit {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
background: $primary-color;
|
||||
color: white;
|
||||
font-size: $large-font-size;
|
||||
font-weight: 600;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
border-radius: 0rpx $primary-border-radius $primary-border-radius 0rpx;
|
||||
}
|
||||
.submit1 {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
background: $primary-color;
|
||||
color: white;
|
||||
font-size: $large-font-size;
|
||||
font-weight: 600;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.add-cart {
|
||||
background-color: #FEB30C;
|
||||
border-radius: $primary-border-radius 0rpx 0rpx $primary-border-radius ;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
// components/static/good-detail-popup/good-detail-popup.ts
|
||||
import { request } from "../../../utils/request";
|
||||
import { getAddress } from "../../../utils/get-address";
|
||||
import { appId, shopId } from "../../../env";
|
||||
import { wxRequestSubscribe } from "../../../utils/wx-api";
|
||||
// @ts-ignore
|
||||
const systemInfo = wx.getWindowInfo();
|
||||
const app = getApp<IAppOption>();
|
||||
Component({
|
||||
properties: {
|
||||
mode: {
|
||||
// one: 单规格, list: 多规格
|
||||
type: String,
|
||||
value: "one",
|
||||
},
|
||||
skuId: Number,
|
||||
spuId: Number,
|
||||
hasTabBar: Boolean,
|
||||
showShopCart: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
joinedGroupI9d: {
|
||||
type: String,
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
goodList: {},
|
||||
showPopup: false,
|
||||
swiperList: [],
|
||||
count: 1,
|
||||
checkItem: null,
|
||||
skus: [],
|
||||
addressId: null,
|
||||
defualtAddress: "未知地址",
|
||||
safeBottom: getApp().globalData.safeBottom,
|
||||
showStock: app.globalData.showStock,
|
||||
selectedCoupon: null //选择的优惠券
|
||||
},
|
||||
|
||||
methods: {
|
||||
async openPopup(this: WechatMiniprogram.Component.TrivialInstance) {
|
||||
await this.handleGetInfo();
|
||||
this.setData({ showPopup: true });
|
||||
},
|
||||
|
||||
closePopup(this: WechatMiniprogram.Component.TrivialInstance) {
|
||||
this.setData({ showPopup: false });
|
||||
},
|
||||
|
||||
async handleGetInfo(this: WechatMiniprogram.Component.TrivialInstance) {
|
||||
await getAddress(this);
|
||||
|
||||
let api = "";
|
||||
let params = {};
|
||||
|
||||
if (this.data.mode === "one") {
|
||||
api = "/app/shop-store/sku-one";
|
||||
params = { id: Number(this.data.skuId) };
|
||||
} else {
|
||||
api = "/app/shop-store/sku-list";
|
||||
params = { id: Number(this.data.spuId) };
|
||||
}
|
||||
|
||||
try {
|
||||
const res: any = await request({
|
||||
options: { url: api, data: params },
|
||||
loadingTitle: "获取商品信息中",
|
||||
needLogin: false,
|
||||
});
|
||||
|
||||
let swiperList: string[] = [];
|
||||
let checkItem = null;
|
||||
|
||||
if (this.data.mode === "one") {
|
||||
// 单规格
|
||||
swiperList = res.sliderPicUrls?.filter(Boolean) || [res.picUrl];
|
||||
checkItem = res;
|
||||
} else {
|
||||
// 多规格
|
||||
swiperList = res.sliderPicUrls?.filter(Boolean);
|
||||
|
||||
if ((!swiperList || !swiperList.length) && res.skus) {
|
||||
swiperList = res.skus.map((s: any) => s.picUrl ?? s.picUrl);
|
||||
}
|
||||
|
||||
// 找到默认可售SKU
|
||||
checkItem = res.skus.find((s: any) => s.stock > 0) || res.skus[0];
|
||||
|
||||
// 给goodList添加最低价
|
||||
const min = res.skus.reduce((p: any, c: any) =>
|
||||
c.price < p.price ? c : p
|
||||
);
|
||||
res.price = min.price;
|
||||
}
|
||||
|
||||
this.setData({
|
||||
goodList: res,
|
||||
skus: this.data.mode === "one" ? [res] : res.skus,
|
||||
swiperList,
|
||||
checkItem,
|
||||
skuId: checkItem.skuId,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
wx.showToast({ icon: "none", title: "商品信息获取失败" });
|
||||
} finally {
|
||||
wx.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
/** SKU 切换 */
|
||||
onSetSkuId(e: any) {
|
||||
const that = this as any;
|
||||
const checkItem = e.detail.checkItem;
|
||||
if (that.data.checkItem?.skuId === checkItem.skuId) {
|
||||
that.setData({ checkItem: null, skuId: null });
|
||||
return;
|
||||
}
|
||||
if (checkItem.stock < 1) {
|
||||
return wx.showToast({ icon: "none", title: "库存不足" });
|
||||
}
|
||||
that.setData({ checkItem, skuId: checkItem.skuId });
|
||||
},
|
||||
|
||||
onCountChange(e: any) {
|
||||
const that = this as any;
|
||||
that.setData({ count: e.detail });
|
||||
},
|
||||
|
||||
/** 下单 */
|
||||
async handleSubmit() {
|
||||
const that = this as any;
|
||||
if (!app.redirectLogin()) return;
|
||||
if (!that.data.checkItem) {
|
||||
return wx.showToast({ title: "未选择商品!", icon: "none", duration: 2000 });
|
||||
}
|
||||
if (that.data.checkItem.price == 0) return wx.showToast({ title: "暂无报价", icon: "none", duration: 3000 });
|
||||
if (that.data.count > that.data.checkItem.stock) {
|
||||
if (!that.data.showStock) {
|
||||
return wx.showToast({ title: "暂无报价", icon: "none", duration: 2000 });
|
||||
}
|
||||
return wx.showToast({ title: "库存不足!", icon: "none", duration: 2000 });
|
||||
}
|
||||
if (!that.data.addressId || !that.data.defualtAddress) {
|
||||
wx.showToast({ title: "请先填写地址!", icon: "none", duration: 2000 });
|
||||
wx.navigateTo({ url: "/pages/common/location/location" })
|
||||
return
|
||||
}
|
||||
const value = {
|
||||
addressId: that.data.addressId,
|
||||
userId: wx.getStorageSync("userId"),
|
||||
mbuId: wx.getStorageSync("mbuId"),
|
||||
appId,
|
||||
goods: [{ id: that.data.skuId, count: that.data.count }],
|
||||
shopId,
|
||||
type: 1
|
||||
} as any;
|
||||
if (that.data.joinedGroupId) value.roomId = that.data.joinedGroupId;
|
||||
if (that.data.selectedCoupon) {
|
||||
value.couponIds = that.data.selectedCoupon.couponIds;
|
||||
}
|
||||
let subscribeTmplIds = [] as any[];
|
||||
try {
|
||||
const subscribe = await wxRequestSubscribe(['3gO2ryTGiXvUWhMgjMH8il3CyBrYmPaO0AR_gwsx69M', 'ts3RBa4gtlEEAtPomEyr4l1Bt1C8PAjbD7-jvtpddkg']) as any;
|
||||
subscribeTmplIds = Object.entries(subscribe)
|
||||
.filter(([id, status]) => status === 'accept')
|
||||
.map(([id]) => id);
|
||||
if (subscribeTmplIds.length > 0) {
|
||||
value.subscribeTmplIds = subscribeTmplIds;
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.log(e, '下单失败')
|
||||
}
|
||||
try {
|
||||
wx.showLoading({ title: "下单中" });
|
||||
const res: any = await request({
|
||||
options: { url: "/app/app-pay/js-api", data: value },
|
||||
});
|
||||
|
||||
wx.requestPayment({
|
||||
...res.data,
|
||||
package: res.data.packageVal,
|
||||
success: () => {
|
||||
wx.switchTab({ url: "/pages/tab-bar/user/user" });
|
||||
wx.showToast({ title: "支付成功" });
|
||||
},
|
||||
fail() {
|
||||
wx.showToast({ title: "支付取消", icon: "none" });
|
||||
},
|
||||
complete: () => wx.hideLoading(),
|
||||
});
|
||||
} catch (err) {
|
||||
wx.hideLoading();
|
||||
console.log(err, '下单失败')
|
||||
wx.showToast({ title: "下单失败", icon: "none" });
|
||||
}
|
||||
},
|
||||
|
||||
// 选优惠券下单
|
||||
async handleBeforeSubmit() {
|
||||
const that = this as any;
|
||||
if (!app.redirectLogin()) return;
|
||||
if (!that.data.checkItem) {
|
||||
return wx.showToast({ title: "未选择商品!", icon: "none", duration: 2000 });
|
||||
}
|
||||
if (that.data.checkItem.price == 0) return wx.showToast({ title: "暂无报价", icon: "none", duration: 3000 });
|
||||
if (that.data.count > that.data.checkItem.stock) {
|
||||
if (!that.data.showStock) {
|
||||
return wx.showToast({ title: "暂无报价", icon: "none", duration: 2000 });
|
||||
}
|
||||
return wx.showToast({ title: "库存不足!", icon: "none", duration: 2000 });
|
||||
}
|
||||
if (!that.data.addressId || !that.data.defualtAddress) {
|
||||
wx.showToast({ title: "请先填写地址!", icon: "none", duration: 2000 });
|
||||
wx.navigateTo({ url: "/pages/common/location/location" })
|
||||
return
|
||||
}
|
||||
|
||||
const amount = that.data.checkItem.price * that.data.count;
|
||||
|
||||
// 打开优惠券弹窗
|
||||
that.selectComponent("#couponPopup").handleSwitch(amount);
|
||||
},
|
||||
|
||||
//选择优惠券后
|
||||
onCouponSelect(e: any) {
|
||||
const that = this as any;
|
||||
const { coupon } = e.detail;
|
||||
if (coupon) {
|
||||
that.setData({ selectedCoupon: coupon });
|
||||
} else {
|
||||
that.setData({ selectedCoupon: null });
|
||||
}
|
||||
if (that.data.selectedCoupon !== null) {
|
||||
wx.showToast({
|
||||
title: "已使用优惠券",
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
// 继续下单
|
||||
this.handleSubmit();
|
||||
},
|
||||
|
||||
/** 加购物车 */
|
||||
handleAddCart() {
|
||||
const that = this as any;
|
||||
if (!app.redirectLogin()) return;
|
||||
|
||||
const item = that.data.checkItem;
|
||||
if (!item) return wx.showToast({ title: "未选择商品", icon: "none", duration: 3000 });
|
||||
if (item.price == 0) return wx.showToast({ title: "暂无报价", icon: "none", duration: 3000 });
|
||||
if (that.data.count > item.stock) {
|
||||
if (!that.data.showStock) {
|
||||
return wx.showToast({ title: "暂无报价", icon: "none", duration: 2000 });
|
||||
}
|
||||
return wx.showToast({ title: "库存不足", icon: "none", duration: 3000 });
|
||||
}
|
||||
|
||||
let all = wx.getStorageSync("shoppingCart") || [];
|
||||
const mbuId = wx.getStorageSync("mbuId");
|
||||
|
||||
let cart = all.find((i: any) => i.mbuId == mbuId);
|
||||
if (!cart) {
|
||||
cart = { mbuId, data: [] };
|
||||
all.push(cart);
|
||||
}
|
||||
|
||||
const exist = cart.data.find((i: any) => i.skuId == item.skuId);
|
||||
if (exist) {
|
||||
exist.count += that.data.count;
|
||||
if (exist.count > item.stock) {
|
||||
return wx.showToast({ title: "超过库存限制", icon: "none" });
|
||||
}
|
||||
} else {
|
||||
cart.data.push({ ...item, count: that.data.count });
|
||||
}
|
||||
|
||||
wx.setStorageSync("shoppingCart", all);
|
||||
wx.showToast({ title: "加入购物车成功" });
|
||||
},
|
||||
handleSelcetLocation() {
|
||||
if (!app.redirectLogin()) return;
|
||||
wx.navigateTo({
|
||||
url: "/pages/common/location/location",
|
||||
});
|
||||
},
|
||||
},
|
||||
pageLifetimes: {
|
||||
show() {
|
||||
const that = this as any;
|
||||
if (that.data.showPopup === true) getAddress(this);
|
||||
},
|
||||
},
|
||||
lifetimes: {
|
||||
attached: function () {
|
||||
getAddress(this);
|
||||
},
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
<!--components/static/good-detail-popup/good-detail-popup.wxml-->
|
||||
<popup visible="{{showPopup}}" bind:close="closePopup">
|
||||
<view slot="header" class="popup-header">
|
||||
<view class="location">
|
||||
<image src="/assets/img/location.svg" class="icon"></image>
|
||||
<view class="address" bind:tap="handleSelcetLocation">{{defualtAddress}}</view>
|
||||
</view>
|
||||
<view class="info">
|
||||
<view class="left">
|
||||
<image src="{{checkItem ? checkItem.picUrl : skus[0].picUrl}}" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="right">
|
||||
<view wx:if="skuId" class="price">
|
||||
{{ checkItem ? checkItem.price?'¥'+checkItem.price/100:'暂无报价' : skus[0].price?'¥'+skus[0].price/100:'暂无报价' }}
|
||||
</view>
|
||||
<!-- 如果不显示库存的话最多可以加到99999去,显示的话就最多可以加到库存量去 -->
|
||||
<van-stepper integer max="{{showStock?checkItem ? checkItem.stock : skus[0].stock :99999}}" value="{{ count }}"
|
||||
bind:change="onCountChange" />
|
||||
<view class="stock" wx:if="{{showStock}}">库存 {{ checkItem ? checkItem.stock : skus[0].stock }}
|
||||
{{checkItem?checkItem.unit:goodList[0].stock}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="sku-container" style="padding-bottom: {{hasTabBar?safeBottom+ 80 + 120 + 24 :safeBottom + 80+24}}rpx;">
|
||||
<view class="skuTitle">商品分类 ({{skus.length}})</view>
|
||||
<scroll-view scroll-y class="scroll">
|
||||
<view wx:for="{{skus}}" wx:key="skuId">
|
||||
<sku-item active="{{(skuId && skuId==item.skuId)?true:false}}" disable="{{item.stock===0?true:false}}"
|
||||
bind:selectsku="onSetSkuId" item="{{item}}" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="footer">
|
||||
<view bind:tap="handleAddCart" class="submit add-cart" wx:if="{{show}}">
|
||||
加入购物车
|
||||
</view>
|
||||
<view class="footer">
|
||||
<view bind:tap="handleAddCart" wx:if="{{showShopCart}}" class="submit add-cart">
|
||||
加入购物车
|
||||
</view>
|
||||
<!-- bind:tap="handleSubmit" -->
|
||||
<view bind:tap="handleBeforeSubmit" class="submit"
|
||||
style="margin-bottom:{{hasTabBar? safeBottom/2 + 140 : safeBottom}}rpx;border-top-left-radius: {{showShopCart?0:24}}rpx;border-Bottom-left-radius: {{showShopCart?0:24}}rpx;">
|
||||
{{ skuId ? ('立即支付 ¥' + (checkItem.price * count / 100)) : '选择商品' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</popup>
|
||||
|
||||
<coupon-popup
|
||||
id="couponPopup"
|
||||
bind:select="onCouponSelect"
|
||||
/>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
@import "variables";
|
||||
|
||||
.good-container {
|
||||
// 宽:(750rpx - 左右边距 24*2rpx - 中间边距 24rpx)/ 2
|
||||
width: 339rpx;
|
||||
//高 为宽的1.5倍 (3比2)
|
||||
height: 509rpx;
|
||||
overflow: hidden;
|
||||
border-radius: $primary-border-radius;
|
||||
background: white;
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 339rpx;
|
||||
border-radius: $primary-border-radius;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0rpx 12rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.title {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
width: 100%;
|
||||
line-height: 55rpx;
|
||||
height: 55rpx;
|
||||
font-size: $large-font-size;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.desc {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
color: $primary-color;
|
||||
line-height: 45rpx;
|
||||
height: 45rpx;
|
||||
}
|
||||
|
||||
.count {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
line-height: 45rpx;
|
||||
height: 45rpx;
|
||||
color: $primary-color;
|
||||
|
||||
.price {
|
||||
|
||||
.money {
|
||||
font-size: $large-font-size;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.quantity {
|
||||
color: black;
|
||||
margin-right: $primary-gap / 2;
|
||||
// font-size: $small-font-size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
const app = getApp<IAppOption>()
|
||||
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
item: {
|
||||
type: Object,
|
||||
value: {}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
handleClick(e: any) {
|
||||
wx.navigateTo({
|
||||
url: `/pages/common/good-detail/good-detail?spuId=${e.currentTarget.dataset.id}`
|
||||
})
|
||||
}
|
||||
},
|
||||
lifetimes: {
|
||||
created: () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
<!--components/good-item/good-item.wxml-->
|
||||
<view class="good-container" bindtap="handleClick" data-id="{{item.spuId}}">
|
||||
<image class="image" src="{{item.picUrl}}" mode="aspectFill"></image>
|
||||
<view class="content">
|
||||
<view class="title">{{item.name}}</view>
|
||||
<view class="desc">{{item.introduction}}</view>
|
||||
<view class="count">
|
||||
<view class="price"><span class="money">{{item.price?'¥'+item.price/100:'暂无报价'}}</span></view>
|
||||
<view class="quantity">已售 {{item.salesCount}} +</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
.guide-popup {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 40;
|
||||
|
||||
.guide-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(10, 13, 24, 0.42);
|
||||
backdrop-filter: blur(10rpx);
|
||||
opacity: 0;
|
||||
transition: opacity 0.28s ease;
|
||||
}
|
||||
|
||||
.guide-shell {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
transform: translateY(calc(100% + 48rpx));
|
||||
transition: transform 0.28s ease;
|
||||
will-change: transform;
|
||||
pointer-events: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.guide-drag-bar {
|
||||
width: 88rpx;
|
||||
height: 8rpx;
|
||||
margin: 0 auto 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.guide-avatar-wrap {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: -40rpx;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2;
|
||||
width: 132rpx;
|
||||
height: 132rpx;
|
||||
padding: 8rpx;
|
||||
border-radius: 50%;
|
||||
background: #edf0f7;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 18rpx 42rpx rgba(11, 18, 35, 0.14);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.guide-avatar {
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
border-radius: 50%;
|
||||
border: 6rpx solid rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 16rpx 40rpx rgba(0, 0, 0, 0.18);
|
||||
background: #f6f7fb;
|
||||
}
|
||||
|
||||
.guide-avatar-fallback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #ffd7dc 0%, #ff8ba2 100%);
|
||||
color: #ffffff;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.guide-panel {
|
||||
border-radius: 28rpx;
|
||||
padding: 82rpx 24rpx 28rpx;
|
||||
background: linear-gradient(180deg, #edf0f7 0%, #e7ebf4 100%);
|
||||
box-shadow: 0 28rpx 70rpx rgba(11, 18, 35, 0.18);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.guide-topbar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.guide-name {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
color: #2a2f3a;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.guide-entry {
|
||||
position: absolute;
|
||||
right: 5%;
|
||||
top: -30%;
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
color: #2f3442;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.guide-entry-icon {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
}
|
||||
|
||||
.guide-entry-text {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.guide-balance-card {
|
||||
margin-top: 26rpx;
|
||||
padding: 22rpx 20rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.44);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.guide-balance-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.guide-balance-label {
|
||||
color: #5f6675;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.guide-balance-value {
|
||||
margin-top: 10rpx;
|
||||
color: #ff285d;
|
||||
font-size: 54rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.guide-balance-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.guide-pill {
|
||||
min-width: 120rpx;
|
||||
padding: 0 22rpx;
|
||||
height: 58rpx;
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.74);
|
||||
color: #ff4774;
|
||||
font-size: 24rpx;
|
||||
line-height: 1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.guide-pill-primary {
|
||||
background: linear-gradient(135deg, #ff5d87 0%, #ff3365 100%);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.guide-grid {
|
||||
margin-top: 22rpx;
|
||||
padding: 18rpx 12rpx 4rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.32);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
row-gap: 28rpx;
|
||||
}
|
||||
|
||||
.guide-grid-item {
|
||||
min-height: 126rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.guide-grid-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.guide-grid-label {
|
||||
margin-top: 16rpx;
|
||||
color: #2f3442;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.is-visible {
|
||||
.guide-mask {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.guide-shell {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
type GuideMenuItem = {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
};
|
||||
|
||||
interface GuidePopupProperties {
|
||||
show: boolean;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
images: string[];
|
||||
amount: string;
|
||||
}
|
||||
|
||||
interface GuidePopupData {
|
||||
mounted: boolean;
|
||||
visible: boolean;
|
||||
displayName: string;
|
||||
displaySubtitle: string;
|
||||
avatarUrl: string;
|
||||
avatarLetter: string;
|
||||
balanceLabel: string;
|
||||
withdrawText: string;
|
||||
incomeText: string;
|
||||
menuItems: GuideMenuItem[];
|
||||
}
|
||||
|
||||
interface GuidePopupInstance
|
||||
extends WechatMiniprogram.Component.TrivialInstance {
|
||||
data: GuidePopupData;
|
||||
properties: GuidePopupProperties;
|
||||
_hideTimer?: number | null;
|
||||
}
|
||||
|
||||
const DEFAULT_NAME = "当前用户";
|
||||
const DEFAULT_ENTRY = "个人中心";
|
||||
const DEFAULT_BALANCE_LABEL = "可提现金额";
|
||||
const DEFAULT_WITHDRAW_TEXT = "立即提现";
|
||||
const DEFAULT_INCOME_TEXT = "收益明细";
|
||||
const HIDE_DELAY = 280;
|
||||
|
||||
const DEFAULT_MENU_ITEMS: GuideMenuItem[] = [
|
||||
{
|
||||
key: "orders",
|
||||
label: "我的订单",
|
||||
icon: "./icons/orders.svg",
|
||||
},
|
||||
{
|
||||
key: "prize-records",
|
||||
label: "中奖记录",
|
||||
icon: "./icons/gift.svg",
|
||||
},
|
||||
{
|
||||
key: "coupons",
|
||||
label: "我的优惠券",
|
||||
icon: "./icons/coupon.svg",
|
||||
},
|
||||
{
|
||||
key: "comment-board",
|
||||
label: "评论记录板",
|
||||
icon: "./icons/comment.svg",
|
||||
},
|
||||
{
|
||||
key: "nickname",
|
||||
label: "修改昵称",
|
||||
icon: "./icons/edit.svg",
|
||||
},
|
||||
{
|
||||
key: "complaint",
|
||||
label: "投诉",
|
||||
icon: "./icons/warning.svg",
|
||||
},
|
||||
];
|
||||
|
||||
function normalizeImages(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
|
||||
return value.filter(
|
||||
(item): item is string => typeof item === "string" && item.length > 0,
|
||||
);
|
||||
}
|
||||
|
||||
function syncViewModel(
|
||||
instance: GuidePopupInstance,
|
||||
titleText: string,
|
||||
subtitleText: string,
|
||||
imageList: string[],
|
||||
) {
|
||||
const name = titleText || wx.getStorageSync("userName") || DEFAULT_NAME;
|
||||
const subtitle = subtitleText || DEFAULT_ENTRY;
|
||||
const avatarUrl = imageList.length ? imageList[0] : "";
|
||||
const avatarLetter = String(name).trim().slice(0, 1).toUpperCase() || "U";
|
||||
|
||||
instance.setData({
|
||||
displayName: name,
|
||||
displaySubtitle: subtitle,
|
||||
avatarUrl,
|
||||
avatarLetter,
|
||||
});
|
||||
}
|
||||
|
||||
function clearHideTimer(instance: GuidePopupInstance) {
|
||||
if (!instance._hideTimer) return;
|
||||
|
||||
clearTimeout(instance._hideTimer);
|
||||
instance._hideTimer = null;
|
||||
}
|
||||
|
||||
function togglePopup(instance: GuidePopupInstance, show: boolean) {
|
||||
clearHideTimer(instance);
|
||||
|
||||
if (show) {
|
||||
if (instance.data.mounted) {
|
||||
wx.nextTick(() => {
|
||||
instance.setData({ visible: true });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
instance.setData(
|
||||
{
|
||||
mounted: true,
|
||||
visible: false,
|
||||
},
|
||||
() => {
|
||||
wx.nextTick(() => {
|
||||
instance.setData({ visible: true });
|
||||
});
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!instance.data.mounted) return;
|
||||
|
||||
instance.setData({ visible: false });
|
||||
instance._hideTimer = setTimeout(() => {
|
||||
instance.setData({ mounted: false });
|
||||
instance._hideTimer = null;
|
||||
}, HIDE_DELAY);
|
||||
}
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer(this: GuidePopupInstance, nextValue: boolean) {
|
||||
togglePopup(this, !!nextValue);
|
||||
},
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
value: "",
|
||||
observer(this: GuidePopupInstance, nextValue: string) {
|
||||
syncViewModel(
|
||||
this,
|
||||
nextValue || "",
|
||||
this.properties.subtitle || "",
|
||||
normalizeImages(this.properties.images),
|
||||
);
|
||||
},
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
value: "",
|
||||
observer(this: GuidePopupInstance, nextValue: string) {
|
||||
syncViewModel(
|
||||
this,
|
||||
this.properties.title || "",
|
||||
nextValue || "",
|
||||
normalizeImages(this.properties.images),
|
||||
);
|
||||
},
|
||||
},
|
||||
images: {
|
||||
type: Array,
|
||||
value: [],
|
||||
observer(this: GuidePopupInstance, nextValue: unknown) {
|
||||
syncViewModel(
|
||||
this,
|
||||
this.properties.title || "",
|
||||
this.properties.subtitle || "",
|
||||
normalizeImages(nextValue),
|
||||
);
|
||||
},
|
||||
},
|
||||
amount: {
|
||||
type: String,
|
||||
value: "0.00",
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
mounted: false,
|
||||
visible: false,
|
||||
displayName: DEFAULT_NAME,
|
||||
displaySubtitle: DEFAULT_ENTRY,
|
||||
avatarUrl: "",
|
||||
avatarLetter: "U",
|
||||
balanceLabel: DEFAULT_BALANCE_LABEL,
|
||||
withdrawText: DEFAULT_WITHDRAW_TEXT,
|
||||
incomeText: DEFAULT_INCOME_TEXT,
|
||||
menuItems: DEFAULT_MENU_ITEMS,
|
||||
} as GuidePopupData,
|
||||
|
||||
lifetimes: {
|
||||
attached(this: GuidePopupInstance) {
|
||||
syncViewModel(
|
||||
this,
|
||||
this.properties.title || "",
|
||||
this.properties.subtitle || "",
|
||||
normalizeImages(this.properties.images),
|
||||
);
|
||||
togglePopup(this, !!this.properties.show);
|
||||
},
|
||||
|
||||
detached(this: GuidePopupInstance) {
|
||||
clearHideTimer(this);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
noop() {},
|
||||
|
||||
handleClose(this: GuidePopupInstance) {
|
||||
this.triggerEvent("close");
|
||||
},
|
||||
|
||||
handlePreviewAvatar(this: GuidePopupInstance) {
|
||||
const images = normalizeImages(this.properties.images);
|
||||
if (!images.length) return;
|
||||
|
||||
wx.previewImage({
|
||||
current: images[0],
|
||||
urls: images,
|
||||
});
|
||||
},
|
||||
|
||||
handleQuickAction(
|
||||
this: GuidePopupInstance,
|
||||
e: WechatMiniprogram.CustomEvent,
|
||||
) {
|
||||
const key = (e.currentTarget.dataset as { key?: string }).key || "";
|
||||
if (!key) return;
|
||||
|
||||
this.triggerEvent("action", { key });
|
||||
},
|
||||
|
||||
handleMenuTap(this: GuidePopupInstance, e: WechatMiniprogram.CustomEvent) {
|
||||
const key = (e.currentTarget.dataset as { key?: string }).key || "";
|
||||
if (!key) return;
|
||||
|
||||
this.triggerEvent("action", { key });
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
<view wx:if="{{mounted}}" class="guide-popup {{visible ? 'is-visible' : ''}}">
|
||||
<view class="guide-mask" bindtap="handleClose"></view>
|
||||
|
||||
<view class="guide-shell" catchtap="noop" catchtouchmove="noop">
|
||||
<view class="guide-drag-bar"></view>
|
||||
|
||||
<view class="guide-avatar-wrap">
|
||||
<image
|
||||
wx:if="{{avatarUrl}}"
|
||||
class="guide-avatar"
|
||||
src="{{avatarUrl}}"
|
||||
mode="aspectFill"
|
||||
bindtap="handlePreviewAvatar"
|
||||
/>
|
||||
<view wx:else class="guide-avatar guide-avatar-fallback">{{avatarLetter}}</view>
|
||||
</view>
|
||||
|
||||
<view class="guide-panel">
|
||||
<view class="guide-topbar">
|
||||
<view class="guide-name">{{displayName}}</view>
|
||||
<view class="guide-entry">
|
||||
<image class="guide-entry-icon" src="./icons/user.svg" mode="aspectFit" />
|
||||
<text class="guide-entry-text">{{displaySubtitle}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="guide-balance-card">
|
||||
<view class="guide-balance-main">
|
||||
<view class="guide-balance-label">{{balanceLabel}}</view>
|
||||
<view class="guide-balance-value">¥{{amount}}</view>
|
||||
</view>
|
||||
|
||||
<view class="guide-balance-actions">
|
||||
<view class="guide-pill guide-pill-primary" data-key="withdraw" bindtap="handleQuickAction">
|
||||
{{withdrawText}}
|
||||
</view>
|
||||
<view class="guide-pill" data-key="income-detail" bindtap="handleQuickAction">
|
||||
{{incomeText}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="guide-grid">
|
||||
<view
|
||||
wx:for="{{menuItems}}"
|
||||
wx:key="key"
|
||||
class="guide-grid-item"
|
||||
data-key="{{item.key}}"
|
||||
bindtap="handleMenuTap"
|
||||
>
|
||||
<image class="guide-grid-icon" src="{{item.icon}}" mode="aspectFit" />
|
||||
<text class="guide-grid-label">{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 24H76V60H44L28 74V60H20V24Z" stroke="#303644" stroke-width="6" stroke-linejoin="round"/>
|
||||
<path d="M34 38H62" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M34 50H54" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 369 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22 30C29 30 34 25 34 18H74V34C67 34 62 39 62 46C62 53 67 58 74 58V74H34C34 67 29 62 22 62V30Z" stroke="#303644" stroke-width="6" stroke-linejoin="round"/>
|
||||
<path d="M46 28V64" stroke="#303644" stroke-width="6" stroke-dasharray="8 8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 351 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 68L28 52L58 22C61.3 18.7 66.7 18.7 70 22C73.3 25.3 73.3 30.7 70 34L40 64L24 68Z" stroke="#303644" stroke-width="6" stroke-linejoin="round"/>
|
||||
<path d="M54 26L66 38" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M24 76H72" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 424 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="20" y="36" width="56" height="40" rx="8" stroke="#303644" stroke-width="6"/>
|
||||
<path d="M48 36V76" stroke="#303644" stroke-width="6"/>
|
||||
<path d="M20 50H76" stroke="#303644" stroke-width="6"/>
|
||||
<path d="M34 34C28 34 24 30 24 24C24 19 28 16 32 16C40 16 46 27 48 34" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M62 34C68 34 72 30 72 24C72 19 68 16 64 16C56 16 50 27 48 34" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 571 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="22" y="18" width="44" height="58" rx="10" stroke="#303644" stroke-width="6"/>
|
||||
<path d="M34 34H54" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M34 48H54" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M34 62H48" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M66 58L74 66L86 50" stroke="#303644" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 549 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="36" cy="30" r="12" stroke="#303644" stroke-width="6"/>
|
||||
<path d="M16 70C18.5 57.5 27.5 50 36 50C44.5 50 53.5 57.5 56 70" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M62 32H82" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M62 50H76" stroke="#303644" stroke-width="6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 460 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M48 18L76 70H20L48 18Z" stroke="#FF5277" stroke-width="6" stroke-linejoin="round"/>
|
||||
<path d="M48 36V52" stroke="#FF5277" stroke-width="6" stroke-linecap="round"/>
|
||||
<circle cx="48" cy="62" r="3.5" fill="#FF5277"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 330 B |
@@ -0,0 +1,10 @@
|
||||
export type locationItemType = {
|
||||
id: number,
|
||||
mbuId: number,
|
||||
address: string,
|
||||
areaId: number,
|
||||
defaultStatus: boolean | number,
|
||||
location: string[],
|
||||
name: string,
|
||||
mobile: string
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"van-switch": "@vant/weapp/switch/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/* components/location-item/location-item.wxss */
|
||||
@import "variables";
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
border-radius: $primary-border-radius;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: white;
|
||||
margin-bottom: $primary-border-radius;
|
||||
min-height: 180rpx;
|
||||
padding: $primary-gap;
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.location {
|
||||
height: 60rpx;
|
||||
line-height: 60rpx;
|
||||
color: $gray-color;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
// align-items: center;
|
||||
|
||||
.delete {
|
||||
width: $primary-gap * 1.3;
|
||||
height: $primary-gap * 1.3;
|
||||
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.address {
|
||||
// height: 70rpx;
|
||||
line-height: 70rpx;
|
||||
font-size: $large-font-size;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
// align-items: center;
|
||||
.address-info {
|
||||
max-width: 600rpx;
|
||||
}
|
||||
|
||||
.edit {
|
||||
width: $primary-gap * 1.5;
|
||||
height: $primary-gap * 1.5;
|
||||
line-height: 85rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.user {
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
line-height: 60rpx;
|
||||
|
||||
.phone {
|
||||
margin-left: $primary-gap;
|
||||
}
|
||||
}
|
||||
|
||||
.default-address {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $primary-gap;
|
||||
|
||||
.default-item {
|
||||
line-height: 60rpx;
|
||||
color: $primary-color;
|
||||
}
|
||||
|
||||
.switch {
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// @ts-nocheck
|
||||
import { request } from '../../../utils/request'
|
||||
import { locationItemType } from './location-item'
|
||||
Component({
|
||||
properties: {
|
||||
item: {} as locationItemType,
|
||||
selectLocation: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
}
|
||||
},
|
||||
data: {
|
||||
locationStr: ''
|
||||
},
|
||||
observers: {
|
||||
'item.location': function (location: string[]) {
|
||||
if (location && Array.isArray(location)) {
|
||||
this.setData({
|
||||
locationStr: location.join(' ')
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleEdit() {
|
||||
console.log("this.data.item", this.data.item)
|
||||
this.triggerEvent('edit', { item: this.data.item })
|
||||
},
|
||||
handleDelete() {
|
||||
wx.showModal({
|
||||
title: '地址设置',
|
||||
content: '要将此收货地址删除吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
wx.showLoading({ title: "加载中" })
|
||||
try {
|
||||
await request({
|
||||
options: {
|
||||
url: "/app/member-address/remove",
|
||||
data: {
|
||||
// mbuId: Number(wx.getStorageSync("mbuId")),
|
||||
id: this.data.item.id
|
||||
}
|
||||
}
|
||||
}).then(() => {
|
||||
wx.showToast({ title: "删除成功" })
|
||||
if (this.data.item.defaultStatus) {
|
||||
wx.removeStorageSync("addressId");
|
||||
wx.removeStorageSync("defualtAddress");
|
||||
}
|
||||
this.triggerEvent("refresh")
|
||||
})
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
handleSelect() {
|
||||
// console.log(this.data.item)
|
||||
if (this.data.selectLocation) {
|
||||
wx.showModal({
|
||||
title: '地址设置',
|
||||
content: '要选择此收货地址吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.triggerEvent("select", {
|
||||
id: this.data.item.id,
|
||||
item: this.data.item
|
||||
});
|
||||
}
|
||||
},
|
||||
// success: async (res) => {
|
||||
// if (res.confirm) {
|
||||
// const value: any = {
|
||||
// location: this.data.item.location,
|
||||
// address: this.data.item.address,
|
||||
// mobile: this.data.item.mobile,
|
||||
// name: this.data.item.name,
|
||||
// id: this.data.item.id,
|
||||
// defaultStatus: 1,
|
||||
// mbuId: wx.getStorageSync('mbuId')
|
||||
// }
|
||||
// try {
|
||||
// await request<any[]>({
|
||||
// options: {
|
||||
// url: "/app/member-address/editor",
|
||||
// data: value
|
||||
// }, loadingTitle: "保存中"
|
||||
// })
|
||||
// .then(() => {
|
||||
// // wx.showToast({ title: "设置地址成功!", duration: 1000 })
|
||||
// wx.setStorage({ key: "addressId", data: this.data.item.id })
|
||||
// if (this.data.item.location) wx.setStorage({ key: "defualtAddress", data: `${this.data.item.location.join("")}${this.data.item.address}` })
|
||||
// this.triggerEvent("refresh")
|
||||
// }).then(() => {
|
||||
// wx.navigateBack()
|
||||
// })
|
||||
// } catch (e) {
|
||||
// console.log(e, '出现错误')
|
||||
// }
|
||||
// } else if (res.cancel) {
|
||||
// console.log('用户点击了取消')
|
||||
// }
|
||||
// },
|
||||
fail: (e) => {
|
||||
console.log(e, '错误')
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
stopTap() { },//阻止点击穿透
|
||||
handleSwitch(e) {
|
||||
const checked = e.detail;
|
||||
|
||||
if (!checked) {
|
||||
wx.showToast({ title: "不能取消默认地址,请切换到其他默认地址~", icon: "none" });
|
||||
return;
|
||||
}
|
||||
|
||||
wx.showModal({
|
||||
title: '地址设置',
|
||||
content: '要将此地址设置为默认收货地址吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const value = {
|
||||
location: this.data.item.location,
|
||||
address: this.data.item.address,
|
||||
mobile: this.data.item.mobile,
|
||||
name: this.data.item.name,
|
||||
id: this.data.item.id,
|
||||
defaultStatus: 1,
|
||||
mbuId: wx.getStorageSync('mbuId')
|
||||
};
|
||||
|
||||
await request({
|
||||
options: {
|
||||
url: "/app/member-address/editor",
|
||||
data: value
|
||||
},
|
||||
loadingTitle: "保存中"
|
||||
});
|
||||
|
||||
wx.setStorageSync("addressId", this.data.item.id);
|
||||
if (this.data.item.location) {
|
||||
wx.setStorageSync(
|
||||
"defualtAddress",
|
||||
`${this.data.item.location.join("")}${this.data.item.address}`
|
||||
);
|
||||
}
|
||||
|
||||
this.triggerEvent("refresh");
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
<!--components/location-item/location-item.wxml-->
|
||||
<view class="item">
|
||||
<view class="info" bind:tap="handleSelect">
|
||||
<view class="location">
|
||||
{{locationStr}}
|
||||
<view class="delete" catch:tap="handleDelete">
|
||||
<image src="../../../assets/img/delete.svg"></image>
|
||||
</view>
|
||||
</view>
|
||||
<view class="address">
|
||||
<view class="address-info">{{item.address}}</view>
|
||||
<view class="edit" catch:tap="handleEdit">
|
||||
<image style="width: 100%;height: 100%;" src="../../../assets/img/edit.svg" class="icon" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="footer">
|
||||
<view class="user">
|
||||
<view class="name">{{item.name}}</view>
|
||||
<view class="phone">{{item.mobile}}</view>
|
||||
</view>
|
||||
<view class="default-address">
|
||||
<view class="default-item" wx:if="{{item.defaultStatus}}">默认收货地址</view>
|
||||
<view class="default-item" style="color: #AEADB2;" wx:if="{{!item.defaultStatus}}">选为默认收货地址</view>
|
||||
<van-switch class="switch" checked="{{item.defaultStatus == true}}" size="30rpx" active-color="#FA6905"
|
||||
bind:change="handleSwitch" catch:tap="stopTap" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764038866262" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11776" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M0 0h1024v1024H0V0z" fill="#202425" opacity=".01" p-id="11777"></path><path d="M540.842667 82.670933l-18.0224-9.0112a17.066667 17.066667 0 0 0-19.524267 3.003734l-280.917333 271.530666c-159.982933 154.624-159.982933 405.265067 0 559.854934a409.6 409.6 0 0 0 134.997333 86.766933c15.530667 6.144 30.208-14.472533 24.951467-30.378667a136.533333 136.533333 0 1 1 259.345066 0c-5.256533 15.906133 9.4208 36.522667 24.951467 30.378667a409.6 409.6 0 0 0 134.997333-86.766933c148.821333-143.837867 159.197867-370.824533 31.061334-526.267734-10.0352-12.1856-29.661867-3.652267-30.1056 12.151467C800.938667 452.369067 787.285333 546.133333 716.8 546.133333c-33.655467 0-37.819733-36.898133-28.228267-86.459733 28.706133-148.036267-12.868267-309.5552-147.729066-377.002667z" fill="#FF7744" p-id="11778"></path><path d="M656.827733 581.973333l-130.3552-164.795733c-7.9872-10.103467-20.957867-10.103467-28.945066 0l-130.3552 164.795733c-79.9744 101.102933-79.9744 265.079467 0 366.1824 3.549867 4.437333 10.8544 0.443733 9.966933-5.154133a136.533333 136.533333 0 1 1 269.7216 0c-0.887467 5.597867 6.417067 9.6256 9.966933 5.154133 79.9744-101.102933 79.9744-265.079467 0-366.1824z" fill="#FFAA44" p-id="11779"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"good-detail-popup":"/components/static/good-detail-popup/good-detail-popup"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/* components/static/product-popup/product-popup.wxss */
|
||||
@import "variables";
|
||||
|
||||
.product-popup-wrapper {
|
||||
position: fixed;
|
||||
right: -100%;
|
||||
bottom: $primary-gap * 10;
|
||||
transition: right .3s linear;
|
||||
|
||||
.count {
|
||||
position: absolute;
|
||||
background: linear-gradient(100deg, #FEB133 60%, #ffffff00 100%);
|
||||
border-top-left-radius: $primary-border-radius;
|
||||
border-bottom-left-radius: $primary-border-radius;
|
||||
top: -20%;
|
||||
width: 200rpx;
|
||||
color: white;
|
||||
font-weight: 800;
|
||||
line-height: 50rpx;
|
||||
height: 50rpx;
|
||||
.count-icon{
|
||||
position: absolute;
|
||||
top: -20%;
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
z-index: 111;
|
||||
}
|
||||
.count-content{
|
||||
margin-left: $primary-gap*2.2;
|
||||
}
|
||||
.count-content-x{
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.product-popup {
|
||||
width: 200rpx;
|
||||
border-radius: $primary-border-radius;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
|
||||
.close {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
line-height: 30rpx;
|
||||
font-size: 30rpx;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
right: $primary-gap/2;
|
||||
top: $primary-gap/2;
|
||||
}
|
||||
|
||||
.top {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
background: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
image {
|
||||
border-top-left-radius: $primary-border-radius;
|
||||
border-top-right-radius: $primary-border-radius;
|
||||
width: 96%;
|
||||
height: 98%;
|
||||
// border: 1px soild #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
display: -webkit-box;
|
||||
line-clamp: 2;
|
||||
box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
justify-content: center;
|
||||
background: white;
|
||||
color: #000;
|
||||
align-items: center;
|
||||
font-size: 22rpx;
|
||||
height: 60rpx;
|
||||
padding: 0 8rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
background: #FF0E4C;
|
||||
color: white;
|
||||
height: 50rpx;
|
||||
font-weight: 800;
|
||||
align-items: center;
|
||||
|
||||
.price {
|
||||
margin-left: 8rpx;
|
||||
flex: 3;
|
||||
font-size: 34rpx;
|
||||
}
|
||||
|
||||
.button-right {
|
||||
height: 100%;
|
||||
border-top-left-radius: $primary-border-radius;
|
||||
text-align: center;
|
||||
line-height: 50rpx;
|
||||
flex: 1;
|
||||
background: linear-gradient(100deg, #ff7a3c 60%, #ff004d 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { request } from "../../../utils/request";
|
||||
import { join, leave, addCustomEventListener, removeCustomEventListener } from "../../../utils/server-sent-events";
|
||||
const statusId = "roomId-1";
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
open: { type: Boolean, value: false },
|
||||
hasTabBar: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
productNum: {
|
||||
type: Number,
|
||||
value: 1,
|
||||
},
|
||||
joinedGroupId: { type: String, value: "" }, // 用户房间号
|
||||
},
|
||||
|
||||
data: {
|
||||
name: "商品名称",
|
||||
price: 0,
|
||||
picUrl: "https://oneppp.mn188.cn/oneppp/image/037b08ef-e576-4063-9609-090773b50dfa.jpg",
|
||||
skuId: 0,
|
||||
joined: false,
|
||||
skuRes: {},
|
||||
show: false,
|
||||
stock: 0,
|
||||
count:0
|
||||
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
async attached() {
|
||||
const that = this as any;
|
||||
|
||||
// join SSE 频道
|
||||
try {
|
||||
if (!that.data.joined) {
|
||||
await join(statusId);
|
||||
that.setData({ joined: true });
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("join失败", err);
|
||||
}
|
||||
|
||||
// 绑定 SSE 事件
|
||||
that._listenerFn = async (data: any) => {
|
||||
const that = this as any;
|
||||
if (!data || data.type !== "product" || !data.skuId) return;
|
||||
try {
|
||||
if (that.data.skuId == data.skuId) {
|
||||
that.setData({
|
||||
show: true
|
||||
});
|
||||
wx.nextTick(() => {
|
||||
that.setData({ open: true });
|
||||
});
|
||||
return;
|
||||
}
|
||||
const info = await request({
|
||||
options: {
|
||||
url: `/app/shop-store/sku-one`,
|
||||
data: { id: Number(data.skuId) }
|
||||
},
|
||||
isLoading: false
|
||||
});
|
||||
that.setData({
|
||||
name: info.name,
|
||||
price: info.price,
|
||||
picUrl: info.picUrl,
|
||||
skuId: info.skuId,
|
||||
skuRes: info,
|
||||
show: true,
|
||||
stock: info.stock,
|
||||
count: info.count
|
||||
});
|
||||
wx.nextTick(() => {
|
||||
that.setData({ open: true });
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("获取商品信息失败", err);
|
||||
}
|
||||
};
|
||||
|
||||
addCustomEventListener("pushMsg", that._listenerFn);
|
||||
},
|
||||
|
||||
async detached() {
|
||||
const that = this as any;
|
||||
|
||||
// leave SSE 频道
|
||||
try {
|
||||
if (that.data.joined) {
|
||||
await leave(statusId);
|
||||
that.setData({ joined: false });
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("leave失败", err);
|
||||
}
|
||||
|
||||
// 移除事件监听
|
||||
if (that._listenerFn) {
|
||||
removeCustomEventListener("pushMsg", that._listenerFn);
|
||||
that._listenerFn = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleClick() {
|
||||
const that = this as any;
|
||||
wx.navigateTo({
|
||||
url: `/pages/common/live-good-detail/live-good-detail?skuId=${that.data.skuId}&roomId=${that.data.joinedGroupId}`
|
||||
});
|
||||
},
|
||||
handleClose() {
|
||||
const that = this as any;
|
||||
that.setData({ open: false });
|
||||
},
|
||||
openPopup(){
|
||||
const that = this as any;
|
||||
const popup = that.selectComponent('#goodDetailPopup')
|
||||
popup.openPopup()
|
||||
}
|
||||
// ,
|
||||
// handleAddShopCart() {
|
||||
// const that = this as any;
|
||||
// const mbuId = wx.getStorageSync("mbuId");
|
||||
// let allShoppingCart = wx.getStorageSync("shoppingCart");
|
||||
|
||||
// if (!Array.isArray(allShoppingCart)) {
|
||||
// allShoppingCart = [];
|
||||
// }
|
||||
|
||||
// let shoppingCart: any = allShoppingCart.find(
|
||||
// (item: any) => item.mbuId == mbuId
|
||||
// );
|
||||
|
||||
// if (shoppingCart) {
|
||||
// const temp = shoppingCart.data.find(
|
||||
// (item: any) => item.skuId == that.data.skuId
|
||||
// );
|
||||
// if (temp) {
|
||||
// //购物车里已经有了
|
||||
// temp.count += 1;
|
||||
// if (temp.count > that.data.stock) {
|
||||
// wx.showToast({ title: "购物车数量超过库存数量了!", icon: "none" })
|
||||
// return;
|
||||
// }
|
||||
// } else {
|
||||
// //没有在购物车里
|
||||
// shoppingCart.data.push({ ...that.data, count: 1 });
|
||||
// }
|
||||
// } else {
|
||||
// //无添加购物车历史
|
||||
// shoppingCart = {
|
||||
// mbuId,
|
||||
// // @ts-ignore
|
||||
// data: [{ ...this.data, count: 1 }]
|
||||
// };
|
||||
// allShoppingCart.push(shoppingCart);
|
||||
// }
|
||||
|
||||
// wx.setStorageSync("shoppingCart", allShoppingCart);
|
||||
// wx.showToast({ title: "加入购物车成功!" })
|
||||
// }
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
<!--components/static/product-popup/product-popup.wxml-->
|
||||
<view class="product-popup-wrapper" style="right: {{open ? '24rpx' : '-100%'}}" wx:if="{{show}}">
|
||||
<view class="count" wx:if="{{productNum}}">
|
||||
<image class="count-icon" src="./hot.svg"></image>
|
||||
<span class="count-content">热卖</span>
|
||||
<span class="count-content-x">x</span>
|
||||
<span class="count-content-number">{{productNum}}</span>
|
||||
</view>
|
||||
|
||||
<view class="product-popup">
|
||||
<view class="close" bind:tap="handleClose">x</view>
|
||||
|
||||
<view class="top" bind:tap="handleClick">
|
||||
<image mode="aspectFill" src="{{picUrl}}" />
|
||||
</view>
|
||||
|
||||
<view class="name">{{name}}</view>
|
||||
|
||||
<view class='button' bind:tap="openPopup">
|
||||
<view class="price"><span style="font-size: 22rpx;">¥</span>{{price/100}}</view>
|
||||
<view class="button-right">抢</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<good-detail-popup hasTabBar="{{hasTabBar}}" skuId="{{skuId}}" id="goodDetailPopup" joinedGroupId="{{joinedGroupId}}"></good-detail-popup>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
@import "variables";
|
||||
|
||||
.search {
|
||||
padding-left: $primary-gap;
|
||||
width: 100%;
|
||||
height: 70rpx;
|
||||
border: 1px solid $primary-color;
|
||||
border-radius: $primary-border-radius;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.confim {
|
||||
height: 60rpx;
|
||||
width: 100rpx;
|
||||
margin: 4rpx;
|
||||
color: #fff;
|
||||
line-height: 60rpx;
|
||||
text-align: center;
|
||||
background-color: $primary-color;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// @ts-nocheck
|
||||
Component({
|
||||
properties: {
|
||||
placeholder: {
|
||||
type: String,
|
||||
value: "请输入内容"
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
redirect: { //页面跳转的路径
|
||||
type: String,
|
||||
value: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onInput(e: any) {
|
||||
this.setData({ value: e.detail.value })
|
||||
//抛出一个事件,让父组件拿到输入值
|
||||
this.triggerEvent('change', e.detail.value)
|
||||
},
|
||||
onSearch() {
|
||||
const keyword = this.data.value.trim()
|
||||
if (keyword === '' || keyword == undefined) {
|
||||
wx.showToast({
|
||||
title: '请输入商品名称!',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
// 如果父组件绑定有search,则调父组件的方法
|
||||
if (this.dataset.hasOwnProperty('search')) {
|
||||
this.triggerEvent('search', keyword)
|
||||
return
|
||||
}
|
||||
|
||||
//否则执行默认跳转逻辑
|
||||
if (this.data.redirect) {
|
||||
wx.navigateTo({
|
||||
url: `${this.data.redirect}?keyword=${keyword}`
|
||||
})
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: '未配置跳转页面',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
<view class="search">
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
placeholder="{{placeholder}}"
|
||||
value="{{value}}"
|
||||
cursor-color="#FA6905"
|
||||
bindinput="onInput"
|
||||
bindconfirm="onSearch"
|
||||
/>
|
||||
<view class="confim" bind:tap="onSearch">搜索</view>
|
||||
</view>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/* components/sku-item/sku-item.wxss */
|
||||
@import "variables";
|
||||
|
||||
.item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 60rpx;
|
||||
margin: 0rpx $primary-gap $primary-gap $primary-gap;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: $primary-border-radius/2;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0); //给一个透明的边框,不然选中会有一个2rpx的左右边框,宽度会有变化
|
||||
|
||||
.img {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
margin-right: $primary-gap/2;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.pic {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 $primary-gap/2;
|
||||
font-size: $large-font-size;
|
||||
line-height: 60rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.item-active {
|
||||
color: $primary-color;
|
||||
border: 2rpx solid $primary-color;
|
||||
background-color: #ffddcb50;
|
||||
}
|
||||
|
||||
.item-disable {
|
||||
color: $gray-color !important;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0);
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// components/sku-item/sku-item.ts
|
||||
Component({
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
item: {
|
||||
type: Object,
|
||||
value: {}
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
disable: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
handleSelect() {
|
||||
// @ts-ignore
|
||||
this.triggerEvent('selectsku', {
|
||||
// @ts-ignore
|
||||
id: this.properties.item.skuId,
|
||||
// @ts-ignore
|
||||
checkItem: this.properties.item
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
<!--components/sku-item/sku-item.wxml-->
|
||||
<view class=" item {{active?'item-active': ''}} {{disable?'item-disable':''}}" bind:tap="handleSelect">
|
||||
<view class="img">
|
||||
<image class="pic" mode="aspectFill" src="{{item.picUrl}}"></image>
|
||||
</view>
|
||||
<view class="content">{{item.name}}</view>
|
||||
</view>
|
||||