douyin/src/components/slide/SlideAlbum.txt
2024-04-24 18:58:02 +08:00

755 lines
20 KiB
Plaintext
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="slideListEl"
@touchstart.passive="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
>
<div class="img-slide-item" :key="index" v-for="(img, index) in item.imgs">
<img :ref="(e) => setItemRef(e, 'itemRefs')" :src="img + '&d=' + index" />
</div>
</div>
</div>
<Icon
icon="fluent:play-28-filled"
class="pause-icon"
v-if="
state.status === SlideItemPlayStatus.Pause &&
state.operationStatus === SlideAlbumOperationStatus.Normal
"
/>
<template v-if="state.operationStatus === SlideAlbumOperationStatus.Normal">
<!-- <ItemToolbar-->
<!-- class="mb3r"-->
<!-- v-model:item="state.localItem"-->
<!-- :position="position"-->
<!-- v-bind="$attrs"-->
<!-- />-->
<!-- <ItemDesc class="mb3r" v-model:item="state.localItem" :position="position" />-->
</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" :key="index" v-for="(img, index) in item.imgs">
<div class="progress" :style="getWidth(index)"></div>
</div>
</div>
<Teleport to="#home-index" v-if="state.isPreview">
<div class="preview">
<div class="preview-wrapper">
<img
:src="img + '&d=' + index"
:class="{ 'preview-img': index === state.localIndex }"
:key="index"
v-for="(img, index) in props.item.imgs"
:ref="(e) => setItemRef(e, 'previewImgs')"
/>
</div>
<div class="indicator">
<span class="index">{{ state.localIndex + 1 }}</span
>&nbsp;/&nbsp;{{ props.item.imgs.length }}
</div>
</div>
</Teleport>
<Teleport to="#home-index" v-if="state.operationStatus !== SlideAlbumOperationStatus.Normal">
<div class="album-toolbar">
<div class="left">
<Icon
icon="iconamoon:close"
@click="state.operationStatus = SlideAlbumOperationStatus.Normal"
/>
</div>
<div class="right">
<Icon icon="heroicons-outline:menu-alt-1" @click="Utils._no" />
<Icon
icon="fluent:play-28-filled"
v-if="state.status === SlideItemPlayStatus.Pause"
class="pause"
@click="startPlay"
/>
<Icon icon="bi:pause-fill" v-else class="pause" @click="stopPlay" />
<Icon icon="system-uicons:push-down" @click="_notice('已保存到系统相册')" />
</div>
</div>
</Teleport>
</div>
</template>
<script setup lang="jsx">
import enums from '../../utils/enums'
import Utils from '../../utils'
import GM, { _notice } from '../../utils'
import { mat4 } from 'gl-matrix'
import { Icon } from '@iconify/vue'
import {
nextTick,
onBeforeUpdate,
onMounted,
onUnmounted,
provide,
reactive,
ref,
watch
} from 'vue'
import {
getSlideOffset,
slideInit,
slideReset,
slideTouchEnd,
slideTouchMove,
slideTouchStart
} from '@/utils/slide'
import { SlideAlbumOperationStatus, SlideItemPlayStatus, SlideType } from '../../utils/const_var'
import { cloneDeep } from '@/utils'
import bus, { EVENT_KEY } 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()
// provide('isPlaying', computed(() => this.isPlaying))
provide('isPlaying', false)
const props = defineProps({
item: {
type: Object,
default() {
return {
type: 'imgs',
imgs: [
'https://cdn.seovx.com/?mom=302',
'https://cdn.seovx.com/?mom=302',
'https://cdn.seovx.com/?mom=302',
'https://cdn.seovx.com/?mom=302',
'https://cdn.seovx.com/?mom=302',
'https://cdn.seovx.com/?mom=302',
'https://cdn.seovx.com/?mom=302',
'https://cdn.seovx.com/?mom=302'
],
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
}
}
}
},
position: {
type: Object,
default: () => {
return {
uniqueId: '',
index: ''
}
}
}
})
const slideListEl = ref(null)
//用于解决touch事件触发startPlay,然后click事件又触发stopLoop的问题
let lockDatetime = 0
const state = reactive({
type: SlideType.HORIZONTAL,
judgeValue: 20,
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: [],
cycleFn: -1,
status: SlideItemPlayStatus.Play,
isAutoPlay: true,
localItem: props.item
})
function stopPlay() {
state.status = SlideItemPlayStatus.Pause
stopLoop()
}
function startPlay() {
state.isAutoPlay = true
state.status = SlideItemPlayStatus.Play
startLoop()
}
function stopLoop() {
clearInterval(state.cycleFn)
state.cycleFn = -1
}
function startLoop() {
if (state.cycleFn !== -1) return
if (!state.isAutoPlay) return
state.cycleFn = setInterval(() => {
state.status = SlideItemPlayStatus.Play
if (state.localIndex < props.item.imgs.length - 1) {
state.localIndex++
} else {
state.localIndex = 0
}
}, 2000)
}
onMounted(async () => {
await nextTick()
slideInit(slideListEl.value, state, SlideType.HORIZONTAL)
startPlay()
// setTimeout(() => {
// state.operationStatus = SlideAlbumOperationStatus.Zooming
// }, 1000)
bus.on(EVENT_KEY.SINGLE_CLICK_BROADCAST, click)
})
onUnmounted(() => {
bus.off(EVENT_KEY.SINGLE_CLICK_BROADCAST, click)
})
function click({ uniqueId, index, type }) {
// console.log('position,', type, Date.now() - lockDatetime)
if (props.position.uniqueId === uniqueId && props.position.index === index) {
// if (type === EVENT_KEY.ITEM_TOGGLE) {
// if (state.status === SlideItemPlayStatus.Play) {
// stopLoop()
// } else {
// state.isAutoPlay = true
// startLoop()
// }
// }
if (type === EVENT_KEY.ITEM_STOP) {
stopPlay()
setTimeout(() => {
state.localIndex = 0
state.status = SlideItemPlayStatus.Play
state.operationStatus = SlideAlbumOperationStatus.Normal
}, 500)
}
if (type === EVENT_KEY.ITEM_PLAY) {
state.localIndex = 0
startPlay()
}
}
}
// 确保在每次更新之前重置ref
onBeforeUpdate(() => {
state.itemRefs = []
state.previewImgs = []
})
watch(
() => state.localIndex,
() => {
GM.$setCss(slideListEl.value, 'transition-duration', `300ms`)
GM.$setCss(
slideListEl.value,
'transform',
`translate3d(${getSlideOffset(state, slideListEl.value)}px, 0, 0)`
)
}
)
watch(
() => state.operationStatus,
(newVal) => {
if (newVal !== SlideAlbumOperationStatus.Normal) {
bus.emit(EVENT_KEY.ENTER_FULLSCREEN)
} else {
bus.emit(EVENT_KEY.EXIT_FULLSCREEN)
}
}
)
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) {
lockDatetime = Date.now()
// Utils.$showNoticeDialog('start'+e.touches.length)
console.log('start', e.touches.length)
if (e.touches.length === 1) {
slideTouchStart(e, slideListEl.value, state)
} else {
if (state.operationStatus === SlideAlbumOperationStatus.Zooming) {
// state.start.center = Utils.getCenter(state.start.point1, state.start.point2)
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) {
const s = true
if (s) return
// Utils.$showNoticeDialog('move'+e.touches.length)
// console.log('move', e.touches.length, state.operationStatus)
let current1 = { x: e.touches[0].pageX, y: e.touches[0].pageY }
stopLoop()
//单手移动
if (e.touches.length === 1) {
if (state.operationStatus === SlideAlbumOperationStatus.Zooming) {
// console.log('m1')
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 {
// console.log('m2')
state.isAutoPlay = false
slideTouchMove(
e,
slideListEl.value,
state,
canNext,
() => {
//TODO 这里需要这个事件吗
if (state.operationStatus !== SlideAlbumOperationStatus.Normal) {
Utils.$stopPropagation(e)
}
// console.log('move-notNextcb')
// state.operationStatus = SlideAlbumOperationStatus.Normal
},
() => {
if (state.operationStatus !== SlideAlbumOperationStatus.Normal) {
Utils.$stopPropagation(e)
}
}
)
}
} else {
// console.log('m3')
state.operationStatus = SlideAlbumOperationStatus.Zooming
Utils.$stopPropagation(e)
let rect = { x: 0, y: 0 }
if (rectMap.has(state.localIndex)) {
rect = rectMap.get(state.localIndex)
} else {
//TODO 这里去掉jquery
//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 movementRatio = currentRatio - ov[0]
// console.log('movementRatio',movementRatio)
//如果本次比例和上次的不超过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)
} else {
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)
}
state.itemRefs[state.localIndex].style.transform = `matrix3d(${ov.toString()})`
state.last.point1 = current1
state.last.point2 = current2
}
}
function touchEnd(e) {
// console.log('Date.now() - lockDatetime', Date.now() - lockDatetime,)
if (
Date.now() - lockDatetime < 300 &&
state.move.x === 0 &&
state.move.y === 0 &&
state.operationStatus !== SlideAlbumOperationStatus.Zooming
) {
if (state.status === SlideItemPlayStatus.Play) {
stopPlay()
} else {
startPlay()
}
return
}
state.isPreview = false
//这里如果是双指触控的话会触发两次事件第一次touches长度为1第二次为0
//如果是单指触控的话触发一次事件touches长度为0
console.log('end', e.touches.length, state.operationStatus)
// e.touches.length === 1 说明,松开了第一只手指
if (e.touches.length === 1) {
//双指缩放状态下,但只松开了一只手
if (state.operationStatus === SlideAlbumOperationStatus.Zooming) {
Utils.$stopPropagation(e)
state.last.point1 = { x: e.touches[0].pageX, y: e.touches[0].pageY }
startLoop()
}
} else {
if (state.operationStatus === SlideAlbumOperationStatus.Zooming) {
Utils.$stopPropagation(e)
ov = origin
state.itemRefs[state.localIndex].style['transition-duration'] = '300ms'
state.itemRefs[state.localIndex].style.transform = `matrix3d(${origin.toString()})`
startLoop()
state.operationStatus = SlideAlbumOperationStatus.Look
} else {
slideTouchEnd(
e,
state,
canNext,
() => {
console.log('nextCb')
},
() => {
if (state.operationStatus !== SlideAlbumOperationStatus.Normal) {
state.operationStatus = SlideAlbumOperationStatus.Normal
}
console.log('doNotNextCb')
startLoop()
}
)
slideReset(e, slideListEl.value, state)
}
}
}
function getWidth(index) {
if (state.localIndex >= index) return { width: '100%' }
}
function setItemRef(el, key) {
el && state[key].push(el)
}
function canNext(state, isNext) {
let res = !(
(state.localIndex === 0 && !isNext) ||
(state.localIndex === props.item.imgs.length - 1 && isNext)
)
return res
}
</script>
<style scoped lang="less">
#SlideAlbum {
transition: height 0.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: 10rem;
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: #75757580;
position: relative;
overflow: hidden;
.progress {
border-radius: 10rem;
position: absolute;
left: 0;
height: @h;
background: #e5e5e587;
}
}
}
}
</style>
<style lang="less">
.preview {
transition: opacity 0.3s;
position: fixed;
bottom: 0;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
.preview-wrapper {
img {
transition: all 0.3s;
margin: 0 5rem;
width: 30rem;
height: 45rem;
background-color: black;
border-radius: 3rem;
overflow: hidden;
object-fit: cover;
&.preview-img {
margin: 0 15rem;
width: 40rem;
}
}
}
.indicator {
background: var(--footer-color);
width: 100%;
height: var(--footer-height);
color: gray;
display: flex;
align-items: center;
justify-content: center;
.index {
color: white;
}
}
}
.album-toolbar {
position: absolute;
bottom: 0;
color: white;
font-size: 24rem;
background: var(--footer-color);
width: 100%;
box-sizing: border-box;
height: var(--footer-height);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10rem;
@padding: 18rem;
.left {
height: 40rem;
background-color: rgba(71, 71, 86, 0.53);
border-radius: 10rem;
padding: 0 @padding;
display: flex;
align-items: center;
justify-content: center;
}
.right {
.left;
display: flex;
align-items: center;
gap: 20rem;
}
}
</style>