feat: 电子药箱小程序 - 4Tab药品管理
- 电子药箱: 药品库存管理 + 手动录入 + 今日用药打卡 - 药品百科: 搜索 + 分类筛选 + 20种药品静态数据 - 惠教中心: 4条药品使用指南课程 - 我的: 家庭成员信息管理 - 自定义TabBar + navigation-bar组件 - SVG药品分类插图
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"popup": "/components/popup/popup"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/* components/banner/banner.wxss */
|
||||
.banner-wrap {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 7%;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 9999;
|
||||
padding: 30rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.banner__avatar {
|
||||
border-radius: 50%;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.banner__card {
|
||||
border-radius: 25rpx;
|
||||
padding: 18rpx;
|
||||
box-sizing: border-box;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
margin-bottom: 20rpx;
|
||||
display: inline-block;
|
||||
}
|
||||
.banner__icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
.banner__avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
}
|
||||
.banner-id {
|
||||
font-size: 21rpx;
|
||||
}
|
||||
.banner-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.banner__title {
|
||||
max-width: 250rpx;
|
||||
}
|
||||
.text-overflow {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.banner__count {
|
||||
color: #fff;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
.popup-text {
|
||||
color: #000;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.report-options {
|
||||
width: 100%;
|
||||
margin: 24rpx 0;
|
||||
}
|
||||
|
||||
.options-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.option-item radio {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
.other-input {
|
||||
margin: 24rpx 0;
|
||||
}
|
||||
|
||||
.other-input-field {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
padding: 0 20rpx;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.upload-title {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.image-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.uploaded-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.delete-image {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
right: -8rpx;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border: 2rpx dashed #ddd;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 48rpx;
|
||||
color: #999;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.description-section {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.description-input {
|
||||
width: 100%;
|
||||
height: 160rpx;
|
||||
padding: 16rpx;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
text-align: right;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.submit-section {
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: #ff4d4f;
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
border-radius: 44rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.submit-btn:active {
|
||||
background: #ff7875;
|
||||
}
|
||||
|
||||
.submit-btn[disabled] {
|
||||
background: #ccc;
|
||||
color: #999;
|
||||
opacity: 0.7;
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
import { request } from "../../utils/request";
|
||||
|
||||
// components/banner/banner.ts
|
||||
interface BannerData {
|
||||
avatarUrl: string;
|
||||
nickName: string;
|
||||
userId: string;
|
||||
showPopup: boolean;
|
||||
reportOptions: Array<{
|
||||
id: number;
|
||||
text: string;
|
||||
checked: boolean;
|
||||
}>;
|
||||
showOtherInput: boolean;
|
||||
otherReason: string;
|
||||
uploadedImages: string[];
|
||||
description: string;
|
||||
state: number;
|
||||
isSubmitEnabled: boolean;
|
||||
}
|
||||
|
||||
interface BannerProperties {
|
||||
roomInfo: {
|
||||
iconUrl?: string;
|
||||
name?: string;
|
||||
mumId?: string;
|
||||
state: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface BannerComponent extends WechatMiniprogram.Component.TrivialInstance {
|
||||
data: BannerData;
|
||||
properties: BannerProperties;
|
||||
}
|
||||
|
||||
const state: Record<number, string> = {
|
||||
1: "直播中",
|
||||
2: "直播结束",
|
||||
};
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
roomInfo: {
|
||||
type: Object,
|
||||
value: {
|
||||
iconUrl: "",
|
||||
nickname: "",
|
||||
mumId: "",
|
||||
state: null
|
||||
},
|
||||
observer: function (
|
||||
this: BannerComponent,
|
||||
newVal: BannerProperties["roomInfo"]
|
||||
) {
|
||||
this.updateRoomInfo(newVal);
|
||||
},
|
||||
},
|
||||
watchNum:{
|
||||
type:Number,
|
||||
data:0
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
avatarUrl: "",
|
||||
nickName: "",
|
||||
userId: "",
|
||||
state: "",
|
||||
showPopup: false,
|
||||
reportOptions: [
|
||||
{ id: 1, text: "价格虚假", checked: false },
|
||||
{ id: 2, text: "品牌虚假", checked: false },
|
||||
{ id: 3, text: "色情低俗", checked: false },
|
||||
{ id: 4, text: "虚假活动", checked: false },
|
||||
{ id: 5, text: "不良行为", checked: false },
|
||||
{ id: 6, text: "商品信息虚假", checked: false },
|
||||
{ id: 7, text: "出售违禁品", checked: false },
|
||||
{ id: 8, text: "其他", checked: false },
|
||||
],
|
||||
showOtherInput: false,
|
||||
otherReason: "",
|
||||
uploadedImages: [],
|
||||
description: "",
|
||||
isSubmitEnabled: false,
|
||||
},
|
||||
lifetimes: {
|
||||
attached(this: BannerComponent) {
|
||||
this.updateRoomInfo(this.properties.roomInfo);
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
updateRoomInfo(
|
||||
this: BannerComponent,
|
||||
roomInfo?: BannerProperties["roomInfo"]
|
||||
) {
|
||||
const info = roomInfo || this.properties.roomInfo;
|
||||
this.setData({
|
||||
avatarUrl: info?.iconUrl || "",
|
||||
nickName: info?.name || "",
|
||||
userId: info?.mumId || "",
|
||||
state: state[info?.state ?? 2],
|
||||
});
|
||||
},
|
||||
handOpen(this: WechatMiniprogram.Component.TrivialInstance) {
|
||||
this.setData({
|
||||
showPopup: true,
|
||||
isSubmitEnabled: false,
|
||||
});
|
||||
},
|
||||
closePopup(this: WechatMiniprogram.Component.TrivialInstance) {
|
||||
this.setData({
|
||||
showPopup: false,
|
||||
});
|
||||
},
|
||||
|
||||
onOptionChange(this: WechatMiniprogram.Component.TrivialInstance, e: any) {
|
||||
const { value } = e.detail;
|
||||
const reportOptions = this.data.reportOptions.map((item: any) => ({
|
||||
...item,
|
||||
checked: value.includes(item.id.toString()),
|
||||
}));
|
||||
|
||||
const showOtherInput =
|
||||
reportOptions.find((item: any) => item.id === 8)?.checked || false;
|
||||
|
||||
this.setData({
|
||||
reportOptions,
|
||||
showOtherInput,
|
||||
}, () => {
|
||||
this.updateSubmitEnabled();
|
||||
});
|
||||
},
|
||||
|
||||
onOtherInput(this: WechatMiniprogram.Component.TrivialInstance, e: any) {
|
||||
this.setData({
|
||||
otherReason: e.detail.value,
|
||||
}, () => {
|
||||
this.updateSubmitEnabled();
|
||||
});
|
||||
},
|
||||
deleteImage(
|
||||
this: WechatMiniprogram.Component.TrivialInstance,
|
||||
e: WechatMiniprogram.TouchEvent
|
||||
) {
|
||||
const { index } = e.currentTarget.dataset;
|
||||
wx.showModal({
|
||||
title: "提示",
|
||||
content: "确定要删除这张图片吗?",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const uploadedImages = [...this.data.uploadedImages];
|
||||
uploadedImages.splice(index, 1);
|
||||
this.setData({ uploadedImages });
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
chooseImage(this: WechatMiniprogram.Component.TrivialInstance) {
|
||||
wx.chooseImage({
|
||||
count: 3 - this.data.uploadedImages.length,
|
||||
success: (res) => {
|
||||
this.setData({
|
||||
uploadedImages: [...this.data.uploadedImages, ...res.tempFilePaths],
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
previewImage(this: WechatMiniprogram.Component.TrivialInstance, e: any) {
|
||||
const { src } = e.currentTarget.dataset;
|
||||
wx.previewImage({
|
||||
current: src,
|
||||
urls: this.data.uploadedImages,
|
||||
});
|
||||
},
|
||||
|
||||
onDescriptionInput(
|
||||
this: WechatMiniprogram.Component.TrivialInstance,
|
||||
e: any
|
||||
) {
|
||||
this.setData({
|
||||
description: e.detail.value,
|
||||
}, () => {
|
||||
this.updateSubmitEnabled();
|
||||
});
|
||||
},
|
||||
|
||||
updateSubmitEnabled(this: BannerComponent) {
|
||||
const selectedOptions = this.data.reportOptions.filter(
|
||||
(item: any) => item.checked
|
||||
);
|
||||
const hasSelectedOptions = selectedOptions.length > 0;
|
||||
|
||||
const isOtherSelected = selectedOptions.some((item: any) => item.id === 8);
|
||||
const otherReasonValid = !isOtherSelected || (this.data.otherReason && this.data.otherReason.trim().length > 0);
|
||||
|
||||
const hasDescription = this.data.description && this.data.description.trim().length > 0;
|
||||
|
||||
this.setData({
|
||||
isSubmitEnabled: hasSelectedOptions && otherReasonValid && hasDescription,
|
||||
});
|
||||
},
|
||||
|
||||
async submitReport(this: BannerComponent) {
|
||||
if (!this.data.isSubmitEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOptions = this.data.reportOptions.filter(
|
||||
(item: any) => item.checked
|
||||
);
|
||||
if (selectedOptions.length === 0) {
|
||||
wx.showToast({
|
||||
title: "请选择举报原因",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
selectedOptions.some((item: any) => item.id === 8) &&
|
||||
!this.data.otherReason
|
||||
) {
|
||||
wx.showToast({
|
||||
title: "请填写其他举报原因",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 提交举报信息
|
||||
const reportData = {
|
||||
reasons: selectedOptions,
|
||||
otherReason: this.data.otherReason,
|
||||
images: this.data.uploadedImages,
|
||||
description: this.data.description,
|
||||
userId: this.data.userId,
|
||||
roomId: wx.getStorageSync("joinedGroupId"),
|
||||
};
|
||||
console.log("举报信息:", reportData);
|
||||
|
||||
try {
|
||||
await request({
|
||||
options: {
|
||||
url: `/app/video-live/feedback`,
|
||||
data: reportData,
|
||||
},
|
||||
});
|
||||
|
||||
wx.showToast({
|
||||
title: "举报成功",
|
||||
icon: "success",
|
||||
});
|
||||
|
||||
// 提交成功后关闭弹窗
|
||||
this.setData({
|
||||
showPopup: false,
|
||||
reportOptions: this.data.reportOptions.map((item: any) => ({
|
||||
...item,
|
||||
checked: false,
|
||||
})),
|
||||
showOtherInput: false,
|
||||
otherReason: "",
|
||||
uploadedImages: [],
|
||||
description: "",
|
||||
isSubmitEnabled: false,
|
||||
});
|
||||
} catch (error) {
|
||||
this.handleError(error);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
<!--components/banner/banner.wxml-->
|
||||
<view class="banner-wrap">
|
||||
<view>
|
||||
<view class="banner__card banner-box">
|
||||
<image class="banner__avatar" src="{{avatarUrl}}" mode="aspectFill" />
|
||||
<view>
|
||||
<view class="banner__title text-overflow">{{nickName}}</view>
|
||||
<view class="banner-box banner__count">
|
||||
<image class="banner__icon" src="https://img.alicdn.com/imgextra/i1/O1CN01GC0HVZ1kWz8bvu0KX_!!6000000004692-2-tps-36-36.png" mode="widthFix" />
|
||||
<text>{{watchNum}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="banner__card">{{state}}</view>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<view class="banner__card banner-box" bind:tap="handOpen">
|
||||
<image class="banner__icon" src="/assets/img/p.svg" mode="widthFix"/>
|
||||
<text>投诉</text>
|
||||
</view>
|
||||
<view class="banner-id">用户ID:{{userId}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<popup visible="{{showPopup}}" bind:close="closePopup">
|
||||
<view slot="header" class="popup-header">
|
||||
<text>举报本场直播</text>
|
||||
</view>
|
||||
|
||||
|
||||
<view class="popup-content">
|
||||
<radio-group class="report-options" bindchange="onOptionChange">
|
||||
<view class="options-row">
|
||||
<view class="option-item" wx:for="{{reportOptions}}" wx:for-item="item" wx:key="id">
|
||||
<radio value="{{item.id}}" checked="{{item.checked}}"/>
|
||||
<text class="popup-text">{{item.text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</radio-group>
|
||||
|
||||
<view class="other-input" wx:if="{{showOtherInput}}">
|
||||
<input
|
||||
class="other-input-field popup-text"
|
||||
type="text"
|
||||
placeholder="请输入其他举报原因"
|
||||
value="{{otherReason}}"
|
||||
bindinput="onOtherInput"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="upload-section">
|
||||
<view class="upload-title popup-text">上传图片证据(最多3张)</view>
|
||||
<view class="image-list">
|
||||
<block wx:for="{{uploadedImages}}" wx:key="*this">
|
||||
<view class="image-wrapper">
|
||||
<image class="uploaded-image" src="{{item}}" mode="aspectFill" bindtap="previewImage" data-src="{{item}}"/>
|
||||
<view class="delete-image" catchtap="deleteImage" data-index="{{index}}">×</view>
|
||||
</view>
|
||||
</block>
|
||||
<view class="upload-btn" wx:if="{{uploadedImages.length < 3}}" bindtap="chooseImage">
|
||||
<view class="upload-icon">+</view>
|
||||
<text class="upload-text">上传图片</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="description-section">
|
||||
<textarea
|
||||
class="description-input popup-text"
|
||||
placeholder="请补充说明(选填)"
|
||||
value="{{description}}"
|
||||
bindinput="onDescriptionInput"
|
||||
maxlength="200"
|
||||
/>
|
||||
<text class="char-count popup-text">{{description.length}}/200</text>
|
||||
</view>
|
||||
|
||||
<view class="submit-section">
|
||||
<button class="submit-btn" disabled="{{!isSubmitEnabled}}" bindtap="submitReport">提交举报</button>
|
||||
</view>
|
||||
</view>
|
||||
</popup>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/* components/collapse/collapse.wxss */
|
||||
.collapse {
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.collapse-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.arrow.opened {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.collapse-content {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
.content-inner {
|
||||
padding: 24rpx;
|
||||
color: #333;
|
||||
background: #ffddcb;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// components/collapse/collapse.ts
|
||||
export { }
|
||||
Component({
|
||||
properties: {
|
||||
title: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
titleStyle: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
defaultOpen: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
disableOpen: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
offsetHeight: {
|
||||
type: Number,
|
||||
value: 0
|
||||
}
|
||||
},
|
||||
|
||||
data: {
|
||||
open: false,
|
||||
contentHeight: 0
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
ready(this: WechatMiniprogram.Component.TrivialInstance) {
|
||||
this.setData({ open: this.data.defaultOpen })
|
||||
this.calcHeight()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggle(this: WechatMiniprogram.Component.TrivialInstance) {
|
||||
if (this.data.disableOpen) return;
|
||||
|
||||
const open = !this.data.open;
|
||||
this.setData({ open }, () => {
|
||||
if (open) {
|
||||
// 等展开后再算高度
|
||||
this.calcHeight();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
calcHeight(this: WechatMiniprogram.Component.TrivialInstance) {
|
||||
const query = this.createSelectorQuery()
|
||||
query.select('#content').boundingClientRect()
|
||||
query.exec(res => {
|
||||
if (res && res[0]) {
|
||||
this.setData({
|
||||
contentHeight: res[0].height
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
<view class="collapse">
|
||||
<!-- header -->
|
||||
<view class="collapse-header" bind:tap="toggle">
|
||||
<view class="title">
|
||||
<view class="title-text" style="{{ titleStyle }}">
|
||||
{{ title }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="arrow {{ open ? 'open' : '' }}">▼</view>
|
||||
</view>
|
||||
|
||||
<view class="collapse-content" style="max-height: {{ open ? (contentHeight+offsetHeight) + 'px' : '0rpx' }}">
|
||||
<view class="content-inner" id="content">
|
||||
<slot />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/* components/empty/empty.wxss */
|
||||
@import "../../variables";
|
||||
|
||||
.empty {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #333333;
|
||||
font-size: 32rpx;
|
||||
line-height: 44rpx;
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #999999;
|
||||
font-size: 28rpx;
|
||||
line-height: 40rpx;
|
||||
margin-top: 8rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action {
|
||||
height: 54rpx;
|
||||
line-height: 54rpx;
|
||||
border-radius: 46rpx;
|
||||
min-width: 140rpx;
|
||||
padding: 0 24rpx;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
margin-top: 108rpx;
|
||||
background: linear-gradient(139deg, #ff9a33 0%, #ff7200 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-legacy {
|
||||
height: 60vh;
|
||||
color: $gray-color;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.empty-scene {
|
||||
min-height: 1220rpx;
|
||||
padding-top: 400rpx;
|
||||
overflow: hidden;
|
||||
background: #f5f5f5;
|
||||
|
||||
.scene-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 982rpx;
|
||||
border-bottom-left-radius: 28rpx;
|
||||
border-bottom-right-radius: 28rpx;
|
||||
// background: linear-gradient(180deg, #ffd5af 0%, rgba(255, 255, 255, 0.2) 100%);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// components/empty/empty.ts
|
||||
type SceneConfig = {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
buttonText?: string;
|
||||
action?: string;
|
||||
imageStyle?: string;
|
||||
};
|
||||
|
||||
const SCENE_MAP: Record<string, SceneConfig> = {
|
||||
loadFail: {
|
||||
title: "加载失败",
|
||||
description: "请点击刷新重新加载试试~",
|
||||
icon: "./assets/load-failed.png",
|
||||
buttonText: "刷新",
|
||||
action: "refresh",
|
||||
imageStyle: "width: 328rpx; height: 392rpx;"
|
||||
},
|
||||
noDynamic: {
|
||||
title: "暂无动态",
|
||||
description: "这个地方空空如也~",
|
||||
icon: "./assets/no-dynamic.png",
|
||||
imageStyle: "width: 260rpx; height: 378rpx;"
|
||||
},
|
||||
noMessage: {
|
||||
title: "暂无消息",
|
||||
description: "暂无新的消息",
|
||||
icon: "./assets/no-message.png",
|
||||
imageStyle: "width: 344rpx; height: 376rpx;"
|
||||
},
|
||||
noAddress: {
|
||||
title: "暂无地址",
|
||||
description: "快去添加一个吧~",
|
||||
icon: "./assets/no-address.png",
|
||||
buttonText: "添加地址",
|
||||
action: "addAddress",
|
||||
imageStyle: "width: 264rpx; height: 392rpx;"
|
||||
},
|
||||
noArchive: {
|
||||
title: "暂无档案信息",
|
||||
description: "去添加宝贝的成长档案吧~",
|
||||
icon: "./assets/no-archive.png",
|
||||
imageStyle: "width: 388rpx; height: 408rpx;"
|
||||
}
|
||||
};
|
||||
|
||||
const DEFAULT_TITLE = "什么都没搜到";
|
||||
const DEFAULT_ICON = "../../assets/img/empty.svg";
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
scene: {
|
||||
type: String,
|
||||
value: "",
|
||||
observer: "syncScene"
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
value: DEFAULT_TITLE,
|
||||
observer: "syncScene"
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
value: "",
|
||||
observer: "syncScene"
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
value: DEFAULT_ICON,
|
||||
observer: "syncScene"
|
||||
},
|
||||
buttonText: {
|
||||
type: String,
|
||||
value: "",
|
||||
observer: "syncScene"
|
||||
},
|
||||
emptyStyle: {
|
||||
type: String,
|
||||
value: ""
|
||||
},
|
||||
imageStyle: {
|
||||
type: String,
|
||||
value: "",
|
||||
observer: "syncScene"
|
||||
}
|
||||
},
|
||||
|
||||
data: {
|
||||
sceneMode: false,
|
||||
resolvedTitle: "",
|
||||
resolvedDescription: "",
|
||||
resolvedIcon: "",
|
||||
resolvedButtonText: "",
|
||||
resolvedAction: "",
|
||||
resolvedImageStyle: ""
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
(this as any).syncScene();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
syncScene() {
|
||||
const that = this as any;
|
||||
const scene = (that.data.scene || "").trim();
|
||||
const sceneConfig = scene ? SCENE_MAP[scene] : undefined;
|
||||
const title = that.data.title || "";
|
||||
const description = that.data.description || "";
|
||||
const icon = that.data.icon || "";
|
||||
const buttonText = that.data.buttonText || "";
|
||||
const imageStyle = that.data.imageStyle || "";
|
||||
|
||||
that.setData({
|
||||
sceneMode: !!sceneConfig,
|
||||
resolvedTitle: sceneConfig && title === DEFAULT_TITLE ? sceneConfig.title : (title || sceneConfig?.title || DEFAULT_TITLE),
|
||||
resolvedDescription: sceneConfig && !description ? sceneConfig.description : description,
|
||||
resolvedIcon: sceneConfig && icon === DEFAULT_ICON ? sceneConfig.icon : (icon || sceneConfig?.icon || DEFAULT_ICON),
|
||||
resolvedButtonText: sceneConfig && !buttonText ? (sceneConfig.buttonText || "") : buttonText,
|
||||
resolvedAction: sceneConfig?.action || "",
|
||||
resolvedImageStyle: sceneConfig && !imageStyle ? (sceneConfig.imageStyle || "") : imageStyle
|
||||
});
|
||||
},
|
||||
|
||||
handleAction() {
|
||||
const that = this as any;
|
||||
that.triggerEvent("action", {
|
||||
scene: that.data.scene,
|
||||
action: that.data.resolvedAction
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
<!--components/empty/empty.wxml-->
|
||||
<view class="empty {{sceneMode ? 'empty-scene' : 'empty-legacy'}}" style="{{emptyStyle}}">
|
||||
<view wx:if="{{sceneMode}}" class="scene-bg"></view>
|
||||
<view class="content">
|
||||
<view class="icon" style="{{resolvedImageStyle}}">
|
||||
<image src="{{resolvedIcon}}" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="title">{{resolvedTitle}}</view>
|
||||
<view class="description" wx:if="{{resolvedDescription}}">{{resolvedDescription}}</view>
|
||||
<view class="action" wx:if="{{resolvedButtonText}}" bind:tap="handleAction">{{resolvedButtonText}}</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/* components/end/end.wxss */
|
||||
.end {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: none;
|
||||
color: #999;
|
||||
margin: 0;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// components/end/end.ts
|
||||
Component({
|
||||
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,2 @@
|
||||
<!--components/end/end.wxml-->
|
||||
<text class="end">—— · ——</text>
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"player": "/components/player/player",
|
||||
"banner": "/components/banner/banner",
|
||||
"comment": "/components/static/comment/comment",
|
||||
"product-popup": "/components/static/product-popup/product-popup"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/* components/liveroom/liveroom.wxss */
|
||||
.liveroom-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #fcfcfd;
|
||||
background-image: linear-gradient(90deg, #2f1d17 0%, #1e2326 100%);
|
||||
}
|
||||
.text-overflow {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.countdown {
|
||||
margin: 30rpx 0;
|
||||
}
|
||||
|
||||
.countdown-text {
|
||||
color: white;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.countdown-time {
|
||||
color: white;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.enter-room-btn {
|
||||
background: #ff6b6b;
|
||||
width: 400rpx;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
border-radius: 40rpx;
|
||||
border: none;
|
||||
}
|
||||
.live-ended {
|
||||
position: fixed;
|
||||
top: 35%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ended-content {
|
||||
border-radius: 16rpx;
|
||||
padding: 48rpx;
|
||||
text-align: center;
|
||||
width: 600rpx;
|
||||
}
|
||||
|
||||
.ended-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.ended-text {
|
||||
font-size: 36rpx;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.ended-time {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
display: block;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.replay-btn {
|
||||
width: 400rpx;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
background: #ff6b35;
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
border-radius: 40rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.replay-btn:active {
|
||||
background: #ff8555;
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
import { request } from "../../utils/request";
|
||||
import { appId } from "../../env";
|
||||
import {
|
||||
join,
|
||||
leave,
|
||||
addCustomEventListener,
|
||||
removeCustomEventListener,
|
||||
} from "../../utils/server-sent-events";
|
||||
interface LiveroomData {
|
||||
isPlayback: boolean;
|
||||
pluginVisible: boolean;
|
||||
roomInfo: any;
|
||||
pullUrlInfo: any;
|
||||
vodInfo: any;
|
||||
watchNum: number;
|
||||
productNum: number;
|
||||
chatEnhancementApproved: boolean;
|
||||
}
|
||||
interface LiveroomProperties {
|
||||
joinedGroupId: string;
|
||||
}
|
||||
interface LiveroomComponent
|
||||
extends WechatMiniprogram.Component.TrivialInstance {
|
||||
data: LiveroomData;
|
||||
properties: LiveroomProperties;
|
||||
_listener?: (data: any) => void;
|
||||
onLiveMessage: (data: any) => void;
|
||||
init: () => Promise<void>;
|
||||
cleanup: () => void;
|
||||
handleError: (error?: unknown) => void;
|
||||
setChatEnhancementApproval: (approved: boolean) => void;
|
||||
}
|
||||
|
||||
const statusId = "roomId-1";
|
||||
// components/liveroom/liveroom.ts
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
joinedGroupId: {
|
||||
type: String,
|
||||
value: "",
|
||||
observer: function (
|
||||
this: LiveroomComponent,
|
||||
newVal: string,
|
||||
oldVal: string,
|
||||
) {
|
||||
if (newVal && newVal !== oldVal) {
|
||||
this.init();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
lifetimes: {
|
||||
attached(this: LiveroomComponent) {},
|
||||
detached(this: LiveroomComponent) {
|
||||
this.cleanup();
|
||||
},
|
||||
},
|
||||
pageLifetimes: {
|
||||
show(this: LiveroomComponent) {
|
||||
this._listener = this.onLiveMessage.bind(this);
|
||||
addCustomEventListener("pushMsg", this._listener);
|
||||
join(statusId);
|
||||
},
|
||||
hide() {
|
||||
// @ts-ignore
|
||||
removeCustomEventListener("pushMsg", this._listener);
|
||||
leave(statusId);
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
isPlayback: false,
|
||||
pluginVisible: true,
|
||||
roomInfo: null,
|
||||
watchNum: 0,
|
||||
productNum: 0,
|
||||
durationInterval: null,
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
onLiveMessage(data: any) {
|
||||
console.log("收到通知:", data);
|
||||
const that = this as any;
|
||||
if (data.type == "watchNum") {
|
||||
that.setData({ watchNum: data.count });
|
||||
}
|
||||
|
||||
if (data.type == "productNum") {
|
||||
that.setData({ productNum: data.count });
|
||||
}
|
||||
},
|
||||
|
||||
updateIsPlayback(this: any, bool: boolean) {
|
||||
this.isPlayback = bool;
|
||||
},
|
||||
controlstoggle(this: LiveroomComponent, e: WechatMiniprogram.CustomEvent) {
|
||||
const { state } = e.detail;
|
||||
if (state && this.data.roomInfo) {
|
||||
this.setData({
|
||||
roomInfo: {
|
||||
...this.data.roomInfo,
|
||||
state,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async init(this: LiveroomComponent) {
|
||||
const app = getApp<IAppOption>();
|
||||
if (!app.globalData.isLogin) return;
|
||||
const roomId = this.properties.joinedGroupId;
|
||||
const params = {
|
||||
roomId: roomId,
|
||||
appId: appId,
|
||||
openId: wx.getStorageSync("openId"),
|
||||
};
|
||||
|
||||
try {
|
||||
const data = await request({
|
||||
options: {
|
||||
url: `/app/video-live/details`,
|
||||
data: params,
|
||||
method: "GET",
|
||||
},
|
||||
});
|
||||
const bool = this.compareTime(data.data.onlineDatetime);
|
||||
console.log(bool, "12313");
|
||||
this.setData({
|
||||
roomInfo: data.data,
|
||||
pullUrlInfo: data.data,
|
||||
vodInfo: data.data,
|
||||
bool,
|
||||
});
|
||||
} catch (error) {
|
||||
this.handleError(error);
|
||||
}
|
||||
},
|
||||
|
||||
handleError(this: LiveroomComponent) {
|
||||
wx.showToast({
|
||||
title: "获取直播信息失败",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
},
|
||||
|
||||
cleanup(this: LiveroomComponent) {
|
||||
this.setData({
|
||||
isPlayback: false,
|
||||
pluginVisible: true,
|
||||
roomInfo: null,
|
||||
watchNum: 0,
|
||||
productNum: 0,
|
||||
chatEnhancementApproved: false,
|
||||
});
|
||||
},
|
||||
|
||||
compareTime(targetTimeStr: string) {
|
||||
const targetTime = new Date(targetTimeStr);
|
||||
const currentTime = new Date();
|
||||
return targetTime > currentTime;
|
||||
},
|
||||
|
||||
startDurationTimer(this: LiveroomComponent) {
|
||||
this.durationInterval = setInterval(() => {
|
||||
const now = new Date().getTime();
|
||||
const startTime = new Date(this.data.roomInfo.onlineDatetime).getTime();
|
||||
if (now < startTime) {
|
||||
const remainingTime = Math.floor((startTime - now) / 1000);
|
||||
const days = Math.floor(remainingTime / (24 * 3600));
|
||||
const hours = Math.floor((remainingTime % (24 * 3600)) / 3600);
|
||||
const minutes = Math.floor((remainingTime % 3600) / 60);
|
||||
const seconds = remainingTime % 60;
|
||||
this.setData({
|
||||
isLiveStarted: false,
|
||||
countdown:
|
||||
days > 0
|
||||
? `${days}天${hours.toString().padStart(2, "0")}:${minutes
|
||||
.toString()
|
||||
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`
|
||||
: `${hours.toString().padStart(2, "0")}:${minutes
|
||||
.toString()
|
||||
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`,
|
||||
});
|
||||
} else {
|
||||
const duration = Math.floor((now - startTime) / 1000);
|
||||
const hours = Math.floor(duration / 3600);
|
||||
const minutes = Math.floor((duration % 3600) / 60);
|
||||
const seconds = duration % 60;
|
||||
this.setData({
|
||||
isLiveStarted: true,
|
||||
duration: `${hours.toString().padStart(2, "0")}:${minutes
|
||||
.toString()
|
||||
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`,
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
stopDurationTimer(this: LiveroomComponent) {
|
||||
if (this.durationInterval) {
|
||||
clearInterval(this.durationInterval);
|
||||
this.durationInterval = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
<!--components/liveroom/liveroom.wxml-->
|
||||
<view class="liveroom-wrap">
|
||||
|
||||
<banner room-info="{{roomInfo}}" watchNum="{{watchNum}}"/>
|
||||
|
||||
<!-- 直播未开始提示
|
||||
<view class="live-ended" wx:if="{{bool}}" >
|
||||
<view class="ended-content">
|
||||
<text class="ended-text">直播未开始</text>
|
||||
<view class="countdown">
|
||||
<text class="countdown-text">距离开始还有:</text>
|
||||
<text class="countdown-time">{{roomInfo.onlineDatetime}}</text>
|
||||
</view>
|
||||
<button class="enter-room-btn" bind:tap="enterRoom">进入房间</button>
|
||||
</view>
|
||||
</view> -->
|
||||
|
||||
<!-- 直播结束提示 -->
|
||||
<view class="live-ended" wx:if="{{roomInfo.state == 2}}">
|
||||
<view class="ended-content">
|
||||
<text class="ended-text">直播已结束</text>
|
||||
<text class="ended-time">结束时间:{{roomInfo.offlineDatetime}}</text>
|
||||
<button class="replay-btn">观看回放</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<banner room-info="{{roomInfo}}"/>
|
||||
|
||||
<player
|
||||
is-playback="{{isPlayback}}"
|
||||
is-living="{{isLiving}}"
|
||||
pull-url-info="{{pullUrlInfo}}"
|
||||
vod-info="{{vodInfo}}"
|
||||
bind:controlstoggle="controlstoggle"
|
||||
/>
|
||||
<comment wx:if="{{roomInfo.chat}}"/>
|
||||
<product-popup joinedGroupId="{{joinedGroupId}}" productNum="{{productNum}}"/>
|
||||
</view>
|
||||
@@ -0,0 +1,33 @@
|
||||
export type LoginRes = {
|
||||
data: {
|
||||
code?: number;
|
||||
appId: string;
|
||||
id: number;
|
||||
mpId: number;
|
||||
openId: number;
|
||||
mpUserId: number;
|
||||
unionId: string;
|
||||
nickName: string;
|
||||
avatarUrl: string;
|
||||
msg?: string,
|
||||
|
||||
}
|
||||
statusCode: number,
|
||||
msg?: string,
|
||||
}
|
||||
|
||||
export type userInfoRes = {
|
||||
openId: string,
|
||||
mbuId: number,
|
||||
nickName: string,
|
||||
gender: number,
|
||||
city: string,
|
||||
province: string,
|
||||
country: string,
|
||||
avatarUrl: string,
|
||||
unionId: string,
|
||||
watermark: {
|
||||
appid: string,
|
||||
timestamp: string
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/* components/login/login.wxss */
|
||||
@import "../../variables";
|
||||
|
||||
.loginContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 999;
|
||||
width: 750rpx;
|
||||
height: 100%;
|
||||
background-color: rgba($color: #000000, $alpha: .3);
|
||||
backdrop-filter: blur(12rpx);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.contain {
|
||||
width: 100%;
|
||||
height: 25vh;
|
||||
background-color: #ffffff;
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
text {
|
||||
font-size: 35rpx;
|
||||
width: 80%;
|
||||
line-height: 50rpx;
|
||||
margin-bottom: 50rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.login {
|
||||
width: 60%;
|
||||
height: 80rpx;
|
||||
background: $primary-color-blue;
|
||||
color: white;
|
||||
font-size: 40rpx;
|
||||
font-weight: 800;
|
||||
line-height: 80rpx;
|
||||
text-align: center;
|
||||
border-radius: $primary-border-radius-20;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// components/login/login.ts
|
||||
import { BASE_URL, appId } from "../../env";
|
||||
import { LoginRes } from "./login.d";
|
||||
import { wxGetUserInfo, wxLogin, wxRequest } from "../../utils/wx-api";
|
||||
|
||||
interface LoginComponent {
|
||||
data: {
|
||||
mbuId: number;
|
||||
userName: string;
|
||||
};
|
||||
setData: (data: Partial<{ mbuId: number; userName: string }>) => void;
|
||||
triggerEvent: (name: string, detail?: any) => void;
|
||||
handleLogin: () => Promise<void>;
|
||||
}
|
||||
const app = getApp<IAppOption>();
|
||||
|
||||
Component({
|
||||
data: {
|
||||
mbuId: wx.getStorageSync("mbuId"),
|
||||
userName: wx.getStorageSync("userName"),
|
||||
},
|
||||
|
||||
methods: {
|
||||
async handleLogin(this: LoginComponent) {
|
||||
try {
|
||||
wx.showLoading({ title: "登录中" });
|
||||
const loginRes = await wxLogin();
|
||||
const infoRes = await wxGetUserInfo();
|
||||
if (!loginRes.code) throw new Error(loginRes.errMsg);
|
||||
|
||||
const loginResult = await wxRequest<LoginRes>({
|
||||
url: `${BASE_URL}/auth/app-login?appId=${appId}&code=${loginRes.code}`,
|
||||
method: "POST",
|
||||
data: {
|
||||
...infoRes.userInfo,
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
loginResult?.statusCode !== 200 ||
|
||||
(loginResult.data.code && loginResult.data.code !== 200)
|
||||
) {
|
||||
throw new Error(loginResult?.msg || "登录失败");
|
||||
}
|
||||
|
||||
const { mpUserId, id, openId, unionId, nickName } = loginResult.data;
|
||||
wx.setStorageSync("openId", openId);
|
||||
wx.setStorageSync("userId", mpUserId);
|
||||
wx.setStorageSync("mbuId", id);
|
||||
wx.setStorageSync("unionId", unionId);
|
||||
wx.setStorageSync("userName", nickName);
|
||||
wx.setStorageSync("avatarUrl", infoRes.userInfo.avatarUrl);
|
||||
|
||||
this.setData({ mbuId: id, userName: nickName });
|
||||
app.globalData.isLogin = true;
|
||||
this.triggerEvent("loginSuccess", {
|
||||
userName: nickName,
|
||||
avatarUrl: infoRes.userInfo.avatarUrl,
|
||||
});
|
||||
console.log("登录成功");
|
||||
} catch (err: any) {
|
||||
console.log("登录失败:", err);
|
||||
app.globalData.isLogin = false;
|
||||
wx.showToast({
|
||||
title: `登录失败:${err.message || err.errMsg}`,
|
||||
icon: "none",
|
||||
});
|
||||
} finally {
|
||||
wx.hideLoading();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
<!--components/login/login.wxml-->
|
||||
<view class="loginContainer" wx:if="{{!userName||!mbuId}}" catch:touchmove="emptyEvent">
|
||||
<view class="contain">
|
||||
<text>观看请点击学习课程,如不想观看请点击右上角圆圈“○”,退出小程序</text>
|
||||
<view class="login" bind:tap="handleLogin">学习课程</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/* 容器样式 */
|
||||
.quiz-container {
|
||||
padding: 40rpx 30rpx 230rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
/* 题目卡片 */
|
||||
.question-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 题目标题 */
|
||||
.question-title {
|
||||
margin-bottom: 30rpx;
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline;
|
||||
font-size: 28rpx;
|
||||
margin-right: 16rpx;
|
||||
border-radius: 6rpx;
|
||||
font-size: 36rpx;
|
||||
color: #04befe
|
||||
}
|
||||
|
||||
.question-text {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
/* 选项区域 */
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
/* 选项项 */
|
||||
.option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #f8f8f8;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.option-item:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 选项文本 */
|
||||
.option-item text {
|
||||
margin-left: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
/* 正确答案样式 */
|
||||
.option-item.correct {
|
||||
background: #f6ffed;
|
||||
border: 2rpx solid #b7eb8f;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.option-item.correct text {
|
||||
color: #52c41a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 错误答案样式 */
|
||||
.option-item.wrong {
|
||||
background: #fff2f0;
|
||||
border: 2rpx solid #ffccc7;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.option-item.wrong text {
|
||||
color: #ff4d4f;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 选中状态样式 - 使用微信小程序兼容的方式 */
|
||||
.option-item radio:checked + text,
|
||||
.option-item checkbox:checked + text {
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 由于微信小程序不支持:has()选择器,我们通过JS动态添加class来实现选中状态样式 */
|
||||
.option-item.selected {
|
||||
background: #e6f7ff;
|
||||
// border: 2rpx solid #91d5ff;
|
||||
}
|
||||
|
||||
.option-item.selected text {
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
// 定义接口类型
|
||||
interface Question {
|
||||
id: number;
|
||||
type: number;
|
||||
question: string;
|
||||
option: Record<string, any>;
|
||||
}
|
||||
|
||||
interface AnswerItem {
|
||||
queId: string;
|
||||
ans: string[];
|
||||
suc?: boolean;
|
||||
}
|
||||
|
||||
interface QuizComponent {
|
||||
properties: {
|
||||
questions: Question[];
|
||||
answerList: AnswerItem[];
|
||||
};
|
||||
data: {
|
||||
selectedAnswers: Record<number, string[]>;
|
||||
renderQuestions: Array<
|
||||
Question & { options: Array<{ key: string; value: string }> }
|
||||
>;
|
||||
};
|
||||
setData: (data: Partial<Record<string, any>>, callback?: () => void) => void;
|
||||
triggerEvent: (name: string, data?: any) => void;
|
||||
isSelected: (qid: string | number, key: string) => boolean;
|
||||
getSelectedAnswer: (qid: string | number, index: number) => string;
|
||||
isCorrect: (qid: string, key: string) => boolean;
|
||||
isWrong: (qid: string, key: string) => boolean;
|
||||
handleRadioChange: (e: WechatMiniprogram.RadioGroupChange) => void;
|
||||
handleCheckboxChange: (e: WechatMiniprogram.CheckboxGroupChange) => void;
|
||||
getFormattedAnswers: () => Array<{
|
||||
questionId: number;
|
||||
answers: string[];
|
||||
questionType: number;
|
||||
}>;
|
||||
getAllAnswers: () => Array<{
|
||||
questionId: number;
|
||||
answers: string[];
|
||||
questionType: number;
|
||||
}>;
|
||||
isAllAnswered: () => boolean;
|
||||
getProgress: () => { answered: number; total: number; percentage: number };
|
||||
}
|
||||
|
||||
// 定义常量
|
||||
const QUESTION_TYPE = {
|
||||
SINGLE: 1,
|
||||
MULTIPLE: 2,
|
||||
};
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
questions: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
answerList: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
},
|
||||
|
||||
observers: {
|
||||
"questions, answerList": function (this: QuizComponent) {
|
||||
const renderQuestions = (this.properties.questions || []).map(
|
||||
(q: Question) => ({
|
||||
...q,
|
||||
options: Object.keys(q.option || {}).map((k) => ({
|
||||
key: k,
|
||||
value: q.option[k],
|
||||
})),
|
||||
})
|
||||
);
|
||||
|
||||
const answerList = Array.isArray(this.properties.answerList)
|
||||
? this.properties.answerList
|
||||
: [];
|
||||
const selectedAnswers = answerList.reduce((acc, item) => {
|
||||
if (item && item.queId && item.ans) {
|
||||
acc[Number(item.queId)] = Array.isArray(item.ans)
|
||||
? item.ans
|
||||
: [item.ans];
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<number, string[]>);
|
||||
|
||||
this.setData({
|
||||
renderQuestions,
|
||||
selectedAnswers,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
selectedAnswers: {} as Record<number, string[]>,
|
||||
renderQuestions: [] as Array<
|
||||
Question & { options: Array<{ key: string; value: string }> }
|
||||
>,
|
||||
},
|
||||
|
||||
methods: {
|
||||
isSelected(this: QuizComponent, qid: string | number, key: string) {
|
||||
const selected = this.data.selectedAnswers[Number(qid)];
|
||||
return Array.isArray(selected) && selected.includes(key);
|
||||
},
|
||||
|
||||
getSelectedAnswer(this: QuizComponent, qid: string | number, index: number) {
|
||||
const selected = this.data.selectedAnswers[Number(qid)];
|
||||
return Array.isArray(selected) ? selected[index] : "";
|
||||
},
|
||||
|
||||
isCorrect(this: QuizComponent, qid: string, key: string) {
|
||||
const answerList = Array.isArray(this.properties.answerList)
|
||||
? this.properties.answerList
|
||||
: [];
|
||||
return answerList.some((item: AnswerItem) => {
|
||||
const answers = Array.isArray(item.ans) ? item.ans : [];
|
||||
return String(item.queId) === String(qid) && answers.includes(key) && item.suc;
|
||||
});
|
||||
},
|
||||
|
||||
isWrong(this: QuizComponent, qid: string, key: string) {
|
||||
const answerList = Array.isArray(this.properties.answerList)
|
||||
? this.properties.answerList
|
||||
: [];
|
||||
return answerList.some((item: AnswerItem) => {
|
||||
const answers = Array.isArray(item.ans) ? item.ans : [];
|
||||
return String(item.queId) === String(qid) && answers.includes(key) && !item.suc;
|
||||
});
|
||||
},
|
||||
|
||||
// 处理单选变化 { queId: q.id, ans: selectedAnswers }
|
||||
handleRadioChange(
|
||||
this: QuizComponent,
|
||||
e: WechatMiniprogram.RadioGroupChange
|
||||
) {
|
||||
const { questionId } = e.currentTarget.dataset;
|
||||
const value = e.detail.value;
|
||||
if (!questionId) return;
|
||||
const numericQuestionId = Number(questionId);
|
||||
const newAnswers = value ? [value] : [];
|
||||
|
||||
this.setData({
|
||||
[`selectedAnswers.${numericQuestionId}`]: newAnswers,
|
||||
});
|
||||
this.triggerEvent("change", {
|
||||
queId: numericQuestionId,
|
||||
ans: newAnswers,
|
||||
});
|
||||
},
|
||||
|
||||
// 处理多选变化
|
||||
handleCheckboxChange(
|
||||
this: QuizComponent,
|
||||
e: WechatMiniprogram.CheckboxGroupChange
|
||||
) {
|
||||
const { questionId } = e.currentTarget.dataset;
|
||||
const values = Array.isArray(e.detail.value) ? e.detail.value : [];
|
||||
if (!questionId) return;
|
||||
const numericQuestionId = Number(questionId);
|
||||
this.setData({
|
||||
[`selectedAnswers.${numericQuestionId}`]: values,
|
||||
});
|
||||
|
||||
this.triggerEvent("change", {
|
||||
queId: numericQuestionId,
|
||||
ans: values,
|
||||
});
|
||||
},
|
||||
|
||||
getFormattedAnswers(this: QuizComponent) {
|
||||
const answers = [];
|
||||
for (const [questionId, selectedOptions] of Object.entries(
|
||||
this.data.selectedAnswers
|
||||
)) {
|
||||
if (selectedOptions && selectedOptions.length > 0) {
|
||||
answers.push({
|
||||
queId: Number(questionId),
|
||||
ans: selectedOptions,
|
||||
});
|
||||
}
|
||||
}
|
||||
return answers;
|
||||
},
|
||||
|
||||
getAllAnswers(this: QuizComponent) {
|
||||
return this.getFormattedAnswers();
|
||||
},
|
||||
|
||||
isAllAnswered(this: QuizComponent) {
|
||||
const totalQuestions = this.properties.questions.length;
|
||||
const answeredQuestions = Object.values(this.data.selectedAnswers).filter(
|
||||
(answers) => answers && answers.length > 0
|
||||
).length;
|
||||
return totalQuestions === answeredQuestions;
|
||||
},
|
||||
|
||||
getProgress(this: QuizComponent) {
|
||||
const totalQuestions = this.data.renderQuestions.length;
|
||||
const answeredQuestions = Object.values(this.data.selectedAnswers).filter(
|
||||
(answers) => answers && answers.length > 0
|
||||
).length;
|
||||
return {
|
||||
answered: answeredQuestions,
|
||||
total: totalQuestions,
|
||||
percentage:
|
||||
totalQuestions > 0
|
||||
? Math.round((answeredQuestions / totalQuestions) * 100)
|
||||
: 0,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
<!--components/my-quiz/my-quiz.wxml-->
|
||||
<view class="quiz-container">
|
||||
<block wx:for="{{renderQuestions}}" wx:key="id" wx:for-item="q" wx:for-index="qi">
|
||||
<view class="question-card">
|
||||
<view class="question-title">
|
||||
<text class="tag">{{qi + 1}}.</text>
|
||||
<text class="tag" wx:if="{{q.type == 2}}">[ 多选题 ]</text>
|
||||
<text class="tag" wx:else>[ 单选题 ]</text>
|
||||
{{q.question}}
|
||||
</view>
|
||||
<view class="options">
|
||||
<!-- 单选题使用 radio-group -->
|
||||
<radio-group
|
||||
wx:if="{{q.type == 1}}"
|
||||
bindchange="handleRadioChange"
|
||||
data-question-id="{{q.id}}"
|
||||
value="{{getSelectedAnswer(q.id, 0)}}"
|
||||
>
|
||||
<block wx:for="{{q.options}}" wx:key="key" wx:for-item="op" wx:if="{{op.value}}">
|
||||
<label class="option-item {{isCorrect(q.id, op.key) ? 'correct' : isWrong(q.id, op.key) ? 'wrong' : ''}} {{getSelectedAnswer(q.id, 0) === op.key ? 'selected' : ''}}">
|
||||
<radio
|
||||
value="{{op.key}}"
|
||||
checked="{{getSelectedAnswer(q.id, 0) === op.key}}"
|
||||
/>
|
||||
<text>{{op.key}}. {{op.value}}</text>
|
||||
</label>
|
||||
</block>
|
||||
</radio-group>
|
||||
|
||||
<!-- 多选题使用 checkbox-group -->
|
||||
<checkbox-group
|
||||
wx:if="{{q.type == 2}}"
|
||||
bindchange="handleCheckboxChange"
|
||||
data-question-id="{{q.id}}"
|
||||
value="{{selectedAnswers[q.id] || []}}"
|
||||
>
|
||||
<block wx:for="{{q.options}}" wx:key="key" wx:for-item="op" wx:if="{{op.value}}">
|
||||
<label class="option-item {{isCorrect(q.id, op.key) ? 'correct' : isWrong(q.id, op.key) ? 'wrong' : ''}} {{isSelected(q.id, op.key) ? 'selected' : ''}}">
|
||||
<checkbox
|
||||
value="{{op.key}}"
|
||||
checked="{{isSelected(q.id, op.key)}}"
|
||||
/>
|
||||
<text>{{op.key}}. {{op.value}}</text>
|
||||
</label>
|
||||
</block>
|
||||
</checkbox-group>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/* components/my-video.wxss */
|
||||
.live-content {
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.video {
|
||||
width: 100%;
|
||||
min-height: 400rpx; /* 200px -> 400rpx */
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
.screen {
|
||||
.video {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
.tools {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
bottom: 0;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
color: white;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
.video-poster {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
z-index: 2;
|
||||
.play-button {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
.play-icon {
|
||||
font-size: 50rpx;
|
||||
color:white;
|
||||
margin-left: 6rpx;
|
||||
margin-bottom: 3rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
.poster-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
.cover {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
z-index: 998;
|
||||
height: 20%;
|
||||
width: 70%;
|
||||
left: 10%;
|
||||
background-color: rgba(255, 255, 255, 0.007);
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
// components/my-video/index.ts
|
||||
import { AES_KEY, AES_IV } from "../../env";
|
||||
import { request } from "../../utils/request";
|
||||
import { aesEncrypt } from "../../utils/crypto";
|
||||
|
||||
interface VideoData {
|
||||
screen: boolean;
|
||||
playing: boolean;
|
||||
currentTime: number;
|
||||
isReady: boolean;
|
||||
sendDurationTimer: number | null;
|
||||
lastUpdateTime: number;
|
||||
isConnected: boolean; // 新增网络连接状态字段
|
||||
}
|
||||
interface MyVideoComponent {
|
||||
data: VideoData;
|
||||
videoContext: WechatMiniprogram.VideoContext | null;
|
||||
pauseVideoHandler?: () => void;
|
||||
resumeVideoHandler?: () => void;
|
||||
setData: (d: Partial<VideoData>, cb?: () => void) => void;
|
||||
initNetworkListener: () => void;
|
||||
startSendProgress: () => void;
|
||||
clearSendProgressTimer: () => void;
|
||||
sendProgress: () => Promise<void>;
|
||||
}
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
src: String,
|
||||
backgroundUrl: String,
|
||||
},
|
||||
|
||||
data: {
|
||||
screen: false,
|
||||
playing: false,
|
||||
currentTime: 0,
|
||||
isReady: false,
|
||||
isConnected: false,
|
||||
sendDurationTimer: 0,
|
||||
lastUpdateTime: 0,
|
||||
} as VideoData,
|
||||
|
||||
lifetimes: {
|
||||
attached(this: MyVideoComponent) {
|
||||
this.initNetworkListener();
|
||||
|
||||
// 获取 videoContext 并初始化
|
||||
this.videoContext = wx.createVideoContext("videoPlayer", this as any);
|
||||
|
||||
const pauseVideo = () => {
|
||||
if (this.data.playing) {
|
||||
wx.setStorageSync("interruptedTime", this.data.currentTime);
|
||||
this.videoContext?.pause();
|
||||
this.setData({ playing: false });
|
||||
this.clearSendProgressTimer();
|
||||
}
|
||||
};
|
||||
|
||||
const resumeVideo = () => {
|
||||
if (!this.videoContext) {
|
||||
this.videoContext = wx.createVideoContext("videoPlayer", this as any);
|
||||
}
|
||||
this.videoContext.pause();
|
||||
};
|
||||
|
||||
this.pauseVideoHandler = pauseVideo;
|
||||
this.resumeVideoHandler = resumeVideo;
|
||||
|
||||
wx.onAudioInterruptionBegin(this.pauseVideoHandler);
|
||||
wx.onAudioInterruptionEnd(this.resumeVideoHandler);
|
||||
wx.onAppHide(this.pauseVideoHandler);
|
||||
wx.onAppShow(this.resumeVideoHandler);
|
||||
},
|
||||
|
||||
detached(this: MyVideoComponent) {
|
||||
if (this.pauseVideoHandler) {
|
||||
wx.offAudioInterruptionBegin(this.pauseVideoHandler);
|
||||
wx.offAppHide(this.pauseVideoHandler);
|
||||
}
|
||||
if (this.resumeVideoHandler) {
|
||||
wx.offAudioInterruptionEnd(this.resumeVideoHandler);
|
||||
wx.offAppShow(this.resumeVideoHandler);
|
||||
}
|
||||
this.clearSendProgressTimer();
|
||||
this.videoContext = null;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
initNetworkListener(this: MyVideoComponent) {
|
||||
// 首先获取当前网络状态
|
||||
wx.getNetworkType({
|
||||
success: (res) => {
|
||||
this.setData({
|
||||
isConnected: res.networkType !== "none",
|
||||
});
|
||||
},
|
||||
});
|
||||
wx.onNetworkStatusChange(
|
||||
(res: WechatMiniprogram.OnNetworkStatusChangeCallbackResult) => {
|
||||
this.setData({
|
||||
isConnected: res.isConnected,
|
||||
});
|
||||
if (!res.isConnected && this.data.playing) {
|
||||
this.videoContext?.pause();
|
||||
this.setData({ playing: false });
|
||||
wx.showToast({
|
||||
title: "网络连接已断开",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
if (res.isConnected) {
|
||||
wx.showToast({
|
||||
title: "网络已连接",
|
||||
icon: "success",
|
||||
duration: 1500,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
handleLoadedMetadata(this: MyVideoComponent) {
|
||||
this.setData({ isReady: true });
|
||||
},
|
||||
|
||||
handlePosterTap(this: MyVideoComponent) {
|
||||
if (!this.videoContext || !this.data.isReady) return;
|
||||
if (!this.data.isConnected) {
|
||||
wx.showToast({
|
||||
title: "网络连接异常,请检查网络设置",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newPlaying = !this.data.playing;
|
||||
const resumeTime =
|
||||
(wx.getStorageSync("interruptedTime") as number) ||
|
||||
this.data.currentTime ||
|
||||
(wx.getStorageSync("currentTime") as number) ||
|
||||
0;
|
||||
|
||||
if (newPlaying) {
|
||||
if (resumeTime > 0) {
|
||||
this.videoContext.seek(resumeTime);
|
||||
}
|
||||
this.videoContext.play();
|
||||
if (!this.data.sendDurationTimer) {
|
||||
this.startSendProgress();
|
||||
}
|
||||
} else {
|
||||
this.videoContext.pause();
|
||||
this.clearSendProgressTimer();
|
||||
}
|
||||
|
||||
this.setData({ playing: newPlaying });
|
||||
wx.removeStorageSync("interruptedTime");
|
||||
},
|
||||
|
||||
handlePause(this: MyVideoComponent) {
|
||||
wx.setStorageSync("interruptedTime", this.data.currentTime);
|
||||
this.setData({ playing: false });
|
||||
this.clearSendProgressTimer();
|
||||
},
|
||||
|
||||
handleTimeUpdate(
|
||||
this: MyVideoComponent,
|
||||
e: WechatMiniprogram.VideoTimeUpdate
|
||||
) {
|
||||
const now = Date.now();
|
||||
const currentTime = Math.floor(e.detail.currentTime ?? 0);
|
||||
if (now - this.data.lastUpdateTime >= 1000) {
|
||||
this.setData({ currentTime });
|
||||
(this.data as VideoData).lastUpdateTime = now;
|
||||
wx.setStorageSync("currentTime", currentTime);
|
||||
}
|
||||
},
|
||||
|
||||
handleEnded(this: MyVideoComponent) {
|
||||
this.sendProgress();
|
||||
wx.setStorageSync("ended", true);
|
||||
this.setData({ playing: false, currentTime: 0 });
|
||||
wx.removeStorageSync("currentTime");
|
||||
wx.removeStorageSync("interruptedTime");
|
||||
this.clearSendProgressTimer();
|
||||
},
|
||||
|
||||
handleFullscreenChange(this: MyVideoComponent) {
|
||||
const newScreen = !this.data.screen;
|
||||
if (newScreen) {
|
||||
this.videoContext?.requestFullScreen({ direction: 90 });
|
||||
} else {
|
||||
this.videoContext?.exitFullScreen();
|
||||
}
|
||||
this.setData({ screen: newScreen });
|
||||
},
|
||||
|
||||
handleError(this: MyVideoComponent, e: WechatMiniprogram.VideoError) {
|
||||
console.error("播放错误:", e.detail.errMsg);
|
||||
this.setData({ playing: false });
|
||||
},
|
||||
|
||||
startSendProgress(this: MyVideoComponent) {
|
||||
this.clearSendProgressTimer();
|
||||
const timer = setInterval(() => {
|
||||
this.sendProgress();
|
||||
}, 15000) as unknown as number;
|
||||
this.setData({ sendDurationTimer: timer });
|
||||
},
|
||||
|
||||
clearSendProgressTimer(this: MyVideoComponent) {
|
||||
const t = this.data.sendDurationTimer;
|
||||
if (t) {
|
||||
clearInterval(t);
|
||||
this.setData({ sendDurationTimer: 0 });
|
||||
}
|
||||
},
|
||||
|
||||
async sendProgress(this: MyVideoComponent) {
|
||||
try {
|
||||
const openId = wx.getStorageSync("openId");
|
||||
const code = wx.getStorageSync("code");
|
||||
const state = wx.getStorageSync("state");
|
||||
const seeId = wx.getStorageSync("seeId");
|
||||
const duration = this.data.currentTime + 1;
|
||||
if (code) {
|
||||
const params = {
|
||||
openId,
|
||||
code,
|
||||
duration
|
||||
};
|
||||
await request({
|
||||
options: {
|
||||
url: `/app/video-watch/inspect`,
|
||||
data: aesEncrypt(JSON.stringify(params), AES_KEY, AES_IV),
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
} else {
|
||||
const params = {
|
||||
openId,
|
||||
state,
|
||||
seeId,
|
||||
duration
|
||||
};
|
||||
await request({
|
||||
options: {
|
||||
url: `/app/watch/inspect`,
|
||||
data: aesEncrypt(JSON.stringify(params), AES_KEY, AES_IV),
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("上报观看进度失败:", err);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
<!--components/my-video.wxml-->
|
||||
<view class="live-content">
|
||||
<video id="videoPlayer"
|
||||
class="video"
|
||||
danmu-list="{{danmuList}}"
|
||||
enable-danmu="{{false}}"
|
||||
show-fullscreen-btn
|
||||
show-center-play-btn='{{false}}'
|
||||
enable-progress-gesture="{{false}}"
|
||||
show-play-btn="{{true}}"
|
||||
controls="{{true}}"
|
||||
object-fit="contain"
|
||||
picture-in-picture-mode="{{[]}}"
|
||||
show-background-playback-button="{{false}}"
|
||||
src="{{src}}"
|
||||
bindended="handleEnded"
|
||||
binderror="handleError"
|
||||
bindpause="handlePause"
|
||||
bindtimeupdate="handleTimeUpdate"
|
||||
bindfullscreenchange="handleFullscreenChange"
|
||||
bindloadedmetadata="handleLoadedMetadata"
|
||||
>
|
||||
<cover-view class="cover"></cover-view>
|
||||
<image wx:if="{{!playing}}" src="{{backgroundUrl}}" mode="aspectFill" class="poster-image"></image>
|
||||
<view class="video-poster" wx:if="{{!playing}}" catchtap="handlePosterTap">
|
||||
<view class="play-button">
|
||||
<view class="play-icon">
|
||||
▶
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</video>
|
||||
</view>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"component": true,
|
||||
"styleIsolation": "apply-shared",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
.weui-navigation-bar {
|
||||
--weui-FG-0:rgba(0,0,0,.9);
|
||||
--height: 44px;
|
||||
--left: 16px;
|
||||
}
|
||||
.weui-navigation-bar .android {
|
||||
--height: 48px;
|
||||
}
|
||||
|
||||
.weui-navigation-bar {
|
||||
overflow: hidden;
|
||||
color: var(--weui-FG-0);
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.weui-navigation-bar__inner {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: calc(var(--height) + env(safe-area-inset-top));
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: env(safe-area-inset-top);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.weui-navigation-bar__left {
|
||||
position: relative;
|
||||
padding-left: var(--left);
|
||||
padding-top: 16rpx; //默认位置偏高
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.weui-navigation-bar__btn_goback_wrapper {
|
||||
padding: 11px 18px 11px 16px;
|
||||
margin: -11px -18px -11px -16px;
|
||||
}
|
||||
|
||||
.weui-navigation-bar__btn_goback_wrapper.weui-active {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.weui-navigation-bar__btn_goback {
|
||||
font-size: 12px;
|
||||
width: 12px;
|
||||
height: 24px;
|
||||
-webkit-mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%;
|
||||
mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%;
|
||||
-webkit-mask-size: cover;
|
||||
mask-size: cover;
|
||||
background-color: var(--weui-FG-0);
|
||||
}
|
||||
|
||||
.weui-navigation-bar__center {
|
||||
font-size: 17px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.weui-navigation-bar__loading {
|
||||
margin-right: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.weui-loading {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
background: transparent url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='80px' height='80px' viewBox='0 0 80 80' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3Eloading%3C/title%3E%3Cdefs%3E%3ClinearGradient x1='94.0869141%25' y1='0%25' x2='94.0869141%25' y2='90.559082%25' id='linearGradient-1'%3E%3Cstop stop-color='%23606060' stop-opacity='0' offset='0%25'%3E%3C/stop%3E%3Cstop stop-color='%23606060' stop-opacity='0.3' offset='100%25'%3E%3C/stop%3E%3C/linearGradient%3E%3ClinearGradient x1='100%25' y1='8.67370605%25' x2='100%25' y2='90.6286621%25' id='linearGradient-2'%3E%3Cstop stop-color='%23606060' offset='0%25'%3E%3C/stop%3E%3Cstop stop-color='%23606060' stop-opacity='0.3' offset='100%25'%3E%3C/stop%3E%3C/linearGradient%3E%3C/defs%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' opacity='0.9'%3E%3Cg%3E%3Cpath d='M40,0 C62.09139,0 80,17.90861 80,40 C80,62.09139 62.09139,80 40,80 L40,73 C58.2253967,73 73,58.2253967 73,40 C73,21.7746033 58.2253967,7 40,7 L40,0 Z' fill='url(%23linearGradient-1)'%3E%3C/path%3E%3Cpath d='M40,0 L40,7 C21.7746033,7 7,21.7746033 7,40 C7,58.2253967 21.7746033,73 40,73 L40,80 C17.90861,80 0,62.09139 0,40 C0,17.90861 17.90861,0 40,0 Z' fill='url(%23linearGradient-2)'%3E%3C/path%3E%3Ccircle id='Oval' fill='%23606060' cx='40.5' cy='3.5' r='3.5'%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A") no-repeat;
|
||||
background-size: 100%;
|
||||
margin-left: 0;
|
||||
animation: loading linear infinite 1s;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
from {
|
||||
transform: rotate(0);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
interface NavigationBarComponent {
|
||||
data: {
|
||||
displayStyle: string;
|
||||
ios?: boolean;
|
||||
innerPaddingRight?: string;
|
||||
leftWidth?: string;
|
||||
safeAreaTop?: string;
|
||||
delta?: any;
|
||||
animated?: any;
|
||||
};
|
||||
setData: (
|
||||
data: Partial<NavigationBarComponent["data"]>,
|
||||
callback?: () => void
|
||||
) => void;
|
||||
triggerEvent: <DetailType = any>(
|
||||
name: string,
|
||||
detail?: DetailType,
|
||||
options?: WechatMiniprogram.Component.TriggerEventOption
|
||||
) => void;
|
||||
_showChange: (show: boolean) => void;
|
||||
back: () => void;
|
||||
handleBack: () => void;
|
||||
}
|
||||
|
||||
Component({
|
||||
options: {
|
||||
multipleSlots: true, // 在组件定义时的选项中启用多slot支持
|
||||
},
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
extClass: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
background: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
back: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
homeButton: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
animated: {
|
||||
// 显示隐藏的时候opacity动画效果
|
||||
type: Boolean,
|
||||
value: true,
|
||||
},
|
||||
show: {
|
||||
// 显示隐藏导航,隐藏的时候navigation-bar的高度占位还在
|
||||
type: Boolean,
|
||||
value: true,
|
||||
observer: "_showChange",
|
||||
},
|
||||
// back为true的时候,返回的页面深度
|
||||
delta: {
|
||||
type: Number,
|
||||
value: 1,
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
displayStyle: "",
|
||||
},
|
||||
lifetimes: {
|
||||
attached(this: NavigationBarComponent) {
|
||||
const rect = wx.getMenuButtonBoundingClientRect();
|
||||
wx.getSystemInfo({
|
||||
success: (res) => {
|
||||
const isAndroid = res.platform === "android";
|
||||
const isDevtools = res.platform === "devtools";
|
||||
this.setData({
|
||||
ios: !isAndroid,
|
||||
innerPaddingRight: `padding-right: ${res.windowWidth - rect.left
|
||||
}px`,
|
||||
leftWidth: `width: ${res.windowWidth - rect.left}px`,
|
||||
safeAreaTop:
|
||||
isDevtools || isAndroid
|
||||
? `height: calc(var(--height) + ${res.safeArea.top
|
||||
}px); padding-top: ${res.safeArea.top || 24}px`
|
||||
: ``,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
_showChange(this: NavigationBarComponent, show: boolean) {
|
||||
const animated = this.data.animated;
|
||||
let displayStyle = "";
|
||||
if (animated) {
|
||||
displayStyle = `opacity: ${show ? "1" : "0"};transition:opacity 0.5s;`;
|
||||
} else {
|
||||
displayStyle = `display: ${show ? "" : "none"}`;
|
||||
}
|
||||
this.setData({
|
||||
displayStyle,
|
||||
});
|
||||
},
|
||||
back(this: NavigationBarComponent) {
|
||||
this.triggerEvent("handleBack", { delta: 1 });
|
||||
const data = this.data;
|
||||
const pages = getCurrentPages();
|
||||
|
||||
//通过链接进入,或只有一个栈时,点击返回是没有用的,直接返回首页
|
||||
if (pages.length <= 1) {
|
||||
wx.switchTab({ url: "/pages/tab-bar/medicine-box/index" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.delta) {
|
||||
wx.navigateBack({
|
||||
delta: data.delta,
|
||||
});
|
||||
} else {
|
||||
wx.switchTab({ url: "/pages/tab-bar/medicine-box/index" });
|
||||
}
|
||||
this.triggerEvent("back", { delta: data.delta }, {});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
<view class="weui-navigation-bar {{extClass}}">
|
||||
<view class="weui-navigation-bar__inner {{ios ? 'ios' : 'android'}}" style="color: {{color}}; background: {{background}}; {{displayStyle}}; {{innerPaddingRight}}; {{safeAreaTop}};">
|
||||
|
||||
<!-- 左侧按钮 -->
|
||||
<view class='weui-navigation-bar__left' style="{{leftWidth}};">
|
||||
<block wx:if="{{back || homeButton}}">
|
||||
<!-- 返回上一页 -->
|
||||
<block wx:if="{{back}}">
|
||||
<view class="weui-navigation-bar__buttons weui-navigation-bar__buttons_goback">
|
||||
<view
|
||||
bindtap="back"
|
||||
class="weui-navigation-bar__btn_goback_wrapper"
|
||||
hover-class="weui-active"
|
||||
hover-stay-time="100"
|
||||
aria-role="button"
|
||||
aria-label="返回"
|
||||
>
|
||||
<view class="weui-navigation-bar__button weui-navigation-bar__btn_goback"></view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<!-- 返回首页 -->
|
||||
<block wx:if="{{homeButton}}">
|
||||
<view class="weui-navigation-bar__buttons weui-navigation-bar__buttons_home">
|
||||
<view
|
||||
bindtap="home"
|
||||
class="weui-navigation-bar__btn_home_wrapper"
|
||||
hover-class="weui-active"
|
||||
aria-role="button"
|
||||
aria-label="首页"
|
||||
>
|
||||
<view class="weui-navigation-bar__button weui-navigation-bar__btn_home"></view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<slot name="left"></slot>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 标题 -->
|
||||
<view class='weui-navigation-bar__center'>
|
||||
<view wx:if="{{loading}}" class="weui-navigation-bar__loading" aria-role="alert">
|
||||
<view
|
||||
class="weui-loading"
|
||||
aria-role="img"
|
||||
aria-label="加载中"
|
||||
></view>
|
||||
</view>
|
||||
<block wx:if="{{title}}">
|
||||
<text>{{title}}</text>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<slot name="center"></slot>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 右侧留空 -->
|
||||
<view class='weui-navigation-bar__right'>
|
||||
<slot name="right"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"pageOrientation": "portrait",
|
||||
"usingComponents": {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/* components/player.wxss */
|
||||
.player-object {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.player-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.video {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
interface PlayerProperties {
|
||||
isPlayback: boolean;
|
||||
isLiving: boolean;
|
||||
pullUrlInfo: {
|
||||
rtmpUrl: string;
|
||||
rtmpOriaacUrl: string;
|
||||
hlsOriaacUrl: string;
|
||||
hlsUrl: string;
|
||||
};
|
||||
vodInfo: {
|
||||
state: number;
|
||||
pullUrl: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PlayerData {
|
||||
liveUrl: string;
|
||||
vodUrl: string;
|
||||
isPlaying: boolean;
|
||||
showVideo: boolean;
|
||||
fullScreen: boolean;
|
||||
height: number;
|
||||
retryCount: number;
|
||||
maxRetryCount: number;
|
||||
}
|
||||
|
||||
interface PlayerComponent extends WechatMiniprogram.Component.TrivialInstance {
|
||||
data: PlayerData;
|
||||
properties: PlayerProperties;
|
||||
videoContext: WechatMiniprogram.VideoContext | null;
|
||||
}
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
isPlayback: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
isLiving: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
pullUrlInfo: {
|
||||
type: Object,
|
||||
value: {
|
||||
rtmpUrl: "",
|
||||
rtmpOriaacUrl: "",
|
||||
hlsOriaacUrl: "",
|
||||
hlsUrl: "",
|
||||
},
|
||||
},
|
||||
vodInfo: {
|
||||
type: Object,
|
||||
value: {
|
||||
state: 0,
|
||||
pullUrl: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
liveUrl: "",
|
||||
vodUrl: "",
|
||||
isPlaying: false,
|
||||
fullScreen: false,
|
||||
height: 0,
|
||||
showVideo: true,
|
||||
retryCount: 0,
|
||||
maxRetryCount: 3,
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached(this: PlayerComponent) {
|
||||
this.initPlayer();
|
||||
},
|
||||
detached(this: PlayerComponent) {
|
||||
this.cleanup();
|
||||
},
|
||||
},
|
||||
|
||||
observers: {
|
||||
"isLiving, pullUrlInfo": function (
|
||||
this: PlayerComponent,
|
||||
isLiving: boolean,
|
||||
pullUrlInfo: PlayerProperties["pullUrlInfo"]
|
||||
) {
|
||||
if (isLiving && pullUrlInfo.rtmpUrl) {
|
||||
this.setData({ liveUrl: pullUrlInfo.rtmpUrl });
|
||||
} else {
|
||||
this.setData({ liveUrl: "" });
|
||||
}
|
||||
},
|
||||
vodInfo: function (
|
||||
this: PlayerComponent,
|
||||
data: PlayerProperties["vodInfo"]
|
||||
) {
|
||||
if (data && data.pullUrl) {
|
||||
this.setData({
|
||||
vodUrl: data.pullUrl,
|
||||
isPlaying: true,
|
||||
showVideo: true,
|
||||
});
|
||||
this.playVideo();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
playVideo(this: PlayerComponent) {
|
||||
if (!this.videoContext || !this.data.showVideo) return;
|
||||
setTimeout(() => {
|
||||
try {
|
||||
this.videoContext?.play();
|
||||
} catch (error) {
|
||||
this.handlePlayError();
|
||||
}
|
||||
}, 500);
|
||||
},
|
||||
|
||||
handlePlayError(this: PlayerComponent) {
|
||||
const { retryCount, maxRetryCount } = this.data;
|
||||
if (retryCount < maxRetryCount) {
|
||||
this.setData({ retryCount: retryCount + 1 });
|
||||
setTimeout(() => {
|
||||
this.playVideo();
|
||||
}, 2000);
|
||||
} else {
|
||||
this.setData({
|
||||
showVideo: false,
|
||||
retryCount: 0,
|
||||
});
|
||||
wx.showToast({
|
||||
title: "播放失败,请检查网络连接",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
initPlayer(this: PlayerComponent) {
|
||||
try {
|
||||
const sysInfo = wx.getSystemInfoSync();
|
||||
this.setData({ height: sysInfo.screenHeight });
|
||||
this.videoContext = wx.createVideoContext("vodVideo", this);
|
||||
} catch (err) {
|
||||
console.error("初始化播放器失败:", err);
|
||||
wx.showToast({ title: "播放器初始化失败", icon: "none" });
|
||||
}
|
||||
},
|
||||
|
||||
onLiveStateChange(
|
||||
this: PlayerComponent,
|
||||
event: WechatMiniprogram.CustomEvent
|
||||
) {
|
||||
const code = event.detail.code;
|
||||
const stateMap: Record<number, string> = {
|
||||
2001: "已连接",
|
||||
2002: "已断开",
|
||||
2003: "缓冲中",
|
||||
2004: "播放开始",
|
||||
2005: "播放结束",
|
||||
2006: "加载中",
|
||||
};
|
||||
console.log("直播状态:", stateMap[code] || `未知(${code})`);
|
||||
},
|
||||
|
||||
onLiveError(this: PlayerComponent, event: WechatMiniprogram.CustomEvent) {
|
||||
const errCode = event.detail.errCode;
|
||||
const errorMap: Record<number, string> = {
|
||||
10001: "系统错误",
|
||||
10002: "网络错误",
|
||||
10003: "解码错误",
|
||||
10004: "直播流不存在",
|
||||
};
|
||||
const msg = errorMap[errCode] || `直播错误: ${event.detail.errMsg}`;
|
||||
wx.showToast({ title: msg, icon: "none" });
|
||||
},
|
||||
|
||||
onVideoPlay(this: PlayerComponent) {
|
||||
this.setData({ isPlaying: true, retryCount: 0, showVideo: true });
|
||||
},
|
||||
onVideoPause(this: PlayerComponent) {
|
||||
this.setData({ isPlaying: false });
|
||||
},
|
||||
onVideoEnded(this: PlayerComponent) {
|
||||
this.setData({ isPlaying: false });
|
||||
},
|
||||
onVideoError(this: PlayerComponent, err: WechatMiniprogram.CustomEvent) {
|
||||
if (err) {
|
||||
this.setData({ isPlaying: false, showVideo: false });
|
||||
this.triggerEvent("controlstoggle", { state: 2 });
|
||||
}
|
||||
},
|
||||
|
||||
cleanup(this: PlayerComponent) {
|
||||
this.videoContext = null;
|
||||
this.setData({
|
||||
liveUrl: "",
|
||||
vodUrl: "",
|
||||
isPlaying: false,
|
||||
fullScreen: false,
|
||||
showVideo: true,
|
||||
retryCount: 0,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
<!--components/player.wxml-->
|
||||
<view class="player-object">
|
||||
<video
|
||||
wx:if="{{showVideo && vodUrl}}"
|
||||
id="vodVideo"
|
||||
src="{{vodUrl}}"
|
||||
autoplay="{{true}}"
|
||||
show-fullscreen-btn="{{false}}"
|
||||
show-play-btn="{{false}}"
|
||||
show-center-play-btn="{{false}}"
|
||||
class="video"
|
||||
object-fit="contain"
|
||||
direction="0"
|
||||
bindplay="onVideoPlay"
|
||||
bindpause="onVideoPause"
|
||||
bindended="onVideoEnded"
|
||||
binderror="onVideoError"
|
||||
></video>
|
||||
|
||||
<live-player
|
||||
wx:if="{{liveUrl}}"
|
||||
live-url="{{liveUrl}}"
|
||||
vod-url="{{vodUrl}}"
|
||||
is-playback="{{isPlayback}}"
|
||||
room-title="{{roomInfo.title}}"
|
||||
viewer-count="{{roomInfo.viewerCount}}"
|
||||
is-living="{{roomInfo.isLiving}}"
|
||||
object-fit="contain"
|
||||
mode="live"
|
||||
class="player-video"
|
||||
bindstatechange="onLiveStateChange"
|
||||
binderror="onLiveError"
|
||||
bindplay="onVideoPlay"
|
||||
bindpause="onVideoPause"
|
||||
bindended="onVideoEnded"
|
||||
/>
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
@import "../../variables";
|
||||
|
||||
.mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
z-index: 999;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
.mask.show {
|
||||
opacity: 1;
|
||||
}
|
||||
.mask.hide {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
max-height: 90%;
|
||||
background: #fff;
|
||||
// background: linear-gradient(180deg, #edf0f7 0%, #e7ebf4 100%);
|
||||
// box-shadow: 0 28rpx 70rpx rgba(11, 18, 35, 0.18);
|
||||
border-top-left-radius: $primary-gap;
|
||||
border-top-right-radius: $primary-border-radius;
|
||||
padding: $primary-gap;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
|
||||
.content.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.content.hide {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// @ts-nocheck
|
||||
const systemInfo = wx.getWindowInfo();
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer(newVal:any) {
|
||||
if (newVal) {
|
||||
//先挂载再执行动画
|
||||
this.setData({ showSelf: true }, () => {
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
popupAnimate: 'show',
|
||||
maskAnimate: 'show'
|
||||
})
|
||||
}, 20)
|
||||
})
|
||||
} else {
|
||||
//先执行动画,不见了再卸载
|
||||
this.setData({
|
||||
popupAnimate: 'hide',
|
||||
maskAnimate: 'hide'
|
||||
})
|
||||
// 等动画结束再卸载
|
||||
setTimeout(() => {
|
||||
this.setData({ showSelf: false })
|
||||
}, 500) //和transition一致
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
showSelf: false,
|
||||
safeBottom: systemInfo.safeArea.bottom - systemInfo.safeArea.height, //底部的安全距离,避免被返回键挡住了
|
||||
popupAnimate: '',
|
||||
maskAnimate: ''
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.triggerEvent('close');
|
||||
},
|
||||
emptyEvent() {
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
<!-- 下半弹窗 -->
|
||||
<view wx:if="{{showSelf}}" class="mask {{maskAnimate}}" bind:tap="handleClose" catchtouchmove="emptyEvent" >
|
||||
<view class="content {{popupAnimate}}" style="padding-bottom: {{safeBottom}}rpx;" catchtap="emptyEvent" catchtouchmove="emptyEvent">
|
||||
<slot name="header"></slot>
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
@@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||