feat: 电子药箱小程序 - 4Tab药品管理

- 电子药箱: 药品库存管理 + 手动录入 + 今日用药打卡
- 药品百科: 搜索 + 分类筛选 + 20种药品静态数据
- 惠教中心: 4条药品使用指南课程
- 我的: 家庭成员信息管理
- 自定义TabBar + navigation-bar组件
- SVG药品分类插图
This commit is contained in:
陈誉午
2026-07-29 11:20:52 +08:00
commit 994b267f5c
683 changed files with 43849 additions and 0 deletions
@@ -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>