douyin/src/components/slide/SlideAlbum.vue
2023-01-25 03:53:45 +08:00

621 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div id="SlideAlbum">
<div class="img-slide-wrapper">
<div class="img-slide-list"
ref="wrapperEl"
@touchstart.passive="touchStart"
@touchmove="touchMove"
@touchend="touchEnd">
<div class="img-slide-item" v-for="img in props.modelValue.imgs">
<img :ref="e=>setItemRef(e,'itemRefs')"
:src="img">
</div>
</div>
</div>
<template v-if=" state.operationStatus === SlideAlbumOperationStatus.Normal">
<ItemToolbar :item="props.modelValue"
:index="0"
prefix="sadfa"
/>
<ItemDesc
:item="props.modelValue"
:index="0"
prefix="sadfa"
/>
</template>
<!--不知为啥touch事件在下部20px的空间内不触发加上click事件不好了 -->
<div class="progress-bar"
v-if="!state.isPreview && state.operationStatus!== SlideAlbumOperationStatus.Zooming"
@click="null"
@touchstart="progressBarTouchStart"
@touchmove="progressBarTouchMove"
@touchend="progressBarTouchMEnd"
>
<div class="bar" v-for="(img,index) in modelValue.imgs">
<div class="progress"
:style="getProgressWidth(index)"></div>
</div>
</div>
<Teleport to="#slideHook" v-if="state.isPreview">
<div class="preview">
<div class="preview-wrapper">
<img :src="img"
:class="{'preview-img':index === state.localIndex}"
v-for="(img,index) in props.modelValue.imgs"
:ref="e=>setItemRef(e,'previewImgs')"
>
</div>
<div class="indicator">
<span class="index">{{ state.localIndex + 1 }}</span>&nbsp;/&nbsp;{{ props.modelValue.imgs.length }}
</div>
</div>
</Teleport>
<Teleport to="#slideHook" v-if="state.operationStatus === SlideAlbumOperationStatus.Detail">
<div class="album-toolbar">
<div class="left">关闭</div>
<div class="right">
<div class="option">评论</div>
<div class="option">切换</div>
<div class="option">下载</div>
</div>
</div>
</Teleport>
</div>
</template>
<script setup lang="jsx">
import enums from "../../utils/enums";
import Utils from '../../utils'
import {mat4} from 'gl-matrix'
import {onMounted, onBeforeUpdate, reactive, ref, watch, computed} from "vue";
import {
getSlideDistance,
slideInit,
slideReset,
slideTouchEnd,
slideTouchMove,
slideTouchStart
} from "../../pages/slideHooks/common";
import {SlideAlbumOperationStatus, SlideType} from "../../utils/const_var";
import ItemToolbar from "./ItemToolbar";
import ItemDesc from "./ItemDesc";
import GM from "../../utils";
import {cloneDeep} from "lodash";
import bus from "../../utils/bus";
let out = new Float32Array([
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0
])
let ov = new Float32Array([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]);
let origin = cloneDeep(ov)
const rectMap = new Map()
const props = defineProps({
modelValue: {
type: Object,
default() {
return {
type: 'imgs',
imgs: [
new URL('../../assets/img/poster/0.jpg', import.meta.url).href,
new URL('../../assets/img/poster/1.jpg', import.meta.url).href,
new URL('../../assets/img/poster/2.jpg', import.meta.url).href,
new URL('../../assets/img/poster/3.jpg', import.meta.url).href,
new URL('../../assets/img/poster/4.jpg', import.meta.url).href,
new URL('../../assets/img/poster/5.jpg', import.meta.url).href,
new URL('../../assets/img/poster/6.jpg', import.meta.url).href,
],
"id": "034ae83b-ca0a-401a-b7c6-cf78361bae7b",
video: 'http://douyin.ttentau.top/0.mp4',
"video_data_size": 26829508,
"duration": 427780,
"desc": "我不管我们宿舍第一好看",
"allow_download": 0,
"allow_duet": 0,
"allow_react": 0,
"allow_music": 1,
"allow_douplus": 1,
"allow_share": 1,
"digg_count": 10480000,
"comment_count": 79000,
"download_count": 6,
"play_count": 0,
"share_count": 119000,
"forward_count": 0,
"collect_count": 3,
"sort": 195,
"is_top": 0,
"city": "北京",
address: '中央戏剧学院',
"musicId": "2ee213c6-3e3f-4758-ba5a-7f1c955604a4",
"create_time": "1630423555",
"creator_id": "93864497380",
"status": 1,
"topics": [
{
"id": "85ceda30-898f-4b57-b891-0e58b3ab99a9",
"name": "敬礼变装",
"creator_id": "93864497380",
"create_time": "1630423555",
"status": 1
},
{
"id": "85ceda30-898f-4b57-b891-0e58b3ab99a9",
"name": "宿舍",
"creator_id": "93864497380",
"create_time": "1630423555",
"status": 1
}
],
"music": {
"id": "cde50af2-628c-4d28-b9c6-67237a62518e",
"cover": "https://p29.douyinpic.com/img/tos-cn-avt-0015/f4de202ff2e41b523838a4a767aebd16~c5_100x100.jpeg?from=116350172",
"mp3": "https://sf3-cdn-tos.douyinstatic.com/obj/ies-music/1658584661080088.mp3",
"title": "@穷电影创作的原声-小高快起来跳舞",
"creator_id": "93864497380",
"create_time": "1630423555",
"status": 1
},
"author": {
"id": "1",
"unique_id_modify_time": "1630393144",
"unique_id": "10040050",
"favoriting_count": 143,
"avatar": new URL('../../assets/img/icon/avatar/3.png', import.meta.url).href,
school: {
name: '中央戏剧学院',
department: null,
joinTime: null,
education: null,
displayType: enums.DISPLAY_TYPE.ALL,
},
"city": "",
"province": '北京',
"country": "",
"location": "",
"birthday": "2002-01-01",
"cover": "https://p3.douyinpic.com/obj/c8510002be9a3a61aad2?from=116350172",
"following_count": 66,
"follower_count": 235000,
"aweme_count": 1796000,
"nickname": "我是小睿耶",
certification: '',
"phone": "",
"sex": "",
"last_login_time": "1630423555",
"create_time": "1630423555",
"status": 1,
"desc": `一个普普通通学表演的\n看到的人都能开开心心`,
"is_private": 0
}
}
}
}
})
const judgeValue = 20
const wrapperEl = ref(null)
const state = reactive({
name: 'SlideHorizontal',
localIndex: 0,
needCheck: true,
isPreview: false,
isZoom: false,
operationStatus: SlideAlbumOperationStatus.Normal,
next: false,
wrapper: {width: 0, height: 0, childrenLength: 0},
last: {
point1: {x: 0, y: 0},
point2: {x: 0, y: 0},
},
start: {
x: 0, y: 0,
point1: {x: 0, y: 0},
point2: {x: 0, y: 0},
center: {x: 0, y: 0},
time: 0
},
move: {x: 0, y: 0},
itemRefs: [],
previewImgs: [],
status: 'play',//stop,custom
progress: 0,
cycleFn: null,
})
onMounted(() => {
slideInit(wrapperEl.value, state, SlideType.HORIZONTAL)
state.cycleFn = () => {
return
if (state.status !== 'play') return cancelAnimationFrame(state.cycleFn)
if (state.progress < props.modelValue.imgs.length * 100) {
state.progress += .4
state.localIndex = parseInt(state.progress / 100)
if (wrapperEl.value) {
Utils.$setCss(wrapperEl.value, 'transition-duration', `300ms`)
Utils.$setCss(wrapperEl.value, 'transform', `translate3d(${getSlideDistance(state)}px, 0px, 0px)`)
}
} else {
state.progress = 0
// cancelAnimationFrame(this.cycleFn)
}
requestAnimationFrame(state.cycleFn)
}
requestAnimationFrame(state.cycleFn)
})
// 确保在每次更新之前重置ref
onBeforeUpdate(() => {
state.itemRefs = []
state.previewImgs = []
})
watch(
() => state.localIndex,
(newVal) => {
GM.$setCss(wrapperEl.value, 'transition-duration', `300ms`)
GM.$setCss(wrapperEl.value, 'transform', `translate3d(${getSlideDistance(state, SlideType.HORIZONTAL)}px, 0, 0)`)
state.progress = (state.localIndex + 1) * 100
}
)
watch(
() => state.operationStatus,
(newVal) => {
if (newVal === SlideAlbumOperationStatus.Zooming) {
bus.emit('enterFullscreen')
} else {
bus.emit('exitFullscreen')
}
}
)
const isZooming = computed(() => {
return state.operationStatus === SlideAlbumOperationStatus.Zooming
})
function calcCurrentIndex(e) {
state.isPreview = true
let x = e.touches[0].pageX
let current = -1
let length = state.previewImgs.length
for (let i = length - 1; i >= 0; i--) {
let rect = state.previewImgs[i].getBoundingClientRect()
if (rect.x < x) {
current = i
break
}
}
if (current > -1) {
state.localIndex = current
}
}
function progressBarTouchStart(e) {
Utils.$stopPropagation(e)
}
function progressBarTouchMove(e) {
Utils.$stopPropagation(e)
calcCurrentIndex(e)
}
function progressBarTouchMEnd(e) {
Utils.$stopPropagation(e)
state.isPreview = false
}
function touchStart(e) {
console.log('start', e.touches.length)
if (e.touches.length === 1) {
slideTouchStart(e, wrapperEl.value, state)
} else {
if (isZooming.value) return
state.operationStatus = SlideAlbumOperationStatus.Zooming
state.itemRefs[state.localIndex].style['transition-duration'] = '0ms';
state.last.point1 = state.start.point1 = {x: e.touches[0].pageX, y: e.touches[0].pageY};
state.last.point2 = state.start.point2 = {x: e.touches[1].pageX, y: e.touches[1].pageY};
state.start.center = Utils.getCenter(state.start.point1, state.start.point2)
}
}
function touchMove(e) {
console.log('move', e.touches.length,)
let current1 = {x: e.touches[0].pageX, y: e.touches[0].pageY}
if (isZooming.value && e.touches.length === 1) {
console.log('m1')
state.status = 'pause'
Utils.$stopPropagation(e)
// console.log('单手移动',)
let movementX = current1.x - state.last.point1.x
let movementY = current1.y - state.last.point1.y
// console.log(movementX, movementY)
const t = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, movementX, movementY, 0, 1,]);
ov = mat4.multiply(out, t, ov);
state.itemRefs[state.localIndex].style.transform = `matrix3d(${ov.toString()})`;
state.last.point1 = current1
} else {
if (e.touches.length === 1) {
console.log('m2')
slideTouchMove(e, wrapperEl.value, state, judgeValue, canNext,
() => {
state.status = 'pause'
}, SlideType.HORIZONTAL,
() => {
if (state.operationStatus === SlideAlbumOperationStatus.Detail) {
Utils.$stopPropagation(e)
}
})
} else {
console.log('m3')
state.operationStatus = SlideAlbumOperationStatus.Zooming
Utils.$stopPropagation(e)
state.status = 'pause'
let rect = {x: 0, y: 0}
if (rectMap.has(state.localIndex)) {
rect = rectMap.get(state.localIndex)
} else {
//getBoundingClientRect在手机上获取不到值
let offset = $(state.itemRefs[state.localIndex]).offset()
rect = {x: offset.left, y: offset.top}
rectMap.set(state.localIndex, rect)
}
let current2 = {x: e.touches[1].pageX, y: e.touches[1].pageY}
// 双指缩放比例,就是对应的放大倍数
let currentRatio = Utils.getDistance(current1, current2) / Utils.getDistance(state.start.point1, state.start.point2);
let center = Utils.getCenter(current1, current2)
center.x -= rect.x
center.y -= rect.y
//用最新的放大倍数ratio除以之前的放大ov[0]倍数,算出本次要累加放大的倍数
let zoom = currentRatio / ov[0]
const x = center.x * (1 - zoom);
const y = center.y * (1 - zoom);
const t = new Float32Array([zoom, 0, 0, 0, 0, zoom, 0, 0, 0, 0, 1, 0, x, y, 0, 1,]);
//如果zoom是每次都是最后放大倍数第三个参数用原值矩阵x乘时都是乘以单位矩阵
//如果zoom是累加放大比如每次都是0.15第三个参数用ov。这里还是采用累加计算
ov = mat4.multiply(out, t, ov);
let movementRatio = currentRatio - ov[0]
//如果本次比例和上次的不超过0.02。那么判定为平移
if (Math.abs(movementRatio) <= 0.02) {
let movementX = current1.x - state.last.point1.x
let movementY = current1.y - state.last.point1.y
let movement2X = current2.x - state.last.point2.x
let movement2Y = current2.y - state.last.point2.y
let minX = Math.min(movementX, movement2X)
let minY = Math.min(movementY, movement2Y)
const t1 = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, minX, minY, 0, 1,]);
ov = mat4.multiply(out, t1, ov);
}
state.itemRefs[state.localIndex].style.transform = `matrix3d(${ov.toString()})`;
state.last.point1 = current1
state.last.point2 = current2
}
}
}
function touchEnd(e) {
state.isPreview = false
console.log('end', e.touches.length, state.operationStatus)
//双指缩放,但只松开了一只手
if (isZooming.value && e.touches.length === 1) {
Utils.$stopPropagation(e)
state.last.point1 = {x: e.touches[0].pageX, y: e.touches[0].pageY}
} else {
if (isZooming.value) {
state.operationStatus = SlideAlbumOperationStatus.Detail
ov = origin
Utils.$stopPropagation(e)
state.itemRefs[state.localIndex].style['transition-duration'] = '300ms';
state.itemRefs[state.localIndex].style.transform = `matrix3d(${origin.toString()})`;
} else {
slideTouchEnd(e, state, canNext,
() => {
state.status = 'custom'
state.progress = (state.localIndex + 1) * 100
},
() => {
console.log('notNextCb')
state.operationStatus = SlideAlbumOperationStatus.Normal
if (state.status !== 'custom') {
state.status = 'play'
requestAnimationFrame(state.cycleFn)
}
}
)
slideReset(wrapperEl.value, state, SlideType.HORIZONTAL, null)
}
}
}
function getProgressWidth(index) {
if (state.progress >= (index + 1) * 100) return {width: '100%'}
return {width: `${state.progress - index * 100 < 0 ? 0 : state.progress - index * 100}%`}
}
function setItemRef(el, key) {
el && state[key].push(el)
}
function canNext(isNext, e) {
let res = !((state.localIndex === 0 && !isNext) || (state.localIndex === props.modelValue.imgs.length - 1 && isNext));
if (!res && state.operationStatus === SlideAlbumOperationStatus.Detail && e) {
Utils.$stopPropagation(e)
}
return res
}
</script>
<style scoped lang="less">
@import "@/assets/less/index";
#SlideAlbum {
transition: height .3s;
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
color: white;
font-size: 14rem;
.img-slide-wrapper {
position: relative;
height: 100%;
width: 100%;
.img-slide-list {
height: 100%;
width: 100%;
display: flex;
position: relative;
.img-slide-item {
height: 100%;
width: 100%;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
img {
transform-origin: 0 0;
width: 100%;
}
}
}
}
.progress-bar {
position: absolute;
width: 100%;
bottom: 0;
display: flex;
box-sizing: border-box;
padding: 0 5rem;
@h: 4rem;
//height: @h;
height: 10rem;
//background-color: red;
align-items: flex-end;
justify-content: space-between;
.bar {
border-radius: 10rem;
flex: 1;
margin: 0 2rem;
height: @h;
background: rgba(#000, .5);
position: relative;
overflow: hidden;
.progress {
border-radius: 10rem;
position: absolute;
left: 0;
height: @h;
background: white;
}
}
}
}
</style>
<style lang="less">
@import "@/assets/less/index";
.preview {
transition: opacity .3s;
position: fixed;
bottom: 0;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
.preview-wrapper {
img {
transition: width .3s;
margin: 0 5rem;
width: 30rem;
height: 50rem;
background-color: black;
object-fit: contain;
border-radius: 3rem;
overflow: hidden;
&.preview-img {
width: 40rem;
}
}
}
.indicator {
background: @footer-color;
width: 100%;
height: @footer-height;
color: gray;
display: flex;
align-items: center;
justify-content: center;
.index {
color: white;
}
}
}
.album-toolbar {
position: absolute;
bottom: 0;
background: @footer-color;
width: 100%;
box-sizing: border-box;
height: @footer-height;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10rem;
@padding: 12rem;
.left {
height: 34rem;
background-color: gray;
border-radius: 6rem;
padding: 0 @padding;
display: flex;
align-items: center;
justify-content: center;
}
.right {
.left;
.option {
margin: 0 5rem;
}
}
}
</style>