一、原型依據
在我這個專案中小程式端所需要實作的只有紅包雨的下落影片和通屏背景圖的兼容,關于紅包點擊金額的計算是由后端實作的,首先來看下需要實作的效果圖,






二、實作代碼
首先是第一次進入的頁面,在這個頁面的時候會進行靜默登錄,靜默登錄成功的話會顯示當前剩余次數,否則在點擊開始的時候回跳轉登錄頁面,不論是否登錄成功都會去呼叫活動資訊監測的介面,判斷當前是否有可以參加的活動,在點擊開始后進行三秒鐘的倒計時,倒計時結束則轉入下一個界面實作紅包雨的下落,
<!--RedEnvelopes/pages/RedEnvelopes/index/index.wxml-->
<nav-bar navbar-data='{{nvabarData}}'></nav-bar>
<view style='padding-top: {{height}}px;' wx:if="{{ready}}">
<block wx:if="{{!showTime}}">
<view class="start" bindtap="participateIn" wx:if="{{readyTime != 0}}">
<view class="start-aperture">
<view class="start-aperture-bg">
<text class="start-aperture-txt" wx:if="{{!showTime}}">開始</text>
<text class="start-aperture-txt" wx:else>{{readyTime}}</text>
</view>
</view>
</view>
</block>
<block wx:else>
<view class="start" wx:if="{{readyTime != 0}}">
<view class="start-aperture">
<view class="start-aperture-bg">
<text class="start-aperture-txt" wx:if="{{!showTime}}">開始</text>
<text class="start-aperture-txt" wx:else>{{readyTime}}</text>
</view>
</view>
</view>
</block>
<view class="remaTimes" wx:if="{{showLogin && !showTime}}">今日剩余次數: {{RemainingTimes}}/{{MaxSharingAwardTimes}}</view>
<view class="rule" wx:if="{{!showTime}}">
<view class="rule-top">
<text class="iconfont iconxiangxia2"></text>
</view>
<view class="rule-body">
<view class="rule-body-one">
<view class="rule-body-title">規則:</view>
<view class="rule-body-html">{{ActivityRules}}</view>
</view>
</view>
</view>
</view>
<!-- 紅包雨組件 -->
<block wx:if="{{readyTime == 0}}">
<sol-packet-rain visible="{{visible}}" createSpeed="{{createSpeed}}" time="{{time}}" min="{{min}}" max="{{max}}" bind:finish="success"></sol-packet-rain>
</block>
// RedEnvelopes/pages/RedEnvelopes/index/index.js
const api = require('../../../../server/api.js');
const http = require('../../../../server/request.js');
const appJs = require('../../../../utils/uselogn.js');
const app = getApp()
let readyTimer = null
Page({
/**
* 頁面的初始資料
*/
data: {
// 導航頭組件所需的引數
nvabarData: {
showCapsule: 1, //是否顯示左上角圖示 1表示顯示 0表示不顯示
title: '搶紅包', //導航欄 中間的標題
white: true, // 是就顯示白的,不是就顯示黑的,
address: api.pictureServer + '/res/shopImg/RedEnvelopes.png' // 加個背景 不加就是沒有
},
// 導航頭的高度
height: app.globalData.height * 2 + 20,
readyTime: 3,
showTime: false,
btnText: '獲取驗證碼',
phone: '',
VerificationCode: '',
unbind: false,
showLogin: true,
ready: false,
tell: '',
redId: '', //搶紅包活動標識
BackgroundMap: '', //活動背景圖
ThemeMap: '', //活動主題圖
ActivityRules: '', //活動規則
ShareDescription: '', //分享描述
ShareIcon: '', //分享圖示
MaxSharingAwardTimes: '0',
RemainingTimes: '0'
},
onLogin: appJs.userLogin,
/**
* 生命周期函式--監聽頁面加載
*/
onl oad: function(options) {
if (options.refer && options.ShareMemberId) {
let SharedMemberId = wx.getStorageSync('CustomerService').MemberId;
let parm = {
Id: options.refer,
ShareMemberId: options.ShareMemberId,
SharedMemberId: SharedMemberId
}
this.getRedEnvelopeLotteryShare(parm)
let white = 'nvabarData.white'
this.setData({
[white]: false
})
}
},
/**
* 生命周期函式--監聽頁面初次渲染完成
*/
onReady: function() {
},
/**
* 生命周期函式--監聽頁面顯示
*/
onShow: function() {
this.setData({
ready: true,
showTime: false,
readyTime: 3
})
this.onLogin(this.authCallback, this.authCallback1);
},
authCallback() {
let that = this
// 獲取當前時間搶紅包資訊
that.getGrabRed();
that.setData({
showLogin: true
})
},
authCallback1() {
let that = this
// 獲取當前時間搶紅包資訊
that.getGrabRed();
that.setData({
showLogin: false
})
},
/**
* 生命周期函式--監聽頁面隱藏
*/
onHide: function() {},
/**
* 生命周期函式--監聽頁面卸載
*/
onUnload: function() {
},
/**
* 頁面相關事件處理函式--監聽用戶下拉動作
*/
onPullDownRefresh: function() {
},
/**
* 頁面上拉觸底事件的處理函式
*/
onReachBottom: function() {
},
/**
* 用戶點擊右上角分享
*/
onShareAppMessage: function() {
},
// 開始準備倒計時
cultdown: function() {
let _this = this
let {
readyTime
} = this.data
readyTimer = setInterval(function() {
if (--readyTime <= 0) {
clearInterval(readyTimer)
// 顯示紅包雨
_this.run();
}
_this.setData({
readyTime: readyTime
})
}, 1000)
},
start: function() {
if (!this.data.showLogin) {
setTimeout(() => {
wx.navigateTo({
url: '/pages/member/loginAndRegister/loginAndRegister',
})
}, 500)
return
}
if (this.data.RemainingTimes == 0){
return
}
if (!this.data.showTime) {
this.cultdown();
this.setData({
showTime: true
})
}
},
run: function() {
let address = 'nvabarData.address'
this.setData({
visible: true,
createSpeed: 5, // 速度
time: 10, // 游戲時間
min: 1, // 金幣最小是0
max: 1, // 金幣最大是10
[address]: this.data.ThemeMap || api.pictureServer + '/res/shopImg/RedEnvelopes1.png'
})
clearInterval(this.readyTimer);
},
// 結束
success(e) {
let that = this;
let redId = this.data.redId;
console.log('bind:finish', e.detail)
let RedEnvelopeNum = e.detail;
// 中獎
let parm = {
Id: redId,
RedEnvelopeNum: RedEnvelopeNum
}
http.requestLoading('/api/services/app/RedEnvelope/ExcuteGrabRedEnvelope', parm, '', 'POST').then(res => {
if (res.data.Result.Code === 0) {
console.log(res)
that.setData({
visible: false // 隱藏界面
})
wx.navigateTo({
url: '/RedEnvelopes/pages/RedEnvelopes/WinningRedEnvelope/WinningRedEnvelope?Id=' + res.data.Result.Data.Id + '&PartId=' + res.data.Result.Data.PartId + '&ShareDescription=' + that.data.ShareDescription + '&ShareIcon=' + that.data.ShareIcon
})
}
})
},
// 立即參與
participateIn() {
let that = this;
let redId = this.data.redId;
let parm = {
Id: redId
}
http.requestLoading('/api/services/app/RedEnvelope/GetGrabRedEnvelopeAppSingle', parm, '', 'GET').then(res => {
if (res.data.Result.Code === 0) {
console.log(res)
if (redId != res.data.Result.Data.Id) {
wx.showToast({
title: '當前活動資訊不符',
icon: 'none'
})
} else {
that.setData({
redId: res.data.Result.Data.Id
})
// 驗證會員是否可以參加該活動
let parms = {
Id: res.data.Result.Data.Id,
RedEnvelopeNum: 0
}
if (!this.data.showLogin) {
setTimeout(() => {
wx.navigateTo({
url: '/pages/member/loginAndRegister/loginAndRegister',
})
}, 500)
return
}
http.requestLoading('/api/services/app/RedEnvelope/DrawGrabRedEnvelope', parms, '', 'POST').then(res => {
if (res.data.Result.Code === 0) {
that.setData({
ready: true
})
that.start();
}
})
}
}
})
},
// 獲取當前時間搶紅包資訊
getGrabRed: function(redId) {
let that = this;
let address = 'nvabarData.address'
http.requestLoading('/api/services/app/RedEnvelope/GetGrabRedEnvelopeAppSingle', {}, '', 'GET').then(res => {
if (res.data.Result.Code === 0) {
console.log(res)
that.setData({
redId: res.data.Result.Data.Id,
BackgroundMap: res.data.Result.Data.BackgroundMap,
ThemeMap: res.data.Result.Data.ThemeMap, //活動主題圖
ActivityRules: res.data.Result.Data.ActivityRules, //活動規則
ShareDescription: res.data.Result.Data.ShareDescription, //分享描述
ShareIcon: res.data.Result.Data.ShareIcon, //分享圖示
[address]: res.data.Result.Data.BackgroundMap
})
if (that.data.showLogin) {
that.getRemainingTimes();
}
}
})
},
// 分享得額外抽獎次數
getRedEnvelopeLotteryShare(parm) {
http.requestLoading('/api/services/app/RedEnvelope/RedEnvelopeLotteryShare', parm, '', 'POST').then(res => {
if (res.data.Result.Code === 0) {
console.log(res)
}
})
},
// 獲取總次數
getRemainingTimes() {
let that = this;
let parm = {
Id: this.data.redId
}
http.requestLoading('/api/services/app/RedEnvelope/GetRemainingTimes', parm, '', 'GET').then(res => {
if (res.data.Result.Code === 0) {
console.log(res)
that.setData({
MaxSharingAwardTimes: res.data.Result.Data.MaxSharingAwardTimes,
RemainingTimes: res.data.Result.Data.RemainingTimes
})
}
})
}
})
/* RedEnvelopes/pages/RedEnvelopes/index/index.wxss */
page {
position: relative;
}
.login {
width: 626rpx;
height: 554rpx;
background: #fff;
border-radius: 10rpx;
position: absolute;
top: 50%;
left: 0;
right: 0;
margin: auto;
}
.login-center {
width: 501rpx;
height: 138rpx;
border-bottom: 1rpx solid #e2e2e2;
margin: auto;
display: flex;
align-items: center;
}
.userIpt {
font-size: 28rpx;
font-family: Source Han Sans CN Regular, Source Han Sans CN Regular-Regular;
font-weight: 400;
color: #999;
}
.posion {
position: relative;
}
.zc_view {
position: absolute;
right: 0;
top: 40rpx;
width: 170rpx;
height: 52rpx;
line-height: 52rpx;
margin-left: 12rpx;
border: 1rpx solid #e2e2e2;
text-align: center;
font-size: 24rpx;
border-radius: 10rpx;
}
.login-state {
font-size: 40rpx;
font-family: Microsoft YaHei Regular, Microsoft YaHei Regular-Regular;
font-weight: 400;
color: #333;
text-align: center;
margin-top: 146rpx;
}
.login-ready {
font-size: 30rpx;
font-family: Microsoft YaHei Regular, Microsoft YaHei Regular-Regular;
font-weight: 400;
color: #333;
text-align: center;
margin-top: 89rpx;
}
.login-btn {
width: 299rpx;
height: 74rpx;
background: #fc7297;
border-radius: 37rpx;
box-shadow: 0rpx 2rpx 6rpx 0rpx rgba(3, 0, 6, 0.1);
margin: auto;
margin-top: 65rpx;
display: flex;
align-items: center;
justify-content: center;
}
.btn-txt {
font-size: 30rpx;
font-family: Microsoft YaHei Regular, Microsoft YaHei Regular-Regular;
font-weight: 500;
color: #fff;
}
/* 開始按鈕 */
.start {
position: absolute;
top: 45%;
padding: 0 250rpx;
}
.start-aperture {
width: 250rpx;
height: 250rpx;
background: rgb(247, 136, 44, 0.29);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.start-aperture-bg {
width: 210rpx;
height: 210rpx;
background: linear-gradient(0deg, #f0510b 0%, #ffc350 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.start-aperture-txt {
font-size: 60rpx;
font-family: Source Han Sans CN Medium, Source Han Sans CN Medium-Medium;
font-weight: 500;
color: #fff;
}
/* 規則 */
.rule {
position: absolute;
left: 0;
right: 0;
top: 73%;
}
.rule-top {
width: 160rpx;
height: 36rpx;
background: rgb(237, 216, 255, 0.4);
border-top-left-radius: 20rpx;
border-top-right-radius: 20rpx;
margin: auto;
text-align: center;
}
.iconxiangxia2 {
font-size: 28rpx;
color: #fff;
}
.rule-body {
width: 660rpx;
/* height: 208rpx; */
background: rgb(237, 216, 255, 0.4);
border-radius: 20rpx;
margin: auto;
padding: 38rpx 0 38rpx 38rpx;
}
.rule-body-one{
display: flex;
}
.rule-body-title {
display: inline-block;
width: 81rpx;
font-size: 26rpx;
font-family: Source Han Sans CN Normal, Source Han Sans CN Normal-Normal;
color: #fff;
}
.rule-body-html {
display: inline-block;
width: 417rpx;
font-size: 26rpx;
font-family: Source Han Sans CN Normal, Source Han Sans CN Normal-Normal;
color: #fff;
}
/* 新 */
.remaTimes{
position: absolute;
left: 0;
right: 0;
top: 65%;
font-size: 30rpx;
font-family: Microsoft YaHei Regular, Microsoft YaHei Regular-Regular;
font-weight: 400;
color: #fff;
text-align: center;
margin-top: 20rpx;
}
在當前的頁面中引入了兩個組件,第一個是實作自定義導航欄的,因為在當前的界面中背景圖的要求是全屏的,第二個組件就是紅包雨的下落是參考網上一大佬的紅包雨實作的,
//app.js
// 引入請求檔案
App({
onLaunch: function(e) {
//獲取設備頂部視窗的高度(不同設備視窗高度不一樣,根據這個來設定自定義導航欄的高度)
wx.getSystemInfo({
success: res => {
this.globalData.height = res.statusBarHeight
}
})
this.getSystemInfo();
},
globalData: {
share: false, // 分享默認為false
height: 0, // 頂部高度
systemInfo: {} //設備資訊
},
getSystemInfo: function() {
var info = util.getSystemInfoSync();
var iphoneX = "";
if (info) {
var statusBarHeight = info.statusBarHeight;
var model = info.model;
var windowHeight = info.windowHeight;
var totalTopHeight = 68;
model.indexOf("iPhone X") !== -1 ?
((totalTopHeight = 94), (iphoneX = "iphone-x")) :
-1 !== model.indexOf("iPhone") ? (totalTopHeight = 64) : -1 !== model.indexOf("MI 8") && (totalTopHeight = 88);
var titleBarHeight = totalTopHeight - statusBarHeight;
this.globalData.systemInfo = Object.assign({}, info, {
statusBarHeight,
titleBarHeight,
totalTopHeight,
iphoneX,
windowHeight
})
}
}
})
在app.js中針對不同的設備的導航條的高度進行了處理,使之能夠實作適配,
<!--componets/shoppingMall-components/navbar/navbar.wxml-->
<view class='nav-wrap' style='height: {{height*2 + 20}}px;'>
<!-- 導航欄背景圖片 -->
<image class="backgroundimg" src="{{navbarData.address}}" bindload="imgLoaded" style="width:{{imageWidth}}px;height:{{imageHeight}}px" />
<!-- // 導航欄 中間的標題 -->
<view class='nav-title' wx:if='{{!navbarData.white}}' style='line-height: {{height*2 + 44}}px;'>
{{navbarData.title}}
</view>
<view class='nav-title' wx:else='{{!navbarData.white}}' style='line-height: {{height*2 + 44}}px; color:#ffffff'>
{{navbarData.title}}
</view>
<view style='display: flex; justify-content: space-around;flex-direction: column'>
<!-- // 導航欄 左上角的回傳按鈕 -->
<!-- // 其中wx:if='{{navbarData.showCapsule}}' 是控制左上角按鈕的顯示隱藏,首頁不顯示 -->
<view class='nav-capsule' style='height: {{height*2 + 44}}px;' wx:if='{{navbarData.showCapsule}}'>
<!-- //左上角的回傳按鈕,wx:if='{{!share}}'空制回傳按鈕顯示 -->
<!-- //從分享進入小程式時 回傳上一級按鈕不應該存在 -->
<!-- navbarData.white是控制按鈕顏色的,因為背景有深淺色,回傳按鈕自己找圖片 -->
<view bindtap='_navback' wx:if='{{showBack}}'>
<text class="iconfont iconxiangzuo"></text>
</view>
<view bindtap='_navbackTo' wx:else>
<text class="iconfont iconxiaochengxushouye"></text>
</view>
</view>
</view>
</view>
<!-- 導航欄下面的背景圖片 -->
<image class="backgroundimg" src="{{navbarData.address}}" bindload="imgLoaded" style="width:{{imageWidth}}px;height:{{imageHeight}}px" />
// componets/shoppingMall-components/navbar/navbar.js
const app = getApp()
const util = require('../../../utils/indexPage.js');
Component({
/**
* 組件的屬性串列
*/
properties: {
navbarData: {
//navbarData 由父頁面傳遞的資料,變數名字自命名
type: Object,
value: {},
observer: function(newVal, oldVal) {}
}
},
options: {
styleIsolation: 'apply-shared'
},
/**
* 組件的初始資料
*/
data: {
height: '',
//默認值 默認顯示左上角
navbarData: {
showCapsule: 1
},
imageWidth: wx.getSystemInfoSync().windowWidth, // 背景圖片的高度
imageHeight: '', // 背景圖片的長度,通過計算獲取
showBack:false
},
attached: function() {
// 獲取是否是通過分享進入的小程式
this.setData({
share: app.globalData.share
})
let pages = getCurrentPages();
if (pages.length > 1){
this.setData({
showBack: true
})
}
// 定義導航欄的高度 方便對齊
this.setData({
height: app.globalData.height
})
},
/**
* 組件的方法串列
*/
methods: {
// 回傳上一頁面
_navback() {
wx.navigateBack()
},
_navbackTo(){
util.indexPage();
},
// 計算圖片高度
imgLoaded(e) {
// console.log(e, wx.getSystemInfoSync())
this.setData({
imageHeight: e.detail.height *
(wx.getSystemInfoSync().windowHeight / e.detail.height)
})
}
//回傳到首頁
// _backhome() {
// wx.switchTab({
// url: '/pages/index/index'
// })
// }
}
})
/* componets/shoppingMall-components/navbar/navbar.wxss */
/* 頂部要固定定位 標題要居中 自定義按鈕和標題要和右邊微信原生的膠囊上下對齊 */
.nav-wrap {
/* display: none; */
position: fixed;
width: 100%;
top: 0;
background: #fff;
color: #000;
z-index: 9999999;
background: #000;
overflow: hidden;
}
/* 背景圖 */
.backgroundimg {
position: absolute;
z-index: -1;
}
/* 標題要居中 */
.nav-title {
position: absolute;
text-align: center;
max-width: 400rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
font-size: 36rpx;
color: #2c2b2b;
font-weight: 450;
}
.nav-capsule {
display: flex;
align-items: center;
margin-left: 30rpx;
width: 140rpx;
justify-content: space-between;
height: 100%;
}
.back-pre {
width: 36rpx;
height: 40rpx;
margin-top: 4rpx;
padding: 10rpx;
}
.nav-capsule {
width: 36rpx;
height: 40rpx;
margin-top: 3rpx;
}
.iconxiangzuo{
font-size: 41rpx;
color: #fff;
}
.iconxiaochengxushouye {
font-size: 41rpx;
color: #fff;
}
在自定義導航欄中針對不同的進入情況對左上角的按鈕做不同的處理,當是通過掃碼或分享進入的時候左上角是回傳首頁的小房子,通過其他頁面跳轉過來時則是回傳上一級的箭頭,
<view wx:if="{{visible}}" class="red-envelope-popup">
<view class="container flex-center">
<view bind:tap="handleClose" catch:touchmove="handleScrollTouch" class="close-bg"></view>
<block>
<block wx:if="{{showStatus===2}}">
<view class="rain-wrapper flex-column">
<view class="canvas-wrapper">
<view class='score-change' animation="{{scoreAni}}">
+{{showChangeScore}}
</view>
<canvas disableScroll binderror="canvasIdErrorCallback" bindtouchstart="handleClickRain" canvasId="rain-canvas" style="width: 100vw; height: 100vh;z-index: 9999999;"></canvas>
</view>
</view>
</block>
<block wx:if="{{showStatus===3}}">
<view class="result-wrapper flex-column-center">
<block>
<view class="group-content flex-column-center" bindtap="handleClose">
</view>
</block>
</view>
</block>
</block>
</view>
</view>
const innerAudioContext = wx.createInnerAudioContext()
const api = require('../../../server/api.js');
const APP = getApp()
let readyTimer = null
let rainTimer = null
let redEnvelopes = []
let animation = null
const minWidth = 30 // 紅包圖片最小寬度
const maxWidth = 40 // 紅包圖片最大寬度
Component({
properties: {
// 是否開始展示游戲
visible: {
type: Boolean,
value: false
},
// 游戲時間
time: {
type: Number,
value: 10
},
// 速度
createSpeed: {
type: Number,
value: 5
},
// 單個最小金額
min: {
type: Number,
value: 0
},
// 單個最大金額
max: {
type: Number,
value: 3
}
},
data: {
showRainTotalTime: 10, // 紅包雨時間
showStatus: 1, // 紅包雨狀態:1:準備倒計時,2:正在紅包雨,3:紅包雨結束
windowWidth: 375,
windowHeight: 555,
rainResult: {},
loading: false,
showScore: 0,
showChangeScore: 0,
scoreStyle: ''
},
ready: function() {
// 重置
redEnvelopes = []
// clearTimeout(readyTimer)
clearTimeout(rainTimer)
this.cancelCustomAnimationFrame(animation)
// 開始準備倒計時
this.showRain()
const {
windowWidth,
windowHeight
} = APP.globalData.systemInfo
this.data.windowWidth = windowWidth
this.data.windowWidth = windowHeight
},
detached: function() {
// readyTimer && clearInterval(readyTimer)
rainTimer && clearInterval(rainTimer)
animation && this.cancelCustomAnimationFrame(animation)
},
methods: {
// 展示紅包雨界面
showRain: function() {
let _this = this
// 顯示紅包雨
this.setData({
showStatus: 2
})
// 初始化紅包雨
this.initRain()
// 倒計時進度條
this.ininProgress()
// 紅包雨倒計時
let showRainTotalTime = this.data.time
rainTimer = setInterval(function() {
if (--showRainTotalTime <= 0) {
clearInterval(rainTimer)
if (animation) {
// 結束
_this.showRainResult()
_this.cancelCustomAnimationFrame(animation)
}
}
_this.setData({
showRainTotalTime
});
}, 1000);
},
// 倒計時進度條
ininProgress() {
const {
time
} = this.data
const animation = wx.createAnimation({
duration: time * 1000
})
animation.translateX(-120).step()
this.setData({
progressAni: animation.export()
})
},
//分數影片
animationOfScore(x, y) {
const position = wx.createAnimation({
duration: 0
})
position.left(x).top(y).step()
this.setData({
scoreAni: position.export()
})
const animation = wx.createAnimation({
duration: 300,
timingFunction: 'ease'
})
animation.opacity(1).step()
setTimeout(function() {
animation.opacity(0).step()
this.setData({
scoreAni: animation.export()
})
}.bind(this), 10)
},
// 關閉
handleClose: function() {
this.triggerEvent("finish", this.data.showScore)
},
// 顯示結果
showRainResult: function() {
// 結束影片
this.cancelCustomAnimationFrame(animation)
this.setData({
showStatus: 3,
rainResult: {
amount: 100
}
});
},
// 紅包下落函式
customRequestAnimationFrame: function(e) {
let _this = this
let timer = setTimeout(function() {
e.call(_this), clearTimeout(timer);
}, 1000 / 60)
return timer
},
// 清除紅包下落函式
cancelCustomAnimationFrame: function(e) {
e && (clearTimeout(e), animation = null)
},
// 開始下落
doDrawRain: function() {
const context = this.context
const {
windowWidth,
windowHeight
} = this.data
context.clearRect(0, 0, windowWidth, windowHeight)
for (let n = 0; n < redEnvelopes.length; n += 1) {
const i = redEnvelopes[n] // 紅包
const {
x,
y,
vx,
vy,
width,
height,
open
} = i
const img = open ? this.openEnvelopeImg : this.redEnvelopeImg
const imgWidth = open ? width + 20 : width
const imgHeight = open ? height + 25 : height
if (x < 0) {
x += 50;
} else if (x > 750) {
x -= 50;
}
context.drawImage(img, x, y, imgWidth, imgHeight)
i.x += vx
i.y += vy
i.y >= windowHeight && (i.y = 0, i.open = false)
i.x + width <= 0 && (i.x = windowWidth - width, i.open = false)
}
context.draw()
// 下落函式
animation = this.customRequestAnimationFrame(this.doDrawRain);
},
// 亂數
randNum: function(min, max) {
return Math.floor(min + Math.random() * (max - min));
},
// 準備紅包雨下落
initRainDrops: function() {
const {
windowWidth,
windowHeight,
createSpeed,
max,
min
} = this.data
for (let n = 0; n < 10; n += 1) {
const startX = Math.floor(Math.random() * windowWidth)
// 優化位置,防止紅包越界現象,保證每個紅包都在螢屏之內
if (startX < 0) {
startX += 50;
} else if (startX > windowWidth) {
startX -= 50;
}
const startY = Math.floor(Math.random() * windowHeight)
// 紅包圖片寬度大小30~40
const width = this.randNum(minWidth, maxWidth)
// 寬度為紅包高度的百分之八十
const height = Math.floor(width / .8)
// 速度
const vy = 1 * Math.random() + createSpeed
// 紅包金額
const score = this.randNum(min, max + 1)
redEnvelopes.push({
x: startX,
y: startY,
vx: -1, // x軸速度
vy: vy, // y軸速度
score: score,
width: width,
height: height,
open: false
});
}
this.doDrawRain();
},
// 點擊紅包事件
handleClickRain: function(e) {
let touch = e.touches[0]
let touchX = touch.x
let touchY = touch.y
let _this = this
for (let o = 0; o < redEnvelopes.length; o += 1) {
let i = redEnvelopes[o],
rainX = i.x,
rainY = i.y,
width = i.width,
height = i.height,
gapX = touchX - rainX,
gapY = touchY - rainY;
if (gapX >= -20 && gapX <= width + 20 && gapY >= -20 && gapY <= height + 20) {
_this.animationOfScore(touchX, touchY)
innerAudioContext.play()
i.open = true;
let score = _this.data.showScore + i.score
_this.setData({
showScore: score,
showChangeScore: i.score
})
break;
}
}
},
// 初始化 canvas
initRain: function() {
this.context = wx.createCanvasContext("rain-canvas", this)
this.redEnvelopeImg = "./images/red-packet-rain.png",
this.openEnvelopeImg = "./images/red-packet-rain-open.png"
// 初始化紅包雨
this.initRainDrops()
// 音效
this.audioOfClick()
},
handleScrollTouch: function() {},
audioOfClick() {
innerAudioContext.autoplay = false
innerAudioContext.src = 'https://imgs.solui.cn/weapp/dianji.mp3'
innerAudioContext.onPlay(() => {})
innerAudioContext.onError(res => {})
},
}
});
.flex {
display: -ms-flexbox;
display: flex
}
.flex-center {
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
-ms-flex-align: center;
align-items: center;
-ms-flex-pack: center;
justify-content: center;
width: 100%;
height: 100%
}
.flex-column {
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column
}
.flex-column-center {
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
-ms-flex-align: center;
align-items: center
}
.flex-column-sb {
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
-ms-flex-align: center;
align-items: center;
-ms-flex-pack: justify;
justify-content: space-between
}
.flex-column-c {
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
-ms-flex-align: center;
align-items: center;
-ms-flex-pack: center;
justify-content: center
}
.flex-row {
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center
}
.flex-row-center {
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
width: 100%
}
.flex-content-center {
display: -ms-flexbox;
display: flex;
-ms-flex-pack: center;
justify-content: center;
width: 100%
}
.flex-column-right {
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
-ms-flex-align: end;
align-items: flex-end
}
.flex-column-left {
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
-ms-flex-align: start;
align-items: flex-start
}
.flex-sb {
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
-ms-flex-pack: justify;
justify-content: space-between
}
.flex-sa {
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
-ms-flex-pack: distribute;
justify-content: space-around
}
.flex-c {
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
-ms-flex-pack: center;
justify-content: center
}
.flex-sb-start {
display: -ms-flexbox;
display: flex;
-ms-flex-pack: justify;
justify-content: space-between
}
.flex-start {
display: -ms-flexbox;
display: flex;
-ms-flex-pack: start;
justify-content: flex-start
}
.flex-end {
display: -ms-flexbox;
display: flex;
-ms-flex-pack: end;
justify-content: flex-end
}
.flex-center-end {
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
-ms-flex-pack: end;
justify-content: flex-end
}
.flex-end-center {
display: -ms-flexbox;
display: flex;
-ms-flex-align: end;
align-items: flex-end;
-ms-flex-pack: center;
justify-content: center
}
.flex-end-sb {
display: -ms-flexbox;
display: flex;
-ms-flex-align: end;
align-items: flex-end;
-ms-flex-pack: justify;
justify-content: space-between
}
.flex-end-start {
display: -ms-flexbox;
display: flex;
-ms-flex-align: end;
align-items: flex-end
}
.red-envelope-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
/* background: rgba(0,0,0,.8) */
}
.red-envelope-popup .close-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0
}
.red-envelope-popup .reminder-wrapper {
position: relative;
width: 750rpx;
height: 716rpx;
color: #fff;
box-sizing: border-box
}
.red-envelope-popup .reminder-wrapper .title {
font-size: 60rpx;
color: #fffdc5
}
.red-envelope-popup .reminder-wrapper .time {
margin-top: 100rpx;
font-size: 240rpx;
color: #fffdc5
}
.red-envelope-popup .rain-wrapper {
/* width: 100%; */
/* height: 100%; */
/* background-image: url(https://imgs.solui.cn/weapp/redBacketBG.jpg); */
/* background-size: 100% 100%; */
/* background-repeat: no-repeat */
}
.red-envelope-popup .rain-wrapper .time-info {
position: absolute;
top: 80rpx;
left: 45rpx;
font-size: 24rpx;
color: #fff
}
.red-envelope-popup .rain-wrapper .time-info .progress-wrapper {
position: relative;
height: 16rpx;
width: 240rpx;
margin-left: 20rpx;
margin-right: 20rpx;
border-radius: 16rpx;
background: #fff;
overflow: hidden
}
.red-envelope-popup .rain-wrapper .time-info .progress-wrapper .progress {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 240rpx;
border-radius: 16rpx;
background: #fe2e00;
z-index: 1
}
.red-envelope-popup .rain-wrapper .time-info .total-score {
font-size: 40rpx
}
.red-envelope-popup .rain-wrapper .canvas-wrapper {
position: relative
}
.red-envelope-popup .rain-wrapper .canvas-wrapper .score-change {
position: absolute;
width: 50rpx;
height: 50rpx;
font-size: 40rpx;
color: #fffdc5;
opacity: 0
}
.red-envelope-popup .result-wrapper {
width: 100%;
height: 100%;
display: -ms-flexbox;
display: flex;
-ms-flex-pack: center;
justify-content: center
}
.red-envelope-popup .result-wrapper .group-content {
position: relative;
width: 550rpx;
height: 700rpx;
/* background-image: url(https://imgs.solui.cn/weapp/l-rain-gold@2x.png); */
background-image: url(http://hmspimg.afarsoft.com/static/crmmicroapp/res/shopImg/Demolition.png);
background-size: 100% 100%;
background-repeat: no-repeat
}
.red-envelope-popup .result-wrapper .group-content .result-title {
margin-top: 50rpx;
font-size: 40rpx;
color: #fff
}
.red-envelope-popup .result-wrapper .group-content .money-wrapper {
margin-top: 56rpx;
color: #fff
}
.red-envelope-popup .result-wrapper .group-content .money-wrapper .money {
font-size: 120rpx
}
.red-envelope-popup .result-wrapper .group-content .money-wrapper .unit {
position: relative;
top: 34rpx;
font-size: 32rpx
}
.red-envelope-popup .result-wrapper .group-content .result-btn {
margin-top: 158rpx;
width: 300rpx;
height: 70rpx;
background-color: #fff9e8;
text-align: center;
line-height: 70rpx;
border-radius: 40rpx;
font-size: 30rpx;
color: #b10000
}
在這個組件中,在attached這個生命周期的時候進是紅包下落的方法開始執行,在創建紅包橫向坐標的時候需要對坐標進行優化防止紅包出現在螢屏外,
<!--RedEnvelopes/pages/RedEnvelopes/WinningRedEnvelope/WinningRedEnvelope.wxml-->
<view wx:if="{{showMode}}">
<image src="{{url}}" class="winning"></image>
<view class="congratulations" wx:if="{{NotWinning}}">很遺憾!</view>
<view class="congratulations" wx:else>恭喜您!</view>
<view class="prize" wx:if="{{NotWinning}}">您本次沒有中獎,</view>
<view class="prize" wx:else>您一共獲得<block wx:if="{{RedEnvelope.AmountCount}}">{{RedEnvelope.AmountCount}}個紅包</block><block wx:if="{{RedEnvelope.AmountCount && RedEnvelope.CouponCount}}">,</block><block wx:if="{{RedEnvelope.CouponCount}}">{{RedEnvelope.CouponCount}}張優惠券</block></view>
<block wx:for="{{RedEnvelope.CouponWinInfoList}}" wx:key="index">
<view class="prize-details">
<view class="prize-quan">
<image src="{{quan}}" class="quan"></image>
<text class="quan-txt" wx:if="{{item.UseType == 0}}">{{item.DiscountAmount}}元</text>
<text class="quan-txt" wx:else>{{item.DiscountRate}}折</text>
<text class="quan-type coupon">優惠券</text>
</view>
<view class="prize-name">{{item.CouponName}}</view>
<view class="prize-num">數量:{{item.Num}}</view>
</view>
</block>
<block wx:for="{{RedEnvelope.AmountWinInfoList}}" wx:key="index">
<view class="prize-details">
<view class="prize-quan">
<image src="{{quan}}" class="quan"></image>
<text class="quan-txt">{{item.RedEnvelopeAmount}}元</text>
<text class="quan-type cash">現金</text>
</view>
<view class="prize-name">{{item.RedEnvelopeAmount}}元現金紅包</view>
<view class="prize-num">數量:{{item.Num}}</view>
</view>
</block>
<view class="share">
<view class="share-left">邀請好友參加活動</view>
<view class="share-left lower">每天可獲得
<text class="share-num" wx:if="{{NotWinning}}">{{MaxSharingAwardTimes}}次</text>
<text class="share-num" wx:else>{{RedEnvelope.MaxSharingAwardTimes}}次</text>分享獎勵</view>
<button class="share-btn" open-type="share">
<text class="share-txt">去分享</text>
</button>
</view>
</view>
// RedEnvelopes/pages/RedEnvelopes/WinningRedEnvelope/WinningRedEnvelope.js
const api = require('../../../../server/api.js');
const http = require('../../../../server/request.js');
Page({
/**
* 頁面的初始資料
*/
data: {
url: api.pictureServer + '/res/shopImg/winning.png',
quan: api.pictureServer + '/res/shopImg/redCoupon.png',
Id: '', //搶紅包活動標識
PartId: '', //搶紅包參與資訊標識
RedEnvelope: {},
ShareDescription: '', //分享描述
ShareIcon: '', //分享圖示
NotWinning: true, //未中獎
showMode: false
},
/**
* 生命周期函式--監聽頁面加載
*/
onl oad: function(options) {
console.log(options)
this.setData({
Id: options.Id,
PartId: options.PartId,
ShareDescription: options.ShareDescription,
ShareIcon: options.ShareIcon,
})
let parm = {
Id: options.Id,
PartId: options.PartId
}
this.getGrabRedEnvelopeWinInfo(parm);
},
/**
* 生命周期函式--監聽頁面初次渲染完成
*/
onReady: function() {
},
/**
* 生命周期函式--監聽頁面顯示
*/
onShow: function() {
},
/**
* 生命周期函式--監聽頁面隱藏
*/
onHide: function() {
},
/**
* 生命周期函式--監聽頁面卸載
*/
onUnload: function() {
},
/**
* 頁面相關事件處理函式--監聽用戶下拉動作
*/
onPullDownRefresh: function() {
},
/**
* 頁面上拉觸底事件的處理函式
*/
onReachBottom: function() {
},
/**
* 用戶點擊右上角分享
*/
onShareAppMessage: function() {
let ShareMemberId = wx.getStorageSync('CustomerService').MemberId
return {
title: this.data.ShareDescription,
// desc: '快來搶紅包雨',
imageUrl: this.data.ShareIcon,
path: '/RedEnvelopes/pages/RedEnvelopes/index/index?refer=' + this.data.Id + '&ShareMemberId=' + ShareMemberId,
success: function(res) {
// 轉發成功
wx.showToast({
title: '分享成功',
icon: 'none'
})
},
fail: function(res) {
// 轉發失敗
}
}
},
// 拆開紅包得到中獎資訊
getGrabRedEnvelopeWinInfo: function(parm) {
let that = this;
http.requestLoading('/api/services/app/RedEnvelope/GetGrabRedEnvelopeWinInfo', parm, '', 'GET').then(res => {
if (res.data.Result.Code === 0) {
console.log(res)
that.setData({
NotWinning: false,
RedEnvelope: res.data.Result.Data,
showMode: true
})
} else if (res.data.Result.Code === 1) {
//未中獎
that.setData({
NotWinning: true,
MaxSharingAwardTimes: res.data.Result.Data.MaxSharingAwardTimes,
showMode: true
})
}
})
}
})
/* RedEnvelopes/pages/RedEnvelopes/WinningRedEnvelope/WinningRedEnvelope.wxss */
page {
width: 100%;
background: rgb(254, 177, 198, 0.2);
}
/* 禮品 */
.winning {
width: 286rpx;
height: 294rpx;
display: block;
margin: 20rpx auto;
}
.congratulations {
font-size: 46rpx;
font-family: Adobe Heiti Std R, Adobe Heiti Std R-R;
color: #ff3005;
text-align: center;
}
.prize {
font-size: 28rpx;
font-family: Adobe Heiti Std R, Adobe Heiti Std R-R;
color: #fc7297;
text-align: center;
margin: 38rpx auto;
}
/* 獎勵內容 */
.prize-details {
width: 668rpx;
height: 150rpx;
background: #fff;
border-radius: 10rpx;
display: block;
margin: auto;
padding: 38rpx 0 38rpx 38rpx;
margin-bottom: 20rpx;
position: relative;
}
.prize-quan {
width: 177rpx;
position: relative;
}
.quan {
width: 177rpx;
height: 74rpx;
}
.quan-txt {
position: absolute;
top: 20rpx;
left: 30rpx;
font-size: 28rpx;
font-family: Source Han Sans CN Regular, Source Han Sans CN Regular-Regular;
font-weight: 400;
color: #fff;
}
.quan-type {
width: 19rpx;
font-size: 20rpx;
font-family: Source Han Sans CN Regular, Source Han Sans CN Regular-Regular;
font-weight: 400;
text-align: left;
color: #fff;
line-height: 22rpx;
}
.coupon {
position: absolute;
top: 7rpx;
right: 16rpx;
height: 63rpx;
}
.cash {
position: absolute;
top: 15rpx;
right: 16rpx;
height: 39rpx;
}
.prize-name {
font-size: 30rpx;
font-family: Source Han Sans CN Regular, Source Han Sans CN Regular-Regular;
font-weight: 400;
color: #333;
position: absolute;
top: 37rpx;
left: 244rpx;
}
.prize-num {
font-size: 24rpx;
font-family: Source Han Sans CN Regular, Source Han Sans CN Regular-Regular;
font-weight: 400;
color: #333;
position: absolute;
top: 86rpx;
left: 244rpx;
}
/* 分享 */
.share {
width: 668rpx;
height: 150rpx;
background: #f77b7b;
border-radius: 10rpx;
margin: 40rpx auto;
position: relative;
}
.share-left {
font-size: 30rpx;
font-family: Source Han Sans CN Normal, Source Han Sans CN Normal-Normal;
color: #fff;
position: absolute;
top: 32rpx;
left: 47rpx;
}
.lower {
position: absolute;
top: 73rpx;
left: 47rpx;
}
.share-num {
font-size: 38rpx;
}
.share-btn {
width: 170rpx;
height: 49rpx;
background: linear-gradient(0deg, #fa8d35 0%, #ffc341 100%);
border-radius: 25rpx;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 55rpx;
right: 35rpx;
}
.share-txt {
font-size: 30rpx;
font-family: Source Han Sans CN Normal, Source Han Sans CN Normal-Normal;
font-weight: 500;
color: #fff;
}
在查看紅包界面中沒有太多需要主要的東西,主要實作分享即可,
<!--RedEnvelopes/pages/RedEnvelopes/WinningRecord/WinningRecord.wxml-->
<wxs module="func">
module.exports = {
strSplit: function(str, con) {
return str.split(con);
}
}
</wxs>
<view>
<view class="calendar_container container">
<view class="select" bindtap="dateClick">
<view>{{showStartDate||showEndDate?showStartDate+'-'+showEndDate:'全部'}}</view>
</view>
<i class="iconfont iconrili" bindtap="dateClick"></i>
</view>
<view class="container">
<block wx:for="{{DrawRecordlist}}" wx:key="{{index}}">
<view class="DrawRecordItem">
<view class="item0">
<view class="item_txt">{{item.CreateTime}}</view>
</view>
<view class="item1">
<block wx:for="{{item.HongBaoWinningRecordList}}" wx:key="{{index}}">
<view class="item_txt dib mr-5">{{item.RedEnvelopeAmount}}元紅包
<text class="lookCode lookCodes">X{{item.HongBaoNum}}</text>
</view>
</block>
<block wx:for="{{item.CouponWinningRecordList}}" wx:key="{{index}}">
<view class="item_txt dib mr-5">{{item.CouponName}}
<text class="lookCode">X{{item.CouponNum}}</text>
</view>
</block>
</view>
</view>
</block>
</view>
<CalendarComponent id="calendar-component" startDate="{{startDate}}" endDate="{{endDate}}" bind:myevent="getDate"></CalendarComponent>
</view>
// RedEnvelopes/pages/RedEnvelopes/WinningRecord/WinningRecord.js
// 引入URL
const http = require('../../../../server/request.js');
//呼叫公共js物件以便呼叫其方法
var app = require('../../../../utils/uselogn.js'); //獲取應用實體
var util = require('../../../../utils/util.js') //公用方法
const api = require('../../../../server/api.js');
var pageNum = 1;
Page({
/**
* 頁面的初始資料
*/
data: {
//組件資料
componentData: null,
// 僅支持 年-月-1
startDate: "1960/1/1",
endDate: new Date().getFullYear() + "/" + (new Date().getMonth() + 1) + "/" + new Date().getDate(),
pictureServer: api.pictureServer,
tabIndex: 0,
skinStyle: "",
userDaysAward: [], //用戶簽到獎勵
signInReward: [], //連續簽到獎勵
showStartDate: "", //顯示用的
showEndDate: "", //顯示用的
time1: "",
time2: "",
DrawRecordlist: [],
ActivityId: ""
},
onLogin: app.userLogin,
/**
* 生命周期函式--監聽頁面加載
*/
onl oad: function(options) {
pageNum = 1;
//創建自定義組件實體
this.setData({
componentData: this.selectComponent('#calendar-component')
})
this.setData({
skinStyle: wx.getStorageSync("skin")
})
if (options.ActivityId) {
this.setData({
ActivityId: options.ActivityId
})
}
this.onLogin(this.callback)
},
/**
* 生命周期函式--監聽頁面初次渲染完成
*/
onReady: function() {
},
/**
* 生命周期函式--監聽頁面顯示
*/
onShow: function() {},
/**
* 生命周期函式--監聽頁面隱藏
*/
onHide: function() {
},
/**
* 生命周期函式--監聽頁面卸載
*/
onUnload: function() {
},
/**
* 頁面相關事件處理函式--監聽用戶下拉動作
*/
onPullDownRefresh: function() {
},
/**
* 頁面上拉觸底事件的處理函式
*/
onReachBottom: function() {
pageNum++;
this.getLotteryRecord();
},
/**
* 用戶點擊右上角分享
*/
onShareAppMessage: function() {
},
callback() {
this.getLotteryRecord(this.data.time1,
this.data.time2)
},
/**
* tab切換
*/
//獲取會員的中獎紀錄資訊
getLotteryRecord(s, e) {
http.requestLoading("/api/services/app/RedEnvelope/MemberWinningRecord", {
"StartDate": s ? s + " 00:00:00" : '',
"EndDate": e ? e + " 23:59:59" : '',
"page": pageNum,
"rows": 10
}, "", "POST").then(res => {
if (res.data.Result.Code === 0) {
if (!res.data.Result.Rows) {
wx.showToast({
title: '無更多資料了',
icon: "none"
})
return
}
this.setData({
DrawRecordlist: this.data.DrawRecordlist.concat(res.data.Result.Rows)
})
}
})
},
showClick(e) {
const {
index
} = e.currentTarget.dataset
const data = this.data.signInReward
data[index].show = !data[index].show
this.setData({
signInReward: data
})
},
//顯示組件
dateClick() {
this.data.componentData.createAni()
},
//獲取開始結束
getDate(e) {
const startDate = e.detail[0]
const endDate = e.detail[1]
this.setData({
showStartDate: startDate.replace(/-/g, '/'),
showEndDate: endDate ? endDate.replace(/-/g, '/') : "",
time1: startDate,
time2: endDate ? endDate : "",
DrawRecordlist: []
})
this.getLotteryRecord(startDate, endDate)
}
})
/* RedEnvelopes/pages/RedEnvelopes/WinningRecord/WinningRecord.wxss */
page {
background-color: #f5f6f7;
}
.container {
padding: 0 40rpx;
box-sizing: border-box;
}
/* 選擇日歷 */
.calendar_container {
width: 100%;
height: 86rpx;
display: flex;
align-items: center;
}
.select {
width: 320rpx;
height: 58rpx;
background: #fff;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: space-between;
color: #999;
font-size: 24rpx;
padding-left: 20rpx;
padding-right: 10rpx;
}
.iconrili {
margin-left: 22rpx;
font-size: 32rpx;
color: rgba(205, 205, 205, 0.97);
}
.DrawRecordItem {
margin-top: 16rpx;
background: #fff;
border-radius: 10rpx;
}
.item1 {
padding: 0 22rpx 22rpx 22rpx;
width: 100%;
background: #fff;
border-radius: 10rpx;
}
.lookCode {
font-size: 24rpx;
font-family: Source Han Sans CN Normal, Source Han Sans CN Normal-Normal;
font-weight: 400;
color: #4ace8c;
}
.lookCodes {
color: #fc7297;
}
.item0 {
padding: 0 16rpx 0 26rpx;
width: 100%;
height: 66rpx;
background: #fff;
border-radius: 10rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.item_txt {
font-size: 24rpx;
font-family: Source Han Sans CN Normal, Source Han Sans CN Normal-Normal;
font-weight: 400;
color: #666;
}
.mr-5 {
margin-right: 20rpx;
}
在中獎紀錄查看的頁面中使用的自己封裝的日歷組件,僅是為了復合專案的要求,在此就不做展示了,
紅包雨整個流程到此就結束了,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/70950.html
標籤:其他
上一篇:[題解]the moon
