初始化
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
.wrap {
|
||||
width: 100vw;
|
||||
padding: 12px 16px;
|
||||
// background: #262626;
|
||||
// border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
@mixin categoryWrap() {
|
||||
// margin-top: 18px;
|
||||
// padding-bottom: 18px;
|
||||
|
||||
// border-bottom: $borderBottom;
|
||||
}
|
||||
|
||||
@mixin categoryList() {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
align-items: center;
|
||||
}
|
||||
@mixin categoryTitle() {
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
|
||||
.more {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
// color: #fff;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin vanTab() {
|
||||
--van-tabs-line-height: 24px;
|
||||
--van-tabs-nav-background: transparent;
|
||||
--van-tabs-bottom-bar-width: 16px;
|
||||
--van-tabs-bottom-bar-height: 2px;
|
||||
|
||||
--van-tab-font-size: 14px;
|
||||
|
||||
--van-tab-text-color: rgba(255, 255, 255, 0.6);
|
||||
--van-tab-active-text-color: #fff;
|
||||
--van-tabs-bottom-bar-color: #6954e7;
|
||||
|
||||
.tabs {
|
||||
:deep(.van-tabs__wrap) {
|
||||
padding: 10px 0;
|
||||
box-sizing: content-box;
|
||||
.van-tab__text--ellipsis {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.van-tabs__nav {
|
||||
.van-tab {
|
||||
width: auto;
|
||||
padding: 0 16px;
|
||||
margin: 0;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
flex-basis: 36px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
.van-tabs__line {
|
||||
bottom: 12px;
|
||||
// width: 20px;
|
||||
// height: 2px;
|
||||
// @if $theme == 'red' {
|
||||
// background: url('@/assets/images/png/tab-line-red.svg') no-repeat;
|
||||
// } @else {
|
||||
// background: url('@/assets/images/png/tab-line-black.svg') no-repeat;
|
||||
// }
|
||||
// background-size: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
.list {
|
||||
margin: 0;
|
||||
border-bottom: 0;
|
||||
height: calc(100vh - 44px - 10px - 24px);
|
||||
height: calc(100vh - 44px - 10px - 24px - constant(safe-area-inset-bottom));
|
||||
height: calc(100vh - 44px - 10px - 24px - env(safe-area-inset-bottom));
|
||||
overflow-y: auto;
|
||||
}
|
||||
:deep(.van-tabs__content) {
|
||||
.van-tab__panel {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin vanTab-btn() {
|
||||
--van-tab-text-color: #acbabf;
|
||||
--van-tab-active-text-color: $fff;
|
||||
--van-tabs-line-height: 24px;
|
||||
--van-tabs-nav-background: transparent;
|
||||
--van-tabs-bottom-bar-width: 14px;
|
||||
--van-tabs-bottom-bar-height: 2px;
|
||||
--van-tabs-bottom-bar-color: $themeBgLgColor;
|
||||
|
||||
--van-tab-font-size: 14px;
|
||||
|
||||
.tabs {
|
||||
:deep(.van-tabs__wrap) {
|
||||
padding: 16px 0 12px;
|
||||
height: auto;
|
||||
box-sizing: content-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
.van-tabs__nav {
|
||||
border-radius: 24px;
|
||||
background: #20252f;
|
||||
padding: 4px;
|
||||
|
||||
.van-tab {
|
||||
padding: 5px 32px;
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
box-sizing: content-box;
|
||||
border-radius: 15px;
|
||||
|
||||
&.van-tab--active {
|
||||
background: $themeBgLgColor;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.van-tabs__line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// .list {
|
||||
// margin: 0;
|
||||
// border-bottom: 0;
|
||||
// height: calc(100vh - 44px - 10px - 24px);
|
||||
// height: calc(100vh - 44px - 10px - 24px - constant(safe-area-inset-bottom));
|
||||
// height: calc(100vh - 44px - 10px - 24px - env(safe-area-inset-bottom));
|
||||
// overflow-y: auto;
|
||||
// }
|
||||
:deep(.van-tabs__content) {
|
||||
.van-tab__panel {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin vanTab-list() {
|
||||
:deep(.van-tabs) {
|
||||
--van-tabs-nav-background: transform;
|
||||
--van-tab-active-text-color: #6954e7;
|
||||
--van-tab-text-color: rgba(255, 255, 255, 0.6);
|
||||
--van-tabs-bottom-bar-height: 0;
|
||||
.van-tabs__nav--line {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.van-tab {
|
||||
flex: 0;
|
||||
flex-basis: 40px;
|
||||
justify-content: flex-start;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.van-tabs__line {
|
||||
display: none;
|
||||
}
|
||||
.van-tab__panel {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin collected-btn() {
|
||||
display: inline-flex;
|
||||
padding: 4px 12px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 90px;
|
||||
background: $themeBgColor;
|
||||
// box-shadow: 0px 2px 10px 0px rgba(255, 66, 68, 0.8);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
// line-height: 16px; /* 133.333% */
|
||||
|
||||
&.collected {
|
||||
background: #222433;
|
||||
// box-shadow: none;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
svg {
|
||||
// width: 14px;
|
||||
// height: 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin tag() {
|
||||
.permission {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: inline-flex;
|
||||
padding: 2px 6px;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
border-radius: 8px 0px 8px 8px;
|
||||
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px; /* 150% */
|
||||
|
||||
&.permission1 {
|
||||
background: linear-gradient(135deg, #b378ff 0%, #482cf5 100%);
|
||||
}
|
||||
&.permission2 {
|
||||
color: #212121;
|
||||
background: linear-gradient(135deg, #adfcac 0%, #5cef97 100%);
|
||||
}
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div ref="dvLazyImg" class="acg-image-container">
|
||||
<transition name="bounceFade">
|
||||
<div v-if="isLoading && decyImg" class="box">
|
||||
<!-- <img v-show="isLoad" class="left" :src="decyImg" @load="load" />
|
||||
<img v-show="isLoad" class="right" :src="decyImg" /> -->
|
||||
<img v-show="isLoad" :src="decyImg" @load="load" />
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="fade">
|
||||
<div v-if="!isLoad" class="loading"></div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ImgObserver',
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import AppApi from '@/service/app';
|
||||
const emit = defineEmits(['updateIndex', 'updateSize']);
|
||||
const isLoading = ref(false);
|
||||
const props = defineProps<{
|
||||
url: string;
|
||||
index: number;
|
||||
init: boolean;
|
||||
}>();
|
||||
watch(
|
||||
() => props.init,
|
||||
(bool: boolean) => {
|
||||
if (bool) {
|
||||
isLoading.value = true;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const dvLazyImg = ref();
|
||||
const observer = new IntersectionObserver(
|
||||
function (entries) {
|
||||
const entry = entries[0];
|
||||
const { intersectionRatio, isIntersecting } = entry;
|
||||
if (intersectionRatio > 0 || isIntersecting) {
|
||||
isLoading.value = true;
|
||||
}
|
||||
if (intersectionRatio >= 0.5) {
|
||||
emit('updateIndex', { index: props.index, url: props.url });
|
||||
}
|
||||
},
|
||||
{ threshold: 0.8 },
|
||||
);
|
||||
onMounted(() => {
|
||||
observer.observe(dvLazyImg.value);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
observer.unobserve(dvLazyImg.value);
|
||||
});
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const decyImg = ref('');
|
||||
function imgDecypt() {
|
||||
try {
|
||||
let imgSrc = '';
|
||||
const isHttp = new RegExp('http').test(props.url);
|
||||
isHttp == true ? (imgSrc = props.url) : (imgSrc = configStore.sourceInfo.IMAGE + '/' + props.url);
|
||||
AppApi.imgDecypt(imgSrc)
|
||||
.then((url: any) => {
|
||||
decyImg.value = url;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
watch(isLoading, (bool) => {
|
||||
if (bool) {
|
||||
imgDecypt();
|
||||
}
|
||||
});
|
||||
|
||||
const isLoad = ref(false);
|
||||
const load = (e: Event) => {
|
||||
const el = e.target as HTMLImageElement;
|
||||
isLoad.value = true;
|
||||
emit('updateSize', { url: props.url, height: el.height, width: el.width });
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 1s;
|
||||
}
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.bounceFade-enter-active {
|
||||
animation: bounceFade-in 0.4s;
|
||||
}
|
||||
.bounceFade-leave-active {
|
||||
animation: bounceFade-in 0.4s;
|
||||
}
|
||||
|
||||
@keyframes bounceFade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.acg-image-container {
|
||||
.loading {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('@/assets/images/webp/placeholder.webp') no-repeat;
|
||||
background-size: cover;
|
||||
background-position: 50% 50%;
|
||||
background-color: rgb(52, 52, 52);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
object-fit: cover !important;
|
||||
}
|
||||
}
|
||||
|
||||
.box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover !important;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.left {
|
||||
width: 100%;
|
||||
clip-path: inset(0 50% 0 0);
|
||||
}
|
||||
.right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
clip-path: inset(0 0 0 49%);
|
||||
}
|
||||
}
|
||||
|
||||
&.move {
|
||||
.left {
|
||||
transform: translateX(-76.5px);
|
||||
}
|
||||
.right {
|
||||
transform: translateX(76.5px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="(props.list || []).length"
|
||||
class="category-content"
|
||||
:class="[props.coverDirection, `wrap_${props.contentWrap}`, `showType_${props.showType}`]"
|
||||
>
|
||||
<div v-if="$props.id" class="title" @click="goTopicPage(props.id, props.contentType, props.title, props.showType)">
|
||||
<slot name="title"></slot>
|
||||
<span v-if="props.title">{{ props.title }}</span>
|
||||
<span class="more flex-vcenter">
|
||||
{{ props.navTitle || '更多' }}
|
||||
<SvgIcon class="icon" name="moreIcon" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="props.contentWrap" ref="categoryContentList" class="list"></div>
|
||||
<div v-else ref="categoryContentList" class="list" @touchstart.stop @touchmove.stop @touchend.stop></div>
|
||||
<Teleport v-if="categoryContentList" :to="categoryContentList">
|
||||
<CategoryContentItem
|
||||
v-for="(item, index) in props.list"
|
||||
:key="index"
|
||||
:id="item.id"
|
||||
:url="props.coverDirection === 'landscape' ? item.horizontalCover : item.verticalCover"
|
||||
:title="item.title"
|
||||
:contentType="item.mediaType"
|
||||
:info="item"
|
||||
>
|
||||
<template #delBtn>
|
||||
<div class="delBtn" v-if="show" @click.stop="handleDel(item.id)">
|
||||
<div class="selectActive"></div>
|
||||
</div>
|
||||
</template>
|
||||
</CategoryContentItem>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, inject } from 'vue';
|
||||
import { goTopicPage } from '@/views/acgModule/utils/common';
|
||||
import CategoryContentItem from './categoryContentItem.vue';
|
||||
const emit = defineEmits(['onDelCollect', 'del']);
|
||||
const categoryContentList = ref();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
contentWrap?: boolean; //换行显示,portrait三个一行,landscape两个一行
|
||||
contentType?: string; // 'video' | 'image';
|
||||
coverDirection?: 'landscape' | 'portrait'; //landscape横向,portrait纵向
|
||||
title?: string;
|
||||
navTitle?: string;
|
||||
list: Types.Media.AllMediaInfoItem[];
|
||||
id?: string;
|
||||
showType?: number;
|
||||
isDelCollect?: boolean;
|
||||
}>(),
|
||||
{
|
||||
contentWrap: false,
|
||||
contentType: 'video',
|
||||
coverDirection: 'portrait',
|
||||
title: '',
|
||||
navTitle: '更多',
|
||||
list: () => [],
|
||||
id: '',
|
||||
showType: 0,
|
||||
isDelCollect: false,
|
||||
},
|
||||
);
|
||||
const show: boolean | undefined = inject('isEdit', false);
|
||||
const handleDel = (id: string) => {
|
||||
emit('del', id);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '../assets/style.scss';
|
||||
.category-content {
|
||||
@include categoryWrap();
|
||||
|
||||
.title {
|
||||
@include categoryTitle();
|
||||
}
|
||||
.list {
|
||||
@include categoryList();
|
||||
}
|
||||
|
||||
&.landscape {
|
||||
.list {
|
||||
.item {
|
||||
width: 152px;
|
||||
height: 110px;
|
||||
}
|
||||
}
|
||||
&.showType_205 {
|
||||
.list {
|
||||
.item {
|
||||
width: 132px;
|
||||
height: 222px;
|
||||
margin-right: 7px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
//&:nth-child(2n) {
|
||||
// margin-right: 0px;
|
||||
//}
|
||||
//&:nth-child(3n) {
|
||||
// margin-right: 7px;
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.wrap_true {
|
||||
.list {
|
||||
overflow: hidden;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&.landscape {
|
||||
.list {
|
||||
.item {
|
||||
width: 168px;
|
||||
height: 141px;
|
||||
margin-right: 7px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
&:nth-child(2n) {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.portrait {
|
||||
.list {
|
||||
.item {
|
||||
width: 111px;
|
||||
height: 199px;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
&:nth-child(3n) {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 四宫格-横
|
||||
&.showType_102 {
|
||||
.list {
|
||||
.item {
|
||||
width: 168px;
|
||||
height: 263px;
|
||||
margin-bottom: 6px;
|
||||
margin-right: 0;
|
||||
|
||||
&:nth-child(2n) {
|
||||
margin-left: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 四宫格-竖
|
||||
&.showType_201 {
|
||||
.list {
|
||||
.item {
|
||||
width: 168px;
|
||||
height: 270px;
|
||||
margin-right: 7px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
&:nth-child(2n) {
|
||||
margin-right: 0px;
|
||||
}
|
||||
&:nth-child(3n) {
|
||||
margin-right: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.delBtn {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
.selectActive {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
background: rgba(245, 44, 86, 1);
|
||||
background-image: url('@/assets/images/png/del.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: 14px 14px;
|
||||
background-position: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.selectBorder {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid rgba(103, 103, 103, 1);
|
||||
background: #000;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="item" @click="goPage">
|
||||
<slot name="item"></slot>
|
||||
<lazyImg class="cover" :src="autoUrl" :key="autoUrl">
|
||||
<PermissionTag :info="props.info" :content-type="props.contentType" />
|
||||
<slot name="cover"></slot>
|
||||
<slot name="collectDel"></slot>
|
||||
<img
|
||||
v-if="props.info.mediaType === 'text' && props.info.mediaSubType === 1"
|
||||
class="textIcon"
|
||||
src="@/assets/images/png/acg/type_text.png"
|
||||
/>
|
||||
</lazyImg>
|
||||
<div class="title-box">
|
||||
<div class="title">{{ title }}</div>
|
||||
<slot></slot>
|
||||
<UpdateStatus :info="props.info"></UpdateStatus>
|
||||
</div>
|
||||
<slot name="delBtn"></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
// import { useRouter } from 'vue-router';
|
||||
// import { useGlobalStore } from '@/stores/global';
|
||||
// import MediaApi from '@/service/media';
|
||||
// import { showToast } from 'vant';
|
||||
import { goDetailPage } from '../utils/common';
|
||||
import type { MediaTypeKey } from '../utils/common';
|
||||
import PermissionTag from './permission-tag.vue';
|
||||
import UpdateStatus from './updateStatus.vue';
|
||||
// const router = useRouter();
|
||||
// const globalStore = useGlobalStore();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id: string;
|
||||
url: string;
|
||||
title: string;
|
||||
contentType?: string;
|
||||
info: Types.Media.AllMediaInfoItem;
|
||||
}>(),
|
||||
{
|
||||
id: '',
|
||||
url: '',
|
||||
title: '',
|
||||
contentType: 'video',
|
||||
info: {} as any,
|
||||
},
|
||||
);
|
||||
const goPage = () => {
|
||||
goDetailPage(props.info.mediaType as MediaTypeKey, props.id, props.info.mediaSubType);
|
||||
};
|
||||
|
||||
const autoUrl = computed(() => {
|
||||
return props.url || props.info.verticalCover || props.info.horizontalCover;
|
||||
});
|
||||
|
||||
// const hasCollected = ref(props.info.mediaStatus?.hasAddBookshelf || false);
|
||||
// const onFollow = () => {
|
||||
// const isCollect = !hasCollected.value;
|
||||
// (isCollect ? MediaApi.postBookshelfAdd : MediaApi.postBookshelfDel)({
|
||||
// mid: props.id,
|
||||
// }).then(() => {
|
||||
// hasCollected.value = isCollect;
|
||||
// showToast(isCollect ? '成功加入书架' : '成功移出书架');
|
||||
// });
|
||||
// };
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
.item {
|
||||
flex-shrink: 0;
|
||||
width: 132px;
|
||||
height: 234px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: calc(100% - 42px);
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.animationImg) {
|
||||
img {
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
.collect {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 29px;
|
||||
height: 29px;
|
||||
}
|
||||
.textIcon {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
.title-box {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
// height: 33px;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// .icon {
|
||||
// width: 18px;
|
||||
// height: 18px;
|
||||
// flex-shrink: 0;
|
||||
// margin-right: 2px;
|
||||
// margin-bottom: -2px;
|
||||
// &.video {
|
||||
// width: 12px;
|
||||
// height: 12px;
|
||||
// margin-bottom: 0px;
|
||||
// }
|
||||
// }
|
||||
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin: 4px 0 4px;
|
||||
line-height: 18px;
|
||||
@include textEllipsis();
|
||||
}
|
||||
.updateStatus {
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<van-swipe class="category" :autoplay="5000" :show-indicators="false">
|
||||
<van-swipe-item v-for="(item, index) in props.list" :key="index">
|
||||
<div
|
||||
:class="['item', props.mediaType, `item_${(index + 1) % 3}`, titleClass(item.name)]"
|
||||
@click="$router.push({ path: '/AcgTopicList', query: { kind: item.key, type: mediaType, title: item.name } })"
|
||||
>
|
||||
<!-- 标题大于四个字为纵向、其余为横向 -->
|
||||
<img
|
||||
v-if="titleClass(item.name) === 'landscape'"
|
||||
class="title-icon"
|
||||
src="@/assets/images/png/acg/tag-title-1.png"
|
||||
/>
|
||||
<img v-else class="title-icon portrait" src="@/assets/images/png/acg/tag-title-2.png" />
|
||||
<span class="title">{{ item.name }}</span>
|
||||
<!-- <img class="cover" src="@/assets/images/png/acg/tag-icon-2.png" /> -->
|
||||
</div>
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {} from 'vue';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
list: any[];
|
||||
mediaType: string;
|
||||
}>(),
|
||||
{
|
||||
list: () => [],
|
||||
mediaType: () => '',
|
||||
},
|
||||
);
|
||||
|
||||
//landscape横向,portrait纵向
|
||||
const titleClass = (title: string) => {
|
||||
if (title.length < 4) {
|
||||
return 'landscape';
|
||||
} else {
|
||||
return 'portrait';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
.category {
|
||||
margin: 0 -16px;
|
||||
padding: 18px 0;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
align-items: center;
|
||||
|
||||
// .item {
|
||||
// flex-shrink: 0;
|
||||
// width: 78px;
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// margin-left: 25px;
|
||||
// &:first-child {
|
||||
// margin-left: 30px;
|
||||
// }
|
||||
|
||||
// .icon {
|
||||
// width: 100%;
|
||||
// height: 66px;
|
||||
// }
|
||||
// .title {
|
||||
// text-align: center;
|
||||
// width: 100%;
|
||||
// color: #ccc;
|
||||
// font-size: 14px;
|
||||
// font-style: normal;
|
||||
// font-weight: 400;
|
||||
// line-height: normal;
|
||||
|
||||
// @include textEllipsis();
|
||||
// }
|
||||
// }
|
||||
.item {
|
||||
width: 100%;
|
||||
height: 158px;
|
||||
position: relative;
|
||||
&.image {
|
||||
&.item_1 {
|
||||
background:
|
||||
// url(@/assets/images/png/acg/tag-title-1.png) 6px 70px / 78px 6px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) -64px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-1.png) -90px 100% / 205px 110px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-icon-image-1.png) 125px 100% / 150px 158px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-0.png) 85px 100% / 215px 100px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) 313px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-2.png) 275px 100% / 205px 110px no-repeat;
|
||||
}
|
||||
|
||||
&.item_2 {
|
||||
background:
|
||||
// url(@/assets/images/png/acg/tag-title-2.png) 16px 54px / 6px 78px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) -64px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-2.png) -90px 100% / 205px 110px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-icon-image-2.png) 125px 100% / 150px 158px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-0.png) 85px 100% / 215px 100px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) 313px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-3.png) 275px 100% / 205px 110px no-repeat;
|
||||
}
|
||||
|
||||
&.item_0 {
|
||||
background:
|
||||
// url(@/assets/images/png/acg/tag-title-1.png) 6px 70px / 78px 6px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) -64px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-3.png) -90px 100% / 205px 110px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-icon-image-3.png) 125px 100% / 150px 158px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-0.png) 85px 100% / 215px 100px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) 313px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-1.png) 275px 100% / 205px 110px no-repeat;
|
||||
}
|
||||
}
|
||||
&.video {
|
||||
&.item_1 {
|
||||
background:
|
||||
// url(@/assets/images/png/acg/tag-title-1.png) 6px 70px / 78px 6px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) -64px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-video-1.png) -90px 100% / 205px 110px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-icon-video-1.png) 125px 100% / 142px 158px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-0.png) 85px 100% / 215px 100px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) 313px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-video-2.png) 275px 100% / 205px 110px no-repeat;
|
||||
}
|
||||
|
||||
&.item_2 {
|
||||
background:
|
||||
// url(@/assets/images/png/acg/tag-title-2.png) 16px 54px / 6px 78px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) -64px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-video-2.png) -90px 100% / 205px 110px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-icon-video-3.png) 115px 100% / 150px 158px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-0.png) 85px 100% / 215px 100px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) 313px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-video-3.png) 275px 100% / 205px 110px no-repeat;
|
||||
}
|
||||
|
||||
&.item_0 {
|
||||
background:
|
||||
// url(@/assets/images/png/acg/tag-title-1.png) 6px 70px / 78px 6px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) -64px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-video-3.png) -90px 100% / 205px 110px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-icon-video-2.png) 145px 100% / 104px 158px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-0.png) 85px 100% / 215px 100px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) 313px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-video-1.png) 275px 100% / 205px 110px no-repeat;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
line-height: normal;
|
||||
word-wrap: break-word;
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
left: 5px;
|
||||
width: 76px;
|
||||
}
|
||||
|
||||
.cover {
|
||||
position: absolute;
|
||||
width: 150px;
|
||||
height: 158px;
|
||||
left: 125px;
|
||||
bottom: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
height: 6px;
|
||||
width: 78px;
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
left: 6px;
|
||||
&.portrait {
|
||||
height: 78px;
|
||||
width: 6px;
|
||||
position: absolute;
|
||||
top: 54px;
|
||||
left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&.portrait {
|
||||
.title {
|
||||
top: 50px;
|
||||
left: 30px;
|
||||
width: 39px;
|
||||
}
|
||||
}
|
||||
|
||||
// url(/src/assets/images/png/acg/bg-bobo-right.png) 100% 50px / 42px no-repeat,
|
||||
// url(/src/assets/images/png/acg/bg-bobo-right.png) 100% 250px / 182px no-repeat;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div class="item" @click="getPage">
|
||||
<slot name="item"></slot>
|
||||
<lazyImg class="cover" :src="props.info.cover">
|
||||
<div class="permission permission2">免费</div>
|
||||
<!-- <div v-else-if="props.info.permission === 1 && props.info.price" class="permission permission1">
|
||||
<SvgIcon name="diamond" />
|
||||
{{ props.info.price }}
|
||||
</div> -->
|
||||
</lazyImg>
|
||||
<div class="title-box">
|
||||
<div class="title">{{ props.info.title }}</div>
|
||||
<slot></slot>
|
||||
<UpdateStatus :info="props.info"></UpdateStatus>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { goDetailPage } from '../utils/common';
|
||||
import type { MediaTypeKey } from '../utils/common';
|
||||
import UpdateStatus from './updateStatus.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
info: Types.Acg.DiscountChildListItem;
|
||||
}>(),
|
||||
{
|
||||
info: () => {},
|
||||
},
|
||||
);
|
||||
const getPage = () => {
|
||||
goDetailPage(props.info.mediaType as MediaTypeKey, props.info.id, props.info.mediaSubType);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
@import '../assets/style.scss';
|
||||
.item {
|
||||
flex-shrink: 0;
|
||||
width: 111px;
|
||||
height: 188px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: calc(100% - 46px);
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.animationImg) {
|
||||
img {
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
.collect {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 29px;
|
||||
height: 29px;
|
||||
}
|
||||
.text-1-box {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: inline-flex;
|
||||
padding: 2px 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
|
||||
.textIcon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
@include tag();
|
||||
}
|
||||
.title-box {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
// height: 33px;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// .icon {
|
||||
// width: 18px;
|
||||
// height: 18px;
|
||||
// flex-shrink: 0;
|
||||
// margin-right: 2px;
|
||||
// margin-bottom: -2px;
|
||||
// &.video {
|
||||
// width: 12px;
|
||||
// height: 12px;
|
||||
// margin-bottom: 0px;
|
||||
// }
|
||||
// }
|
||||
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin: 6px 0 4px;
|
||||
@include textEllipsis();
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div
|
||||
class="episode-text"
|
||||
:class="{ active: props.episodeNumber == props.info.episodeNumber }"
|
||||
v-if="showType == 'text'"
|
||||
>
|
||||
第{{ props.info.episodeNumber }}话
|
||||
</div>
|
||||
<!-- 方格模式 -->
|
||||
<div
|
||||
v-else
|
||||
class="episode flex-center-center"
|
||||
:class="{ active: props.episodeNumber == props.info.episodeNumber, video: props.isVideo, tag: hasPaid || goldTag }"
|
||||
>
|
||||
{{ props.info.episodeNumber }}
|
||||
<iconByte v-if="props.episodeNumber == props.info.episodeNumber && props.isVideo" class="icon"></iconByte>
|
||||
<!-- <div v-if="props.info.listenPermission === 0" class="permission permission0">VIP</div> -->
|
||||
<!-- <div v-if="hasPaid" class="permission permission1">已解锁</div> -->
|
||||
<!-- <div v-else-if="goldTag" class="permission permission1">
|
||||
<SvgIcon name="gold" />
|
||||
{{ props.info.price }}
|
||||
</div> -->
|
||||
<!-- <div v-else-if="props.info.listenPermission === 2" class="permission permission2">免费</div> -->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, Ref } from 'vue';
|
||||
import iconByte from './icon-byte.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
isVideo?: boolean;
|
||||
episodeNumber: number;
|
||||
info: Types.Acg.MediaContentListItem;
|
||||
// parentInfo: Types.Media.MediaInfo;
|
||||
showType?: string; // 默认方格模式,text: 文字列表
|
||||
}>(),
|
||||
{
|
||||
isVideo: true,
|
||||
episodeNumber: 0,
|
||||
info: () => {
|
||||
return {} as Types.Acg.MediaContentListItem;
|
||||
},
|
||||
// parentInfo: () => {
|
||||
// return {} as Types.Media.MediaInfo;
|
||||
// },
|
||||
},
|
||||
);
|
||||
const parentInfo = inject('historyData', null) as Ref<Types.Media.MediaInfo> | null;
|
||||
const hasPaid = computed(() => {
|
||||
if (props.info.hasBuy) return true;
|
||||
if (parentInfo && parentInfo.value.mediaStatus && parentInfo.value.mediaStatus.hasPaid) return true;
|
||||
return false;
|
||||
});
|
||||
const goldTag = computed(() => {
|
||||
return props.info.listenPermission === 1 && props.info.price;
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.episode {
|
||||
height: 58px;
|
||||
width: 58px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border: 1px solid #4b4b4b;
|
||||
|
||||
&.active {
|
||||
border-color: $themeColor;
|
||||
color: $themeColor;
|
||||
background: rgba(255, 211, 0, 0.1);
|
||||
&.tag {
|
||||
align-items: flex-end !important;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-bottom: -2px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.permission {
|
||||
display: inline-flex;
|
||||
padding: 2px 4px;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
font-size: 9px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 14px; /* 160% */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
border-radius: 0px 0px 9px 0px;
|
||||
color: #000;
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
&.permission0 {
|
||||
background: linear-gradient(134deg, #f6cbb8 -1.45%, #eeb4aa 102.9%);
|
||||
}
|
||||
&.permission1 {
|
||||
background: #3e64ea;
|
||||
color: #fff;
|
||||
}
|
||||
&.permission2 {
|
||||
background: linear-gradient(135deg, #adfcac 0%, #5cef97 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
.episode-text {
|
||||
margin-top: 12px;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
}
|
||||
.active {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="item" :class="{ active: props.episodeNumber === props.info.episodeNumber }">
|
||||
<!-- <div v-if="props.info.listenPermission === 0" class="tag vip flex-vcenter">VIP</div> -->
|
||||
<div v-if="props.info.price && props.info.listenPermission === 1" class="tag price flex-vcenter">
|
||||
<SvgIcon name="diamond" />
|
||||
{{ props.info.price }}
|
||||
</div>
|
||||
<div v-else-if="props.info.listenPermission === 2" class="tag">免费</div>
|
||||
<lazyImg class="cover" :src="props.info.cover" :key="props.info.cover">
|
||||
<div v-if="progress" class="mask">
|
||||
<div class="progress-txt">{{ progress }}%</div>
|
||||
<div class="progress-box">
|
||||
<span :style="{ width: progress + '%' }"></span>
|
||||
</div>
|
||||
</div>
|
||||
</lazyImg>
|
||||
<div class="name ellipsis">{{ props.info.name }}</div>
|
||||
<span class="date">{{ dateFormatYyyyMmDd(props.info.createdAt) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { dateFormatYyyyMmDd } from '@/utils/index';
|
||||
const props = defineProps<{
|
||||
episodeNumber: number;
|
||||
info: Types.Acg.MediaContentListItem;
|
||||
progress?: 0;
|
||||
}>();
|
||||
const progress = computed(() => {
|
||||
return props.progress || 0;
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.item {
|
||||
flex-shrink: 0;
|
||||
width: 111px;
|
||||
height: 165px;
|
||||
overflow: hidden;
|
||||
margin: 0 5px 12px 0;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
position: relative;
|
||||
&.active {
|
||||
.cover {
|
||||
border: 1px solid #ff004f;
|
||||
}
|
||||
}
|
||||
&:nth-child(3n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.tag {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #adfcac 0%, #5cef97 100%);
|
||||
color: #212121;
|
||||
padding: 3px 8px;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
&.price {
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #b378ff 0%, #482cf5 100%);
|
||||
}
|
||||
&.vip {
|
||||
background: linear-gradient(135deg, #ffe580 0%, #facc15 100%);
|
||||
}
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: 111px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
.mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
.progress-txt {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 4px;
|
||||
right: 4px;
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
.progress-box {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
left: 4px;
|
||||
right: 4px;
|
||||
height: 3px;
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
span {
|
||||
display: block;
|
||||
height: 3px;
|
||||
background-color: $themeBgColor;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.name {
|
||||
color: #fff;
|
||||
margin-top: 8px;
|
||||
padding: 0 6px;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
}
|
||||
.date {
|
||||
padding: 0 6px;
|
||||
color: #c8c8c8;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<!-- @click.stop="
|
||||
props.customClick
|
||||
? void 0
|
||||
: $router.push({
|
||||
name: 'AcgImageDetailWatch',
|
||||
query: { id: props.mediaId, episodeNumber: props.episodeNumber },
|
||||
})
|
||||
" -->
|
||||
<div class="episode-box flex-vcenter" :class="{ progress: progress && props.progressUrl }" @click.stop="onClick">
|
||||
<lazyImg class="cover" :src="props.info.cover"></lazyImg>
|
||||
<div class="info">
|
||||
<!-- <div class="num">第{{ props.episodeNumber }}话</div> -->
|
||||
<span class="title ellipsis">{{ props.info.name }}</span>
|
||||
</div>
|
||||
|
||||
<SvgIcon name="backWhite"></SvgIcon>
|
||||
<lazyImg
|
||||
v-if="progress && props.progressUrl"
|
||||
class="episode-content"
|
||||
:src="props.progressUrl"
|
||||
:key="props.progressUrl"
|
||||
>
|
||||
<div class="mask flex-center-center">
|
||||
<div class="progress-container flex-vcenter">
|
||||
{{ progress }}%
|
||||
<div class="progress-box">
|
||||
<span :style="{ width: progress + '%' }"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</lazyImg>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
const props = defineProps<{
|
||||
mediaId: string;
|
||||
info:
|
||||
| Types.Acg.MediaContentListItem
|
||||
| {
|
||||
cover: string;
|
||||
id: string;
|
||||
name: string;
|
||||
urlSet: string[];
|
||||
};
|
||||
episodeNumber: number;
|
||||
mediaSubType: number;
|
||||
progress?: number;
|
||||
progressUrl?: string;
|
||||
customClick?: boolean;
|
||||
}>();
|
||||
|
||||
const progress = computed(() => {
|
||||
return props.progress || 0;
|
||||
});
|
||||
|
||||
const onClick = () => {
|
||||
if (props.customClick) return;
|
||||
const name = props.mediaSubType === 2 ? 'AcgImageDetailWatch2' : 'AcgImageDetailWatch';
|
||||
router.push({
|
||||
name,
|
||||
query: { id: props.mediaId, episodeNumber: props.episodeNumber },
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.episode-box {
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
margin: 18px auto;
|
||||
display: flex;
|
||||
width: 343px;
|
||||
// height: 64px;
|
||||
padding: 8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
color: #424242;
|
||||
&.progress {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.cover {
|
||||
flex-shrink: 0;
|
||||
width: 56px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.info {
|
||||
flex: 1;
|
||||
margin: 0 8px;
|
||||
color: #424242;
|
||||
.num,
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
}
|
||||
// .title {
|
||||
// font-size: 14px;
|
||||
// font-style: normal;
|
||||
// font-weight: 400;
|
||||
// line-height: 20px;
|
||||
// }
|
||||
}
|
||||
|
||||
svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.episode-content {
|
||||
margin: 12px -8px 0;
|
||||
width: calc(100% + 16px);
|
||||
height: 180px;
|
||||
border-radius: 0 0 8px 8px;
|
||||
overflow: hidden;
|
||||
|
||||
.mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
|
||||
.progress-container {
|
||||
padding: 8px 12px;
|
||||
border-radius: 25px;
|
||||
background: #fff;
|
||||
width: 311px;
|
||||
height: 33px;
|
||||
position: relative;
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
.progress-box {
|
||||
margin-left: 4px;
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
border-radius: 6px;
|
||||
background: rgb(51, 51, 51, 10%);
|
||||
span {
|
||||
display: block;
|
||||
height: 6px;
|
||||
background-color: #333;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div v-if="(props.list || []).length" class="category-content">
|
||||
<div v-if="$props.title" class="title" @click="goTopicPage(props.id, props.contentType, props.title)">
|
||||
<span>{{ props.title }}</span>
|
||||
<SvgIcon class="icon" name="moreIcon" />
|
||||
</div>
|
||||
<div v-if="props.contentWrap" ref="categoryContentList" class="list"></div>
|
||||
<div v-else ref="categoryContentList" class="list" @touchstart.stop @touchmove.stop @touchend.stop></div>
|
||||
<Teleport v-if="categoryContentList" :to="categoryContentList">
|
||||
<CategoryContentItem
|
||||
v-for="(item, index) in props.list"
|
||||
:key="index"
|
||||
:id="item.id"
|
||||
:url="item.horizontalCover || item.verticalCover"
|
||||
:title="item.title"
|
||||
:contentType="props.contentType"
|
||||
:info="item"
|
||||
></CategoryContentItem>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { goTopicPage } from '@/views/acgModule/utils/common';
|
||||
import CategoryContentItem from './categoryContentItem.vue';
|
||||
const categoryContentList = ref();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
contentWrap?: boolean; //换行显示,portrait三个一行,landscape两个一行
|
||||
contentType?: string; // 'video' | 'image';
|
||||
title?: string;
|
||||
list: Types.Media.AllMediaInfoItem[];
|
||||
id?: string;
|
||||
}>(),
|
||||
{
|
||||
contentWrap: false,
|
||||
contentType: 'video',
|
||||
title: '',
|
||||
list: () => [],
|
||||
id: '',
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '../assets/style.scss';
|
||||
.category-content {
|
||||
@include categoryWrap();
|
||||
.title {
|
||||
@include categoryTitle();
|
||||
}
|
||||
.list {
|
||||
@include categoryList();
|
||||
}
|
||||
|
||||
.list {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
.item {
|
||||
width: 168px;
|
||||
height: 110px;
|
||||
margin-right: 0;
|
||||
margin-bottom: 10px;
|
||||
&:first-child {
|
||||
width: 343px;
|
||||
height: 196px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="byte-wrap flex-center-center">
|
||||
<span v-for="i in 3" :key="i" class="item" :class="'dance_' + arr[i - 1]"></span>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
function shuffleArray(arr: number[]) {
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
const arr = shuffleArray([1, 2, 3]);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.byte-wrap {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
justify-content: space-evenly !important;
|
||||
.item {
|
||||
width: 2px;
|
||||
height: 11px;
|
||||
border-radius: 2px;
|
||||
background-color: #f68804;
|
||||
animation: dance 0.6s infinite linear alternate;
|
||||
@for $i from 1 through 3 {
|
||||
&.dance_#{$i} {
|
||||
animation-delay: #{0.2 + ($i - 1) * 0.2}s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dance {
|
||||
0% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<van-popup :show="isShow" z-index="2001" position="bottom" @click-overlay="emit('close')">
|
||||
<div class="wrapper flex-center-center">
|
||||
<div class="content" :class="[props.theme]" @click.stop="void 0">
|
||||
<div class="line_top" />
|
||||
<div class="title">购买作品</div>
|
||||
<div class="priceList">
|
||||
<div class="leader">作品解锁</div>
|
||||
<div class="price gold flex-vcenter">
|
||||
<SvgIcon name="gold" />
|
||||
{{ gold }}金币
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="priceList">
|
||||
<div class="leader">作品原价</div>
|
||||
<div class="price through flex-vcenter">{{ gold }}金币</div>
|
||||
</div> -->
|
||||
<!-- <div v-if="userInfo.isVip" class="priceList vip">
|
||||
<div class="leader">您当前享受{{ sale }}折扣优惠</div>
|
||||
<div class="price underline flex-vcenter" @click.stop="$router.push('member')">升级会员</div>
|
||||
</div>
|
||||
<div v-else class="priceList vip">
|
||||
<div class="leader">您当前不享受免费观看</div>
|
||||
<div class="price underline flex-vcenter" @click.stop="$router.push('member')">购买VIP享免费观看</div>
|
||||
</div> -->
|
||||
<div class="priceList split">
|
||||
<div class="leader">我的金币</div>
|
||||
<div class="price flex-vcenter">
|
||||
{{ recharge }}
|
||||
<!-- 100 -->
|
||||
<span class="go" @click.stop="$router.push('wallet'), emit('close')">充值</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btnBox flex-center-between">
|
||||
<div class="btn flex-center-center" @click="unlockNow('all')">{{ gold }}金币/立即支付</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</Teleport>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import useLoading from '@/hooks/loading';
|
||||
import VideoApi from '@/service/video';
|
||||
import { computed, inject, nextTick, Ref } from 'vue';
|
||||
import { ProductType } from '@/enums/app';
|
||||
import { showToast } from 'vant';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
videoInfo: any; //Types.Video.VInfosItem | Types.Acg.MediaContentListItem
|
||||
isOpen: boolean;
|
||||
theme?: string; //black 为黑色主题,默认为白色主题
|
||||
}>(),
|
||||
{
|
||||
theme: 'black',
|
||||
},
|
||||
);
|
||||
|
||||
const userStore = useUserStore();
|
||||
const userInfo = userStore.userInfo;
|
||||
|
||||
const historyData = inject('historyData') as Ref<Types.Media.MediaInfo>;
|
||||
// const salePrice = computed(() => {
|
||||
// let { contentsPrice = 0, price = 0, updateStatus = 0 } = historyData.value;
|
||||
// if (updateStatus === 1) return 0;
|
||||
// if (contentsPrice <= 0 || price <= 0) return 0;
|
||||
// const res = contentsPrice - price;
|
||||
// return res < 0 ? 0 : res;
|
||||
// });
|
||||
const gold = computed(() => {
|
||||
return historyData.value.price;
|
||||
});
|
||||
const sale = computed(() => {
|
||||
return 10 - Math.floor(((props.videoInfo.originCoins - props.videoInfo.coins) / props.videoInfo.originCoins) * 10);
|
||||
});
|
||||
|
||||
const recharge = computed(() => {
|
||||
return (userStore.walletInfo.amount * 100 + userStore.walletInfo.income * 100) / 100;
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const { openLoading, closeLoading } = useLoading();
|
||||
const emit = defineEmits(['close', 'buySuccess']);
|
||||
const isShow = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
const unlockNow = (type: 'all') => {
|
||||
try {
|
||||
if (recharge.value < gold.value) {
|
||||
emit('close');
|
||||
router.push('/wallet');
|
||||
return;
|
||||
}
|
||||
let productID = historyData.value.id;
|
||||
if (!productID) {
|
||||
showToast('购买出错~');
|
||||
return;
|
||||
}
|
||||
openLoading();
|
||||
|
||||
VideoApi.videoBuying({
|
||||
productType: ProductType.AcgAll,
|
||||
productID,
|
||||
// goldVideoCouponNum,
|
||||
})
|
||||
.then(async () => {
|
||||
showToast('购买成功');
|
||||
userStore.updateWallet();
|
||||
emit('buySuccess', { type, productID });
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e.toString().indexOf('8000') != -1) {
|
||||
emit('close');
|
||||
nextTick(() => {
|
||||
router.push({
|
||||
path: '/wallet',
|
||||
query: {
|
||||
activeTab: 1,
|
||||
id: props.videoInfo.id,
|
||||
title: props.videoInfo.title || '无标题',
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
closeLoading();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.content {
|
||||
color: #000;
|
||||
width: 100%;
|
||||
padding: 18px 10px 0;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
border-radius: 10px 10px 0 0;
|
||||
padding-bottom: 24px;
|
||||
.line_top {
|
||||
width: 36px;
|
||||
height: 5px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
margin: 0 auto;
|
||||
border-radius: 99px;
|
||||
}
|
||||
.title {
|
||||
padding: 18px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
.priceList {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 18px 0;
|
||||
&.vip {
|
||||
.leader,
|
||||
.price {
|
||||
color: #ac7e6e;
|
||||
}
|
||||
}
|
||||
&.split {
|
||||
border-top: $borderSplit;
|
||||
margin-top: 18px;
|
||||
padding-top: 18px;
|
||||
}
|
||||
.leader {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 14px;
|
||||
}
|
||||
.price {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
> svg {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.go {
|
||||
text-decoration-line: underline;
|
||||
color: #a4634d;
|
||||
margin-left: 12px;
|
||||
background: linear-gradient(286deg, #a4634d 0%, #ac7e6e 100%);
|
||||
background-clip: text;
|
||||
}
|
||||
}
|
||||
.gold {
|
||||
color: #ff9000;
|
||||
}
|
||||
.through {
|
||||
text-decoration: line-through;
|
||||
color: #747b80;
|
||||
}
|
||||
.underline {
|
||||
text-decoration: underline;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
&.black {
|
||||
background-color: #0f0f0f;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
.line_top {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.leader {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
.text {
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
margin: 30px 0 18px;
|
||||
span {
|
||||
font-size: 28px;
|
||||
color: $themeColor;
|
||||
}
|
||||
|
||||
&.line {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
.text-center {
|
||||
position: relative;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 30%;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
background: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
p {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.btnBox {
|
||||
margin-top: 26px;
|
||||
|
||||
.btn {
|
||||
height: 44px;
|
||||
width: 100%;
|
||||
background: #f68804;
|
||||
text-align: center;
|
||||
line-height: 44px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: $btnPrimaryColor;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<GoldPopup :is-open="visiblePermission1" :video-info="props.info" @close="emit('close')" @buy-success="buySuccess" />
|
||||
<VipPopup :is-open="visiblePermission0" @close="emit('close')" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import GoldPopup from './GoldPopup.vue';
|
||||
// import VipPopup from './VipPopup.vue';
|
||||
import { computed, ref, watch, onActivated, inject, Ref } from 'vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import UserApi from '@/service/user';
|
||||
// import { ProductType } from '@/enums/app';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
info: any; // Types.Media.MediaInfo | Types.Acg.MediaContentListItem;
|
||||
isOpen: boolean;
|
||||
}>(),
|
||||
{
|
||||
info: () => <any>{},
|
||||
isOpen: () => false,
|
||||
},
|
||||
);
|
||||
|
||||
const historyData = inject('historyData') as Ref<Types.Media.MediaInfo>;
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const emit = defineEmits(['close', 'buySuccess']);
|
||||
|
||||
const visiblePermission0 = ref(false);
|
||||
const visiblePermission1 = ref(false);
|
||||
// const visibleTodayFree = ref(false);
|
||||
|
||||
const isTypeVip = computed(() => {
|
||||
return userStore.isVip;
|
||||
});
|
||||
|
||||
// permission 收听权限 0:会员 1:金币购买 2:免费
|
||||
const permission = computed(() => {
|
||||
return historyData.value.permission;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.isOpen,
|
||||
(bool) => {
|
||||
if (!bool) {
|
||||
// visibleTodayFree.value = false;
|
||||
visiblePermission0.value = false;
|
||||
visiblePermission1.value = false;
|
||||
return;
|
||||
} else {
|
||||
if (permission.value === undefined) {
|
||||
emit('close');
|
||||
return;
|
||||
}
|
||||
if (permission.value === 1) {
|
||||
visiblePermission1.value = true;
|
||||
} else if (permission.value === 0) {
|
||||
visiblePermission0.value = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
onActivated(() => {
|
||||
// 会员类型要从接口取用户最新信息
|
||||
if (permission.value === 0 && !isTypeVip.value) {
|
||||
UserApi.queryUserInfo(null).then((res) => {
|
||||
userStore.saveUserInfo(res);
|
||||
if (res.isVip) {
|
||||
emit('close');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const buySuccess = (obj: any) => {
|
||||
emit('buySuccess', obj);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<van-overlay :show="isShow" z-index="2001" @click.stop="emit('close')">
|
||||
<div class="wrapper flex-center-center">
|
||||
<div class="content">
|
||||
<div class="title">温馨提示</div>
|
||||
<div class="horizontalLine"></div>
|
||||
<div class="text">本{{ typeName }}需要开通VIP会员</div>
|
||||
<p>开通会员 即可解锁查看完整版</p>
|
||||
<div class="btnBox flex-center-center" @click.stop="$router.push('member')">立即开通</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-overlay>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
const route = useRoute();
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
}>();
|
||||
const emit = defineEmits(['close']);
|
||||
const isShow = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
|
||||
const typeName = computed(() => {
|
||||
if ('AcgVideoDetail' === ((route.name as string) || '')) {
|
||||
return '视频';
|
||||
} else {
|
||||
return '章节';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.content {
|
||||
width: 311px;
|
||||
height: 242px;
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
background: $popupBg;
|
||||
position: relative;
|
||||
border-radius: 12px;
|
||||
.title {
|
||||
color: #fff;
|
||||
padding: 32px 0 12px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
.horizontalLine {
|
||||
height: 1px;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.text {
|
||||
text-align: center;
|
||||
margin: 18px 0 30px;
|
||||
}
|
||||
p {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.btnBox {
|
||||
margin: 26px auto 0;
|
||||
color: white;
|
||||
background: $themeColor;
|
||||
width: 263px;
|
||||
height: 44px;
|
||||
border-radius: 44px;
|
||||
text-align: center;
|
||||
line-height: 44px;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div v-if="configStore.appConfig.vipMark && props.info.permission === 0" class="permission permission_0">VIP</div>
|
||||
<div
|
||||
v-if="configStore.appConfig.coinMark && props.info.permission === 1 && props.info.price"
|
||||
class="permission permission_1"
|
||||
>
|
||||
金币
|
||||
<!-- {{ props.info.price }} -->
|
||||
</div>
|
||||
<!-- <div v-if="configStore.appConfig.freeMark && props.info.permission === 2" class="permission permission_2">免费</div> -->
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
contentType?: string;
|
||||
info: Types.Media.AllMediaInfoItem;
|
||||
}>(),
|
||||
{
|
||||
contentType: 'video',
|
||||
info: {} as any,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.text-1-box {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
|
||||
.textIcon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.permission {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 27px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
border-radius: 3px;
|
||||
font-family: 'PingFang SC';
|
||||
color: #292929;
|
||||
background: linear-gradient(135deg, #f8f3ae 0%, #ddac44 100%);
|
||||
|
||||
&.permission_1 {
|
||||
color: #8b3e00;
|
||||
background: linear-gradient(135deg, #ffe580 0%, #facc15 100%);
|
||||
}
|
||||
&.permission_2 {
|
||||
color: #212121;
|
||||
background: linear-gradient(135deg, #adfcac 0%, #5cef97 100%);
|
||||
}
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="popularity">
|
||||
<div v-if="props.id" class="title" @click="goTopicPage(props.id, props.contentType, props.title)">
|
||||
<slot name="title"></slot>
|
||||
<span v-if="props.title">{{ props.title }}</span>
|
||||
<span class="more flex-vcenter">
|
||||
{{ props.navTitle || '更多' }}
|
||||
<SvgIcon class="icon" name="moreIcon" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="list" @touchstart.stop @touchmove.stop @touchend.stop>
|
||||
<div class="item" v-for="(item, index) in props.list" :key="index" @click="getPage(item)">
|
||||
<lazyImg class="cover" :src="item.verticalCover">
|
||||
<img
|
||||
v-if="item.mediaType === 'text' && item.mediaSubType === 1"
|
||||
class="textIcon"
|
||||
src="@/assets/images/png/acg/type_text.png"
|
||||
/>
|
||||
<PermissionTag :info="item" :content-type="props.contentType" />
|
||||
</lazyImg>
|
||||
<div class="content">
|
||||
<div class="title ellipsis2">{{ item.title }}</div>
|
||||
<!-- <div class="desc">{{ item.summary }}</div> -->
|
||||
<!-- <div class="tag-box">
|
||||
<span v-for="tag in item.tagDetails || []" :key="tag" class="tag">{{ tag.name }}</span>
|
||||
</div> -->
|
||||
<UpdateStatus :info="item"></UpdateStatus>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { goTopicPage } from '@/views/acgModule/utils/common';
|
||||
import UpdateStatus from './updateStatus.vue';
|
||||
import { goDetailPage } from '../utils/common';
|
||||
import type { MediaTypeKey } from '../utils/common';
|
||||
import PermissionTag from './permission-tag.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id: string;
|
||||
title: string;
|
||||
list: any[];
|
||||
contentType?: string; // 'video' | 'image';
|
||||
navTitle?: string;
|
||||
}>(),
|
||||
{
|
||||
id: '',
|
||||
title: '',
|
||||
list: () => [[]],
|
||||
navTitle: '更多',
|
||||
contentType: 'video',
|
||||
},
|
||||
);
|
||||
|
||||
const getPage = (item: any) => {
|
||||
goDetailPage(item.mediaType as MediaTypeKey, item.id, item.mediaSubType);
|
||||
// //类型video:动漫,image:漫画 )
|
||||
// if (props.contentType === 'video') {
|
||||
// router.push({
|
||||
// path: '/animationMovieDetails',
|
||||
// query: {
|
||||
// vid: item.id,
|
||||
// },
|
||||
// });
|
||||
// } else {
|
||||
// router.push({
|
||||
// path: '/AcgCaricatureDetail',
|
||||
// query: {
|
||||
// id: item.id,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import '../assets/style.scss';
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
.popularity {
|
||||
@include categoryWrap();
|
||||
|
||||
> .title {
|
||||
@include categoryTitle();
|
||||
}
|
||||
|
||||
.list {
|
||||
@include categoryList();
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
height: 330px;
|
||||
|
||||
.item {
|
||||
width: 262px;
|
||||
height: 100px;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 30px;
|
||||
&:nth-child(3n) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
|
||||
.cover {
|
||||
width: 72px;
|
||||
height: 100px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 10px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
.textIcon {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
|
||||
.title {
|
||||
// @include textEllipsis();
|
||||
color: #ffffffe5;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
@include lineClamp(2);
|
||||
color: #666;
|
||||
margin: 12px 0 8px;
|
||||
}
|
||||
.tag-box {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.tag {
|
||||
color: #00929c;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
padding: 1px 4px;
|
||||
border-radius: 32px;
|
||||
border: 1px solid #00929c;
|
||||
margin: 0 4px 6px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<h2 class="h2Title" v-if="props.title">{{ props.title }}</h2>
|
||||
<Loading v-if="stateList.loading"></Loading>
|
||||
<template v-else>
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh" style="height: auto">
|
||||
<div v-if="stateList.list.length > 0">
|
||||
<CategoryContent
|
||||
class="content"
|
||||
:class="{ marginHide: props.marginHide }"
|
||||
:contentType="props.contentType"
|
||||
:contentWrap="true"
|
||||
:list="stateList.list"
|
||||
></CategoryContent>
|
||||
</div>
|
||||
<Empty v-else style="height: auto"></Empty>
|
||||
</PullRefresh>
|
||||
</template>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import Empty from './../../../components/Empty/index.vue';
|
||||
import { reactive } from 'vue';
|
||||
import CategoryContent from './categoryContent.vue';
|
||||
import AcgApi from '@/service/acg';
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default() {
|
||||
return '精彩推荐';
|
||||
},
|
||||
},
|
||||
marginHide: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
mediaId: {
|
||||
type: String,
|
||||
},
|
||||
contentType: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const stateList = reactive({
|
||||
loading: false,
|
||||
list: <any>[],
|
||||
});
|
||||
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: 9,
|
||||
};
|
||||
|
||||
const moreParams = reactive({
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
finished: false,
|
||||
isNoData: false,
|
||||
error: false,
|
||||
isHigehtMax: false,
|
||||
});
|
||||
|
||||
const getList = async () => {
|
||||
try {
|
||||
const TempfilterParams = Object.assign({}, params, { mediaId: props.mediaId });
|
||||
let res: any = await AcgApi.getMediaRecommend(TempfilterParams);
|
||||
stateList.loading = false;
|
||||
moreParams.loading = false;
|
||||
moreParams.refreshing = false;
|
||||
|
||||
let list = res.list || [];
|
||||
|
||||
if (params.pageNumber === 1) {
|
||||
stateList.list = list;
|
||||
} else {
|
||||
stateList.list = stateList.list.concat(list);
|
||||
}
|
||||
moreParams.isNoData = params.pageNumber == 1 && stateList.list.length == 0;
|
||||
moreParams.finished = !res.hasNext;
|
||||
} catch (e) {
|
||||
moreParams.loading = false;
|
||||
moreParams.error = true;
|
||||
}
|
||||
};
|
||||
|
||||
const onLoad = () => {
|
||||
moreParams.loading = true;
|
||||
params.pageNumber++;
|
||||
getList();
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
params.pageNumber = 1;
|
||||
moreParams.refreshing = true;
|
||||
moreParams.finished = false;
|
||||
getList();
|
||||
};
|
||||
|
||||
getList();
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.h2Title {
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
margin: 18px 0 0;
|
||||
}
|
||||
.content {
|
||||
margin: 16px;
|
||||
&.marginHide {
|
||||
margin: 16px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="header flex-center-between">
|
||||
<div
|
||||
class="searchBtn flex-vcenter"
|
||||
@click="$router.push({ path: '/AcgSearch', query: { mediaType: props.mediaType } })"
|
||||
>
|
||||
<div class="search"></div>
|
||||
<span>请输入搜索文字</span>
|
||||
</div>
|
||||
<div class="theme_btn_bg filter" style="margin-right: 12px" @click="$router.push('myBookshelf')">
|
||||
<SvgIcon name="bookshelf" class="bookshelf" style="color: #fff"></SvgIcon>
|
||||
</div>
|
||||
<div
|
||||
class="theme_btn_bg filter"
|
||||
@click="$router.push({ path: '/AcgSearchFilter', query: { mediaType: props.mediaType } })"
|
||||
>
|
||||
<SvgIcon name="acg_filter" style="color: #fff"></SvgIcon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
mediaType: string;
|
||||
}>();
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
padding: 7px 16px;
|
||||
background-color: transparent;
|
||||
height: 44px;
|
||||
height: 44px;
|
||||
|
||||
.searchBtn {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
flex: 1;
|
||||
height: 30px;
|
||||
border-radius: 30px;
|
||||
font-size: 14px;
|
||||
padding: 0 6px 0 12px;
|
||||
color: #999;
|
||||
margin-right: 12px;
|
||||
.search {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
background: url('@/assets/images/png/home/search.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div ref="rootRef" class="summary-box">
|
||||
<!-- <div v-if="props.title" class="title">{{ props.title }}</div> -->
|
||||
<TextOverflow :text="props.text" :maxLines="3">
|
||||
<template #default="{ clickToggle, expanded }">
|
||||
<Teleport :to="rootRef">
|
||||
<div @click.stop="clickToggle" class="expanded flex-center-center">
|
||||
{{ expanded ? '收起更多' : '更多简介' }}
|
||||
<SvgIcon name="back" :class="['icon', expanded ? 'down' : 'up']" />
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
</TextOverflow>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default() {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const rootRef = ref();
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.summary-box {
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
position: relative;
|
||||
// background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
color: #989898;
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
.expanded {
|
||||
position: relative;
|
||||
left: 0;
|
||||
margin-top: 12px;
|
||||
// margin-left: -9px;
|
||||
svg {
|
||||
transform: rotate(-90deg);
|
||||
font-size: 18px;
|
||||
|
||||
&.down {
|
||||
transform: rotate(-270deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="advBox" v-if="advList.length">
|
||||
<AdvSwiper class="advSwiper" :list="advList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { AdType } from '@/enums/app';
|
||||
import { getAdItem } from '@/utils/getConfig';
|
||||
const advList = ref(getAdItem(AdType.ACGADV));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.advSwiper {
|
||||
height: 143px;
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
transform: translateZ(0px);
|
||||
position: relative;
|
||||
:deep(.van-swipe-item) {
|
||||
height: 143px !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
:deep(.van-swipe__indicators) {
|
||||
left: auto;
|
||||
right: 12px;
|
||||
transform: none;
|
||||
.van-swipe__indicator {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 6px;
|
||||
background: rgb(245, 44, 86, 60%);
|
||||
border: none;
|
||||
&.van-swipe__indicator--active {
|
||||
width: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div v-if="props.list" class="fiveGrid">
|
||||
<div class="rectAdvBox" @click="jumpAdv(item)" v-for="item in props.list" :key="item.id">
|
||||
<lazyImg class="rectAdvIcon" :src="item.cover" />
|
||||
<div class="rectName ellipsis">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
list: Types.App.AdsInfoListItem[];
|
||||
}>(),
|
||||
{
|
||||
list: () => [],
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.fiveGrid {
|
||||
padding: 18px 0 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
|
||||
grid-row-gap: 12px;
|
||||
grid-column-gap: 10px;
|
||||
.rectAdvBox {
|
||||
width: 60px;
|
||||
.rectAdvIcon {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.rectName {
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="navMap.length"
|
||||
class="nav-section flex-vcenter overflow-x-auto"
|
||||
@touchstart.stop
|
||||
@touchmove.stop
|
||||
@touchend.stop
|
||||
>
|
||||
<div class="item flex-center-center" v-for="item in navMap" :key="item.name" @click="goPage(item)">
|
||||
<lazyImg class="icon" :src="item.img" :key="item.img"></lazyImg>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { innerJump, compatibleAppStoreJump } from '@/utils/index';
|
||||
|
||||
const useConfig = useConfigStore();
|
||||
const navMap = (useConfig.appConfig?.jgArea || []).filter((item) => item.type === 1);
|
||||
const goPage = (item: Types.App.JingGangItem) => {
|
||||
const { link_type, link_url } = item;
|
||||
const index = link_url.indexOf('yinseinner://');
|
||||
if (link_type === 2 || index !== -1) {
|
||||
innerJump(link_url.substring(13));
|
||||
} else {
|
||||
compatibleAppStoreJump(link_url);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.nav-section {
|
||||
width: 100%;
|
||||
.item {
|
||||
flex-shrink: 0;
|
||||
border-radius: 100px;
|
||||
padding: 12px 0;
|
||||
margin: 0 20px;
|
||||
|
||||
color: $titleColor;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
flex-direction: column;
|
||||
.icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div v-if="props.data.tab.length > 0" class="rank-wrap">
|
||||
<div class="bg"></div>
|
||||
<img class="title" src="@/assets/images/png/acg/rank-title.png" />
|
||||
<van-tabs v-model:active="tabActive" :animated="true" @rendered="onTabRendered" class="tabs" swipeable>
|
||||
<van-tab v-for="(item, index) in props.data.tab" :key="item.type" :title="item.name" :name="item.type">
|
||||
<List
|
||||
v-if="tabIsInit(item.type)"
|
||||
:type="item.type"
|
||||
:mediaType="mediaType"
|
||||
:defaultData="index === 0 ? props.data.list || [] : []"
|
||||
></List>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { inject } from 'vue';
|
||||
import tabAction from '@/utils/tabAction';
|
||||
// import { rankTabList } from '../../utils/common';
|
||||
import type { MediaTypeKey } from '../../utils/common';
|
||||
import List from './rankList.vue';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
data: Types.Acg.DefaultAcgRanking;
|
||||
}>(),
|
||||
{
|
||||
data: () => {
|
||||
return {
|
||||
tab: [],
|
||||
list: [],
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const mediaType = inject('mediaType') as MediaTypeKey;
|
||||
|
||||
const { tabActive, tabIsInit, onTabRendered } = tabAction(false);
|
||||
if (props.data.tab.length > 0) {
|
||||
tabActive.value = props.data.tab[0].type;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '../../assets/style.scss';
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
.rank-wrap {
|
||||
width: 375px;
|
||||
height: 600px;
|
||||
margin-left: -16px;
|
||||
position: relative;
|
||||
// @include vanTab-btn();
|
||||
@include popupTab();
|
||||
.bg {
|
||||
background: url(@/assets/images/png/acg/rank-bg.png) 0 0 / contain no-repeat;
|
||||
// filter: blur(6px);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
margin: 18px 0 12px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
--van-padding-xs: 16px;
|
||||
--van-tab-font-size: 12px;
|
||||
|
||||
.tabs {
|
||||
z-index: 2;
|
||||
--van-tabs-line-height: 28px;
|
||||
:deep(.van-tabs__wrap) {
|
||||
.van-tab--active {
|
||||
border-radius: 100px;
|
||||
background-color: $themeBgLgColor;
|
||||
font-weight: normal;
|
||||
}
|
||||
.van-tabs__line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
:deep(.van-tabs__content) {
|
||||
.van-tab__panel {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div class="list-wrap">
|
||||
<Loading v-if="state.loading"></Loading>
|
||||
<template v-else>
|
||||
<div class="container" v-if="state.list.length > 0">
|
||||
<CategoryContentItem
|
||||
v-for="(item, index) in state.list"
|
||||
:key="index"
|
||||
:id="item.id"
|
||||
:url="item.verticalCover || item.horizontalCover"
|
||||
:title="item.title"
|
||||
:contentType="item.mediaType"
|
||||
:info="item"
|
||||
class="item"
|
||||
>
|
||||
<template v-slot:item>
|
||||
<img v-if="index === 0" class="icon" src="@/assets/images/png/acg/rank-top-1.png" />
|
||||
<img v-else-if="index === 1" class="icon" src="@/assets/images/png/acg/rank-top-2.png" />
|
||||
<img v-else-if="index === 2" class="icon" src="@/assets/images/png/acg/rank-top-3.png" />
|
||||
<div v-else class="icon-txt">{{ index + 1 }}</div>
|
||||
</template>
|
||||
</CategoryContentItem>
|
||||
</div>
|
||||
<Empty v-else class="empty" />
|
||||
<div class="btn" @click="goMore">
|
||||
查看更多
|
||||
<SvgIcon class="icon" name="moreIcon" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRaw, reactive, inject } from 'vue';
|
||||
import CategoryContentItem from '../../components/categoryContentItem.vue';
|
||||
import AcgApi from '@/service/acg';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type: number;
|
||||
mediaType: string;
|
||||
defaultData: Types.Media.ModuleItem[];
|
||||
}>(),
|
||||
{
|
||||
type: () => 1,
|
||||
mediaType: 'video',
|
||||
defaultData: () => [],
|
||||
},
|
||||
);
|
||||
|
||||
const state = reactive({
|
||||
loading: true,
|
||||
list: <any>[],
|
||||
});
|
||||
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: 6,
|
||||
};
|
||||
|
||||
const getList = async () => {
|
||||
try {
|
||||
if (props.defaultData.length > 0) {
|
||||
state.list = props.defaultData;
|
||||
state.loading = false;
|
||||
return;
|
||||
}
|
||||
const TempfilterParams = Object.assign({}, params, toRaw(props));
|
||||
let res: any = await AcgApi.getRanking(TempfilterParams);
|
||||
state.list = res.list || [];
|
||||
state.loading = false;
|
||||
} catch (e) {
|
||||
state.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
getList();
|
||||
|
||||
const goRankTab = inject('acg_go_rank_tab') as any;
|
||||
const goMore = () => {
|
||||
goRankTab && goRankTab();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list-wrap {
|
||||
.content {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.btn {
|
||||
width: 343px;
|
||||
height: 44px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
border-radius: 100px;
|
||||
background: $themeBgLgColor;
|
||||
color: #fff;
|
||||
.icon {
|
||||
color: #fff;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
.empty {
|
||||
margin-top: 18px;
|
||||
padding-bottom: 18px;
|
||||
min-height: 428px;
|
||||
}
|
||||
.container {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 18px;
|
||||
padding-bottom: 18px;
|
||||
min-height: 428px;
|
||||
.item {
|
||||
width: 111px;
|
||||
height: 199px;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 12px;
|
||||
position: relative;
|
||||
|
||||
// :deep(.text-1-box) {
|
||||
// display: none;
|
||||
// }
|
||||
|
||||
&:nth-child(3n) {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.icon,
|
||||
.icon-txt {
|
||||
&.icon {
|
||||
object-fit: contain;
|
||||
}
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
&.icon-txt {
|
||||
background: #000;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
line-height: 24px; /* 128.571% */
|
||||
text-indent: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,268 @@
|
||||
<!-- 全量视频(海角样式) -->
|
||||
<template>
|
||||
<div class="acg-tab-item overflow-y-auto" :id="'home' + tabIndex" ref="rootEl">
|
||||
<div class="acg-theme">
|
||||
<HomeHeader
|
||||
:advType="AdType.COMMUNITYCAROUSEL"
|
||||
class="advBox"
|
||||
v-if="!props.pureVersion"
|
||||
/>
|
||||
<!-- 标签区 -->
|
||||
<div class="labelBox" v-if="state.allSection && state.allSection.length">
|
||||
<div
|
||||
class="label ellipsis"
|
||||
v-for="item in state.allSection"
|
||||
:key="item.sectionID"
|
||||
@click="toTagShortVideo(item)"
|
||||
>
|
||||
{{ item.sectionName }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-sort flex-vcenter" ref="dvSort">
|
||||
<div
|
||||
class="item"
|
||||
:class="{ active: item.key === moduleSort }"
|
||||
v-for="item in moduleSortMap"
|
||||
:key="item.key"
|
||||
@click="onChange(item.key)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
|
||||
<div class="list">
|
||||
<CategoryContentItem
|
||||
v-for="(item, index) in state.list"
|
||||
:key="index"
|
||||
class="item"
|
||||
:id="item.id"
|
||||
:url="item.verticalCover"
|
||||
:title="item.title"
|
||||
:contentType="item.mediaType"
|
||||
:info="item"
|
||||
></CategoryContentItem>
|
||||
</div>
|
||||
</PullRefresh>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, inject } from 'vue';
|
||||
import CategoryContentItem from '../categoryContentItem.vue';
|
||||
import HomeApi from '@/service/home';
|
||||
import { AdType } from '@/enums/app';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { getMediaTypeKey } from '../../utils/common';
|
||||
|
||||
const router = useRouter();
|
||||
const dvSort = ref();
|
||||
|
||||
const props = defineProps<{
|
||||
moduleId: string;
|
||||
moduleName: string;
|
||||
tabIndex?: number;
|
||||
type: number;
|
||||
advType?: number;
|
||||
pureVersion?: boolean;
|
||||
}>();
|
||||
|
||||
const rootEl = ref();
|
||||
|
||||
const moduleSortMap = [
|
||||
{
|
||||
name: '最多观看',
|
||||
key: 3,
|
||||
},
|
||||
{
|
||||
name: '最新发布',
|
||||
key: 1,
|
||||
},
|
||||
{
|
||||
name: '最多收藏',
|
||||
key: 7,
|
||||
},
|
||||
|
||||
// {
|
||||
// name: '本周最热',
|
||||
// key: 2,
|
||||
// },
|
||||
// {
|
||||
// name: '最多点赞',
|
||||
// key: 3,
|
||||
// },
|
||||
];
|
||||
|
||||
const moduleSort = ref(moduleSortMap[0].key);
|
||||
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: 12,
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
list: <Types.Media.AllMediaInfoItem[]>[],
|
||||
allSection: <Types.Media.allSection[]>[],
|
||||
});
|
||||
|
||||
const moreParams = reactive({
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
finished: false,
|
||||
isNoData: false,
|
||||
error: false,
|
||||
isHigehtMax: false,
|
||||
});
|
||||
|
||||
let sortTop = ref(0);
|
||||
const getList = async (type = '') => {
|
||||
let req = {
|
||||
id: props.moduleId,
|
||||
data: Object.assign({}, params, { moduleSort: moduleSort.value }),
|
||||
};
|
||||
try {
|
||||
let res: any = await HomeApi.getModuleList(req);
|
||||
moreParams.loading = false;
|
||||
moreParams.refreshing = false;
|
||||
let list = res.allMediaInfo || [];
|
||||
|
||||
const allSection = res.allSection || [];
|
||||
if (allSection.length && params.pageNumber === 1) {
|
||||
state.allSection = allSection;
|
||||
}
|
||||
|
||||
if (params.pageNumber === 1) {
|
||||
moreParams.isNoData = list.length ? false : true;
|
||||
}
|
||||
if (type === 'refresh') {
|
||||
state.list = list;
|
||||
} else {
|
||||
state.list = [...state.list, ...list];
|
||||
}
|
||||
if (!res.hasNext) {
|
||||
moreParams.finished = true;
|
||||
}
|
||||
if (sortTop.value === 0) {
|
||||
sortTop.value = dvSort.value.offsetTop;
|
||||
}
|
||||
} catch (e) {
|
||||
moreParams.loading = false;
|
||||
moreParams.error = true;
|
||||
}
|
||||
};
|
||||
const onLoad = () => {
|
||||
moreParams.loading = true;
|
||||
params.pageNumber++;
|
||||
getList();
|
||||
};
|
||||
const onRefresh = () => {
|
||||
params.pageNumber = 1;
|
||||
moreParams.refreshing = true;
|
||||
moreParams.finished = false;
|
||||
getList('refresh');
|
||||
};
|
||||
// 初始化
|
||||
const init = () => {
|
||||
// 获取轮播图数据
|
||||
getList();
|
||||
};
|
||||
init();
|
||||
|
||||
const onChange = (key: number) => {
|
||||
moduleSort.value = key;
|
||||
if (sortTop.value < rootEl.value.scrollTop) {
|
||||
rootEl.value.scrollTop = sortTop.value;
|
||||
}
|
||||
onRefresh();
|
||||
};
|
||||
|
||||
const type = getMediaTypeKey(props.type);
|
||||
const toTagShortVideo = (val: any) => {
|
||||
router.push({
|
||||
path: '/AcgTopicList',
|
||||
query: {
|
||||
id: val.sectionID,
|
||||
title: val.sectionName,
|
||||
type,
|
||||
showType: 0,
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '../../assets/style.scss';
|
||||
.acg-tab-item {
|
||||
height: calc(100vh - 44px * 2);
|
||||
width: 100vw;
|
||||
.tab-sort {
|
||||
background: #0f0f0f;
|
||||
padding: 0 26px;
|
||||
height: 44px;
|
||||
position: sticky;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
// background-color: var(--theme-color);
|
||||
.item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
&:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
right: 0px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
&.active {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 16px;
|
||||
.item {
|
||||
width: 111px;
|
||||
height: 190px;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:nth-child(3n) {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.labelBox {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
grid-row-gap: 7px;
|
||||
grid-column-gap: 9px;
|
||||
padding: 0 16px;
|
||||
margin-top: 18px;
|
||||
.label {
|
||||
width: auto;
|
||||
height: 32px;
|
||||
padding: 0 4px;
|
||||
border-radius: 2px;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.advBox {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,249 @@
|
||||
<template>
|
||||
<div class="acg-tab-item overflow-y-auto" :id="`acg_${props.moduleId}`">
|
||||
<!-- <Search class="search-box" :mediaType="mediaType"></Search> -->
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
|
||||
<div class="wrap">
|
||||
<!-- 广告轮播 -->
|
||||
<HomeHeader
|
||||
:advType="AdType.COMMUNITYCAROUSEL"
|
||||
class="advBox"
|
||||
v-if="!props.pureVersion"
|
||||
/>
|
||||
<!-- <CategoryTag v-if="['video', 'image'].includes(mediaType)" :list="tagList" :mediaType="mediaType"></CategoryTag> -->
|
||||
<template v-for="(item, index) in state.list" :key="item.sectionID">
|
||||
<!--
|
||||
101 一大四小 1
|
||||
102 四宫格(横acg+影视共用) 1
|
||||
205 2.5左右滑动(竖ACG)
|
||||
201 四宫格(竖ACG) 1
|
||||
202 六宫格(竖ACG) 1
|
||||
203 九宫格(竖ACG) 1
|
||||
204 小列表 (竖ACG) 1
|
||||
-->
|
||||
<!-- 一大四小 -->
|
||||
<FiveContent
|
||||
v-if="item.showType === 101"
|
||||
:coverDirection="getCoverDirection(item.showType)"
|
||||
:id="item.sectionID"
|
||||
:title="item.sectionName"
|
||||
:list="item.allMediaInfo || []"
|
||||
:contentType="mediaType"
|
||||
></FiveContent>
|
||||
<CategoryContent
|
||||
v-else-if="[205].includes(item.showType)"
|
||||
:coverDirection="getCoverDirection(item.showType)"
|
||||
:id="item.sectionID"
|
||||
:title="item.sectionName"
|
||||
:list="item.allMediaInfo || []"
|
||||
:contentType="mediaType"
|
||||
:showType="item.showType"
|
||||
></CategoryContent>
|
||||
<!-- <Topic
|
||||
v-else-if="item.showType === 5"
|
||||
:id="item.sectionID"
|
||||
:title="item.sectionName"
|
||||
:list="item.allMediaInfo || []"
|
||||
:disVoteCount="item.disVoteCount"
|
||||
:voteCount="item.voteCount"
|
||||
:contentType="mediaType"
|
||||
@disVoteCount="item.disVoteCount++"
|
||||
@voteCount="item.voteCount++"
|
||||
></Topic> -->
|
||||
<CategoryContent
|
||||
v-else-if="[201].includes(item.showType)"
|
||||
:contentWrap="true"
|
||||
:id="item.sectionID"
|
||||
:title="item.sectionName"
|
||||
:list="item.allMediaInfo || []"
|
||||
:contentType="mediaType"
|
||||
:showType="item.showType"
|
||||
></CategoryContent>
|
||||
<CategoryContent
|
||||
v-else-if="[202, 203].includes(item.showType)"
|
||||
:contentWrap="true"
|
||||
:id="item.sectionID"
|
||||
:title="item.sectionName"
|
||||
:list="item.allMediaInfo || []"
|
||||
:contentType="mediaType"
|
||||
:showType="item.showType"
|
||||
></CategoryContent>
|
||||
<Popularity
|
||||
v-else-if="item.showType === 204"
|
||||
:id="item.sectionID"
|
||||
:title="item.sectionName"
|
||||
:list="item.allMediaInfo || []"
|
||||
:contentType="mediaType"
|
||||
></Popularity>
|
||||
<!-- 默认按照102处理 -->
|
||||
<CategoryContent
|
||||
v-else
|
||||
coverDirection="landscape"
|
||||
:contentWrap="true"
|
||||
:id="item.sectionID"
|
||||
:title="item.sectionName"
|
||||
:list="item.allMediaInfo || []"
|
||||
:contentType="mediaType"
|
||||
></CategoryContent>
|
||||
<template v-if="(index + 1) % 2 === 0 && !props.pureVersion">
|
||||
<HeaderAdv
|
||||
class="advBox"
|
||||
:advType="AdType.TopicGridAdv"
|
||||
random
|
||||
:length="0"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</PullRefresh>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, computed, ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import Search from '../search.vue';
|
||||
import CategoryTag from '../categoryTag.vue';
|
||||
import CategoryContent from '../categoryContent.vue';
|
||||
import FiveContent from '../fiveContent.vue';
|
||||
import Topic from '../topic.vue';
|
||||
import Popularity from '../popularity.vue';
|
||||
import HomeApi from '@/service/home';
|
||||
import { useAcgStore } from '@/stores/acg';
|
||||
import { AdType } from '@/enums/app';
|
||||
import { getAdItem } from '@/utils/getConfig';
|
||||
|
||||
const acgStore = useAcgStore();
|
||||
const props = defineProps<{
|
||||
moduleId: string;
|
||||
moduleName: string;
|
||||
showType: number;
|
||||
advType?: number;
|
||||
pureVersion?: boolean;
|
||||
}>();
|
||||
|
||||
const mediaType = computed(() => {
|
||||
if (props.moduleName.includes('小说')) {
|
||||
return 'text';
|
||||
}
|
||||
switch (props.moduleName) {
|
||||
case '动漫':
|
||||
return 'video';
|
||||
case '漫画':
|
||||
return 'image';
|
||||
// case '小说':
|
||||
// return 'text';
|
||||
case '剧场':
|
||||
return 'theatre';
|
||||
default:
|
||||
return 'video';
|
||||
}
|
||||
});
|
||||
|
||||
const getCoverDirection = (showType: number) => {
|
||||
return showType === 2 ? 'portrait' : 'landscape';
|
||||
};
|
||||
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
moduleSort: 1,
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
list: <Types.Media.ModuleItem[]>[],
|
||||
advList: <Array<Types.App.AdsInfoListItem>>[], // 广告轮播
|
||||
});
|
||||
|
||||
const { library } = storeToRefs(acgStore);
|
||||
const tagList = computed(() => {
|
||||
return library.value.find((item) => item.key === mediaType.value)?.kind || [];
|
||||
});
|
||||
|
||||
const moreParams = reactive({
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
finished: false,
|
||||
isNoData: false,
|
||||
error: false,
|
||||
isHigehtMax: false,
|
||||
});
|
||||
|
||||
const getList = async () => {
|
||||
let req = {
|
||||
id: props.moduleId,
|
||||
data: params,
|
||||
};
|
||||
try {
|
||||
let res: any = await HomeApi.getModuleList(req);
|
||||
moreParams.loading = false;
|
||||
moreParams.refreshing = false;
|
||||
let list = res.allSection || [];
|
||||
|
||||
const isRefresh = params.pageNumber === 1;
|
||||
if (isRefresh) {
|
||||
state.list = list;
|
||||
} else {
|
||||
state.list = state.list.concat(list);
|
||||
}
|
||||
if (isRefresh && state.list.length == 0) {
|
||||
moreParams.isNoData = true;
|
||||
}
|
||||
if (!res.hasNext) {
|
||||
moreParams.finished = true;
|
||||
}
|
||||
} catch (e) {
|
||||
moreParams.loading = false;
|
||||
moreParams.error = true;
|
||||
}
|
||||
};
|
||||
const onLoad = () => {
|
||||
moreParams.loading = true;
|
||||
params.pageNumber++;
|
||||
getList();
|
||||
};
|
||||
const onRefresh = () => {
|
||||
params.pageNumber = 1;
|
||||
moreParams.refreshing = true;
|
||||
moreParams.finished = false;
|
||||
getList();
|
||||
};
|
||||
|
||||
const advList = ref([] as Types.App.AdsInfoListItem[]);
|
||||
// 初始化
|
||||
const init = () => {
|
||||
advList.value = getAdItem(AdType.TopicGridAdv) || [];
|
||||
console.log(advList.value);
|
||||
getList();
|
||||
};
|
||||
init();
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '../../assets/style.scss';
|
||||
.acg-tab-item {
|
||||
height: calc(100vh - 44px * 2);
|
||||
overflow: visible;
|
||||
.advSwiper {
|
||||
height: 143px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transform: translateZ(0px);
|
||||
position: relative;
|
||||
:deep(.van-swipe-item) {
|
||||
height: 143px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.search-box {
|
||||
background-color: #fff;
|
||||
position: sticky;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 22;
|
||||
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.wrap {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<!--
|
||||
模块showType枚举
|
||||
2: 海角
|
||||
3: 专题
|
||||
-->
|
||||
<van-tabs
|
||||
v-model:active="tabActive"
|
||||
:animated="true"
|
||||
:scrollspy="false"
|
||||
shrink
|
||||
:lazy-render="true"
|
||||
swipeable
|
||||
@rendered="onTabRendered"
|
||||
class="tabs oepnX"
|
||||
>
|
||||
<van-tab v-for="item in secondTab" :key="item.id" :title="item.moduleName" :name="item.id">
|
||||
<template v-if="tabIsInit(item.id)">
|
||||
<component
|
||||
class="contain overflow-y-auto"
|
||||
:is="item.showType == 2 ? TabItem : TabHome"
|
||||
:id="prefixKey + item.id"
|
||||
:moduleId="item.id"
|
||||
:moduleName="item.moduleName"
|
||||
:showType="item.showType"
|
||||
:advType="props.advType"
|
||||
:type="props.type"
|
||||
:pureVersion="item.pureVersion"
|
||||
/>
|
||||
</template>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, provide, unref, onActivated } from 'vue';
|
||||
import { useGlobalStore } from '@/stores/global';
|
||||
// import AcgApi from '@/service/acg';
|
||||
import { getNewDate } from '../../utils/common';
|
||||
import type { MediaTypeKey } from '../../utils/common';
|
||||
import tabAction from '@/utils/tabAction';
|
||||
import TabItem from './tabItem.vue';
|
||||
import TabHome from './tabHome.vue';
|
||||
import { showToast } from 'vant';
|
||||
import { AdType } from '@/enums/app';
|
||||
// const componentMap: any = {
|
||||
// 2: TabHome,
|
||||
// 3: TabItem,
|
||||
// };
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type: number;
|
||||
mediaType: MediaTypeKey;
|
||||
moduleName: string;
|
||||
advType?: number;
|
||||
}>(),
|
||||
{
|
||||
advType: AdType.HOME,
|
||||
},
|
||||
);
|
||||
provide('mediaType', unref(props.mediaType));
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const currentTime = ref(getNewDate().getTime());
|
||||
|
||||
onActivated(() => {
|
||||
currentTime.value = getNewDate().getTime();
|
||||
});
|
||||
|
||||
const secondTab = computed(() => {
|
||||
// return (globalStore.tabObj[props.type === 6 ? 'novel' : 'cartoon'] || []).filter((item: any) => {
|
||||
// return item.type === props.type;
|
||||
// });
|
||||
return globalStore.tabObj[props.type === 6 ? 'novel' : 'cartoon'] || [];
|
||||
});
|
||||
const secondTabNavSectionIndex = computed(() => {
|
||||
return secondTab.value.findIndex((item: any) => [1, 2].includes(item.showType));
|
||||
});
|
||||
|
||||
const prefixKey = 'acg_second_page_';
|
||||
const { tabActive, tabIsInit, onTabRendered } = tabAction(true, prefixKey);
|
||||
|
||||
provide('acg_go_rank_tab', () => {
|
||||
const item = secondTab.value.find((item: any) => item.showType === 5);
|
||||
if (item) {
|
||||
tabActive.value = item.id;
|
||||
} else {
|
||||
showToast('暂无更多');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '../../assets/style.scss';
|
||||
.tabs {
|
||||
:deep(> .van-tabs__wrap) {
|
||||
--van-tabs-nav-background: transparent;
|
||||
--van-tab-active-text-color: #fff;
|
||||
--van-tab-text-color: #9fa19f;
|
||||
--van-padding-xs: 6px;
|
||||
.van-tabs__nav--line {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.van-tab__text {
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.van-tabs__line {
|
||||
width: 16px;
|
||||
bottom: 4px;
|
||||
height: 2px;
|
||||
border-radius: 12px;
|
||||
background: #f68804;
|
||||
}
|
||||
}
|
||||
|
||||
.contain {
|
||||
height: calc(100vh - 44px * 3);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div class="taskList" v-if="props.list.length > 0">
|
||||
<div class="taskItem flex-center-between" v-for="item in props.list" :key="item.id">
|
||||
<div class="leftBox flex-vcenter">
|
||||
<LazyImg :src="item.icon" class="taskIcon" :key="item.icon" />
|
||||
<div class="taskInfo">
|
||||
<div class="taskName">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div class="taskDesc" v-if="item.awardList.length > 0">+{{ item.awardList[0]?.num }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action flex-center-center-direction">
|
||||
<div class="btn" v-if="item?.state == 0" @click.stop="goDetailPage(props.mediaType, item.objId)">去完成</div>
|
||||
<div class="btn receive" v-else-if="item?.state == 1" @click.stop="getMediaTaskReceiveList(item)">待领取</div>
|
||||
<div class="btn received" v-else-if="item?.state == 2">已领取</div>
|
||||
<span class="progress">({{ item.progress }}/{{ item.target }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="props.completeAllTask.id" class="all-task">
|
||||
<div class="go flex-center-between">
|
||||
完美任务完成直达领券
|
||||
<span v-if="state === 0" class="status0" @click.stop="goDetailPage(props.mediaType, props.completeAllTask.objId)">
|
||||
去完成
|
||||
<SvgIcon class="icon" name="moreIcon" />
|
||||
</span>
|
||||
<span v-else-if="state === 1" class="status1 flex-vcenter" @click.stop="getMediaTaskReceive()">
|
||||
立即领取
|
||||
<SvgIcon class="icon" name="moreIcon" />
|
||||
</span>
|
||||
|
||||
<span v-if="state === 2" class="status2">已领取</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { goDetailPage } from '../../utils/common';
|
||||
import type { MediaTypeKey } from '../../utils/common';
|
||||
import AcgApi from '@/service/acg';
|
||||
import { showToast } from 'vant';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
list: Types.Acg.TaskListItem[];
|
||||
completeAllTask?: Types.Acg.CompleteAllTask;
|
||||
mediaType: MediaTypeKey;
|
||||
}>(),
|
||||
{
|
||||
list: () => [],
|
||||
mediaType: 'image',
|
||||
completeAllTask: () => {},
|
||||
},
|
||||
);
|
||||
|
||||
const getMediaTaskReceiveList = (item: Types.Acg.TaskListItem) => {
|
||||
AcgApi.getMediaTaskReceive({ taskId: item.id }).then(() => {
|
||||
item.state = 2;
|
||||
showToast('领取成功');
|
||||
});
|
||||
};
|
||||
|
||||
const state = ref(props.completeAllTask.state);
|
||||
watch(
|
||||
() => props.completeAllTask.state,
|
||||
(num: number) => {
|
||||
state.value = num || 0;
|
||||
},
|
||||
);
|
||||
const getMediaTaskReceive = () => {
|
||||
AcgApi.getMediaTaskReceive({ taskId: props.completeAllTask.id }).then(() => {
|
||||
state.value = 2;
|
||||
showToast('领取成功');
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.taskList {
|
||||
margin: 0 auto;
|
||||
width: 343px;
|
||||
padding: 0px 12px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
.taskItem {
|
||||
padding: 12px 0;
|
||||
border-bottom: 0.5px solid rgba(255, 255, 255, 0.05);
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.leftBox {
|
||||
flex: 1;
|
||||
.taskIcon {
|
||||
flex-shrink: 0;
|
||||
height: 65px;
|
||||
width: 65px;
|
||||
// border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.taskInfo {
|
||||
flex: 1;
|
||||
margin-left: 12px;
|
||||
.taskName {
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.taskDesc {
|
||||
font-size: 12px;
|
||||
color: #aaaaaa;
|
||||
margin-top: 5px;
|
||||
line-height: 16px;
|
||||
width: 170px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.action {
|
||||
.btn {
|
||||
height: 28px;
|
||||
width: 60px;
|
||||
background: #ff004f;
|
||||
border-radius: 40px;
|
||||
text-align: center;
|
||||
line-height: 28px;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
&.receive {
|
||||
color: #692a1f;
|
||||
background: #ffd5a8;
|
||||
}
|
||||
&.received {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.all-task {
|
||||
margin: 24px 16px;
|
||||
|
||||
.go {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 22px; /* 137.5% */
|
||||
border-radius: 9px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status0 {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.status1 {
|
||||
color: #ddac44;
|
||||
}
|
||||
.status2 {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<div class="topic">
|
||||
<div class="title">
|
||||
{{ props.title }}
|
||||
</div>
|
||||
<div v-for="(item, index) in autoList" :key="index" class="list" @touchstart.stop @touchmove.stop @touchend.stop>
|
||||
<CategoryContentItem
|
||||
class="item"
|
||||
v-for="childItem in item"
|
||||
:key="childItem.id"
|
||||
:id="childItem.id"
|
||||
:url="childItem.verticalCover"
|
||||
:title="childItem.title"
|
||||
:info="childItem"
|
||||
:contentType="props.contentType"
|
||||
></CategoryContentItem>
|
||||
</div>
|
||||
<div class="score-wrap">
|
||||
<div class="score">
|
||||
<span>{{ props.disVoteCount }}</span>
|
||||
<span>{{ props.voteCount }}</span>
|
||||
</div>
|
||||
<div class="bar">
|
||||
<div
|
||||
class="red"
|
||||
:style="{
|
||||
width: redBar + '%',
|
||||
}"
|
||||
></div>
|
||||
<div
|
||||
class="blue"
|
||||
:style="{
|
||||
width: blueBar + '%',
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="btn">
|
||||
<div class="red" @click="onVote(2)">
|
||||
<img class="icon" src="@/assets/images/png/acg/icon-btn-red.png" />
|
||||
贫乳!!
|
||||
</div>
|
||||
<img class="pk" src="@/assets/images/png/acg/topic-pk.png" />
|
||||
<div class="blue" @click="onVote(1)">
|
||||
<img class="icon" src="@/assets/images/png/acg/icon-btn-blue.png" />
|
||||
巨乳!!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import CategoryContentItem from './categoryContentItem.vue';
|
||||
import MediaApi from '@/service/media';
|
||||
import { showToast } from 'vant';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title: string;
|
||||
list: Types.Media.AllMediaInfoItem[];
|
||||
disVoteCount: number; //红
|
||||
voteCount: number; //蓝
|
||||
id: string;
|
||||
contentType: string;
|
||||
}>(),
|
||||
{
|
||||
disVoteCount: 0,
|
||||
voteCount: 0,
|
||||
title: '',
|
||||
list: () => [],
|
||||
id: '',
|
||||
},
|
||||
);
|
||||
const emit = defineEmits(['disVoteCount', 'voteCount']);
|
||||
|
||||
const autoList = computed(() => {
|
||||
const list: Types.Media.AllMediaInfoItem[][] = [[]];
|
||||
const arr = props.list || [];
|
||||
const len = arr.length;
|
||||
if (len > 1) {
|
||||
list.push([]);
|
||||
const oneLen = Math.floor(len / 2);
|
||||
list[0] = arr.slice(0, oneLen);
|
||||
list[1] = arr.slice(oneLen);
|
||||
} else {
|
||||
list[0] = arr;
|
||||
}
|
||||
|
||||
return list;
|
||||
});
|
||||
|
||||
const redBar = computed(() => {
|
||||
return (props.disVoteCount / (props.disVoteCount + props.voteCount)) * 96 || 48;
|
||||
});
|
||||
|
||||
const blueBar = computed(() => {
|
||||
return (props.voteCount / (props.disVoteCount + props.voteCount)) * 96 || 48;
|
||||
});
|
||||
|
||||
let isVoteIng = false;
|
||||
const onVote = (status: number) => {
|
||||
if (isVoteIng) return;
|
||||
isVoteIng = true;
|
||||
const key = `section_vote`;
|
||||
const value = localStorage.getItem(key) || '';
|
||||
if (value.includes(props.id)) {
|
||||
showToast('您已经投过票啦~');
|
||||
return;
|
||||
}
|
||||
// 状态 1、支持大胸 2、支持贫胸
|
||||
MediaApi.postSctionVote({
|
||||
sId: props.id,
|
||||
status,
|
||||
})
|
||||
.then(() => {
|
||||
showToast('投票成功');
|
||||
if (status === 1) {
|
||||
emit('voteCount');
|
||||
} else {
|
||||
emit('disVoteCount');
|
||||
}
|
||||
localStorage.setItem(key, value + ',' + props.id);
|
||||
})
|
||||
.finally(() => {
|
||||
isVoteIng = false;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '../assets/style.scss';
|
||||
.topic {
|
||||
@include categoryWrap();
|
||||
margin-top: 12px;
|
||||
|
||||
.title {
|
||||
@include categoryTitle();
|
||||
}
|
||||
|
||||
.list {
|
||||
@include categoryList();
|
||||
.item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.score-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&.score {
|
||||
margin-top: 12px;
|
||||
color: $textColor;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
&.bar {
|
||||
margin: 12px 0;
|
||||
height: 10px;
|
||||
width: 100%;
|
||||
> div {
|
||||
height: 100%;
|
||||
width: 100px;
|
||||
border-radius: 5px;
|
||||
|
||||
&.red {
|
||||
background: #e05a54;
|
||||
}
|
||||
&.blue {
|
||||
background: #3675df;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.btn {
|
||||
overflow: hidden;
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
width: 101px;
|
||||
height: 34px;
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
> img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
&.red {
|
||||
background: #e05a54;
|
||||
}
|
||||
&.blue {
|
||||
background: #3675df;
|
||||
}
|
||||
}
|
||||
.pk {
|
||||
height: 39px;
|
||||
margin: 0 10px;
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="updateStatus" :class="{ end: props.info.updateStatus != 1 }">
|
||||
<template v-if="props.info.updateStatus != 1">共{{ props.info.totalEpisode }}章 已完结</template>
|
||||
<template v-else>更新{{ props.info.currentEpisode || props.info.totalEpisode }}章 连载中</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
info: Types.Media.MediaInfo | Types.Media.AllMediaInfoItem | Types.Acg.DiscountChildListItem;
|
||||
}>(),
|
||||
{
|
||||
info: {} as any,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.updateStatus {
|
||||
color: #eec76b;
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
// text-indent: 2px;
|
||||
&.end {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,773 @@
|
||||
<template>
|
||||
<div class="detail-wrap overflow-y-auto" @scroll="onScroll">
|
||||
<template v-if="state.info.id">
|
||||
<div class="header flex-center-between" :style="headStyle">
|
||||
<SvgIcon name="back_white_round1" class="back" @click="$router.go(-1)" />
|
||||
<p v-show="scrollTop >= 100" class="title ellipsis">{{ state.info.title }}</p>
|
||||
<div class="bookself">
|
||||
<div class="menuItem">
|
||||
<SvgIcon
|
||||
class="collected"
|
||||
name="acg-collect-img-1"
|
||||
v-if="historyData?.mediaStatus?.hasCollected"
|
||||
@click="onToogleCollected"
|
||||
></SvgIcon>
|
||||
<SvgIcon class="collected" name="acg-collect-img-0" v-else @click="onToogleCollected"></SvgIcon>
|
||||
<SvgIcon name="acg-share1" @click="openShare" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cover-box flex-vcenter">
|
||||
<lazyImg class="cover" :src="state.info.verticalCover || state.info.horizontalCover"></lazyImg>
|
||||
<div class="content">
|
||||
<h1 class="title ellipsis">{{ state.info.title }}</h1>
|
||||
<div class="status">
|
||||
<!-- <template v-if="state.info.updateStatus == 2">已完结 共{{ state.info.totalEpisode }}话</template>
|
||||
<template v-else>更新至第{{ state.info.currentEpisode || state.info.totalEpisode }}话</template> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<div class="watch" :class="state.info.mediaType">
|
||||
<div class="info">
|
||||
<div class="count flex-vcenter">
|
||||
<div class="count-item">
|
||||
<SvgIcon name="eye-open" />
|
||||
{{ watchCount(state.info.countBrowse) }}
|
||||
</div>
|
||||
<div class="count-item">
|
||||
<SvgIcon name="acg-heart-2" />
|
||||
{{ watchCount(state.info.countLike) }}
|
||||
</div>
|
||||
<div class="count-item">
|
||||
{{ state.info.permission == 0 ? 'VIP' : (state.info.price || '0') + '金币' }}
|
||||
</div>
|
||||
|
||||
<!-- <div class="collected flex-vcenter" @click="onToogleCollected">
|
||||
<SvgIcon v-if="!hasCollected" name="acg-collect-star-0" />
|
||||
<SvgIcon v-else name="acg-collect-star-1" style="color: #f52c56" />
|
||||
{{ watchCount(state.info.countCollect) }}
|
||||
</div> -->
|
||||
</div>
|
||||
<div :class="['summary', !showMoreSummary && 'ellipsis3']">{{ state.info.summary || '' }}</div>
|
||||
<div
|
||||
v-if="state.info.summary"
|
||||
:class="['more-summary', showMoreSummary && 'showMoreSummary']"
|
||||
@click="showMoreSummary = !showMoreSummary"
|
||||
>
|
||||
更多简介
|
||||
<SvgIcon name="rightArrow1" />
|
||||
</div>
|
||||
<div class="tags" v-if="tagDetails.length">
|
||||
<div
|
||||
class="tag"
|
||||
v-for="item in tagDetails"
|
||||
:key="item.id"
|
||||
@click="goAcgTagPage(item.id, item.name, 'image')"
|
||||
>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav" @click="visibleCatalogue = true">
|
||||
<span class="title">选集</span>
|
||||
<span class="count flex-vcenter">
|
||||
{{ EpisodeTitle }}
|
||||
<SvgIcon name="rightArrow1" />
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="episode-box"
|
||||
ref="episodeBox"
|
||||
@touchstart.stop
|
||||
@touchmove.stop
|
||||
@touchend.stop
|
||||
@scroll="onScrollEpisodeBox"
|
||||
>
|
||||
<Episode
|
||||
showType="text"
|
||||
:id="'episode-' + item.id"
|
||||
class="item"
|
||||
:episodeNumber="childState.episodeNumber"
|
||||
:info="item"
|
||||
v-for="item in autoList?.slice(0, 4) || []"
|
||||
:key="item.id"
|
||||
@click="childState.episodeNumber = item.episodeNumber"
|
||||
></Episode>
|
||||
</div>
|
||||
</div>
|
||||
<RecommendedList :type="state.info.mediaType" :tagId="tagDetails[0]?.id" class="recommendedBox" />
|
||||
<div class="action">
|
||||
<div class="btn flex-vcenter" @click="onToogleLiked">
|
||||
<SvgIcon v-if="historyData?.mediaStatus?.hasLiked" name="acg-heart-1" />
|
||||
<SvgIcon v-else name="acg-heart-0" />
|
||||
喜欢
|
||||
</div>
|
||||
<div class="btn flex-vcenter go-page" @click="goWatch">
|
||||
{{ currentEpisodeName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<Loading v-else />
|
||||
</div>
|
||||
<van-popup v-model:show="visibleCatalogue" position="bottom" z-index="1001" :style="{ height: '438px' }">
|
||||
<div class="play-list-box">
|
||||
<div class="line"></div>
|
||||
<div class="title flex-center-between">
|
||||
漫画列表
|
||||
<div class="right">
|
||||
<SvgIcon name="acg_sort_0" v-if="sort" />
|
||||
<SvgIcon name="acg_sort_1" v-else />
|
||||
<span @click="sort = !sort">
|
||||
{{ sort ? '正序' : '倒序' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-box overflow-y-auto">
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
|
||||
<div class="list">
|
||||
<Episode
|
||||
showType="text"
|
||||
class="item"
|
||||
:episodeNumber="childState.episodeNumber"
|
||||
:info="item"
|
||||
v-for="item in autoList || []"
|
||||
:key="item.id"
|
||||
@click="handlePopupSwitchEpisode(item.episodeNumber)"
|
||||
></Episode>
|
||||
</div>
|
||||
</PullRefresh>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
<PayForWatch
|
||||
v-if="childState.validateItem && visiblePayForWatch"
|
||||
ref="dvPayForWatch"
|
||||
:isOpen="visiblePayForWatch"
|
||||
:info="childState.validateItem"
|
||||
@close="visiblePayForWatch = false"
|
||||
@buySuccess="beforeBuySuccess"
|
||||
></PayForWatch>
|
||||
<AppSharePopup
|
||||
coverType="portrait"
|
||||
:is-open="visibleShare"
|
||||
:cover="state.info.verticalCover || state.info.horizontalCover"
|
||||
@close="visibleShare = false"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AcgImageDetail',
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRaw, watch, computed, watchEffect, provide, onDeactivated, onActivated } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import PayForWatch from '../components/pay/PayForWatch.vue';
|
||||
import tabAction from '@/utils/tabAction';
|
||||
import { AdType } from '@/enums/app';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import MediaInfo from '../utils/mediaInfo';
|
||||
import mediaContentInfo from '../utils/mediaContentInfo';
|
||||
import Episode from '../components/episode.vue';
|
||||
import { handleCollected } from '../utils/collect';
|
||||
import { dateFormatYyyyMmDd, watchCount } from '@/utils/index';
|
||||
import { Pay_parent_on, Pay_child_on } from '../utils/pay-mitt';
|
||||
import type { PayInfo } from '../utils/pay-mitt';
|
||||
import { goAcgTagPage } from '../utils/common';
|
||||
import CommunityApi from '@/service/community';
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const dvComment = ref();
|
||||
const showMoreSummary = ref(false);
|
||||
|
||||
const { state, historyEpisodeNumber, historyData, addHistory } = MediaInfo({
|
||||
dbType: HistoryVideoType.AcgImage,
|
||||
});
|
||||
Pay_parent_on({ id: route.query.id as string, state, historyData, addHistory });
|
||||
provide('historyData', historyData);
|
||||
const {
|
||||
state: childState,
|
||||
autoList,
|
||||
visibleCatalogue,
|
||||
handlePopupSwitchEpisode,
|
||||
moreParams,
|
||||
sort,
|
||||
onLoad,
|
||||
onRefresh,
|
||||
visiblePayForWatch,
|
||||
validateHasBuy,
|
||||
currentItem,
|
||||
} = mediaContentInfo({ dbType: HistoryVideoType.AcgImage, isGetHistoryInit: true, isGetInfo: false });
|
||||
|
||||
Pay_child_on(childState.id, childState);
|
||||
const { id = '' } = route.query;
|
||||
|
||||
watch(historyEpisodeNumber, (num) => {
|
||||
childState.episodeNumber = num;
|
||||
});
|
||||
const currentEpisodeName = computed(() => {
|
||||
return '开始阅读';
|
||||
});
|
||||
|
||||
const EpisodeTitle = computed(() => {
|
||||
return '全' + state.info.totalEpisode + '话';
|
||||
});
|
||||
|
||||
const tagDetails = computed(() => {
|
||||
const list = state.info.tagDetails || [];
|
||||
const seenIds = new Set<string>();
|
||||
const seenNames = new Set<string>();
|
||||
return list.filter((item) => {
|
||||
if (!item) return false;
|
||||
if (seenIds.has(item.id) || seenNames.has(item.name)) return false;
|
||||
seenIds.add(item.id);
|
||||
seenNames.add(item.name);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
const goWatch = () => {
|
||||
if (validateHasBuy(currentItem.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.push({
|
||||
path: '/AcgImageDetailWatch',
|
||||
query: {
|
||||
id,
|
||||
episode: childState.episodeNumber,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
watch(dvComment, (el) => {
|
||||
if (!el) return;
|
||||
if (!id) return;
|
||||
dvComment.value[0].state.videoInfo = toRaw(state.info);
|
||||
dvComment.value[0].getCommentList();
|
||||
});
|
||||
|
||||
const beforeBuySuccess = (obj: PayInfo) => {
|
||||
if (obj.type === 'all') {
|
||||
state.info.mediaStatus.hasPaid = true;
|
||||
historyData.value.mediaStatus.hasPaid = true;
|
||||
addHistory();
|
||||
childState.list.forEach((item) => {
|
||||
item.hasBuy = true;
|
||||
});
|
||||
}
|
||||
|
||||
buySuccess(obj);
|
||||
};
|
||||
|
||||
const buySuccess = ({ type }: PayInfo) => {
|
||||
if (type === 'one') {
|
||||
const item = (childState.list as Types.Acg.MediaContentListItem[]).find(
|
||||
(item) => item.episodeNumber === currentItem.value.episodeNumber,
|
||||
);
|
||||
if (item) {
|
||||
item.hasBuy = true;
|
||||
}
|
||||
}
|
||||
|
||||
visiblePayForWatch.value = false;
|
||||
goWatch();
|
||||
};
|
||||
|
||||
let isInitCollected = false;
|
||||
watch(
|
||||
() => state.info,
|
||||
(obj) => {
|
||||
if (isInitCollected) return;
|
||||
isInitCollected = true;
|
||||
},
|
||||
);
|
||||
// 收藏
|
||||
const onToogleCollected = async () => {
|
||||
try {
|
||||
const tempIsCollect = !historyData.value.mediaStatus.hasCollected;
|
||||
handleCollected(state.info.id, tempIsCollect).then(() => {
|
||||
historyData.value.mediaStatus.hasCollected = tempIsCollect;
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
// 点赞
|
||||
const onToogleLiked = () => {
|
||||
const zan = !historyData.value.mediaStatus.hasLiked;
|
||||
|
||||
(zan ? CommunityApi.zan : CommunityApi.cancleZan)(
|
||||
zan
|
||||
? {
|
||||
type: 'image',
|
||||
objID: state.info.id,
|
||||
}
|
||||
: {
|
||||
type: 'image',
|
||||
objIDs: [state.info.id],
|
||||
},
|
||||
).then(() => {
|
||||
historyData.value.mediaStatus.hasLiked = zan;
|
||||
});
|
||||
};
|
||||
const scrollTop = ref(0);
|
||||
const onScroll = (e: Event) => {
|
||||
const currentTop = (e.target as HTMLElement).scrollTop;
|
||||
scrollTop.value = currentTop;
|
||||
};
|
||||
const headStyle = computed(() => {
|
||||
return {
|
||||
background: `rgba(0, 0, 0, ${scrollTop.value / 100})`,
|
||||
};
|
||||
});
|
||||
|
||||
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 episodeBox = ref();
|
||||
let episodeBoxLeft = 0;
|
||||
let isDeactivated = false;
|
||||
const onScrollEpisodeBox = (e: Event) => {
|
||||
if (!episodeBox.value) return;
|
||||
const scrollLeft = (e.target as HTMLElement).scrollLeft;
|
||||
episodeBoxLeft = scrollLeft;
|
||||
};
|
||||
onDeactivated(() => {
|
||||
isDeactivated = true;
|
||||
});
|
||||
onActivated(() => {
|
||||
if (episodeBox.value && episodeBoxLeft) {
|
||||
episodeBox.value.scrollLeft = episodeBoxLeft;
|
||||
}
|
||||
});
|
||||
watch(
|
||||
() => childState.episodeNumber,
|
||||
(num: number) => {
|
||||
if (isDeactivated) {
|
||||
handlePopupSwitchEpisode(num);
|
||||
isDeactivated = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
@import '../assets/style.scss';
|
||||
.detail-wrap {
|
||||
$themeBgColor2: #222324;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@include vanTab();
|
||||
|
||||
--van-tab-active-text-color: rgba(255, 255, 255, 0.9);
|
||||
.borderBottomDiv {
|
||||
margin: 0 14px;
|
||||
width: calc(100% - 28px);
|
||||
}
|
||||
.header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
// background: $themeBgColor2;
|
||||
height: 39px;
|
||||
z-index: 10;
|
||||
padding: 0 16px;
|
||||
.back {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
color: #292929;
|
||||
}
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
line-height: 24px; /* 133.333% */
|
||||
margin: 0 16px;
|
||||
}
|
||||
.bookself {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
border-radius: 11px;
|
||||
color: #ffffff99;
|
||||
.menuItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.collected {
|
||||
margin-right: 10px;
|
||||
color: #fff;
|
||||
}
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cover-box {
|
||||
width: 100%;
|
||||
height: 216px;
|
||||
padding: 11px 16px 23px;
|
||||
margin-bottom: 25px;
|
||||
position: relative;
|
||||
align-items: flex-end;
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
&::after {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, #000 100%);
|
||||
}
|
||||
}
|
||||
.content {
|
||||
z-index: 3;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
}
|
||||
.status {
|
||||
color: #e0e0e0;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
margin: 4px 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.info-box {
|
||||
width: 100%;
|
||||
border-radius: 24px 24px 0px 0px;
|
||||
background: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0) 100%);
|
||||
}
|
||||
.watch {
|
||||
padding: 16px 0 6px;
|
||||
margin-top: -50px;
|
||||
border-radius: 20px 20px 0px 0px;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
.info {
|
||||
padding: 0 10px 10px;
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 10px;
|
||||
margin: 12px 0 14px;
|
||||
color: #e0e0e0;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
.tag {
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 4px 8px;
|
||||
font-weight: 400;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
}
|
||||
.summary {
|
||||
color: #989898;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 142.857% */
|
||||
}
|
||||
.more-summary {
|
||||
color: #efefef;
|
||||
font-size: 12px;
|
||||
margin: 12px auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #fff;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
.showMoreSummary {
|
||||
svg {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
.count {
|
||||
color: #a7a7a7;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 180% */
|
||||
.count-item {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 10px 24px 16px 0;
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #fff;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 14px;
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
.count {
|
||||
font-size: 12px;
|
||||
color: #989898;
|
||||
font-weight: 400;
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.episode-box {
|
||||
margin: 9px 0 0;
|
||||
padding-left: 16px;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: column;
|
||||
.item {
|
||||
flex-shrink: 0;
|
||||
margin-right: 11px;
|
||||
}
|
||||
}
|
||||
// .btn {
|
||||
// display: flex;
|
||||
// width: 249px;
|
||||
// height: 39px;
|
||||
// justify-content: center;
|
||||
// align-items: center;
|
||||
// border-radius: 19px;
|
||||
// background: $themeBgLgColor;
|
||||
|
||||
// color: #fff;
|
||||
// font-size: 16px;
|
||||
// font-weight: 500;
|
||||
// margin: 0 auto;
|
||||
// }
|
||||
}
|
||||
|
||||
.play-list {
|
||||
@include categoryWrap();
|
||||
border: none;
|
||||
|
||||
margin-top: 0;
|
||||
padding-top: 16px;
|
||||
.title {
|
||||
padding: 0 14px;
|
||||
@include categoryTitle();
|
||||
}
|
||||
|
||||
.list {
|
||||
@include categoryList();
|
||||
padding-left: 14px;
|
||||
|
||||
.item {
|
||||
height: 118px;
|
||||
flex-shrink: 0;
|
||||
width: 97px;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 4px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: 97px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.icon {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
z-index: 1;
|
||||
width: 18px;
|
||||
height: 14px;
|
||||
}
|
||||
.count {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 97px;
|
||||
height: 26px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.play {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.title-box {
|
||||
width: 100%;
|
||||
height: 21px;
|
||||
padding: 0 5px;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
flex: 1;
|
||||
color: $textColor;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
@include textEllipsis();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.action {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 1) 0%, #000 100%);
|
||||
height: 68px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
padding: 0 16px;
|
||||
@include SafeArea('padding', 'bottom', 0);
|
||||
display: flex;
|
||||
.btn {
|
||||
width: 108px;
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
height: 44px;
|
||||
color: #989898;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
justify-content: center;
|
||||
&.go-page {
|
||||
background: $themeColor;
|
||||
width: 223px;
|
||||
color: #fff;
|
||||
}
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.play-list-box {
|
||||
$paddingBottom: 38px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 16px 16px 0px 0px;
|
||||
background: $popupBg;
|
||||
padding: 18px 16px $paddingBottom;
|
||||
@include SafeArea('padding', 'bottom', 0);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: $textColor;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
|
||||
> .title {
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
line-height: 25px; /* 140% */
|
||||
margin: 16px 0 0;
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
svg {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
margin-right: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
span {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
}
|
||||
}
|
||||
.line {
|
||||
width: 36px;
|
||||
height: 5px;
|
||||
border-radius: 100px;
|
||||
background: #989898;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.list-box {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.recommendedBox {
|
||||
padding: 0 16px;
|
||||
:deep(.van-tabs__wrap) {
|
||||
// top: 39px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,761 @@
|
||||
<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>
|
||||
@@ -0,0 +1,538 @@
|
||||
<template>
|
||||
<div class="detail-wrap overflow-y-auto" @scroll="onScroll">
|
||||
<template v-if="state.info.id">
|
||||
<div class="header flex-center-between" :class="{ header_bg: scrollTop > 100 }">
|
||||
<SvgIcon name="back1" class="back" @click="$router.go(-1)" />
|
||||
<!-- 详情 -->
|
||||
<div class="collect" :class="{ collected: state.info.mediaStatus.hasCollected }" @click="onToogleCollected">
|
||||
<SvgIcon name="acg-collect-star-text-0" class="svg-icon" v-if="!state.info.mediaStatus.hasCollected" />
|
||||
<SvgIcon name="acg-collect-star-text-1" class="svg-icon" v-else style="color: #f52c56" />
|
||||
</div>
|
||||
</div>
|
||||
<lazyImg class="detail_image_bg" :src="state.info.verticalCover || state.info.horizontalCover">
|
||||
<div class="detail_box">
|
||||
<div class="cover-box flex-vcenter">
|
||||
<lazyImg class="cover" :src="state.info.verticalCover || state.info.horizontalCover"></lazyImg>
|
||||
<div class="content">
|
||||
<h1 class="title ellipsis">{{ state.info.title }}</h1>
|
||||
<div class="actress flex-vcenter" v-if="state.info.actress">
|
||||
<lazyImg class="favicon" :src="state.info.actress.portrait"></lazyImg>
|
||||
{{ state.info.actress.name }}
|
||||
</div>
|
||||
<div class="episode">
|
||||
共{{ state.info.totalEpisode || state.info.currentEpisode }}话
|
||||
<span class="episode_line">/</span>
|
||||
<span :class="{ episode_status1: statusText == '连载中' }">{{ statusText }}</span>
|
||||
</div>
|
||||
|
||||
<div class="tags">
|
||||
<span
|
||||
class="item"
|
||||
v-for="item in state.info.tagDetails || []"
|
||||
:key="item.id"
|
||||
@click="goAcgTagPage(item.id, item.name, 'text')"
|
||||
>
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- <div class="summary ellipsis2">{{ state.info.summary }}</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</lazyImg>
|
||||
|
||||
<div class="episode-box">
|
||||
<Summary v-if="state.info.summary" class="summary" :text="state.info.summary"></Summary>
|
||||
<div class="title">
|
||||
共{{ state.info.totalEpisode }}章
|
||||
<div class="flex-vcenter" @click="visibleCatalogue = true">
|
||||
目录
|
||||
<SvgIcon class="icon" name="rightArrow"></SvgIcon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list">
|
||||
<div
|
||||
class="item flex-vcenter"
|
||||
:class="{ active: item.episodeNumber === childState.episodeNumber }"
|
||||
v-for="item in childState.list.slice(0, 4)"
|
||||
:key="item.id"
|
||||
@click="openWatch(item.episodeNumber)"
|
||||
>
|
||||
<span class="name">第{{ item.episodeNumber }}章</span>
|
||||
<SvgIcon
|
||||
name="moreIcon"
|
||||
class="back"
|
||||
:class="{ active: item.episodeNumber === childState.episodeNumber }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<RecommendList
|
||||
:media-id="state.info.id"
|
||||
:contentType="state.info.mediaType"
|
||||
title="相关推荐"
|
||||
:margin-hide="true"
|
||||
></RecommendList>
|
||||
</div>
|
||||
<!-- <AdvSwiper v-if="advList.length" class="adv" :list="advList" /> -->
|
||||
<div class="read-box">
|
||||
<div class="btn" @click="openWatch(currentEpisode)">
|
||||
{{ currentEpisodeName }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<Loading v-else />
|
||||
<van-popup v-model:show="visibleCatalogue" z-index="1000" position="bottom" round :style="{ width: '100%' }">
|
||||
<div class="play-list-box">
|
||||
<div class="line" />
|
||||
<div class="title">
|
||||
{{ state.info.title }}
|
||||
<div class="sort" @click="sort = !sort">
|
||||
<span :class="{ active: sort }">正序</span>
|
||||
/
|
||||
<span :class="{ active: !sort }">倒序</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list overflow-y-auto">
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
|
||||
<div
|
||||
:id="`section_${item.episodeNumber}`"
|
||||
class="item flex-vcenter"
|
||||
:class="{ active: item.episodeNumber === childState.episodeNumber }"
|
||||
v-for="item in autoList"
|
||||
:key="item.id"
|
||||
@click="openWatch(item.episodeNumber)"
|
||||
>
|
||||
<span class="name">第{{ item.episodeNumber }}章</span>
|
||||
<SvgIcon name="moreIcon" class="back" />
|
||||
</div>
|
||||
</PullRefresh>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
<PayForWatch
|
||||
v-if="childState.validateItem && visiblePayForWatch"
|
||||
ref="dvPayForWatch"
|
||||
:isOpen="visiblePayForWatch"
|
||||
:info="childState.validateItem"
|
||||
@close="visiblePayForWatch = false"
|
||||
@buySuccess="beforeBuySuccess"
|
||||
></PayForWatch>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AcgTextDetail',
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, provide, watch } from 'vue';
|
||||
import { watchCount, dateFormatYyyyMmDd } from '@/utils/index';
|
||||
import MediaInfo from '../utils/mediaInfo';
|
||||
// import { AdType } from '@/enums/app';
|
||||
// import { getAdItem } from '@/utils/getConfig';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { handleCollected, Collected_emit } from '../utils/collect';
|
||||
import RecommendList from '../components/recommendList.vue';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import mediaContentInfo from '../utils/mediaContentInfo';
|
||||
import PayForWatch from '../components/pay/PayForWatch.vue';
|
||||
import { Pay_parent_on, Pay_child_on, Pay_emit } from '../utils/pay-mitt';
|
||||
import type { PayInfo } from '../utils/pay-mitt';
|
||||
import { goAcgTagPage } from '../utils/common';
|
||||
import Summary from '../components/summary.vue';
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// const advList = ref(getAdItem(AdType.VIDEOSWIPER));
|
||||
|
||||
const { state, historyEpisodeNumber, historyData, addHistory, statusText } = MediaInfo({
|
||||
dbType: HistoryVideoType.AcgText,
|
||||
});
|
||||
Pay_parent_on({ id: route.query.id as string, state, historyData, addHistory });
|
||||
provide('historyData', historyData);
|
||||
const {
|
||||
state: childState,
|
||||
autoList,
|
||||
visibleCatalogue,
|
||||
moreParams,
|
||||
sort,
|
||||
onLoad,
|
||||
onRefresh,
|
||||
visiblePayForWatch,
|
||||
validateHasBuy,
|
||||
} = mediaContentInfo({ dbType: HistoryVideoType.AcgText, isGetHistoryInit: true, isGetInfo: false });
|
||||
|
||||
Pay_child_on(childState.id, childState);
|
||||
|
||||
const currentEpisode = computed(() => {
|
||||
if (historyEpisodeNumber.value === 0) return 0;
|
||||
return historyEpisodeNumber.value;
|
||||
});
|
||||
watch(currentEpisode, () => {
|
||||
childState.episodeNumber = currentEpisode.value;
|
||||
});
|
||||
const currentEpisodeName = computed(() => {
|
||||
if (currentEpisode.value === 0) return '开始阅读';
|
||||
let prefix = '';
|
||||
|
||||
if (state.episodeNumber > historyEpisodeNumber.value) {
|
||||
prefix = '观看';
|
||||
} else {
|
||||
prefix = '续看';
|
||||
}
|
||||
return prefix + currentEpisode.value.toString().padStart(3, '0') + '话';
|
||||
});
|
||||
// 收藏
|
||||
const onToogleCollected = async () => {
|
||||
try {
|
||||
const {
|
||||
id,
|
||||
mediaStatus: { hasCollected },
|
||||
} = state.info;
|
||||
const tempIsCollect = !hasCollected;
|
||||
handleCollected(id, tempIsCollect).then(() => {
|
||||
state.info.mediaStatus.hasCollected = tempIsCollect;
|
||||
Collected_emit(id, tempIsCollect);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
let tempEpisodeNumber = 0;
|
||||
const openWatch = (num: number) => {
|
||||
num = num === 0 ? 1 : num;
|
||||
tempEpisodeNumber = num;
|
||||
const item = childState.list.find((item) => item.episodeNumber === num);
|
||||
if (!item) {
|
||||
goWatch(num);
|
||||
return;
|
||||
}
|
||||
if (validateHasBuy(item)) return;
|
||||
goWatch(num);
|
||||
};
|
||||
|
||||
const goWatch = (num = 0) => {
|
||||
// mediaSubType: 0 | 1; //0:文本 1:有声
|
||||
const { mediaSubType } = state.info;
|
||||
router.push({
|
||||
name: `AcgTextDetailWatch${mediaSubType}`,
|
||||
query: {
|
||||
id: route.query.id,
|
||||
episodeNumber: num || historyEpisodeNumber.value || 1,
|
||||
title: state.info.title,
|
||||
},
|
||||
});
|
||||
|
||||
visibleCatalogue.value = false;
|
||||
};
|
||||
|
||||
const beforeBuySuccess = (obj: PayInfo) => {
|
||||
Pay_emit(historyData.value.id, obj);
|
||||
if (obj.type === 'all') {
|
||||
state.info.mediaStatus.hasPaid = true;
|
||||
historyData.value.mediaStatus.hasPaid = true;
|
||||
addHistory();
|
||||
childState.list.forEach((item) => {
|
||||
item.hasBuy = true;
|
||||
});
|
||||
}
|
||||
|
||||
buySuccess(obj);
|
||||
};
|
||||
const buySuccess = ({ type }: PayInfo) => {
|
||||
if (type === 'one') {
|
||||
const item = (childState.list as Types.Acg.MediaContentListItem[]).find(
|
||||
(item) => item.episodeNumber === tempEpisodeNumber,
|
||||
);
|
||||
if (item) {
|
||||
item.hasBuy = true;
|
||||
}
|
||||
}
|
||||
|
||||
visiblePayForWatch.value = false;
|
||||
goWatch(tempEpisodeNumber);
|
||||
};
|
||||
|
||||
const scrollTop = ref(0);
|
||||
const onScroll = (e: Event) => {
|
||||
scrollTop.value = (e.target as HTMLElement).scrollTop;
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
@import '../assets/style.scss';
|
||||
.detail-wrap {
|
||||
height: 100%;
|
||||
// background: url('@/assets/images/png/acg/text-bg.png') #11111a no-repeat;
|
||||
background: #000;
|
||||
.header {
|
||||
padding: 0 16px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
z-index: 10;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
.back {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
color: #fff;
|
||||
}
|
||||
.collect {
|
||||
svg {
|
||||
width: 62px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.header_bg {
|
||||
background: #000;
|
||||
}
|
||||
.detail_image_bg {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
height: 280px;
|
||||
overflow: hidden;
|
||||
:deep(img) {
|
||||
object-position: top;
|
||||
filter: brightness(0.5);
|
||||
}
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.45) 0%, rgba(0, 0, 0, 0.78) 65%, #000 100%);
|
||||
}
|
||||
.detail_box {
|
||||
padding: 60px 16px 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
.cover-box {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
.cover {
|
||||
flex-shrink: 0;
|
||||
width: 111px;
|
||||
height: 146px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
}
|
||||
.actress {
|
||||
margin: 6px 0;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
.favicon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.episode {
|
||||
color: #989898;
|
||||
font-size: 12px;
|
||||
line-height: 18px; /* 150% */
|
||||
margin-top: 12px;
|
||||
.episode_status1 {
|
||||
color: $themeColor;
|
||||
}
|
||||
.episode_line {
|
||||
margin: 0 8px 0 6px;
|
||||
}
|
||||
}
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 10px;
|
||||
margin: 10px 0 12px;
|
||||
.item {
|
||||
flex-shrink: 0;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
background: #22252d;
|
||||
color: #fff;
|
||||
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.adv {
|
||||
height: 75px;
|
||||
:deep(.van-swipe-item) {
|
||||
height: 75px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.read-box {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 68px;
|
||||
//background: linear-gradient(180deg, rgba(34, 35, 50, 0) 0%, #222332 100%);
|
||||
@include SafeArea('padding', 'bottom', 21px);
|
||||
.btn {
|
||||
margin: 0 auto;
|
||||
width: 343px;
|
||||
padding: 11px 0;
|
||||
border-radius: 3px;
|
||||
background: #f68804;
|
||||
color: var(---f, #fff);
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
.episode-box {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
padding: 0 16px;
|
||||
margin-top: -72px;
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
> div {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
> svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.item {
|
||||
padding: 12px 0;
|
||||
justify-content: space-between;
|
||||
color: #989898;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
&.active {
|
||||
color: #f68804;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
flex: 1;
|
||||
@include textEllipsis();
|
||||
}
|
||||
.back {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
// transform: rotate(180deg);
|
||||
flex-shrink: 1;
|
||||
color: #fff;
|
||||
&.active {
|
||||
color: #f68804;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.play-list-box {
|
||||
width: 375px;
|
||||
height: 400px;
|
||||
border-radius: 18px 18px 0px 0px;
|
||||
background: #0f0f0f;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
padding: 13px 16px 0;
|
||||
@include SafeArea('padding', 'bottom', 18px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.line {
|
||||
width: 36px;
|
||||
height: 5px;
|
||||
border-radius: 99px;
|
||||
background: #989898;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.sort {
|
||||
color: #747b80;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
.active {
|
||||
color: #f68804;
|
||||
}
|
||||
}
|
||||
}
|
||||
.list {
|
||||
flex: 1;
|
||||
.item {
|
||||
padding: 18px 0;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
&.active {
|
||||
color: #f68804;
|
||||
}
|
||||
.name {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
flex: 1;
|
||||
@include textEllipsis();
|
||||
}
|
||||
.back {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,358 @@
|
||||
<template>
|
||||
<div class="text-wrap" :class="theme">
|
||||
<Transition name="top">
|
||||
<Header v-show="visibleAction" class="header" :title="currentItem.id ? currentItem.name : ''">
|
||||
<template v-slot:right>
|
||||
<div class="right">
|
||||
<SvgIcon v-if="theme === ThemeMode.dark" name="theme-mode-light" @click="switchTheme" />
|
||||
<SvgIcon v-else name="theme-mode-dark" @click="switchTheme" />
|
||||
<SvgIcon name="acg_catalogue" @click="visibleCatalogue = true" style="margin-left: 12px" />
|
||||
</div>
|
||||
</template>
|
||||
</Header>
|
||||
</Transition>
|
||||
<Transition name="top">
|
||||
<Header
|
||||
v-show="!visibleAction"
|
||||
class="header header2"
|
||||
leftIcon=""
|
||||
:title="currentItem.id ? `${state.title} / ${currentItem.name}` : ''"
|
||||
/>
|
||||
</Transition>
|
||||
<template v-if="state.info.id">
|
||||
<div
|
||||
ref="rootEl"
|
||||
class="content overflow-y-auto"
|
||||
v-html="autoText"
|
||||
@scroll="onScroll"
|
||||
@click="visibleAction = !visibleAction"
|
||||
></div>
|
||||
<Transition name="bottom">
|
||||
<div v-show="visibleAction" class="action">
|
||||
<div class="prev" @click="switchEpisodeNumber(-1)">
|
||||
<SvgIcon class="icon" name="acg_text_prev" />
|
||||
上一章
|
||||
</div>
|
||||
<div class="next" @click="switchEpisodeNumber(1)">
|
||||
下一章
|
||||
<SvgIcon class="icon" name="acg_text_prev" />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
<Loading v-else></Loading>
|
||||
</div>
|
||||
<van-popup
|
||||
v-model:show="visibleCatalogue"
|
||||
z-index="1000"
|
||||
position="right"
|
||||
:style="{ height: '100%', width: '210px' }"
|
||||
>
|
||||
<div class="play-list-box">
|
||||
<div class="title">{{ state.title }}</div>
|
||||
<div class="sort">
|
||||
<span>共{{ historyData.totalEpisode }}章</span>
|
||||
<span class="status">{{ statusText }}</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 class="list overflow-y-auto">
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
|
||||
<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>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AcgTextDetailWatch0',
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, provide, watch } from 'vue';
|
||||
import mediaContentInfo from '../utils/mediaContentInfo';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import PayForWatch from '../components/pay/PayForWatch.vue';
|
||||
import { Pay_emit } from '../utils/pay-mitt';
|
||||
import type { PayInfo } from '../utils/pay-mitt';
|
||||
enum ThemeMode {
|
||||
light = 'light',
|
||||
dark = 'dark',
|
||||
}
|
||||
const theme = ref(localStorage.getItem('theme') || ThemeMode.light);
|
||||
|
||||
const {
|
||||
state,
|
||||
statusText,
|
||||
moreParams,
|
||||
onLoad,
|
||||
onRefresh,
|
||||
sort,
|
||||
autoList,
|
||||
visibleCatalogue,
|
||||
historyData,
|
||||
visiblePayForWatch,
|
||||
closePayForWatch,
|
||||
buySuccess,
|
||||
switchEpisodeNumberCatalogue,
|
||||
switchEpisodeNumber,
|
||||
currentItem,
|
||||
addHistoryReadData,
|
||||
} = mediaContentInfo({ dbType: HistoryVideoType.AcgText });
|
||||
provide('historyData', historyData);
|
||||
const autoText = computed(() => {
|
||||
return (state.info.text || '').replace(/\n/g, '<br>');
|
||||
});
|
||||
const rootEl = ref();
|
||||
watch(currentItem, () => {
|
||||
if (!rootEl.value) return;
|
||||
const list = historyData.value.historyReadData || [];
|
||||
const item = list.find((i) => i.episodeNumber === state.episodeNumber) || {};
|
||||
const progress = Number(item.progress);
|
||||
rootEl.value.scrollTop = item.scrollTop || 0;
|
||||
addHistoryReadData(state.episodeNumber, {
|
||||
progress: Number.isFinite(progress) ? progress : 0,
|
||||
scrollTop: item.scrollTop || 0,
|
||||
});
|
||||
});
|
||||
|
||||
const visibleAction = ref(true);
|
||||
let scrollTop = 0;
|
||||
const onScroll = (e: Event) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const currentTop = target.scrollTop;
|
||||
const maxScrollTop = target.scrollHeight - target.offsetHeight;
|
||||
const progress = maxScrollTop <= 0 ? 100 : Math.max(1, Math.min(100, Math.ceil((currentTop / maxScrollTop) * 100)));
|
||||
addHistoryReadData(state.episodeNumber, {
|
||||
progress,
|
||||
scrollTop: currentTop,
|
||||
});
|
||||
|
||||
if (currentTop - scrollTop > 20) {
|
||||
visibleAction.value = false;
|
||||
scrollTop = currentTop;
|
||||
return;
|
||||
}
|
||||
if (currentTop - scrollTop < -20) {
|
||||
visibleAction.value = true;
|
||||
scrollTop = currentTop;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const switchTheme = () => {
|
||||
theme.value = theme.value === ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
|
||||
};
|
||||
const beforeBuySuccess = (obj: PayInfo) => {
|
||||
Pay_emit(historyData.value.id, obj);
|
||||
buySuccess(obj.type);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
.text-wrap {
|
||||
padding: 18px 0;
|
||||
&.light {
|
||||
--bg-color: #fff;
|
||||
--icon-color: #000;
|
||||
--text-header-color: rgba(0, 0, 0, 0.9);
|
||||
--text-color: #333;
|
||||
}
|
||||
&.dark {
|
||||
--bg-color: #030f18;
|
||||
--icon-color: #fff;
|
||||
--text-header-color: #fff;
|
||||
--text-color: #ffffffe5;
|
||||
}
|
||||
height: 100%;
|
||||
background-color: var(--bg-color);
|
||||
.header {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
background-color: var(--bg-color) !important;
|
||||
color: var(--text-header-color);
|
||||
:deep(.svg-icon) {
|
||||
color: var(--icon-color);
|
||||
}
|
||||
:deep(.title) {
|
||||
color: var(--icon-color);
|
||||
left: 44px;
|
||||
transform: none;
|
||||
}
|
||||
.right {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 10px;
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.header2 {
|
||||
:deep(.title) {
|
||||
color: #525252;
|
||||
left: 16px;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
height: 100%;
|
||||
padding: 44px 12px 110px;
|
||||
color: var(--text-color);
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
.action {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
width: 375px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 10px;
|
||||
@include SafeArea('padding', 'bottom', 12px);
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-header-color);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
> div {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-header-color);
|
||||
font-size: 16px;
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&.next .icon {
|
||||
transform: rotate(180deg);
|
||||
margin-right: 0;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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(100%);
|
||||
}
|
||||
|
||||
.play-list-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #0f0f0f;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
|
||||
.title {
|
||||
flex-shrink: 0;
|
||||
width: inherit;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.sort {
|
||||
flex-shrink: 0;
|
||||
width: inherit;
|
||||
height: 24px;
|
||||
margin: 0 0 12px;
|
||||
padding: 2px 16px;
|
||||
background: #f68804;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
.status {
|
||||
flex: 1;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.section {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
flex: 1;
|
||||
.item {
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
padding: 0 16px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
@include textEllipsis();
|
||||
&.active {
|
||||
color: #f68804;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,626 @@
|
||||
<template>
|
||||
<div class="audio-wrap overflow-y-auto">
|
||||
<div class="page-nav" v-if="state.info.id">
|
||||
<SvgIcon name="back_white" class="back" @click="$router.go(-1)" />
|
||||
{{ state.title }}
|
||||
<span></span>
|
||||
</div>
|
||||
<div v-else class="page-nav">
|
||||
<SvgIcon name="back_white" class="back" @click="$router.go(-1)" />
|
||||
</div>
|
||||
<div class="content-wrap">
|
||||
<template v-if="state.info.id">
|
||||
<div class="cd flex-center-center">
|
||||
<audio
|
||||
ref="audio"
|
||||
@timeupdate="onTimeUpdate"
|
||||
@loadedmetadata="onLoadedmetadata"
|
||||
@ended="onEnded"
|
||||
style="display: none"
|
||||
></audio>
|
||||
<lazyImg class="cover" :src="currentItem.cover || historyData.verticalCover" />
|
||||
</div>
|
||||
<div class="content">正在播放:{{ state.info.name }}</div>
|
||||
<div class="bar flex-center-between" @touchstart.stop @touchmove.stop @touchend.stop>
|
||||
<SvgIcon class="icon" name="acg_audio_time_seek-10" @click="upadteProgress(-10)"></SvgIcon>
|
||||
<van-slider
|
||||
class="progress"
|
||||
v-model="currentProgress"
|
||||
@change="onChange"
|
||||
@drag-start="onDragStart"
|
||||
@drag-end="onDragEnd"
|
||||
/>
|
||||
<SvgIcon class="icon" name="acg_audio_time_seek+10" @click="upadteProgress(10)"></SvgIcon>
|
||||
</div>
|
||||
<div class="time-box flex-center-between">
|
||||
<span>{{ currentPlayTime }}</span>
|
||||
<span>{{ duration }}</span>
|
||||
</div>
|
||||
<div class="action">
|
||||
<div class="item">
|
||||
<SvgIcon :name="playMode" class="mode" @click="onChangePlayMode" />
|
||||
{{ playModeText }}
|
||||
</div>
|
||||
<SvgIcon name="acg_audio_action_prev" class="prev" @click="switchEpisodeNumber(-1)" />
|
||||
<SvgIcon v-show="!isPlay" name="acg_audio_action_play" class="play" @click="isPlay = true" />
|
||||
<SvgIcon v-show="isPlay" name="acg_audio_pause" class="play" @click="isPlay = false" />
|
||||
<SvgIcon name="acg_audio_action_prev" class="prev next" @click="switchEpisodeNumber(1)" />
|
||||
<div class="item">
|
||||
<SvgIcon name="acg_catalogue" class="mode" @click="visibleCatalogue = true" />
|
||||
播放列表
|
||||
</div>
|
||||
</div>
|
||||
<div class="split"></div>
|
||||
<RecommendList
|
||||
v-if="mediaId"
|
||||
title="相关推荐"
|
||||
:media-id="mediaId"
|
||||
:contentType="'text'"
|
||||
:marginHide="true"
|
||||
></RecommendList>
|
||||
</template>
|
||||
<Loading v-else />
|
||||
</div>
|
||||
</div>
|
||||
<van-popup v-model:show="visibleCatalogue" z-index="1000" position="bottom" round :style="{ width: '100%' }">
|
||||
<div class="play-list-box">
|
||||
<div class="line" />
|
||||
<div class="title">
|
||||
{{ state.title }}
|
||||
|
||||
<div class="sort" @click="sort = !sort">
|
||||
<span :class="{ active: sort }">正序</span>
|
||||
/
|
||||
<span :class="{ active: !sort }">倒序</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list overflow-y-auto">
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
|
||||
<div
|
||||
:id="`section_${item.episodeNumber}`"
|
||||
class="item flex-vcenter"
|
||||
:class="{ active: item.episodeNumber === state.episodeNumber }"
|
||||
v-for="item in autoList"
|
||||
:key="item.id"
|
||||
@click="switchEpisodeNumberCatalogue(item)"
|
||||
>
|
||||
<span class="name">第{{ item.episodeNumber }}章</span>
|
||||
<!-- <SvgIcon
|
||||
v-if="item.episodeNumber === state.episodeNumber"
|
||||
name="acg_audio_episode_active"
|
||||
class="episode_active"
|
||||
/> -->
|
||||
<SvgIcon name="moreIcon" class="back" />
|
||||
</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="visiblePayForWatch = false"
|
||||
@buySuccess="beforeBuySuccess"
|
||||
></PayForWatch>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AcgTextDetailWatch1',
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed, nextTick, onUnmounted, provide, onActivated, onDeactivated } from 'vue';
|
||||
// import PopupList from './components/popup-list.vue';
|
||||
import { videoTime } from '@/utils/index';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
// import PayForWatch from '../components/PayForWatch.vue';
|
||||
import mediaContentInfo from '../utils/mediaContentInfo';
|
||||
import { showToast } from 'vant';
|
||||
import { useRoute } from 'vue-router';
|
||||
import RecommendList from '../components/recommendList.vue';
|
||||
import PayForWatch from '../components/pay/PayForWatch.vue';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import { handleCollected } from '../utils/collect';
|
||||
import { Pay_emit, Pay_child_on } from '../utils/pay-mitt';
|
||||
import type { PayInfo } from '../utils/pay-mitt';
|
||||
|
||||
const route = useRoute();
|
||||
const mediaId = (route.query.id || '') as string;
|
||||
|
||||
const {
|
||||
state,
|
||||
currentItem,
|
||||
autoList,
|
||||
visibleCatalogue,
|
||||
moreParams,
|
||||
onLoad,
|
||||
onRefresh,
|
||||
visiblePayForWatch,
|
||||
buySuccess,
|
||||
switchEpisodeNumberCatalogue,
|
||||
switchEpisodeNumber,
|
||||
validateHasBuy,
|
||||
historyData,
|
||||
addHistory,
|
||||
addHistoryReadData,
|
||||
sort,
|
||||
} = mediaContentInfo({ dbType: HistoryVideoType.AcgText, isGetHistoryInit: true });
|
||||
provide('historyData', historyData);
|
||||
|
||||
Pay_child_on(state.id, state);
|
||||
|
||||
const isPlay = ref(false);
|
||||
const audio = ref();
|
||||
const currentProgress = ref(0);
|
||||
const audioTotalTime = ref(0);
|
||||
const audioCurrentTime = ref(0);
|
||||
const initPlay = ref(false);
|
||||
|
||||
enum PlayMode {
|
||||
listCycle = 'acg_audio_action_mode1',
|
||||
singleCycle = 'acg_audio_action_mode2',
|
||||
}
|
||||
|
||||
enum PlayModeText {
|
||||
listCycle = '多集循环',
|
||||
singleCycle = '单集循环',
|
||||
}
|
||||
|
||||
const playMode = ref(localStorage.getItem('PlayMode') || PlayMode.listCycle);
|
||||
const playModeText = computed(() => {
|
||||
if (playMode.value === PlayMode.listCycle) {
|
||||
return PlayModeText.listCycle;
|
||||
} else {
|
||||
return PlayModeText.singleCycle;
|
||||
}
|
||||
});
|
||||
const onChangePlayMode = () => {
|
||||
if (playMode.value === PlayMode.listCycle) {
|
||||
showToast(PlayModeText.singleCycle);
|
||||
playMode.value = PlayMode.singleCycle;
|
||||
} else {
|
||||
showToast(PlayModeText.listCycle);
|
||||
playMode.value = PlayMode.listCycle;
|
||||
}
|
||||
};
|
||||
|
||||
const currentUrl = ref('');
|
||||
watch(
|
||||
() => state.info.audioUrl,
|
||||
(url) => {
|
||||
currentUrl.value = url;
|
||||
},
|
||||
);
|
||||
watch(currentUrl, () => {
|
||||
if (!currentUrl.value) return '';
|
||||
|
||||
// const url = `${baseUrl + currentUrl.value}?token=${useUserStore().token}&c=${globalStore.videoRoadLine.url}`;
|
||||
const url = useConfigStore().sourceInfo.AUDIO + '/' + currentUrl.value;
|
||||
nextTick(() => {
|
||||
audio.value.src = url;
|
||||
});
|
||||
});
|
||||
|
||||
watch(isPlay, (bool) => {
|
||||
if (bool && !initPlay.value) {
|
||||
initPlay.value = true;
|
||||
}
|
||||
if (bool) audio.value.play();
|
||||
else audio.value.pause();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => state.episodeNumber,
|
||||
() => {
|
||||
currentProgress.value = 0;
|
||||
audioCurrentTime.value = 0;
|
||||
|
||||
if (audio.value) {
|
||||
audio.value.currentTime = 0;
|
||||
}
|
||||
|
||||
addHistoryReadData(state.episodeNumber, {
|
||||
progress: 0,
|
||||
currentTime: 0,
|
||||
});
|
||||
addHistory(state.episodeNumber);
|
||||
},
|
||||
);
|
||||
|
||||
const onLoadedmetadata = () => {
|
||||
if (audio.value === null) return;
|
||||
audioTotalTime.value = audio.value.duration;
|
||||
const list = historyData.value.historyReadData || [];
|
||||
const item = list.find((i) => i.episodeNumber === state.episodeNumber) || {};
|
||||
const savedProgress = Number(item.progress || 0);
|
||||
if (audioTotalTime.value > 0 && savedProgress > 0 && savedProgress < 100) {
|
||||
const restoreTime = (savedProgress / 100) * audioTotalTime.value;
|
||||
audio.value.currentTime = restoreTime;
|
||||
audioCurrentTime.value = restoreTime;
|
||||
currentProgress.value = savedProgress;
|
||||
} else if (savedProgress >= 100) {
|
||||
currentProgress.value = 0;
|
||||
audioCurrentTime.value = 0;
|
||||
} else {
|
||||
currentProgress.value = 0;
|
||||
audioCurrentTime.value = 0;
|
||||
}
|
||||
try {
|
||||
audio.value.play();
|
||||
isPlay.value = !audio.value.paused;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
addHistory(state.episodeNumber);
|
||||
};
|
||||
|
||||
const currentPlayTime = computed(() => {
|
||||
return videoTime(parseInt(audioCurrentTime.value.toString()));
|
||||
});
|
||||
const duration = computed(() => {
|
||||
return videoTime(parseInt(audioTotalTime.value.toString()));
|
||||
});
|
||||
|
||||
let isDrag = false;
|
||||
const onDragStart = () => {
|
||||
isDrag = true;
|
||||
};
|
||||
const onDragEnd = () => {
|
||||
isDrag = false;
|
||||
};
|
||||
const onTimeUpdate = () => {
|
||||
if (audio.value === null) return;
|
||||
if (visiblePayForWatch.value) {
|
||||
audio.value.pause();
|
||||
isPlay.value = false;
|
||||
return;
|
||||
}
|
||||
if (validateHasBuy(currentItem.value)) {
|
||||
audio.value.pause();
|
||||
isPlay.value = false;
|
||||
}
|
||||
const { currentTime } = audio.value;
|
||||
const duration = Number(audio.value.duration || 0);
|
||||
if (duration <= 0) return;
|
||||
const progress = Math.max(0, Math.min(100, (currentTime / duration) * 100));
|
||||
if (!isDrag) {
|
||||
currentProgress.value = progress;
|
||||
}
|
||||
audioCurrentTime.value = currentTime;
|
||||
addHistoryReadData(state.episodeNumber, {
|
||||
progress: Math.round(progress),
|
||||
currentTime,
|
||||
});
|
||||
};
|
||||
|
||||
const onEnded = async () => {
|
||||
addHistoryReadData(state.episodeNumber, {
|
||||
progress: 100,
|
||||
currentTime: audio.value?.duration || audioCurrentTime.value,
|
||||
});
|
||||
if (playMode.value === PlayMode.singleCycle) {
|
||||
currentProgress.value = 0;
|
||||
audio.value.play();
|
||||
} else {
|
||||
const bool = await switchEpisodeNumber(1);
|
||||
if (bool) {
|
||||
currentProgress.value = 0;
|
||||
audio.value.play();
|
||||
} else {
|
||||
isPlay.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (progress: number) => {
|
||||
const currentTime = (progress * audio.value.duration) / 100;
|
||||
audio.value.currentTime = currentTime;
|
||||
};
|
||||
|
||||
const upadteProgress = (num: number) => {
|
||||
if (!isPlay.value) return;
|
||||
const currentTime = audio.value.currentTime + num;
|
||||
audio.value.currentTime = currentTime;
|
||||
};
|
||||
|
||||
const beforeBuySuccess = (obj: PayInfo) => {
|
||||
Pay_emit(historyData.value.id, obj);
|
||||
buySuccess(obj.type);
|
||||
if (obj.type === 'all') {
|
||||
historyData.value.mediaStatus.hasPaid = true;
|
||||
addHistory(state.episodeNumber);
|
||||
}
|
||||
};
|
||||
|
||||
// 收藏
|
||||
// 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);
|
||||
// }
|
||||
// };
|
||||
|
||||
onUnmounted(() => {
|
||||
localStorage.setItem('PlayMode', playMode.value);
|
||||
});
|
||||
onActivated(() => {
|
||||
const episodeNumber = Number((route.query.episodeNumber || '0') as string);
|
||||
if (episodeNumber && episodeNumber != state.episodeNumber) {
|
||||
state.episodeNumber = episodeNumber;
|
||||
} else {
|
||||
if (audio.value) audio.value.play();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0% {
|
||||
transform-origin: top center;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform-origin: top center;
|
||||
transform: rotate(-10deg);
|
||||
}
|
||||
100% {
|
||||
transform-origin: top center;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
.audio-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
--van-slider-active-background: #e1351f;
|
||||
--van-slider-inactive-background: #3d3d3d;
|
||||
--van-slider-button-width: 12px;
|
||||
--van-slider-button-height: 12px;
|
||||
--van-slider-button-shadow: 0 0 0 4px rgb(245, 44, 86, 61%);
|
||||
.page-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// justify-content: space-between;
|
||||
height: 44px;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
padding: 0 16px;
|
||||
font-weight: 600;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
|
||||
background: #000;
|
||||
|
||||
.back {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.more {
|
||||
width: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.content-wrap {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 0 16px;
|
||||
height: 100%;
|
||||
@include SafeArea('padding', 'bottom', 44px);
|
||||
.cd {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
height: 188px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
margin-top: 28px;
|
||||
position: relative;
|
||||
.cover {
|
||||
width: 111px;
|
||||
height: 148px;
|
||||
border-radius: 9px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
$margin: 0px;
|
||||
.content {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 18px $margin;
|
||||
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
.bar {
|
||||
flex-shrink: 0;
|
||||
width: calc(100% - $margin * 2);
|
||||
height: 24px;
|
||||
margin: 0 $margin 10px;
|
||||
position: relative;
|
||||
|
||||
.progress {
|
||||
width: 251px;
|
||||
height: 2px;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
:deep(.van-slider__bar) {
|
||||
background: #f68804;
|
||||
}
|
||||
:deep(.van-slider__button) {
|
||||
box-shadow: 0 0 0 3px rgba(246, 136, 4, 0.7);
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.time {
|
||||
flex-shrink: 0;
|
||||
width: 50px;
|
||||
color: #fff;
|
||||
font-size: 9px;
|
||||
|
||||
margin-right: 0;
|
||||
margin-left: 6px;
|
||||
text-align: right;
|
||||
|
||||
&.current {
|
||||
margin-right: 6px;
|
||||
margin-left: 0;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
.time-box {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin: 10px 0 18px;
|
||||
}
|
||||
.action {
|
||||
flex-shrink: 0;
|
||||
width: calc(100% - $margin * 2);
|
||||
height: 58px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 0 auto;
|
||||
.item {
|
||||
// width: 68px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
.mode {
|
||||
width: 29px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.prev,
|
||||
.next {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
|
||||
&.next {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
.play {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.split {
|
||||
display: flex;
|
||||
width: 363px;
|
||||
height: 1px;
|
||||
margin: 18px auto;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.play-list-box {
|
||||
width: 375px;
|
||||
height: 400px;
|
||||
border-radius: 18px 18px 0px 0px;
|
||||
background: #0f0f0f;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
padding: 13px 16px 0;
|
||||
@include SafeArea('padding', 'bottom', 18px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.line {
|
||||
width: 36px;
|
||||
height: 5px;
|
||||
border-radius: 99px;
|
||||
background: #989898;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.sort {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
.active {
|
||||
color: #f68804;
|
||||
}
|
||||
}
|
||||
}
|
||||
.list {
|
||||
flex: 1;
|
||||
.item {
|
||||
padding: 18px 0;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #ffffff1a;
|
||||
&.active {
|
||||
color: #f68804;
|
||||
}
|
||||
.name {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
flex: 1;
|
||||
@include textEllipsis();
|
||||
}
|
||||
.back {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,411 @@
|
||||
<template>
|
||||
<div class="introduction-wrap overflow-y-auto">
|
||||
<div class="titleBox ellipsis2">
|
||||
{{ props.info.title }}
|
||||
</div>
|
||||
<div class="tags">
|
||||
<span
|
||||
class="tag"
|
||||
@click="goAcgTagPage(item.id, item.name, 'video')"
|
||||
v-for="item in info.tagDetails || []"
|
||||
:key="item.id"
|
||||
>
|
||||
#{{ item.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="videoInfoBox flex-vcenter">
|
||||
<div class="item margin-r-88">
|
||||
<span>{{ watchCount(props.info.countBrowse) }} 观看量</span>
|
||||
</div>
|
||||
<div class="item" @click="onToogleLiked">
|
||||
<SvgIcon v-if="props.info.mediaStatus.hasLiked" name="acg-like-1" />
|
||||
<SvgIcon v-else name="acg-like-0" />
|
||||
<span>{{ watchCount(props.info.countLike) || '点赞' }}</span>
|
||||
</div>
|
||||
<div class="item" @click="onToogleCollected">
|
||||
<SvgIcon name="acg-collect-star-1" v-if="props.info.mediaStatus.hasCollected" style="color: #f52c56"></SvgIcon>
|
||||
<SvgIcon name="acg-collect-star-0" v-else></SvgIcon>
|
||||
<span>{{ watchCount(props.info.countCollect) || '收藏' }}</span>
|
||||
</div>
|
||||
<div class="item" @click="openShare">
|
||||
<SvgIcon name="acg-share" />
|
||||
<span>分享</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="actress-box flex-center-between"
|
||||
v-if="props.info.actress && props.info.actress.id"
|
||||
@click="
|
||||
$router.push({
|
||||
name: 'AcgActress',
|
||||
query: {
|
||||
id: props.info.actress.id,
|
||||
},
|
||||
})
|
||||
"
|
||||
>
|
||||
<lazyImg class="portrait" :src="props.info.actress.portrait"></lazyImg>
|
||||
<div class="info">
|
||||
<p class="name">{{ props.info.actress.name }}</p>
|
||||
<div class="count-box flex-vcenter">
|
||||
<SvgIcon name="acg-video-icon-a-video" />
|
||||
{{ watchCount(props.info.actress.acgCount) }}部
|
||||
<SvgIcon name="acg-video-icon-a-collect" style="margin-left: 12px" />
|
||||
{{ watchCount(props.info.actress.collectCount) }}
|
||||
</div>
|
||||
</div>
|
||||
<SvgIcon class="icon" name="moreIcon" />
|
||||
</div>
|
||||
<!-- 选集区域 -->
|
||||
<div class="episodeArea">
|
||||
<div class="episodeHeader flex-center-between padding16">
|
||||
<div class="episodeTitle">选集</div>
|
||||
<div class="episodeAll flex-vcenter" @click="visibleCatalogue = true">
|
||||
全{{ props.info.totalEpisode }}话
|
||||
<SvgIcon class="icon" name="moreIcon" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="episodeBox" @touchstart.stop @touchmove.stop @touchend.stop>
|
||||
<Episode
|
||||
class="item"
|
||||
@click="switchEpisodeNumberCatalogue(item)"
|
||||
v-for="item in autoList"
|
||||
:key="item.id"
|
||||
:info="item"
|
||||
:episode-number="state.episodeNumber"
|
||||
></Episode>
|
||||
</div>
|
||||
</div>
|
||||
<HomeHeader :advType="AdType.VIDEOADV" />
|
||||
<!-- <RecommendList
|
||||
:media-id="props.info.id"
|
||||
:contentType="props.info.mediaType"
|
||||
title="猜你喜欢"
|
||||
:marginHide="true"
|
||||
></RecommendList> -->
|
||||
<RecommendedList :type="info.mediaType" :tagId="info.tagDetails?.[0]?.id" />
|
||||
<!-- 分享弹窗 -->
|
||||
<AppSharePopup
|
||||
:is-open="visibleShare"
|
||||
@close="visibleShare = false"
|
||||
coverType="portrait"
|
||||
:cover="props.info.verticalCover || props.info.horizontalCover"
|
||||
/>
|
||||
|
||||
<Teleport to=".video-detail-wrap">
|
||||
<van-popup v-model:show="visibleCatalogue" position="bottom" z-index="1001" :style="{ height: '438px' }">
|
||||
<div class="play-list-box">
|
||||
<div class="line"></div>
|
||||
<div class="title flex-center-between">
|
||||
剧集列表
|
||||
<!-- <span @click="sort = !sort">
|
||||
{{ sort ? '正序' : '倒序' }}
|
||||
</span> -->
|
||||
</div>
|
||||
<div class="grading-group" v-if="gradingGroup.length > 1">
|
||||
<div
|
||||
class="grading-group-item"
|
||||
v-for="(item, index) in gradingGroup"
|
||||
:key="index"
|
||||
@click.stop="state.gradingGroupIndex = index"
|
||||
>
|
||||
{{ item[0].episodeNumber }}-{{ item[item.length - 1].episodeNumber }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-box overflow-y-auto">
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
|
||||
<div class="list">
|
||||
<Episode
|
||||
class="item"
|
||||
:episodeNumber="state.episodeNumber"
|
||||
:info="item"
|
||||
v-for="item in gradingGroup[state.gradingGroupIndex]"
|
||||
:key="item.id"
|
||||
@click="state.episodeNumber = item.episodeNumber"
|
||||
></Episode>
|
||||
</div>
|
||||
</PullRefresh>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed, watchEffect, Ref } from 'vue';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import { goAcgTagPage } from '../../utils/common';
|
||||
import { watchCount, videoTime } from '@/utils/index';
|
||||
// import RecommendList from '../../components/recommendList.vue';
|
||||
import { handleCollected, Collected_emit } from '../../utils/collect';
|
||||
import { AdType } from '@/enums/app';
|
||||
import mediaContentInfo from '../../utils/mediaContentInfo';
|
||||
import CommunityApi from '@/service/community';
|
||||
import { useGlobalStore } from '@/stores/global';
|
||||
import Episode from '../../components/episode.vue';
|
||||
const emits = defineEmits(['updateLiked', 'updateCollected', 'updateItem', 'openRoadLine']);
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
info: Types.Media.MediaInfo;
|
||||
}>(),
|
||||
{
|
||||
info: {},
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
state,
|
||||
currentItem,
|
||||
autoList,
|
||||
// switchEpisodeNumberCatalogue,
|
||||
visibleCatalogue,
|
||||
sort,
|
||||
moreParams,
|
||||
onLoad,
|
||||
onRefresh,
|
||||
getList,
|
||||
buySuccess,
|
||||
replaceRouter,
|
||||
} = mediaContentInfo({
|
||||
isGetInfo: false,
|
||||
isGetHistoryInit: true,
|
||||
pageSize: 30,
|
||||
dbType: HistoryVideoType.AcgVideo,
|
||||
isScrollTop: false,
|
||||
});
|
||||
const gradingGroup = computed(() => {
|
||||
let arr = autoList.value || [];
|
||||
if (arr.length > 30) {
|
||||
const result = [];
|
||||
for (let i = 0; i < arr.length; i += 30) {
|
||||
result.push(arr.slice(i, i + 30));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return [arr];
|
||||
});
|
||||
watch(
|
||||
() => currentItem.value.id,
|
||||
() => {
|
||||
emits('updateItem', currentItem.value);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
defineExpose({
|
||||
buySuccess,
|
||||
});
|
||||
const switchEpisodeNumberCatalogue = (item: Types.Acg.MediaContentListItem) => {
|
||||
visibleCatalogue.value = false;
|
||||
state.episodeNumber = item.episodeNumber;
|
||||
replaceRouter(item.episodeNumber);
|
||||
};
|
||||
|
||||
// 打开分享
|
||||
const visibleShare = ref(false);
|
||||
const openShare = () => {
|
||||
const { id, verticalCover, horizontalCover } = props.info;
|
||||
let obj = {
|
||||
id,
|
||||
cover: verticalCover || horizontalCover,
|
||||
};
|
||||
// globalStore.setShareInfo(obj);
|
||||
visibleShare.value = true;
|
||||
};
|
||||
const onToogleCollected = () => {
|
||||
try {
|
||||
const {
|
||||
id,
|
||||
mediaStatus: { hasCollected },
|
||||
} = props.info;
|
||||
const tempIsCollect = !hasCollected;
|
||||
handleCollected(id, tempIsCollect).then(() => {
|
||||
emits('updateCollected', tempIsCollect);
|
||||
Collected_emit(id, tempIsCollect);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
const onToogleLiked = () => {
|
||||
const zan = !props.info.mediaStatus.hasLiked;
|
||||
(zan ? CommunityApi.zan : CommunityApi.cancleZan)(
|
||||
zan
|
||||
? {
|
||||
type: 'video',
|
||||
objID: props.info.id,
|
||||
}
|
||||
: {
|
||||
type: 'video',
|
||||
objIDs: [props.info.id],
|
||||
},
|
||||
).then(() => {
|
||||
emits('updateLiked', zan);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
.introduction-wrap {
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
.titleBox {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
margin: 18px 0;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 10px;
|
||||
margin: 10px 0 14px;
|
||||
.tag {
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
}
|
||||
|
||||
.videoInfoBox {
|
||||
color: #989898;
|
||||
font-size: 12px;
|
||||
margin: 10px 0 18px;
|
||||
justify-content: space-between;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.actress-box {
|
||||
height: 60px;
|
||||
margin: 24px 0;
|
||||
.portrait {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.info {
|
||||
flex: 1;
|
||||
.name {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.count-box {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.episodeArea {
|
||||
margin-bottom: 14px;
|
||||
border-top: 0.5px solid rgba(255, 255, 255, 0.1);
|
||||
border-bottom: 0.5px solid rgba(255, 255, 255, 0.1);
|
||||
padding-top: 10px;
|
||||
.episodeHeader {
|
||||
font-weight: 500;
|
||||
.episodeTitle {
|
||||
font-size: 16px;
|
||||
}
|
||||
.episodeAll {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.episodeBox {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
.item {
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.play-list-box {
|
||||
$paddingBottom: 38px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 20px 20px 0px 0px;
|
||||
background: #0f0f0f;
|
||||
padding: 18px 16px $paddingBottom;
|
||||
@include SafeArea('padding', 'bottom', 0);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: $textColor;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
> .title {
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 25px; /* 140% */
|
||||
margin: 16px 0;
|
||||
span {
|
||||
color: #e0e0e0;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
}
|
||||
.line {
|
||||
width: 36px;
|
||||
height: 5px;
|
||||
border-radius: 100px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.list-box {
|
||||
flex: 1;
|
||||
.list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
|
||||
grid-column-gap: 5px;
|
||||
grid-row-gap: 9px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.margin-r-88 {
|
||||
margin-right: 88px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,468 @@
|
||||
<template>
|
||||
<div ref="rootRef" class="video-detail-wrap">
|
||||
<SvgIcon name="back_white_round" class="back" @click="$router.go(-1)" />
|
||||
<template v-if="state.info.id">
|
||||
<div class="movieBox">
|
||||
<div class="videoAdv" v-if="state.advsList?.length && state.isShowAdv">
|
||||
<div class="advVideoBox">
|
||||
<AdvSwiper :list="state.advsList" :autoplay="2000" v-if="state.advsList.length" class="adv" />
|
||||
<div class="timer_close" v-if="isVip">
|
||||
<div v-if="state.timer > 0">
|
||||
{{ state.timer }}s |
|
||||
<span @click="state.isShowAdv = false">关闭广告</span>
|
||||
</div>
|
||||
<div v-else><span @click="state.isShowAdv = false">关闭</span></div>
|
||||
</div>
|
||||
<div class="timer_close" v-else>
|
||||
<div v-if="state.timer > 0">
|
||||
{{ state.timer }}s |
|
||||
<span @click="vipJump">VIP可关闭广告</span>
|
||||
</div>
|
||||
<div v-else><span @click="state.isShowAdv = false">关闭</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<AnimationHorizontalVideoInfo
|
||||
:videoInfo="currentItem"
|
||||
:mediaInfo="state.info"
|
||||
v-if="currentItem?.videoUrl && showVideo"
|
||||
:key="currentItem?.episodeNumber"
|
||||
@buySuccess="beforeBuySuccess"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<CountdownBanner
|
||||
@openMemberPay="showPaymentStatusPopup = true"
|
||||
/>
|
||||
<div class="changeLineBox flex-vcenter" :class="!isVip ? 'top258' : ''">
|
||||
<div class="flex-vcenter" @click.stop="openChangeRoadLine">
|
||||
<SvgIcon name="changeLineIcon" />
|
||||
<span>线路切换</span>
|
||||
</div>
|
||||
</div>
|
||||
<van-tabs
|
||||
v-model:active="tabActive"
|
||||
:animated="true"
|
||||
@rendered="onTabRendered"
|
||||
class="tabs"
|
||||
:class="{ newUser: !isVip }"
|
||||
>
|
||||
<van-tab :title="item.label" v-for="item in itemList" :key="item.value" :name="prefixKey + item.value">
|
||||
<template v-slot:title>
|
||||
{{ item.label }}
|
||||
<Teleport to=".van-tabs__nav">
|
||||
<span v-if="item.value === 2" class="countComment" :class="{ active: tabActive === prefixKey + 2 }">
|
||||
{{ watchCount(state.info.countComment) }}
|
||||
</span>
|
||||
</Teleport>
|
||||
</template>
|
||||
<!--
|
||||
@selectEpisode="selectEpisode" -->
|
||||
<Introduction
|
||||
v-if="tabIsInit(prefixKey + item.value) && item.value == 1 && state.info.id"
|
||||
ref="introductionRef"
|
||||
:info="state.info"
|
||||
@updateCollected="updateCollected"
|
||||
@updateLiked="updateLiked"
|
||||
@updateItem="updateItem"
|
||||
></Introduction>
|
||||
<CommentBox
|
||||
v-else-if="tabIsInit(prefixKey + item.value) && item.value == 2 && state.info.id"
|
||||
:id="prefixKey + item.value"
|
||||
ref="dvComment"
|
||||
:isOpen="true"
|
||||
renderType="normal"
|
||||
objType="cartoon"
|
||||
theme="black"
|
||||
class="comment-comp"
|
||||
@addComment="state.info.countComment += 1"
|
||||
:showCollected="false"
|
||||
></CommentBox>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</template>
|
||||
<Loading v-else />
|
||||
</div>
|
||||
<!-- 切换路线框 -->
|
||||
<SelectionLinePopup :is-open="showChangeRoadLine" @closeSelectLine="closeSelectLine" @close="closeChangeRoadLine" />
|
||||
<!-- 开通会员 -->
|
||||
<PaymentStatusPopup
|
||||
:show="showPaymentStatusPopup"
|
||||
:video-id="String(state.info?.id || '')"
|
||||
@close="closePaymentStatusPopup"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AcgVideoDetail',
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { ref, onUnmounted, nextTick, provide, computed, toRaw, watch, Ref, onMounted, defineAsyncComponent } from 'vue';
|
||||
import { HistoryVideoType, AdType } from '@/enums/app';
|
||||
import mediaInfo from '../utils/mediaInfo';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import MineApi from '@/service/mine';
|
||||
import { watchCount } from '@/utils/index';
|
||||
// import { Pay_emit } from '../utils/pay-mitt';
|
||||
import type { PayInfo } from '../utils/pay-mitt';
|
||||
import Introduction from './components/introduction.vue';
|
||||
import tabAction from '@/utils/tabAction';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getAdItem } from '@/utils/getConfig';
|
||||
import CountdownBanner from '@/components/CountdownBanner/index.vue';
|
||||
|
||||
const PaymentStatusPopup = defineAsyncComponent(() => import('@/components/paymentStatusPopup/index.vue'));
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const currentItem = ref({}) as Ref<Types.Acg.MediaContentListItem>;
|
||||
const itemList = [
|
||||
{
|
||||
label: '简介',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '评论',
|
||||
value: 2,
|
||||
},
|
||||
];
|
||||
const showPaymentStatusPopup = ref(false);
|
||||
const closePaymentStatusPopup = () => {
|
||||
showPaymentStatusPopup.value = false;
|
||||
};
|
||||
const prefixKey = 'acg_video_';
|
||||
const { tabActive, tabIsInit, onTabRendered } = tabAction(true);
|
||||
|
||||
const { isVip } = useUserStore();
|
||||
|
||||
const showVideo = ref(true);
|
||||
const { state, historyData, addHistory } = mediaInfo({ dbType: HistoryVideoType.AcgVideo });
|
||||
|
||||
provide('historyData', historyData);
|
||||
|
||||
const showChangeRoadLine = ref(false);
|
||||
// ------------------- 广告相关逻辑完善 -------------------
|
||||
const vipJump = () => {
|
||||
router.push('/member');
|
||||
};
|
||||
|
||||
// 获取广告
|
||||
const getRandomAdv = () => {
|
||||
state.advsList = <any>[];
|
||||
let advs = getAdItem(AdType.NEW_FREE_VIDEO).filter((item: any) => item.watchTime > 0);
|
||||
if (advs.length > 0) {
|
||||
state.advsList = advs;
|
||||
if (!(isVip || historyData.value?.mediaStatus?.hasPaid)) {
|
||||
state.isShowAdv = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
state.isShowAdv = false;
|
||||
};
|
||||
|
||||
// 倒计时
|
||||
const countdown = () => {
|
||||
let advs = getAdItem(AdType.NEW_FREE_VIDEO);
|
||||
if (advs && advs.length) {
|
||||
state.timer = getAdItem(AdType.NEW_FREE_VIDEO)[0].watchTime;
|
||||
} else {
|
||||
state.timer = 0;
|
||||
}
|
||||
if (state.countdowner) {
|
||||
clearInterval(state.countdowner);
|
||||
}
|
||||
state.countdowner = setInterval(() => {
|
||||
state.timer--;
|
||||
if (state.timer <= 0) {
|
||||
clearInterval(state.countdowner);
|
||||
state.countdowner = null;
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
onUnmounted(() => {
|
||||
state.countdowner && clearInterval(state.countdowner);
|
||||
});
|
||||
// 只在动漫数据获取到后再处理广告
|
||||
watch(
|
||||
() => currentItem.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
getRandomAdv();
|
||||
if (state.advsList.length > 0) {
|
||||
countdown();
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
const openChangeRoadLine = () => {
|
||||
showChangeRoadLine.value = true;
|
||||
};
|
||||
const closeChangeRoadLine = () => {
|
||||
showChangeRoadLine.value = false;
|
||||
};
|
||||
|
||||
// 关闭线路选择
|
||||
const closeSelectLine = () => {
|
||||
showVideo.value = false;
|
||||
showChangeRoadLine.value = false;
|
||||
nextTick(() => {
|
||||
showVideo.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
const dvComment = ref();
|
||||
|
||||
const initComment = () => {
|
||||
dvComment.value[0].state.videoInfo = toRaw(state.info);
|
||||
dvComment.value[0].getCommentList();
|
||||
};
|
||||
|
||||
watch(
|
||||
dvComment,
|
||||
(el) => {
|
||||
if (!el) return;
|
||||
initComment();
|
||||
},
|
||||
{
|
||||
once: true,
|
||||
},
|
||||
);
|
||||
const updateCollected = (collected: boolean) => {
|
||||
state.info.mediaStatus.hasCollected = collected;
|
||||
state.info.countCollect = state.info.countCollect + (collected ? +1 : -1);
|
||||
};
|
||||
|
||||
const updateLiked = (liked: boolean) => {
|
||||
state.info.mediaStatus.hasLiked = liked;
|
||||
state.info.countLike = state.info.countLike + (liked ? +1 : -1);
|
||||
};
|
||||
|
||||
const updateItem = (item: any) => {
|
||||
currentItem.value = item;
|
||||
if (route.query.tabActive) {
|
||||
tabActive.value = (prefixKey + route.query.tabActive) as string;
|
||||
}
|
||||
};
|
||||
|
||||
const introductionRef = ref();
|
||||
const beforeBuySuccess = (obj: PayInfo) => {
|
||||
if (obj.type === 'all') {
|
||||
state.info.mediaStatus.hasPaid = true;
|
||||
historyData.value.mediaStatus.hasPaid = true;
|
||||
addHistory(Number(route.query.episodeNumber || 1));
|
||||
}
|
||||
introductionRef.value[0].buySuccess(obj.type);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.video-detail-wrap {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
.back {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
z-index: 800;
|
||||
}
|
||||
|
||||
.movieBox {
|
||||
width: 375px;
|
||||
height: 212px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
//margin-left: -6px;
|
||||
z-index: 2;
|
||||
.videoAdv {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 99999;
|
||||
width: 100%;
|
||||
height: 212px;
|
||||
background: rgba(0, 0, 0, 1);
|
||||
.advVideoBox {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
.timer_close {
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 10;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
padding: 4px 12px;
|
||||
border-radius: 40px;
|
||||
color: white;
|
||||
|
||||
// span {
|
||||
// color: #ff7f0f;
|
||||
// }
|
||||
}
|
||||
|
||||
.adv {
|
||||
height: 211px;
|
||||
width: 375px;
|
||||
overflow: hidden;
|
||||
:deep(.van-swipe-item) {
|
||||
height: 211px !important;
|
||||
width: 375px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.newUserBg {
|
||||
height: 46px;
|
||||
background: url('@/assets/images/png/newUserBg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding-left: 98px;
|
||||
padding-right: 9px;
|
||||
color: #fff;
|
||||
&.noNewUser {
|
||||
// background-image: url('@/assets/images/png/newUserBg2.png');
|
||||
background-image: url('@/assets/images/png/newUserBg.png');
|
||||
}
|
||||
.nowOpenBtn {
|
||||
color: #000;
|
||||
background: linear-gradient(270deg, #e3b284 0%, #feeede 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.changeLineBox {
|
||||
position: absolute;
|
||||
top: 212px;
|
||||
right: 16px;
|
||||
z-index: 2;
|
||||
height: 44px;
|
||||
width: 100px;
|
||||
justify-content: flex-end;
|
||||
|
||||
color: #989898;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
.countComment {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 112px;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
&.active {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
.tabs {
|
||||
--van-tab-active-text-color: rgba(255, 255, 255, 0.9);
|
||||
--van-tab-text-color: rgba(255, 255, 255, 0.45);
|
||||
height: calc(100% - 269px + 11px);
|
||||
:deep(> .van-tabs__wrap) {
|
||||
background-color: unset;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
.van-tabs__nav {
|
||||
background: unset;
|
||||
.van-tab {
|
||||
font-size: 14px;
|
||||
padding: unset;
|
||||
width: 60px;
|
||||
flex: unset;
|
||||
}
|
||||
|
||||
.van-tabs__line {
|
||||
background-color: $themeColor;
|
||||
height: 4px;
|
||||
width: 16px;
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(> .van-tabs__content) {
|
||||
height: calc(100% - 44px);
|
||||
overflow-x: hidden;
|
||||
.van-tab__panel {
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
&.newUser {
|
||||
height: calc(100% - 269px + 11px);
|
||||
}
|
||||
}
|
||||
|
||||
.comment-comp {
|
||||
height: 100%;
|
||||
:deep(.comment-wrap) {
|
||||
height: 100%;
|
||||
.wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
.headerBox {
|
||||
display: none;
|
||||
}
|
||||
.slider {
|
||||
display: none;
|
||||
}
|
||||
.main {
|
||||
background: transparent;
|
||||
// border-top: 1px solid #e0e0e0;
|
||||
height: 100%;
|
||||
}
|
||||
.bottomBox {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
.commentBtnBox {
|
||||
.commentInput {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.top258 {
|
||||
top: 258px !important;
|
||||
}
|
||||
:deep(.adv_swiper) {
|
||||
border-radius: unset !important;
|
||||
.adv-container {
|
||||
.adv-swiper-slide {
|
||||
border-radius: unset !important;
|
||||
.cover {
|
||||
border-radius: unset !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div :id="`acg_topic_sortType_${props.sortType}`" class="topic-list overflow-y-auto">
|
||||
<template v-if="!state.loading">
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
|
||||
<CategoryContent
|
||||
class="content"
|
||||
coverDirection="portrait"
|
||||
:contentType="props.contentType"
|
||||
:contentWrap="true"
|
||||
:list="state.list"
|
||||
></CategoryContent>
|
||||
</PullRefresh>
|
||||
</template>
|
||||
<Loading v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRaw, reactive, watch, computed } from 'vue';
|
||||
import CategoryContent from '../../components/categoryContent.vue';
|
||||
import MediaApi from '@/service/media';
|
||||
import { useRoute } from 'vue-router';
|
||||
const route = useRoute();
|
||||
const sId = route.query.id || '';
|
||||
const kind = route.query.kind || '';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
sortType: number;
|
||||
contentType: string;
|
||||
}>(),
|
||||
{
|
||||
sortType: () => 3,
|
||||
contentType: 'video',
|
||||
},
|
||||
);
|
||||
|
||||
const showType = computed(() => {
|
||||
return Number(route.query.showType || '0');
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
loading: true,
|
||||
list: <any>[],
|
||||
});
|
||||
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: 12,
|
||||
};
|
||||
|
||||
const moreParams = reactive({
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
finished: false,
|
||||
isNoData: false,
|
||||
error: false,
|
||||
isHigehtMax: false,
|
||||
});
|
||||
|
||||
const getList = async (type = '') => {
|
||||
try {
|
||||
const TempfilterParams = Object.assign({}, params, { sId, sortType: props.sortType, kind });
|
||||
let res: any = await MediaApi.getMediaTopic(TempfilterParams);
|
||||
state.loading = false;
|
||||
moreParams.loading = false;
|
||||
moreParams.refreshing = false;
|
||||
|
||||
let list = res.list || [];
|
||||
if (params.pageNumber === 1) {
|
||||
moreParams.isNoData = list.length ? false : true;
|
||||
}
|
||||
if (type === 'refresh') {
|
||||
state.list = list;
|
||||
} else {
|
||||
state.list = [...state.list, ...list];
|
||||
}
|
||||
|
||||
if (!res.hasNext) {
|
||||
moreParams.finished = true;
|
||||
}
|
||||
} catch (e) {
|
||||
moreParams.loading = false;
|
||||
moreParams.error = true;
|
||||
}
|
||||
};
|
||||
getList();
|
||||
|
||||
const onLoad = () => {
|
||||
moreParams.loading = true;
|
||||
params.pageNumber++;
|
||||
getList();
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
params.pageNumber = 1;
|
||||
moreParams.refreshing = true;
|
||||
moreParams.finished = false;
|
||||
getList('refresh');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
.topic-list {
|
||||
height: calc(100vh - 44px * 2);
|
||||
padding: 0 16px;
|
||||
@include SafeArea('padding', 'bottom', 0);
|
||||
|
||||
.content {
|
||||
border-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<!-- ACG大类列表,漫画、动漫、有声小说 -->
|
||||
<template>
|
||||
<div class="topic-wrap">
|
||||
<Header :title="route.query.title" />
|
||||
<van-tabs
|
||||
v-model:active="tabActive"
|
||||
title-active-color="#fff"
|
||||
title-inactive-color="#747B80"
|
||||
:animated="true"
|
||||
@rendered="onTabRendered"
|
||||
class="tabs"
|
||||
>
|
||||
<van-tab v-for="item in itemList" :key="item.key" :title="item.title" :name="prefixKey + item.key">
|
||||
<List v-if="tabIsInit(prefixKey + item.key)" :sortType="item.key" :contentType="contentType"></List>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AcgTopicList',
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import List from './components/list.vue';
|
||||
import tabAction from '@/utils/tabAction';
|
||||
import { useRoute } from 'vue-router';
|
||||
const route = useRoute();
|
||||
const contentType = route.query.type as string;
|
||||
const itemList = [
|
||||
{ title: '热门推荐', key: 3 },
|
||||
{ title: '最新上架', key: 1 },
|
||||
{ title: '最新热评', key: 4 },
|
||||
];
|
||||
|
||||
const prefixKey = 'acg_topic_sortType_';
|
||||
const { tabActive, tabIsInit, onTabRendered } = tabAction(true);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.topic-wrap {
|
||||
.tabs {
|
||||
:deep(.van-tabs__wrap) {
|
||||
border-radius: 46px;
|
||||
margin: 0 auto;
|
||||
//background: #030f18;
|
||||
padding: 0 40px;
|
||||
.van-tabs__nav {
|
||||
background: unset;
|
||||
.van-tab {
|
||||
font-size: 14px;
|
||||
|
||||
&:not(:nth-last-child(2))::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
right: 0px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
// .van-tab:nth-of-type(3) {
|
||||
// &::after {
|
||||
// display: none;
|
||||
// }
|
||||
// }
|
||||
.van-tabs__line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
import MediaApi from '@/service/media';
|
||||
import { showToast } from 'vant';
|
||||
import { onMounted, onUnmounted, Ref } from 'vue';
|
||||
import eventBus from '@/utils/eventBus';
|
||||
import { AcgEventBusKey } from '@/enums/app';
|
||||
|
||||
/**
|
||||
* @Description 收藏acg
|
||||
* @param {string} objID
|
||||
* @param {string} mediaType
|
||||
* @param {boolean} isCollect
|
||||
* @return {*}
|
||||
*/
|
||||
const handleCollected = async (objID: string, isCollect: boolean) => {
|
||||
try {
|
||||
return (isCollect ? MediaApi.postBookshelfAdd : MediaApi.postBookshelfDel)({
|
||||
id: objID,
|
||||
}).then(() => {
|
||||
showToast(isCollect ? '成功收藏' : '成功取消');
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @Description eventBus监听
|
||||
* @param {string} id
|
||||
* @param {Ref} hasCollected
|
||||
* @return {*}
|
||||
*/
|
||||
const Collected_on = (id: string, hasCollected: Ref<boolean>, callback?: any) => {
|
||||
const update = (bool: any) => {
|
||||
if (callback) {
|
||||
callback(bool);
|
||||
return;
|
||||
}
|
||||
hasCollected.value = bool;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on(AcgEventBusKey.Collected + id, update);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
eventBus.off(AcgEventBusKey.Collected + id, update);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @Description eventBus赋值
|
||||
* @param {string} id
|
||||
* @param {boolean} bool
|
||||
* @return {*}
|
||||
*/
|
||||
const Collected_emit = (id: string, bool: boolean) => {
|
||||
eventBus.emit(AcgEventBusKey.Collected + id, bool);
|
||||
};
|
||||
|
||||
export { handleCollected, Collected_on, Collected_emit };
|
||||
@@ -0,0 +1,129 @@
|
||||
import router from '@/router';
|
||||
const goTopicPage = (id: string, type: string | number, title: string, showType?: number) => {
|
||||
if (typeof type === 'number') {
|
||||
type = getMediaTypeKey(type);
|
||||
}
|
||||
router.push({ path: '/AcgTopicList', query: { id, type, title, showType } });
|
||||
};
|
||||
|
||||
const goAcgTagPage = (id: string, tagName: string, newsType?: string) => {
|
||||
router.push({ path: '/tagVideoList', query: { id, tagName, newsType } });
|
||||
};
|
||||
// kind的id对应字符串
|
||||
enum MediaType {
|
||||
video = 4,
|
||||
image = 5,
|
||||
text = 6,
|
||||
}
|
||||
type MediaTypeKey = keyof typeof MediaType;
|
||||
type MediaTypeValue = { [key in MediaType]: number };
|
||||
|
||||
function getMediaTypeKey(value: number): string {
|
||||
if (value === MediaType.video) {
|
||||
return 'video';
|
||||
} else if (value === MediaType.image) {
|
||||
return 'image';
|
||||
} else if (value === MediaType.text) {
|
||||
return 'text';
|
||||
// } else if (value === MediaType.theatre) {
|
||||
// return 'theatre';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getMediaTypeValue(value: string): MediaType | number {
|
||||
const mediaTypeKeys = Object.keys(MediaType).filter((key) => isNaN(Number(key)));
|
||||
const mediaTypeValues = mediaTypeKeys.map((key) => MediaType[key]);
|
||||
const index = mediaTypeKeys.indexOf(value);
|
||||
|
||||
if (index !== -1) {
|
||||
return mediaTypeValues[index];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const goFilterPage = () => {
|
||||
const route = router.currentRoute;
|
||||
const { mediaType = 'video' } = route.value.query;
|
||||
router.push({ path: '/AcgSearchFilter', query: { mediaType } });
|
||||
};
|
||||
//获取当前时间
|
||||
const getNewDate = () => {
|
||||
return new Date(new Date().getTime() + (window._diffTime || 0));
|
||||
};
|
||||
|
||||
const rankTabList = [
|
||||
{ title: '日榜', key: 1 },
|
||||
{ title: '周榜', key: 2 },
|
||||
{ title: '人气榜', key: 3 },
|
||||
{ title: '热度榜', key: 4 },
|
||||
{ title: '钻石榜', key: 5 },
|
||||
{ title: '连载榜', key: 6 },
|
||||
];
|
||||
|
||||
function getElementDistance(element: HTMLElement, parent: HTMLElement) {
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
|
||||
const distance = {
|
||||
top: elementRect.top - parentRect.top,
|
||||
right: parentRect.right - elementRect.right,
|
||||
bottom: parentRect.bottom - elementRect.bottom,
|
||||
left: elementRect.left - parentRect.left,
|
||||
};
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
const goDetailPage = (mediaType: MediaTypeKey, id: string, mediaSubType?: number) => {
|
||||
let name = 'AcgImageDetail';
|
||||
if (mediaType === 'text') {
|
||||
name = 'AcgTextDetail';
|
||||
} else if (mediaType === 'video') {
|
||||
name = 'AcgVideoDetail';
|
||||
}
|
||||
|
||||
router.push({
|
||||
name,
|
||||
query: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const freeSet = new Set();
|
||||
|
||||
const getUpdateStatusText = (updateStatus?: number) => {
|
||||
if (updateStatus === 2) return '已完结';
|
||||
return '连载中';
|
||||
};
|
||||
|
||||
/** 按 id(无 id 则按 name)去重,保留首次出现顺序 */
|
||||
const dedupeTagDetails = (tags?: Types.Media.TagDetailsItem[] | null): Types.Media.TagDetailsItem[] => {
|
||||
if (!tags?.length) return [];
|
||||
const seen = new Set<string>();
|
||||
return tags.filter((item) => {
|
||||
const key = String(item.name || '').trim();
|
||||
if (!key || seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
goTopicPage,
|
||||
goFilterPage,
|
||||
getMediaTypeKey,
|
||||
getMediaTypeValue,
|
||||
getNewDate,
|
||||
rankTabList,
|
||||
getElementDistance,
|
||||
goDetailPage,
|
||||
freeSet,
|
||||
getUpdateStatusText,
|
||||
goAcgTagPage,
|
||||
dedupeTagDetails,
|
||||
};
|
||||
export type { MediaTypeKey, MediaTypeValue };
|
||||
@@ -0,0 +1,66 @@
|
||||
import { ref, Ref } from 'vue';
|
||||
import { getVideoHistoryByKey, addVideoHistory } from '@/utils/index';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import eventBus from '@/utils/eventBus';
|
||||
|
||||
const AcgHistoryData = {} as {
|
||||
[key: string]: {
|
||||
[key: string]: Types.Media.MediaInfo;
|
||||
};
|
||||
};
|
||||
|
||||
const key = 'AcgHistoryData_';
|
||||
|
||||
const getData = async (id: string, dbType = HistoryVideoType.AcgImage, isParent: boolean) => {
|
||||
if (!AcgHistoryData[dbType]) {
|
||||
AcgHistoryData[dbType] = {};
|
||||
}
|
||||
let item = AcgHistoryData[dbType][id];
|
||||
if (item && item.id) {
|
||||
return item;
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
AcgHistoryData[dbType][id] = { id: '' } as any;
|
||||
item = (await getVideoHistoryByKey(id, dbType)) as any;
|
||||
} else {
|
||||
item = (await new Promise((resolve) => {
|
||||
eventBus.on(key + id, (obj: Types.Media.MediaInfo) => {
|
||||
eventBus.off(key + id);
|
||||
resolve(obj);
|
||||
});
|
||||
})) as any;
|
||||
}
|
||||
|
||||
if (item && item.id) {
|
||||
AcgHistoryData[dbType][id] = item;
|
||||
eventBus.emit(key + id, item);
|
||||
}
|
||||
return item;
|
||||
};
|
||||
|
||||
export default (dbType = HistoryVideoType.AcgImage, isParent: boolean) => {
|
||||
const historyEpisodeNumber = ref(0);
|
||||
const historyData = ref({}) as Ref<Types.Media.MediaInfo>;
|
||||
|
||||
const getHistoryData = async (id: string) => {
|
||||
const history = (await getData(id, dbType, isParent)) as any;
|
||||
if (history) {
|
||||
historyData.value = history;
|
||||
if (history.historyEpisodeNumber) {
|
||||
historyEpisodeNumber.value = history.historyEpisodeNumber;
|
||||
}
|
||||
} else {
|
||||
historyEpisodeNumber.value = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const setHistoryData = (data: any, dbType: HistoryVideoType) => {
|
||||
const item = (AcgHistoryData[dbType] && AcgHistoryData[dbType][data.id]) || {};
|
||||
const newItem = Object.assign(item, data);
|
||||
AcgHistoryData[dbType][data.id] = newItem;
|
||||
eventBus.emit(key + data.id, newItem);
|
||||
addVideoHistory(newItem, dbType, new Date().getTime());
|
||||
};
|
||||
return { historyData, historyEpisodeNumber, getHistoryData, setHistoryData };
|
||||
};
|
||||
@@ -0,0 +1,542 @@
|
||||
import { reactive, ref, ComputedRef, watchEffect, watch, nextTick, computed, onBeforeUnmount } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import indexDB from './indexDB';
|
||||
import { showToast } from 'vant';
|
||||
import AcgApi from '@/service/acg';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { freeSet } from './common';
|
||||
interface ReadData {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export default ({
|
||||
mediaId = '',
|
||||
isScrollTop = true,
|
||||
isGetInfo = true,
|
||||
isUpdate = true,
|
||||
isNeedHistory = true,
|
||||
isGetHistoryInit = false,
|
||||
pageSize = 30,
|
||||
dbType = HistoryVideoType.AcgImage,
|
||||
}) => {
|
||||
const userStore = useUserStore();
|
||||
const { openLoading, closeLoading } = useLoading();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { id, episodeNumber, title } = route.query;
|
||||
|
||||
const state = reactive({
|
||||
list: [] as Types.Acg.MediaContentListItem[],
|
||||
info: {} as Types.Acg.MediaContentInfo,
|
||||
validateItem: {} as Types.Acg.MediaContentListItem,
|
||||
episodeNumber: Number(episodeNumber || '1'),
|
||||
id: (mediaId || id) as string,
|
||||
title,
|
||||
gradingGroupIndex: 0,
|
||||
});
|
||||
|
||||
const { historyData, historyEpisodeNumber, getHistoryData, setHistoryData } = indexDB(dbType, true);
|
||||
const addHistory = (num: number) => {
|
||||
if (num === -1) {
|
||||
num = state.episodeNumber;
|
||||
}
|
||||
const data = { ...historyData.value, historyEpisodeNumber: num, historyReadData };
|
||||
setHistoryData(data, dbType);
|
||||
};
|
||||
|
||||
const historyReadData = [] as Types.Acg.ReadData[];
|
||||
|
||||
const addHistoryReadData = (num: number, obj: ReadData, imgObj?: { url: string; width: number; height: number }) => {
|
||||
const listItem = state.list.find((item) => item.episodeNumber === num);
|
||||
if (!listItem) return;
|
||||
|
||||
const index = (historyReadData || []).findIndex((item) => item.episodeNumber === num);
|
||||
const isHas = index > -1;
|
||||
|
||||
const imgs = isHas ? historyReadData[index].imgs || {} : {};
|
||||
if (imgObj?.url) {
|
||||
imgs[imgObj?.url] = {
|
||||
width: imgObj?.width,
|
||||
height: imgObj?.height,
|
||||
};
|
||||
}
|
||||
let item = {};
|
||||
if (isHas) {
|
||||
item = historyReadData.splice(index, 1)[0];
|
||||
}
|
||||
|
||||
const newItem = Object.assign({}, { ...item }, { ...listItem, ...obj, imgs });
|
||||
historyReadData.push(newItem);
|
||||
};
|
||||
|
||||
if (isUpdate) {
|
||||
onBeforeUnmount(() => {
|
||||
nextTick(() => {
|
||||
addHistory(-1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const currentItem = computed(() => {
|
||||
if (state.list.length === 0) return {};
|
||||
return state.list.find((item) => item.episodeNumber === state.episodeNumber) || {};
|
||||
}) as ComputedRef<Types.Acg.MediaContentListItem>;
|
||||
|
||||
let initScrollLeft = true;
|
||||
watch(currentItem, () => {
|
||||
if (initScrollLeft) {
|
||||
goSectionScrollLeft();
|
||||
initScrollLeft = false;
|
||||
}
|
||||
if (['AcgTextDetail', 'AcgImageDetail'].includes((route.name as string) || '')) {
|
||||
return;
|
||||
}
|
||||
validateHasBuy(currentItem.value);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => state.episodeNumber,
|
||||
() => {
|
||||
addHistory(state.episodeNumber);
|
||||
},
|
||||
);
|
||||
|
||||
const getPageNumber = () => {
|
||||
return Math.ceil(state.episodeNumber / 30);
|
||||
};
|
||||
const pageNumber = getPageNumber();
|
||||
|
||||
const params = {
|
||||
prevPageNumber: pageNumber,
|
||||
nextPageNumber: pageNumber,
|
||||
pageSize,
|
||||
};
|
||||
|
||||
const moreParams = reactive({
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
finished: false,
|
||||
isNoData: false,
|
||||
error: false,
|
||||
isHigehtMax: false,
|
||||
});
|
||||
|
||||
const moreParams_finished = {
|
||||
prev: false,
|
||||
next: false,
|
||||
};
|
||||
|
||||
const getList = async (type: 'prev' | 'next', pageNumber?: number) => {
|
||||
try {
|
||||
const tempParams = {
|
||||
mediaId: state.id,
|
||||
pageSize: params.pageSize,
|
||||
pageNumber: pageNumber ? pageNumber : type === 'prev' ? params.prevPageNumber : params.nextPageNumber,
|
||||
};
|
||||
let res: any = await AcgApi.getMediaContentList(tempParams);
|
||||
if ((res.list || []).length === 0 && state.list.length === 0 && tempParams.pageNumber !== 1) {
|
||||
params.prevPageNumber = 1;
|
||||
params.nextPageNumber = 1;
|
||||
tempParams.pageNumber = 1;
|
||||
res = await AcgApi.getMediaContentList(tempParams);
|
||||
}
|
||||
|
||||
historyData.value.currentEpisode = res.total;
|
||||
|
||||
moreParams.loading = false;
|
||||
moreParams.refreshing = false;
|
||||
|
||||
let list = res.list || [];
|
||||
if (list.length > 0) {
|
||||
const reList = (list || []).reduce(
|
||||
(acc: Types.Acg.MediaContentListItem[], current: Types.Acg.MediaContentListItem) => {
|
||||
const x = acc.find((item) => item.episodeNumber === current.episodeNumber);
|
||||
if (!x) {
|
||||
return acc.concat([current]);
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
if (isInitGetList && res.hasNext && list.length !== reList.length) {
|
||||
list = reList;
|
||||
onLoad();
|
||||
}
|
||||
}
|
||||
isInitGetList = false;
|
||||
|
||||
if (state.list.length === 0) {
|
||||
state.list = list;
|
||||
} else {
|
||||
state.list = state.list.concat(list);
|
||||
}
|
||||
if (tempParams.pageNumber == 1 && state.list.length == 0) {
|
||||
moreParams.isNoData = true;
|
||||
}
|
||||
if (!res.hasNext) {
|
||||
moreParams_finished[type] = true;
|
||||
moreParams.finished = type === 'next' && sort.value;
|
||||
}
|
||||
return list;
|
||||
} catch (e) {
|
||||
moreParams.loading = false;
|
||||
moreParams.error = true;
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const onLoad = () => {
|
||||
moreParams.loading = true;
|
||||
if (sort.value) {
|
||||
params.nextPageNumber += 1;
|
||||
getList('next');
|
||||
} else {
|
||||
if (params.prevPageNumber === 1) {
|
||||
moreParams.loading = false;
|
||||
return;
|
||||
}
|
||||
params.prevPageNumber -= 1;
|
||||
getList('prev');
|
||||
}
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
if (sort.value) {
|
||||
if (params.prevPageNumber == 1) {
|
||||
moreParams.refreshing = false;
|
||||
return;
|
||||
}
|
||||
params.prevPageNumber -= 1;
|
||||
moreParams.refreshing = true;
|
||||
moreParams.finished = false;
|
||||
getList('prev');
|
||||
} else {
|
||||
if (moreParams_finished['next']) {
|
||||
moreParams.refreshing = false;
|
||||
return;
|
||||
}
|
||||
params.nextPageNumber += 1;
|
||||
moreParams.refreshing = true;
|
||||
getList('next');
|
||||
}
|
||||
};
|
||||
let isInitGetList = true;
|
||||
const init = () => {
|
||||
getList('next');
|
||||
};
|
||||
|
||||
if (isNeedHistory) {
|
||||
getHistoryData(state.id).then(() => {
|
||||
historyReadData.push(...JSON.parse(JSON.stringify(historyData.value.historyReadData || '[]')));
|
||||
if (isGetHistoryInit) {
|
||||
if (!episodeNumber) {
|
||||
state.episodeNumber = historyData.value.historyEpisodeNumber || state.episodeNumber;
|
||||
}
|
||||
|
||||
const pageNumber = getPageNumber();
|
||||
|
||||
params.prevPageNumber = pageNumber;
|
||||
params.nextPageNumber = pageNumber;
|
||||
}
|
||||
|
||||
init();
|
||||
});
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
const getContent = async () => {
|
||||
if (state.info.id) {
|
||||
openLoading();
|
||||
}
|
||||
|
||||
await AcgApi.getMediaContentInfo({ id: currentItem.value.id }).then((res) => {
|
||||
state.info = res;
|
||||
});
|
||||
|
||||
closeLoading();
|
||||
};
|
||||
|
||||
if (isGetInfo) {
|
||||
watch(
|
||||
() => currentItem.value.id,
|
||||
(newId, oldId) => {
|
||||
if (!newId || newId === oldId) return;
|
||||
getContent();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const statusText = computed(() => {
|
||||
switch (historyData.value.updateStatus) {
|
||||
case 1:
|
||||
return '连载中';
|
||||
case 2:
|
||||
return '已完结';
|
||||
default:
|
||||
return '已完结';
|
||||
}
|
||||
});
|
||||
|
||||
//目录
|
||||
const visibleCatalogue = ref(false);
|
||||
const sort = ref(true);
|
||||
const autoList = computed(() => {
|
||||
return (state.list || []).sort(
|
||||
sort.value ? (a, b) => a.episodeNumber - b.episodeNumber : (a, b) => b.episodeNumber - a.episodeNumber,
|
||||
);
|
||||
});
|
||||
|
||||
const scrollToElement = (elementId: string) => {
|
||||
const element = document.getElementById(elementId) as HTMLElement;
|
||||
if (!element) return;
|
||||
const parent = element.closest('.list') as HTMLElement;
|
||||
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
|
||||
const offsetTop = elementRect.top - parentRect.top;
|
||||
const offsetBottom = elementRect.bottom - parentRect.bottom;
|
||||
|
||||
if (offsetTop < 0 || offsetBottom > 0) {
|
||||
// parent.scrollTop = element.offsetTop - (parent.offsetHeight - element.offsetHeight) / 2;
|
||||
parent.scrollTop = element.offsetTop - parent.offsetHeight / 2;
|
||||
}
|
||||
};
|
||||
const goSectionScrollTop = () => {
|
||||
if (!isScrollTop) return;
|
||||
nextTick(() => {
|
||||
scrollToElement(`section_${state.episodeNumber}`);
|
||||
});
|
||||
};
|
||||
|
||||
let isInit = false;
|
||||
|
||||
watchEffect(() => {
|
||||
if (!visibleCatalogue.value) return;
|
||||
|
||||
if (isInit) {
|
||||
return;
|
||||
}
|
||||
|
||||
isInit = true;
|
||||
|
||||
goSectionScrollTop();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => sort.value,
|
||||
() => {
|
||||
moreParams.finished = moreParams_finished[sort.value ? 'next' : 'prev'];
|
||||
goSectionScrollTop();
|
||||
},
|
||||
);
|
||||
|
||||
//支付
|
||||
const dvPayForWatch = ref();
|
||||
const visiblePayForWatch = ref(false);
|
||||
|
||||
const validatePermission = () => {
|
||||
// 购买优先
|
||||
if (historyData.value.id && historyData.value.mediaStatus && historyData.value.mediaStatus.hasPaid) return true;
|
||||
// 会员
|
||||
if (historyData.value.permission === 0 && userStore.isVip) return true;
|
||||
// 付费
|
||||
if (historyData.value.permission === 1) {
|
||||
if (!historyData.value.price) {
|
||||
return true;
|
||||
}
|
||||
if (userStore.userInfo.vipLevel >= 2) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 免费 || 前几集免费
|
||||
if (historyData.value.permission === 2 || historyData.value.freeEpisode >= state.validateItem.episodeNumber)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const validateHasBuy = (item: Types.Acg.MediaContentListItem) => {
|
||||
state.validateItem = item;
|
||||
if (!validatePermission()) {
|
||||
visiblePayForWatch.value = true;
|
||||
// showToast('付费章节,购买后才能观看喔');
|
||||
return true;
|
||||
}
|
||||
visiblePayForWatch.value = false;
|
||||
return false;
|
||||
};
|
||||
|
||||
const buySuccess = (type = 'one') => {
|
||||
state.info.hasBuy = true;
|
||||
if (state.validateItem.id) {
|
||||
state.validateItem.hasBuy = true;
|
||||
state.episodeNumber = state.validateItem.episodeNumber;
|
||||
}
|
||||
visiblePayForWatch.value = false;
|
||||
visibleCatalogue.value = false;
|
||||
if (type === 'all') {
|
||||
state.list.forEach((item) => {
|
||||
item.hasBuy = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// watch(
|
||||
// dvPayForWatch,
|
||||
// (el) => {
|
||||
// debugger;
|
||||
// if (!el) return;
|
||||
// validateHasBuy(currentItem.value);
|
||||
// },
|
||||
// // {
|
||||
// // once: true,
|
||||
// // },
|
||||
// );
|
||||
|
||||
const closePayForWatch = () => {
|
||||
if (validatePermission()) {
|
||||
visiblePayForWatch.value = false;
|
||||
return;
|
||||
}
|
||||
router.back();
|
||||
};
|
||||
|
||||
const loadEpisodeList = async (num: number, type: 'prev' | 'next') => {
|
||||
const item = state.list.find((item) => item.episodeNumber === num);
|
||||
if (item) return item;
|
||||
|
||||
openLoading();
|
||||
await getList(type);
|
||||
closeLoading();
|
||||
|
||||
return state.list.find((item) => item.episodeNumber === num);
|
||||
};
|
||||
|
||||
const replaceRouter = (num: number) => {
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
episodeNumber: num,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const switchEpisodeNumber = async (num: number, isValidate = true) => {
|
||||
|
||||
if (num === -1) {
|
||||
if (state.episodeNumber !== 1) {
|
||||
const prevNum = state.episodeNumber - 1;
|
||||
const item = await loadEpisodeList(prevNum, 'prev');
|
||||
if (isValidate) {
|
||||
if (validateHasBuy(item as Types.Acg.MediaContentListItem)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
state.episodeNumber = prevNum;
|
||||
if (dbType === HistoryVideoType.AcgText) {
|
||||
addHistoryReadData(prevNum, { progress: 0, scrollTop: 0 });
|
||||
addHistory(prevNum);
|
||||
}
|
||||
replaceRouter(prevNum);
|
||||
return true;
|
||||
} else {
|
||||
showToast('这是第一章了喔~');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (state.episodeNumber < historyData.value.currentEpisode) {
|
||||
const nextNum = state.episodeNumber + 1;
|
||||
const item = await loadEpisodeList(nextNum, 'next');
|
||||
if (isValidate) {
|
||||
if (validateHasBuy(item as Types.Acg.MediaContentListItem)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
state.episodeNumber = nextNum;
|
||||
if (dbType === HistoryVideoType.AcgText) {
|
||||
addHistoryReadData(nextNum, { progress: 0, scrollTop: 0 });
|
||||
addHistory(nextNum);
|
||||
}
|
||||
replaceRouter(nextNum);
|
||||
return true;
|
||||
} else {
|
||||
showToast('已经到最后一章了喔~');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const switchEpisodeNumberCatalogue = (item: Types.Acg.MediaContentListItem) => {
|
||||
if (validateHasBuy(item)) return;
|
||||
|
||||
visibleCatalogue.value = false;
|
||||
if (dbType === HistoryVideoType.AcgText) {
|
||||
addHistoryReadData(item.episodeNumber, { progress: 0, scrollTop: 0 });
|
||||
addHistory(item.episodeNumber);
|
||||
}
|
||||
state.episodeNumber = item.episodeNumber;
|
||||
replaceRouter(item.episodeNumber);
|
||||
};
|
||||
|
||||
const scrollToElement2 = (elementId: string) => {
|
||||
const element = document.getElementById(elementId) as HTMLElement;
|
||||
if (!element) return;
|
||||
const parent = element.closest('.episode-box') as HTMLElement;
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
|
||||
const offsetLeft = elementRect.left - parentRect.left;
|
||||
const offsetRight = elementRect.right - parentRect.right;
|
||||
|
||||
if (offsetLeft < 0 || offsetRight > 0) {
|
||||
parent.scrollLeft = element.offsetLeft - parent.offsetWidth / 2;
|
||||
}
|
||||
};
|
||||
const goSectionScrollLeft = () => {
|
||||
const item = state.list.find((item) => item.episodeNumber === state.episodeNumber);
|
||||
if (!item) return;
|
||||
nextTick(() => {
|
||||
scrollToElement2(`episode-${item.id}`);
|
||||
});
|
||||
};
|
||||
const handlePopupSwitchEpisode = (num: number) => {
|
||||
state.episodeNumber = num;
|
||||
visibleCatalogue.value = false;
|
||||
|
||||
goSectionScrollLeft();
|
||||
};
|
||||
|
||||
return {
|
||||
state,
|
||||
currentItem,
|
||||
statusText,
|
||||
moreParams,
|
||||
onLoad,
|
||||
onRefresh,
|
||||
getList,
|
||||
visibleCatalogue,
|
||||
handlePopupSwitchEpisode,
|
||||
sort,
|
||||
autoList,
|
||||
dvPayForWatch,
|
||||
visiblePayForWatch,
|
||||
validateHasBuy,
|
||||
buySuccess,
|
||||
closePayForWatch,
|
||||
switchEpisodeNumber,
|
||||
historyData,
|
||||
historyEpisodeNumber,
|
||||
addHistory,
|
||||
addHistoryReadData,
|
||||
switchEpisodeNumberCatalogue,
|
||||
validatePermission,
|
||||
goSectionScrollTop,
|
||||
replaceRouter,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { reactive, computed, onActivated, onUnmounted, provide } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import MediaApi from '@/service/media';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import indexDB from './indexDB';
|
||||
import { dedupeTagDetails } from './common';
|
||||
// import { showToast } from 'vant';
|
||||
|
||||
export default ({ id = '', dbType = HistoryVideoType.AcgImage }) => {
|
||||
const route = useRoute();
|
||||
const madieId = id || (route.query.id as string);
|
||||
//内容
|
||||
const state = reactive({
|
||||
info: {} as Types.Media.MediaInfo,
|
||||
episodeNumber: Number(route.query.episode) || 1,
|
||||
advsList: <any>[],
|
||||
timer: 0,
|
||||
isShowAdv: true,
|
||||
countdowner: <any>null,
|
||||
});
|
||||
|
||||
const { historyData, historyEpisodeNumber, getHistoryData, setHistoryData } = indexDB(dbType, true);
|
||||
provide('historyData', historyData);
|
||||
const addHistory = (num = 0) => {
|
||||
const historyReadData = historyData.value.historyReadData || [];
|
||||
const obj = { ...historyData.value, ...state.info, historyReadData };
|
||||
if (num > 0) {
|
||||
obj.historyEpisodeNumber = num;
|
||||
}
|
||||
setHistoryData(obj, dbType);
|
||||
};
|
||||
|
||||
onActivated(() => {
|
||||
if (!state.info.id) return;
|
||||
setTimeout(() => {
|
||||
getHistoryData(madieId);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
getHistoryData(madieId).then(() => {
|
||||
addHistory();
|
||||
});
|
||||
});
|
||||
|
||||
const getData = async () => {
|
||||
await getHistoryData(madieId);
|
||||
const res = await MediaApi.getMediaInfo({ id: madieId });
|
||||
const info = res as Types.Media.MediaInfo;
|
||||
if (info.tagDetails?.length) {
|
||||
info.tagDetails = dedupeTagDetails(info.tagDetails);
|
||||
}
|
||||
state.info = info;
|
||||
|
||||
addHistory();
|
||||
if (!historyData.value.id) {
|
||||
historyData.value = state.info;
|
||||
}
|
||||
};
|
||||
getData();
|
||||
|
||||
const statusText = computed(() => {
|
||||
switch (state.info.updateStatus) {
|
||||
case 1:
|
||||
return '连载中';
|
||||
case 2:
|
||||
return '已完结';
|
||||
default:
|
||||
return '已完结';
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
state,
|
||||
historyData,
|
||||
addHistory,
|
||||
historyEpisodeNumber,
|
||||
statusText,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { onMounted, onUnmounted, Ref } from 'vue';
|
||||
import eventBus from '@/utils/eventBus';
|
||||
import { AcgEventBusKey } from '@/enums/app';
|
||||
|
||||
interface PayInfo {
|
||||
type: 'one' | 'all';
|
||||
productID: string;
|
||||
isFree: boolean;
|
||||
}
|
||||
|
||||
const Pay_parent_on = ({
|
||||
id,
|
||||
state,
|
||||
historyData,
|
||||
addHistory,
|
||||
}: {
|
||||
id: string;
|
||||
state: {
|
||||
info: Types.Media.MediaInfo;
|
||||
};
|
||||
historyData: Ref<Types.Media.MediaInfo>;
|
||||
addHistory: any;
|
||||
}) => {
|
||||
const update = () => {
|
||||
state.info.mediaStatus.hasPaid = true;
|
||||
historyData.value.mediaStatus.hasPaid = true;
|
||||
addHistory();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on(AcgEventBusKey.HasPaid + id, update as any);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
eventBus.off(AcgEventBusKey.HasPaid + id, update as any);
|
||||
});
|
||||
};
|
||||
|
||||
const Pay_child_on = (parentId: string, state: any) => {
|
||||
const update = (id: any) => {
|
||||
const item = (state.list as Types.Acg.MediaContentListItem[]).find((item) => item.id === id);
|
||||
if (item) {
|
||||
item.hasBuy = true;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on(AcgEventBusKey.HasBuy + parentId, update);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
eventBus.off(AcgEventBusKey.HasBuy + parentId, update);
|
||||
});
|
||||
};
|
||||
|
||||
const Pay_emit = (id: string, obj: PayInfo) => {
|
||||
if (obj.type === 'one') {
|
||||
eventBus.emit(AcgEventBusKey.HasBuy + id, obj.productID);
|
||||
} else {
|
||||
eventBus.emit(AcgEventBusKey.HasPaid + id);
|
||||
}
|
||||
};
|
||||
|
||||
export { Pay_parent_on, Pay_child_on, Pay_emit };
|
||||
export type { PayInfo };
|
||||
@@ -0,0 +1,242 @@
|
||||
interface Rules {
|
||||
toBottom: number;
|
||||
toTop: number;
|
||||
toLeft: number;
|
||||
toRight: number;
|
||||
isClickCallback?: boolean;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
time: number;
|
||||
}
|
||||
export interface Result {
|
||||
toTop: boolean;
|
||||
toBottom: boolean;
|
||||
toLeft: boolean;
|
||||
toRight: boolean;
|
||||
}
|
||||
class TouchSlide {
|
||||
element;
|
||||
startCallback: ((event: TouchEvent) => void) | undefined;
|
||||
moveCallback: ((event: TouchEvent) => void) | undefined;
|
||||
endCallback: ((event: TouchEvent) => void) | undefined;
|
||||
isTouch;
|
||||
isTouchMove;
|
||||
|
||||
event: TouchEvent | undefined;
|
||||
rules = {
|
||||
toBottom: 0,
|
||||
toTop: 0,
|
||||
toLeft: 0,
|
||||
toRight: 0,
|
||||
isClickCallback: false,
|
||||
} as Rules;
|
||||
scrollDirection: number | undefined;
|
||||
startPos = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
time: 0,
|
||||
} as Position;
|
||||
movePos = [] as Position[];
|
||||
endPos = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
time: 0,
|
||||
} as Position;
|
||||
isInit = false;
|
||||
scrollDirectionSibling = 0;
|
||||
result = {
|
||||
toTop: false,
|
||||
toBottom: false,
|
||||
toLeft: false,
|
||||
toRight: false,
|
||||
};
|
||||
eventType = 'click';
|
||||
|
||||
constructor({
|
||||
element,
|
||||
rules,
|
||||
startCallback,
|
||||
moveCallback,
|
||||
endCallback,
|
||||
}: {
|
||||
element: HTMLElement;
|
||||
rules?: Rules;
|
||||
startCallback?: (event: TouchEvent) => void;
|
||||
moveCallback?: (event: TouchEvent) => void;
|
||||
endCallback?: (event: TouchEvent) => void;
|
||||
startPos?: Position;
|
||||
movePos?: Position[];
|
||||
endPos?: Position;
|
||||
scrollDirection?: number;
|
||||
scrollDirectionSibling?: number;
|
||||
result?: Result;
|
||||
eventType?: 'click' | 'slide';
|
||||
}) {
|
||||
this.element = element;
|
||||
this.startCallback = startCallback;
|
||||
this.moveCallback = moveCallback;
|
||||
this.endCallback = endCallback;
|
||||
|
||||
this.isTouch = false;
|
||||
this.isTouchMove = false;
|
||||
this.initRules(rules || {});
|
||||
|
||||
const options = {
|
||||
capture: false,
|
||||
passive: false,
|
||||
};
|
||||
|
||||
this.element.addEventListener('touchstart', this.eventStart.bind(this), options);
|
||||
this.element.addEventListener('touchmove', this.eventMove.bind(this), options);
|
||||
this.element.addEventListener('touchend', this.eventEnd.bind(this), options);
|
||||
}
|
||||
// 初始化规则,如果没有传参,则返默认.5
|
||||
initRules({ toBottom = 0, toTop = 0, toLeft = 0, toRight = 0 }) {
|
||||
if (!toRight && !toLeft && !toTop && !toBottom) {
|
||||
toRight = toLeft = toTop = toBottom = 0.3;
|
||||
}
|
||||
|
||||
this.rules = {
|
||||
toBottom,
|
||||
toTop,
|
||||
toLeft,
|
||||
toRight,
|
||||
};
|
||||
}
|
||||
isPassRules() {
|
||||
if (this.scrollDirection && (this.rules.toBottom || this.rules.toTop)) {
|
||||
return true;
|
||||
} else if (!this.scrollDirection && (this.rules.toRight || this.rules.toLeft)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getTargetTouche(event: TouchEvent) {
|
||||
return event.targetTouches[0];
|
||||
}
|
||||
|
||||
eventStart(event: TouchEvent) {
|
||||
this.event = event;
|
||||
const touch = this.getTargetTouche(event);
|
||||
|
||||
this.startPos = {
|
||||
x: touch.screenX,
|
||||
y: touch.screenY,
|
||||
time: +new Date(),
|
||||
};
|
||||
|
||||
this.movePos = [];
|
||||
this.endPos = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
time: 0,
|
||||
};
|
||||
this.scrollDirection = 0;
|
||||
this.isInit = false;
|
||||
this.isTouch = true;
|
||||
this.isTouchMove = false;
|
||||
|
||||
if (this.startCallback) {
|
||||
this.startCallback(event);
|
||||
}
|
||||
}
|
||||
|
||||
eventMove(event: TouchEvent) {
|
||||
this.event = event;
|
||||
|
||||
this.isTouchMove = true;
|
||||
|
||||
if (!this.isTouch) {
|
||||
return;
|
||||
}
|
||||
if (event.targetTouches.length > 1 || (event.scale && event.scale !== 1)) {
|
||||
return;
|
||||
}
|
||||
const touch = this.getTargetTouche(event);
|
||||
|
||||
this.endPos = {
|
||||
x: touch.screenX - this.startPos.x,
|
||||
y: touch.screenY - this.startPos.y,
|
||||
time: +new Date(),
|
||||
};
|
||||
|
||||
this.movePos.push(JSON.parse(JSON.stringify(this.endPos)));
|
||||
|
||||
if (!this.isInit) {
|
||||
this.isInit = true;
|
||||
this.scrollDirection = Math.abs(this.endPos.x) < Math.abs(this.endPos.y) ? 1 : 0;
|
||||
}
|
||||
|
||||
this.scrollDirectionSibling = this.scrollDirection ? (this.endPos.y < 0 ? 1 : -1) : this.endPos.x < 0 ? 1 : -1;
|
||||
|
||||
if (this.moveCallback && this.isPassRules()) {
|
||||
this.moveCallback(event);
|
||||
}
|
||||
}
|
||||
eventEnd(event: TouchEvent) {
|
||||
this.isTouch = false;
|
||||
if (!this.isPassRules() || !this.isTouchMove) {
|
||||
if (!this.isTouchMove) {
|
||||
this.eventType = 'click';
|
||||
} else {
|
||||
this.eventType = 'slide';
|
||||
}
|
||||
|
||||
if (this.rules.isClickCallback && this.endCallback) {
|
||||
this.endCallback(event);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.eventType = 'slide';
|
||||
|
||||
const movePos = this.movePos,
|
||||
movePosLen = movePos.length,
|
||||
endMovePos = movePos[movePosLen - 1],
|
||||
end2Second = movePos[movePosLen - 2];
|
||||
|
||||
let [toTop, toBottom, toLeft, toRight] = [false, false, false, false];
|
||||
if (endMovePos) {
|
||||
const rules = this.rules;
|
||||
const speed = end2Second
|
||||
? Math.abs(this.scrollDirection ? endMovePos.y - end2Second.y : endMovePos.x - end2Second.x) /
|
||||
(endMovePos.time - end2Second.time)
|
||||
: 0,
|
||||
slideSpeed = 2;
|
||||
const { innerHeight, innerWidth } = window;
|
||||
|
||||
if (this.scrollDirection) {
|
||||
if (this.scrollDirectionSibling >= 0) {
|
||||
toTop = speed > slideSpeed || rules['toTop'] * innerHeight <= Math.abs(endMovePos.y);
|
||||
} else {
|
||||
toBottom = speed > slideSpeed || rules['toBottom'] * innerHeight <= Math.abs(endMovePos.y);
|
||||
}
|
||||
} else {
|
||||
if (this.scrollDirectionSibling <= 0) {
|
||||
toRight = speed > slideSpeed || rules['toRight'] * innerWidth <= Math.abs(endMovePos.x);
|
||||
} else {
|
||||
toLeft = speed > slideSpeed || rules['toLeft'] * innerWidth <= Math.abs(endMovePos.x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.result = {
|
||||
toTop,
|
||||
toBottom,
|
||||
toLeft,
|
||||
toRight,
|
||||
};
|
||||
|
||||
if (this.endCallback) {
|
||||
this.endCallback(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TouchSlide;
|
||||
Reference in New Issue
Block a user