Files
91fans_h5/src/views/acgModule/detail-image/watch.vue
T
2026-09-15 15:25:05 +07:00

762 lines
19 KiB
Vue

<template>
<div
v-if="state.info.id"
ref="dvCaricatureWrap"
class="caricature-wrap overflow-y-auto"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@scroll="onContentScroll"
>
<Transition name="top">
<div v-show="visibleHead" class="nav" @click.stop="visibleHead = false">
<SvgIcon name="back_white" class="back" @click.stop="$router.go(-1)" />
<span class="title">{{ currentItem.name }}</span>
<div class="right flex-vcenter">
<div
class="collect"
:class="{ collected: historyData.mediaStatus?.hasCollected }"
@click.stop="onToogleCollected"
>
<SvgIcon name="acg-collect-img-0" class="svg-icon" v-if="!historyData.mediaStatus?.hasCollected" />
<SvgIcon name="acg-collect-img-1" class="svg-icon" style="color: #f52c56" v-else />
</div>
<SvgIcon class="share" name="acg-share1" @click.stop="openShare" />
</div>
<!-- <SvgIcon class="back right" name="acg_catalogue" @click.stop="visibleCatalogue = true" /> -->
</div>
</Transition>
<div class="like-box" :class="isToolShow ? 'top150' : ''" v-show="visibleHead" @click.stop="onToogleLiked">
<SvgIcon v-if="historyData?.mediaStatus?.hasLiked" name="acg-heart-1" />
<SvgIcon v-else name="acg-heart-0" />
喜欢
</div>
<div class="list" @click="onClick">
<ImgObserver
v-for="(url, index) in currentEpisode"
class="item"
:style="getImgHeight(url)"
:class="{ move: visibleCatalogue }"
:key="url"
:index="index"
:url="url"
:init="index - currentProgress <= 1"
@updateIndex="updateIndex"
@updateSize="updateSize"
></ImgObserver>
</div>
<div
v-show="running"
class="running-box"
@click.stop="
{
running = false;
visibleHead = true;
}
"
>
<img class="running" src="@/assets/gif/running.gif" />
</div>
<Transition name="bottom">
<div v-show="visibleHead && !running" ref="bottomRef" class="action">
<div class="box">
<div class="func" @click.stop="visibleCatalogue = true">
<SvgIcon class="icon" name="acg_catalogue" />
目录
</div>
<div class="func" @click.stop="visibleComment = true">
<SvgIcon class="icon" name="acg_comment" />
评论
</div>
<div class="func" @click.stop="isToolShow = !isToolShow">
<SvgIcon class="icon" :name="isToolShow ? 'ddown' : 'acg_progress'" />
进度
</div>
<div class="func" @click.stop="running = true">
<SvgIcon class="icon" name="acg_automatic" />
自动
</div>
<!-- <div class="prev" @click.stop="switchEpisodeNumber(-1)">
<SvgIcon class="icon" name="acg_prev" />
上一话
</div>
<div class="next" @click.stop="switchEpisodeNumber(1)">
<SvgIcon class="icon" name="acg_prev" />
下一话
</div> -->
</div>
<Transition name="van-slide-up">
<div v-show="isToolShow && !running" class="tools">
<div class="prev" @click.stop="switchEpisodeNumber(-1)">
<SvgIcon class="icon" name="backWhite" />
<span>上一篇</span>
</div>
<div class="slider">
<van-slider
style="--van-slider-button-background: #ffd300"
:model-value="progressRatio"
@update:model-value="(v) => (progressRatio = v)"
active-color="#FFD300"
inactive-color="#fff3"
button-size="12px"
bar-height="4px"
@change="reposition"
></van-slider>
</div>
<div class="next" @click.stop="switchEpisodeNumber(1)">
<SvgIcon class="icon" name="backWhite" />
<span>下一篇</span>
</div>
</div>
</Transition>
</div>
</Transition>
<!-- <div v-show="!visibleHead" class="tips">
第{{ state.episodeNumber }}话 {{ currentProgress }}/{{ currentEpisode.length }}
</div> -->
</div>
<Loading v-else />
<van-popup v-model:show="visibleCatalogue" position="left" :style="{ width: '50%', height: '100%' }">
<div class="play-list-box">
<div class="header">
<!-- <SvgIcon class="close" name="popup_close" @click.stop="visibleCatalogue = false" /> -->
<div class="header-title ellipsis">{{ historyData.title }}</div>
<div class="sort flex-center-between">
<span class="menu-info">
共{{ historyData.totalEpisode }}话 {{ getUpdateStatusText(historyData.updateStatus) }}
</span>
<span class="btn" @click="sort = !sort">
<SvgIcon v-show="sort" class="section" name="acg_sort_0" />
<SvgIcon v-show="!sort" class="section" name="acg_sort_1" />
<span>{{ sort ? '正序' : '倒序' }}</span>
</span>
</div>
</div>
<div class="list overflow-y-auto">
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh" finishedText="">
<div
:id="`section_${item.episodeNumber}`"
class="item"
:class="{ active: item.episodeNumber === state.episodeNumber }"
v-for="item in autoList"
:key="item.id"
@click="switchEpisodeNumberCatalogue(item)"
>
{{ item.episodeNumber }}.{{ item.name }}
</div>
</PullRefresh>
</div>
</div>
</van-popup>
<PayForWatch
v-if="state.info.id"
ref="dvPayForWatch"
:isOpen="visiblePayForWatch"
:info="state.validateItem.id ? state.validateItem : state.info"
@close="closePayForWatch"
@buySuccess="beforeBuySuccess"
></PayForWatch>
<AppSharePopup
:is-open="visibleShare"
coverType="portrait"
@close="visibleShare = false"
:cover="historyData.verticalCover || state.info.cover"
/>
<CommentBox
v-if="state.info.id"
ref="dvComment"
class="comment"
:isOpen="visibleComment"
objType="cartoon"
@close="visibleComment = false"
:showCollected="false"
></CommentBox>
</template>
<script lang="ts">
export default {
name: 'AcgImageDetailWatch',
};
</script>
<script lang="ts" setup>
import { ref, watch, computed, onDeactivated, onUnmounted, onBeforeUnmount, provide, reactive, toRaw } from 'vue';
import PayForWatch from '../components/pay/PayForWatch.vue';
import mediaContentInfo from '../utils/mediaContentInfo';
import ImgObserver from '../components/ImgObserver.vue';
import { HistoryVideoType } from '@/enums/app';
import { showToast } from 'vant';
import { Pay_emit } from '../utils/pay-mitt';
import type { PayInfo } from '../utils/pay-mitt';
import { handleCollected, Collected_emit } from '../utils/collect';
import CommunityApi from '@/service/community';
import { getUpdateStatusText } from '../utils/common';
import { debounce } from '@/utils';
// import { useGlobalStore } from '@/stores/global';
// const globalStore = useGlobalStore();
const running = ref(false);
const isToolShow = ref(false);
const visibleHead = ref(true);
const progressRatio = ref(0);
const contentMaxScrollTop = ref(0);
const contentScrollTop = ref(0);
const visibleSides = ref(true);
const bottomRef = ref();
const {
state,
currentItem,
visibleCatalogue,
dvPayForWatch,
visiblePayForWatch,
closePayForWatch,
buySuccess,
switchEpisodeNumber,
autoList,
moreParams,
onLoad,
onRefresh,
switchEpisodeNumberCatalogue,
historyData,
addHistoryReadData,
sort,
} = mediaContentInfo({ isGetHistoryInit: true, dbType: HistoryVideoType.AcgImage });
provide('historyData', historyData);
const dvCaricatureWrap = ref();
const currentProgress = ref(1);
const currentEpisode = computed(() => {
return state.info.urlSet || [];
});
const reposition = () => {
if (dvCaricatureWrap.value) {
dvCaricatureWrap.value.scrollTop = (contentMaxScrollTop.value * progressRatio.value) / 100;
}
};
const currentHistoryReadData = computed(() => {
const arr = historyData.value.historyReadData || [];
return arr.find((item) => item.episodeNumber === state.episodeNumber) || { scrollTop: 0, imgs: {} };
});
watch(
dvCaricatureWrap,
() => {
dvCaricatureWrap.value.scrollTop = currentHistoryReadData.value.scrollTop || 0;
},
{
once: true,
},
);
const onContentScroll = (e: Event) => {
const target = e.target as HTMLElement;
contentScrollTop.value = target.scrollTop;
contentMaxScrollTop.value = target.scrollHeight - target.offsetHeight;
if (contentMaxScrollTop.value > 0) {
progressRatio.value = (contentScrollTop.value / contentMaxScrollTop.value) * 100;
}
const progress =
contentMaxScrollTop.value <= 0 ? 100 : Math.max(1, Math.min(100, Math.ceil((contentScrollTop.value / contentMaxScrollTop.value) * 100)));
addHistoryReadData(state.episodeNumber, {
progress,
scrollTop: contentScrollTop.value,
});
const isEnd = contentScrollTop.value + 10 >= contentMaxScrollTop.value;
if (isEnd) {
console.log('阅读结束')
running.value = false;
visibleSides.value = true;
bottomRef.value?.showTool();
return;
}
if (!running.value) {
if (contentScrollTop.value < 100) {
visibleSides.value = true;
}
// visibleSides.value = contentScrollTop.value < 100
}
};
const getImgHeight = (url: string) => {
const imgs = (currentHistoryReadData.value.imgs || {})[url];
const { width = 0, height = 0 } = imgs || {};
if (width && height) {
return {
height: (window.innerWidth / width) * height + 'px',
};
}
return {};
};
const updateIndex = ({ index, url }: { index: number; url: string }) => {
let progress = 0;
if (dvCaricatureWrap.value.scrollTop === 0) {
progress = 1;
} else {
progress = Number(index) + 1;
}
currentProgress.value = progress;
addHistoryReadData(state.episodeNumber, {
progress: Math.ceil((progress / currentEpisode.value.length) * 100),
progressUrl: url,
scrollTop: dvCaricatureWrap.value.scrollTop,
});
};
const updateSize = (imgObj: any) => {
addHistoryReadData(state.episodeNumber, {}, imgObj);
};
watch(
() => state.episodeNumber,
(num) => {
addHistoryReadData(num, { progress: 1 });
if (dvCaricatureWrap.value) {
dvCaricatureWrap.value.scrollTop = 0;
}
visibleHead.value = true;
},
);
addHistoryReadData(state.episodeNumber + 1, {});
onBeforeUnmount(() => {
addHistoryReadData(state.episodeNumber, {
scrollTop: dvCaricatureWrap.value.scrollTop,
});
addHistoryReadData(state.episodeNumber + 1, {});
});
// 点赞
const onToogleLiked = () => {
const zan = !historyData.value.mediaStatus.hasLiked;
(zan ? CommunityApi.zan : CommunityApi.cancleZan)(
zan
? {
type: 'image',
objID: historyData.value.id,
}
: {
type: 'image',
objIDs: [historyData.value.id],
},
).then(() => {
historyData.value.mediaStatus.hasLiked = zan;
});
};
const onClick = (e: MouseEvent) => {
if (running.value) {
visibleHead.value = !visibleHead.value;
return;
}
const { clientY } = e;
let scrollTop = 0;
const OneThird = window.innerHeight / 3.5;
if (clientY < OneThird) {
scrollTop = -300;
} else if (clientY > OneThird * 2) {
scrollTop = 300;
} else {
visibleHead.value = !visibleHead.value;
return;
}
if (scrollTop !== 0) {
visibleHead.value = false;
dvCaricatureWrap.value.scrollTo({ top: scrollTop + dvCaricatureWrap.value.scrollTop, behavior: 'smooth' });
}
};
let lastScrollTop = 0;
const onTouchStart = (e: TouchEvent) => {
const { clientY } = e.targetTouches[0];
lastScrollTop = clientY;
};
const onTouchMove = (e: TouchEvent) => {
const { clientY } = e.targetTouches[0];
const distance = lastScrollTop - clientY;
if (distance > 30) {
visibleHead.value = false;
return;
}
if (distance < -30) {
visibleHead.value = true;
return;
}
};
// watch(visibleCatalogue, (bool) => {
// if (bool) {
// visibleHead.value = false;
// }
// });
const scrollSpeed = 1;
let moveInterval = null as ReturnType<typeof setInterval> | null;
const cancelAutoMove = () => {
moveInterval && clearInterval(moveInterval);
moveInterval = null;
running.value = false;
};
const autoMove = () => {
const { scrollTop, scrollHeight } = dvCaricatureWrap.value;
if (scrollTop >= scrollHeight) {
// 停止自动移动
cancelAutoMove();
return;
}
let newPosition = scrollTop + scrollSpeed;
dvCaricatureWrap.value.scrollTo(0, newPosition);
};
onDeactivated(() => {
cancelAutoMove();
});
onUnmounted(() => {
cancelAutoMove();
});
watch(running, (bool) => {
if (bool) {
moveInterval = setInterval(autoMove, 10);
showToast({
message: '自动阅读模式',
onClose: () => {
if (moveInterval) {
visibleHead.value = false;
}
},
});
} else {
cancelAutoMove();
}
});
const beforeBuySuccess = (obj: PayInfo) => {
Pay_emit(historyData.value.id, obj);
buySuccess(obj.type);
};
// 收藏
const onToogleCollected = async () => {
try {
const {
id,
mediaStatus: { hasCollected },
} = historyData.value;
const tempIsCollect = !hasCollected;
handleCollected(id, tempIsCollect).then(() => {
historyData.value.mediaStatus.hasCollected = tempIsCollect;
Collected_emit(id, tempIsCollect);
});
} catch (e) {
console.log(e);
}
};
const visibleShare = ref(false);
const openShare = () => {
const { id, verticalCover, horizontalCover } = historyData.value;
let obj = {
id,
cover: verticalCover || horizontalCover,
};
// globalStore.setShareInfo(obj);
visibleShare.value = true;
};
const visibleComment = ref(false);
const dvComment = ref();
watch(dvComment, (el) => {
if (!el) return;
if (!state.info.id || !dvComment.value?.state) return;
console.log(state, 'state.info', state.validateItem);
dvComment.value.state.videoInfo = toRaw({
...state.info,
id: state.id,
});
dvComment.value.getCommentList();
});
</script>
<style lang="scss" scoped>
@import '@/assets/styles/mixins.scss';
.caricature-wrap {
width: 100%;
height: 100%;
// @include SafeArea('padding', 'bottom', 60px);
.like-box {
position: fixed;
bottom: 81px;
right: 0;
z-index: 100;
border-radius: 999px 0px 0px 999px;
background: rgba(0, 0, 0, 0.5);
width: 86px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #fff;
svg {
margin-right: 5px;
width: 24px;
height: 24px;
}
}
.top150 {
bottom: 150px !important;
}
.nav {
position: fixed;
left: 0;
top: 0;
z-index: 2;
width: 100%;
height: 44px;
background: #181824;
display: flex;
align-items: center;
padding: 0 6px;
font-size: 16px;
font-weight: 500;
color: #fff;
.back {
flex-shrink: 0;
width: 24px;
height: 24px;
flex-shrink: 0;
color: #fff;
margin-right: 4px;
}
.title {
flex: 1;
text-align: left;
}
.right {
flex-shrink: 0;
margin-left: 4px;
.collect {
svg {
width: 24px;
height: 24px;
}
}
.share {
width: 19px;
height: 19px;
margin: 0 16px 0 12px;
}
}
}
> .list {
padding-top: 1px;
.item {
width: 100%;
min-height: 200px;
height: auto;
:deep(.animationImg) {
overflow: visible;
.loading {
height: 200px;
}
}
}
}
.action {
position: fixed;
box-sizing: content-box;
@include SafeArea('padding', 'bottom', 0);
// left: 88px;
// right: 88px;
height: 48px;
background: #000;
width: 100%;
left: 0;
bottom: 0;
transition: all 0.3s;
.box {
z-index: 2;
border-radius: 100px;
padding: 12px 16px;
// background: rgba(0, 0, 0, 0.60)
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
> div {
display: flex;
flex-direction: column;
align-items: center;
color: rgba(255, 255, 255, 0.45);
font-size: 12px;
.icon {
width: 24px;
height: 24px;
color: rgba(255, 255, 255, 0.9);
}
&.next .icon {
transform: rotate(180deg);
}
&.disabled {
color: rgba(0, 0, 0, 0.4);
}
}
}
}
// .tips {
// position: fixed;
// @include SafeAreaFixed('bottom', 40px);
// right: 20px;
// border-radius: 8px;
// background: rgba(0, 0, 0, 0.6);
// padding: 8px 12px;
// color: #f1f1f1;
// font-size: 14px;
// font-weight: 500;
// }
}
.play-list-box {
position: relative;
width: 100%;
height: 100%;
background: #0f0f0f;
display: flex;
flex-direction: column;
color: rgba(255, 255, 255, 0.9);
.header {
padding-top: 85px;
width: 100%;
.close {
width: 14px;
height: 14px;
}
.header-title {
width: 100%;
text-indent: 16px;
font-size: 18px;
font-weight: 900;
font-family: Roboto;
}
.sort {
flex-shrink: 0;
margin: 16px 0 14px;
padding: 8px 16px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
font-weight: 500;
color: #141414;
background: #f68804;
.btn {
display: flex;
align-items: center;
.section {
width: 20px;
height: 20px;
margin-right: 4px;
}
}
.menu-info {
font-size: 14px;
font-weight: 500;
}
}
}
.list {
flex: 1;
.item {
width: 100%;
height: 19px;
line-height: 19px;
padding: 0 16px;
margin-bottom: 11px;
color: rgba(255, 255, 255, 0.55);
font-size: 16px;
font-weight: 400;
@include textEllipsis();
&.active {
color: #f68804;
}
}
}
}
.running-box {
background-color: #fff;
padding: 10px;
border-radius: 50%;
position: fixed;
bottom: 50px;
left: 50%;
transform: translateX(-50%);
z-index: 100;
.running {
width: 36px;
height: 36px;
}
}
.tools {
position: absolute;
top: -70px;
left: 0;
width: 100%;
height: 70px;
padding: 0 16px;
color: #fff;
background-color: #000;
display: flex;
justify-content: space-between;
.prev,
.next {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 12px;
line-height: 18px;
.icon {
width: 24px;
height: 24px;
margin-bottom: 4px;
}
}
.next .icon {
transform: rotate(180deg);
}
.slider {
width: 245px;
align-self: center;
}
}
.top-enter-active,
.top-leave-active,
.bottom-enter-active,
.bottom-leave-active {
will-change: transform;
transition: transform 0.4s ease;
}
.top-enter-from,
.top-leave-to {
transform: translateY(-100%);
}
.bottom-enter-from,
.bottom-leave-to {
transform: translateY(200%);
}
</style>