初始化

This commit is contained in:
谢宇宁
2026-09-15 15:25:05 +07:00
commit 77c16ab4a2
1310 changed files with 132250 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
<template>
<div class="main_wrapper">
<div class="main_content">
<router-view v-slot="{ Component }">
<keep-alive :include="mainViewKeep">
<component :is="Component" />
</keep-alive>
</router-view>
</div>
<!-- 右侧导航抽屉 -->
<Drawer elType="home" />
<Drawer elType="darkNet" />
<AccountCredentialsPopup v-if="isMine" />
<TabBar />
<PaymentGuidePopup
:show="vipContentGuideVisible"
:guide="vipContentGuide"
:scene="vipContentGuideScene"
@close="closeVipContentGuide"
/>
</div>
</template>
<script lang="ts">
export default {
name: 'mainView',
};
</script>
<script lang="ts" setup>
import { useGlobalStore } from '@/stores/global';
import { useConfigStore } from '@/stores/config';
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import PaymentGuidePopup from '@/components/PaymentGuidePopup/index.vue';
import { useVipContentUpdateGuideOnMainTabs } from '@/hooks/useVipContentUpdateGuideOnMainTabs';
const route = useRoute();
const globalStore = useGlobalStore();
const configStore = useConfigStore();
const {
visible: vipContentGuideVisible,
guide: vipContentGuide,
scene: vipContentGuideScene,
closeGuide: closeVipContentGuide,
} = useVipContentUpdateGuideOnMainTabs();
const mainViewKeep = computed(() => {
return globalStore.$state.mainViewKeep;
});
const backgroundTheme = computed(() => {
return configStore.appConfig.backgroundTheme;
});
const isMine = computed(() => {
let path = route.path;
if (path.indexOf('mine') > 0) {
return true;
}
return false;
});
</script>
<style lang="scss" scoped>
.main_wrapper {
height: 100%;
.main_content {
height: $contentHeight;
position: relative;
}
}
</style>
+481
View File
@@ -0,0 +1,481 @@
<template>
<div class="wrapper">
<!-- 广告轮播 -->
<SplashAdvSwiper
class="advSwiper"
:autoTime="2500"
:list="state.launchInfo"
:isShowIndicators="state.launchInfo && state.launchInfo.length >= 2"
v-if="state.launchInfo && state.launchInfo.length"
/>
<div class="countdown" v-if="state.countdown >= 0">
<span v-show="state.countdown > 0">{{ state.countdown }}</span>
<div class="jump_over" v-show="state.countdown <= 0" @click="goApp">跳过</div>
</div>
<EnterGroupPopup v-if="state.isCheckError" />
<!-- <UpgradePopup :show="state.isUpgrade" /> -->
<div class="selectLineBox" v-if="state.loading">
<van-loading color="#1989fa" size="30" />
<div class="selectLine">选线中...</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { AdType, SourceType } from '@/enums/app';
import AppApi from '@/service/app';
import { useAppStore } from '@/stores/app';
import { useConfigStore } from '@/stores/config';
import { useUserStore } from '@/stores/user';
import { getDevType } from '@/utils';
import { getAdItem } from '@/utils/getConfig';
import { showDialog } from 'vant';
import { reactive } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { jumpAdv } from '@/utils/index';
import { useGlobalStore } from '@/stores/global';
import axios from 'axios';
import setTheme from '@/assets/theme/index';
import { markHomeOldUserGuidePendingFromSplash } from '@/utils/paymentGuide';
const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
const userStore = useUserStore();
const configStore = useConfigStore();
const globalStore = useGlobalStore();
const state = reactive({
countdown: -1, //广告倒计时
launchInfo: <Array<Types.App.AdsInfoListItem>>[], //广告
timer: <any>null, //广告定时器
checkTimer: <any>null, //检测图片是否加载成功定时器
checkTime: 3, //图片加载成功检测时间
isCheckError: false, //是否选线失败
checkLineErrorNum: 0, //选线失败次数
isResLock: false, // 请求结果锁
resResult: <any>[], // 响应结果
pingCheckCount: 0, // 请求的ping失败次数
loading: true, // 选线loading
isUpgrade: true, //是否升级
});
/**
* @description 检测广告图片是否加载成功
*/
const checkImgLoad = () => {
state.checkTimer = setInterval(() => {
if (state.checkTime <= 0) {
clearInterval(state.checkTimer);
state.checkTimer = null;
if (!state.timer) {
countdownStrat();
}
return;
}
state.checkTime--;
}, 1000);
};
/**
* @description 广告失败
*/
const imgError = () => {
if (!state.timer) {
countdownStrat();
if (state.checkTimer) {
clearInterval(state.checkTimer);
state.checkTimer = null;
}
}
};
/**
* @description 广告加载成功
*/
const imgLoad = () => {
if (!state.timer) {
countdownStrat();
if (state.checkTimer) {
clearInterval(state.checkTimer);
state.checkTimer = null;
}
}
};
/**
* @description 启动倒计时
*/
const countdownStrat = () => {
state.loading = false;
state.countdown = 3;
// 倒计时期间预取首页壳+内容,缩短跳过/倒计时结束后的等待(不改 3s 时长)
void import('@/views/MainView.vue');
void import('@/views/lldhome/index.vue');
userStore.updateWallet(); // 获取用户钱包
globalStore.setTabs(); // 获取tabs数据
globalStore.setAnnounce(); // 获取跑马灯数据
// 预售活动开启了,获取预售信息
if (configStore.appConfig.advanceStatus && configStore.appConfig.advanceStatus.activityStatus) {
globalStore.setPresaleActivityDetail();
}
globalStore.setVideoRoadLine(configStore.sourceInfo.VID[0]); // 默认选择第一个线路
state.timer = setInterval(() => {
if (state.countdown <= 0) {
clearInterval(state.timer);
} else {
state.countdown--;
}
}, 1000);
};
/**
* @description 开机广告
*/
function adControl() {
const launchList = getAdItem(AdType.START);
if (launchList.length > 0) {
state.launchInfo = launchList;
}
// checkImgLoad();
countdownStrat();
}
/**
* @description 进入APP
*/
const goApp = () => {
// 仅启动后首次进首页允许弹老用户吸底;首页消费该标记
markHomeOldUserGuidePendingFromSplash();
// 同安卓:启动固定落在短剧(默认 AI短剧)
router.replace('/shortVideo');
let createAt = new Date(userStore.userInfo.createdAt).getTime();
let now = Date.now();
if (now - createAt > configStore.appConfig?.newbieSaleTime * 60 * 60 * 1000 && now - createAt > 3600000 * 24) {
getFirstLoginTime();
}
};
const getFirstLoginTime = () => {
const key = `first_login_time_${userStore.userInfo.uid}`; // userId 是登录用户的唯一标识
let firstLoginTime = localStorage.getItem(key);
firstLoginTime = Date.now().toString();
localStorage.setItem(key, firstLoginTime);
};
const toAdv = (item: any) => {
jumpAdv(item);
};
/**
* @description 如果没有用户ID就生成一个
*/
const generateUid = () => {
if (!appStore.uid) {
// 配合商店包,如果链接中存在设备ID,就当作uuid,没有就使用自动生成的
if (route.query.uuid && JSON.stringify(route.query.uuid).indexOf('null') == -1) {
appStore.setUid(route.query.uuid.toString());
} else {
appStore.generateUid();
}
}
};
/**
* @description 登陆
* @returns 是否登陆成功
*/
const login = async (): Promise<boolean> => {
try {
const uid = appStore.uid;
const sysType = getDevType();
const isAppStore = route.query.isAppStore ? true : false;
let cutInfos: any = undefined;
// 判断是否来自商店包
if (isAppStore) {
// 判断是否有pc值,也就是粘贴板是否有内容
const pcStr: any = route.query.pc || route.query.dc;
if (pcStr) {
// 解析pc的JSON字符串
const pc = JSON.parse(pcStr);
// 如果JSON对象有以下值,则直接传pc值
if (pc.dc || pc.pc || pc.ch) {
cutInfos = route.query.pc || route.query.dc;
}
}
} else {
// 非商店包,原本的读取方式
cutInfos = Object.keys(route.query).length > 0 ? JSON.stringify(route.query) : undefined;
}
let userInfo = await AppApi.login(uid, sysType, cutInfos, isAppStore);
await userStore.saveLoginResult({ userInfo, token: userInfo.token });
return true;
} catch (error) {
return false;
}
};
const restoreUserFromStorage = () => {
if (userStore.userInfo && userStore.token) return;
const userInfo = localStorage.getItem('userInfo');
const token = localStorage.getItem('token');
if (userInfo && token) {
userStore.saveToken(JSON.parse(token));
userStore.saveUserInfo(JSON.parse(userInfo));
}
};
const ensureAuthAndConfig = async (): Promise<boolean> => {
restoreUserFromStorage();
if (!(userStore.userInfo && userStore.token)) {
const loginOk = await login();
if (!loginOk) {
showDialog({
title: '温馨提示',
message: '用户登陆失败',
confirmButtonText: '重试',
theme: 'round-button',
}).then(() => {
appStore.$reset();
userStore.$reset();
configStore.$reset();
state.isResLock = false;
init();
});
return false;
}
}
const configOk = await getConfig();
if (!configOk) {
showDialog({
title: '温馨提示',
message: '获取配置信息失败',
confirmButtonText: '重试',
theme: 'round-button',
}).then(() => {
appStore.$reset();
userStore.$reset();
configStore.$reset();
userStore.saveToken('');
localStorage.removeItem('userInfo');
localStorage.removeItem('token');
init();
});
return false;
}
adControl();
return true;
};
/**
* @description 初始化资源域名配置
* @param list 资源列表
*/
const initSourceInfo = (list: Array<Types.App.SourceListItem>): void => {
let sourceInfo: Types.App.SourceInfo = {
GROUP: '',
GUIDE: '',
AUDIO: '',
QRCODE: '',
IMAGE: '',
VID: [],
LAND: '',
PROXYRULE: '',
FAQ: '',
WELFARE: '',
ACT: '',
AppStore: '',
UP: '',
PRESALEBGIMG: '',
};
for (let item of list) {
switch (item.type) {
case SourceType.GROUP:
sourceInfo.GROUP = item.domain[0]?.url;
break;
case SourceType.GUIDE:
sourceInfo.GUIDE = item.domain[0]?.url;
break;
case SourceType.AUDIO:
sourceInfo.AUDIO = item.domain[0]?.url;
break;
case SourceType.PRESALEBGIMG:
sourceInfo.PRESALEBGIMG = item.domain[0]?.url;
break;
case SourceType.IMAGE:
sourceInfo.IMAGE = item.domain[0]?.url;
break;
case SourceType.VID:
sourceInfo.VID = item.domain;
break;
case SourceType.LAND:
sourceInfo.LAND = item.domain[0]?.url;
break;
case SourceType.PROXYRULE:
sourceInfo.PROXYRULE = item.domain[0]?.url;
break;
case SourceType.FAQ:
sourceInfo.FAQ = item.domain[0]?.url;
break;
case SourceType.QRCODE:
sourceInfo.QRCODE = item.domain[0]?.url;
break;
case SourceType.WELFARE:
sourceInfo.WELFARE = item.domain[0]?.url;
break;
case SourceType.AppStore:
sourceInfo.AppStore = item.domain[0]?.url;
break;
case SourceType.ACT:
sourceInfo.ACT = item.domain[0]?.url;
break;
case SourceType.UP:
sourceInfo.UP = item.domain[0]?.url;
break;
default:
break;
}
}
configStore.saveSourceInfo(sourceInfo);
};
/**
* @description 获取配置信息
* @returns 获取配置信息是否成功
*/
const getConfig = async (): Promise<boolean> => {
try {
const configList = await AppApi.queryDomain();
configStore.saveAppConfig(configList);
globalStore.setAdvanceStatus(configList.advanceStatus);
initSourceInfo(configList.sourceList);
setTheme(configList.festivalUi);
return true;
} catch (error) {
state.isResLock = false;
return false;
}
};
const pingErrorPop = (hosts: any) => {
// 只有最后一条域名出错时,判断响应结果不存在true的时候,说明全部不通,即弹出选线失败
state.pingCheckCount++;
if (state.pingCheckCount == hosts.length) {
state.pingCheckCount = 0;
let index = state.resResult.indexOf(true);
if (index == -1) {
if (state.checkLineErrorNum == 1) {
state.isCheckError = true;
return;
}
showDialog({
title: '温馨提示',
message: '选线失败',
confirmButtonText: '重试',
theme: 'round-button',
}).then(() => {
state.checkLineErrorNum++;
appStore.$reset();
userStore.$reset();
configStore.$reset();
init();
});
}
}
};
/**
* 初始化
*/
const init = async () => {
generateUid();
route.query.isAppStore
? localStorage.setItem('isAppStore', route.query.isAppStore.toString())
: localStorage.setItem('isAppStore', ''); // 保存是否来自商店包,用于判断是否跳转外链
const baseUrls: string = process.env.VITE_BASE_URL || '[]';
const hosts = JSON.parse(baseUrls);
state.resResult = new Array(hosts.length);
hosts.forEach((url: any, index: number) => {
axios({
method: 'GET',
url: url + '/api/app/ping/check',
timeout: 1000 * 10, // 10秒超时
})
.then(async (res) => {
if (res.data.code == 200) {
if (!state.isResLock) {
state.isResLock = true;
state.resResult[index] = true;
//选线成功
useConfigStore().saveBaseUrl(url);
await ensureAuthAndConfig();
}
} else {
pingErrorPop(hosts);
}
})
.catch(() => {
pingErrorPop(hosts);
});
});
};
/**
* created
*/
init();
</script>
<style lang="scss" scoped>
.wrapper {
width: 100%;
height: 100%;
background: url('../assets/images/webp/splash.webp') no-repeat;
background-size: 100% 100%;
position: relative;
.advSwiper {
width: 100vw;
height: 100vh;
border-radius: 0px !important;
position: relative;
:deep(.adv-swiper-slide) {
height: 100% !important;
border-radius: 0px !important;
}
}
.countdown {
position: absolute;
top: 30px;
right: 25px;
z-index: 999;
width: 76px;
height: 36px;
border-radius: 18px;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 20px;
font-family: Inter;
font-weight: 800;
.jump_over {
font-size: 16px;
font-weight: 400;
}
}
.selectLineBox {
height: 120px;
width: 150px;
background-color: rgba(0, 0, 0, 0.7);
border-radius: 10px;
position: fixed;
z-index: 1;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.selectLine {
margin-top: 20px;
font-size: 16px;
}
}
}
</style>
+244
View File
@@ -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>
+133
View File
@@ -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>
+65
View File
@@ -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>
+216
View File
@@ -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>
+773
View File
@@ -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>
+761
View File
@@ -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>
+538
View File
@@ -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>
+358
View File
@@ -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>
+626
View File
@@ -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>
+468
View File
@@ -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>
+75
View File
@@ -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>
+59
View File
@@ -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 };
+129
View File
@@ -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 };
+66
View File
@@ -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,
};
};
+80
View File
@@ -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,
};
};
+63
View File
@@ -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 };
+242
View File
@@ -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;
+52
View File
@@ -0,0 +1,52 @@
<template>
<div class="activityWeb">
<div class="main">
<iframe v-if="activityUrl" class="iframe" :src="activityUrl" frameborder="0"></iframe>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { showToast } from 'vant';
const route = useRoute();
const router = useRouter();
/**
* 活动链接:仅接受 http(s) 外链;非法链接直接返回上一页,避免空白 iframe
*/
const activityUrl = computed(() => {
console.log(route.query);
const raw = route.query.url;
const url = typeof raw === 'string' ? raw : Array.isArray(raw) ? raw[0] : '';
if (!url || !/^https?:\/\//i.test(url?.trim())) {
return '';
}
return url?.trim();
});
onMounted(() => {
if (!activityUrl.value) {
showToast('活动链接无效');
router.back();
}
});
</script>
<style lang="scss" scoped>
.activityWeb {
height: 100%;
.main {
width: 100%;
height: 100%;
.iframe {
width: 100%;
height: 100%;
}
}
}
</style>
@@ -0,0 +1,119 @@
<template>
<div class="itemBox">
<template v-if="!state.loading">
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="listBox" v-for="(item, index) in state.list" :key="index">
<Post
class="post"
:postInfo="item"
:currentIndex="index"
:list="state.list"
:key="item"
:isNoShowTags="true"
:is-list-mode="true"
/>
</div>
</PullRefresh>
</template>
<div class="isLoading" v-else>
<Loading />
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, onActivated } from 'vue';
import VideoApi from '@/service/video';
const state = reactive({
pageNumber: 1,
pageSize: 12,
list: <any>[],
loading: true,
});
const props = defineProps<{
tagID: string;
sortType: number;
newsType: string;
}>();
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
onActivated(() => {
let datas = localStorage.getItem('operateData') ? JSON.parse(localStorage.getItem('operateData') as string) : '';
if (datas) {
state.list.forEach((v: any, index: any) => {
if (v.id == datas.id) {
state.list.splice(index, 1, datas);
}
});
}
});
const getList = async () => {
let req = {
pageNumber: state.pageNumber,
pageSize: state.pageSize,
tagID: props.tagID,
sortType: props.sortType,
newsType: props.newsType,
};
try {
let res: any = await VideoApi.searchTagPost(req);
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (state.pageNumber == 1) {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (!res.hasNext) {
moreParams.finished = true;
}
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
getList();
const onLoad = () => {
if (moreParams.refreshing) return;
moreParams.loading = true;
state.pageNumber++;
getList();
};
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
getList();
};
</script>
<style lang="scss" scoped>
.itemBox {
// height: calc(100vh - 90px);
padding: 0px 16px;
box-sizing: border-box;
.listBox {
margin-bottom: 20px;
}
.isLoading {
height: calc(100vh - 44px * 2);
}
}
</style>
@@ -0,0 +1,237 @@
<template>
<div class="communityTagDeatil">
<!-- <Header :title="scrollTop < 100 ? state.detail?.name : ''">
<div class="mess_info mess_info2" v-show="scrollTop > 100">
<div class="mess">
<lazyImg :src="state.detail?.coverImg" :key="state.detail?.coverImg" round class="avatar2" />
<div class="hader_title">{{ state.detail?.name || '-' }}</div>
</div>
<div class="unfollowbtn flex-center-center" @click="followUp" v-if="state.detail?.hasCollected">已关注</div>
<div class="followbtn flex-center-center" v-else @click="followUp">
<svgIcon name="follow_add"></svgIcon>
关注
</div>
</div>
</Header> -->
<Header :title="state.title" />
<div class="content overflow-y-auto" @scroll="onScroll">
<!-- <div class="mess_info">
<div class="mess">
<lazyImg :src="state.detail?.coverImg" :key="state.detail?.coverImg" class="avatar" />
<div class="title_box">
<div class="title_box_title">{{ state.detail?.name || '-' }}</div>
<div class="title_box_info">
<span>{{ watchCount(state.detail?.videoCount) }}个帖子</span>
</div>
<div class="title_box_info">
<span>{{ watchCount(state.detail?.playCount) }}浏览</span>
</div>
</div>
</div>
<div class="unfollowbtn flex-center-center" @click="followUp" v-if="state.detail?.hasCollected">已关注</div>
<div class="followbtn flex-center-center" v-else @click="followUp">
<svgIcon name="follow_add"></svgIcon>
关注
</div>
</div> -->
<van-tabs
v-model:active="state.tabActive"
class="communityTab"
title-active-color="#fff"
title-inactive-color="#747B80"
animated
:scrollspy="false"
swipeable
:lazy-render="true"
>
<van-tab title="最多收藏" :name="2">
<itemBox v-if="state.tabActive == 2" :tagID="state.tagID" :sortType="2" :newsType="state.newsType" />
</van-tab>
<van-tab title="最新上架" :name="1">
<itemBox v-if="state.tabActive == 1" :tagID="state.tagID" :sortType="1" :newsType="state.newsType" />
</van-tab>
<van-tab title="最新热评" :name="5">
<itemBox v-if="state.tabActive == 5" :tagID="state.tagID" :sortType="5" :newsType="state.newsType" />
</van-tab>
<!-- <van-tab title="精华">
<itemBox v-if="state.tabActive == 3" :id="state.tagID" :sort="4" />
</van-tab>
<van-tab title="视频">
<itemBox v-if="state.tabActive == 4" :id="state.tagID" :sort="5" />
</van-tab> -->
</van-tabs>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'communityTagDeatil',
};
</script>
<script lang="ts" setup>
import { reactive, onActivated, watch, nextTick, ref } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import CommunityApi from '@/service/community';
import { watchCount } from '@/utils/index';
import itemBox from './components/itemBox.vue';
import { showToast } from 'vant';
const router = useRouter();
const route = useRoute();
const state = reactive({
tabActive: 2,
detail: <any>{},
pageNumber: 1,
pageSize: 10,
loading: true,
tagID: route.query.id as string,
title: route.query.title,
newsType: (route.query.newsType as string) || '',
isChange: true,
});
// 获取帖子模块数据
const getList = async () => {
const data = {
tagID: state.tagID,
};
try {
let res: any = await CommunityApi.queryTagDetail(data);
state.loading = false;
state.detail = res;
} catch (e) {
console.log(e);
}
};
// getList();
// 关注
const followUp = async () => {
try {
let isCollect = state.detail.hasCollected;
let req = {
isCollect: !isCollect,
objID: state.detail.id,
type: 'tag',
};
await CommunityApi.collect(req);
state.detail.hasCollected = !isCollect;
showToast(`${state.detail.hasCollected ? '关注' : '取消'}成功`);
} catch (e) {
console.log(e);
}
};
const scrollTop = ref(0);
const onScroll = (e: Event) => {
scrollTop.value = (e.target as HTMLElement).scrollTop;
};
</script>
<style lang="scss" scoped>
.communityTagDeatil {
width: 100%;
height: 100%;
background: #0f0f0f;
.mess_info {
width: 100%;
padding: 12px 10px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
.mess {
flex: 1;
display: flex;
align-items: center;
.avatar {
width: 95px;
height: 93px;
overflow: hidden;
border-radius: 10px;
margin: 0 14px 0 0;
}
.avatar2 {
width: 30px;
height: 30px;
margin: 0 10px 0 0;
}
.hader_title {
font-size: 14px;
}
.title_box {
.title_box_title {
margin-bottom: 10px;
font-size: 14px;
}
.title_box_info {
font-size: 12px;
color: #999999;
}
.title_box_info:nth-of-type(2) {
margin-bottom: 10px;
}
}
}
.followbtn {
svg {
width: 12px;
height: 12px;
margin: 0 3px 0 0;
}
border: solid 1px #3476ff;
border-radius: 20px;
font-size: 12px;
color: #3476ff;
width: 60px;
height: 24px;
}
.unfollowbtn {
width: 60px;
height: 24px;
color: #fff;
background: #3476ff;
border-radius: 20px;
font-size: 12px;
}
}
.mess_info2 {
height: 100%;
padding: 0;
}
.content {
height: calc(100vh - 44px);
:deep(.van-tabs__wrap) {
position: sticky;
top: 0px;
left: 0;
z-index: 50;
padding: 0 20px;
}
:deep(.van-tabs__nav) {
background: transparent;
}
:deep(.van-tabs__line) {
display: none;
}
:deep(.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);
}
}
:deep(.van-tab--active) {
font-size: 14px;
// background: #3476ff;
// border-radius: 20px;
}
}
}
</style>
@@ -0,0 +1,291 @@
<template>
<div class="communityItem overflow-y-auto" :id="'community' + tabIndex" @scroll="onScroll">
<template v-if="!state.loading">
<!-- 广告轮播 -->
<HomeHeader :advType="AdType.COMMUNITYCAROUSEL" class="advBox" v-if="!props.pureVersion" />
<!-- 帖子模块 -->
<div class="postTagBox" v-if="state.taglist && state.taglist.length">
<div
class="postag_item"
:class="{ postag_active: state.tagActive == item.id }"
v-for="(item, index) in state.taglist"
:key="index"
@click="onTagClick(item)"
>
<lazyImg :src="item.coverImg" :key="item.coverImg" class="avatar" />
<div class="dialog">
<p>{{ item.tagName }}</p>
{{ watchCount(item.videoCount) }}个帖子
</div>
<div class="hotMark" v-if="item.hotMark">{{ item.hotMark }}</div>
</div>
</div>
<van-tabs
v-model:active="state.tabActive"
title-active-color="rgb(255,255,255)"
title-inactive-color="rgba( 113, 118 ,126)"
color="rgb(255,255,255)"
animated
:scrollspy="false"
swipeable
:lazy-render="true"
>
<van-tab :title="item.title" :name="item.moduleSort" v-for="(item, index) in state.tablist" :key="index">
<itemBox
ref="itemBoxRef"
v-if="state.tabActive == item.moduleSort"
:moduleId="props.moduleId"
:moduleSort="item.moduleSort"
:tagId="state.tagActive"
/>
</van-tab>
</van-tabs>
</template>
<Loading v-else />
</div>
</template>
<script lang="ts" setup>
import { nextTick, reactive, ref, watch } from 'vue';
import itemBox from './itemBox.vue';
import { AdType } from '@/enums/app';
import HomeApi from '@/service/home';
import { watchCount } from '@/utils/index';
const props = withDefaults(
defineProps<{
moduleId: string;
tabIndex?: number;
moduleName: string;
pureVersion?: boolean;
defaultTagId: string;
}>(),
{
moduleId: '',
pureVersion: false,
},
);
const itemBoxRef = ref<Array<any>>([]);
const state = reactive({
advList: <Array<Types.App.AdsInfoListItem>>[], // 广告轮播
taglist: <Array<any>>[], //帖子专题列表
pageNumber: 1,
pageSize: 10,
tabActive: 7,
loading: true,
tagActive: '',
tablist: [
{
title: '推荐',
moduleSort: 7,
},
{
title: '最新',
moduleSort: 1,
},
{
title: '最热',
moduleSort: 3,
},
{
title: '热评',
moduleSort: 9,
},
// {
// title: '精华',
// moduleSort: 2,
// },
// {
// title: '视频',
// moduleSort: 6,
// },
],
});
// 获取帖子模块数据
const getList = async () => {
const data = {
id: props.moduleId,
data: {
pageNumber: state.pageNumber,
pageSize: state.pageSize,
moduleSort: state.tabActive,
},
};
try {
let res: any = await HomeApi.getModuleList(data);
state.loading = false;
let list;
if (res.allSection.length) {
list = res.allSection[0].allTags || [];
} else {
list = [];
}
state.taglist = list;
} catch (e) {
console.log(e);
}
};
// 初始化
const init = () => {
getList();
};
init();
const onTagClick = (item: any) => {
if (state.tagActive == item.id) {
state.tagActive = '';
} else {
state.tagActive = item.id;
}
nextTick(() => {
if (itemBoxRef.value && itemBoxRef.value.length) {
itemBoxRef.value[0].resetList();
}
});
};
const scrollTop = ref(0);
const onScroll = (e: Event) => {
scrollTop.value = (e.target as HTMLElement).scrollTop;
};
watch(
() => props.defaultTagId,
(val) => {
if (val) {
state.tagActive = val;
}
},
{ immediate: true },
);
</script>
<style lang="scss" scoped>
.communityItem {
width: 100vw;
height: calc(100vh - 44px * 3);
.advBox {
padding: 0 16px;
}
.postTagBox {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-column-gap: 7px;
margin-top: 10px;
padding: 0 16px;
.postag_item {
margin: 0 0 10px 0;
height: 51px;
border-radius: 5px;
overflow: hidden;
position: relative;
.dialog {
position: absolute;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
font-size: 12px;
p {
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
}
}
.hotMark {
position: absolute;
top: 0;
left: 0;
font-size: 10px;
background-image: linear-gradient(to right, rgb(252, 118, 118), rgb(212, 39, 39));
border-radius: 0 0 8px 0;
padding: 1px 3px;
box-sizing: border-box;
}
}
.postag_active {
border: 1px solid #f68804;
.dialog {
color: #f68804;
}
}
}
:deep(.van-tabs) {
.van-tabs__wrap {
box-sizing: border-box;
box-sizing: border-box;
position: sticky;
top: -1px;
font-size: 14px;
z-index: 20;
.van-tabs__nav {
background: transparent;
padding: 0px 30px;
.van-tab {
width: 58px !important;
margin: 0 5px 0 0;
font-size: 14px !important;
}
}
.van-tabs__line {
display: none;
}
.van-tab--active {
font-size: 14px;
// background: #3476ff;
border-radius: 20px;
}
}
.van-tabs__content {
padding: 0 16px;
}
}
}
.communityItem_bg {
background: url('@/assets/images/png/communityItem_bg.png') no-repeat;
background-size: 100% 100%;
// :deep(.van-tabs) {
// .van-tabs__wrap {
// height: 50px !important;
// box-sizing: border-box;
// box-sizing: border-box;
// position: sticky;
// top: -1px;
// font-size: 14px;
// z-index: 20;
// .van-tabs__nav {
// background: transparent;
// padding: 10px 30px;
// .van-tab {
// height: 30px;
// width: 60px !important;
// margin: 0 5px 0 0;
// font-size: 14px !important;
// }
// }
// .van-tabs__line {
// display: none;
// }
// }
// .van-tabs__content {
// padding: 0 10px;
// }
// }
:deep(.van-tab--active) {
background: #950007 !important;
}
:deep(.choosen) {
background: #b40400;
}
:deep(.post_menu_right) {
color: #b40400 !important;
}
}
</style>
@@ -0,0 +1,262 @@
<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" /> -->
<!-- <div class="currency-icon"></div>-->
{{ gold }}金币
</div>
</div>
<div class="priceList split">
<div class="leader">我的金币</div>
<div class="price flex-vcenter">
{{ recharge }}
<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 MineApi from '@/service/mine';
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 为黑色主题,默认为白色主题
price?: string;
groupId?: string;
productId?: string;
}>(),
{
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 props.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 < Number(gold.value)) {
emit('close');
router.push('/wallet');
return;
}
let productID = props.productId;
if (!productID) {
showToast('购买出错~');
return;
}
openLoading();
MineApi.goldExchange({
productID: props.productId,
productType: 101,
})
.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,
},
});
});
}
});
} 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: #f5f5f5;
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: 1px solid rgba(255, 255, 255, 0.1);
margin-top: 18px;
padding-top: 18px;
}
.leader {
color: #757575;
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;
}
// .currency-icon {
// background: url('@/assets/images/png/currency-icon.png') no-repeat;
// background-size: 100% 100%;
// width: 18px;
// height: 18px;
// margin-right: 10px;
// }
}
.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: #fff;
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%;
border-radius: 3px;
background: #f68804;
text-align: center;
line-height: 44px;
font-size: 14px;
font-weight: 500;
color: #fff;
}
}
}
}
</style>
@@ -0,0 +1,29 @@
<template>
<div class="waterfall">
<CoverItem v-for="(item, index) in props.list" :key="item.id" :info="item" :index="index"></CoverItem>
</div>
</template>
<script lang="ts" setup>
import CoverItem from '@/views/community/coverPage/components/coverItem.vue';
const props = withDefaults(
defineProps<{
list: any;
}>(),
{
list: () => [],
},
);
</script>
<style lang="scss" scoped>
.waterfall {
padding: 6px 16px;
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: start;
:deep(.cover-item) {
width: 111px;
}
}
</style>
@@ -0,0 +1,181 @@
<template>
<!-- 广告轮播 -->
<div class="coverSeekType">
<HomeHeader
:advType="AdType.COMMUNITYCAROUSEL"
class="advBox"
v-if="!props.pureVersion"
/>
<van-tabs class="seek_tabs" v-model:active="state.tabActive" background="transparent" animated @change="tabCange">
<van-tab v-for="item in state.tabList" :key="item.type" :title="item.title" :name="item.type">
<template v-if="!state.loading">
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<CoverSeekList :list="state.list" />
</PullRefresh>
</template>
<div v-else class="isloading">
<Loading />
</div>
</van-tab>
</van-tabs>
</div>
</template>
<script setup lang="ts">
import { nextTick, reactive } from 'vue';
import CoverSeekList from './coverSeekList.vue';
import HomeApi from '@/service/home';
import CommunityApi from '@/service/community';
import { AdType } from '@/enums/app';
const props = withDefaults(
defineProps<{
moduleId: string;
pureVersion?: boolean;
}>(),
{
moduleId: '',
pureVersion: false,
},
);
const state = reactive({
tabActive: 3,
tabList: [
{ title: '最多观看', type: 3 },
{ title: '最新发布', type: 1 },
// { title: '本周最热', type: 2 },
{ title: '最多收藏', type: 7 },
],
list: <Array<any>>[],
loading: true,
pageNumber: 1,
pageSize: 12,
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 数据获取
const getList = async (type = '') => {
if (!props.moduleId) return;
let params;
if (props.moduleId == 'all') {
params = {
pageNumber: state.pageNumber,
pageSize: state.pageSize,
newsType: 'PIC',
sortType: state.tabActive,
};
} else {
params = {
id: props.moduleId,
data: { moduleSort: state.tabActive, coverType: 'cover', pageNumber: state.pageNumber, pageSize: state.pageSize },
};
}
try {
let res;
if (props.moduleId == 'all') {
res = await CommunityApi.getCommunityRecommend(params);
} else {
res = await HomeApi.getModuleList(params);
}
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
const list = res.allVideoInfo || [];
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
console.log(e);
}
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.loading = true;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
const tabCange = () => {
state.list = [];
state.loading = true;
reset();
};
</script>
<style scoped lang="scss">
.coverSeekType {
width: 100vw;
}
.advBox {
padding: 0 16px;
}
.seek_tabs {
:deep(> .van-tabs__wrap) {
position: sticky;
top: 0;
padding: 0 10px;
background: #0f0f0f;
--van-tab-text-color: #999;
z-index: 20;
.van-tabs__nav {
.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-tabs__line {
display: none;
// width: 10px;
// height: 2px;
// background: #f25477;
// border-radius: 99px;
// bottom: 24px;
}
.van-tab--active {
color: #ffffffe5;
}
}
.isloading {
height: calc(100vh - 230px);
}
}
</style>
+176
View File
@@ -0,0 +1,176 @@
<template>
<!-- 广告轮播 -->
<div class="gameItem">
<HeaderAdv class="advBox" :advType="AdType.COMMUNITYCAROUSEL" :length="10" />
<van-tabs class="game_tabs" v-model:active="state.tabActive" background="transparent" animated @change="tabCange">
<van-tab v-for="item in state.tabList" :key="item.type" :title="item.title" :name="item.type">
<template v-if="!state.loading">
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="moreMoviesBox">
<HorizontalGameBox class="gameBox" :videoInfo="item" v-for="item in state.list" :key="item.id" />
</div>
</PullRefresh>
</template>
<div v-else class="isloading">
<Loading />
</div>
</van-tab>
</van-tabs>
</div>
</template>
<script setup lang="ts">
import { nextTick, reactive } from 'vue';
import HomeApi from '@/service/home';
import CommunityApi from '@/service/community';
import { AdType } from '@/enums/app';
const props = withDefaults(
defineProps<{
moduleId: string;
}>(),
{
moduleId: '',
},
);
const state = reactive({
tabActive: 7,
tabList: [
{ title: '最新', type: 1 },
{ title: '最热', type: 2 },
{ title: '畅销', type: 8 },
],
list: <Array<any>>[],
loading: true,
pageNumber: 1,
pageSize: 12,
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 数据获取
const getList = async (type = '') => {
if (!props.moduleId) return;
let params;
if (props.moduleId == 'all') {
params = {
pageNumber: state.pageNumber,
pageSize: state.pageSize,
newsType: 'SEED_LINK',
sortType: state.tabActive,
};
} else {
params = {
id: props.moduleId,
data: { moduleSort: state.tabActive, coverType: 'cover', pageNumber: state.pageNumber, pageSize: state.pageSize },
};
}
try {
let res;
if (props.moduleId == 'all') {
res = await CommunityApi.getCommunityRecommend(params);
} else {
res = await HomeApi.getModuleList(params);
}
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
const list = res.allVideoInfo || [];
if (state.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
console.log(e);
}
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
state.pageNumber = 1;
moreParams.loading = true;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
reset();
// 触底加载
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList();
};
// 下拉刷新
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
const tabCange = () => {
state.list = [];
state.loading = true;
reset();
};
</script>
<style scoped lang="scss">
.gameItem {
width: 100vw;
}
.advBox {
padding: 0 16px;
}
.game_tabs {
:deep(> .van-tabs__wrap) {
position: sticky;
top: 0;
padding: 0 50px;
background: #030f18;
--van-tab-text-color: #999;
z-index: 20;
.van-tabs__nav {
.van-tab {
margin: 0 5px 0 0;
font-size: 14px;
}
}
.van-tabs__line {
display: none;
// width: 10px;
// height: 2px;
// background: #f25477;
// border-radius: 99px;
// bottom: 24px;
}
.van-tab--active {
color: #ffffffe5;
}
}
.isloading {
height: calc(100vh - 230px);
}
.moreMoviesBox {
display: grid;
grid-template-columns: 1fr 1fr;
grid-column-gap: 6px;
grid-row-gap: 12px;
padding: 0 16px;
.gameBox {
width: 168px;
}
}
}
</style>
+168
View File
@@ -0,0 +1,168 @@
<template>
<div class="itemBox">
<template v-if="!state.loading">
<van-pull-refresh
v-model="moreParams.refreshing"
@refresh="onRefresh"
:disabled="moreParams.disabled"
success-text="刷新成功"
:class="[{ 'my-refresh': moreParams.isHigehtMax, none: moreParams.refreshing }]"
>
<van-list
v-if="!moreParams.isNoData"
v-model:loading="moreParams.loading"
:finished="moreParams.finished"
finished-text="没有更多数据了"
@load="onLoad"
:offset="10"
v-model:error="moreParams.error"
:immediate-check="false"
error-text="请求失败,点击重新加载"
>
<div class="listBox" v-for="(item, index) in state.list" :key="index">
<Post
class="post"
:postInfo="item"
:currentIndex="index"
:list="state.list"
:key="item"
:isListMode="true"
/>
<!-- <div class="advBox">
<lazyImg
v-if="state.advsList.length > 0"
class="advImg"
:src="state.advsList[index % state.advsList.length].cover"
:key="state.advsList[index % state.advsList.length].cover"
@click="jumpAdv(state.advsList[index % state.advsList.length])"
/>
</div> -->
</div>
</van-list>
<div class="sizeBox" v-if="!moreParams.isNoData"></div>
<Empty v-if="moreParams.isNoData" />
</van-pull-refresh>
</template>
<div v-else class="isloading">
<Loading />
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
// import { AdType } from '@/enums/app';
// import { getAdItem } from '@/utils/getConfig';
import HomeApi from '@/service/home';
const props = withDefaults(
defineProps<{
moduleId: string;
moduleSort?: number;
tagId: string;
}>(),
{
moduleId: '',
},
);
const state = reactive({
// advsList: <Array<Types.App.AdsInfoListItem>>[], // 专栏之间的广告
list: <Array<Types.Home.SectionsItem>>[],
pageNumber: 1,
pageSize: 10,
loading: true,
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: true,
disabled: false,
});
// 获取帖子数据
const getList = async (type: any) => {
const data = {
id: props.moduleId,
data: {
pageNumber: state.pageNumber,
pageSize: state.pageSize,
moduleSort: props.moduleSort,
tagId: props.tagId || null,
},
};
try {
let res: any = await HomeApi.getModuleList(data);
moreParams.loading = false;
moreParams.refreshing = false;
state.loading = false;
let list = res.allVideoInfo || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1) {
moreParams.isNoData = list.length ? false : true;
}
if (!res.hasNext) {
moreParams.finished = true;
}
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
// 初始化
const init = () => {
// 专题之间的广告
// state.advsList = getAdItem(AdType.HOMETOPIC);
getList('');
};
init();
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList(null);
};
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
getList('refresh');
};
const resetList = () => {
state.loading = true;
state.list = [];
state.pageNumber = 1;
moreParams.loading = true;
moreParams.finished = false;
moreParams.isNoData = false;
getList('');
};
defineExpose({
resetList,
});
</script>
<style lang="scss" scoped>
.itemBox {
.isloading {
height: 400px;
}
.advBox {
margin-bottom: 20px;
}
.sizeBox {
height: 100px;
}
}
</style>
@@ -0,0 +1,166 @@
<template>
<div class="action-wrap">
<div class="wrap_item">
<div class="item" @click.stop="onLike">
<SvgIcon class="icon" v-if="info.vidStatus?.hasLiked" name="post_inheart" />
<SvgIcon class="icon" v-else name="post_heart" />
<span>{{ watchCount(info.likeCount) }}</span>
</div>
<div class="item" @click="onOpenComment">
<SvgIcon class="icon" name="post_commit" />
<!-- <span>{{ watchCount(info.commentCount) }}</span> -->
<span>{{ info.commentCount ? watchCount(info.commentCount) : '评论' }}</span>
</div>
<div class="item" @click="onCollect">
<SvgIcon class="icon" v-if="info.vidStatus?.hasCollected" name="post_followin" />
<SvgIcon class="icon" v-else name="post_follow" />
<span>{{ watchCount(info.collectCount) }}</span>
</div>
<div class="item" @click="openShare" style="margin-right: 0px">
<SvgIcon class="icon" name="post_share2" />
<span>分享</span>
</div>
</div>
<slot></slot>
<CommnentBox
v-if="props.isInitComment"
ref="dvComment"
class="comment"
:isOpen="visibleComment"
@close="visibleComment = false"
commentType="short"
></CommnentBox>
<AppSharePopup :is-open="state.visibleShare" @close="state.visibleShare = false" />
</div>
</template>
<script lang="ts">
export default {
name: 'Action',
};
</script>
<script lang="ts" setup>
import { reactive, ref, toRaw, watch } from 'vue';
import { watchCount } from '@/utils/index';
import CommunityApi from '@/service/community';
import CommnentBox from '@/components/CommentBox/index.vue';
import { useGlobalStore } from '@/stores/global';
import { useRouter } from 'vue-router';
import { showToast } from 'vant';
import { LikeType } from '@/enums/app';
const router = useRouter();
const globalStore = useGlobalStore();
const props = defineProps<{
info: Types.Video.VInfosItem;
isInitComment: boolean;
}>();
const emit = defineEmits(['update']);
const state = reactive({
videoInfo: props.info,
visibleShare: false,
});
const visibleComment = ref(false);
const dvComment = ref();
const onLike = () => {
try {
if (!state.videoInfo.vidStatus.hasLiked) {
let req = {
type: LikeType.PIC,
objID: state.videoInfo.id,
};
CommunityApi.zan(req).then(() => {
emit('update', 'vidStatus.hasLiked', true);
emit('update', 'likeCount', state.videoInfo.likeCount + 1);
showToast('点赞成功');
});
} else {
let req = {
type: LikeType.PIC,
objIDs: [state.videoInfo.id],
};
CommunityApi.cancleZan(req).then(() => {
emit('update', 'vidStatus.hasLiked', false);
emit('update', 'likeCount', state.videoInfo.likeCount - 1);
showToast('取消点赞');
});
}
} catch (e) {
console.log(e);
}
};
const onCollect = () => {
let isCollect = !state.videoInfo.vidStatus.hasCollected;
let req = {
objID: state.videoInfo.id,
isCollect,
type: LikeType.PIC,
};
CommunityApi.collect(req).then(() => {
emit('update', 'vidStatus.hasCollected', isCollect);
emit('update', 'collectCount', state.videoInfo.collectCount + (isCollect ? +1 : -1));
showToast(isCollect ? '收藏成功' : '取消收藏');
});
};
const onOpenComment = () => {
if (!props.isInitComment) return;
dvComment.value.state.videoInfo = toRaw(state.videoInfo);
dvComment.value.getCommentList();
visibleComment.value = true;
};
watch(
() => props.info,
(newVal) => {
state.videoInfo = newVal;
},
);
const openShare = () => {
let obj = {
id: state.videoInfo.id,
cover: state.videoInfo.cover,
};
globalStore.setShareInfo(obj);
state.visibleShare = true;
};
</script>
<style lang="scss" scoped>
.action-wrap {
width: 100%;
padding: 16px;
background: #000;
@include SafeArea('padding', 'bottom', 35px);
display: flex;
align-items: center;
.wrap_item {
display: flex;
align-items: center;
width: 270px;
justify-content: space-between;
}
.item {
flex-shrink: 0;
display: flex;
align-items: center;
font-size: 12px;
font-weight: 400;
line-height: 18px;
> span {
color: #999;
&.active {
color: $themeColor;
}
}
svg {
width: 20px;
height: 20px;
margin-right: 4px;
color: #989898;
}
}
}
</style>
@@ -0,0 +1,204 @@
<template>
<div class="cover-item" @click="goPage">
<lazyImg class="cover" :src="info.cover">
<div class="count-box">
<div class="play">
<svgIcon name="post_eye2" width="16" height="16" />
<span>{{ watchCount(info?.playCount) }}</span>
</div>
<div class="covers">
<svgIcon name="cover_pic" width="16" height="16" />
<span>{{ watchCount(info.seriesCover?.length) || 0 }}</span>
</div>
</div>
</lazyImg>
<div class="title-box">
<div class="title">{{ info.title }}</div>
</div>
<!-- <div class="goldLabel" v-if="info.originCoins > 0 && !info.watch?.isFreeWatch">
<SvgIcon name="loveCoin" />
<span>{{ info.coins }}</span>
</div> -->
</div>
</template>
<script lang="ts" setup>
import { watchCount } from '@/utils/index';
import { useRouter } from 'vue-router';
import VideoApi from '@/service/video';
import { computed, toRefs } from 'vue';
import { useUserStore } from '@/stores/user';
const router = useRouter();
const userStore = useUserStore();
const props = withDefaults(
defineProps<{
info: any;
index: number;
}>(),
{
info: {} as any,
index: 0,
},
);
const { info: video_info } = toRefs(props);
const computedCoverHeight = (info: any) => {
// if ((info.playTime || 0) > 300) {
// return 156 * ratio;
// } else {
// return 260 * ratio;
// }
// if (props.index === 0) {
// return 220;
// }
// return 130;
return 144;
};
// const ratio = window.outerWidth / 375;
const computedRatio = (info: any) => {
// const titleLen = (info.title || '').length;
// let height = 33;
// if (titleLen > (168 - 7 * 2) / 12) {
// height = 50;
// }
// const titleHeight = Math.ceil((height + 7) / ratio);
// return { gridRowEnd: `span ${Math.ceil(computedCoverHeight(info) / ratio) + titleHeight + 7}` };
return { gridRowEnd: `span ${Math.ceil(computedCoverHeight(info)) + 30}` };
};
let userFreeCountInit = false;
let userFreeCount = { isCan: false, watchCount: 0 };
const getFreeWatchCount = async () => {
if (!userFreeCountInit) {
await VideoApi.getPlayCount(props.info.id).then((userCount) => {
userFreeCount = userCount;
userFreeCountInit = true;
});
}
let req: any = {
publisher: props.info.publisher.uid,
videoID: props.info.id,
beginTime: 0,
endTime: 0,
playWay: userFreeCount.isCan ? (userFreeCount.watchCount ? 0 : 1) : 2,
via: 1,
longer: 0,
};
if (props.info.tags && props.info.tags.length > 0) {
req.tagID = props.info.tags[0].id;
}
VideoApi.getPlayVideo(req);
};
const PayType = computed(() => {
const { freeArea, vidStatus, originCoins } = video_info.value;
// 免费/购买
if (freeArea || vidStatus?.hasPaid) return 'free';
if (video_info.value.seriesCover?.length <= 9) return 'free';
// 会员
if (originCoins === 0 && !freeArea && !userStore.isVip) return 'vip';
// 购买
if (originCoins > 0 && !freeArea) return 'gold';
return 'free';
});
const goPage = () => {
getFreeWatchCount();
// if (PayType.value == 'free') {
router.push({ path: '/coverPreview', query: { index: 1, id: props.info.id } });
// } else {
// router.push({ path: '/coverDetail', query: { id: props.info.id } });
// }
};
</script>
<style lang="scss" scoped>
@import '@/assets/styles/mixins.scss';
.cover-item {
width: 138px;
display: flex;
flex-wrap: wrap;
position: relative;
margin-bottom: 12px;
.cover {
width: inherit;
background: #262626;
border-radius: 8px;
overflow: hidden;
height: 148px;
}
.count-box {
width: 100%;
height: 30px;
position: absolute;
bottom: 0;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 10px;
line-height: 18px;
color: #fff;
padding: 0 7px;
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, #000 100%);
.play {
display: flex;
align-items: center;
svg {
margin-right: 2px;
}
}
.covers {
display: flex;
align-items: center;
svg {
margin-right: 2px;
}
}
}
.title-box {
width: 100%;
// height: 50px;
// padding: 8px 7px;
.title {
font-size: 12px;
line-height: normal;
color: #fff;
overflow: hidden;
@include textEllipsis();
}
}
// .goldLabel {
// position: absolute;
// top: 0;
// right: 0;
// height: 15px;
// background: #ff1060;
// padding: 0 6px;
// display: flex;
// justify-content: center;
// align-items: center;
// border-radius: 0 0 0 4px;
// > svg {
// width: 11px;
// height: 11px;
// }
// span {
// font-size: 11px;
// color: #fff;
// padding-left: 3px;
// }
// }
}
</style>
@@ -0,0 +1,48 @@
<template>
<div class="type-tab">
<!-- <van-tabs v-model:active="tabActive" :animated="true" @rendered="onTabRendered" class="tabs">
<van-tab v-for="item in itemList" :key="item.key" :title="item.title" :name="prefixKey + item.key">
<Item v-if="tabIsInit(prefixKey + item.key)" :scrollId="prefixKey + item.key" :coverType="item.key"></Item>
</van-tab>
</van-tabs> -->
<Item :scrollId="prefixKey + 'cover'" coverType="cover" />
</div>
</template>
<script lang="ts" setup>
import {} from 'vue';
import Item from './index-typeTabContent.vue';
import tabAction from '@/utils/tabAction';
const itemList = [
{ title: '图集', key: 'cover' },
// { title: '网黄', key: 'actress' },
];
const prefixKey = 'cover_page_type_';
const { tabActive, tabIsInit, onTabRendered } = tabAction(true);
</script>
<style lang="scss" scoped>
@import '@/views/acgModule/assets/style.scss';
.type-tab {
@include vanTab-btn();
height: calc(100vh - 44px - 60px);
overflow: hidden;
:deep(.van-tabs) {
height: 100%;
display: flex;
flex-direction: column;
.van-tabs__wrap {
height: 20px;
padding: 12px 0;
background: #f7f7f7;
.van-tabs__nav {
padding: 0;
}
}
.van-tab__panel {
padding: 0 !important;
overflow: hidden;
height: 100%;
}
}
}
</style>
@@ -0,0 +1,250 @@
<template>
<div class="type-wrap">
<div class="nav">
<div
class="item"
:class="{ active: activeTab === item.sectionID }"
v-for="(item, index) in stickSection"
:key="index"
@click="onLeftTab(item)"
>
{{ item.sectionName }}
</div>
<div
class="item"
:class="{ active: activeTab === item.sectionID }"
v-for="item in allSection"
:key="item.sectionID"
@click="onLeftTab(item)"
>
{{ item.sectionName }}
</div>
</div>
<div class="tab_right">
<div class="right_tabs" v-if="activeTab != 'news' && activeTab != 'recommend'">
<div class="tabs_item" :class="sortType == 'watch' ? 'tabs_item_active' : ''" @click="onTabs('watch')">
最多观看
</div>
<div class="tabs_item" :class="sortType == 'new' ? 'tabs_item_active' : ''" @click="onTabs('new')">
最新发布
</div>
<div class="tabs_item" :class="sortType == 'collect' ? 'tabs_item_active' : ''" @click="onTabs('collect')">
最多收藏
</div>
</div>
<div class="right_list overflow-y-auto" :id="props.scrollId" @scroll="onScroll">
<PullRefresh v-if="!state.loading" :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<Waterfall v-if="props.coverType === 'cover'" :list="state.list"></Waterfall>
<Video v-else :list="state.list" :sectionID="activeTab"></Video>
</PullRefresh>
<Loading v-else />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { inject, Ref, ref, watchEffect, reactive, nextTick } from 'vue';
import HomeApi from '@/service/home';
import UserApi from '@/service/user';
import { useUserStore } from '@/stores/user';
import Video from './videoItem.vue';
import Waterfall from './waterfall.vue';
import TabPageScroll from '@/utils/resetTabPageScrollTop';
const tabPageScroll = new TabPageScroll();
const userStore = useUserStore();
const props = withDefaults(
defineProps<{
coverType: string;
scrollId: string;
}>(),
{
coverType: 'cover',
scrollId: '',
},
);
setTimeout(() => {
tabPageScroll.add(`#${props.scrollId}`);
}, 100);
const stickSection = [
// { sectionName: '我的收藏', sectionID: 'collect' },
{ sectionName: '最新', sectionID: 'news' },
{ sectionName: '推荐', sectionID: 'recommend' },
];
const allSection = inject('coverPage_allSection') as Ref<any[]>;
const moduleId = inject('coverPage_moduleId') as Ref<string>;
const activeTab = ref(stickSection[0].sectionID);
let api = (() => void 0) as any;
let apiParams = {};
let apiListKey = '';
const params = {
pageNumber: 1,
pageSize: 12,
};
const state = reactive({
loading: true,
list: <any>[],
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
const getList = async (type = '') => {
try {
const tempParams: any = Object.assign({}, apiParams);
if (tempParams.data) {
Object.assign(tempParams.data, params);
} else {
Object.assign(tempParams, params);
}
let res: any = await api(tempParams);
let list = res[apiListKey] || [];
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
if (params.pageNumber === 1) {
moreParams.isNoData = list.length ? false : true;
}
if (type === 'refresh') {
state.list = list;
} else {
state.list = [...state.list, ...list];
}
moreParams.finished = !res.hasNext;
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
const onLoad = () => {
moreParams.loading = true;
params.pageNumber++;
getList();
};
const onRefresh = () => {
moreParams.refreshing = true;
reset('refresh');
};
// 数据重置
const reset = (type = '') => {
nextTick(() => {
params.pageNumber = 1;
moreParams.loading = true;
moreParams.finished = false;
moreParams.isNoData = false;
getList(type);
});
};
watchEffect(() => {
const isCover = props.coverType === 'cover';
params.pageNumber = 1;
switch (activeTab.value) {
case 'collect':
apiListKey = 'list';
apiParams = { type: props.coverType === 'cover' ? 'img' : props.coverType, uid: userStore.userInfo.uid };
api = UserApi.queryCollectList;
break;
case 'news':
apiListKey = isCover ? 'allVideoInfo' : 'allActress';
apiParams = {
id: moduleId.value,
data: { coverSort: 'news', coverType: props.coverType },
};
api = HomeApi.getModuleList;
break;
case 'recommend':
apiListKey = isCover ? 'allVideoInfo' : 'allActress';
apiParams = {
id: moduleId.value,
data: { coverSort: 'recommend', coverType: props.coverType },
};
api = HomeApi.getModuleList;
break;
default:
apiListKey = isCover ? 'videos' : 'actresses';
apiParams = {
id: activeTab.value,
data: { coverType: props.coverType, sortType: sortType.value },
};
api = HomeApi.getVidSectionList;
break;
}
reset();
});
// watch最多播放 collect: 收藏 new:最新
const sortType = ref('watch');
const onTabs = (type: string) => {
state.list = [];
sortType.value = type;
reset();
};
const onLeftTab = (item: any) => {
state.list = [];
activeTab.value = item.sectionID;
};
const emit = defineEmits(['onScrollTop']);
const onScroll = (e: Event) => {
emit('onScrollTop', (e.target as Element).scrollTop);
};
</script>
<style lang="scss" scoped>
// @import '@/assets/styles/mixins.scss';
.type-wrap {
display: flex;
height: 100%;
.nav {
width: 72px;
height: inherit;
flex-shrink: 0;
overflow-y: auto;
.item {
font-size: 12px;
padding: 8px 2px;
text-align: center;
@include textEllipsis();
&.active {
background: $themeBgColor;
}
}
}
.tab_right {
flex: 1;
.right_tabs {
display: flex;
justify-content: space-around;
margin-bottom: 12px;
padding: 0px 36px;
.tabs_item {
border-radius: 99px;
color: #757575;
padding: 4px 12px;
line-height: 18px;
font-size: 12px;
}
.tabs_item_active {
color: #fff;
background: $themeColor;
}
}
.right_list {
padding: 0 8px;
height: calc(100vh - 50px) !important;
}
}
}
</style>
@@ -0,0 +1,190 @@
<template>
<div ref="dvLazyImg" class="lazt_image">
<transition name="bounceFade">
<img class="img_content" v-show="isLoad" :src="decyImg" @load="load" />
</transition>
<transition name="fade">
<div v-if="!isLoad" class="loading"></div>
</transition>
<slot />
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, watch, computed } 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 && isIntersecting) {
emit('updateIndex', { index: props.index, url: props.url });
}
},
{ threshold: 0.8 },
);
onMounted(() => {
observer.observe(dvLazyImg.value);
});
onBeforeUnmount(() => {
observer.unobserve(dvLazyImg.value);
});
const { sourceInfo, appConfig } = useConfigStore();
const decyImg = ref('');
function imgDecypt() {
try {
if (!props.url) {
return;
}
if (/^(blob:|data:)/.test(props.url)) {
decyImg.value = props.url;
return;
}
let imgSrc = '';
const isHttp = new RegExp('http').test(props.url);
isHttp == true ? (imgSrc = props.url) : (imgSrc = 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;
}
}
.loading {
width: 100%;
height: 100%;
background: url('@/assets/images/webp/place_holder_logo.webp') no-repeat;
background-size: 50% auto;
background-position: 50% 50%;
background-color: #343434;
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
img {
object-fit: cover !important;
}
}
.lazt_image {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
overflow: hidden;
.img_content {
width: 100%;
height: 100%;
object-fit: cover !important;
transition: transform 0.3s;
}
.wa_position0 {
top: 16px;
left: 16px;
}
.wa_position1 {
left: 50%;
top: 16px;
transform: translate(-50%, 0%);
}
.wa_position2 {
top: 16px;
right: 16px;
}
.wa_position3 {
left: 16px;
bottom: 50%;
transform: translate(0%, 50%);
}
.wa_position5 {
right: 16px;
bottom: 50%;
transform: translate(0%, 50%);
}
.wa_position6 {
bottom: 16px;
left: 16px;
}
.wa_position7 {
left: 50%;
bottom: 16px;
transform: translate(-50%, 0%);
}
.wa_position8 {
bottom: 16px;
right: 16px;
}
}
</style>
@@ -0,0 +1,83 @@
<template>
<div :class="['pay-wrap']" @click.stop="onPay">
全本完整作品共{{ props.surplus }}张
<div class="pay_gold" v-if="PayType == 'gold'">{{ videoInfo.coins }}金币解锁</div>
<div class="pay_vip" v-if="PayType == 'vip'">开通会员解锁</div>
</div>
<GoldPopup
:is-open="isGoldCoinPurchase"
:video-info="videoInfo"
@close="isGoldCoinPurchase = false"
@buy-success="buySuccess"
:isSale="false"
/>
<VipPopup :is-open="isJoinMerbershipShow" :video-info="videoInfo" @close="isJoinMerbershipShow = false" />
</template>
<script lang="ts" setup>
import { ref, onUnmounted, onDeactivated } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const props = defineProps<{
surplus: number;
PayType: string;
videoInfo: Types.Video.VInfosItem;
}>();
const isGoldCoinPurchase = ref(false);
const isJoinMerbershipShow = ref(false);
const onPay = () => {
if (props.PayType === 'free') return;
if (props.PayType === 'gold') {
isGoldCoinPurchase.value = true;
return;
}
if (props.PayType === 'vip') {
// isJoinMerbershipShow.value = true;
router.push('/member');
return;
}
};
const emit = defineEmits(['goldBuySuccess']);
const buySuccess = () => {
isGoldCoinPurchase.value = false;
emit('goldBuySuccess');
};
const closePopup = () => {
isGoldCoinPurchase.value = false;
isJoinMerbershipShow.value = false;
};
onUnmounted(() => {
closePopup();
});
onDeactivated(() => {
closePopup();
});
</script>
<style lang="scss" scoped>
.pay-wrap {
width: 100%;
border-radius: 8px;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
color: #fff;
font-size: 14px;
.pay_gold {
color: #ff9000;
margin-top: 16px;
text-decoration: underline;
}
.pay_vip {
color: #facc15;
margin-top: 16px;
text-decoration: underline;
}
}
</style>
@@ -0,0 +1,168 @@
<template>
<div class="video-wrap">
<div
class="video"
v-for="item in props.list"
:key="item.id"
@click="$router.push({ path: '/actressDetails', query: { id: item.id } })"
>
<lazyImg class="cover" :src="item.portrait">
<div class="stars">
<SvgIcon v-for="score in fullStars(item)" :key="score" name="star-full" />
<SvgIcon v-if="fullStars(item) !== halfStars(item)" name="star-half" />
</div>
<div class="action">
<span>
<SvgIcon v-if="item.hasCollected" name="commit_like" />
<SvgIcon v-else name="commit_unlike" />
{{ watchCount(item.collectCount) }}
</span>
<span>
<SvgIcon v-if="item.hasLiked" name="post_zan" />
<SvgIcon v-else name="post_unzan" />
{{ watchCount(item.likeCount) }}
</span>
</div>
</lazyImg>
<div class="info">
<p class="name">{{ item.name }}</p>
<div class="count">
<span>
<SvgIcon name="actress_video" />
{{ watchCount(item.videoCount) }}
</span>
<span>
<SvgIcon name="actress_image" />
{{ watchCount(item.picsCount) }}
</span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { watchCount } from '@/utils/index';
const props = withDefaults(
defineProps<{
list: any;
sectionID: string;
}>(),
{
list: () => [],
sectionID: '',
},
);
const fullStars = (item: any) => {
const { score } = item;
if (/\d+/.test(score)) {
return Math.floor(item.score / 2);
} else {
return 5;
}
};
const halfStars = (item: any) => {
const { score } = item;
if (/\d+/.test(score)) {
return Math.ceil(item.score / 2);
} else {
return 5;
}
};
</script>
<style lang="scss" scoped>
.video-wrap {
display: flex;
flex-wrap: wrap;
.video {
flex-shrink: 0;
display: flex;
width: 92px;
height: 138px;
flex-direction: column;
align-items: center;
flex-shrink: 0;
border-radius: 4px;
background: #f7f7f7;
margin-bottom: 8px;
&:not(:nth-child(3n)) {
margin-right: 5px;
}
.cover {
width: 92px;
height: 98px;
border-radius: 4px 4px 0 0;
overflow: hidden;
position: relative;
.stars {
position: absolute;
z-index: 2;
top: 4px;
right: 0;
display: flex;
justify-content: flex-end;
svg {
width: 6px;
height: 6px;
}
}
.action {
height: 20px;
width: 100%;
position: absolute;
z-index: 2;
bottom: 4px;
right: 0;
display: flex;
justify-content: space-between;
align-items: center;
color: #fff;
font-size: 10px;
padding: 0 4px;
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, #000 100%);
span {
display: flex;
justify-content: space-between;
align-items: center;
}
svg {
width: 13px;
height: 13px;
margin-right: 4px;
}
}
}
.info {
flex: 1;
padding: 4px 8px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
color: #000;
font-size: 10px;
width: 100%;
.count {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
span {
display: flex;
justify-content: space-between;
align-items: center;
svg {
width: 14px;
height: 14px;
margin-right: 4px;
color: $themeColor;
}
}
}
}
}
}
</style>
@@ -0,0 +1,26 @@
<template>
<div class="waterfall">
<CoverItem v-for="(item, index) in props.list" :key="item.id" :info="item" :index="index"></CoverItem>
</div>
</template>
<script lang="ts" setup>
import CoverItem from './coverItem.vue';
const props = withDefaults(
defineProps<{
list: any;
}>(),
{
list: () => [],
},
);
</script>
<style lang="scss" scoped>
.waterfall {
display: grid;
grid-auto-rows: 1px;
grid-template-columns: repeat(auto-fill, 142px);
align-items: start;
justify-content: space-between;
}
</style>
+272
View File
@@ -0,0 +1,272 @@
<template>
<div v-if="info.id" class="cover-detail">
<Header title="">
<div class="post_hader_title">
<div class="ellipsis1">{{ info.title }}</div>
<!-- <div class="title_user">
<div class="left" @click="toUserpage">
<lazyImg class="avatar" :src="info.publisher?.portrait" round :key="info.publisher?.portrait" />
<div class="info">
<p>
<span class="ellipsis">{{ info.publisher?.name }}</span>
<VipImg :isVip="info.publisher?.vipLevel" />
</p>
</div>
</div>
<div class="right-btn" @click="followUp" v-if="userStore.userInfo.uid != info.publisher?.uid">
<div class="post-top-right-follow flex-center-center" v-if="info.publisher?.hasFollowed">已关注</div>
<div class="post-top-right flex-center-center" v-else>
<svgIcon name="follow_add"></svgIcon>
关注
</div>
</div>
</div> -->
</div>
</Header>
<div class="page-content overflow-y-auto">
<!-- <div class="content">{{ info.title }}</div> -->
<!-- <div class="count">{{ info.seriesCover.length || 0 }}张</div> -->
<div class="list">
<lazyImg v-for="(url, index) in seriesCover" :key="index" class="item" :src="url" @click="toPath(index)" />
</div>
<Pay
v-if="PayType !== 'free'"
:surplus="surplus"
:PayType="(PayType as string)"
:videoInfo="info"
@goldBuySuccess="goldBuySuccess"
></Pay>
<Action :info="info" :isInitComment="true" @update="actionUpdate" class="detail-action"></Action>
</div>
</div>
<Loading v-else />
</template>
<script lang="ts">
export default {
name: 'coverDetail',
};
</script>
<script lang="ts" setup>
import { watch, computed, nextTick, onBeforeUnmount } from 'vue';
import Action from './components/action.vue';
import getVideoInfo from './utils/getVideoInfo';
import CommunityApi from '@/service/community';
import Pay from './components/pay.vue';
import { useRoute, useRouter } from 'vue-router';
import { showToast } from 'vant';
import { addVideoHistory } from '@/utils';
import useSwipeHook from '@/utils/swipeHook';
import { HistoryVideoType } from '@/enums/app';
const route = useRoute();
const router = useRouter();
// const dvComment = ref();
const { info, getData, clearData, actionUpdate, PayType, surplus, goldBuySuccess } = getVideoInfo();
clearData();
let id = '';
watch(
route,
() => {
// if (route.path != '/coverDetail') return;
if (route.path != '/coverPreview') return;
const { id: _id } = route.query;
if (!_id || id === _id) return;
id = _id as string;
info.value.id = '';
nextTick(async () => {
getData(route);
});
},
{
immediate: true,
},
);
const toUserpage = () => {
router.push({ path: '/userInfo', query: { uid: info.value.publisher.uid } });
};
const followUp = async () => {
try {
let req = {
isFollow: !info.value.publisher.hasFollowed,
followUID: info.value.publisher?.uid,
};
await CommunityApi.careOrcancle(req);
info.value.publisher.hasFollowed = !info.value.publisher.hasFollowed;
showToast(`${info.value.publisher.hasFollowed ? '关注' : '取消'}成功`);
} catch (e) {
console.log(e);
}
};
const seriesCover = computed(() => {
if (PayType.value === 'free') {
return info.value.seriesCover || [];
} else {
return info.value.seriesCover.slice(0, 9);
}
});
const toPath = (index: number) => {
if (PayType.value !== 'free' && seriesCover.value.length <= 9) {
showToast('请先解锁即可查看大图');
return;
}
router.push({ path: '/coverPreview', query: { index, id: route.query.id } });
};
watch(
() => PayType.value,
() => {
if (PayType.value == 'free') {
nextTick(() => {
router.replace({ path: '/coverPreview', query: { index: 1, id: route.query.id } });
});
}
},
);
onBeforeUnmount(() => {
addVideoHistory(info.value, HistoryVideoType.PIC, new Date().getTime());
});
useSwipeHook((e: any) => {
if (e === 'right') {
router.back();
}
});
</script>
<style lang="scss" scoped>
.cover-detail {
.post_hader_title {
display: flex;
height: 100%;
width: 100%;
align-items: center;
padding-left: 6px;
.title_text {
width: 100%;
text-align: center;
}
.title_user {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
.avatar {
width: 30px;
height: 30px;
margin-right: 12px;
}
}
}
.left {
display: flex;
align-items: center;
.info {
p {
font-size: 14px;
display: flex;
align-items: center;
span {
display: inline-block;
max-width: 200px;
margin-right: 5px;
}
.font {
font-size: 10px;
color: #1a1a1a;
background: #ffd300;
border-radius: 8px;
font-weight: bold;
padding: 1px 3px;
}
}
.location-box {
svg {
width: 16px;
height: 16px;
margin-right: 5px;
color: #fff;
}
font-size: 12px;
color: #999999;
margin-top: 5px;
}
}
}
.post-top-right {
svg {
width: 12px;
height: 12px;
margin: 0 3px 0 0;
}
border: solid 1px $themeColor;
border-radius: 20px;
font-size: 12px;
color: $themeColor;
width: 60px;
height: 24px;
}
.post-top-right-follow {
width: 60px;
height: 24px;
color: #c8c8c8;
background: #f2f2f2;
border-radius: 20px;
font-size: 12px;
}
.page-content {
padding: 0 15px;
height: calc(100dvh - 44px);
@include SafeArea('padding', 'bottom', 77px);
.content {
margin: 12px 0;
color: $textColor;
font-size: 14px;
font-weight: 600;
line-height: normal;
}
.count {
margin-bottom: 12px;
color: #9a9a9a;
font-size: 12px;
line-height: 18px;
}
.list {
display: flex;
flex-wrap: wrap;
padding-bottom: 13px;
.item {
width: 111px;
height: 111px;
border-radius: 8px;
overflow: hidden;
flex-shrink: 0;
margin-bottom: 5px;
margin-right: 6px;
&:nth-child(3n) {
margin-right: 0;
}
}
}
.comment {
margin-left: -16px;
margin-right: -16px;
}
}
.detail-action {
justify-content: center;
position: fixed;
left: 0px;
bottom: 0;
@include SafeArea('padding', 'bottom', 35px);
z-index: 2;
}
}
</style>
+99
View File
@@ -0,0 +1,99 @@
<template>
<div class="cover-wrap">
<Header title="套图站" calss="header" v-if="isHeader" />
<van-tabs
class="cover-tab"
v-model:active="tabActive"
animated
@rendered="onTabRendered"
@change="onTabChange"
scrollspy
>
<van-tab v-for="(item, index) in itemList" :key="item.id" :title="item.moduleName" :name="item.id">
<TypeTabContent
v-if="tabActive === item.id"
:scrollId="`${prefixKey}${index}`"
coverType="cover"
@onScrollTop="onScrollTop"
/>
</van-tab>
</van-tabs>
</div>
</template>
<script lang="ts">
export default {
name: 'coverPage',
};
</script>
<script lang="ts" setup>
import { ref, provide } from 'vue';
import TypeTabContent from './components/index-typeTabContent.vue';
import tabAction from '@/utils/tabAction';
import { useGlobalStore } from '@/stores/global';
import HomeApi from '@/service/home';
const { tabActive, onTabRendered } = tabAction();
const globalStore = useGlobalStore();
const prefixKey = 'cover_page_type_';
const itemList = ref([] as any);
const init = () => {
let tabObj: any = globalStore.tabObj;
itemList.value.push(...(tabObj.pics || []));
};
init();
const allSection = ref([] as any);
provide('coverPage_allSection', allSection);
provide('coverPage_moduleId', tabActive);
const onTabChange = async (id: string) => {
isHeader.value = true;
allSection.value = [];
const params = {
id: tabActive.value,
data: {
pageNumber: 1,
pageSize: 20,
},
};
const res = await HomeApi.getModuleList(params);
allSection.value = res.allSection || [];
};
const isHeader = ref(true);
const onScrollTop = (top: number) => {
isHeader.value = top > 44 ? false : true;
};
</script>
<style lang="scss" scoped>
.header {
box-shadow: none;
}
.cover-wrap {
&:deep(.cover-tab.van-tabs) {
--van-tabs-line-height: 36px;
--van-tab-text-color: #999;
--van-tab-active-text-color: $themeColor;
--van-tabs-bottom-bar-color: $themeColor;
--van-tabs-bottom-bar-width: 22px;
display: flex;
flex-direction: column;
height: 100%;
> .van-tabs__wrap {
margin-bottom: 12px;
.van-tab {
flex: none;
padding: 0 18px;
box-sizing: border-box;
.van-tabs__line {
width: 18px;
height: 2px;
bottom: 25px;
}
}
}
.van-tabs__content {
flex: 1;
overflow: hidden;
}
}
}
</style>
+325
View File
@@ -0,0 +1,325 @@
<template>
<Header calss="header" title="">
<div class="header_title ellipsis1">
{{ info.title }}
</div>
</Header>
<div class="overflow-y-auto theatres_detail" @scroll="onScroll" id="overflowY">
<LaztImage
v-for="(url, index) in imgList"
class="image_item"
:key="index"
:index="index"
:url="url"
@click="toPicPage(index)"
@updateIndex="updateIndex"
:init="index - currentIndex <= 1"
>
<template v-if="index === info.freeTime - 1">
<div class="play_box coins_box" v-if="PayType == 'gold'">
<div class="cions_title">
你只需花费
<span>{{ info?.coins }}</span>
金币
</div>
<div class="cions_tip">即可解锁继续查看</div>
<div class="cions_btn_box">
<div class="btn2" @click.stop="nowBuy">立即解锁</div>
</div>
</div>
<div class="play_box coins_box" v-if="PayType == 'vip'">
<div class="cions_title">试看结束</div>
<div class="cions_title">本内容需开通VIP免费观看</div>
<div class="cions_btn_box">
<div class="btn1" @click.stop="$router.push('/share')">邀请得3日VIP</div>
<div class="btn2" @click.stop="$router.push('/member')">开通VIP免费看</div>
</div>
</div>
</template>
</LaztImage>
<!-- 占位 -->
<div class="stand" />
<Action class="detail-action" :info="info" :isInitComment="true" @update="actionUpdate">
<div class="len">
<span class="red">{{ currentIndex }}</span>
/{{ info.seriesCover?.length }}
</div>
</Action>
</div>
</template>
<script lang="ts">
export default {
name: 'coverPreview',
};
</script>
<script setup lang="ts">
import { computed, nextTick, onActivated, onBeforeUnmount, onDeactivated, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import getVideoInfo from './utils/getVideoInfo';
import Action from './components/action.vue';
import LaztImage from './components/lazt-image.vue';
import { showToast } from 'vant';
import VideoApi from '@/service/video';
import { ProductType } from '@/enums/app';
import { useUserStore } from '@/stores/user';
import { addVideoHistory } from '@/utils';
import { HistoryVideoType } from '@/enums/app';
const { info, getData, actionUpdate, PayType, surplus, goldBuySuccess, clearData } = getVideoInfo();
const currentIndex = ref(1);
const userStore = useUserStore();
const router = useRouter();
onActivated(() => {
setScrollTop(scrollTop.value);
getData();
clearData();
});
onDeactivated(() => {
actionUpdate('id', '');
});
const imgList = computed(() => {
if (info.value.seriesCover && info.value.seriesCover.length) {
if (PayType.value === 'free') {
return info.value.seriesCover;
} else {
const count = info.value.freeTime ? info.value.freeTime : 8;
return info.value.seriesCover.slice(0, count);
}
} else {
return [];
}
});
// const isGoldCoinPurchase = ref(false);
// const isJoinMerbershipShow = ref(false);
// const onGoidCloes = () => {
// isGoldCoinPurchase.value = false;
// router.back();
// };
// const buySuccess = () => {
// isGoldCoinPurchase.value = false;
// };
// watch(
// () => PayType.value,
// () => {
// onPay();
// },
// );
// const onPay = () => {
// if (PayType.value === 'free') return;
// if (PayType.value === 'gold') {
// isGoldCoinPurchase.value = true;
// return;
// }
// if (PayType.value === 'vip') {
// isJoinMerbershipShow.value = true;
// return;
// }
// };
const updateIndex = ({ index, url }: { index: number; url: string }) => {
if (!scrollTop.value) {
currentIndex.value = 1;
} else {
currentIndex.value = Number(index) + 1;
}
};
const scrollTop = ref(0);
const onScroll = (event: any) => {
scrollTop.value = event.target.scrollTop;
};
// 设置top值
const setScrollTop = (top: number) => {
const overflowY = document.getElementById('overflowY');
nextTick(() => {
if (overflowY) {
setTimeout(() => {
overflowY.scrollTop = top;
});
}
});
};
const toPicPage = (index: any) => {
if (PayType.value !== 'free') {
showToast('解锁后可查看图片详情');
return info.value.seriesCover;
}
router.push({
path: '/picturePreview',
query: {
imgs: imgList.value,
index: index,
},
});
};
/**
* @description 视频购买
*/
const nowBuy = () => {
try {
let productID = info.value.id;
if (!productID) {
showToast('购买出错~');
return;
}
VideoApi.videoBuying({ productType: ProductType.VIDEO, productID: productID })
.then(async () => {
showToast('购买成功');
userStore.updateWallet();
goldBuySuccess();
})
.catch((e) => {
if (e.toString().indexOf('8000') != -1) {
router.push({
path: '/wallet',
query: {
id: info.value.id,
title: info.value.title || '无标题',
},
});
}
});
} catch (e) {
console.log(e);
}
};
onBeforeUnmount(() => {
addVideoHistory(info.value, HistoryVideoType.PIC, new Date().getTime());
});
</script>
<style scoped lang="scss">
.header {
box-shadow: none;
.header_title {
width: 100%;
justify-content: flex-start;
line-height: 44px;
padding-left: 6px;
}
}
.theatres_detail {
padding: 0px 0px 40px;
height: calc(100vh - 44px);
.stand {
height: 80px;
}
}
.image_item {
width: 100%;
min-height: 200px;
height: auto;
position: relative;
:deep(.animationImg) {
overflow: visible;
.loading {
height: 300px;
}
}
.play_box {
color: #fff;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
svg {
width: 45px;
height: 45px;
}
}
.coins_box {
background: rgba(0, 0, 0, 0.7);
.cions_title {
margin-bottom: 16px;
color: #fff;
font-size: 16px;
line-height: 22px;
span {
color: #f68804;
}
}
.cions_tip {
color: #fff;
font-size: 12px;
line-height: 18px;
margin-bottom: 16px;
}
.cions_btn_box {
display: flex;
align-items: center;
.btn1 {
background: linear-gradient(90deg, #ffe8be 1.65%, #e6b764 99.23%);
height: 30px;
padding: 0 16px;
border-radius: 99px;
display: flex;
align-items: center;
font-size: 12px;
margin-right: 16px;
}
.btn2 {
background: linear-gradient(286deg, #ff4d4d 0%, #ff6e6e 100%);
height: 30px;
padding: 0 16px;
border-radius: 99px;
display: flex;
align-items: center;
font-size: 12px;
}
.btn3 {
border: 1px solid #fff;
height: 36px;
padding: 0 16px;
border-radius: 99px;
display: flex;
align-items: center;
font-size: 14px;
margin-left: 16px;
}
}
}
}
.detail-action {
position: fixed;
left: 0;
bottom: 0;
z-index: 2;
:deep(.wrap_item) {
width: 260px;
}
.len {
position: absolute;
right: 12px;
min-width: 48px;
color: #000;
padding: 4px 12px;
border-radius: 99px;
background: #fff;
font-size: 12px;
// margin-left: 24px;
.red {
color: $themeColor;
}
}
}
</style>
@@ -0,0 +1,103 @@
import { ref, onUnmounted, computed, nextTick } from 'vue';
import { useRoute, RouteLocationNormalizedLoaded, useRouter } from 'vue-router';
import VideoApi from '@/service/video';
import { useUserStore } from '@/stores/user';
import { addVideoHistory } from '@/utils/index';
import { HistoryVideoType } from '@/enums/app';
export default () => {
const dataKey = 'coverDetail_';
const info = ref<Types.Video.VInfosItem>({});
const router = useRouter();
let videoID = '';
const getData = (_route?: RouteLocationNormalizedLoaded) => {
const route = _route || useRoute();
videoID = (route.query.id || '') as string;
if (!videoID) return;
VideoApi.queryVideoInfo({ videoID }).then((res) => {
info.value = res as Types.Video.VInfosItem;
info.value.freeTime = info.value.freeTime === 0 ? 8 : info.value.freeTime;
// if (info.value.seriesCover.length && route.query.index) {
// info.value.seriesCover.unshift(info.value.seriesCover.splice(Number(route.query.index), 1)[0]);
// }
// 添加浏览记录
addVideoHistory(info.value, HistoryVideoType.IMG, new Date().getTime());
});
};
const clearData = () => {
onUnmounted(() => {
sessionStorage.removeItem(dataKey + videoID);
});
};
function setValue(obj: Types.Video.VInfosItem, key: string, value: any) {
const keys = key.split('.');
keys.reduce((acc, currentKey, index) => {
if (index === keys.length - 1) {
acc[currentKey] = value;
} else if (acc && typeof acc === 'object' && currentKey in acc) {
return acc[currentKey];
} else {
acc[currentKey] = {};
return acc[currentKey];
}
}, obj);
}
const actionUpdate = (key: string, value: any) => {
setValue(info.value, key, value);
sessionStorage.setItem(dataKey + videoID, JSON.stringify(info.value));
};
// 支付
const userStore = useUserStore();
const PayType = computed(() => {
const { freeArea, vidStatus, originCoins } = info.value;
let isType;
// 免费/购买
if (freeArea || vidStatus?.hasPaid) {
isType = 'free';
return isType;
}
if (info.value.seriesCover?.length <= 8) {
isType = 'free';
return isType;
}
if (originCoins == 0 && !freeArea && userStore.userInfo.vipLevel) {
isType = 'free';
return isType;
}
if (originCoins == 0 && !freeArea && !userStore.userInfo.vipLevel) {
isType = 'vip';
return isType;
}
if (originCoins > 0 && !freeArea) {
if (userStore.userInfo.vipLevel <= 1) {
isType = 'gold';
return isType;
}
if (userStore.userInfo.vipLevel >= 2) {
isType = 'free';
return isType;
}
}
return isType;
});
const surplus = computed(() => {
if (PayType.value === 'free') return 0;
else {
const total = (info.value.seriesCover || []).length;
// const count = total - 9;
const count = total;
return count < 0 ? 0 : count;
}
});
const goldBuySuccess = () => {
info.value.vidStatus.hasPaid = true;
nextTick(() => {
router.replace({ path: '/coverPreview', query: { index: 1, id: info.value.id } });
});
};
return { info, getData, clearData, actionUpdate, PayType, surplus, goldBuySuccess };
};
+650
View File
@@ -0,0 +1,650 @@
<template>
<div class="gameDetails">
<Header :title="state.gameDetails.title || '游戏详情'" />
<div class="main_box overflow-y-auto" id="gameDetailsId">
<template v-if="!state.loading">
<div class="padding16">
<!-- <div class="coverBox">
<lazyImg class="cover" :src="item" v-for="item in state.gameDetails.seriesCover || []" :key="item" />
</div> -->
<!-- <div class="videoBox" v-if="state.gameDetails.sourceURL">
<lazyImg class="videoCover" :src="state.gameDetails.cover" :key="state.gameDetails.cover" />
<div class="mask flex-center-center" @click="openVideo()">
<SvgIcon name="playIcon1" />
</div>
</div> -->
<SwiperSlide :list="state.gameDetails.seriesCover || []" />
<div class="subTitle">游戏简介:</div>
<div class="describe" v-html="state.gameDetails?.content.replaceAll('\n', '<br/>')"></div>
<!-- 会员模块 -->
<template v-if="state.gameDetails.originCoins == 0 && !state.gameDetails.freeArea">
<div class="flex-center-center download" v-if="isVip" @click="copyLink">
点击复制
<span>「种子资源」</span>
</div>
<div class="gameLinkBox" v-else>
<svg width="100%" height="100%" class="gameLinkBorder">
<rect
x="0"
y="0"
width="100%"
rx="12"
ry="15"
height="100%"
fill="none"
stroke="#c781ff"
stroke-width="2"
stroke-dasharray="8, 6"
/>
</svg>
<div class="Link_content flex-center-center">内容已隐藏,解锁后查看</div>
</div>
<div class="payBtn" v-if="!isVip" @click="$router.push('member')">开通VIP立即解锁</div>
</template>
<!-- 金币模块 -->
<template
v-else-if="state.gameDetails.originCoins > 0 && !state.gameDetails.freeArea && state.gameDetails.coins != 0"
>
<div class="flex-center-center download" v-if="state.gameDetails.vidStatus?.hasPaid" @click="copyLink">
点击复制
<span>「种子资源」</span>
</div>
<div class="gameLinkBox" v-else>
<svg width="100%" height="100%" class="gameLinkBorder">
<rect
x="0"
y="0"
width="100%"
height="100%"
fill="none"
stroke="#c781ff"
stroke-width="2"
stroke-dasharray="8, 6"
/>
</svg>
<div class="Link_content flex-center-center">内容已隐藏,解锁后查看</div>
</div>
<div class="payBtn" v-if="!state.gameDetails.vidStatus?.hasPaid" @click="nowBuy">
{{ state.gameDetails.originCoins }}金币/立即解锁
</div>
</template>
<!-- 免费模块 -->
<template
v-else-if="
state.gameDetails.freeArea || (state.gameDetails.coins == 0 && state.gameDetails.originCoins != 0)
"
>
<div class="flex-center-center download" @click="copyLink">
点击复制
<span>「种子资源」</span>
</div>
</template>
<div class="tagBox">
<div class="tag" @click.stop="jumpTag(item)" v-for="item in state.gameDetails?.tags" :key="item.id">
{{ item.name }}
</div>
</div>
<!-- 操作栏 -->
<div class="post_menu">
<div class="item">
<SvgIcon class="icon" name="post_eye" />
<span class="ellipsis">{{ watchCount(state.gameDetails.pageViewCount) }}</span>
</div>
<div class="item" @click.stop="clickCollect">
<SvgIcon class="zan" v-if="state.gameDetails.vidStatus?.hasCollected" name="post_followin" />
<SvgIcon class="icon" v-else name="post_follow" />
<span class="ellipsis">{{ watchCount(state.gameDetails.collectCount) }}</span>
</div>
<div class="item" @click.stop="clickLike">
<SvgIcon class="zan" v-if="state.gameDetails.vidStatus?.hasLiked" name="post_inheart" />
<SvgIcon class="icon" v-else name="post_heart" />
<span class="ellipsis">{{ watchCount(state.gameDetails.likeCount) }}</span>
</div>
<div class="item">
<SvgIcon class="icon" name="post_lock" />
<span class="ellipsis">{{ watchCount(state.gameDetails.purchaseCount) }}</span>
</div>
</div>
<div class="horizontalLine" />
</div>
<div class="tabArea">
<div class="commentCount" :class="state.tabActive == 2 ? 'commentCountSelected' : 'commentCountUnSelect'">
{{ watchCount(state.commentCount) }}
</div>
<van-tabs
v-model:active="state.tabActive"
class="oepnX tabs"
title-active-color="#fff"
title-inactive-color="#989898"
animated
:scrollspy="false"
swipeable
:lazy-render="true"
>
<van-tab :title="item.label" v-for="item in state.tabList" :key="item.value" :name="item.value">
<Introduction
:video-info="state.gameDetails"
ref="introRef"
v-if="item.value == 1 && JSON.stringify(state.gameDetails) != '{}'"
/>
<CommentBox
v-if="item.value == 2 && JSON.stringify(state.gameDetails) != '{}'"
ref="dvComment"
:isOpen="true"
renderType="normal"
theme="black"
class="comment-comp"
SendBtnTeleportEl="#gameSendBox"
commentType="short"
></CommentBox>
</van-tab>
</van-tabs>
</div>
<div v-show="state.tabActive == 2" id="gameSendBox" />
</template>
<Loading v-else />
</div>
<van-overlay class="showPlayVideoPopup" :show="state.showPlayVideo" z-index="9999">
<SvgIcon name="back" class="close" @click="close" />
<SmallVideo
v-if="state.showPlayVideo"
class="playVideoBox"
:sourceURL="state.sourceURL"
:h265Url="state.h265Url"
:key="state.sourceURL"
/>
</van-overlay>
<!-- 金币弹窗 -->
<GoldPopup
:is-open="state.isGoldCoinPurchase"
:video-info="state.gameDetails"
@close="closeGoldCoin"
title="购买下载地址"
:isSale="false"
@buy-success="buySuccess"
/>
</div>
<!-- 分享弹窗 -->
<!-- <SharePopupPost :sourceURL="state.gameDetails.sourceURL" /> -->
</template>
<script lang="ts">
export default {
name: 'gameDetails',
};
</script>
<script setup lang="ts">
import { ref, reactive, watch, toRaw } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { watchCount, addVideoHistory } from '@/utils';
import { showToast } from 'vant';
import { useUserStore } from '@/stores/user';
import { useGlobalStore } from '@/stores/global';
import { LikeType } from '@/enums/app';
import VideoApi from '@/service/video';
import CommunityApi from '@/service/community';
import toClipboard from '@/utils/clipboard';
import Introduction from './introduction.vue';
import SwiperSlide from './swiper_slide.vue';
import { HistoryVideoType } from '@/enums/app';
import TabPageScroll from '@/utils/resetTabPageScrollTop';
const tabPageScroll = new TabPageScroll();
setTimeout(() => {
tabPageScroll.add(`#gameDetailsId`);
}, 100);
const route = useRoute();
const router = useRouter();
const globalStore = useGlobalStore();
const introRef = ref();
const { isVip } = useUserStore();
const state = reactive({
gameDetails: <any>{},
tabList: <any>[
{
label: '相关推荐',
value: 1,
},
{
label: '评论',
value: 2,
},
],
tabActive: 0,
showPlayVideo: false,
sourceURL: '',
h265Url: '' as string | null,
content: '', // 评论内容
commentCount: 0, // 评论数
placeholder: '请输入评论内容',
childrenCommentObj: <any>null,
loading: true,
refreshLoading: false, // 刷新loading
isGoldCoinPurchase: false, // 金币购买
});
const getDetails = async () => {
try {
let req = {
videoID: route.query.id,
};
let res: any = await VideoApi.queryVideoInfo(req);
state.loading = false;
state.refreshLoading = false;
state.gameDetails = res;
state.commentCount = res.commentCount;
addVideoHistory(res, HistoryVideoType.SEEKBRANCHSTORY, new Date().getTime()); // 保存历史记录
} catch (e) {
console.log(e);
}
};
const init = () => {
getDetails();
};
init();
const openVideo = () => {
state.sourceURL = state.gameDetails.sourceURL;
state.h265Url = state.gameDetails.h265Url || '';
state.showPlayVideo = true;
};
const close = () => {
state.showPlayVideo = false;
};
// 复制链接
const copyLink = async () => {
try {
if (!state.gameDetails.seedLinkUrl) {
showToast('复制失败');
return;
}
await toClipboard(state.gameDetails.seedLinkUrl); //实现复制
showToast('链接已复制');
} catch (e: any) {
console.log(e);
}
};
/**
* @description 帖子购买
*/
const nowBuy = () => {
state.isGoldCoinPurchase = true;
};
// 标签跳转
const jumpTag = (item: any) => {
// router.push({
// name: 'gameTagDetails',
// query: {
// id: item.id,
// name: item.name,
// },
// });
};
const closeGoldCoin = () => {
state.isGoldCoinPurchase = false;
};
const buySuccess = () => {
closeGoldCoin();
getDetails();
};
// 点赞
const clickLike = async () => {
try {
if (!state.gameDetails.vidStatus.hasLiked) {
let req = {
type: LikeType.SEED_LINK,
objID: state.gameDetails.id,
};
await CommunityApi.zan(req);
state.gameDetails.vidStatus.hasLiked = true;
state.gameDetails.likeCount++;
showToast('点赞成功');
} else {
let req = {
type: LikeType.SEED_LINK,
objIDs: [state.gameDetails.id],
};
await CommunityApi.cancleZan(req);
state.gameDetails.vidStatus.hasLiked = false;
state.gameDetails.likeCount--;
showToast('取消成功');
}
} catch (e) {
console.log(e);
}
};
// 收藏
const clickCollect = async () => {
const params = {
type: LikeType.SEED_LINK,
objID: state.gameDetails.id,
isCollect: !state.gameDetails.vidStatus.hasCollected,
};
try {
await CommunityApi.collect(params);
if (!state.gameDetails.vidStatus.hasCollected) {
state.gameDetails.collectCount++;
} else {
state.gameDetails.collectCount--;
}
showToast(!state.gameDetails.vidStatus.hasCollected ? '收藏成功' : '取消搜藏');
state.gameDetails.vidStatus.hasCollected = !state.gameDetails.vidStatus.hasCollected;
} catch (e) {
console.log(e);
}
};
// const openShare = () => {
// let obj = {
// id: state.gameDetails.id,
// cover: state.gameDetails.cover ? state.gameDetails.cover : state.gameDetails.seriesCover[0],
// };
// globalStore.setShareInfo(obj);
// globalStore.setShowShare(true);
// };
const dvComment = ref();
const initComment = () => {
dvComment.value[0].state.videoInfo = toRaw(state.gameDetails);
dvComment.value[0].getCommentList();
};
watch(dvComment, (el) => {
if (!el) return;
initComment();
});
</script>
<style scoped lang="scss">
.back {
position: fixed;
left: 14px;
top: 12px;
z-index: 99;
> svg {
height: 20px;
width: 20px;
}
}
.gameDetails {
height: 100%;
.header {
background: #000;
height: 44px;
padding: 0 14px;
.back1 {
position: fixed;
left: 14px;
top: 12px;
> svg {
height: 20px;
width: 20px;
}
}
.title {
color: #fff;
font-size: 16px;
max-width: 310px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
.padding16 {
padding: 0 16px;
}
.main_box {
height: calc(100% - 44px);
padding-top: 16px;
.coverBox {
.cover {
margin-bottom: 10px;
}
}
.videoBox {
height: 190px;
position: relative;
.videoCover {
height: 100%;
width: 100%;
}
.mask {
height: 100%;
width: 100%;
position: absolute;
left: 0;
top: 0;
z-index: 1;
background: rgba(0, 0, 0, 0.7);
> svg {
height: 50px;
width: 50px;
}
}
}
.subTitle {
color: #dcdcdc;
font-size: 16px;
margin: 16px 0;
}
.describe {
color: #dcdcdc;
font-size: 14px;
line-height: 20px;
margin-top: 10px;
}
.download {
border: dashed 1px #ffffff66;
margin: 20px auto;
border-radius: 99px;
width: 180px;
height: 44px;
font-size: 14px;
span {
display: inline-block;
color: #e1351f;
}
}
.gameLinkBox {
margin: 20px 0;
padding: 12px;
position: relative;
.gameLinkBorder {
position: absolute;
left: 0;
top: 0;
}
.Link_content {
width: 100%;
min-height: 100px;
word-break: break-all;
line-height: 18px;
font-size: 14px;
border-radius: 8px;
background: #1c272f;
padding: 12px;
.red {
color: #ff004f;
}
}
}
.payBtn {
height: 44px;
width: 100%;
border-radius: 6px;
background: #e1351f;
line-height: 44px;
text-align: center;
font-size: 16px;
margin: 0 auto 20px;
}
.tagBox {
display: flex;
flex-wrap: wrap;
gap: 10px;
.tag {
color: #999;
font-size: 12px;
display: flex;
padding: 7px 12px;
border-radius: 4px;
background: #22252d;
width: fit-content;
}
}
.infoBar {
margin: 20px 0 10px;
.infoBox {
display: flex;
align-items: center;
margin-right: 18px;
.infoIcon {
height: 20px;
width: 20px;
margin-right: 2px;
}
.unlock {
height: 16.667px;
width: 14.167px;
margin-right: 5px;
}
.infoQuantity {
color: rgba(255, 255, 255, 0.4);
font-size: 12px;
}
}
}
.tabArea {
position: relative;
.commentCount {
position: absolute;
left: 150px;
top: 13px;
font-size: 14px;
z-index: 1;
}
.commentCountUnSelect {
color: #989898;
}
.commentCountSelected {
color: #e1351f;
}
.tabs {
:deep(.van-tabs__wrap) {
padding: 0 16px;
.van-tabs__nav--line {
padding-bottom: 0;
}
.van-tabs__nav {
background: unset;
.van-tab {
font-size: 16px;
padding: unset;
width: 60px;
flex: unset;
}
.van-tab:nth-child(1) {
width: 80px;
}
.van-tab--active {
font-size: 18px;
font-weight: 600;
}
.van-tabs__line {
width: 16px;
height: 4px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
background: $themeColor;
bottom: 4px;
}
}
}
}
}
}
#gameSendBox {
position: fixed;
bottom: 0;
left: 0;
z-index: 10;
width: 100%;
}
.comment-comp {
padding-bottom: 70px;
:deep(.wrapper) {
height: auto;
.headerBox {
// display: none;
padding-top: 10px;
height: auto;
.horizontalLineBlock {
display: none;
}
.title {
margin: 0;
}
}
.main {
background: transparent;
height: auto;
overflow: hidden !important;
}
}
}
.post_menu {
display: flex;
align-items: center;
justify-content: space-between;
width: 270px;
margin: 16px 0;
.item {
display: flex;
align-items: center;
.icon {
width: 20px;
height: 20px;
margin-right: 5px;
color: #989898;
}
.zan {
width: 18px;
height: 18px;
color: transparent;
margin-right: 5px;
}
.ellipsis {
font-size: 12px;
color: #989898;
}
}
}
.horizontalLine {
height: 1px;
background-color: #ffffff1a;
margin: 16px 0;
}
.showPlayVideoPopup {
height: 100%;
width: 100%;
> svg {
position: absolute;
left: 16px;
top: 16px;
z-index: 99;
height: 24px;
width: 24px;
}
.playVideoBox {
height: 100%;
width: 100%;
}
}
}
</style>
@@ -0,0 +1,98 @@
<template>
<div class="introduction">
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="moreMoviesBox">
<HorizontalGameBox class="gameBox" :videoInfo="item" v-for="item in state.list" :key="item.id" />
</div>
</PullRefresh>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import VideoApi from '@/service/video';
const props = withDefaults(
defineProps<{
videoInfo: any;
}>(),
{
videoInfo: {},
},
);
const state = reactive({
videoInfo: props.videoInfo,
pageNumber: 1,
pageSize: 10,
list: <Array<Types.Video.VInfosItem>>[],
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 获取更多视频
const getList = async (type: any) => {
let req = {
pageNumber: state.pageNumber,
pageSize: state.pageSize,
vid: state.videoInfo.id,
};
try {
let res: any = await VideoApi.queryRecommend(req);
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && 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;
state.pageNumber++;
getList(null);
};
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
moreParams.isNoData = false;
getList('refresh');
};
const init = () => {
getList('');
};
init();
defineExpose({
onRefresh,
});
</script>
<style lang="scss" scoped>
.introduction {
padding: 12px 16px;
height: 100%;
.moreMoviesBox {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.gameBox {
width: 168px;
margin-bottom: 10px;
}
}
}
</style>
@@ -0,0 +1,78 @@
<template>
<div class="swiper_box" @touchmove.stop>
<swiper
v-if="state.list.length"
:loop="true"
:autoplay="false"
effect="slide"
:slidesPerView="1"
:observer="true"
:observeParents="true"
:pagination="{
clickable: false,
type: 'bullets', // 分页类型: 'bullets', 'fraction', 'progressbar', 'custom'
}"
:navigation="false"
:modules="modules"
class="swiper_content"
@slideChange="onSlideChange"
>
<swiper-slide v-for="(item, index) in state.list" :key="index">
<div class="item_box">
<lazyImg :src="item" :borderRadius="8" />
</div>
</swiper-slide>
</swiper>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { Pagination, Autoplay, Navigation, Swiper as SwiperInstance } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/vue';
import 'swiper/swiper-bundle.css';
let modules = [Pagination, Autoplay, Navigation];
const props = defineProps({
list: {
type: Array,
default: () => [],
},
});
const state = reactive({
list: props.list,
});
const swiperActiveIndex = ref(0);
const onSlideChange = (swiper: SwiperInstance) => {
swiperActiveIndex.value = swiper.realIndex;
};
</script>
<style lang="scss" scoped>
.swiper_box {
// padding: 0 16px;
}
.swiper_content {
.item_box {
width: 100%;
height: 143px;
}
}
:deep(.swiper-pagination) {
display: flex;
width: 100%;
justify-content: flex-end;
padding-right: 12px;
}
:deep(.swiper-pagination-bullet) {
width: 7px;
height: 7px;
border: none;
background: rgba(225, 53, 31, 1);
opacity: 0.5;
}
:deep(.swiper-pagination-bullet-active) {
width: 12px;
height: 6px;
border-radius: 4px;
opacity: 1;
background: rgba(225, 53, 31, 1);
}
</style>
@@ -0,0 +1,113 @@
<template>
<div class="gameTagDetails">
<Header :title="state.tagName" :isDefault="true" @back="back" />
<div class="main overflow-y-auto" id="gameTagDetails" v-show="!state.loading">
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="moreMoviesBox">
<HorizontalGameBox class="gameBox" :videoInfo="item" v-for="item in state.list" :key="item.id" />
</div>
</PullRefresh>
</div>
<div class="main" v-show="state.loading">
<Loading />
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'gameTagDetails',
};
</script>
<script lang="ts" setup>
import { reactive, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import VideoApi from '@/service/video';
import TabPageScroll from '@/utils/resetTabPageScrollTop';
const route = useRoute();
const router = useRouter();
const tabPageScroll = new TabPageScroll();
setTimeout(() => {
tabPageScroll.add(`#gameTagDetails`);
}, 100);
const state = reactive({
tagId: <string>route.query.id,
tagName: <string>route.query.name,
pageNumber: 1,
pageSize: 10,
list: <any>[], // 真实列表
loading: true, // loading
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 获取更多视频
const getList = async (type: any) => {
let req = {
pageNumber: state.pageNumber,
pageSize: state.pageSize,
sortType: 1,
tagID: route.query.id,
newsType: 'SEED_LINK',
};
try {
let res: any = await VideoApi.searchTagPost(req);
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (!res.hasNext) {
moreParams.finished = true;
}
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
getList('');
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList(null);
};
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
moreParams.isNoData = false;
getList('refresh');
};
const back = () => {
router.go(-1);
};
</script>
<style lang="scss" scoped>
.gameTagDetails {
.main {
height: calc(100vh - 44px);
padding: 0 6px;
.moreMoviesBox {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
.gameBox {
width: 179px;
margin-bottom: 10px;
}
}
}
}
</style>
+531
View File
@@ -0,0 +1,531 @@
<template>
<div class="group-chat" v-if="!state.loading">
<Header class="header" :title="state.title" @rightFuc="toUserpage2">
<template v-slot:right>
<!-- <LazyImg :src="state.userInfo.portrait" class="avatar" /> -->
<SvgIcon name="user_default_avatar" class="avatar" />
</template>
</Header>
<div class="content">
<!-- <van-notice-bar
v-if="notice"
class="notice"
left-icon="volume-o"
mode="closeable"
:scrollable="true"
:text="notice"
@close="notice = ''"
/> -->
<van-pull-refresh
class="my-refresh"
v-model="state.isLoading"
@refresh="onRefresh"
success-text="加载成功"
v-show="!isFirst"
pulling-text="下拉加载历史消息"
loosing-text="释放加载"
>
<div
:class="['msg_item', item.uid != state.MineuserInfo.uid ? 'avatar_info_left' : 'avatar_info_right']"
v-for="item in state.list"
:key="item.id"
>
<!-- 如果发送者和当前发送的用户id一样 就用sendid -->
<LazyImg
v-if="item.uid == state.MineuserInfo.uid"
:src="item.portrait"
:key="item.portrait"
round
class="avatar"
@click.prevent="toUserpage(item.uid)"
/>
<LazyImg
v-if="item.uid != state.MineuserInfo.uid"
:src="item.portrait"
:key="item.portrait"
round
class="avatar"
@click.prevent="toUserpage(item.uid)"
/>
<div class="text_info">
<div class="msg_name" v-if="item.name">{{ item.name }}</div>
<div class="msg_text" v-if="item.content" v-html="Highlight(item)" @click="goMovieDetails(item)"></div>
<div class="msg_img" v-if="item.image && item.image.length > 0">
<template v-for="(itemImg, indexImg) in item.image.split(',')" :key="indexImg">
<LazyImg
:src="itemImg"
class="cover"
@click="
$router.push({ path: '/picturePreview', query: { imgs: item.image.split(','), index: indexImg } })
"
/>
</template>
</div>
</div>
</div>
</van-pull-refresh>
</div>
<div class="send_box">
<!--
@delImg="delImg" -->
<van-field
v-model="state.msgValue"
@blur="blur"
:placeholder="state.placeholder"
class="sendInput"
ref="msgRef"
/>
<UploadImgMess
class="upload-img"
:maxCount="1"
:isCover="false"
@uploadSuccess="uploadSuccess"
ref="uploadFileRef"
:multiple="true"
/>
<img class="send" @click="sendMsg" src="@/assets/images/png/sendBtn.png" alt="" />
</div>
</div>
<Loading v-else />
</template>
<script lang="ts">
export default {
name: 'groupChat',
};
</script>
<script lang="ts" setup>
import { reactive, ref, nextTick } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import UserApi from '@/service/user';
import MessageApi from '@/service/message';
import { useUserStore } from '@/stores/user';
import { useConfigStore } from '@/stores/config';
import { showToast } from 'vant';
import CommunityApi from '@/service/community';
const configStore = useConfigStore();
const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
const msgRef = ref<HTMLElement>();
const uploadFileRef = ref(null);
const state = reactive({
userInfo: <Types.User.UserInfoType>{},
MineuserInfo: userStore.userInfo, //自己的信息
placeholder: '',
msgValue: '',
loading: true,
list: [],
pageNumber: 1,
pageSize: 10,
sessionId: '',
isLoading: false,
isFirst: true,
isNodata: false,
imgList: [],
isnowpage: false,
freshPage: 1,
title: '',
groupInfo: <any>{},
});
nextTick(() => {
// state.placeholder = '请输入消息 消耗' + configStore.appConfig.sendMsgPrice + '金币';
state.placeholder = '请输入消息';
state.title = route.query.name as string;
// getInfo();
// getsessionGet();
getChatDetailList('');
});
const toUserpage = (id: any) => {
router.push({ path: '/userInfo', query: { uid: id } });
localStorage.removeItem('isChange');
};
const toUserpage2 = (id: any) => {
router.push({ path: '/userInfo', query: { uid: route.query.uid } });
localStorage.removeItem('isChange');
};
// 获取是否加入群组接口
const getInfo = async () => {
try {
let req = {
groupId: route.query.id,
};
let res: any = await CommunityApi.queryImgroupInfo(req);
console.log(res);
state.loading = false;
state.groupInfo = res;
} catch (e) {
console.log(e);
}
};
// 会话id
const getsessionGet = async () => {
try {
// let req = {
// takeUid: Number(route.query.uid),
// };
// let ret = await MessageApi.sessionGet(req);
// state.sessionId = ret;
getChatDetailList('');
} catch (e: any) {}
};
// 会话列表
const getChatDetailList = async (type: any) => {
try {
let req = {
groupId: route.query.id,
pageNumber: state.pageNumber,
pageSize: state.pageSize,
};
let ret = await CommunityApi.imMessageList(req);
state.loading = false;
let list = ret.list || [];
state.isnowpage = ret.hasNext;
list.length ? (state.isNodata = false) : (state.isNodata = true);
if (!state.isNodata && list.length) {
if (state.pageNumber === 1) {
state.list = [];
}
state.list.unshift(...list.reverse());
state.isLoading = false;
console.log(state.list);
} else {
state.isNodata = true;
!state.isFirst ? showToast('没有更多消息') : '';
state.isLoading = false;
}
nextTick(() => {
if (state.isFirst) {
state.isFirst = false;
setTimeout(() => {
let dom = document.querySelector('.msg_item:last-child');
if (dom) {
dom.scrollIntoView();
}
}, 100);
}
});
} catch (e: any) {
state.loading = false;
}
};
// 发送消息
async function sendMsg() {
if (state.imgList.length == 0 && !state.msgValue.trim()) {
showToast({
// position: 'bottom',
message: '内容不能为空',
});
return;
}
let req = {
groupId: Number(route.query.id),
content: state.msgValue.trim(),
image: state.imgList.toString(),
};
try {
let ret = await CommunityApi.imMessageSend(req);
state.pageNumber = 1;
await getChatDetailList('');
showToast('发送成功');
let uploadFilesRef: any = uploadFileRef.value;
uploadFilesRef.clearImg();
state.imgList = [];
state.msgValue = '';
uploadFilesRef.setIMG();
setTimeout(() => {
let dom = document.querySelector('.msg_item:last-child');
if (dom) {
if (state.list.length > 4) {
dom.scrollIntoView();
}
}
}, 0);
} catch (error) {
if (error.toString().indexOf('8000')) {
showToast('余额不足,请先充值金币');
// setTimeout(()=>{
// router.push('/wallet')
// },10)
}
// else if (error.toString().indexOf('25000')) {
// showToast('充值vip才可私信');
// }
}
}
const uploadSuccess = (img: string) => {
console.log(img);
state.imgList.push(img);
sendMsg();
};
// 使之失去焦点
function blur() {
msgRef.value?.blur();
nextTick(() => {
window.scrollTo(0, 0);
});
}
function onRefresh() {
if (state.isnowpage) {
state.isLoading = true;
state.pageNumber++;
getChatDetailList('refresh');
} else {
state.isLoading = false;
// state.pageNumber = state.pageNumber
}
}
const notice = ref(sessionStorage.getItem('notice') || '');
(() => {
if (notice.value) return;
MessageApi.announce().then((data) => {
notice.value = data as string;
sessionStorage.setItem('notice', data);
});
})();
const Highlight = (item: any) => {
const { objId, objName, content } = item;
if (objId && objName) {
// return content.replace(
// new RegExp('\\[(影片)?' + objName + '\\]'),
// `<span style="color: #00D6BE">[$1${objName}]</span>`,
// );
return content.replace(objName, `<span style="color: #00D6BE">${objName}</span>`);
} else {
return content;
}
};
const goMovieDetails = (item: any) => {
const { objId, objName } = item;
if (!objId || !objName) return;
router.push({
path: '/movieDetails',
query: {
vid: objId,
},
});
};
</script>
<style lang="scss" scoped>
.group-chat {
width: 100%;
height: 100vh;
.header {
:deep(.right-btn) {
color: #666;
}
.avatar {
width: 24px;
height: 24px;
border-radius: 50%;
overflow: hidden;
}
}
.send_box {
position: fixed;
bottom: 0;
left: 0;
z-index: 8;
width: 100%;
height: 80px;
background: #000;
display: flex;
padding: 14px 16px 0 16px;
box-sizing: border-box;
display: flex;
align-items: flex-start;
justify-content: space-between;
.upload-img {
width: 24px;
height: 24px;
margin: 10px 10px 18px 12px;
display: flex;
align-items: center;
}
.send {
width: 24px;
height: 24px;
margin-top: 10px;
}
:deep(.van-cell) {
height: 38px;
background: rgba(255, 255, 255, 0.1);
border-radius: 20px;
margin: 0;
.van-cell__value {
height: 100%;
}
.van-field__control {
height: 100%;
color: #fff;
}
::placeholder {
color: rgba(255, 255, 255, 0.35);
}
}
:deep(.van-cell:after) {
border-bottom: transparent;
}
}
.content {
position: fixed;
top: 44px;
left: 0;
z-index: 5;
width: 100%;
height: calc(100% - 124px);
padding: 10px 0;
box-sizing: border-box;
.my-refresh {
height: 100%;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.msg_img {
width: 165px;
background: #22252d;
font-size: 12px;
border-radius: 8px;
overflow: hidden;
margin-top: 18px;
.cover {
// width: 166px;
// height: 94px;
// overflow: hidden;
// border-radius: 6px 6px 0 0;
}
.video_info {
padding: 10px;
.title {
width: 100%;
font-size: 12px;
color: #dbdbdb;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.view {
text-align: right;
padding-top: 10px;
padding-right: 10px;
color: #03e8e1;
font-size: 11px;
}
}
}
.msg_item {
margin: 18px 16px;
&:last-child {
height: 80px;
}
}
.avatar_info_left {
display: flex;
.avatar {
width: 38px;
height: 38px;
border-radius: 50%;
}
.text_info {
padding: 0 0 0 10px;
.time {
font-size: 11px;
color: rgba(152, 152, 152, 1);
padding-bottom: 10px;
}
.msg_name {
font-size: 12px;
color: #fff;
padding-bottom: 6px;
line-height: 20px;
}
.msg_text {
max-width: 250px;
border-radius: 4px 12px 12px 12px;
background: rgba(255, 255, 255, 0.05);
font-size: 14px;
color: #fff;
font-weight: 400;
line-height: normal;
padding: 11px;
white-space: pre-wrap;
// text-indent: 15px;
}
}
}
.avatar_info_right {
display: flex;
flex-direction: row-reverse;
.avatar {
width: 38px;
height: 38px;
border-radius: 50%;
}
.text_info {
padding: 0 10px 0 0;
.time {
font-size: 11px;
color: rgba(41, 41, 41, 1);
padding-bottom: 10px;
text-align: right;
}
.msg_name {
font-size: 12px;
color: #fff;
padding-bottom: 6px;
line-height: 20px;
text-align: right;
}
.msg_text {
max-width: 250px;
border-radius: 12px 4px 12px 12px;
background: #1dc194;
font-size: 14px;
font-weight: 400;
line-height: normal;
padding: 11px;
white-space: pre-wrap;
word-break: break-all;
color: #141414;
// text-indent: 15px;
}
}
}
}
}
.notice {
position: absolute;
bottom: 0;
left: 16px;
right: 16px;
bottom: 12px;
z-index: 5;
height: 34px;
padding: 6px 12px;
border-radius: 6px;
background: #202133;
color: #fff;
:deep(.van-notice-bar__left-icon) {
color: #3476ff;
}
}
</style>
+249
View File
@@ -0,0 +1,249 @@
<template>
<div class="holidays_bg community_tab">
<SvgIcon name="search" class="search_icon" @click.stop="$router.push('/searchPage')" />
<template v-if="getTheme.holidays !== 'default'">
<div class="holidays_sign_icon holidays_sign_icon_community" @click.stop="$router.push('/task_sign_day')"></div>
</template>
<template v-else>
<lazyImg
v-if="configStore.appConfig.signIcon"
:src="configStore.appConfig.signIcon"
class="sign_icon"
@click.stop="$router.push('/task_sign_day')"
/>
</template>
<van-tabs
v-model:active="state.tabActive"
animated
:scrollspy="false"
:lazy-render="true"
:line-height="0"
:line-width="0"
shrink
background="transparent"
title-inactive-color="#808080"
@change="tabChange"
class="tabs"
>
<van-tab v-for="item in secondTab" :key="item.id" :title="item.moduleName" :name="item.id">
<template #title>
<div class="tab_title" v-if="state.tabActive == item.id">
<img
src="@/assets/images/png/findModule/tab_community.png"
v-if="state.tabActive == 'community'"
alt=""
class="tab_img"
/>
<img
src="@/assets/images/png/findModule/tab_seek.png"
v-if="state.tabActive == 'seek'"
alt=""
class="tab_img"
/>
<!-- <img-->
<!-- src="@/assets/images/png/findModule/tab_chat.png"-->
<!-- v-if="state.tabActive == 'chat'"-->
<!-- alt=""-->
<!-- class="tab_img"-->
<!-- />-->
<img
src="@/assets/images/png/findModule/tab_fiction.png"
v-if="state.tabActive == 'fiction'"
alt=""
class="tab_img"
/>
</div>
<div class="tab_title" v-else>
{{ item.moduleName }}
</div>
</template>
<TabItemCommunity v-if="item.id == 'community'" />
<TabItemSecond
v-if="item.id == 'fiction'"
:moduleName="item.moduleName"
:type="6"
mediaType="text"
:advType="AdType.COMMUNITYCAROUSEL"
/>
<TabItemSeek v-if="item.id == 'seek'" />
<!-- <TabItemChat v-if="item.id == 'chat'" />-->
</van-tab>
</van-tabs>
<!-- 发布帖子 -->
<div class="add_info" @click="state.showAddBox = true" v-if="state.tabActive == 'community'" />
<van-popup v-model:show="state.showAddBox" round position="bottom">
<div class="addPopup">
<div class="title">
<div class="add_line" />
选择发布类型
<!-- <SvgIcon name="popup_close" @click="state.showAddBox = false"></SvgIcon> -->
</div>
<div class="pop_mess">
<div @click="$router.push('/pubilshImg'), (state.showAddBox = false)">
<img src="@/assets/images/png/add_img.png" alt="" />
图片
</div>
<div @click="$router.push('/publishVideo'), (state.showAddBox = false)">
<img src="@/assets/images/png/add_video.png" alt="" />
视频
</div>
<div @click="$router.push('/publishGraphics'), (state.showAddBox = false)">
<img src="@/assets/images/png/add_text.png" alt="" />
图文
</div>
</div>
</div>
</van-popup>
</div>
<MarqueeBar class="marqueeBar" />
</template>
<script lang="ts">
export default {
name: 'community',
};
</script>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { AdType } from '@/enums/app';
// import { useGlobalStore } from '@/stores/global';
import TabItemCommunity from './tabPage/tabItem-community.vue';
import TabItemSeek from './tabPage/tabItem-seek.vue';
import TabPageScroll from '@/utils/resetTabPageScrollTop';
import TabItemSecond from '@/views/acgModule/components/tabPage/tabItemSecond.vue';
import TabItemChat from './tabPage/tabItem-chat.vue';
// import TabItemGame from './tabPage/tabItem-game.vue';
import { useRoute } from 'vue-router';
import setTheme from '@/assets/theme/index';
import { useConfigStore } from '@/stores/config';
const configStore = useConfigStore();
const getTheme = setTheme(configStore.appConfig.festivalUi);
const route = useRoute();
const tabPageScroll = new TabPageScroll();
// const globalStore = useGlobalStore();
const state = reactive({
tabActive: (route.query.type as string) || 'community',
showAddBox: false,
});
const secondTab = ref([
{ id: 'community', moduleName: '社区' },
{ id: 'seek', moduleName: '色图' },
{ id: 'fiction', moduleName: '小说' },
// { id: 'chat', moduleName: '群聊' },
]);
const prefixKey = 'find_page_';
const tabChange = () => {
setTimeout(() => {
tabPageScroll.add(`#${prefixKey}${state.tabActive}`);
}, 100);
};
tabChange();
</script>
<style lang="scss" scoped>
.community_tab {
height: 100%;
position: relative;
background-color: #0f0f0f !important;
.search_icon {
position: absolute;
right: 16px;
top: 10px;
width: 24px;
height: 24px;
z-index: 10;
}
.sign_icon {
position: absolute;
right: 50px;
top: 6px;
width: 20px;
height: 32px;
z-index: 10;
}
.add_info {
position: fixed;
bottom: 130px;
right: 16px;
z-index: 10;
width: 40px;
height: 40px;
background: url('@/assets/images/png/add_post.png') no-repeat;
background-size: 100% 100%;
}
.addPopup {
width: 100%;
padding: 18px 16px 18px 16px;
box-sizing: border-box;
background: #0f0f0f;
@include SafeArea('padding', 'bottom', 21px);
.title {
text-align: center;
margin-bottom: 20px;
font-size: 20px;
position: relative;
color: rgba(255, 255, 255, 0.9);
svg {
width: 13px;
height: 13px;
position: absolute;
right: 10px;
}
.add_line {
margin: 0 auto 16px;
width: 36px;
height: 5px;
background: rgba(255, 255, 255, 0.2);
border-radius: 99px;
}
}
.pop_mess {
display: flex;
align-items: center;
justify-content: space-around;
color: #a3a2a2;
padding: 0 30px;
> div {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
font-size: 14px;
}
img {
display: block;
width: 40px;
height: 40px;
margin-bottom: 10px;
}
}
}
}
.tabs {
:deep(> .van-tabs__wrap) {
height: 48px;
}
.tab_img {
width: 31px;
height: 30px;
}
.tab_title {
display: flex;
font-size: 14px;
}
.contain {
height: calc(100vh - 44px * 2);
}
}
.marqueeBar {
position: absolute;
bottom: calc(0px + constant(safe-area-inset-bottom));
bottom: calc(0px + env(safe-area-inset-bottom));
left: 0;
right: 0;
z-index: 100;
}
</style>
@@ -0,0 +1,94 @@
<template>
<!-- 返回按钮 -->
<div class="picture-preview">
<Header title="" class="isHader" headerBg="'transparent'" leftIcon="back_white_round" />
<template v-if="!state.loading">
<van-image-preview
ref="vanPreview"
v-model:show="state.isPreviewShow"
:startPosition="state.current"
:images="state.imgs"
lazy-load
:closeable="false"
:before-close="() => false"
:show-index="false"
@change="onChange"
/>
<div class="custom-indicator">
<span class="red">{{ Number(state.current) + 1 }}</span>
/ {{ state.imgs ? state.imgs.length : 0 }}
</div>
</template>
<Loading v-else />
</div>
</template>
<script lang="ts">
export default {
name: 'PicturePreview',
};
</script>
<script lang="ts" setup>
import { reactive, ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useConfigStore } from '@/stores/config';
import AppApi from '@/service/app';
const configStore = useConfigStore();
const route = useRoute();
const state = reactive({
current: <any>route.query.index || 0,
imgs: <any>route.query.imgs,
postInfo: <any>{}, // 视频信息
loading: true,
isPreviewShow: false,
});
const swipes = ref<any>(null);
onMounted(() => {
openPop();
});
const openPop = async () => {
const decryptedImgs = await Promise.all(
state.imgs.map(async (img: string) => {
const isHttp = new RegExp('http').test(img);
const imgSrc = isHttp ? img : `${configStore.sourceInfo.IMAGE}/${img}`;
return await AppApi.imgDecypt(imgSrc);
}),
);
state.imgs = decryptedImgs;
state.loading = false;
state.isPreviewShow = true;
};
const onChange = (index: any) => {
state.current = index;
};
</script>
<style lang="scss" scoped>
.isHader {
z-index: 2100;
}
.picture-preview {
height: 100dvh;
.custom-indicator {
position: fixed;
bottom: 40px;
right: 16px;
border-radius: 99px;
font-size: 12px;
z-index: 2100;
background: #fff;
color: #000;
padding: 4px 12px;
.red {
color: $themeColor;
}
}
:deep(.van-swipe-item) {
background: #000;
height: 100%;
}
}
</style>
+504
View File
@@ -0,0 +1,504 @@
<template>
<div class="comment">
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="commentBox" v-for="item in state.list" :key="item.id">
<SvgIcon name="goldCommentIcon" class="goldCommentIcon" v-if="item.isGodComment" />
<LazyImg
:src="item.userPortrait"
class="avatar"
:key="item.userPortrait"
@click="$router.push({ path: '/userInfo', query: { uid: item.userID } })"
/>
<div class="commentInfoBox">
<div class="name flex-vcenter">
<span class="ellipsis">
{{ item.userName || '未知用户' }}
</span>
</div>
<div class="content">
<!-- {{ item.content }} -->
<HighlightText
:text="item.content"
:keyword="item.searchKeyword"
:linkStr="item.linkStr"
:linkType="item.linkType"
></HighlightText>
<lazyImg v-if="item.image" class="image" :src="item.image"></lazyImg>
</div>
<div class="commentInfo flex-center-between">
<div class="time">{{ moment(item.createdAt).format('YYYY-MM-DD HH:mm:ss') }}</div>
<div v-if="![1, 2].includes(item.linkType)" class="optBox flex-vcenter">
<SvgIcon name="commentMsg" @click="reply(item, undefined, undefined)" />
<div class="zanBox">
<SvgIcon @click="clickLike(item)" v-if="item.isLike" name="commentZan" />
<SvgIcon @click="clickLike(item)" v-else name="commentUnZan" />
<span>{{ watchCount(item.likeCount) }}</span>
</div>
</div>
</div>
<!-- 子评论区域 -->
<div class="subCommentArea">
<div class="subShow" v-if="item.Info.length">
<div class="subCommentBox" v-for="subItem in item.Info" :key="subItem.id">
<LazyImg
:src="subItem.userPortrait"
class="avatar"
:key="subItem.userPortrait"
@click="$router.push({ path: '/userInfo', query: { uid: subItem.userID } })"
/>
<div class="commentInfoBox">
<div class="nameBox flex-vcenter">
<div class="name">{{ subItem.userName || '未知用户' }}</div>
<SvgIcon name="reply" v-if="subItem.toUserName" />
<div class="name">{{ subItem.toUserName }}</div>
</div>
<div class="content">
<!-- {{ subItem.content }} -->
<HighlightText
:text="subItem.content"
:keyword="subItem.searchKeyword"
:linkStr="subItem.linkStr"
:linkType="subItem.linkType"
></HighlightText>
<lazyImg v-if="item.image" class="image" :src="item.image"></lazyImg>
</div>
<div class="commentInfo flex-center-between">
<div class="time">{{ moment(subItem.createdAt).format('YYYY-MM-DD HH:mm:ss') }}</div>
<div v-if="![1, 2].includes(subItem.linkType)" class="optBox flex-vcenter">
<SvgIcon name="commentMsg" @click="reply(subItem, subItem.userID, item.id)" />
<div class="zanBox">
<SvgIcon @click="clickLike(subItem)" v-if="subItem.isLike" name="commentZan" />
<SvgIcon @click="clickLike(subItem)" v-else name="commentUnZan" />
<span>{{ watchCount(subItem.likeCount) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="subShow pdunset" v-if="item.subShow">
<div class="subCommentBox" v-for="subItem in item.subList" :key="subItem.id">
<LazyImg
:src="subItem.userPortrait"
class="avatar"
:key="subItem.userPortrait"
@click="$router.push({ path: '/userInfo', query: { uid: subItem.userID } })"
/>
<div class="commentInfoBox">
<div class="nameBox flex-vcenter">
<div class="name">{{ subItem.userName || '未知用户' }}</div>
<SvgIcon name="reply" v-if="subItem.toUserName" />
<div class="name">{{ subItem.toUserName }}</div>
</div>
<div class="content">
<!-- {{ subItem.content }} -->
<HighlightText
:text="subItem.content"
:keyword="subItem.searchKeyword"
:linkStr="subItem.linkStr"
:linkType="subItem.linkType"
></HighlightText>
<lazyImg v-if="subItem.image" class="image" :src="subItem.image"></lazyImg>
</div>
<div class="commentInfo flex-center-between">
<div class="time">{{ moment(subItem.createdAt).format('YYYY-MM-DD HH:mm:ss') }}</div>
<div class="optBox flex-vcenter">
<SvgIcon name="commentMsg" @click="reply(subItem, subItem.userID, item.id)" />
<div class="zanBox">
<SvgIcon @click="clickLike(subItem)" v-if="subItem.isLike" name="commentZan" />
<SvgIcon @click="clickLike(subItem)" v-else name="commentUnZan" />
<span>{{ watchCount(subItem.likeCount) }}</span>
</div>
</div>
</div>
</div>
</div>
<Loading v-if="item.subLoading" />
</div>
<div class="expandBox flex-vcenter" v-if="item.commCount - 1 > 0">
<div
class="expandMore flex-vcenter"
@click="expandComment(item)"
v-if="!(item.subList && item.subList.length) && !item.subHasNext"
>
<div class="flex-vcenter">
——— 展开{{ item.commCount - 1 }}条回复
<van-icon name="arrow-down" />
</div>
</div>
<div class="expandMore flex-vcenter" @click="loadMore(item)" v-if="item.subHasNext">
<div class="flex-vcenter">
——— 查看更多
<van-icon name="arrow-down" />
</div>
</div>
<div class="closeBox" @click="expandComment(item)" v-if="item.subList && item.subList.length">
<div class="close flex-center-center">
<span>收起</span>
<van-icon name="arrow-up" />
</div>
</div>
</div>
</div>
</div>
</div>
</PullRefresh>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import { showToast } from 'vant';
import moment from 'moment';
import CommunityApi from '@/service/community';
import { watchCount } from '@/utils/index';
import HighlightText from '@/components/HighlightText/index.vue';
const props = defineProps<{
videoInfo: Types.Video.VInfosItem;
}>();
const emits = defineEmits(['reply']);
const state = reactive({
content: '',
list: <Array<any>>[], // 评论列表
pageNumber: 1, // 评论页码
pageSize: 15, // 评论一页的数量
subPageNumber: 1, // 子评论页码
subPageSize: 15, // 子评论一页的数量
videoInfo: props.videoInfo,
});
// 上拉加载及下拉刷新参数
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 获取评论数据
const getList = async (type: string) => {
try {
let req = {
objID: String(state.videoInfo.id),
curTime: new Date().toISOString(),
pageNumber: String(state.pageNumber),
pageSize: String(state.pageSize),
};
let res: any = await CommunityApi.getCommentList(req);
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && 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;
state.pageNumber++;
getList('');
};
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
moreParams.isNoData = false;
getList('refresh');
};
getList('');
// 评论成功后增加评论数据
const addComment = (res: any, childrenCommentObj: any) => {
moreParams.isNoData = false;
if (childrenCommentObj) {
state.list.map((comment: any) => {
if (comment.id == childrenCommentObj.id) {
if (!comment.subList) {
comment.subLoading = false;
comment.subList = [];
comment.subHasNext = true;
comment.subShow = true;
}
comment.Info.unshift(res);
}
return comment;
});
} else {
state.list.unshift(res);
}
};
// 切换视频后,评论数据需要被改变
const changeComment = (videoInfo: Types.Video.VInfosItem) => {
state.videoInfo = videoInfo;
state.pageNumber = 1;
moreParams.finished = false;
moreParams.isNoData = false;
getList('refresh');
};
// 点赞
const clickLike = async (comment: any) => {
try {
let req = {
type: 'comment',
objID: comment.id,
};
if (!comment.isLike) {
await CommunityApi.zan(req);
comment.isLike = true;
comment.likeCount++;
showToast('点赞成功');
} else {
await CommunityApi.cancleZan(req);
comment.isLike = false;
comment.likeCount--;
showToast('取消成功');
}
} catch (e) {
console.log(e);
}
};
const reply = (comment: any, toUserID: any, parentId: any) => {
comment.toUserID = toUserID ? toUserID : undefined;
comment.rid = toUserID ? comment.id : undefined;
comment.id = toUserID ? parentId : comment.id;
emits('reply', comment);
};
// 展开评论
const expandComment = async (comment: any) => {
if (!comment.subShow) {
comment.subShow = true;
comment.subHasNext = true; // 子评论是否还有下一页数据
comment.subList = []; // 子评论列表
comment.subLoading = true; // 子评论加载loading
state.subPageNumber = 1;
const resObj: any = await getSubList(comment);
comment.subLoading = false;
comment.subHasNext = resObj.subHasNext;
if (resObj.list.length) {
if (!comment.subList) {
comment.subList = [];
}
comment.subList = comment.subList.concat(resObj.list);
}
} else {
comment.subLoading = false;
comment.subList = []; // 清空子评论列表
comment.subHasNext = false; // 子评论是否还有下一页数据
comment.subShow = false;
}
};
// 获取更多评论
const loadMore = async (comment: any) => {
comment.subLoading = true;
state.subPageNumber++;
const resObj: any = await getSubList(comment);
comment.subLoading = false;
comment.subList = comment.subList.concat(resObj.list);
comment.subHasNext = resObj.subHasNext;
};
// 获取二级评论
const getSubList = async (comment: any) => {
let resObj = {
subHasNext: true,
list: <any>[],
};
let req = {
objID: comment.objID,
cmtId: comment.id,
fstID: comment.Info[0].id, // 第一条评论的ID
curTime: new Date().toISOString(),
pageNumber: state.subPageNumber,
pageSize: state.subPageSize,
};
let res: any = await CommunityApi.getSecCommentList(req);
if (res.list && res.list.length > 0) {
resObj.list = res.list;
}
if (!res.hasNext) {
resObj.subHasNext = false;
}
return resObj;
};
defineExpose({
addComment,
changeComment,
});
</script>
<style lang="scss" scoped>
.comment {
height: calc(100vh - 386px);
.commentBox {
display: flex;
padding: 15px 15px 0px;
position: relative;
.goldCommentIcon {
width: 67px;
height: 50px;
position: absolute;
right: 20px;
top: 10px;
}
.avatar {
height: 36px;
width: 36px;
border-radius: 50%;
overflow: hidden;
position: relative;
transform: translateZ(0px);
:deep(.loading) {
background-size: 28px 18.23px;
}
}
.commentInfoBox {
flex: 1;
margin-left: 10px;
font-size: 14px;
.name {
color: #9a9a9a;
font-weight: 500;
height: 30px;
flex: 1;
span {
max-width: 240px;
}
}
.content {
margin-top: 3px;
line-height: 20px;
font-size: 14px;
.image {
width: 111px;
border-radius: 10px;
margin: 4px 0;
overflow: hidden;
}
}
.commentInfo {
margin-top: 8px;
color: #9a9a9a;
.time {
font-size: 12px;
color: #9a9a9a;
}
.optBox {
svg {
height: 20px;
width: 20px;
color: white;
}
.zanBox {
margin-left: 14px;
display: flex;
align-items: center;
span {
margin-left: 4px;
font-size: 14px;
color: white;
}
}
}
}
}
.subCommentArea {
border-radius: 12px;
margin-top: 11px;
.subShow {
padding: 4px 0px 4px 12px;
.subCommentBox {
display: flex;
margin: 10px 0 0;
.avatar {
height: 24px;
width: 24px;
border-radius: 50%;
overflow: hidden;
position: relative;
transform: translateZ(0px);
:deep(.loading) {
background-size: 20px 13px;
}
}
.commentInfoBox {
flex: 1;
margin-left: 10px;
font-size: 12px;
.nameBox {
.name {
height: 24px;
color: #9a9a9a;
font-weight: 500;
line-height: 24px;
flex: unset;
}
svg {
color: rgba(204, 204, 204, 1);
height: 14px;
width: 14px;
margin: 0 3px;
}
}
.content {
margin-top: 3px;
line-height: 20px;
font-size: 12px;
.image {
width: 111px;
border-radius: 10px;
margin: 4px 0;
overflow: hidden;
}
}
.commentInfo {
margin-top: 8px;
.time {
font-size: 12px;
color: #999999;
}
}
}
}
}
.pdunset {
padding-top: unset;
}
.expandBox {
color: #999999;
font-size: 12px;
flex: 1;
padding: 0 12px 12px;
svg {
height: 12px;
width: 12px;
margin-left: 6px;
}
.closeBox {
display: flex;
align-items: center;
width: 100%;
padding-right: 5px;
.close {
width: 55px;
span {
margin-right: 3px;
}
}
}
}
}
}
.commentBoxBg {
background: url('@/assets/images/png/commentBoxBg.png') no-repeat;
background-size: 428px 57px;
}
}
</style>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,119 @@
<template>
<van-overlay :show="isShow" z-index="2001" @click.stop="emit('close')">
<div class="wrapper flex-center-center">
<div class="content" :class="[props.theme]">
<div class="title">发布规则</div>
<div class="horizontalLine"></div>
<div class="text">
· 禁止发布广告图文 (含Line、WeChat、QQ、网址、二维码等外链)
<br />
· 禁止发布诱导图文 (求网址、求软体等)
<br />
· 禁止发布儿童色情、兽*内容
<br />
· 禁止发布交易相关、政治等内容
<br />
<div class="text_info">请遵守社区规则,否则会被官方机器人拉黑</div>
</div>
<div class="btnBox flex-center-between">
<div class="btn flex-center-center" @click.stop="emit('close')">我知道了</div>
</div>
</div>
</div>
</van-overlay>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
const props = withDefaults(
defineProps<{
isOpen: boolean;
theme?: string; //black 为黑色主题,默认为白色主题
}>(),
{
theme: 'black',
},
);
const emit = defineEmits(['close']);
const isShow = computed(() => {
return props.isOpen;
});
</script>
<style lang="scss" scoped>
.wrapper {
width: 100%;
height: 100%;
color: #333;
.content {
width: 311px;
height: 306px;
background-color: #0f0f0f;
position: relative;
border-radius: 12px;
padding: 0 24px;
.title {
padding: 32px 0 12px;
font-size: 20px;
font-weight: 500;
text-align: center;
color: rgba(255, 255, 255, 0.9);
}
.horizontalLine {
height: 1px;
background-color: rgba(255, 255, 255, 0.1);
margin-bottom: 12px;
}
.text {
color: rgba(255, 255, 255, 0.55);
text-align: center;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 137.5% */
.text_info {
font-size: 12px;
padding-top: 12px;
}
}
// p {
// font-size: 14px;
// text-align: center;
// opacity: 0.9;
// }
&.black {
background-color: #030f18;
color: #fff;
.title {
color: #fff;
}
.text {
color: rgba(255, 255, 255, 0.5);
}
}
}
.btnBox {
margin-top: 24px;
.inviteBtn {
height: 38px;
width: 108px;
text-align: center;
line-height: 38px;
font-size: 14px;
border-radius: 38px;
background: #f6f6f6;
color: $textColor;
}
.btn {
height: 44px;
width: 100%;
text-align: center;
line-height: 38px;
font-size: 16px;
font-weight: 500;
border-radius: 4px;
background: #f68804;
color: #fff;
}
}
}
</style>
@@ -0,0 +1,207 @@
<template>
<van-popup v-model:show="isShow" round position="bottom" @click-overlay="emit('close')">
<div class="wrapper">
<div class="hader">
<div class="hader_line" />
<!-- <SvgIcon name="ht_close" class="ht_close" @click="emit('close')" /> -->
选择标签
<!-- <div>{{ communityStore.tags?.id ? 1 : 0 }}/1</div> -->
</div>
<div class="content overflow-y-auto">
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div :class="['cell flex-center-between']" v-for="item in state.list" :key="item" @click="selectTag(item)">
<lazyImg :src="item.coverImg" class="avatar" />
<div class="mess">
<div class="name ellipsis">#{{ item.name }}</div>
<div class="count">
<span>{{ watchCount(item.vidCount) }}个帖子</span>
<span>{{ watchCount(item.playCount) }}浏览</span>
<span>{{ watchCount(item.followCount) }}收藏</span>
</div>
</div>
<!-- <div class="cell_btn" :class="item.select ? 'btn_active' : ''">{{ item.select ? '已选' : '选择' }}</div> -->
<SvgIcon name="ht_select_active" height="24px" width="24px" color="#F25477" v-if="item.select" />
<SvgIcon name="ht_select" height="24px" width="24px" color="#70708C" v-else />
</div>
</PullRefresh>
</div>
</div>
</van-popup>
</template>
<script lang="ts" setup>
import { computed, nextTick, reactive } from 'vue';
import VideoApi from '@/service/video';
import { useCommunityStore } from '@/stores/community';
import { watchCount } from '@/utils/index';
const props = defineProps<{
isOpen: boolean;
}>();
const emit = defineEmits(['close']);
const isShow = computed(() => {
return props.isOpen;
});
const communityStore = useCommunityStore();
const state = reactive({
pageNumber: 1,
pageSize: 20,
list: <any>[],
selectTagValue: communityStore.tags,
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
const selectTag = (tag: any) => {
communityStore.setTopics(null);
nextTick(() => {
communityStore.setTopics(tag);
state.selectTagValue = communityStore.tags;
showSelected();
emit('close');
});
};
const showSelected = () => {
if (state.selectTagValue) {
state.list = state.list.map((itemTag: any) => {
if (itemTag.id == state.selectTagValue?.['id']) {
itemTag.select = true;
} else {
itemTag.select = false;
}
return itemTag;
});
}
};
const getList = async (type: any) => {
// let req = {
// keyWords:[""],
// realm :"tag",
// pageNumber: state.pageNumber,
// pageSize: state.pageSize,
// };
try {
let res: any = await VideoApi.searchPublishPost();
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (!res.hasNext) {
moreParams.finished = true;
}
showSelected();
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
getList(null);
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList(null);
};
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
getList('refresh');
};
</script>
<style lang="scss" scoped>
.wrapper {
width: 100%;
height: 100%;
background: #fff;
.hader {
display: flex;
padding: 16px 12px;
justify-content: center;
flex-direction: column;
align-items: center;
font-size: 18px;
.hader_line {
width: 36px;
height: 5px;
background: #f5f5f5;
border-radius: 99px;
margin-bottom: 20px;
}
// .ht_close {
// height: 20px;
// width: 20px;
// }
// > div {
// color: #666666;
// }
}
.content {
height: 400px;
.cell {
width: 100%;
padding: 0 12px;
box-sizing: border-box;
margin-bottom: 12px;
border-radius: 5px;
.avatar {
width: 56px;
height: 56px;
border-radius: 4px;
overflow: hidden;
margin: 0 10px 0 0;
}
.mess {
flex: 1;
height: 45px;
display: flex;
flex-direction: column;
justify-content: space-around;
font-size: 14px;
}
.count {
display: flex;
align-items: center;
// justify-content: space-between;
font-size: 10px;
color: #999999;
> span {
margin-right: 12px;
}
}
.cell_btn {
width: 52px;
height: 26px;
line-height: 26px;
font-size: 12px;
text-align: center;
background: #3476ff;
color: #fff;
border-radius: 99px;
}
.btn_active {
background: #30324d;
color: #9a9a9a;
}
}
}
}
</style>
@@ -0,0 +1,125 @@
<template>
<div class="publish_box">
<Header title="发布图片" rightTitle="规则" @rightFuc="state.ruleShow = true" />
<div class="content overflow-y-auto">
<div class="changeboxs" @click="$router.push('/topicMore')">
<div v-if="communityStore.tags?.id" class="info">
<lazyImg :src="communityStore.tags.coverImg" class="avatar" />
<div class="mess">
<div class="name ellipsis">#{{ communityStore.tags.name }}</div>
</div>
</div>
<div class="flex-vcenter" v-else>
<SvgIcon name="publish_#" />
<span>选择标签</span>
</div>
<SvgIcon name="publishRight" color="#fff" />
</div>
<van-field @blur="blur" class="titleText" v-model="state.title" placeholder="请填写标题" />
<h2 class="subTitle">
添加图集
<span>最大每张1M以内</span>
</h2>
<UploadImg :isCover="true" @uploadSuccess="uploadSuccess" @delImg="delImg" ref="uploadFileRef" :multiple="true" />
<div class="btnbox">
<div class="btn flex-center-center" @click="publish">确认发布</div>
</div>
</div>
<!-- <PublishTip :isOpen="state.isOpen" @close="(state.isOpen = false), $router.go(-1)" /> -->
<!-- <SelectHt :isOpen="state.htOpen" @close="state.htOpen = false" /> -->
<RulePopup :isOpen="state.ruleShow" @close="state.ruleShow = false" />
</div>
</template>
<script lang="ts">
export default {
name: 'pubilshImg',
};
</script>
<script lang="ts" setup>
import { ref, reactive, nextTick, watch } from 'vue';
import { showToast } from 'vant';
import { useRoute, useRouter } from 'vue-router';
import { useCommunityStore } from '@/stores/community';
import CommunityApi from '@/service/community';
// import SelectHt from '../components/select_ht.vue';
import RulePopup from '../components/rule_popup.vue';
const communityStore = useCommunityStore();
const router = useRouter();
const route = useRoute();
const uploadFileRef = ref(null);
const state = reactive({
title: '', //标题
imgList: <Array<string>>[], //上传图片
isOpen: false,
htOpen: false,
ruleShow: false,
});
watch(
() => route.path,
(toPath, fromPath) => {
try {
if (toPath == '/community') {
resetPublish();
}
} catch (e) {
console.log(e);
}
},
);
const blur = () => {
nextTick(() => {
window.scrollTo(0, 0);
});
};
const uploadSuccess = (img: string) => {
state.imgList.push(img);
};
const delImg = (index: number) => {
state.imgList.splice(index, 1);
};
// 发布
const publish = async () => {
if (!communityStore.tags?.id) return showToast('请选择帖子标签');
if (!state.title.trim()) return showToast('请输入标题');
if (!state.imgList.length) return showToast('请上传图片');
let req = {
title: state.title,
newsType: 'PIC',
seriesCover: state.imgList,
tags: [communityStore.tags.id],
};
try {
await CommunityApi.publishGraphics(req);
resetPublish();
showToast('发布成功');
setTimeout(() => {
router.back();
}, 1000);
// state.isOpen = true;
} catch (err) {
console.log(err);
}
};
const resetPublish = () => {
state.title = '';
state.imgList = [];
let uploadFilesRef: any = uploadFileRef.value;
uploadFilesRef.clearImg();
communityStore.setTopics(null);
};
</script>
<style lang="scss" scoped>
@import '../styles/publish.scss';
</style>
@@ -0,0 +1,165 @@
<template>
<div class="publish_box">
<Header title="发布图文" rightTitle="规则" @rightFuc="state.ruleShow = true" />
<div class="content overflow-y-auto">
<div class="changeboxs" @click="$router.push('/topicMore')">
<div v-if="communityStore.tags?.id" class="info">
<lazyImg :src="communityStore.tags.coverImg" class="avatar" />
<div class="mess">
<div class="name ellipsis">#{{ communityStore.tags.name }}</div>
</div>
</div>
<div class="flex-vcenter" v-else>
<SvgIcon name="publish_#" />
<span>选择标签</span>
</div>
<SvgIcon name="publishRight" color="#fff" />
</div>
<van-field @blur="blur" class="titleText" v-model="state.title" placeholder="请填写标题" />
<van-field
@blur="blur"
class="titleText"
v-model="state.content"
rows="5"
autosize
maxlength="300"
show-word-limit
type="textarea"
placeholder="随手发张手里的涩图吧..."
/>
<!-- <div class="setPrice" @click="state.showSetPricePop = true">
<div class="flex-center-center">
<img src="@/assets/images/png/coins.png" class="price_icon" alt="" />
设置价格
</div>
<div class="flex-center-center">
<span v-if="state.price">{{ Number(state.price) ? state.price + '金币' : '' }}</span>
<span style="color: #00000080" v-else>免费</span>
<SvgIcon name="publishRight" color="#2D2D38" />
</div>
</div> -->
<h2 class="subTitle">
添加图集
<span>最大每张1M以内</span>
</h2>
<UploadImg :isCover="true" @uploadSuccess="uploadSuccess" @delImg="delImg" ref="uploadFileRef" :multiple="true" />
<div class="btnbox">
<div class="btn flex-center-center" @click="publish">确认发布</div>
</div>
</div>
<!-- <PublishTip :isOpen="state.isOpen" @close="(state.isOpen = false), $router.go(-1)" /> -->
<!-- <SetPricePop v-if="state.showSetPricePop" @blur="blur" :isOpen="state.showSetPricePop" @close="setPrices" /> -->
<!-- <SelectHt :isOpen="state.htOpen" @close="state.htOpen = false" /> -->
<RulePopup :isOpen="state.ruleShow" @close="state.ruleShow = false" />
</div>
</template>
<script lang="ts">
export default {
name: 'publishGraphics',
};
</script>
<script lang="ts" setup>
import { ref, reactive, onActivated, nextTick, watch } from 'vue';
import { showToast } from 'vant';
import { useRoute, useRouter } from 'vue-router';
import { useCommunityStore } from '@/stores/community';
import CommunityApi from '@/service/community';
// import SelectHt from '../components/select_ht.vue';
import RulePopup from '../components/rule_popup.vue';
const communityStore = useCommunityStore();
const router = useRouter();
const route = useRoute();
const uploadFileRef = ref(null);
const state = reactive({
title: '', //标题
content: '', //描述
imgList: <Array<string>>[], //上传图片
tags: <any>communityStore.tags,
isOpen: false,
showSetPricePop: false,
price: '',
htOpen: false,
ruleShow: false,
});
watch(
() => route.path,
(toPath, fromPath) => {
try {
if (toPath == '/community') {
resetPublish();
}
} catch (e) {
console.log(e);
}
},
);
const blur = () => {
nextTick(() => {
window.scrollTo(0, 0);
});
};
const uploadSuccess = (img: string) => {
state.imgList.push(img);
};
const delImg = (index: number) => {
state.imgList.splice(index, 1);
};
// 发布
const publish = async () => {
if (!communityStore.tags?.id) return showToast('请选择帖子标签');
if (!state.title.trim()) return showToast('请输入标题');
if (!state.content.trim()) return showToast('请输入内容');
if (!state.imgList.length) return showToast('请上传图片');
let req = {
content: state.content, //描述
title: state.title,
newsType: 'COVER',
seriesCover: state.imgList,
tags: [communityStore.tags.id],
coins: Number(state.price),
};
console.log(req);
try {
await CommunityApi.publishGraphics(req);
resetPublish();
showToast('发布成功');
setTimeout(() => {
router.back();
}, 1000);
// state.isOpen = true;
} catch (err) {
console.log(err);
}
};
const setPrices = (data: any) => {
state.price = data;
state.showSetPricePop = false;
};
const resetPublish = () => {
state.title = '';
state.content = '';
state.tags = '';
state.price = '';
state.imgList = [];
let uploadFilesRef: any = uploadFileRef.value;
uploadFilesRef.clearImg();
communityStore.setTopics(null);
};
</script>
<style lang="scss" scoped>
@import '../styles/publish.scss';
</style>
@@ -0,0 +1,61 @@
<template>
<div class="publishRule">
<Header title="发布规则" />
<div class="content overflow-y-auto">
<div class="mess">
欢迎优秀的您加入Mr.色,我们珍视每一位UP主, 始终致力于为大家带来最优质的产品与服务。您的
作品在妖精动漫的售卖无时间、次数限制,销售的次数越多, 获得的收入就会越多。无论您是在睡觉还是在工作,它们都
将持续为您带来源源不断的收入。
</div>
<div class="mess">
<p>上传规则</p>
<p>1.UP主及普通用户上传收费视频比例为2:1,即上传2个免费视频才可上传1个收费视频。</p>
<p>2.原创举牌up主上传收费视频比例为1:1,即上传1个免费视频才可上传1个收费视频。</p>
<p>3.视频清晰度需在360P以上,且时长不小于30秒。</p>
<p>4.套图大于6张才可以设置价格。</p>
<p>5.审核时间为48小时内,请在[创作中心]查收反馈。</p>
<p>6.视频中的当事人须满18岁以上,且当事人同意视频被上传分享。</p>
</div>
<div class="mess">
<p>审核规则</p>
<p>1.原创拍摄、原创剪辑作品,会更容易通过并获得官方推荐。</p>
<p>2.禁止直接搬运网络视频,重复率高且不容易通过,多次违规将降低账号推荐权重。</p>
<p>3.禁止在视频/图片中添加个人联系方式或插入广告网址将不会通过审核。</p>
<p>4.禁止上传幼女、人兽、真实强奸等侵害他人的视频。</p>
<p>5.加强用户隐私性,允许原创视频为人物面部等重要部分添加遮挡或马赛克。</p>
<p>6.上传的视频内容不符合上传要求将不会通过审核,如若退回视频未作修改再次发起审核将禁止上传。</p>
</div>
<div class="mess">
<p>定价规则</p>
<p>1.发布内容默认为免费,用户可根据作品内容质量调整为金币视频。</p>
<p>2.认证UP主发布原创举牌长视频,建议定价50-200金币</p>
<p>3.原创举牌作品,建议定价30-50金币</p>
<p>4.原创剪辑作品,建议定价20-40金币</p>
<p>5.非原创短片,建议定价10-20金币</p>
</div>
</div>
</div>
</template>
<script></script>
<style lang="scss" scoped>
.publishRule {
width: 100%;
height: 100%;
.content {
height: calc(100vh - 44px);
padding: 12px 16px 100px 16px;
box-sizing: border-box;
.mess {
font-size: 12px;
line-height: 20px;
letter-spacing: 0.1px;
margin-bottom: 25px;
}
}
}
</style>
@@ -0,0 +1,223 @@
<template>
<div class="publish_box">
<Header title="发布视频" rightTitle="规则" @rightFuc="state.ruleShow = true" />
<div class="content overflow-y-auto">
<div class="changeboxs" @click="$router.push('/topicMore')">
<div v-if="communityStore.tags?.id" class="info">
<lazyImg :src="communityStore.tags.coverImg" class="avatar" />
<div class="mess">
<div class="name ellipsis">#{{ communityStore.tags.name }}</div>
</div>
</div>
<div class="flex-vcenter" v-else>
<SvgIcon name="publish_#" />
<span>选择标签</span>
</div>
<SvgIcon name="publishRight" color="#fff" />
</div>
<van-field @blur="blur" class="titleText" v-model="state.title" placeholder="请填写标题" />
<van-field
@blur="blur"
class="titleText"
v-model="state.content"
rows="5"
autosize
type="textarea"
maxlength="300"
show-word-limit
placeholder="有趣的介绍能让你的逼格提高N个档次!..."
/>
<!-- <van-field
@blur="blurPrcie"
class="titleText"
v-model="state.price"
placeholder="请设置解锁价格0~999金币,不设置则免费"
/> -->
<h2 class="subTitle">
添加视频
<span>最大300M以内</span>
</h2>
<div class="video_wrap">
<UploadImg
:isCover="true"
@uploadSuccess="coverUploadSuccess"
@delImg="coverDelImg"
ref="couverUploadFileRef"
text="上传封面"
maxCount="1"
/>
<UploadVideo @uploadVideoSuccess="uploadVideoSuccess" ref="uploadVideoRef" />
</div>
<h2 class="subTitle">
添加图集
<span>最大每张1M以内</span>
</h2>
<UploadImg
:isCover="true"
@uploadSuccess="uploadSuccess"
@delImg="delImg"
ref="uploadFileRef"
:multiple="true"
maxCount="9"
/>
<div class="setPrice" @click="state.showSetPricePop = true">
<div class="flex-center-center">
<img src="@/assets/images/png/coins.png" class="price_icon" alt="" />
设置价格
</div>
<div class="flex-center-center">
<span v-if="state.price">{{ Number(state.price) ? state.price + '金币' : '' }}</span>
<span style="color: #989898" v-else>免费</span>
<SvgIcon name="publishRight" color="#fff" />
</div>
</div>
<div class="btnbox">
<div class="btn flex-center-center" @click="publish">确认发布</div>
</div>
</div>
<!-- <PublishTip :isOpen="state.isOpen" @close="(state.isOpen = false), $router.go(-1)" /> -->
<SetPricePop @blur="blur" :isOpen="state.showSetPricePop" @close="setPrices" />
<!-- <SelectHt :isOpen="state.htOpen" @close="state.htOpen = false" /> -->
<RulePopup :isOpen="state.ruleShow" @close="state.ruleShow = false" theme="rulePop" />
</div>
</template>
<script lang="ts">
export default {
name: 'publishGraphics',
};
</script>
<script lang="ts" setup>
import { ref, reactive, nextTick, watch } from 'vue';
import { showToast } from 'vant';
import { useRoute, useRouter } from 'vue-router';
import { useCommunityStore } from '@/stores/community';
import CommunityApi from '@/service/community';
// import SelectHt from '../components/select_ht.vue';
import RulePopup from '../components/rule_popup.vue';
const communityStore = useCommunityStore();
const router = useRouter();
const route = useRoute();
const uploadFileRef = ref(null);
const uploadVideoRef = ref(null);
const couverUploadFileRef = ref(null);
const state = reactive({
title: '', //标题
content: '', //描述
imgList: <Array<string>>[], //上传图片
uploadData: <any>{},
price: '',
isOpen: false,
showSetPricePop: false,
htOpen: false,
ruleShow: false,
cover: '',
});
watch(
() => route.path,
(toPath, fromPath) => {
try {
if (toPath == '/community') {
resetPublish();
}
} catch (e) {
console.log(e);
}
},
);
const blur = () => {
nextTick(() => {
window.scrollTo(0, 0);
});
};
const blurPrcie = () => {
if (state.imgList.length < 6) {
state.price = '';
showToast('当图片小于6张不可设置金额');
}
nextTick(() => {
window.scrollTo(0, 0);
});
};
const uploadSuccess = (img: string) => {
state.imgList.push(img);
};
const coverUploadSuccess = (img: string) => {
state.cover = img;
};
const delImg = (index: number) => {
state.imgList.splice(index, 1);
};
const coverDelImg = () => {
state.cover = '';
};
const uploadVideoSuccess = (uploadData: any) => {
state.uploadData = uploadData;
};
// 发布
const publish = async () => {
if (!communityStore.tags?.id) return showToast('请选择帖子标签');
if (!state.title.trim()) return showToast('请输入标题');
if (!state.content.trim()) return showToast('请输入内容');
if (!state.cover) return showToast('请上传封面');
if (!state.imgList.length) return showToast('请上传封面');
if (!state.uploadData.sourceURL) {
showToast('请上传视频或等视频上传完毕');
return;
}
state.uploadData.title = state.title;
state.uploadData.tags = [communityStore.tags.id];
state.uploadData.content = state.content;
state.uploadData.coins = Number(state.price);
state.uploadData.seriesCover = state.imgList;
state.uploadData.cover = state.cover;
state.uploadData.coverThumb = state.cover;
state.uploadData.newsType = 'SP';
try {
await CommunityApi.publishGraphics(state.uploadData);
resetPublish();
showToast('发布成功');
setTimeout(() => {
router.back();
}, 1000);
// state.isOpen = true;
} catch (err) {
console.log(err);
}
};
const setPrices = (data: any) => {
state.price = data;
state.showSetPricePop = false;
};
const resetPublish = () => {
state.title = '';
communityStore.setTopics(null);
state.price = '';
state.content = '';
state.imgList = [];
let uploadFilesRef: any = uploadFileRef.value;
uploadFilesRef.clearImg();
let uploadVideosRef: any = uploadVideoRef.value;
uploadVideosRef.uploadDelete();
uploadVideosRef.reductionData();
let coverUploadFilesRef: any = couverUploadFileRef.value;
coverUploadFilesRef.clearImg();
};
</script>
<style lang="scss" scoped>
@import '../styles/publish.scss';
</style>
@@ -0,0 +1,133 @@
.publish_box {
width: 100%;
height: 100%;
overflow: hidden;
.content {
height: calc(100vh - 44px);
width: 100%;
padding: 18px 16px calc(140px + constant(safe-area-inset-bottom)) 16px;
padding: 18px 16px calc(140px + env(safe-area-inset-bottom)) 16px;
box-sizing: border-box;
.changeboxs {
width: 100%;
height: 50px;
padding: 0 12px;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.05);
display: flex;
align-items: center;
justify-content: space-between;
color: #989898;
font-size: 12px;
border-radius: 8px;
span {
margin-left: 4px;
}
svg {
width: 18px;
height: 18px;
}
.info {
flex: 1;
display: flex;
align-items: center;
.avatar {
width: 38px;
height: 38px;
border-radius: 5px;
overflow: hidden;
margin: 0 12px 0 0;
}
.mess {
font-weight: 550;
flex: 1;
height: 38px;
display: flex;
flex-direction: column;
justify-content: space-around;
font-size: 14px;
color: #fff;
}
}
}
.titleText {
margin-top: 12px;
background: rgba(255, 255, 255, 0.05);
border-radius: 5px;
font-size: 12px;
:deep(.van-field__control) {
color: #fff;
overflow: hidden;
}
::placeholder {
color: #525252;
}
}
:deep(.van-cell::after) {
border-bottom: solid 1px transparent !important;
}
.subTitle {
margin: 18px 0 14px 0;
display: flex;
align-items: flex-end;
font-weight: bold;
font-size: 18px;
span {
font-weight: normal;
padding-left: 10px;
box-sizing: border-box;
font-size: 12px;
color: #525252;
}
}
.video_wrap{
display: flex;
:deep(.upload-img) {
margin-right: 12px;
}
}
.setPrice {
background: rgba(255, 255, 255, 0.05);
border-radius: 5px;
font-size: 12px;
width: 100%;
height: 50px;
margin-top: 16px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
color: #989898;
.price_icon {
width: 24px;
height: 24px;
margin-right: 6px;
}
svg {
width: 16px;
height: 16px;
margin-left: 3px;
}
span {
color: #F68804;
}
}
.btnbox {
width: 100%;
height: 64px;
position: fixed;
bottom: constant(safe-area-inset-bottom);
bottom: env(safe-area-inset-bottom);
left: 0;
padding: 0 40px;
box-sizing: border-box;
.btn {
height: 44px;
border-radius: 3px;
background: #F68804;
}
}
}
}
@@ -0,0 +1,188 @@
<template>
<div class="topicMore">
<!-- <Header title="选择标签" :rightTitle="state.selectTagValue ? '1/1' : '0/1'" /> -->
<Header title="选择标签" />
<div class="content overflow-y-auto">
<template v-if="!state.loading">
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<!-- :class="['cell flex-center-between',item.select ? 'active' : '']" -->
<div class="cell flex-center-between" v-for="item in state.list" :key="item" @click="selectTag(item)">
<div class="info">
<lazyImg :src="item.coverImg" class="avatar" />
<div class="mess">
<div class="name ellipsis">#{{ item.name }}</div>
<div class="count">
<span>{{ watchCount(item.vidCount) }}个帖子</span>
<span>{{ watchCount(item.playCount) }}浏览</span>
<span>{{ watchCount(item.followCount) }}关注</span>
</div>
</div>
</div>
<div :class="['btn flex-center-center', item.select ? 'active' : '']">
{{ item.select ? '已选' : '选择' }}
</div>
</div>
</PullRefresh>
</template>
<Loading v-else />
</div>
</div>
</template>
<script lang="ts" setup>
import { nextTick, reactive } from 'vue';
import { useRouter } from 'vue-router';
import VideoApi from '@/service/video';
import { useCommunityStore } from '@/stores/community';
import { watchCount } from '@/utils/index';
const router = useRouter();
const communityStore = useCommunityStore();
const state = reactive({
pageNumber: 1,
pageSize: 10,
list: <any>[],
selectTagValue: communityStore.tags,
loading: true,
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: true,
});
const selectTag = (tag: any) => {
communityStore.setTopics(null);
nextTick(() => {
communityStore.setTopics(tag);
state.selectTagValue = communityStore.tags;
showSelected();
router.go(-1);
});
};
const showSelected = () => {
if (state.selectTagValue) {
state.list = state.list.map((itemTag: any) => {
if (itemTag.id == state.selectTagValue?.['id']) {
itemTag.select = true;
} else {
itemTag.select = false;
}
return itemTag;
});
}
};
const getList = async (type: any) => {
// let req = {
// keyWords:[""],
// realm :"tag",
// pageNumber: state.pageNumber,
// pageSize: state.pageSize,
// };
try {
let res: any = await VideoApi.searchPublishPost();
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (!res.hasNext) {
moreParams.finished = true;
}
showSelected();
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
getList(null);
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList(null);
};
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
getList('refresh');
};
</script>
<style lang="scss" scoped>
.topicMore {
width: 100%;
height: 100%;
.content {
height: calc(100vh - 44px);
.cell {
height: 66px;
width: 100%;
padding: 0 12px;
box-sizing: border-box;
border-radius: 5px;
background: rgba(255, 255, 255, 0.05);
margin: 0 0 12px 0;
.info {
flex: 1;
display: flex;
align-items: center;
.avatar {
width: 38px;
height: 38px;
border-radius: 5px;
overflow: hidden;
margin: 0 12px 0 0;
}
.mess {
flex: 1;
height: 38px;
display: flex;
flex-direction: column;
justify-content: space-around;
font-size: 16px;
font-weight: 550;
}
.count {
display: flex;
align-items: center;
font-size: 10px;
color: #999999;
margin: 5px 0 0 0;
font-weight: normal;
span {
padding-right: 10px;
}
}
}
.btn {
width: 52px;
height: 26px;
border-radius: 30px;
font-size: 12px;
color: #dcdcdc;
background: rgba(255, 255, 255, 0.2);
}
.active {
background: #f68804;
color: #fff;
}
}
}
}
</style>
@@ -0,0 +1,364 @@
<template>
<div v-if="type == 'COVER'" class="postBox">
<Post :postInfo="info" :rank="rank" />
</div>
<div class="itemBox" v-else @click="handleToPath">
<div class="rank-item" :class="{ 'rank-acg': ['video', 'image', 'PIC'].includes(type) }">
<div class="rank-cover">
<lazyImg class="rank-cover-img" :src="info.cover || info.horizontalCover || info.verticalCover" alt="封面" />
<template v-if="['SP'].includes(type)">
<div class="duration">{{ secondsToTimeFormat(info.playTime) }}</div>
<div class="watch-count">
<SvgIcon name="playCountIcon" />
{{ watchCount(info.playCount) }}
</div>
</template>
</div>
<div class="rank-info">
<div v-if="props.rank < 4" class="rank-icon" :class="`rank-icon-${props.rank}`"></div>
<div v-else class="sortbox">
<div class="sortbox-icon"></div>
NO.{{ props.rank }}
</div>
<div class="title ellipsis2">{{ info.title }}</div>
<div class="rank-info-footer">
<div class="tag-box" v-if="['COVER'].includes(type)" @touchstart.stop @touchmove.stop @touchend.stop>
<div class="tag-box-item" v-for="tag in info.tags" :key="tag">{{ tag.name }}</div>
</div>
<div class="acg-info" v-if="['video', 'image'].includes(type)">
<span v-if="info.updateStatus != 1">已完结 · {{ info.totalEpisode }}话</span>
<span class="loading" v-else>更新{{ info.currentEpisode || info.totalEpisode }}章 · 连载中</span>
</div>
<div class="meta" v-else>
<!-- <span>{{ moment(info.createdAt).format('YYYY/MM/DD') }}</span> -->
<div class="browseBox" v-if="['PIC'].includes(type)">
<SvgIcon name="browse" />
{{ watchCount(info.playCount) }}
</div>
<span class="meta-like">
<SvgIcon name="rank-like" />
{{ watchCount(info.likeCount) }}
</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { watchCount, secondsToTimeFormat, isLongFormVideo } from '@/utils';
import moment from 'moment';
import { useRouter } from 'vue-router';
import { useVideoStore } from '@/stores/video';
import { useGlobalStore } from '@/stores/global';
const videoStore = useVideoStore();
const globalStore = useGlobalStore();
const router = useRouter();
const props = defineProps({
info: {
type: Object,
required: true,
},
type: {
type: String,
required: true,
},
rank: {
type: Number,
default: 0,
},
list: {
type: Array,
default: () => [],
},
});
const handleToPath = () => {
const { info, type } = props;
switch (type) {
case 'SP': // 影片
openVideo(info);
break;
case 'video': //动漫
router.push({
path: '/AcgVideoDetail',
query: { id: info.id },
});
break;
case 'COVER': // 帖子
router.push({
path: '/postDetail',
query: { postid: info.id },
});
break;
case 'image': // 漫画
router.push({
path: '/AcgImageDetail',
query: { id: info.id },
});
break;
case 'PIC': // 图集
router.push({ path: '/coverPreview', query: { index: 1, id: info.id } });
// router.push({
// path: '/coverDetail',
// query: { id: info.id },
// });
break;
}
};
const openVideo = (item: any) => {
if (isLongFormVideo(item)) {
router.push({
path: '/movieDetails',
query: {
vid: item.id,
uid: item.publisher?.uid,
},
});
} else {
videoStore.deleteVideoList();
videoStore.saveVideoList([item as any], item as any);
router.push(`/shortVideoDetails?id=${item.id}`);
}
};
</script>
<style lang="scss" scoped>
.postBox {
padding: 0 12px;
}
.itemBox {
padding: 0 16px;
.rank-item {
margin-bottom: 16px;
display: flex;
gap: 12px;
}
.rank-cover {
position: relative;
width: 168px;
height: 95px;
border-radius: 5px;
overflow: hidden;
background: #333;
&:before {
content: '';
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.6));
z-index: 1;
}
&-img {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
top: 0;
left: 0;
}
}
.duration {
position: absolute;
right: 4px;
bottom: 4px;
color: #fff;
padding: 2px 4px;
font-size: 10px;
border-radius: 3px;
height: 16px;
z-index: 2;
//background: rgba(0, 0, 0, 0.7);
}
.watch-count {
position: absolute;
left: 4px;
bottom: 4px;
color: #fff;
font-size: 10px;
border-radius: 3px;
//background: rgba(0, 0, 0, 0.7);
padding: 2px 4px;
display: flex;
align-items: center;
z-index: 2;
.svg-icon {
margin-right: 0;
font-size: 16px;
}
}
.rank-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
.rank-icon {
width: 59px;
height: 24px;
&-1 {
background: url('@/assets/images/png/rank_one.png') no-repeat center center;
background-size: 100% 100%;
}
&-2 {
background: url('@/assets/images/png/rank_two.png') no-repeat center center;
background-size: 100% 100%;
}
&-3 {
background: url('@/assets/images/png/rank_three.png') no-repeat center center;
background-size: 100% 100%;
}
&-4 {
background: url('@/assets/images/png/rank_four.png') no-repeat center center;
background-size: 100% 100%;
}
}
.sortbox {
display: flex;
align-items: center;
color: #bdbdbd;
font-family: 'PingFang SC';
font-size: 14px;
.sortbox-icon {
margin-right: 4px;
width: 24px;
height: 24px;
background: url('@/assets/images/png/rank_other.png') no-repeat center center;
background-size: 100% 100%;
}
}
.rank-info-footer {
display: flex;
align-items: center;
justify-content: space-between;
.tag-box {
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: 4px;
overflow-x: auto;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
flex: 1;
// 隐藏滚动条但保持可滚动
&::-webkit-scrollbar {
display: none;
}
&-item {
border-radius: 4px;
border: 0.5px solid #560000;
background: linear-gradient(100deg, #810906 0%, #370000 100%);
color: #fff;
padding: 2px 4px;
font-size: 10px;
width: fit-content;
white-space: nowrap;
}
}
.meta {
color: #999;
font-size: 10px;
display: flex;
width: 40px;
display: flex;
align-items: center;
.browseBox {
display: inline-flex;
align-items: center;
font-size: 12px;
color: #989898;
svg {
width: 16px;
height: 16px;
margin-right: 4px;
}
margin-right: 10px;
}
.meta-like {
display: inline-flex;
align-items: center;
font-size: 12px;
color: #989898;
svg {
width: 16px;
height: 16px;
margin-right: 4px;
}
}
}
}
}
.title {
color: #fff;
font-size: 12px;
line-height: 1.4;
margin-top: 6px;
margin-bottom: 8px;
}
}
// 动漫、漫画
.rank-acg {
.rank-cover {
width: 80px;
height: 108px;
&:before {
content: '';
width: 100%;
height: 30px;
position: absolute;
left: 0;
bottom: 0;
top: unset;
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.6));
z-index: 1;
}
}
.rank-info {
.title {
margin-top: 12px;
margin-bottom: 12px;
}
}
.rank-info-footer {
.acg-info {
font-size: 10px;
color: #999;
.loading {
color: yellow;
}
}
}
}
</style>
@@ -0,0 +1,210 @@
<template>
<div class="list-container">
<div v-if="!state.loading">
<van-tabs
v-model:active="state.tabActive"
title-active-color="rgb(255,255,255)"
title-inactive-color="rgba( 113, 118 ,126)"
color="rgb(255,255,255)"
animated
:scrollspy="false"
:lazy-render="true"
@change="onTabChange"
>
<van-tab title="周榜" :name="2"></van-tab>
<van-tab title="月榜" :name="3"></van-tab>
<van-tab title="年榜" :name="5"></van-tab>
<van-tab title="总榜" :name="4"></van-tab>
</van-tabs>
<div class="list-box overflow-y-auto" :id="`${scrollId}_${state.tabActive}`" ref="mainRef">
<PullRefresh v-if="!state.loading" :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<Item
v-for="(item, index) in state.list"
:key="item.id"
:rank="index + 1"
:info="item"
:type="props.type"
:list="state.list"
/>
</PullRefresh>
</div>
</div>
<div class="isLoading" v-else>
<Loading />
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, nextTick, onMounted } from 'vue';
import Item from './item.vue';
import CommunityApi from '@/service/community';
import AcgApi from '@/service/acg';
import TabPageScroll from '@/utils/resetTabPageScrollTop';
import useCommon from '@/hooks/common';
const { routeLeaveRecord, routeBackRecord } = useCommon();
const mainRef = ref();
const props = defineProps({
type: {
type: String,
required: true,
},
scrollId: {
type: String,
},
});
const state = reactive({
tabActive: 1,
loading: true,
pageNumber: 1,
pageSize: 10,
list: [],
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
});
const tabPageScroll = new TabPageScroll();
const getList = async () => {
try {
let req = {
pageNumber: state.pageNumber,
pageSize: state.pageSize,
newsType: props.type,
type: state.tabActive,
// mediaType: props.type,
};
let ajax = AcgApi.getRanking;
if (['video', 'image'].includes(props.type)) {
ajax = AcgApi.getAcgRanking;
}
let res: any = await ajax(req);
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (state.pageNumber == 1) {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (!res.hasNext) {
moreParams.finished = true;
}
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
onMounted(async () => {
routeLeaveRecord(mainRef.value);
routeBackRecord(mainRef.value);
await getList();
nextTick(() => {
tabPageScroll.onChange(`${props.scrollId}_${state.tabActive}`);
});
});
const onLoad = () => {
if (moreParams.refreshing) return;
moreParams.loading = true;
state.pageNumber++;
getList();
};
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
getList();
};
const onTabChange = async (index: number) => {
state.tabActive = index;
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
moreParams.isNoData = false;
moreParams.error = false;
state.list = [];
await getList();
nextTick(() => {
tabPageScroll.onChange(`${props.scrollId}_${index}`);
});
};
</script>
<style lang="scss" scoped>
.isLoading {
height: 100%;
}
.list-container {
height: 100%;
}
:deep(.van-tabs) {
--van-tabs-nav-background: transparent;
--van-tab-text-color: #999;
--van-tab-active-text-color: #fff;
.van-tabs__wrap {
height: 26px !important;
box-sizing: border-box;
box-sizing: border-box;
position: sticky;
top: -1px;
font-size: 12px;
z-index: 20;
// background-color: rgba(255, 255, 255, 0.05) !important;
background-color: transparent !important;
width: 230px;
margin: 12px auto;
border-radius: 999px;
.van-tabs__nav {
background: transparent;
display: flex;
align-items: center;
padding-left: 0;
padding-right: 0;
.van-tab {
height: 26px;
width: 68px !important;
margin: 0 5px 0 0;
font-size: 14px !important;
font-weight: 400;
}
.van-tab--active {
font-weight: 500;
}
}
.van-tabs__line {
display: none;
}
}
.van-tabs__content {
padding: 0 10px;
}
}
.list-box {
height: calc(100vh - 140px);
overflow-y: auto;
}
</style>
+91
View File
@@ -0,0 +1,91 @@
<template>
<div class="ranking-page">
<!-- 顶部导航栏 -->
<Header title="热门排行" />
<!-- 分类标签页 -->
<van-tabs
v-model:active="state.type"
title-active-color="#fff"
title-inactive-color="#999"
animated
:scrollspy="false"
swipeable
:lazy-render="true"
>
<van-tab :title="item.title" v-for="item in state.tabList" :key="item.type" :name="item.type">
<List :type="item.type" :scrollId="`rankingList_${item.type}`" />
</van-tab>
</van-tabs>
</div>
</template>
<script lang="ts">
export default {
name: 'rankingList',
};
</script>
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue';
import List from './components/list.vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const state = reactive({
type: 'SP',
list: [],
tabList: [
{
title: '影片',
type: 'SP',
},
// {
// title: '动漫',
// type: 'video',
// },
// {
// title: '漫画',
// type: 'image',
// },
{
title: '帖子',
type: 'COVER',
},
{
title: '图集',
type: 'PIC',
},
],
});
onMounted(() => {
if (route.query.type) {
state.type = route.query.type as string;
}
});
</script>
<style lang="scss" scoped>
.ranking-page {
min-height: 100vh;
}
:deep(.van-tabs) {
--van-tabs-nav-background: transparent;
--van-tab-text-color: #fff;
.van-tabs__wrap {
background-color: #000;
}
}
:deep(.van-tabs__nav) {
padding-left: 30px;
padding-right: 30px;
font-size: 16px;
.van-tabs__line {
background: linear-gradient(90deg, rgba(246, 136, 4, 0) 0%, #f68804 100%);
width: 16px;
height: 4px;
}
}
</style>
@@ -0,0 +1,237 @@
<template>
<!-- 广告轮播 -->
<div class="chat_wrapper overflow-y-auto" id="soulScrollId">
<HomeHeader
:advType="AdType.COMMUNITYCAROUSEL"
class="advBox"
/>
<PullRefresh v-if="!state.loading" :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="soul-item" v-for="item in state.list" :key="item.id">
<div class="soul-item-avatar">
<!-- <img src="@/assets/images/png/soul/tab1.png" alt="" /> -->
<lazyImg :src="item.cover" />
</div>
<div class="soul-item-info">
<div class="soul-item-info-name">{{ item.name }}</div>
<div class="soul-item-info-desc">{{ item.summary }}</div>
<div class="soul-item-info-btn">
<div>
<span class="soul-id">ID: {{ item.groupId }}</span>
<span>{{ watchCount(item.fakeMemberNum) }}人在玩</span>
</div>
<span class="join-btn" @click="joinGroup(item)">加入群聊</span>
</div>
</div>
</div>
</PullRefresh>
<Loading v-else />
<GoldPopup
:isOpen="state.showGoldPopup"
@close="closeGoldPopup"
:price="state.price"
:productId="state.id"
:groupId="state.groupId"
@buySuccess="handleBuySuccess"
/>
<TipPopup :isOpen="state.isVipShowTip" :tip="state.tip" @close="handleCloseTipPopup"></TipPopup>
</div>
</template>
<script setup lang="ts">
import { AdType } from '@/enums/app';
import { reactive, nextTick } from 'vue';
// import SoulApi from '@/service/soul';
import { watchCount } from '@/utils/index';
import { useRouter } from 'vue-router';
import GoldPopup from '../components/GoldPopup.vue';
import MineApi from '@/service/mine';
import TabPageScroll from '@/utils/resetTabPageScrollTop';
import { useUserStore } from '@/stores/user';
import CommunityApi from '@/service/community';
const userStore = useUserStore();
const tabPageScroll = new TabPageScroll();
const router = useRouter();
const state = reactive({
loading: true,
pageNumber: 1,
pageSize: 10,
list: <any>[],
showGoldPopup: false,
groupId: '',
price: '',
name: '',
id: '',
isVipShowTip: false,
tip: '本内容需要开通至尊VIP会员<br>开通会员 即可畅享高级群聊特权',
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 获取话题
const getList = async (type: any) => {
try {
let req = {
pageNumber: state.pageNumber,
pageSize: state.pageSize,
};
let res: any = await CommunityApi.queryImgroupList(req);
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
console.log(list, 'list');
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (!res.hasNext) {
moreParams.finished = true;
}
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
nextTick(async () => {
await getList('');
tabPageScroll.add('#soulScrollId');
});
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList(null);
};
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
getList('refresh');
};
// 加入群聊
const joinGroup = async (item: any) => {
console.log(userStore.isVip);
if (userStore.userInfo.vipLevel < 4) {
state.isVipShowTip = true;
return;
}
try {
const res = await checkJoin(item);
console.log(res);
if (res.status === 2) {
state.id = item.id;
state.groupId = item.groupId;
state.price = item.price;
state.name = item.name;
if (item.price > 0) {
state.showGoldPopup = true;
} else {
buyGroup(item);
}
} else {
router.push(`/groupChat?id=${item.groupId}&name=${item.name}`);
}
} catch (error) {
console.log(error);
// state.groupId = item.groupId;
// state.price = item.price;
// state.showGoldPopup = true;
}
};
const checkJoin = async (item: any) => {
let res: any = await CommunityApi.queryImgroupHasjoin({ groupId: item.groupId });
return res;
};
const closeGoldPopup = () => {
state.showGoldPopup = false;
};
const buyGroup = async (item: any) => {
await MineApi.goldExchange({
productID: item.id,
productType: 101,
});
router.push(`/groupChat?id=${item.groupId}&name=${item.name}`);
};
const handleBuySuccess = () => {
state.showGoldPopup = false;
router.push(`/groupChat?id=${state.groupId}&name=${state.name}`);
};
const handleCloseTipPopup = () => {
state.isVipShowTip = false;
};
</script>
<style scoped lang="scss">
.chat_wrapper {
width: 100vw;
height: calc(100vh - 100px);
.soul-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 13px 12px;
background-color: rgba(255, 255, 255, 0.05);
border-radius: 6px;
margin: 10px;
.soul-item-avatar {
width: 64px;
height: 64px;
border-radius: 50%;
overflow: hidden;
img {
width: 100%;
height: 100%;
}
}
.soul-item-info {
flex: 1;
margin-left: 10px;
.soul-item-info-name {
font-size: 14px;
font-weight: 400;
margin-bottom: 4px;
}
.soul-item-info-desc {
font-size: 12px;
color: rgba(255, 255, 255, 0.55);
margin-bottom: 8px;
line-height: 18px;
}
.soul-item-info-btn {
font-size: 12px;
color: #999;
display: flex;
align-items: center;
justify-content: space-between;
.soul-id {
margin-right: 8px;
}
}
.join-btn {
background: #f68804;
color: #fff;
padding: 6px 12px;
border-radius: 3px;
}
}
}
}
</style>
@@ -0,0 +1,137 @@
<template>
<div class="community">
<van-tabs
v-model:active="globalStore.communityActiveIndex"
class="oepnX"
title-active-color="#fff"
title-inactive-color="#9FA19F"
background="transparent"
animated
:scrollspy="false"
:lazy-render="true"
@change="change"
>
<van-tab v-for="(item, index) in state.tabList" :key="item.id">
<template #title>
<div class="tab_title">
{{ item.moduleName }}
<div class="tab_title_line" v-if="globalStore.communityActiveIndex == index" />
</div>
</template>
<CommunityItem
:tabIndex="index"
:moduleId="item.id"
:moduleName="item.moduleName"
:pureVersion="item.pureVersion"
:defaultTagId="item.defaultTagId"
/>
</van-tab>
</van-tabs>
</div>
</template>
<script lang="ts" setup>
import { reactive, computed, onActivated, onDeactivated } from 'vue';
import { useGlobalStore } from '@/stores/global';
import { useRoute } from 'vue-router';
import { setShowBackTopBtn } from '@/utils/communityCombinedFunc';
import CommunityItem from '@/views/community/components/CommunityItem.vue';
const route = useRoute();
const globalStore = useGlobalStore();
const state = reactive({
tabList: <Array<Types.Home.HomeTabItem>>[],
scrollDistance: <any>0, // 滚动距离
});
const init = () => {
let tabObj: any = globalStore.tabObj;
if (tabObj.community) {
state.tabList = tabObj.community || [];
}
};
init();
let keepScroll = computed<{ [key: string]: any[] }>(() => {
return globalStore.keepScroll; // 获取保存在sessionStore中的滚动距离
});
// 监听滚动执行的方法
const scroll = () => {
let dom = document.getElementById('community' + globalStore.communityActiveIndex); // 获取滚动的dom
let arr = keepScroll.value[route.path]; // 获取当前路由下所保存的滚动距离数组
// 循环获取到的数组
arr.map((item: any) => {
// 找到数组里当前需要更改的对象
if (item.scrollObj == globalStore.communityActiveIndex) {
// 更新对象里面的滚动距离
item.scrollDistance = dom?.scrollTop;
if (item.scrollDistance >= 500) {
setShowBackTopBtn(globalStore.communityActiveIndex, true);
} else {
setShowBackTopBtn(globalStore.communityActiveIndex, false);
}
}
return item;
});
// 将更新好后的数组赋值给当前路由下所保存的滚动距离数组
keepScroll.value[route.path] = arr;
};
// 切换tab的时候需要执行的事件
const change = () => {
let dom = document.getElementById('community' + globalStore.communityActiveIndex);
let arr = keepScroll.value[route.path]; // 获取当前路由下所保存的滚动距离数组
let obj = arr.find((item: any) => item.scrollObj == globalStore.communityActiveIndex); // 循环获取到的数组,并找出是否存在当前tab所保存的滚动对象
if (obj) {
dom?.scrollTo(0, obj.scrollDistance); // 如果存在说明已经访问过,回到滚动条高度
} else {
// 不存在说明是第一次访问,那么就创建一个对象push到数组中
arr.push({
scrollObj: globalStore.communityActiveIndex, // 当前的tabActive索引当作对象的key
scrollDistance: 0, // 滚动高度
});
}
window.addEventListener('scroll', scroll, true); // 添加滚动监听,并执行滚动的方法
};
onActivated(() => {
change(); // 从别的页面回来时,触发缓存方法,并需要执行change函数里面的步骤
});
onDeactivated(() => {
window.removeEventListener('scroll', scroll, true); // 离开当前页面时,需要将监听滚动的事件移除,否则占用内存
});
</script>
<style lang="scss" scoped>
.community {
overflow: hidden;
:deep(.van-tabs) {
.van-tabs__wrap {
height: 44px;
background: #0f0f0f;
.van-tabs__nav {
background: unset;
.van-tab {
font-size: 15px;
}
.van-tabs__line {
height: 4px;
width: 10px;
background: linear-gradient(rgb(132, 164, 249), rgb(43, 93, 222));
border-radius: 10px;
bottom: 24px;
display: none;
}
}
}
}
.tab_title {
display: flex;
flex-direction: column;
align-items: center;
height: 28px;
.tab_title_line {
margin-top: 6px;
width: 16px;
height: 2px;
border-radius: 12px;
background: #f68804;
}
}
}
</style>
@@ -0,0 +1,70 @@
<template>
<van-tabs
v-model:active="tabActive"
:animated="true"
:scrollspy="false"
shrink
:lazy-render="true"
swipeable
@rendered="onTabRendered"
class="tabs"
>
<van-tab v-for="item in secondTab" :key="item.id" :title="item.moduleName" :name="item.id">
<template v-if="tabIsInit(item.id)">
<GameItem :moduleId="item.id" :id="prefixKey + item.id" class="contain overflow-y-auto" />
</template>
</van-tab>
</van-tabs>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { useGlobalStore } from '@/stores/global';
import GameItem from '../components/gameItem.vue';
import tabAction from '@/utils/tabAction';
const globalStore = useGlobalStore();
const secondTab = computed(() => {
return [
{
moduleName: '热门推荐',
id: 'all',
},
...(globalStore.tabObj.game || []),
];
});
const prefixKey = 'game_page_';
const { tabActive, tabIsInit, onTabRendered } = tabAction(true, prefixKey);
</script>
<style lang="scss" scoped>
.tabs {
--van-tabs-nav-background: transparent;
--van-tab-active-text-color: #fff;
--van-tab-text-color: #9fa19f;
--van-padding-xs: 6px;
:deep(> .van-tabs__wrap) {
.van-tabs__nav--line {
padding-bottom: 0;
}
.van-tab__text {
padding: 4px;
border-radius: 4px;
}
.van-tabs__line {
width: 16px;
height: 2px;
border-radius: 12px;
background: #f68804;
bottom: 4px;
}
}
.contain {
height: calc(100vh - 44px * 3);
}
}
</style>
@@ -0,0 +1,75 @@
<template>
<van-tabs
v-model:active="tabActive"
:animated="true"
:scrollspy="false"
shrink
:lazy-render="true"
swipeable
@rendered="onTabRendered"
class="tabs"
>
<van-tab v-for="item in secondTab" :key="item.id" :title="item.moduleName" :name="item.id">
<template v-if="tabIsInit(item.id)">
<coverSeekType
:moduleId="item.id"
:id="prefixKey + item.id"
class="contain overflow-y-auto"
:pureVersion="item.pureVersion"
/>
</template>
</van-tab>
</van-tabs>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import tabAction from '@/utils/tabAction';
import { useGlobalStore } from '@/stores/global';
import coverSeekType from '../components/coverSeekType.vue';
const globalStore = useGlobalStore();
const secondTab = computed(() => {
return [
{
moduleName: '热门推荐',
id: 'all',
},
...(globalStore.tabObj.pics || []),
];
});
const prefixKey = 'seek_page_';
const { tabActive, tabIsInit, onTabRendered } = tabAction(true, prefixKey);
</script>
<style scoped lang="scss">
.tabs {
--van-tabs-nav-background: transparent;
--van-tab-active-text-color: #fff;
--van-tab-text-color: #9fa19f;
--van-padding-xs: 6px;
:deep(> .van-tabs__wrap) {
.van-tabs__nav--line {
padding-bottom: 0;
}
.van-tab__text {
padding: 4px;
border-radius: 4px;
}
.van-tabs__line {
width: 16px;
height: 2px;
border-radius: 12px;
background: #f68804;
bottom: 4px;
}
}
.contain {
height: calc(100vh - 44px * 3);
}
}
</style>
@@ -0,0 +1,82 @@
<template>
<div class="comment-wrap">
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<UserCommentItem v-for="item in state.list" :key="item.id" :item="item"></UserCommentItem>
</PullRefresh>
</div>
</template>
<script lang="ts">
export default {
name: 'comment',
};
</script>
<script lang="ts" setup>
import { reactive } from 'vue';
import UserApi from '@/service/user';
import { useRoute } from 'vue-router';
import UserCommentItem from '@/components/UserCommentItem';
const route = useRoute();
const state = reactive({
loading: true,
list: [],
});
const page = {
pageNumber: 1,
pageSize: 20,
};
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
const getList = async () => {
const { pageNumber } = page;
let req = {
pageNumber,
pageSize: page.pageSize,
uid: route.query.uid,
};
try {
let res: any = await UserApi.getCmment(req);
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (pageNumber === 1) {
state.list = list;
} else {
state.list = state.list.concat(list);
}
moreParams.isNoData = state.list.length === 0;
if (!res.hasNext) {
moreParams.finished = true;
}
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
getList();
const onLoad = () => {
moreParams.loading = true;
page.pageNumber++;
getList();
};
const onRefresh = () => {
page.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
getList();
};
</script>
<style lang="scss" scoped>
.comment-wrap {
height: 100%;
padding: 0 10px;
}
</style>
@@ -0,0 +1,22 @@
.userInfo-tab-wrap {
height: 100%;
}
.type-box {
position: absolute;
top: 14px;
right: 10px;
z-index: 3;
color: #ccc;
font-size: 12px;
.active {
color: #3476ff;
font-weight: 500;
}
span:not(:last-child) {
&::after {
content: '/';
color: #ccc;
margin: 0 8px;
}
}
}
@@ -0,0 +1,157 @@
<template>
<div class="post-wrap userInfo-tab-wrap">
<Teleport to=".van-tabs__wrap">
<div class="type-box">
<span
:class="{ active: item.value === state.typeActive }"
@click="(state.typeActive = item.value), (state.list = [])"
v-for="item in state.typeList"
:key="item.value"
>
{{ item.name }}
</span>
</div>
</Teleport>
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div v-if="state.typeActive === 'video'" class="moreMoviesBox">
<HorizontalVideoBox
class="videoBox"
:videoInfo="item"
:videoList="state.list"
v-for="item in state.list"
:key="item.id"
/>
</div>
<template v-else>
<div class="listBox" v-for="(item, index) in state.list" :key="index">
<!-- <p>
{{ fromNowTime(item.reviewAt) }}
</p> -->
<!-- <UserPost class="post" :postInfo="item" :currentIndex="index" :list="state.list" :key="item" /> -->
<Post
class="post"
:postInfo="item"
:currentIndex="index"
:list="state.list"
:key="item"
:isNoShowTags="true"
/>
</div>
</template>
</PullRefresh>
</div>
</template>
<script lang="ts">
export default {
name: 'index',
};
</script>
<script lang="ts" setup>
import { reactive, watch } from 'vue';
import UserApi from '@/service/user';
import { fromNowTime } from '@/utils/index';
import { useRoute } from 'vue-router';
const route = useRoute();
const emit = defineEmits(['resetScrollTop']);
const state = reactive({
list: <Array<any>>[],
typeActive: 'video',
typeList: [
{
value: 'video',
name: '视频',
},
{
value: 'image',
name: '帖子',
},
],
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
disabled: false,
});
const page = {
pageNumber: 1,
pageSize: 10,
};
// 获取帖子数据
const getList = async () => {
try {
const { pageNumber } = page;
let req = {
uid: route.query.uid,
pageNumber,
pageSize: page.pageSize,
likeType: state.typeActive,
};
let res: any = await UserApi.queryLikeList(req);
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (pageNumber === 1) {
state.list = list;
} else {
state.list = state.list.concat(list);
}
moreParams.isNoData = state.list.length === 0;
if (!res.hasNext) {
moreParams.finished = true;
}
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
getList();
const onLoad = () => {
moreParams.loading = true;
page.pageNumber++;
getList();
};
const onRefresh = () => {
page.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
getList();
};
watch(
() => state.typeActive,
() => {
onRefresh();
emit('resetScrollTop');
},
);
</script>
<style lang="scss" scoped>
@import './common.scss';
.post-wrap {
.listBox {
padding: 0px 10px;
p {
margin: 10px 10px 0;
font-size: 12px;
color: #c0c1d1;
}
}
.moreMoviesBox {
display: grid;
grid-template-columns: 1fr 1fr;
grid-column-gap: 7px;
padding: 0 10px;
}
.videoBox {
width: 174px;
}
}
</style>
@@ -0,0 +1,127 @@
<template>
<div class="post-wrap userInfo-tab-wrap">
<Teleport to=".van-tabs__wrap">
<div class="type-box">
<span
:class="{ active: item.value === state.typeActive }"
@click="state.typeActive = item.value"
v-for="item in state.typeList"
:key="item.value"
>
{{ item.name }}
</span>
</div>
</Teleport>
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div class="listBox" v-for="(item, index) in state.list" :key="index">
<p>发布时间:{{ fromNowTime(item.reviewAt) }}</p>
<UserPost class="post" :postInfo="item" :currentIndex="index" :list="state.list" :key="item" />
</div>
</PullRefresh>
</div>
</template>
<script lang="ts">
export default {
name: 'index',
};
</script>
<script lang="ts" setup>
import { reactive, watch } from 'vue';
import UserApi from '@/service/user';
import { fromNowTime } from '@/utils/index';
import { useRoute } from 'vue-router';
const route = useRoute();
const emit = defineEmits(['resetScrollTop']);
const state = reactive({
list: [],
typeActive: 'hot',
typeList: [
{
value: 'hot',
name: '最热',
},
{
value: 'new',
name: '最新',
},
],
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
disabled: false,
});
const page = {
pageNumber: 1,
pageSize: 4,
};
// 获取帖子数据
const getList = async () => {
try {
const { pageNumber } = page;
let req = {
uid: route.query.uid,
pageNumber,
pageSize: page.pageSize,
type: state.typeActive,
};
let res: any = await UserApi.queryCollectionList(req);
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (pageNumber === 1) {
state.list = list;
} else {
state.list = state.list.concat(list);
}
moreParams.isNoData = state.list.length === 0;
if (!res.hasNext) {
moreParams.finished = true;
}
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
getList();
const onLoad = () => {
moreParams.loading = true;
page.pageNumber++;
getList();
};
const onRefresh = () => {
page.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
getList();
};
watch(
() => state.typeActive,
() => {
onRefresh();
emit('resetScrollTop');
},
);
</script>
<style lang="scss" scoped>
@import './common.scss';
.post-wrap {
.listBox {
p {
margin: 10px 10px 0;
font-size: 12px;
color: #c0c1d1;
}
}
}
</style>
+555
View File
@@ -0,0 +1,555 @@
<template>
<div class="userHomePage overflow-y-auto" id="userHomePage">
<template v-if="!state.loading">
<div class="headerBox">
<SvgIcon name="back" @click="$router.go(-1)" />
<div class="avatarBox">
<lazyImg :src="state.userInfo.portrait" :key="state.userInfo.portrait" class="avatar" />
</div>
<img src="@/assets/images/png/userInfo_bg.png" alt="" class="userBg" />
<!-- <lazyImg
class="userBg"
v-if="state.userInfo.background && state.userInfo.background.length"
:src="state.userInfo.background[0]"
:key="state.userInfo.background[0]"
/> -->
</div>
<div class="header flex-center-between" v-if="state.scrollDistance > 150">
<div class="leftBox flex-vcenter">
<SvgIcon name="back" class="headerBack" @click="$router.go(-1)" />
<lazyImg class="cover" :src="state.userInfo.portrait" :key="state.userInfo.portrait" />
<div class="name ellipsis">
{{ state.userInfo.name }}
</div>
</div>
<div class="rightBtn" v-if="route.query.uid">
<div class="focusedBtn flex-center-center" @click="focus" v-if="!state.userInfo.isFollow">
<SvgIcon name="follow_add_user" />
关注
</div>
<div class="focusBtn" v-else @click="focus">
<svgIcon name="follow_add_yes" />
已关注
</div>
</div>
</div>
<div class="main">
<div class="padding16">
<div class="rightBox flex-vcenter" v-if="route.query.uid">
<div class="rightBtn">
<div class="focusedBtn flex-center-center" @click="focus" v-if="!state.userInfo.isFollow">
<SvgIcon name="follow_add_user" />
关注
</div>
<div class="focusBtn" v-else @click="focus">
<SvgIcon name="follow_add_yes" />
已关注
</div>
</div>
</div>
<div class="placeholder" v-else></div>
<div class="userName flex-vcenter">
<div class="name ellipsis">
{{ state.userInfo.name }}
</div>
</div>
<div class="info-box">
<div class="info-box-item">
{{ watchCount(state.userInfo.coverUploadCount + state.userInfo.vidUploadCount) }} 作品
</div>
<div class="info-box-item">{{ watchCount(state.userInfo.fans) }} 粉丝</div>
</div>
<div class="desc ellipsis2" v-if="state.userInfo.summary">
{{ state.userInfo.summary }}
</div>
</div>
<div class="userHomePageTab">
<van-tabs
v-model:active="state.tabActive"
class="oepnX"
title-active-color="rgba(255, 255, 255, 0.9)"
title-inactive-color="rgba(255, 255, 255, 0.45)"
animated
:scrollspy="false"
:swipeable="false"
:lazy-render="true"
>
<van-tab :title="item.label" v-for="item in state.moduleSorts" :key="item.value">
<PostItem ref="postRef" :moduleSort="item.value" v-if="item.value == 'COVER'" />
<VideoItem ref="videoRef" :moduleSort="item.value" v-else />
</van-tab>
</van-tabs>
</div>
</div>
</template>
<Loading v-else />
</div>
</template>
<script lang="ts">
export default {
name: 'userInfo',
};
</script>
<script setup lang="ts">
import { ref, reactive, onActivated, onDeactivated, onBeforeUnmount } from 'vue';
import { showToast } from 'vant';
import { useRoute } from 'vue-router';
import { watchCount } from '@/utils/index';
import VideoItem from './video.vue';
import PostItem from './postItem.vue';
import UserApi from '@/service/user';
import useLoading from '@/hooks/loading';
const postRef = ref();
const videoRef = ref();
const route = useRoute();
const { openLoading, closeLoading } = useLoading();
const state = reactive({
userInfo: <Types.User.UserInfoType>{}, // 用户信息
rewardList: <any>[], // 打赏列表
width: '0px', // 打赏头像的区域宽度
showReward: false, // 打赏弹窗
moduleSorts: [
{
label: '影视',
value: 'SP',
},
{
label: '抖音',
value: 'SHORT',
},
{
label: '帖子',
value: 'COVER',
},
{
label: '图集',
value: 'PIC',
},
],
tabActive: 0,
scrollDistance: <any>0, // 滚动距离
loading: true,
});
// 查询用户
const getUserInfo = async () => {
try {
let req = {
uid: route.query.uid,
};
let res: any = await UserApi.queryUserInfo(req);
state.userInfo = res;
state.loading = false;
} catch (e) {
console.log(e);
}
};
// 关注用户
const focus = () => {
try {
openLoading();
let hasFollowed = state.userInfo.isFollow;
UserApi.followCancel(state.userInfo?.uid, !hasFollowed).then(() => {
state.userInfo.isFollow = !hasFollowed;
showToast(!hasFollowed ? '关注成功' : '取消成功');
state.userInfo.fans = !hasFollowed ? state.userInfo.fans + 1 : state.userInfo.fans - 1;
});
} catch (e) {
closeLoading();
console.log(e);
} finally {
closeLoading();
}
};
// 监听滚动执行的方法
const scroll = () => {
let dom = document.getElementById('userHomePage');
state.scrollDistance = dom?.scrollTop;
};
onActivated(() => {
getUserInfo();
let dom = document.getElementById('userHomePage');
dom?.scrollTo(0, state.scrollDistance);
window.addEventListener('scroll', scroll, true);
});
onDeactivated(() => {
window.removeEventListener('scroll', scroll, true);
});
onBeforeUnmount(() => {
window.removeEventListener('scroll', scroll, true);
});
</script>
<style scoped lang="scss">
:deep(.van-overlay) {
background-color: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
}
.userHomePage {
height: 100%;
background: rgba(3, 15, 24, 1);
.man-icon {
width: 22px !important;
height: 22px !important;
color: #0071ff;
}
.super-icon {
width: 22px !important;
height: 22px !important;
}
.woman-icon {
width: 22px !important;
height: 22px !important;
color: #fd0563;
}
.headerBox {
height: 150px;
width: 375px;
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 12px 16px;
position: relative;
> svg {
height: 24px;
width: 24px;
z-index: 1;
// color: #000;
}
.report {
font-size: 12px;
color: #757575;
z-index: 1;
}
.avatarBox {
position: absolute;
bottom: -41px;
left: 16px;
z-index: 1;
.avatar {
height: 80px;
width: 80px;
border-radius: 50%;
overflow: hidden;
border: 2px solid rgba(3, 15, 24, 1);
}
.vipIcon {
position: absolute;
right: 0;
bottom: 0;
height: 22px;
width: 22px;
}
}
.userBg {
height: 100%;
width: 100%;
position: absolute;
left: 0;
top: 0;
}
}
.header {
height: 44px;
width: 100%;
position: fixed;
left: 0;
top: 0;
z-index: 10;
padding: 0 16px;
background: black;
.leftBox {
.headerBack {
height: 24px;
width: 24px;
// color: #000;
}
.cover {
height: 30px;
width: 30px;
border-radius: 50%;
overflow: hidden;
margin: 0 4px;
}
.name {
max-width: 150px;
}
.vipName {
color: #facc15;
}
> svg {
height: 18px;
width: 18px;
margin-left: 4px;
}
.userIcon {
width: 18px;
height: 18px;
margin-left: 4px;
}
}
}
.rightBtn {
.focusedBtn {
width: 50px;
height: 24px;
background: rgba(246, 136, 4, 0.1);
border-radius: 4px;
font-size: 10px;
color: #f68804;
> svg {
margin-right: 2px;
height: 16px;
width: 16px;
}
}
.focusBtn {
display: flex;
align-items: center;
justify-content: center;
svg {
width: 16px;
height: 16px;
margin-right: 2px;
}
width: 62px;
height: 24px;
color: #989898;
background: #101b24;
border-radius: 4px;
font-size: 10px;
}
}
.main {
height: 100%;
.padding16 {
padding: 0 16px;
.rightBox {
margin: 18px 0 18px 244px;
.messageBox {
height: 24px;
width: 30px;
margin-right: 8px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(246, 136, 4, 0.1);
color: #f68804;
border-radius: 4px;
font-size: 10px;
> svg {
height: 16px;
width: 16px;
margin-right: 3px;
}
}
}
.placeholder {
height: 59px;
}
.userName {
color: #fff;
font-size: 16px;
font-weight: 500;
line-height: 22px;
margin-bottom: 10px;
.name {
max-width: 240px;
}
.vipName {
color: #facc15;
}
> svg {
height: 18px;
width: 18px;
margin-left: 4px;
}
.userIcon {
width: 18px;
height: 18px;
margin-left: 4px;
}
}
.idBox {
padding: 4px 12px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.05);
width: fit-content;
color: #757575;
font-size: 10px;
margin-bottom: 18px;
> svg {
height: 14px;
width: 14px;
margin-right: 10px;
}
}
.desc {
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
line-height: 18px;
margin-bottom: 18px;
}
.getData {
position: relative;
margin-bottom: 18px;
.itemBox {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.quantity {
color: #fff;
font-size: 20px;
font-weight: 500;
line-height: 28px;
}
.itemDesc {
color: #757575;
font-size: 12px;
line-height: 18px;
margin-top: 4px;
}
}
.verticalLine:nth-last-of-type(1) {
height: 20px;
width: 1px;
background: rgba(255, 255, 255, 0.1);
position: absolute;
left: 116px;
top: 50%;
transform: translate(0, -50%);
}
.verticalLine:nth-last-of-type(2) {
height: 20px;
width: 1px;
background: rgba(255, 255, 255, 0.1);
position: absolute;
right: 116px;
top: 50%;
transform: translate(0, -50%);
}
}
.rewardUserBox {
background: rgb(26, 26, 26);
height: 56px;
border-radius: 56px;
padding: 0 12px;
.rewardUsers {
position: relative;
.userAvatarBox {
position: relative;
width: v-bind('state.width');
.userAvatar {
height: 30px;
width: 30px;
border-radius: 50%;
overflow: hidden;
border: 1px solid white;
position: absolute;
}
.userAvatar:nth-of-type(1) {
left: 0;
z-index: 1;
}
.userAvatar:nth-of-type(2) {
left: 18px;
z-index: 2;
}
.userAvatar:nth-of-type(3) {
left: 36px;
z-index: 3;
}
.userAvatar:nth-of-type(4) {
left: 54px;
z-index: 4;
}
.userAvatar:nth-of-type(5) {
left: 72px;
z-index: 5;
}
.userAvatar:nth-of-type(6) {
left: 90px;
z-index: 6;
}
.userAvatar:nth-of-type(7) {
left: 108px;
z-index: 7;
}
.userAvatar:nth-of-type(8) {
left: 126px;
z-index: 8;
}
}
.people {
color: #fff;
font-size: 14px;
font-weight: 400;
margin-left: 12px;
min-width: 90px;
}
}
.rewardBtn {
display: flex;
width: 53px;
height: 26px;
justify-content: center;
align-items: center;
gap: 10px;
flex-shrink: 0;
border-radius: 15px;
background: linear-gradient(107deg, #f88000 0%, #f70 100%);
color: #fff;
font-size: 14px;
letter-spacing: 0.2px;
}
}
}
.userHomePageTab {
position: relative;
margin-top: 18px;
:deep(.van-tabs) {
.van-tabs__wrap {
position: sticky;
top: 44px;
z-index: 10;
background: rgba(3, 15, 24, 1);
// padding: 0 32px;
display: flex;
.van-tabs__nav {
padding-bottom: unset;
align-items: center;
background: unset;
flex: 1;
.van-tab {
font-size: 14px;
// margin-right: 32px;
}
.van-tab--active {
font-weight: 500;
}
.van-tabs__line {
height: 3px;
width: 13px;
background: linear-gradient(90deg, rgba(246, 136, 4, 0) 0%, #f68804 100%);
border-radius: 99px;
bottom: 4px;
}
}
}
}
}
}
.info-box {
color: rgba(101, 101, 101, 1);
font-size: 12px;
display: flex;
align-items: center;
margin-bottom: 10px;
&-item {
margin-right: 15px;
}
}
}
</style>
+125
View File
@@ -0,0 +1,125 @@
<template>
<div class="postItem">
<!-- <div class="sortType">
<div class="sort" :class="state.sortType == 2 ? 'sortSelected' : ''" @click="changeType(2)">最多点赞</div>
<div class="sort_line" />
<div class="sort" :class="state.sortType == 1 ? 'sortSelected' : ''" @click="changeType(1)">最新上架</div>
</div> -->
<template v-if="!state.loading">
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<Post
:postInfo="item"
:list="state.list"
v-for="(item, index) in state.list"
:key="item.id"
:currentIndex="index"
/>
</PullRefresh>
</template>
<div v-else class="isLoading">
<Loading />
</div>
</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue';
import { useRoute } from 'vue-router';
import UserApi from '@/service/user';
const route = useRoute();
const state = reactive({
pageNumber: 1,
pageSize: 10,
list: <Array<any>>[],
sortType: 2,
loading: true,
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 获取视频信息
const getList = async (type: any) => {
try {
let req = {
uid: route.query.uid,
pageNumber: state.pageNumber,
pageSize: state.pageSize,
newsType: 'COVER',
// sortType: state.sortType,
};
let res: any = await UserApi.minePublish(req);
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (!res.hasNext) {
moreParams.finished = true;
}
} catch (e) {
console.log(e);
moreParams.loading = false;
moreParams.error = true;
}
};
getList('');
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList(null);
};
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
getList('refresh');
};
const changeType = (sortType: number) => {
state.list = <any>[];
state.sortType = sortType;
state.pageNumber = 1;
state.loading = true;
moreParams.isNoData = false;
moreParams.finished = false;
getList('refresh');
};
</script>
<style lang="scss" scoped>
.postItem {
padding: 0 16px;
.isLoading {
height: calc(100vh - 420px);
}
.sortType {
display: flex;
align-items: center;
height: 40px;
.sort_line {
height: 12px;
width: 1px;
background: linear-gradient(180deg, rgba(85, 54, 52, 0) 0%, #553634 50%, rgba(85, 54, 52, 0) 100%);
margin: 0 20px;
}
.sort {
color: #fff;
font-size: 12px;
text-align: center;
}
.sortSelected {
color: #e1351f;
}
}
}
</style>
+170
View File
@@ -0,0 +1,170 @@
<template>
<div class="video">
<!-- <div class="sortType">
<div class="sort" :class="state.sortType == 2 ? 'sortSelected' : ''" @click="changeType(2)">最多点赞</div>
<div class="sort_line" />
<div class="sort" :class="state.sortType == 1 ? 'sortSelected' : ''" @click="changeType(1)">最新上架</div>
</div> -->
<template v-if="!state.loading">
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
<div
:class="[
state.newsType == 'SP' ? 'moreMoviesBox' : 'moreMoviesBoxShortVideo',
state.newsType == 'SHORT' ? 'shortVideo' : '',
]"
>
<HorizontalVideoBox
class="videoBox"
:videoList="state.list"
:videoInfo="item"
v-for="item in state.list"
:key="item.id"
:isPic="state.newsType === 'PIC'"
/>
</div>
</PullRefresh>
</template>
<div v-else class="isLoading">
<Loading />
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import { useRoute } from 'vue-router';
import UserApi from '@/service/user';
const route = useRoute();
const props = withDefaults(
defineProps<{
moduleSort: string;
}>(),
{
moduleSort: 'MOVIE',
},
);
const state = reactive({
loading: true,
pageNumber: 1,
pageSize: 10,
list: <any>[], // 真实列表
sortType: 2,
newsType: props.moduleSort,
});
const moreParams = reactive({
loading: true,
refreshing: false,
finished: false,
isNoData: false,
error: false,
isHigehtMax: false,
});
// 获取更多视频
const getList = async (type: any) => {
let req = {
uid: route.query.uid,
pageNumber: state.pageNumber,
pageSize: state.pageSize,
newsType: state.newsType,
// sortType: state.sortType,
};
try {
let res: any = await UserApi.minePublish(req);
state.loading = false;
moreParams.loading = false;
moreParams.refreshing = false;
let list = res.list || [];
if (type === 'refresh') {
state.list = list;
} else {
state.list = state.list.concat(list);
}
if (state.pageNumber == 1 && state.list.length == 0) {
moreParams.isNoData = true;
}
if (!res.hasNext) {
moreParams.finished = true;
}
} catch (e) {
moreParams.loading = false;
moreParams.error = true;
}
};
getList(null);
const onLoad = () => {
moreParams.loading = true;
state.pageNumber++;
getList(null);
};
const onRefresh = () => {
state.pageNumber = 1;
moreParams.refreshing = true;
moreParams.finished = false;
moreParams.isNoData = false;
getList('refresh');
};
const changeType = (sortType: number) => {
state.list = <any>[];
state.sortType = sortType;
state.pageNumber = 1;
state.loading = true;
moreParams.isNoData = false;
moreParams.finished = false;
getList('refresh');
};
</script>
<style lang="scss" scoped>
.video {
padding: 16px 16px 0;
.moreMoviesBox {
display: grid;
grid-template-columns: 1fr 1fr;
grid-column-gap: 7px;
// grid-row-gap: 12px;
.videoBox {
width: 168px;
}
}
.moreMoviesBoxShortVideo {
display: grid;
grid-template-columns: 1fr 1fr;
grid-column-gap: 6px;
// grid-row-gap: 12px;
.videoBox {
width: 168px;
}
:deep(.horizontalVideoBox .coverBox) {
height: 224px;
}
}
.shortVideo {
:deep(.videoTitleInfo) {
display: none;
}
}
.isLoading {
height: calc(100vh - 420px);
}
.sortType {
display: flex;
align-items: center;
height: 40px;
.sort_line {
height: 12px;
width: 1px;
background: linear-gradient(180deg, rgba(85, 54, 52, 0) 0%, #553634 50%, rgba(85, 54, 52, 0) 100%);
margin: 0 20px;
}
.sort {
color: #fff;
font-size: 12px;
text-align: center;
}
.sortSelected {
color: #e1351f;
}
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More