秒杀商品详情页面完成

This commit is contained in:
wuhui_zzw 2023-02-28 17:40:56 +08:00
parent 0ddf87dd7f
commit 2cf76eb3e7
6 changed files with 830 additions and 10 deletions

View File

@ -0,0 +1,128 @@
// const defaultOption = {
// duration: 300,
// timingFunction: 'linear',
// delay: 0,
// transformOrigin: '50% 50% 0'
// }
// #ifdef APP-NVUE
const nvueAnimation = uni.requireNativePlugin('animation')
// #endif
class MPAnimation {
constructor(options, _this) {
this.options = options
this.animation = uni.createAnimation(options)
this.currentStepAnimates = {}
this.next = 0
this.$ = _this
}
_nvuePushAnimates(type, args) {
let aniObj = this.currentStepAnimates[this.next]
let styles = {}
if (!aniObj) {
styles = {
styles: {},
config: {}
}
} else {
styles = aniObj
}
if (animateTypes1.includes(type)) {
if (!styles.styles.transform) {
styles.styles.transform = ''
}
let unit = ''
if(type === 'rotate'){
unit = 'deg'
}
styles.styles.transform += `${type}(${args+unit}) `
} else {
styles.styles[type] = `${args}`
}
this.currentStepAnimates[this.next] = styles
}
_animateRun(styles = {}, config = {}) {
let ref = this.$.$refs['ani'].ref
if (!ref) return
return new Promise((resolve, reject) => {
nvueAnimation.transition(ref, {
styles,
...config
}, res => {
resolve()
})
})
}
_nvueNextAnimate(animates, step = 0, fn) {
let obj = animates[step]
if (obj) {
let {
styles,
config
} = obj
this._animateRun(styles, config).then(() => {
step += 1
this._nvueNextAnimate(animates, step, fn)
})
} else {
this.currentStepAnimates = {}
typeof fn === 'function' && fn()
this.isEnd = true
}
}
step(config = {}) {
// #ifndef APP-NVUE
this.animation.step(config)
// #endif
// #ifdef APP-NVUE
this.currentStepAnimates[this.next].config = Object.assign({}, this.options, config)
this.currentStepAnimates[this.next].styles.transformOrigin = this.currentStepAnimates[this.next].config.transformOrigin
this.next++
// #endif
return this
}
run(fn) {
// #ifndef APP-NVUE
this.$.animationData = this.animation.export()
this.$.timer = setTimeout(() => {
typeof fn === 'function' && fn()
}, this.$.durationTime)
// #endif
// #ifdef APP-NVUE
this.isEnd = false
let ref = this.$.$refs['ani'] && this.$.$refs['ani'].ref
if(!ref) return
this._nvueNextAnimate(this.currentStepAnimates, 0, fn)
this.next = 0
// #endif
}
}
const animateTypes1 = ['matrix', 'matrix3d', 'rotate', 'rotate3d', 'rotateX', 'rotateY', 'rotateZ', 'scale', 'scale3d',
'scaleX', 'scaleY', 'scaleZ', 'skew', 'skewX', 'skewY', 'translate', 'translate3d', 'translateX', 'translateY',
'translateZ'
]
const animateTypes2 = ['opacity', 'backgroundColor']
const animateTypes3 = ['width', 'height', 'left', 'right', 'top', 'bottom']
animateTypes1.concat(animateTypes2, animateTypes3).forEach(type => {
MPAnimation.prototype[type] = function(...args) {
// #ifndef APP-NVUE
this.animation[type](...args)
// #endif
// #ifdef APP-NVUE
this._nvuePushAnimates(type, args)
// #endif
return this
}
})
export function createAnimation(option, _this) {
if(!_this) return
clearTimeout(_this.timer)
return new MPAnimation(option, _this)
}

View File

@ -0,0 +1,277 @@
<template>
<view v-if="isShow" ref="ani" :animation="animationData" :class="customClass" :style="transformStyles" @click="onClick"><slot></slot></view>
</template>
<script>
import { createAnimation } from './createAnimation'
/**
* Transition 过渡动画
* @description 简单过渡动画组件
* @tutorial https://ext.dcloud.net.cn/plugin?id=985
* @property {Boolean} show = [false|true] 控制组件显示或隐藏
* @property {Array|String} modeClass = [fade|slide-top|slide-right|slide-bottom|slide-left|zoom-in|zoom-out] 过渡动画类型
* @value fade 渐隐渐出过渡
* @value slide-top 由上至下过渡
* @value slide-right 由右至左过渡
* @value slide-bottom 由下至上过渡
* @value slide-left 由左至右过渡
* @value zoom-in 由小到大过渡
* @value zoom-out 由大到小过渡
* @property {Number} duration 过渡动画持续时间
* @property {Object} styles 组件样式 css 样式注意带-连接符的属性需要使用小驼峰写法如`backgroundColor:red`
*/
export default {
name: 'uniTransition',
emits:['click','change'],
props: {
show: {
type: Boolean,
default: false
},
modeClass: {
type: [Array, String],
default() {
return 'fade'
}
},
duration: {
type: Number,
default: 300
},
styles: {
type: Object,
default() {
return {}
}
},
customClass:{
type: String,
default: ''
}
},
data() {
return {
isShow: false,
transform: '',
opacity: 1,
animationData: {},
durationTime: 300,
config: {}
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.open()
} else {
// close,
if (this.isShow) {
this.close()
}
}
},
immediate: true
}
},
computed: {
//
stylesObject() {
let styles = {
...this.styles,
'transition-duration': this.duration / 1000 + 's'
}
let transform = ''
for (let i in styles) {
let line = this.toLine(i)
transform += line + ':' + styles[i] + ';'
}
return transform
},
//
transformStyles() {
return 'transform:' + this.transform + ';' + 'opacity:' + this.opacity + ';' + this.stylesObject
}
},
created() {
//
this.config = {
duration: this.duration,
timingFunction: 'ease',
transformOrigin: '50% 50%',
delay: 0
}
this.durationTime = this.duration
},
methods: {
/**
* ref 触发 初始化动画
*/
init(obj = {}) {
if (obj.duration) {
this.durationTime = obj.duration
}
this.animation = createAnimation(Object.assign(this.config, obj),this)
},
/**
* 点击组件触发回调
*/
onClick() {
this.$emit('click', {
detail: this.isShow
})
},
/**
* ref 触发 动画分组
* @param {Object} obj
*/
step(obj, config = {}) {
if (!this.animation) return
for (let i in obj) {
try {
if(typeof obj[i] === 'object'){
this.animation[i](...obj[i])
}else{
this.animation[i](obj[i])
}
} catch (e) {
console.error(`方法 ${i} 不存在`)
}
}
this.animation.step(config)
return this
},
/**
* ref 触发 执行动画
*/
run(fn) {
if (!this.animation) return
this.animation.run(fn)
},
//
open() {
clearTimeout(this.timer)
this.transform = ''
this.isShow = true
let { opacity, transform } = this.styleInit(false)
if (typeof opacity !== 'undefined') {
this.opacity = opacity
}
this.transform = transform
// nextTick wx
this.$nextTick(() => {
// TODO
this.timer = setTimeout(() => {
this.animation = createAnimation(this.config, this)
this.tranfromInit(false).step()
this.animation.run()
this.$emit('change', {
detail: this.isShow
})
}, 20)
})
},
//
close(type) {
if (!this.animation) return
this.tranfromInit(true)
.step()
.run(() => {
this.isShow = false
this.animationData = null
this.animation = null
let { opacity, transform } = this.styleInit(false)
this.opacity = opacity || 1
this.transform = transform
this.$emit('change', {
detail: this.isShow
})
})
},
//
styleInit(type) {
let styles = {
transform: ''
}
let buildStyle = (type, mode) => {
if (mode === 'fade') {
styles.opacity = this.animationType(type)[mode]
} else {
styles.transform += this.animationType(type)[mode] + ' '
}
}
if (typeof this.modeClass === 'string') {
buildStyle(type, this.modeClass)
} else {
this.modeClass.forEach(mode => {
buildStyle(type, mode)
})
}
return styles
},
//
tranfromInit(type) {
let buildTranfrom = (type, mode) => {
let aniNum = null
if (mode === 'fade') {
aniNum = type ? 0 : 1
} else {
aniNum = type ? '-100%' : '0'
if (mode === 'zoom-in') {
aniNum = type ? 0.8 : 1
}
if (mode === 'zoom-out') {
aniNum = type ? 1.2 : 1
}
if (mode === 'slide-right') {
aniNum = type ? '100%' : '0'
}
if (mode === 'slide-bottom') {
aniNum = type ? '100%' : '0'
}
}
this.animation[this.animationMode()[mode]](aniNum)
}
if (typeof this.modeClass === 'string') {
buildTranfrom(type, this.modeClass)
} else {
this.modeClass.forEach(mode => {
buildTranfrom(type, mode)
})
}
return this.animation
},
animationType(type) {
return {
fade: type ? 1 : 0,
'slide-top': `translateY(${type ? '0' : '-100%'})`,
'slide-right': `translateX(${type ? '0' : '100%'})`,
'slide-bottom': `translateY(${type ? '0' : '100%'})`,
'slide-left': `translateX(${type ? '0' : '-100%'})`,
'zoom-in': `scaleX(${type ? 1 : 0.8}) scaleY(${type ? 1 : 0.8})`,
'zoom-out': `scaleX(${type ? 1 : 1.2}) scaleY(${type ? 1 : 1.2})`
}
},
//
animationMode() {
return {
fade: 'opacity',
'slide-top': 'translateY',
'slide-right': 'translateX',
'slide-bottom': 'translateY',
'slide-left': 'translateX',
'zoom-in': 'scale',
'zoom-out': 'scale'
}
},
// 线
toLine(name) {
return name.replace(/([A-Z])/g, '-$1').toLowerCase()
}
}
}
</script>
<style></style>

View File

@ -1003,6 +1003,24 @@
// #endif // #endif
"navigationBarTitleText": "限时秒杀" "navigationBarTitleText": "限时秒杀"
} }
},
{
"path": "futures/details",
"style": {
// #ifdef H5
"navigationStyle": "custom",
// #endif
"navigationBarTitleText": "商品详情"
}
},
{
"path": "futures/my_shop",
"style": {
// #ifdef H5
"navigationStyle": "custom",
// #endif
"navigationBarTitleText": "我的小店"
}
} }
@ -1010,7 +1028,6 @@
] ]
}, },
{ {

View File

@ -0,0 +1,334 @@
<template>
<page-meta :page-style="themeColor"></page-meta>
<view class="details">
<!--顶部内容-->
<view class="top">
<image class="top-image" :src="$util.img('upload/1/common/images/20230216/20230216050332167653821225076.png')" mode="widthFix"></image>
<view class="top-count-down">
<image class="top-down-image" :src="$util.img('public/static/img/futures/details_down_bg.png')" mode="widthFix"></image>
<view class="top-down-text">
<image class="top-down-text-image" :src="$util.img('public/static/img/futures/details_down_icon.png')" mode="widthFix"></image>
正在销售
</view>
<view class="top-count-down-time">
<text class="count-down-time">{{ h }}</text>:
<text class="count-down-time">{{ i }}</text>:
<text class="count-down-time">{{ s }}</text>
</view>
</view>
</view>
<!--参数信息-->
<view class="params-info">
<view class="title">脑黄金为智慧加分 澳大利亚进口保健品 适合中老年人长期熬夜人群使用</view>
<view class="info-item">
售卖价格<text class="info-num"><text class="unit"></text>1200/</text>
</view>
<view class="info-item">
采购价格<text class="buy-price"><text class="unit"></text>600.34</text>
</view>
<view class="info-item">
<view class="info-block">
采购数量<text class="info-num">12</text>
</view>
<view class="info-block">
销售商<text class="info-num">一颗柚子</text>
</view>
</view>
</view>
<!--商品详情富文本-->
<view class="goods-detail">
<view class="detail-top">
<view class="left">采购商品详情</view>
<view class="right" @click="details_show = !details_show">
展开
<i v-if="details_show" class="icondiy icon-system-jiantoushang"></i>
<i v-else class="icondiy icon-system-jiantouxia"></i>
</view>
</view>
<view class="goods-detail-content">
<uni-transition
id="goodsDetailContent"
ref="detail"
:show="details_show"
mode-class="slide-top"
:duration="2000"
>
这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />
这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />
这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />
这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />
这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />
这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />
这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />
这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />
这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />这里是富文本的监听内容<br />
</uni-transition>
</view>
</view>
<!--按钮-->
<view class="bottom-buttons">
<view class="button-item">添加备注</view>
<view class="button-item">立即采购</view>
</view>
<!--加载动画-->
<loading-cover ref="loadingCover"></loading-cover>
</view>
</template>
<script>
import uniTransition from '@/components/uni-transition/uni-transition.vue';
export default {
data() {
return {
//
end_time: 1680170346,//
diff_time: 0,
h: '00',
i: '00',
s: '00',
interval: '',
//
details_show: false,
};
},
components: {
uniTransition
},
mixins: [],
onLoad(option) {},
onShow() {},
onReady(){
//
if (!uni.getStorageSync('token')) this.$refs.login.open('/pages_promotion/futures/seckill');
//
this.countDown();
if (this.$refs.loadingCover) this.$refs.loadingCover.hide();
},
methods: {
//
countDown(){
this.interval = setInterval(() => {
//
let nowTime = Math.ceil((new Date()).getTime() / 1000);
this.diff_time = this.end_time - nowTime;
//
let result = this.$util.countDown(this.diff_time);
this.h = (parseInt(result.d) * 24) + parseInt(result.h);
this.i = parseInt(result.i);
this.s = parseInt(result.s);
}, 1000)
}
},
onBackPress(options) {
if (options.from === 'navigateBack') return false;
this.$util.redirectTo('/pages/member/index');
return true;
}
};
</script>
<style lang="scss" scoped>
.details{
background: #f5f5f5;// #f5f5f5
min-height: 100vh;
width: 100vw;
.top{
height: 100vw;
position: relative;
.top-image{
width: 100vw;
max-height: 100%;
}
.top-count-down{
--count-down-size--: 96rpx;
--top-count-down-time-width--: 230rpx;
height: var(--count-down-size--);
position: absolute;
bottom: 0;
left: 0;
overflow: hidden;
width: 100%;
.top-down-image{
width: 100%;
height: 100%;
}
.top-down-text{
position: absolute;
bottom: 0;
right: var(--top-count-down-time-width--);
height: var(--count-down-size--);
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: flex-end;
padding-right: 55rpx;
font-size: 30rpx;
font-weight: bold;
color: #fe6276;
.top-down-text-image{
width: 36rpx!important;
margin-right: 15rpx;
}
}
.top-count-down-time{
position: absolute;
bottom: 0;
right: 0;
height: var(--count-down-size--);
width: var(--top-count-down-time-width--);
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: center;
.count-down-time{
--size--: 50rpx;
background: #313131;
color: #c6c7ca;
min-width: calc(var(--size--) - 20rpx);
height: var(--size--);
border-radius: 10rpx;
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
align-content: center;
justify-content: center;
align-items: center;
margin: 0 6rpx;
font-size: 26rpx;
padding: 0 10rpx;
}
}
}
}
.params-info{
padding: 20rpx;
background: #FFFFFF;
.title{
width: 100%;
height: 60rpx;
line-height: 50rpx;
font-size: 34rpx;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.info-item{
height: 50rpx;
line-height: 40rpx;
font-size: 28rpx;
color: #747474;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
.unit{
font-size: 24rpx;
display: inline-block;
width: 22rpx;
}
.info-num{
font-weight: bold;
color: #040404;
}
.buy-price{
font-size: 34rpx;
font-weight: bold;
color: #fe5572;
}
.info-block{
width: 50%;
float: left;
font-size: 28rpx;
color: #747474;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.goods-detail{
background: #fcfcfc;
margin-top: 20rpx;
padding: 20rpx;
.detail-top{
width: 100%;
height: 70rpx;
margin-bottom: 20rpx;
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
.left{
font-size: 30rpx;
font-weight: bold;
color: #3e3e3e;
}
.right{
font-size: 13px;
color: #838383;
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
height: 70rpx;
i{
margin-left: 20rpx;
color: #838383;
}
}
}
.goods-detail-content{
overflow: hidden;
}
}
.bottom-buttons{
position: fixed;
bottom: 0;
left: 0;
width: 100%;
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: space-around;
height: 100rpx;
background: #FFFFFF;
.button-item{
height: 80rpx;
line-height: 80rpx;
font-size: 30rpx;
width: 240rpx;
border-radius: 100vw;
text-align: center;
}
.button-item:nth-of-type(1){
background: #FFFFFF;
border: 2rpx solid #dfdfdf;
color: #838383;
}
.button-item:nth-of-type(2){
background: #ff4546;
color: #fee6e6;
}
}
}
</style>

View File

@ -0,0 +1,68 @@
<template>
<page-meta :page-style="themeColor"></page-meta>
<view class="apply">
<!--加载动画-->
<loading-cover ref="loadingCover"></loading-cover>
</view>
</template>
<script>
export default {
data() {
return {
};
},
components: {},
mixins: [],
onLoad(option) {
},
async onShow() {
},
methods: {
},
onBackPress(options) {
if (options.from === 'navigateBack') return false;
this.$util.redirectTo('/pages/member/index');
return true;
}
};
</script>
<style lang="scss" scoped>
</style>

View File

@ -32,7 +32,7 @@
<view class="price-item">采购价:<text class="purchase_price">1199.99</text></view> <view class="price-item">采购价:<text class="purchase_price">1199.99</text></view>
</view> </view>
<view class="buy-button"> <view class="buy-button">
<view class="buy-btn">前去采购</view> <view class="buy-btn" @click="$util.redirectTo('/pages_promotion/futures/details', { id: item.id }, 'redirectTo');">前去采购</view>
</view> </view>
</view> </view>
</view> </view>
@ -58,9 +58,9 @@ export default {
// //
end_time: 1680170346,// end_time: 1680170346,//
diff_time: 0, diff_time: 0,
h: 0, h: '00',
i: 0, i: '00',
s: 0, s: '00',
interval: '', interval: '',
@ -119,10 +119,6 @@ export default {
} }
}, },
onBackPress(options) { onBackPress(options) {
if (options.from === 'navigateBack') return false; if (options.from === 'navigateBack') return false;
@ -162,7 +158,7 @@ export default {
--size--: 45rpx; --size--: 45rpx;
background: #ffffff; background: #ffffff;
color: #fe7671; color: #fe7671;
max-width: var(--size--); min-width: calc(var(--size--) - 20rpx);
height: var(--size--); height: var(--size--);
border-radius: 10rpx; border-radius: 10rpx;
display: inline-flex; display: inline-flex;