初始化
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,244 @@
|
||||
.wrap {
|
||||
width: 100vw;
|
||||
padding: 12px 16px;
|
||||
// background: #262626;
|
||||
// border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
@mixin categoryWrap() {
|
||||
// margin-top: 18px;
|
||||
// padding-bottom: 18px;
|
||||
|
||||
// border-bottom: $borderBottom;
|
||||
}
|
||||
|
||||
@mixin categoryList() {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
align-items: center;
|
||||
}
|
||||
@mixin categoryTitle() {
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
|
||||
.more {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
// color: #fff;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin vanTab() {
|
||||
--van-tabs-line-height: 24px;
|
||||
--van-tabs-nav-background: transparent;
|
||||
--van-tabs-bottom-bar-width: 16px;
|
||||
--van-tabs-bottom-bar-height: 2px;
|
||||
|
||||
--van-tab-font-size: 14px;
|
||||
|
||||
--van-tab-text-color: rgba(255, 255, 255, 0.6);
|
||||
--van-tab-active-text-color: #fff;
|
||||
--van-tabs-bottom-bar-color: #6954e7;
|
||||
|
||||
.tabs {
|
||||
:deep(.van-tabs__wrap) {
|
||||
padding: 10px 0;
|
||||
box-sizing: content-box;
|
||||
.van-tab__text--ellipsis {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.van-tabs__nav {
|
||||
.van-tab {
|
||||
width: auto;
|
||||
padding: 0 16px;
|
||||
margin: 0;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
flex-basis: 36px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
.van-tabs__line {
|
||||
bottom: 12px;
|
||||
// width: 20px;
|
||||
// height: 2px;
|
||||
// @if $theme == 'red' {
|
||||
// background: url('@/assets/images/png/tab-line-red.svg') no-repeat;
|
||||
// } @else {
|
||||
// background: url('@/assets/images/png/tab-line-black.svg') no-repeat;
|
||||
// }
|
||||
// background-size: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
.list {
|
||||
margin: 0;
|
||||
border-bottom: 0;
|
||||
height: calc(100vh - 44px - 10px - 24px);
|
||||
height: calc(100vh - 44px - 10px - 24px - constant(safe-area-inset-bottom));
|
||||
height: calc(100vh - 44px - 10px - 24px - env(safe-area-inset-bottom));
|
||||
overflow-y: auto;
|
||||
}
|
||||
:deep(.van-tabs__content) {
|
||||
.van-tab__panel {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin vanTab-btn() {
|
||||
--van-tab-text-color: #acbabf;
|
||||
--van-tab-active-text-color: $fff;
|
||||
--van-tabs-line-height: 24px;
|
||||
--van-tabs-nav-background: transparent;
|
||||
--van-tabs-bottom-bar-width: 14px;
|
||||
--van-tabs-bottom-bar-height: 2px;
|
||||
--van-tabs-bottom-bar-color: $themeBgLgColor;
|
||||
|
||||
--van-tab-font-size: 14px;
|
||||
|
||||
.tabs {
|
||||
:deep(.van-tabs__wrap) {
|
||||
padding: 16px 0 12px;
|
||||
height: auto;
|
||||
box-sizing: content-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
.van-tabs__nav {
|
||||
border-radius: 24px;
|
||||
background: #20252f;
|
||||
padding: 4px;
|
||||
|
||||
.van-tab {
|
||||
padding: 5px 32px;
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
box-sizing: content-box;
|
||||
border-radius: 15px;
|
||||
|
||||
&.van-tab--active {
|
||||
background: $themeBgLgColor;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.van-tabs__line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// .list {
|
||||
// margin: 0;
|
||||
// border-bottom: 0;
|
||||
// height: calc(100vh - 44px - 10px - 24px);
|
||||
// height: calc(100vh - 44px - 10px - 24px - constant(safe-area-inset-bottom));
|
||||
// height: calc(100vh - 44px - 10px - 24px - env(safe-area-inset-bottom));
|
||||
// overflow-y: auto;
|
||||
// }
|
||||
:deep(.van-tabs__content) {
|
||||
.van-tab__panel {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin vanTab-list() {
|
||||
:deep(.van-tabs) {
|
||||
--van-tabs-nav-background: transform;
|
||||
--van-tab-active-text-color: #6954e7;
|
||||
--van-tab-text-color: rgba(255, 255, 255, 0.6);
|
||||
--van-tabs-bottom-bar-height: 0;
|
||||
.van-tabs__nav--line {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.van-tab {
|
||||
flex: 0;
|
||||
flex-basis: 40px;
|
||||
justify-content: flex-start;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.van-tabs__line {
|
||||
display: none;
|
||||
}
|
||||
.van-tab__panel {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin collected-btn() {
|
||||
display: inline-flex;
|
||||
padding: 4px 12px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 90px;
|
||||
background: $themeBgColor;
|
||||
// box-shadow: 0px 2px 10px 0px rgba(255, 66, 68, 0.8);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
// line-height: 16px; /* 133.333% */
|
||||
|
||||
&.collected {
|
||||
background: #222433;
|
||||
// box-shadow: none;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
svg {
|
||||
// width: 14px;
|
||||
// height: 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin tag() {
|
||||
.permission {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: inline-flex;
|
||||
padding: 2px 6px;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
border-radius: 8px 0px 8px 8px;
|
||||
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px; /* 150% */
|
||||
|
||||
&.permission1 {
|
||||
background: linear-gradient(135deg, #b378ff 0%, #482cf5 100%);
|
||||
}
|
||||
&.permission2 {
|
||||
color: #212121;
|
||||
background: linear-gradient(135deg, #adfcac 0%, #5cef97 100%);
|
||||
}
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div ref="dvLazyImg" class="acg-image-container">
|
||||
<transition name="bounceFade">
|
||||
<div v-if="isLoading && decyImg" class="box">
|
||||
<!-- <img v-show="isLoad" class="left" :src="decyImg" @load="load" />
|
||||
<img v-show="isLoad" class="right" :src="decyImg" /> -->
|
||||
<img v-show="isLoad" :src="decyImg" @load="load" />
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="fade">
|
||||
<div v-if="!isLoad" class="loading"></div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ImgObserver',
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import AppApi from '@/service/app';
|
||||
const emit = defineEmits(['updateIndex', 'updateSize']);
|
||||
const isLoading = ref(false);
|
||||
const props = defineProps<{
|
||||
url: string;
|
||||
index: number;
|
||||
init: boolean;
|
||||
}>();
|
||||
watch(
|
||||
() => props.init,
|
||||
(bool: boolean) => {
|
||||
if (bool) {
|
||||
isLoading.value = true;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const dvLazyImg = ref();
|
||||
const observer = new IntersectionObserver(
|
||||
function (entries) {
|
||||
const entry = entries[0];
|
||||
const { intersectionRatio, isIntersecting } = entry;
|
||||
if (intersectionRatio > 0 || isIntersecting) {
|
||||
isLoading.value = true;
|
||||
}
|
||||
if (intersectionRatio >= 0.5) {
|
||||
emit('updateIndex', { index: props.index, url: props.url });
|
||||
}
|
||||
},
|
||||
{ threshold: 0.8 },
|
||||
);
|
||||
onMounted(() => {
|
||||
observer.observe(dvLazyImg.value);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
observer.unobserve(dvLazyImg.value);
|
||||
});
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const decyImg = ref('');
|
||||
function imgDecypt() {
|
||||
try {
|
||||
let imgSrc = '';
|
||||
const isHttp = new RegExp('http').test(props.url);
|
||||
isHttp == true ? (imgSrc = props.url) : (imgSrc = configStore.sourceInfo.IMAGE + '/' + props.url);
|
||||
AppApi.imgDecypt(imgSrc)
|
||||
.then((url: any) => {
|
||||
decyImg.value = url;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
watch(isLoading, (bool) => {
|
||||
if (bool) {
|
||||
imgDecypt();
|
||||
}
|
||||
});
|
||||
|
||||
const isLoad = ref(false);
|
||||
const load = (e: Event) => {
|
||||
const el = e.target as HTMLImageElement;
|
||||
isLoad.value = true;
|
||||
emit('updateSize', { url: props.url, height: el.height, width: el.width });
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 1s;
|
||||
}
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.bounceFade-enter-active {
|
||||
animation: bounceFade-in 0.4s;
|
||||
}
|
||||
.bounceFade-leave-active {
|
||||
animation: bounceFade-in 0.4s;
|
||||
}
|
||||
|
||||
@keyframes bounceFade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.acg-image-container {
|
||||
.loading {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('@/assets/images/webp/placeholder.webp') no-repeat;
|
||||
background-size: cover;
|
||||
background-position: 50% 50%;
|
||||
background-color: rgb(52, 52, 52);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
object-fit: cover !important;
|
||||
}
|
||||
}
|
||||
|
||||
.box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover !important;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.left {
|
||||
width: 100%;
|
||||
clip-path: inset(0 50% 0 0);
|
||||
}
|
||||
.right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
clip-path: inset(0 0 0 49%);
|
||||
}
|
||||
}
|
||||
|
||||
&.move {
|
||||
.left {
|
||||
transform: translateX(-76.5px);
|
||||
}
|
||||
.right {
|
||||
transform: translateX(76.5px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="(props.list || []).length"
|
||||
class="category-content"
|
||||
:class="[props.coverDirection, `wrap_${props.contentWrap}`, `showType_${props.showType}`]"
|
||||
>
|
||||
<div v-if="$props.id" class="title" @click="goTopicPage(props.id, props.contentType, props.title, props.showType)">
|
||||
<slot name="title"></slot>
|
||||
<span v-if="props.title">{{ props.title }}</span>
|
||||
<span class="more flex-vcenter">
|
||||
{{ props.navTitle || '更多' }}
|
||||
<SvgIcon class="icon" name="moreIcon" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="props.contentWrap" ref="categoryContentList" class="list"></div>
|
||||
<div v-else ref="categoryContentList" class="list" @touchstart.stop @touchmove.stop @touchend.stop></div>
|
||||
<Teleport v-if="categoryContentList" :to="categoryContentList">
|
||||
<CategoryContentItem
|
||||
v-for="(item, index) in props.list"
|
||||
:key="index"
|
||||
:id="item.id"
|
||||
:url="props.coverDirection === 'landscape' ? item.horizontalCover : item.verticalCover"
|
||||
:title="item.title"
|
||||
:contentType="item.mediaType"
|
||||
:info="item"
|
||||
>
|
||||
<template #delBtn>
|
||||
<div class="delBtn" v-if="show" @click.stop="handleDel(item.id)">
|
||||
<div class="selectActive"></div>
|
||||
</div>
|
||||
</template>
|
||||
</CategoryContentItem>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, inject } from 'vue';
|
||||
import { goTopicPage } from '@/views/acgModule/utils/common';
|
||||
import CategoryContentItem from './categoryContentItem.vue';
|
||||
const emit = defineEmits(['onDelCollect', 'del']);
|
||||
const categoryContentList = ref();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
contentWrap?: boolean; //换行显示,portrait三个一行,landscape两个一行
|
||||
contentType?: string; // 'video' | 'image';
|
||||
coverDirection?: 'landscape' | 'portrait'; //landscape横向,portrait纵向
|
||||
title?: string;
|
||||
navTitle?: string;
|
||||
list: Types.Media.AllMediaInfoItem[];
|
||||
id?: string;
|
||||
showType?: number;
|
||||
isDelCollect?: boolean;
|
||||
}>(),
|
||||
{
|
||||
contentWrap: false,
|
||||
contentType: 'video',
|
||||
coverDirection: 'portrait',
|
||||
title: '',
|
||||
navTitle: '更多',
|
||||
list: () => [],
|
||||
id: '',
|
||||
showType: 0,
|
||||
isDelCollect: false,
|
||||
},
|
||||
);
|
||||
const show: boolean | undefined = inject('isEdit', false);
|
||||
const handleDel = (id: string) => {
|
||||
emit('del', id);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '../assets/style.scss';
|
||||
.category-content {
|
||||
@include categoryWrap();
|
||||
|
||||
.title {
|
||||
@include categoryTitle();
|
||||
}
|
||||
.list {
|
||||
@include categoryList();
|
||||
}
|
||||
|
||||
&.landscape {
|
||||
.list {
|
||||
.item {
|
||||
width: 152px;
|
||||
height: 110px;
|
||||
}
|
||||
}
|
||||
&.showType_205 {
|
||||
.list {
|
||||
.item {
|
||||
width: 132px;
|
||||
height: 222px;
|
||||
margin-right: 7px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
//&:nth-child(2n) {
|
||||
// margin-right: 0px;
|
||||
//}
|
||||
//&:nth-child(3n) {
|
||||
// margin-right: 7px;
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.wrap_true {
|
||||
.list {
|
||||
overflow: hidden;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&.landscape {
|
||||
.list {
|
||||
.item {
|
||||
width: 168px;
|
||||
height: 141px;
|
||||
margin-right: 7px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
&:nth-child(2n) {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.portrait {
|
||||
.list {
|
||||
.item {
|
||||
width: 111px;
|
||||
height: 199px;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
&:nth-child(3n) {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 四宫格-横
|
||||
&.showType_102 {
|
||||
.list {
|
||||
.item {
|
||||
width: 168px;
|
||||
height: 263px;
|
||||
margin-bottom: 6px;
|
||||
margin-right: 0;
|
||||
|
||||
&:nth-child(2n) {
|
||||
margin-left: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 四宫格-竖
|
||||
&.showType_201 {
|
||||
.list {
|
||||
.item {
|
||||
width: 168px;
|
||||
height: 270px;
|
||||
margin-right: 7px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
&:nth-child(2n) {
|
||||
margin-right: 0px;
|
||||
}
|
||||
&:nth-child(3n) {
|
||||
margin-right: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.delBtn {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
.selectActive {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
background: rgba(245, 44, 86, 1);
|
||||
background-image: url('@/assets/images/png/del.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: 14px 14px;
|
||||
background-position: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.selectBorder {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid rgba(103, 103, 103, 1);
|
||||
background: #000;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="item" @click="goPage">
|
||||
<slot name="item"></slot>
|
||||
<lazyImg class="cover" :src="autoUrl" :key="autoUrl">
|
||||
<PermissionTag :info="props.info" :content-type="props.contentType" />
|
||||
<slot name="cover"></slot>
|
||||
<slot name="collectDel"></slot>
|
||||
<img
|
||||
v-if="props.info.mediaType === 'text' && props.info.mediaSubType === 1"
|
||||
class="textIcon"
|
||||
src="@/assets/images/png/acg/type_text.png"
|
||||
/>
|
||||
</lazyImg>
|
||||
<div class="title-box">
|
||||
<div class="title">{{ title }}</div>
|
||||
<slot></slot>
|
||||
<UpdateStatus :info="props.info"></UpdateStatus>
|
||||
</div>
|
||||
<slot name="delBtn"></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
// import { useRouter } from 'vue-router';
|
||||
// import { useGlobalStore } from '@/stores/global';
|
||||
// import MediaApi from '@/service/media';
|
||||
// import { showToast } from 'vant';
|
||||
import { goDetailPage } from '../utils/common';
|
||||
import type { MediaTypeKey } from '../utils/common';
|
||||
import PermissionTag from './permission-tag.vue';
|
||||
import UpdateStatus from './updateStatus.vue';
|
||||
// const router = useRouter();
|
||||
// const globalStore = useGlobalStore();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id: string;
|
||||
url: string;
|
||||
title: string;
|
||||
contentType?: string;
|
||||
info: Types.Media.AllMediaInfoItem;
|
||||
}>(),
|
||||
{
|
||||
id: '',
|
||||
url: '',
|
||||
title: '',
|
||||
contentType: 'video',
|
||||
info: {} as any,
|
||||
},
|
||||
);
|
||||
const goPage = () => {
|
||||
goDetailPage(props.info.mediaType as MediaTypeKey, props.id, props.info.mediaSubType);
|
||||
};
|
||||
|
||||
const autoUrl = computed(() => {
|
||||
return props.url || props.info.verticalCover || props.info.horizontalCover;
|
||||
});
|
||||
|
||||
// const hasCollected = ref(props.info.mediaStatus?.hasAddBookshelf || false);
|
||||
// const onFollow = () => {
|
||||
// const isCollect = !hasCollected.value;
|
||||
// (isCollect ? MediaApi.postBookshelfAdd : MediaApi.postBookshelfDel)({
|
||||
// mid: props.id,
|
||||
// }).then(() => {
|
||||
// hasCollected.value = isCollect;
|
||||
// showToast(isCollect ? '成功加入书架' : '成功移出书架');
|
||||
// });
|
||||
// };
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
.item {
|
||||
flex-shrink: 0;
|
||||
width: 132px;
|
||||
height: 234px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: calc(100% - 42px);
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.animationImg) {
|
||||
img {
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
.collect {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 29px;
|
||||
height: 29px;
|
||||
}
|
||||
.textIcon {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
.title-box {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
// height: 33px;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// .icon {
|
||||
// width: 18px;
|
||||
// height: 18px;
|
||||
// flex-shrink: 0;
|
||||
// margin-right: 2px;
|
||||
// margin-bottom: -2px;
|
||||
// &.video {
|
||||
// width: 12px;
|
||||
// height: 12px;
|
||||
// margin-bottom: 0px;
|
||||
// }
|
||||
// }
|
||||
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin: 4px 0 4px;
|
||||
line-height: 18px;
|
||||
@include textEllipsis();
|
||||
}
|
||||
.updateStatus {
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<van-swipe class="category" :autoplay="5000" :show-indicators="false">
|
||||
<van-swipe-item v-for="(item, index) in props.list" :key="index">
|
||||
<div
|
||||
:class="['item', props.mediaType, `item_${(index + 1) % 3}`, titleClass(item.name)]"
|
||||
@click="$router.push({ path: '/AcgTopicList', query: { kind: item.key, type: mediaType, title: item.name } })"
|
||||
>
|
||||
<!-- 标题大于四个字为纵向、其余为横向 -->
|
||||
<img
|
||||
v-if="titleClass(item.name) === 'landscape'"
|
||||
class="title-icon"
|
||||
src="@/assets/images/png/acg/tag-title-1.png"
|
||||
/>
|
||||
<img v-else class="title-icon portrait" src="@/assets/images/png/acg/tag-title-2.png" />
|
||||
<span class="title">{{ item.name }}</span>
|
||||
<!-- <img class="cover" src="@/assets/images/png/acg/tag-icon-2.png" /> -->
|
||||
</div>
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {} from 'vue';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
list: any[];
|
||||
mediaType: string;
|
||||
}>(),
|
||||
{
|
||||
list: () => [],
|
||||
mediaType: () => '',
|
||||
},
|
||||
);
|
||||
|
||||
//landscape横向,portrait纵向
|
||||
const titleClass = (title: string) => {
|
||||
if (title.length < 4) {
|
||||
return 'landscape';
|
||||
} else {
|
||||
return 'portrait';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
.category {
|
||||
margin: 0 -16px;
|
||||
padding: 18px 0;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
align-items: center;
|
||||
|
||||
// .item {
|
||||
// flex-shrink: 0;
|
||||
// width: 78px;
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// margin-left: 25px;
|
||||
// &:first-child {
|
||||
// margin-left: 30px;
|
||||
// }
|
||||
|
||||
// .icon {
|
||||
// width: 100%;
|
||||
// height: 66px;
|
||||
// }
|
||||
// .title {
|
||||
// text-align: center;
|
||||
// width: 100%;
|
||||
// color: #ccc;
|
||||
// font-size: 14px;
|
||||
// font-style: normal;
|
||||
// font-weight: 400;
|
||||
// line-height: normal;
|
||||
|
||||
// @include textEllipsis();
|
||||
// }
|
||||
// }
|
||||
.item {
|
||||
width: 100%;
|
||||
height: 158px;
|
||||
position: relative;
|
||||
&.image {
|
||||
&.item_1 {
|
||||
background:
|
||||
// url(@/assets/images/png/acg/tag-title-1.png) 6px 70px / 78px 6px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) -64px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-1.png) -90px 100% / 205px 110px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-icon-image-1.png) 125px 100% / 150px 158px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-0.png) 85px 100% / 215px 100px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) 313px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-2.png) 275px 100% / 205px 110px no-repeat;
|
||||
}
|
||||
|
||||
&.item_2 {
|
||||
background:
|
||||
// url(@/assets/images/png/acg/tag-title-2.png) 16px 54px / 6px 78px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) -64px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-2.png) -90px 100% / 205px 110px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-icon-image-2.png) 125px 100% / 150px 158px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-0.png) 85px 100% / 215px 100px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) 313px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-3.png) 275px 100% / 205px 110px no-repeat;
|
||||
}
|
||||
|
||||
&.item_0 {
|
||||
background:
|
||||
// url(@/assets/images/png/acg/tag-title-1.png) 6px 70px / 78px 6px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) -64px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-3.png) -90px 100% / 205px 110px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-icon-image-3.png) 125px 100% / 150px 158px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-0.png) 85px 100% / 215px 100px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) 313px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-1.png) 275px 100% / 205px 110px no-repeat;
|
||||
}
|
||||
}
|
||||
&.video {
|
||||
&.item_1 {
|
||||
background:
|
||||
// url(@/assets/images/png/acg/tag-title-1.png) 6px 70px / 78px 6px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) -64px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-video-1.png) -90px 100% / 205px 110px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-icon-video-1.png) 125px 100% / 142px 158px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-0.png) 85px 100% / 215px 100px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) 313px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-video-2.png) 275px 100% / 205px 110px no-repeat;
|
||||
}
|
||||
|
||||
&.item_2 {
|
||||
background:
|
||||
// url(@/assets/images/png/acg/tag-title-2.png) 16px 54px / 6px 78px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) -64px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-video-2.png) -90px 100% / 205px 110px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-icon-video-3.png) 115px 100% / 150px 158px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-0.png) 85px 100% / 215px 100px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) 313px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-video-3.png) 275px 100% / 205px 110px no-repeat;
|
||||
}
|
||||
|
||||
&.item_0 {
|
||||
background:
|
||||
// url(@/assets/images/png/acg/tag-title-1.png) 6px 70px / 78px 6px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) -64px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-video-3.png) -90px 100% / 205px 110px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-icon-video-2.png) 145px 100% / 104px 158px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-0.png) 85px 100% / 215px 100px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-name.png) 313px 118px / 92px 36px no-repeat,
|
||||
url(@/assets/images/png/acg/tag-bg-video-1.png) 275px 100% / 205px 110px no-repeat;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
line-height: normal;
|
||||
word-wrap: break-word;
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
left: 5px;
|
||||
width: 76px;
|
||||
}
|
||||
|
||||
.cover {
|
||||
position: absolute;
|
||||
width: 150px;
|
||||
height: 158px;
|
||||
left: 125px;
|
||||
bottom: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
height: 6px;
|
||||
width: 78px;
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
left: 6px;
|
||||
&.portrait {
|
||||
height: 78px;
|
||||
width: 6px;
|
||||
position: absolute;
|
||||
top: 54px;
|
||||
left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&.portrait {
|
||||
.title {
|
||||
top: 50px;
|
||||
left: 30px;
|
||||
width: 39px;
|
||||
}
|
||||
}
|
||||
|
||||
// url(/src/assets/images/png/acg/bg-bobo-right.png) 100% 50px / 42px no-repeat,
|
||||
// url(/src/assets/images/png/acg/bg-bobo-right.png) 100% 250px / 182px no-repeat;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div class="item" @click="getPage">
|
||||
<slot name="item"></slot>
|
||||
<lazyImg class="cover" :src="props.info.cover">
|
||||
<div class="permission permission2">免费</div>
|
||||
<!-- <div v-else-if="props.info.permission === 1 && props.info.price" class="permission permission1">
|
||||
<SvgIcon name="diamond" />
|
||||
{{ props.info.price }}
|
||||
</div> -->
|
||||
</lazyImg>
|
||||
<div class="title-box">
|
||||
<div class="title">{{ props.info.title }}</div>
|
||||
<slot></slot>
|
||||
<UpdateStatus :info="props.info"></UpdateStatus>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { goDetailPage } from '../utils/common';
|
||||
import type { MediaTypeKey } from '../utils/common';
|
||||
import UpdateStatus from './updateStatus.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
info: Types.Acg.DiscountChildListItem;
|
||||
}>(),
|
||||
{
|
||||
info: () => {},
|
||||
},
|
||||
);
|
||||
const getPage = () => {
|
||||
goDetailPage(props.info.mediaType as MediaTypeKey, props.info.id, props.info.mediaSubType);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
@import '../assets/style.scss';
|
||||
.item {
|
||||
flex-shrink: 0;
|
||||
width: 111px;
|
||||
height: 188px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: calc(100% - 46px);
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.animationImg) {
|
||||
img {
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
.collect {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 29px;
|
||||
height: 29px;
|
||||
}
|
||||
.text-1-box {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: inline-flex;
|
||||
padding: 2px 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
|
||||
.textIcon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
@include tag();
|
||||
}
|
||||
.title-box {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
// height: 33px;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// .icon {
|
||||
// width: 18px;
|
||||
// height: 18px;
|
||||
// flex-shrink: 0;
|
||||
// margin-right: 2px;
|
||||
// margin-bottom: -2px;
|
||||
// &.video {
|
||||
// width: 12px;
|
||||
// height: 12px;
|
||||
// margin-bottom: 0px;
|
||||
// }
|
||||
// }
|
||||
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin: 6px 0 4px;
|
||||
@include textEllipsis();
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div
|
||||
class="episode-text"
|
||||
:class="{ active: props.episodeNumber == props.info.episodeNumber }"
|
||||
v-if="showType == 'text'"
|
||||
>
|
||||
第{{ props.info.episodeNumber }}话
|
||||
</div>
|
||||
<!-- 方格模式 -->
|
||||
<div
|
||||
v-else
|
||||
class="episode flex-center-center"
|
||||
:class="{ active: props.episodeNumber == props.info.episodeNumber, video: props.isVideo, tag: hasPaid || goldTag }"
|
||||
>
|
||||
{{ props.info.episodeNumber }}
|
||||
<iconByte v-if="props.episodeNumber == props.info.episodeNumber && props.isVideo" class="icon"></iconByte>
|
||||
<!-- <div v-if="props.info.listenPermission === 0" class="permission permission0">VIP</div> -->
|
||||
<!-- <div v-if="hasPaid" class="permission permission1">已解锁</div> -->
|
||||
<!-- <div v-else-if="goldTag" class="permission permission1">
|
||||
<SvgIcon name="gold" />
|
||||
{{ props.info.price }}
|
||||
</div> -->
|
||||
<!-- <div v-else-if="props.info.listenPermission === 2" class="permission permission2">免费</div> -->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, Ref } from 'vue';
|
||||
import iconByte from './icon-byte.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
isVideo?: boolean;
|
||||
episodeNumber: number;
|
||||
info: Types.Acg.MediaContentListItem;
|
||||
// parentInfo: Types.Media.MediaInfo;
|
||||
showType?: string; // 默认方格模式,text: 文字列表
|
||||
}>(),
|
||||
{
|
||||
isVideo: true,
|
||||
episodeNumber: 0,
|
||||
info: () => {
|
||||
return {} as Types.Acg.MediaContentListItem;
|
||||
},
|
||||
// parentInfo: () => {
|
||||
// return {} as Types.Media.MediaInfo;
|
||||
// },
|
||||
},
|
||||
);
|
||||
const parentInfo = inject('historyData', null) as Ref<Types.Media.MediaInfo> | null;
|
||||
const hasPaid = computed(() => {
|
||||
if (props.info.hasBuy) return true;
|
||||
if (parentInfo && parentInfo.value.mediaStatus && parentInfo.value.mediaStatus.hasPaid) return true;
|
||||
return false;
|
||||
});
|
||||
const goldTag = computed(() => {
|
||||
return props.info.listenPermission === 1 && props.info.price;
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.episode {
|
||||
height: 58px;
|
||||
width: 58px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border: 1px solid #4b4b4b;
|
||||
|
||||
&.active {
|
||||
border-color: $themeColor;
|
||||
color: $themeColor;
|
||||
background: rgba(255, 211, 0, 0.1);
|
||||
&.tag {
|
||||
align-items: flex-end !important;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-bottom: -2px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.permission {
|
||||
display: inline-flex;
|
||||
padding: 2px 4px;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
font-size: 9px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 14px; /* 160% */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
border-radius: 0px 0px 9px 0px;
|
||||
color: #000;
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
&.permission0 {
|
||||
background: linear-gradient(134deg, #f6cbb8 -1.45%, #eeb4aa 102.9%);
|
||||
}
|
||||
&.permission1 {
|
||||
background: #3e64ea;
|
||||
color: #fff;
|
||||
}
|
||||
&.permission2 {
|
||||
background: linear-gradient(135deg, #adfcac 0%, #5cef97 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
.episode-text {
|
||||
margin-top: 12px;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
}
|
||||
.active {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="item" :class="{ active: props.episodeNumber === props.info.episodeNumber }">
|
||||
<!-- <div v-if="props.info.listenPermission === 0" class="tag vip flex-vcenter">VIP</div> -->
|
||||
<div v-if="props.info.price && props.info.listenPermission === 1" class="tag price flex-vcenter">
|
||||
<SvgIcon name="diamond" />
|
||||
{{ props.info.price }}
|
||||
</div>
|
||||
<div v-else-if="props.info.listenPermission === 2" class="tag">免费</div>
|
||||
<lazyImg class="cover" :src="props.info.cover" :key="props.info.cover">
|
||||
<div v-if="progress" class="mask">
|
||||
<div class="progress-txt">{{ progress }}%</div>
|
||||
<div class="progress-box">
|
||||
<span :style="{ width: progress + '%' }"></span>
|
||||
</div>
|
||||
</div>
|
||||
</lazyImg>
|
||||
<div class="name ellipsis">{{ props.info.name }}</div>
|
||||
<span class="date">{{ dateFormatYyyyMmDd(props.info.createdAt) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { dateFormatYyyyMmDd } from '@/utils/index';
|
||||
const props = defineProps<{
|
||||
episodeNumber: number;
|
||||
info: Types.Acg.MediaContentListItem;
|
||||
progress?: 0;
|
||||
}>();
|
||||
const progress = computed(() => {
|
||||
return props.progress || 0;
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.item {
|
||||
flex-shrink: 0;
|
||||
width: 111px;
|
||||
height: 165px;
|
||||
overflow: hidden;
|
||||
margin: 0 5px 12px 0;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
position: relative;
|
||||
&.active {
|
||||
.cover {
|
||||
border: 1px solid #ff004f;
|
||||
}
|
||||
}
|
||||
&:nth-child(3n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.tag {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #adfcac 0%, #5cef97 100%);
|
||||
color: #212121;
|
||||
padding: 3px 8px;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
&.price {
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #b378ff 0%, #482cf5 100%);
|
||||
}
|
||||
&.vip {
|
||||
background: linear-gradient(135deg, #ffe580 0%, #facc15 100%);
|
||||
}
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: 111px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
.mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
.progress-txt {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 4px;
|
||||
right: 4px;
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
.progress-box {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
left: 4px;
|
||||
right: 4px;
|
||||
height: 3px;
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
span {
|
||||
display: block;
|
||||
height: 3px;
|
||||
background-color: $themeBgColor;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.name {
|
||||
color: #fff;
|
||||
margin-top: 8px;
|
||||
padding: 0 6px;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
}
|
||||
.date {
|
||||
padding: 0 6px;
|
||||
color: #c8c8c8;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<!-- @click.stop="
|
||||
props.customClick
|
||||
? void 0
|
||||
: $router.push({
|
||||
name: 'AcgImageDetailWatch',
|
||||
query: { id: props.mediaId, episodeNumber: props.episodeNumber },
|
||||
})
|
||||
" -->
|
||||
<div class="episode-box flex-vcenter" :class="{ progress: progress && props.progressUrl }" @click.stop="onClick">
|
||||
<lazyImg class="cover" :src="props.info.cover"></lazyImg>
|
||||
<div class="info">
|
||||
<!-- <div class="num">第{{ props.episodeNumber }}话</div> -->
|
||||
<span class="title ellipsis">{{ props.info.name }}</span>
|
||||
</div>
|
||||
|
||||
<SvgIcon name="backWhite"></SvgIcon>
|
||||
<lazyImg
|
||||
v-if="progress && props.progressUrl"
|
||||
class="episode-content"
|
||||
:src="props.progressUrl"
|
||||
:key="props.progressUrl"
|
||||
>
|
||||
<div class="mask flex-center-center">
|
||||
<div class="progress-container flex-vcenter">
|
||||
{{ progress }}%
|
||||
<div class="progress-box">
|
||||
<span :style="{ width: progress + '%' }"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</lazyImg>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
const props = defineProps<{
|
||||
mediaId: string;
|
||||
info:
|
||||
| Types.Acg.MediaContentListItem
|
||||
| {
|
||||
cover: string;
|
||||
id: string;
|
||||
name: string;
|
||||
urlSet: string[];
|
||||
};
|
||||
episodeNumber: number;
|
||||
mediaSubType: number;
|
||||
progress?: number;
|
||||
progressUrl?: string;
|
||||
customClick?: boolean;
|
||||
}>();
|
||||
|
||||
const progress = computed(() => {
|
||||
return props.progress || 0;
|
||||
});
|
||||
|
||||
const onClick = () => {
|
||||
if (props.customClick) return;
|
||||
const name = props.mediaSubType === 2 ? 'AcgImageDetailWatch2' : 'AcgImageDetailWatch';
|
||||
router.push({
|
||||
name,
|
||||
query: { id: props.mediaId, episodeNumber: props.episodeNumber },
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.episode-box {
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
margin: 18px auto;
|
||||
display: flex;
|
||||
width: 343px;
|
||||
// height: 64px;
|
||||
padding: 8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
color: #424242;
|
||||
&.progress {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.cover {
|
||||
flex-shrink: 0;
|
||||
width: 56px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.info {
|
||||
flex: 1;
|
||||
margin: 0 8px;
|
||||
color: #424242;
|
||||
.num,
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
}
|
||||
// .title {
|
||||
// font-size: 14px;
|
||||
// font-style: normal;
|
||||
// font-weight: 400;
|
||||
// line-height: 20px;
|
||||
// }
|
||||
}
|
||||
|
||||
svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.episode-content {
|
||||
margin: 12px -8px 0;
|
||||
width: calc(100% + 16px);
|
||||
height: 180px;
|
||||
border-radius: 0 0 8px 8px;
|
||||
overflow: hidden;
|
||||
|
||||
.mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
|
||||
.progress-container {
|
||||
padding: 8px 12px;
|
||||
border-radius: 25px;
|
||||
background: #fff;
|
||||
width: 311px;
|
||||
height: 33px;
|
||||
position: relative;
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
.progress-box {
|
||||
margin-left: 4px;
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
border-radius: 6px;
|
||||
background: rgb(51, 51, 51, 10%);
|
||||
span {
|
||||
display: block;
|
||||
height: 6px;
|
||||
background-color: #333;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div v-if="(props.list || []).length" class="category-content">
|
||||
<div v-if="$props.title" class="title" @click="goTopicPage(props.id, props.contentType, props.title)">
|
||||
<span>{{ props.title }}</span>
|
||||
<SvgIcon class="icon" name="moreIcon" />
|
||||
</div>
|
||||
<div v-if="props.contentWrap" ref="categoryContentList" class="list"></div>
|
||||
<div v-else ref="categoryContentList" class="list" @touchstart.stop @touchmove.stop @touchend.stop></div>
|
||||
<Teleport v-if="categoryContentList" :to="categoryContentList">
|
||||
<CategoryContentItem
|
||||
v-for="(item, index) in props.list"
|
||||
:key="index"
|
||||
:id="item.id"
|
||||
:url="item.horizontalCover || item.verticalCover"
|
||||
:title="item.title"
|
||||
:contentType="props.contentType"
|
||||
:info="item"
|
||||
></CategoryContentItem>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { goTopicPage } from '@/views/acgModule/utils/common';
|
||||
import CategoryContentItem from './categoryContentItem.vue';
|
||||
const categoryContentList = ref();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
contentWrap?: boolean; //换行显示,portrait三个一行,landscape两个一行
|
||||
contentType?: string; // 'video' | 'image';
|
||||
title?: string;
|
||||
list: Types.Media.AllMediaInfoItem[];
|
||||
id?: string;
|
||||
}>(),
|
||||
{
|
||||
contentWrap: false,
|
||||
contentType: 'video',
|
||||
title: '',
|
||||
list: () => [],
|
||||
id: '',
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '../assets/style.scss';
|
||||
.category-content {
|
||||
@include categoryWrap();
|
||||
.title {
|
||||
@include categoryTitle();
|
||||
}
|
||||
.list {
|
||||
@include categoryList();
|
||||
}
|
||||
|
||||
.list {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
.item {
|
||||
width: 168px;
|
||||
height: 110px;
|
||||
margin-right: 0;
|
||||
margin-bottom: 10px;
|
||||
&:first-child {
|
||||
width: 343px;
|
||||
height: 196px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="byte-wrap flex-center-center">
|
||||
<span v-for="i in 3" :key="i" class="item" :class="'dance_' + arr[i - 1]"></span>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
function shuffleArray(arr: number[]) {
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
const arr = shuffleArray([1, 2, 3]);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.byte-wrap {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
justify-content: space-evenly !important;
|
||||
.item {
|
||||
width: 2px;
|
||||
height: 11px;
|
||||
border-radius: 2px;
|
||||
background-color: #f68804;
|
||||
animation: dance 0.6s infinite linear alternate;
|
||||
@for $i from 1 through 3 {
|
||||
&.dance_#{$i} {
|
||||
animation-delay: #{0.2 + ($i - 1) * 0.2}s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dance {
|
||||
0% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<van-popup :show="isShow" z-index="2001" position="bottom" @click-overlay="emit('close')">
|
||||
<div class="wrapper flex-center-center">
|
||||
<div class="content" :class="[props.theme]" @click.stop="void 0">
|
||||
<div class="line_top" />
|
||||
<div class="title">购买作品</div>
|
||||
<div class="priceList">
|
||||
<div class="leader">作品解锁</div>
|
||||
<div class="price gold flex-vcenter">
|
||||
<SvgIcon name="gold" />
|
||||
{{ gold }}金币
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="priceList">
|
||||
<div class="leader">作品原价</div>
|
||||
<div class="price through flex-vcenter">{{ gold }}金币</div>
|
||||
</div> -->
|
||||
<!-- <div v-if="userInfo.isVip" class="priceList vip">
|
||||
<div class="leader">您当前享受{{ sale }}折扣优惠</div>
|
||||
<div class="price underline flex-vcenter" @click.stop="$router.push('member')">升级会员</div>
|
||||
</div>
|
||||
<div v-else class="priceList vip">
|
||||
<div class="leader">您当前不享受免费观看</div>
|
||||
<div class="price underline flex-vcenter" @click.stop="$router.push('member')">购买VIP享免费观看</div>
|
||||
</div> -->
|
||||
<div class="priceList split">
|
||||
<div class="leader">我的金币</div>
|
||||
<div class="price flex-vcenter">
|
||||
{{ recharge }}
|
||||
<!-- 100 -->
|
||||
<span class="go" @click.stop="$router.push('wallet'), emit('close')">充值</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btnBox flex-center-between">
|
||||
<div class="btn flex-center-center" @click="unlockNow('all')">{{ gold }}金币/立即支付</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</Teleport>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import useLoading from '@/hooks/loading';
|
||||
import VideoApi from '@/service/video';
|
||||
import { computed, inject, nextTick, Ref } from 'vue';
|
||||
import { ProductType } from '@/enums/app';
|
||||
import { showToast } from 'vant';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
videoInfo: any; //Types.Video.VInfosItem | Types.Acg.MediaContentListItem
|
||||
isOpen: boolean;
|
||||
theme?: string; //black 为黑色主题,默认为白色主题
|
||||
}>(),
|
||||
{
|
||||
theme: 'black',
|
||||
},
|
||||
);
|
||||
|
||||
const userStore = useUserStore();
|
||||
const userInfo = userStore.userInfo;
|
||||
|
||||
const historyData = inject('historyData') as Ref<Types.Media.MediaInfo>;
|
||||
// const salePrice = computed(() => {
|
||||
// let { contentsPrice = 0, price = 0, updateStatus = 0 } = historyData.value;
|
||||
// if (updateStatus === 1) return 0;
|
||||
// if (contentsPrice <= 0 || price <= 0) return 0;
|
||||
// const res = contentsPrice - price;
|
||||
// return res < 0 ? 0 : res;
|
||||
// });
|
||||
const gold = computed(() => {
|
||||
return historyData.value.price;
|
||||
});
|
||||
const sale = computed(() => {
|
||||
return 10 - Math.floor(((props.videoInfo.originCoins - props.videoInfo.coins) / props.videoInfo.originCoins) * 10);
|
||||
});
|
||||
|
||||
const recharge = computed(() => {
|
||||
return (userStore.walletInfo.amount * 100 + userStore.walletInfo.income * 100) / 100;
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const { openLoading, closeLoading } = useLoading();
|
||||
const emit = defineEmits(['close', 'buySuccess']);
|
||||
const isShow = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
const unlockNow = (type: 'all') => {
|
||||
try {
|
||||
if (recharge.value < gold.value) {
|
||||
emit('close');
|
||||
router.push('/wallet');
|
||||
return;
|
||||
}
|
||||
let productID = historyData.value.id;
|
||||
if (!productID) {
|
||||
showToast('购买出错~');
|
||||
return;
|
||||
}
|
||||
openLoading();
|
||||
|
||||
VideoApi.videoBuying({
|
||||
productType: ProductType.AcgAll,
|
||||
productID,
|
||||
// goldVideoCouponNum,
|
||||
})
|
||||
.then(async () => {
|
||||
showToast('购买成功');
|
||||
userStore.updateWallet();
|
||||
emit('buySuccess', { type, productID });
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e.toString().indexOf('8000') != -1) {
|
||||
emit('close');
|
||||
nextTick(() => {
|
||||
router.push({
|
||||
path: '/wallet',
|
||||
query: {
|
||||
activeTab: 1,
|
||||
id: props.videoInfo.id,
|
||||
title: props.videoInfo.title || '无标题',
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
closeLoading();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.content {
|
||||
color: #000;
|
||||
width: 100%;
|
||||
padding: 18px 10px 0;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
border-radius: 10px 10px 0 0;
|
||||
padding-bottom: 24px;
|
||||
.line_top {
|
||||
width: 36px;
|
||||
height: 5px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
margin: 0 auto;
|
||||
border-radius: 99px;
|
||||
}
|
||||
.title {
|
||||
padding: 18px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
.priceList {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 18px 0;
|
||||
&.vip {
|
||||
.leader,
|
||||
.price {
|
||||
color: #ac7e6e;
|
||||
}
|
||||
}
|
||||
&.split {
|
||||
border-top: $borderSplit;
|
||||
margin-top: 18px;
|
||||
padding-top: 18px;
|
||||
}
|
||||
.leader {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 14px;
|
||||
}
|
||||
.price {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
> svg {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.go {
|
||||
text-decoration-line: underline;
|
||||
color: #a4634d;
|
||||
margin-left: 12px;
|
||||
background: linear-gradient(286deg, #a4634d 0%, #ac7e6e 100%);
|
||||
background-clip: text;
|
||||
}
|
||||
}
|
||||
.gold {
|
||||
color: #ff9000;
|
||||
}
|
||||
.through {
|
||||
text-decoration: line-through;
|
||||
color: #747b80;
|
||||
}
|
||||
.underline {
|
||||
text-decoration: underline;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
&.black {
|
||||
background-color: #0f0f0f;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
.line_top {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.leader {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
.text {
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
margin: 30px 0 18px;
|
||||
span {
|
||||
font-size: 28px;
|
||||
color: $themeColor;
|
||||
}
|
||||
|
||||
&.line {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
.text-center {
|
||||
position: relative;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 30%;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
background: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
p {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.btnBox {
|
||||
margin-top: 26px;
|
||||
|
||||
.btn {
|
||||
height: 44px;
|
||||
width: 100%;
|
||||
background: #f68804;
|
||||
text-align: center;
|
||||
line-height: 44px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: $btnPrimaryColor;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<GoldPopup :is-open="visiblePermission1" :video-info="props.info" @close="emit('close')" @buy-success="buySuccess" />
|
||||
<VipPopup :is-open="visiblePermission0" @close="emit('close')" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import GoldPopup from './GoldPopup.vue';
|
||||
// import VipPopup from './VipPopup.vue';
|
||||
import { computed, ref, watch, onActivated, inject, Ref } from 'vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import UserApi from '@/service/user';
|
||||
// import { ProductType } from '@/enums/app';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
info: any; // Types.Media.MediaInfo | Types.Acg.MediaContentListItem;
|
||||
isOpen: boolean;
|
||||
}>(),
|
||||
{
|
||||
info: () => <any>{},
|
||||
isOpen: () => false,
|
||||
},
|
||||
);
|
||||
|
||||
const historyData = inject('historyData') as Ref<Types.Media.MediaInfo>;
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const emit = defineEmits(['close', 'buySuccess']);
|
||||
|
||||
const visiblePermission0 = ref(false);
|
||||
const visiblePermission1 = ref(false);
|
||||
// const visibleTodayFree = ref(false);
|
||||
|
||||
const isTypeVip = computed(() => {
|
||||
return userStore.isVip;
|
||||
});
|
||||
|
||||
// permission 收听权限 0:会员 1:金币购买 2:免费
|
||||
const permission = computed(() => {
|
||||
return historyData.value.permission;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.isOpen,
|
||||
(bool) => {
|
||||
if (!bool) {
|
||||
// visibleTodayFree.value = false;
|
||||
visiblePermission0.value = false;
|
||||
visiblePermission1.value = false;
|
||||
return;
|
||||
} else {
|
||||
if (permission.value === undefined) {
|
||||
emit('close');
|
||||
return;
|
||||
}
|
||||
if (permission.value === 1) {
|
||||
visiblePermission1.value = true;
|
||||
} else if (permission.value === 0) {
|
||||
visiblePermission0.value = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
onActivated(() => {
|
||||
// 会员类型要从接口取用户最新信息
|
||||
if (permission.value === 0 && !isTypeVip.value) {
|
||||
UserApi.queryUserInfo(null).then((res) => {
|
||||
userStore.saveUserInfo(res);
|
||||
if (res.isVip) {
|
||||
emit('close');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const buySuccess = (obj: any) => {
|
||||
emit('buySuccess', obj);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<van-overlay :show="isShow" z-index="2001" @click.stop="emit('close')">
|
||||
<div class="wrapper flex-center-center">
|
||||
<div class="content">
|
||||
<div class="title">温馨提示</div>
|
||||
<div class="horizontalLine"></div>
|
||||
<div class="text">本{{ typeName }}需要开通VIP会员</div>
|
||||
<p>开通会员 即可解锁查看完整版</p>
|
||||
<div class="btnBox flex-center-center" @click.stop="$router.push('member')">立即开通</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-overlay>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
const route = useRoute();
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
}>();
|
||||
const emit = defineEmits(['close']);
|
||||
const isShow = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
|
||||
const typeName = computed(() => {
|
||||
if ('AcgVideoDetail' === ((route.name as string) || '')) {
|
||||
return '视频';
|
||||
} else {
|
||||
return '章节';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.content {
|
||||
width: 311px;
|
||||
height: 242px;
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
background: $popupBg;
|
||||
position: relative;
|
||||
border-radius: 12px;
|
||||
.title {
|
||||
color: #fff;
|
||||
padding: 32px 0 12px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
.horizontalLine {
|
||||
height: 1px;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.text {
|
||||
text-align: center;
|
||||
margin: 18px 0 30px;
|
||||
}
|
||||
p {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.btnBox {
|
||||
margin: 26px auto 0;
|
||||
color: white;
|
||||
background: $themeColor;
|
||||
width: 263px;
|
||||
height: 44px;
|
||||
border-radius: 44px;
|
||||
text-align: center;
|
||||
line-height: 44px;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div v-if="configStore.appConfig.vipMark && props.info.permission === 0" class="permission permission_0">VIP</div>
|
||||
<div
|
||||
v-if="configStore.appConfig.coinMark && props.info.permission === 1 && props.info.price"
|
||||
class="permission permission_1"
|
||||
>
|
||||
金币
|
||||
<!-- {{ props.info.price }} -->
|
||||
</div>
|
||||
<!-- <div v-if="configStore.appConfig.freeMark && props.info.permission === 2" class="permission permission_2">免费</div> -->
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
contentType?: string;
|
||||
info: Types.Media.AllMediaInfoItem;
|
||||
}>(),
|
||||
{
|
||||
contentType: 'video',
|
||||
info: {} as any,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.text-1-box {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
|
||||
.textIcon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.permission {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 27px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
border-radius: 3px;
|
||||
font-family: 'PingFang SC';
|
||||
color: #292929;
|
||||
background: linear-gradient(135deg, #f8f3ae 0%, #ddac44 100%);
|
||||
|
||||
&.permission_1 {
|
||||
color: #8b3e00;
|
||||
background: linear-gradient(135deg, #ffe580 0%, #facc15 100%);
|
||||
}
|
||||
&.permission_2 {
|
||||
color: #212121;
|
||||
background: linear-gradient(135deg, #adfcac 0%, #5cef97 100%);
|
||||
}
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="popularity">
|
||||
<div v-if="props.id" class="title" @click="goTopicPage(props.id, props.contentType, props.title)">
|
||||
<slot name="title"></slot>
|
||||
<span v-if="props.title">{{ props.title }}</span>
|
||||
<span class="more flex-vcenter">
|
||||
{{ props.navTitle || '更多' }}
|
||||
<SvgIcon class="icon" name="moreIcon" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="list" @touchstart.stop @touchmove.stop @touchend.stop>
|
||||
<div class="item" v-for="(item, index) in props.list" :key="index" @click="getPage(item)">
|
||||
<lazyImg class="cover" :src="item.verticalCover">
|
||||
<img
|
||||
v-if="item.mediaType === 'text' && item.mediaSubType === 1"
|
||||
class="textIcon"
|
||||
src="@/assets/images/png/acg/type_text.png"
|
||||
/>
|
||||
<PermissionTag :info="item" :content-type="props.contentType" />
|
||||
</lazyImg>
|
||||
<div class="content">
|
||||
<div class="title ellipsis2">{{ item.title }}</div>
|
||||
<!-- <div class="desc">{{ item.summary }}</div> -->
|
||||
<!-- <div class="tag-box">
|
||||
<span v-for="tag in item.tagDetails || []" :key="tag" class="tag">{{ tag.name }}</span>
|
||||
</div> -->
|
||||
<UpdateStatus :info="item"></UpdateStatus>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { goTopicPage } from '@/views/acgModule/utils/common';
|
||||
import UpdateStatus from './updateStatus.vue';
|
||||
import { goDetailPage } from '../utils/common';
|
||||
import type { MediaTypeKey } from '../utils/common';
|
||||
import PermissionTag from './permission-tag.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id: string;
|
||||
title: string;
|
||||
list: any[];
|
||||
contentType?: string; // 'video' | 'image';
|
||||
navTitle?: string;
|
||||
}>(),
|
||||
{
|
||||
id: '',
|
||||
title: '',
|
||||
list: () => [[]],
|
||||
navTitle: '更多',
|
||||
contentType: 'video',
|
||||
},
|
||||
);
|
||||
|
||||
const getPage = (item: any) => {
|
||||
goDetailPage(item.mediaType as MediaTypeKey, item.id, item.mediaSubType);
|
||||
// //类型video:动漫,image:漫画 )
|
||||
// if (props.contentType === 'video') {
|
||||
// router.push({
|
||||
// path: '/animationMovieDetails',
|
||||
// query: {
|
||||
// vid: item.id,
|
||||
// },
|
||||
// });
|
||||
// } else {
|
||||
// router.push({
|
||||
// path: '/AcgCaricatureDetail',
|
||||
// query: {
|
||||
// id: item.id,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import '../assets/style.scss';
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
.popularity {
|
||||
@include categoryWrap();
|
||||
|
||||
> .title {
|
||||
@include categoryTitle();
|
||||
}
|
||||
|
||||
.list {
|
||||
@include categoryList();
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
height: 330px;
|
||||
|
||||
.item {
|
||||
width: 262px;
|
||||
height: 100px;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 30px;
|
||||
&:nth-child(3n) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
|
||||
.cover {
|
||||
width: 72px;
|
||||
height: 100px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 10px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
.textIcon {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
|
||||
.title {
|
||||
// @include textEllipsis();
|
||||
color: #ffffffe5;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
@include lineClamp(2);
|
||||
color: #666;
|
||||
margin: 12px 0 8px;
|
||||
}
|
||||
.tag-box {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.tag {
|
||||
color: #00929c;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
padding: 1px 4px;
|
||||
border-radius: 32px;
|
||||
border: 1px solid #00929c;
|
||||
margin: 0 4px 6px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<h2 class="h2Title" v-if="props.title">{{ props.title }}</h2>
|
||||
<Loading v-if="stateList.loading"></Loading>
|
||||
<template v-else>
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh" style="height: auto">
|
||||
<div v-if="stateList.list.length > 0">
|
||||
<CategoryContent
|
||||
class="content"
|
||||
:class="{ marginHide: props.marginHide }"
|
||||
:contentType="props.contentType"
|
||||
:contentWrap="true"
|
||||
:list="stateList.list"
|
||||
></CategoryContent>
|
||||
</div>
|
||||
<Empty v-else style="height: auto"></Empty>
|
||||
</PullRefresh>
|
||||
</template>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import Empty from './../../../components/Empty/index.vue';
|
||||
import { reactive } from 'vue';
|
||||
import CategoryContent from './categoryContent.vue';
|
||||
import AcgApi from '@/service/acg';
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default() {
|
||||
return '精彩推荐';
|
||||
},
|
||||
},
|
||||
marginHide: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
mediaId: {
|
||||
type: String,
|
||||
},
|
||||
contentType: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const stateList = reactive({
|
||||
loading: false,
|
||||
list: <any>[],
|
||||
});
|
||||
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: 9,
|
||||
};
|
||||
|
||||
const moreParams = reactive({
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
finished: false,
|
||||
isNoData: false,
|
||||
error: false,
|
||||
isHigehtMax: false,
|
||||
});
|
||||
|
||||
const getList = async () => {
|
||||
try {
|
||||
const TempfilterParams = Object.assign({}, params, { mediaId: props.mediaId });
|
||||
let res: any = await AcgApi.getMediaRecommend(TempfilterParams);
|
||||
stateList.loading = false;
|
||||
moreParams.loading = false;
|
||||
moreParams.refreshing = false;
|
||||
|
||||
let list = res.list || [];
|
||||
|
||||
if (params.pageNumber === 1) {
|
||||
stateList.list = list;
|
||||
} else {
|
||||
stateList.list = stateList.list.concat(list);
|
||||
}
|
||||
moreParams.isNoData = params.pageNumber == 1 && stateList.list.length == 0;
|
||||
moreParams.finished = !res.hasNext;
|
||||
} catch (e) {
|
||||
moreParams.loading = false;
|
||||
moreParams.error = true;
|
||||
}
|
||||
};
|
||||
|
||||
const onLoad = () => {
|
||||
moreParams.loading = true;
|
||||
params.pageNumber++;
|
||||
getList();
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
params.pageNumber = 1;
|
||||
moreParams.refreshing = true;
|
||||
moreParams.finished = false;
|
||||
getList();
|
||||
};
|
||||
|
||||
getList();
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.h2Title {
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
margin: 18px 0 0;
|
||||
}
|
||||
.content {
|
||||
margin: 16px;
|
||||
&.marginHide {
|
||||
margin: 16px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="header flex-center-between">
|
||||
<div
|
||||
class="searchBtn flex-vcenter"
|
||||
@click="$router.push({ path: '/AcgSearch', query: { mediaType: props.mediaType } })"
|
||||
>
|
||||
<div class="search"></div>
|
||||
<span>请输入搜索文字</span>
|
||||
</div>
|
||||
<div class="theme_btn_bg filter" style="margin-right: 12px" @click="$router.push('myBookshelf')">
|
||||
<SvgIcon name="bookshelf" class="bookshelf" style="color: #fff"></SvgIcon>
|
||||
</div>
|
||||
<div
|
||||
class="theme_btn_bg filter"
|
||||
@click="$router.push({ path: '/AcgSearchFilter', query: { mediaType: props.mediaType } })"
|
||||
>
|
||||
<SvgIcon name="acg_filter" style="color: #fff"></SvgIcon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
mediaType: string;
|
||||
}>();
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
padding: 7px 16px;
|
||||
background-color: transparent;
|
||||
height: 44px;
|
||||
height: 44px;
|
||||
|
||||
.searchBtn {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
flex: 1;
|
||||
height: 30px;
|
||||
border-radius: 30px;
|
||||
font-size: 14px;
|
||||
padding: 0 6px 0 12px;
|
||||
color: #999;
|
||||
margin-right: 12px;
|
||||
.search {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
background: url('@/assets/images/png/home/search.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div ref="rootRef" class="summary-box">
|
||||
<!-- <div v-if="props.title" class="title">{{ props.title }}</div> -->
|
||||
<TextOverflow :text="props.text" :maxLines="3">
|
||||
<template #default="{ clickToggle, expanded }">
|
||||
<Teleport :to="rootRef">
|
||||
<div @click.stop="clickToggle" class="expanded flex-center-center">
|
||||
{{ expanded ? '收起更多' : '更多简介' }}
|
||||
<SvgIcon name="back" :class="['icon', expanded ? 'down' : 'up']" />
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
</TextOverflow>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default() {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const rootRef = ref();
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.summary-box {
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
position: relative;
|
||||
// background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
color: #989898;
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
.expanded {
|
||||
position: relative;
|
||||
left: 0;
|
||||
margin-top: 12px;
|
||||
// margin-left: -9px;
|
||||
svg {
|
||||
transform: rotate(-90deg);
|
||||
font-size: 18px;
|
||||
|
||||
&.down {
|
||||
transform: rotate(-270deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="advBox" v-if="advList.length">
|
||||
<AdvSwiper class="advSwiper" :list="advList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { AdType } from '@/enums/app';
|
||||
import { getAdItem } from '@/utils/getConfig';
|
||||
const advList = ref(getAdItem(AdType.ACGADV));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.advSwiper {
|
||||
height: 143px;
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
transform: translateZ(0px);
|
||||
position: relative;
|
||||
:deep(.van-swipe-item) {
|
||||
height: 143px !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
:deep(.van-swipe__indicators) {
|
||||
left: auto;
|
||||
right: 12px;
|
||||
transform: none;
|
||||
.van-swipe__indicator {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 6px;
|
||||
background: rgb(245, 44, 86, 60%);
|
||||
border: none;
|
||||
&.van-swipe__indicator--active {
|
||||
width: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div v-if="props.list" class="fiveGrid">
|
||||
<div class="rectAdvBox" @click="jumpAdv(item)" v-for="item in props.list" :key="item.id">
|
||||
<lazyImg class="rectAdvIcon" :src="item.cover" />
|
||||
<div class="rectName ellipsis">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
list: Types.App.AdsInfoListItem[];
|
||||
}>(),
|
||||
{
|
||||
list: () => [],
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.fiveGrid {
|
||||
padding: 18px 0 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
|
||||
grid-row-gap: 12px;
|
||||
grid-column-gap: 10px;
|
||||
.rectAdvBox {
|
||||
width: 60px;
|
||||
.rectAdvIcon {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.rectName {
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="navMap.length"
|
||||
class="nav-section flex-vcenter overflow-x-auto"
|
||||
@touchstart.stop
|
||||
@touchmove.stop
|
||||
@touchend.stop
|
||||
>
|
||||
<div class="item flex-center-center" v-for="item in navMap" :key="item.name" @click="goPage(item)">
|
||||
<lazyImg class="icon" :src="item.img" :key="item.img"></lazyImg>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { innerJump, compatibleAppStoreJump } from '@/utils/index';
|
||||
|
||||
const useConfig = useConfigStore();
|
||||
const navMap = (useConfig.appConfig?.jgArea || []).filter((item) => item.type === 1);
|
||||
const goPage = (item: Types.App.JingGangItem) => {
|
||||
const { link_type, link_url } = item;
|
||||
const index = link_url.indexOf('yinseinner://');
|
||||
if (link_type === 2 || index !== -1) {
|
||||
innerJump(link_url.substring(13));
|
||||
} else {
|
||||
compatibleAppStoreJump(link_url);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.nav-section {
|
||||
width: 100%;
|
||||
.item {
|
||||
flex-shrink: 0;
|
||||
border-radius: 100px;
|
||||
padding: 12px 0;
|
||||
margin: 0 20px;
|
||||
|
||||
color: $titleColor;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
flex-direction: column;
|
||||
.icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div v-if="props.data.tab.length > 0" class="rank-wrap">
|
||||
<div class="bg"></div>
|
||||
<img class="title" src="@/assets/images/png/acg/rank-title.png" />
|
||||
<van-tabs v-model:active="tabActive" :animated="true" @rendered="onTabRendered" class="tabs" swipeable>
|
||||
<van-tab v-for="(item, index) in props.data.tab" :key="item.type" :title="item.name" :name="item.type">
|
||||
<List
|
||||
v-if="tabIsInit(item.type)"
|
||||
:type="item.type"
|
||||
:mediaType="mediaType"
|
||||
:defaultData="index === 0 ? props.data.list || [] : []"
|
||||
></List>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { inject } from 'vue';
|
||||
import tabAction from '@/utils/tabAction';
|
||||
// import { rankTabList } from '../../utils/common';
|
||||
import type { MediaTypeKey } from '../../utils/common';
|
||||
import List from './rankList.vue';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
data: Types.Acg.DefaultAcgRanking;
|
||||
}>(),
|
||||
{
|
||||
data: () => {
|
||||
return {
|
||||
tab: [],
|
||||
list: [],
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const mediaType = inject('mediaType') as MediaTypeKey;
|
||||
|
||||
const { tabActive, tabIsInit, onTabRendered } = tabAction(false);
|
||||
if (props.data.tab.length > 0) {
|
||||
tabActive.value = props.data.tab[0].type;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '../../assets/style.scss';
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
.rank-wrap {
|
||||
width: 375px;
|
||||
height: 600px;
|
||||
margin-left: -16px;
|
||||
position: relative;
|
||||
// @include vanTab-btn();
|
||||
@include popupTab();
|
||||
.bg {
|
||||
background: url(@/assets/images/png/acg/rank-bg.png) 0 0 / contain no-repeat;
|
||||
// filter: blur(6px);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
margin: 18px 0 12px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
--van-padding-xs: 16px;
|
||||
--van-tab-font-size: 12px;
|
||||
|
||||
.tabs {
|
||||
z-index: 2;
|
||||
--van-tabs-line-height: 28px;
|
||||
:deep(.van-tabs__wrap) {
|
||||
.van-tab--active {
|
||||
border-radius: 100px;
|
||||
background-color: $themeBgLgColor;
|
||||
font-weight: normal;
|
||||
}
|
||||
.van-tabs__line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
:deep(.van-tabs__content) {
|
||||
.van-tab__panel {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div class="list-wrap">
|
||||
<Loading v-if="state.loading"></Loading>
|
||||
<template v-else>
|
||||
<div class="container" v-if="state.list.length > 0">
|
||||
<CategoryContentItem
|
||||
v-for="(item, index) in state.list"
|
||||
:key="index"
|
||||
:id="item.id"
|
||||
:url="item.verticalCover || item.horizontalCover"
|
||||
:title="item.title"
|
||||
:contentType="item.mediaType"
|
||||
:info="item"
|
||||
class="item"
|
||||
>
|
||||
<template v-slot:item>
|
||||
<img v-if="index === 0" class="icon" src="@/assets/images/png/acg/rank-top-1.png" />
|
||||
<img v-else-if="index === 1" class="icon" src="@/assets/images/png/acg/rank-top-2.png" />
|
||||
<img v-else-if="index === 2" class="icon" src="@/assets/images/png/acg/rank-top-3.png" />
|
||||
<div v-else class="icon-txt">{{ index + 1 }}</div>
|
||||
</template>
|
||||
</CategoryContentItem>
|
||||
</div>
|
||||
<Empty v-else class="empty" />
|
||||
<div class="btn" @click="goMore">
|
||||
查看更多
|
||||
<SvgIcon class="icon" name="moreIcon" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRaw, reactive, inject } from 'vue';
|
||||
import CategoryContentItem from '../../components/categoryContentItem.vue';
|
||||
import AcgApi from '@/service/acg';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type: number;
|
||||
mediaType: string;
|
||||
defaultData: Types.Media.ModuleItem[];
|
||||
}>(),
|
||||
{
|
||||
type: () => 1,
|
||||
mediaType: 'video',
|
||||
defaultData: () => [],
|
||||
},
|
||||
);
|
||||
|
||||
const state = reactive({
|
||||
loading: true,
|
||||
list: <any>[],
|
||||
});
|
||||
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: 6,
|
||||
};
|
||||
|
||||
const getList = async () => {
|
||||
try {
|
||||
if (props.defaultData.length > 0) {
|
||||
state.list = props.defaultData;
|
||||
state.loading = false;
|
||||
return;
|
||||
}
|
||||
const TempfilterParams = Object.assign({}, params, toRaw(props));
|
||||
let res: any = await AcgApi.getRanking(TempfilterParams);
|
||||
state.list = res.list || [];
|
||||
state.loading = false;
|
||||
} catch (e) {
|
||||
state.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
getList();
|
||||
|
||||
const goRankTab = inject('acg_go_rank_tab') as any;
|
||||
const goMore = () => {
|
||||
goRankTab && goRankTab();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list-wrap {
|
||||
.content {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.btn {
|
||||
width: 343px;
|
||||
height: 44px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
border-radius: 100px;
|
||||
background: $themeBgLgColor;
|
||||
color: #fff;
|
||||
.icon {
|
||||
color: #fff;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
.empty {
|
||||
margin-top: 18px;
|
||||
padding-bottom: 18px;
|
||||
min-height: 428px;
|
||||
}
|
||||
.container {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 18px;
|
||||
padding-bottom: 18px;
|
||||
min-height: 428px;
|
||||
.item {
|
||||
width: 111px;
|
||||
height: 199px;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 12px;
|
||||
position: relative;
|
||||
|
||||
// :deep(.text-1-box) {
|
||||
// display: none;
|
||||
// }
|
||||
|
||||
&:nth-child(3n) {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.icon,
|
||||
.icon-txt {
|
||||
&.icon {
|
||||
object-fit: contain;
|
||||
}
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
&.icon-txt {
|
||||
background: #000;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
line-height: 24px; /* 128.571% */
|
||||
text-indent: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,268 @@
|
||||
<!-- 全量视频(海角样式) -->
|
||||
<template>
|
||||
<div class="acg-tab-item overflow-y-auto" :id="'home' + tabIndex" ref="rootEl">
|
||||
<div class="acg-theme">
|
||||
<HomeHeader
|
||||
:advType="AdType.COMMUNITYCAROUSEL"
|
||||
class="advBox"
|
||||
v-if="!props.pureVersion"
|
||||
/>
|
||||
<!-- 标签区 -->
|
||||
<div class="labelBox" v-if="state.allSection && state.allSection.length">
|
||||
<div
|
||||
class="label ellipsis"
|
||||
v-for="item in state.allSection"
|
||||
:key="item.sectionID"
|
||||
@click="toTagShortVideo(item)"
|
||||
>
|
||||
{{ item.sectionName }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-sort flex-vcenter" ref="dvSort">
|
||||
<div
|
||||
class="item"
|
||||
:class="{ active: item.key === moduleSort }"
|
||||
v-for="item in moduleSortMap"
|
||||
:key="item.key"
|
||||
@click="onChange(item.key)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
|
||||
<div class="list">
|
||||
<CategoryContentItem
|
||||
v-for="(item, index) in state.list"
|
||||
:key="index"
|
||||
class="item"
|
||||
:id="item.id"
|
||||
:url="item.verticalCover"
|
||||
:title="item.title"
|
||||
:contentType="item.mediaType"
|
||||
:info="item"
|
||||
></CategoryContentItem>
|
||||
</div>
|
||||
</PullRefresh>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, inject } from 'vue';
|
||||
import CategoryContentItem from '../categoryContentItem.vue';
|
||||
import HomeApi from '@/service/home';
|
||||
import { AdType } from '@/enums/app';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { getMediaTypeKey } from '../../utils/common';
|
||||
|
||||
const router = useRouter();
|
||||
const dvSort = ref();
|
||||
|
||||
const props = defineProps<{
|
||||
moduleId: string;
|
||||
moduleName: string;
|
||||
tabIndex?: number;
|
||||
type: number;
|
||||
advType?: number;
|
||||
pureVersion?: boolean;
|
||||
}>();
|
||||
|
||||
const rootEl = ref();
|
||||
|
||||
const moduleSortMap = [
|
||||
{
|
||||
name: '最多观看',
|
||||
key: 3,
|
||||
},
|
||||
{
|
||||
name: '最新发布',
|
||||
key: 1,
|
||||
},
|
||||
{
|
||||
name: '最多收藏',
|
||||
key: 7,
|
||||
},
|
||||
|
||||
// {
|
||||
// name: '本周最热',
|
||||
// key: 2,
|
||||
// },
|
||||
// {
|
||||
// name: '最多点赞',
|
||||
// key: 3,
|
||||
// },
|
||||
];
|
||||
|
||||
const moduleSort = ref(moduleSortMap[0].key);
|
||||
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: 12,
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
list: <Types.Media.AllMediaInfoItem[]>[],
|
||||
allSection: <Types.Media.allSection[]>[],
|
||||
});
|
||||
|
||||
const moreParams = reactive({
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
finished: false,
|
||||
isNoData: false,
|
||||
error: false,
|
||||
isHigehtMax: false,
|
||||
});
|
||||
|
||||
let sortTop = ref(0);
|
||||
const getList = async (type = '') => {
|
||||
let req = {
|
||||
id: props.moduleId,
|
||||
data: Object.assign({}, params, { moduleSort: moduleSort.value }),
|
||||
};
|
||||
try {
|
||||
let res: any = await HomeApi.getModuleList(req);
|
||||
moreParams.loading = false;
|
||||
moreParams.refreshing = false;
|
||||
let list = res.allMediaInfo || [];
|
||||
|
||||
const allSection = res.allSection || [];
|
||||
if (allSection.length && params.pageNumber === 1) {
|
||||
state.allSection = allSection;
|
||||
}
|
||||
|
||||
if (params.pageNumber === 1) {
|
||||
moreParams.isNoData = list.length ? false : true;
|
||||
}
|
||||
if (type === 'refresh') {
|
||||
state.list = list;
|
||||
} else {
|
||||
state.list = [...state.list, ...list];
|
||||
}
|
||||
if (!res.hasNext) {
|
||||
moreParams.finished = true;
|
||||
}
|
||||
if (sortTop.value === 0) {
|
||||
sortTop.value = dvSort.value.offsetTop;
|
||||
}
|
||||
} catch (e) {
|
||||
moreParams.loading = false;
|
||||
moreParams.error = true;
|
||||
}
|
||||
};
|
||||
const onLoad = () => {
|
||||
moreParams.loading = true;
|
||||
params.pageNumber++;
|
||||
getList();
|
||||
};
|
||||
const onRefresh = () => {
|
||||
params.pageNumber = 1;
|
||||
moreParams.refreshing = true;
|
||||
moreParams.finished = false;
|
||||
getList('refresh');
|
||||
};
|
||||
// 初始化
|
||||
const init = () => {
|
||||
// 获取轮播图数据
|
||||
getList();
|
||||
};
|
||||
init();
|
||||
|
||||
const onChange = (key: number) => {
|
||||
moduleSort.value = key;
|
||||
if (sortTop.value < rootEl.value.scrollTop) {
|
||||
rootEl.value.scrollTop = sortTop.value;
|
||||
}
|
||||
onRefresh();
|
||||
};
|
||||
|
||||
const type = getMediaTypeKey(props.type);
|
||||
const toTagShortVideo = (val: any) => {
|
||||
router.push({
|
||||
path: '/AcgTopicList',
|
||||
query: {
|
||||
id: val.sectionID,
|
||||
title: val.sectionName,
|
||||
type,
|
||||
showType: 0,
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '../../assets/style.scss';
|
||||
.acg-tab-item {
|
||||
height: calc(100vh - 44px * 2);
|
||||
width: 100vw;
|
||||
.tab-sort {
|
||||
background: #0f0f0f;
|
||||
padding: 0 26px;
|
||||
height: 44px;
|
||||
position: sticky;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
// background-color: var(--theme-color);
|
||||
.item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
&:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
right: 0px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
&.active {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 16px;
|
||||
.item {
|
||||
width: 111px;
|
||||
height: 190px;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:nth-child(3n) {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.labelBox {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
grid-row-gap: 7px;
|
||||
grid-column-gap: 9px;
|
||||
padding: 0 16px;
|
||||
margin-top: 18px;
|
||||
.label {
|
||||
width: auto;
|
||||
height: 32px;
|
||||
padding: 0 4px;
|
||||
border-radius: 2px;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.advBox {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,249 @@
|
||||
<template>
|
||||
<div class="acg-tab-item overflow-y-auto" :id="`acg_${props.moduleId}`">
|
||||
<!-- <Search class="search-box" :mediaType="mediaType"></Search> -->
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
|
||||
<div class="wrap">
|
||||
<!-- 广告轮播 -->
|
||||
<HomeHeader
|
||||
:advType="AdType.COMMUNITYCAROUSEL"
|
||||
class="advBox"
|
||||
v-if="!props.pureVersion"
|
||||
/>
|
||||
<!-- <CategoryTag v-if="['video', 'image'].includes(mediaType)" :list="tagList" :mediaType="mediaType"></CategoryTag> -->
|
||||
<template v-for="(item, index) in state.list" :key="item.sectionID">
|
||||
<!--
|
||||
101 一大四小 1
|
||||
102 四宫格(横acg+影视共用) 1
|
||||
205 2.5左右滑动(竖ACG)
|
||||
201 四宫格(竖ACG) 1
|
||||
202 六宫格(竖ACG) 1
|
||||
203 九宫格(竖ACG) 1
|
||||
204 小列表 (竖ACG) 1
|
||||
-->
|
||||
<!-- 一大四小 -->
|
||||
<FiveContent
|
||||
v-if="item.showType === 101"
|
||||
:coverDirection="getCoverDirection(item.showType)"
|
||||
:id="item.sectionID"
|
||||
:title="item.sectionName"
|
||||
:list="item.allMediaInfo || []"
|
||||
:contentType="mediaType"
|
||||
></FiveContent>
|
||||
<CategoryContent
|
||||
v-else-if="[205].includes(item.showType)"
|
||||
:coverDirection="getCoverDirection(item.showType)"
|
||||
:id="item.sectionID"
|
||||
:title="item.sectionName"
|
||||
:list="item.allMediaInfo || []"
|
||||
:contentType="mediaType"
|
||||
:showType="item.showType"
|
||||
></CategoryContent>
|
||||
<!-- <Topic
|
||||
v-else-if="item.showType === 5"
|
||||
:id="item.sectionID"
|
||||
:title="item.sectionName"
|
||||
:list="item.allMediaInfo || []"
|
||||
:disVoteCount="item.disVoteCount"
|
||||
:voteCount="item.voteCount"
|
||||
:contentType="mediaType"
|
||||
@disVoteCount="item.disVoteCount++"
|
||||
@voteCount="item.voteCount++"
|
||||
></Topic> -->
|
||||
<CategoryContent
|
||||
v-else-if="[201].includes(item.showType)"
|
||||
:contentWrap="true"
|
||||
:id="item.sectionID"
|
||||
:title="item.sectionName"
|
||||
:list="item.allMediaInfo || []"
|
||||
:contentType="mediaType"
|
||||
:showType="item.showType"
|
||||
></CategoryContent>
|
||||
<CategoryContent
|
||||
v-else-if="[202, 203].includes(item.showType)"
|
||||
:contentWrap="true"
|
||||
:id="item.sectionID"
|
||||
:title="item.sectionName"
|
||||
:list="item.allMediaInfo || []"
|
||||
:contentType="mediaType"
|
||||
:showType="item.showType"
|
||||
></CategoryContent>
|
||||
<Popularity
|
||||
v-else-if="item.showType === 204"
|
||||
:id="item.sectionID"
|
||||
:title="item.sectionName"
|
||||
:list="item.allMediaInfo || []"
|
||||
:contentType="mediaType"
|
||||
></Popularity>
|
||||
<!-- 默认按照102处理 -->
|
||||
<CategoryContent
|
||||
v-else
|
||||
coverDirection="landscape"
|
||||
:contentWrap="true"
|
||||
:id="item.sectionID"
|
||||
:title="item.sectionName"
|
||||
:list="item.allMediaInfo || []"
|
||||
:contentType="mediaType"
|
||||
></CategoryContent>
|
||||
<template v-if="(index + 1) % 2 === 0 && !props.pureVersion">
|
||||
<HeaderAdv
|
||||
class="advBox"
|
||||
:advType="AdType.TopicGridAdv"
|
||||
random
|
||||
:length="0"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</PullRefresh>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, computed, ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import Search from '../search.vue';
|
||||
import CategoryTag from '../categoryTag.vue';
|
||||
import CategoryContent from '../categoryContent.vue';
|
||||
import FiveContent from '../fiveContent.vue';
|
||||
import Topic from '../topic.vue';
|
||||
import Popularity from '../popularity.vue';
|
||||
import HomeApi from '@/service/home';
|
||||
import { useAcgStore } from '@/stores/acg';
|
||||
import { AdType } from '@/enums/app';
|
||||
import { getAdItem } from '@/utils/getConfig';
|
||||
|
||||
const acgStore = useAcgStore();
|
||||
const props = defineProps<{
|
||||
moduleId: string;
|
||||
moduleName: string;
|
||||
showType: number;
|
||||
advType?: number;
|
||||
pureVersion?: boolean;
|
||||
}>();
|
||||
|
||||
const mediaType = computed(() => {
|
||||
if (props.moduleName.includes('小说')) {
|
||||
return 'text';
|
||||
}
|
||||
switch (props.moduleName) {
|
||||
case '动漫':
|
||||
return 'video';
|
||||
case '漫画':
|
||||
return 'image';
|
||||
// case '小说':
|
||||
// return 'text';
|
||||
case '剧场':
|
||||
return 'theatre';
|
||||
default:
|
||||
return 'video';
|
||||
}
|
||||
});
|
||||
|
||||
const getCoverDirection = (showType: number) => {
|
||||
return showType === 2 ? 'portrait' : 'landscape';
|
||||
};
|
||||
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
moduleSort: 1,
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
list: <Types.Media.ModuleItem[]>[],
|
||||
advList: <Array<Types.App.AdsInfoListItem>>[], // 广告轮播
|
||||
});
|
||||
|
||||
const { library } = storeToRefs(acgStore);
|
||||
const tagList = computed(() => {
|
||||
return library.value.find((item) => item.key === mediaType.value)?.kind || [];
|
||||
});
|
||||
|
||||
const moreParams = reactive({
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
finished: false,
|
||||
isNoData: false,
|
||||
error: false,
|
||||
isHigehtMax: false,
|
||||
});
|
||||
|
||||
const getList = async () => {
|
||||
let req = {
|
||||
id: props.moduleId,
|
||||
data: params,
|
||||
};
|
||||
try {
|
||||
let res: any = await HomeApi.getModuleList(req);
|
||||
moreParams.loading = false;
|
||||
moreParams.refreshing = false;
|
||||
let list = res.allSection || [];
|
||||
|
||||
const isRefresh = params.pageNumber === 1;
|
||||
if (isRefresh) {
|
||||
state.list = list;
|
||||
} else {
|
||||
state.list = state.list.concat(list);
|
||||
}
|
||||
if (isRefresh && state.list.length == 0) {
|
||||
moreParams.isNoData = true;
|
||||
}
|
||||
if (!res.hasNext) {
|
||||
moreParams.finished = true;
|
||||
}
|
||||
} catch (e) {
|
||||
moreParams.loading = false;
|
||||
moreParams.error = true;
|
||||
}
|
||||
};
|
||||
const onLoad = () => {
|
||||
moreParams.loading = true;
|
||||
params.pageNumber++;
|
||||
getList();
|
||||
};
|
||||
const onRefresh = () => {
|
||||
params.pageNumber = 1;
|
||||
moreParams.refreshing = true;
|
||||
moreParams.finished = false;
|
||||
getList();
|
||||
};
|
||||
|
||||
const advList = ref([] as Types.App.AdsInfoListItem[]);
|
||||
// 初始化
|
||||
const init = () => {
|
||||
advList.value = getAdItem(AdType.TopicGridAdv) || [];
|
||||
console.log(advList.value);
|
||||
getList();
|
||||
};
|
||||
init();
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '../../assets/style.scss';
|
||||
.acg-tab-item {
|
||||
height: calc(100vh - 44px * 2);
|
||||
overflow: visible;
|
||||
.advSwiper {
|
||||
height: 143px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transform: translateZ(0px);
|
||||
position: relative;
|
||||
:deep(.van-swipe-item) {
|
||||
height: 143px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.search-box {
|
||||
background-color: #fff;
|
||||
position: sticky;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 22;
|
||||
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.wrap {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<!--
|
||||
模块showType枚举
|
||||
2: 海角
|
||||
3: 专题
|
||||
-->
|
||||
<van-tabs
|
||||
v-model:active="tabActive"
|
||||
:animated="true"
|
||||
:scrollspy="false"
|
||||
shrink
|
||||
:lazy-render="true"
|
||||
swipeable
|
||||
@rendered="onTabRendered"
|
||||
class="tabs oepnX"
|
||||
>
|
||||
<van-tab v-for="item in secondTab" :key="item.id" :title="item.moduleName" :name="item.id">
|
||||
<template v-if="tabIsInit(item.id)">
|
||||
<component
|
||||
class="contain overflow-y-auto"
|
||||
:is="item.showType == 2 ? TabItem : TabHome"
|
||||
:id="prefixKey + item.id"
|
||||
:moduleId="item.id"
|
||||
:moduleName="item.moduleName"
|
||||
:showType="item.showType"
|
||||
:advType="props.advType"
|
||||
:type="props.type"
|
||||
:pureVersion="item.pureVersion"
|
||||
/>
|
||||
</template>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, provide, unref, onActivated } from 'vue';
|
||||
import { useGlobalStore } from '@/stores/global';
|
||||
// import AcgApi from '@/service/acg';
|
||||
import { getNewDate } from '../../utils/common';
|
||||
import type { MediaTypeKey } from '../../utils/common';
|
||||
import tabAction from '@/utils/tabAction';
|
||||
import TabItem from './tabItem.vue';
|
||||
import TabHome from './tabHome.vue';
|
||||
import { showToast } from 'vant';
|
||||
import { AdType } from '@/enums/app';
|
||||
// const componentMap: any = {
|
||||
// 2: TabHome,
|
||||
// 3: TabItem,
|
||||
// };
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type: number;
|
||||
mediaType: MediaTypeKey;
|
||||
moduleName: string;
|
||||
advType?: number;
|
||||
}>(),
|
||||
{
|
||||
advType: AdType.HOME,
|
||||
},
|
||||
);
|
||||
provide('mediaType', unref(props.mediaType));
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const currentTime = ref(getNewDate().getTime());
|
||||
|
||||
onActivated(() => {
|
||||
currentTime.value = getNewDate().getTime();
|
||||
});
|
||||
|
||||
const secondTab = computed(() => {
|
||||
// return (globalStore.tabObj[props.type === 6 ? 'novel' : 'cartoon'] || []).filter((item: any) => {
|
||||
// return item.type === props.type;
|
||||
// });
|
||||
return globalStore.tabObj[props.type === 6 ? 'novel' : 'cartoon'] || [];
|
||||
});
|
||||
const secondTabNavSectionIndex = computed(() => {
|
||||
return secondTab.value.findIndex((item: any) => [1, 2].includes(item.showType));
|
||||
});
|
||||
|
||||
const prefixKey = 'acg_second_page_';
|
||||
const { tabActive, tabIsInit, onTabRendered } = tabAction(true, prefixKey);
|
||||
|
||||
provide('acg_go_rank_tab', () => {
|
||||
const item = secondTab.value.find((item: any) => item.showType === 5);
|
||||
if (item) {
|
||||
tabActive.value = item.id;
|
||||
} else {
|
||||
showToast('暂无更多');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '../../assets/style.scss';
|
||||
.tabs {
|
||||
:deep(> .van-tabs__wrap) {
|
||||
--van-tabs-nav-background: transparent;
|
||||
--van-tab-active-text-color: #fff;
|
||||
--van-tab-text-color: #9fa19f;
|
||||
--van-padding-xs: 6px;
|
||||
.van-tabs__nav--line {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.van-tab__text {
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.van-tabs__line {
|
||||
width: 16px;
|
||||
bottom: 4px;
|
||||
height: 2px;
|
||||
border-radius: 12px;
|
||||
background: #f68804;
|
||||
}
|
||||
}
|
||||
|
||||
.contain {
|
||||
height: calc(100vh - 44px * 3);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div class="taskList" v-if="props.list.length > 0">
|
||||
<div class="taskItem flex-center-between" v-for="item in props.list" :key="item.id">
|
||||
<div class="leftBox flex-vcenter">
|
||||
<LazyImg :src="item.icon" class="taskIcon" :key="item.icon" />
|
||||
<div class="taskInfo">
|
||||
<div class="taskName">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div class="taskDesc" v-if="item.awardList.length > 0">+{{ item.awardList[0]?.num }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action flex-center-center-direction">
|
||||
<div class="btn" v-if="item?.state == 0" @click.stop="goDetailPage(props.mediaType, item.objId)">去完成</div>
|
||||
<div class="btn receive" v-else-if="item?.state == 1" @click.stop="getMediaTaskReceiveList(item)">待领取</div>
|
||||
<div class="btn received" v-else-if="item?.state == 2">已领取</div>
|
||||
<span class="progress">({{ item.progress }}/{{ item.target }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="props.completeAllTask.id" class="all-task">
|
||||
<div class="go flex-center-between">
|
||||
完美任务完成直达领券
|
||||
<span v-if="state === 0" class="status0" @click.stop="goDetailPage(props.mediaType, props.completeAllTask.objId)">
|
||||
去完成
|
||||
<SvgIcon class="icon" name="moreIcon" />
|
||||
</span>
|
||||
<span v-else-if="state === 1" class="status1 flex-vcenter" @click.stop="getMediaTaskReceive()">
|
||||
立即领取
|
||||
<SvgIcon class="icon" name="moreIcon" />
|
||||
</span>
|
||||
|
||||
<span v-if="state === 2" class="status2">已领取</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { goDetailPage } from '../../utils/common';
|
||||
import type { MediaTypeKey } from '../../utils/common';
|
||||
import AcgApi from '@/service/acg';
|
||||
import { showToast } from 'vant';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
list: Types.Acg.TaskListItem[];
|
||||
completeAllTask?: Types.Acg.CompleteAllTask;
|
||||
mediaType: MediaTypeKey;
|
||||
}>(),
|
||||
{
|
||||
list: () => [],
|
||||
mediaType: 'image',
|
||||
completeAllTask: () => {},
|
||||
},
|
||||
);
|
||||
|
||||
const getMediaTaskReceiveList = (item: Types.Acg.TaskListItem) => {
|
||||
AcgApi.getMediaTaskReceive({ taskId: item.id }).then(() => {
|
||||
item.state = 2;
|
||||
showToast('领取成功');
|
||||
});
|
||||
};
|
||||
|
||||
const state = ref(props.completeAllTask.state);
|
||||
watch(
|
||||
() => props.completeAllTask.state,
|
||||
(num: number) => {
|
||||
state.value = num || 0;
|
||||
},
|
||||
);
|
||||
const getMediaTaskReceive = () => {
|
||||
AcgApi.getMediaTaskReceive({ taskId: props.completeAllTask.id }).then(() => {
|
||||
state.value = 2;
|
||||
showToast('领取成功');
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.taskList {
|
||||
margin: 0 auto;
|
||||
width: 343px;
|
||||
padding: 0px 12px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
.taskItem {
|
||||
padding: 12px 0;
|
||||
border-bottom: 0.5px solid rgba(255, 255, 255, 0.05);
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.leftBox {
|
||||
flex: 1;
|
||||
.taskIcon {
|
||||
flex-shrink: 0;
|
||||
height: 65px;
|
||||
width: 65px;
|
||||
// border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.taskInfo {
|
||||
flex: 1;
|
||||
margin-left: 12px;
|
||||
.taskName {
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.taskDesc {
|
||||
font-size: 12px;
|
||||
color: #aaaaaa;
|
||||
margin-top: 5px;
|
||||
line-height: 16px;
|
||||
width: 170px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.action {
|
||||
.btn {
|
||||
height: 28px;
|
||||
width: 60px;
|
||||
background: #ff004f;
|
||||
border-radius: 40px;
|
||||
text-align: center;
|
||||
line-height: 28px;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
&.receive {
|
||||
color: #692a1f;
|
||||
background: #ffd5a8;
|
||||
}
|
||||
&.received {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.all-task {
|
||||
margin: 24px 16px;
|
||||
|
||||
.go {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 22px; /* 137.5% */
|
||||
border-radius: 9px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status0 {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.status1 {
|
||||
color: #ddac44;
|
||||
}
|
||||
.status2 {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<div class="topic">
|
||||
<div class="title">
|
||||
{{ props.title }}
|
||||
</div>
|
||||
<div v-for="(item, index) in autoList" :key="index" class="list" @touchstart.stop @touchmove.stop @touchend.stop>
|
||||
<CategoryContentItem
|
||||
class="item"
|
||||
v-for="childItem in item"
|
||||
:key="childItem.id"
|
||||
:id="childItem.id"
|
||||
:url="childItem.verticalCover"
|
||||
:title="childItem.title"
|
||||
:info="childItem"
|
||||
:contentType="props.contentType"
|
||||
></CategoryContentItem>
|
||||
</div>
|
||||
<div class="score-wrap">
|
||||
<div class="score">
|
||||
<span>{{ props.disVoteCount }}</span>
|
||||
<span>{{ props.voteCount }}</span>
|
||||
</div>
|
||||
<div class="bar">
|
||||
<div
|
||||
class="red"
|
||||
:style="{
|
||||
width: redBar + '%',
|
||||
}"
|
||||
></div>
|
||||
<div
|
||||
class="blue"
|
||||
:style="{
|
||||
width: blueBar + '%',
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="btn">
|
||||
<div class="red" @click="onVote(2)">
|
||||
<img class="icon" src="@/assets/images/png/acg/icon-btn-red.png" />
|
||||
贫乳!!
|
||||
</div>
|
||||
<img class="pk" src="@/assets/images/png/acg/topic-pk.png" />
|
||||
<div class="blue" @click="onVote(1)">
|
||||
<img class="icon" src="@/assets/images/png/acg/icon-btn-blue.png" />
|
||||
巨乳!!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import CategoryContentItem from './categoryContentItem.vue';
|
||||
import MediaApi from '@/service/media';
|
||||
import { showToast } from 'vant';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title: string;
|
||||
list: Types.Media.AllMediaInfoItem[];
|
||||
disVoteCount: number; //红
|
||||
voteCount: number; //蓝
|
||||
id: string;
|
||||
contentType: string;
|
||||
}>(),
|
||||
{
|
||||
disVoteCount: 0,
|
||||
voteCount: 0,
|
||||
title: '',
|
||||
list: () => [],
|
||||
id: '',
|
||||
},
|
||||
);
|
||||
const emit = defineEmits(['disVoteCount', 'voteCount']);
|
||||
|
||||
const autoList = computed(() => {
|
||||
const list: Types.Media.AllMediaInfoItem[][] = [[]];
|
||||
const arr = props.list || [];
|
||||
const len = arr.length;
|
||||
if (len > 1) {
|
||||
list.push([]);
|
||||
const oneLen = Math.floor(len / 2);
|
||||
list[0] = arr.slice(0, oneLen);
|
||||
list[1] = arr.slice(oneLen);
|
||||
} else {
|
||||
list[0] = arr;
|
||||
}
|
||||
|
||||
return list;
|
||||
});
|
||||
|
||||
const redBar = computed(() => {
|
||||
return (props.disVoteCount / (props.disVoteCount + props.voteCount)) * 96 || 48;
|
||||
});
|
||||
|
||||
const blueBar = computed(() => {
|
||||
return (props.voteCount / (props.disVoteCount + props.voteCount)) * 96 || 48;
|
||||
});
|
||||
|
||||
let isVoteIng = false;
|
||||
const onVote = (status: number) => {
|
||||
if (isVoteIng) return;
|
||||
isVoteIng = true;
|
||||
const key = `section_vote`;
|
||||
const value = localStorage.getItem(key) || '';
|
||||
if (value.includes(props.id)) {
|
||||
showToast('您已经投过票啦~');
|
||||
return;
|
||||
}
|
||||
// 状态 1、支持大胸 2、支持贫胸
|
||||
MediaApi.postSctionVote({
|
||||
sId: props.id,
|
||||
status,
|
||||
})
|
||||
.then(() => {
|
||||
showToast('投票成功');
|
||||
if (status === 1) {
|
||||
emit('voteCount');
|
||||
} else {
|
||||
emit('disVoteCount');
|
||||
}
|
||||
localStorage.setItem(key, value + ',' + props.id);
|
||||
})
|
||||
.finally(() => {
|
||||
isVoteIng = false;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '../assets/style.scss';
|
||||
.topic {
|
||||
@include categoryWrap();
|
||||
margin-top: 12px;
|
||||
|
||||
.title {
|
||||
@include categoryTitle();
|
||||
}
|
||||
|
||||
.list {
|
||||
@include categoryList();
|
||||
.item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.score-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&.score {
|
||||
margin-top: 12px;
|
||||
color: $textColor;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
&.bar {
|
||||
margin: 12px 0;
|
||||
height: 10px;
|
||||
width: 100%;
|
||||
> div {
|
||||
height: 100%;
|
||||
width: 100px;
|
||||
border-radius: 5px;
|
||||
|
||||
&.red {
|
||||
background: #e05a54;
|
||||
}
|
||||
&.blue {
|
||||
background: #3675df;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.btn {
|
||||
overflow: hidden;
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
width: 101px;
|
||||
height: 34px;
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
> img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
&.red {
|
||||
background: #e05a54;
|
||||
}
|
||||
&.blue {
|
||||
background: #3675df;
|
||||
}
|
||||
}
|
||||
.pk {
|
||||
height: 39px;
|
||||
margin: 0 10px;
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="updateStatus" :class="{ end: props.info.updateStatus != 1 }">
|
||||
<template v-if="props.info.updateStatus != 1">共{{ props.info.totalEpisode }}章 已完结</template>
|
||||
<template v-else>更新{{ props.info.currentEpisode || props.info.totalEpisode }}章 连载中</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
info: Types.Media.MediaInfo | Types.Media.AllMediaInfoItem | Types.Acg.DiscountChildListItem;
|
||||
}>(),
|
||||
{
|
||||
info: {} as any,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.updateStatus {
|
||||
color: #eec76b;
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
// text-indent: 2px;
|
||||
&.end {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,773 @@
|
||||
<template>
|
||||
<div class="detail-wrap overflow-y-auto" @scroll="onScroll">
|
||||
<template v-if="state.info.id">
|
||||
<div class="header flex-center-between" :style="headStyle">
|
||||
<SvgIcon name="back_white_round1" class="back" @click="$router.go(-1)" />
|
||||
<p v-show="scrollTop >= 100" class="title ellipsis">{{ state.info.title }}</p>
|
||||
<div class="bookself">
|
||||
<div class="menuItem">
|
||||
<SvgIcon
|
||||
class="collected"
|
||||
name="acg-collect-img-1"
|
||||
v-if="historyData?.mediaStatus?.hasCollected"
|
||||
@click="onToogleCollected"
|
||||
></SvgIcon>
|
||||
<SvgIcon class="collected" name="acg-collect-img-0" v-else @click="onToogleCollected"></SvgIcon>
|
||||
<SvgIcon name="acg-share1" @click="openShare" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cover-box flex-vcenter">
|
||||
<lazyImg class="cover" :src="state.info.verticalCover || state.info.horizontalCover"></lazyImg>
|
||||
<div class="content">
|
||||
<h1 class="title ellipsis">{{ state.info.title }}</h1>
|
||||
<div class="status">
|
||||
<!-- <template v-if="state.info.updateStatus == 2">已完结 共{{ state.info.totalEpisode }}话</template>
|
||||
<template v-else>更新至第{{ state.info.currentEpisode || state.info.totalEpisode }}话</template> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<div class="watch" :class="state.info.mediaType">
|
||||
<div class="info">
|
||||
<div class="count flex-vcenter">
|
||||
<div class="count-item">
|
||||
<SvgIcon name="eye-open" />
|
||||
{{ watchCount(state.info.countBrowse) }}
|
||||
</div>
|
||||
<div class="count-item">
|
||||
<SvgIcon name="acg-heart-2" />
|
||||
{{ watchCount(state.info.countLike) }}
|
||||
</div>
|
||||
<div class="count-item">
|
||||
{{ state.info.permission == 0 ? 'VIP' : (state.info.price || '0') + '金币' }}
|
||||
</div>
|
||||
|
||||
<!-- <div class="collected flex-vcenter" @click="onToogleCollected">
|
||||
<SvgIcon v-if="!hasCollected" name="acg-collect-star-0" />
|
||||
<SvgIcon v-else name="acg-collect-star-1" style="color: #f52c56" />
|
||||
{{ watchCount(state.info.countCollect) }}
|
||||
</div> -->
|
||||
</div>
|
||||
<div :class="['summary', !showMoreSummary && 'ellipsis3']">{{ state.info.summary || '' }}</div>
|
||||
<div
|
||||
v-if="state.info.summary"
|
||||
:class="['more-summary', showMoreSummary && 'showMoreSummary']"
|
||||
@click="showMoreSummary = !showMoreSummary"
|
||||
>
|
||||
更多简介
|
||||
<SvgIcon name="rightArrow1" />
|
||||
</div>
|
||||
<div class="tags" v-if="tagDetails.length">
|
||||
<div
|
||||
class="tag"
|
||||
v-for="item in tagDetails"
|
||||
:key="item.id"
|
||||
@click="goAcgTagPage(item.id, item.name, 'image')"
|
||||
>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav" @click="visibleCatalogue = true">
|
||||
<span class="title">选集</span>
|
||||
<span class="count flex-vcenter">
|
||||
{{ EpisodeTitle }}
|
||||
<SvgIcon name="rightArrow1" />
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="episode-box"
|
||||
ref="episodeBox"
|
||||
@touchstart.stop
|
||||
@touchmove.stop
|
||||
@touchend.stop
|
||||
@scroll="onScrollEpisodeBox"
|
||||
>
|
||||
<Episode
|
||||
showType="text"
|
||||
:id="'episode-' + item.id"
|
||||
class="item"
|
||||
:episodeNumber="childState.episodeNumber"
|
||||
:info="item"
|
||||
v-for="item in autoList?.slice(0, 4) || []"
|
||||
:key="item.id"
|
||||
@click="childState.episodeNumber = item.episodeNumber"
|
||||
></Episode>
|
||||
</div>
|
||||
</div>
|
||||
<RecommendedList :type="state.info.mediaType" :tagId="tagDetails[0]?.id" class="recommendedBox" />
|
||||
<div class="action">
|
||||
<div class="btn flex-vcenter" @click="onToogleLiked">
|
||||
<SvgIcon v-if="historyData?.mediaStatus?.hasLiked" name="acg-heart-1" />
|
||||
<SvgIcon v-else name="acg-heart-0" />
|
||||
喜欢
|
||||
</div>
|
||||
<div class="btn flex-vcenter go-page" @click="goWatch">
|
||||
{{ currentEpisodeName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<Loading v-else />
|
||||
</div>
|
||||
<van-popup v-model:show="visibleCatalogue" position="bottom" z-index="1001" :style="{ height: '438px' }">
|
||||
<div class="play-list-box">
|
||||
<div class="line"></div>
|
||||
<div class="title flex-center-between">
|
||||
漫画列表
|
||||
<div class="right">
|
||||
<SvgIcon name="acg_sort_0" v-if="sort" />
|
||||
<SvgIcon name="acg_sort_1" v-else />
|
||||
<span @click="sort = !sort">
|
||||
{{ sort ? '正序' : '倒序' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-box overflow-y-auto">
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
|
||||
<div class="list">
|
||||
<Episode
|
||||
showType="text"
|
||||
class="item"
|
||||
:episodeNumber="childState.episodeNumber"
|
||||
:info="item"
|
||||
v-for="item in autoList || []"
|
||||
:key="item.id"
|
||||
@click="handlePopupSwitchEpisode(item.episodeNumber)"
|
||||
></Episode>
|
||||
</div>
|
||||
</PullRefresh>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
<PayForWatch
|
||||
v-if="childState.validateItem && visiblePayForWatch"
|
||||
ref="dvPayForWatch"
|
||||
:isOpen="visiblePayForWatch"
|
||||
:info="childState.validateItem"
|
||||
@close="visiblePayForWatch = false"
|
||||
@buySuccess="beforeBuySuccess"
|
||||
></PayForWatch>
|
||||
<AppSharePopup
|
||||
coverType="portrait"
|
||||
:is-open="visibleShare"
|
||||
:cover="state.info.verticalCover || state.info.horizontalCover"
|
||||
@close="visibleShare = false"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AcgImageDetail',
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRaw, watch, computed, watchEffect, provide, onDeactivated, onActivated } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import PayForWatch from '../components/pay/PayForWatch.vue';
|
||||
import tabAction from '@/utils/tabAction';
|
||||
import { AdType } from '@/enums/app';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import MediaInfo from '../utils/mediaInfo';
|
||||
import mediaContentInfo from '../utils/mediaContentInfo';
|
||||
import Episode from '../components/episode.vue';
|
||||
import { handleCollected } from '../utils/collect';
|
||||
import { dateFormatYyyyMmDd, watchCount } from '@/utils/index';
|
||||
import { Pay_parent_on, Pay_child_on } from '../utils/pay-mitt';
|
||||
import type { PayInfo } from '../utils/pay-mitt';
|
||||
import { goAcgTagPage } from '../utils/common';
|
||||
import CommunityApi from '@/service/community';
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const dvComment = ref();
|
||||
const showMoreSummary = ref(false);
|
||||
|
||||
const { state, historyEpisodeNumber, historyData, addHistory } = MediaInfo({
|
||||
dbType: HistoryVideoType.AcgImage,
|
||||
});
|
||||
Pay_parent_on({ id: route.query.id as string, state, historyData, addHistory });
|
||||
provide('historyData', historyData);
|
||||
const {
|
||||
state: childState,
|
||||
autoList,
|
||||
visibleCatalogue,
|
||||
handlePopupSwitchEpisode,
|
||||
moreParams,
|
||||
sort,
|
||||
onLoad,
|
||||
onRefresh,
|
||||
visiblePayForWatch,
|
||||
validateHasBuy,
|
||||
currentItem,
|
||||
} = mediaContentInfo({ dbType: HistoryVideoType.AcgImage, isGetHistoryInit: true, isGetInfo: false });
|
||||
|
||||
Pay_child_on(childState.id, childState);
|
||||
const { id = '' } = route.query;
|
||||
|
||||
watch(historyEpisodeNumber, (num) => {
|
||||
childState.episodeNumber = num;
|
||||
});
|
||||
const currentEpisodeName = computed(() => {
|
||||
return '开始阅读';
|
||||
});
|
||||
|
||||
const EpisodeTitle = computed(() => {
|
||||
return '全' + state.info.totalEpisode + '话';
|
||||
});
|
||||
|
||||
const tagDetails = computed(() => {
|
||||
const list = state.info.tagDetails || [];
|
||||
const seenIds = new Set<string>();
|
||||
const seenNames = new Set<string>();
|
||||
return list.filter((item) => {
|
||||
if (!item) return false;
|
||||
if (seenIds.has(item.id) || seenNames.has(item.name)) return false;
|
||||
seenIds.add(item.id);
|
||||
seenNames.add(item.name);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
const goWatch = () => {
|
||||
if (validateHasBuy(currentItem.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.push({
|
||||
path: '/AcgImageDetailWatch',
|
||||
query: {
|
||||
id,
|
||||
episode: childState.episodeNumber,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
watch(dvComment, (el) => {
|
||||
if (!el) return;
|
||||
if (!id) return;
|
||||
dvComment.value[0].state.videoInfo = toRaw(state.info);
|
||||
dvComment.value[0].getCommentList();
|
||||
});
|
||||
|
||||
const beforeBuySuccess = (obj: PayInfo) => {
|
||||
if (obj.type === 'all') {
|
||||
state.info.mediaStatus.hasPaid = true;
|
||||
historyData.value.mediaStatus.hasPaid = true;
|
||||
addHistory();
|
||||
childState.list.forEach((item) => {
|
||||
item.hasBuy = true;
|
||||
});
|
||||
}
|
||||
|
||||
buySuccess(obj);
|
||||
};
|
||||
|
||||
const buySuccess = ({ type }: PayInfo) => {
|
||||
if (type === 'one') {
|
||||
const item = (childState.list as Types.Acg.MediaContentListItem[]).find(
|
||||
(item) => item.episodeNumber === currentItem.value.episodeNumber,
|
||||
);
|
||||
if (item) {
|
||||
item.hasBuy = true;
|
||||
}
|
||||
}
|
||||
|
||||
visiblePayForWatch.value = false;
|
||||
goWatch();
|
||||
};
|
||||
|
||||
let isInitCollected = false;
|
||||
watch(
|
||||
() => state.info,
|
||||
(obj) => {
|
||||
if (isInitCollected) return;
|
||||
isInitCollected = true;
|
||||
},
|
||||
);
|
||||
// 收藏
|
||||
const onToogleCollected = async () => {
|
||||
try {
|
||||
const tempIsCollect = !historyData.value.mediaStatus.hasCollected;
|
||||
handleCollected(state.info.id, tempIsCollect).then(() => {
|
||||
historyData.value.mediaStatus.hasCollected = tempIsCollect;
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
// 点赞
|
||||
const onToogleLiked = () => {
|
||||
const zan = !historyData.value.mediaStatus.hasLiked;
|
||||
|
||||
(zan ? CommunityApi.zan : CommunityApi.cancleZan)(
|
||||
zan
|
||||
? {
|
||||
type: 'image',
|
||||
objID: state.info.id,
|
||||
}
|
||||
: {
|
||||
type: 'image',
|
||||
objIDs: [state.info.id],
|
||||
},
|
||||
).then(() => {
|
||||
historyData.value.mediaStatus.hasLiked = zan;
|
||||
});
|
||||
};
|
||||
const scrollTop = ref(0);
|
||||
const onScroll = (e: Event) => {
|
||||
const currentTop = (e.target as HTMLElement).scrollTop;
|
||||
scrollTop.value = currentTop;
|
||||
};
|
||||
const headStyle = computed(() => {
|
||||
return {
|
||||
background: `rgba(0, 0, 0, ${scrollTop.value / 100})`,
|
||||
};
|
||||
});
|
||||
|
||||
const visibleShare = ref(false);
|
||||
|
||||
const openShare = () => {
|
||||
const { id, verticalCover, horizontalCover } = historyData.value;
|
||||
let obj = {
|
||||
id,
|
||||
cover: verticalCover || horizontalCover,
|
||||
};
|
||||
// globalStore.setShareInfo(obj);
|
||||
visibleShare.value = true;
|
||||
};
|
||||
const episodeBox = ref();
|
||||
let episodeBoxLeft = 0;
|
||||
let isDeactivated = false;
|
||||
const onScrollEpisodeBox = (e: Event) => {
|
||||
if (!episodeBox.value) return;
|
||||
const scrollLeft = (e.target as HTMLElement).scrollLeft;
|
||||
episodeBoxLeft = scrollLeft;
|
||||
};
|
||||
onDeactivated(() => {
|
||||
isDeactivated = true;
|
||||
});
|
||||
onActivated(() => {
|
||||
if (episodeBox.value && episodeBoxLeft) {
|
||||
episodeBox.value.scrollLeft = episodeBoxLeft;
|
||||
}
|
||||
});
|
||||
watch(
|
||||
() => childState.episodeNumber,
|
||||
(num: number) => {
|
||||
if (isDeactivated) {
|
||||
handlePopupSwitchEpisode(num);
|
||||
isDeactivated = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
@import '../assets/style.scss';
|
||||
.detail-wrap {
|
||||
$themeBgColor2: #222324;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@include vanTab();
|
||||
|
||||
--van-tab-active-text-color: rgba(255, 255, 255, 0.9);
|
||||
.borderBottomDiv {
|
||||
margin: 0 14px;
|
||||
width: calc(100% - 28px);
|
||||
}
|
||||
.header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
// background: $themeBgColor2;
|
||||
height: 39px;
|
||||
z-index: 10;
|
||||
padding: 0 16px;
|
||||
.back {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
color: #292929;
|
||||
}
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
line-height: 24px; /* 133.333% */
|
||||
margin: 0 16px;
|
||||
}
|
||||
.bookself {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
border-radius: 11px;
|
||||
color: #ffffff99;
|
||||
.menuItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.collected {
|
||||
margin-right: 10px;
|
||||
color: #fff;
|
||||
}
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cover-box {
|
||||
width: 100%;
|
||||
height: 216px;
|
||||
padding: 11px 16px 23px;
|
||||
margin-bottom: 25px;
|
||||
position: relative;
|
||||
align-items: flex-end;
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
&::after {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, #000 100%);
|
||||
}
|
||||
}
|
||||
.content {
|
||||
z-index: 3;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
}
|
||||
.status {
|
||||
color: #e0e0e0;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
margin: 4px 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.info-box {
|
||||
width: 100%;
|
||||
border-radius: 24px 24px 0px 0px;
|
||||
background: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0) 100%);
|
||||
}
|
||||
.watch {
|
||||
padding: 16px 0 6px;
|
||||
margin-top: -50px;
|
||||
border-radius: 20px 20px 0px 0px;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
.info {
|
||||
padding: 0 10px 10px;
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 10px;
|
||||
margin: 12px 0 14px;
|
||||
color: #e0e0e0;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
.tag {
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 4px 8px;
|
||||
font-weight: 400;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
}
|
||||
.summary {
|
||||
color: #989898;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 142.857% */
|
||||
}
|
||||
.more-summary {
|
||||
color: #efefef;
|
||||
font-size: 12px;
|
||||
margin: 12px auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #fff;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
.showMoreSummary {
|
||||
svg {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
.count {
|
||||
color: #a7a7a7;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 180% */
|
||||
.count-item {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 10px 24px 16px 0;
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #fff;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 14px;
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
.count {
|
||||
font-size: 12px;
|
||||
color: #989898;
|
||||
font-weight: 400;
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.episode-box {
|
||||
margin: 9px 0 0;
|
||||
padding-left: 16px;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: column;
|
||||
.item {
|
||||
flex-shrink: 0;
|
||||
margin-right: 11px;
|
||||
}
|
||||
}
|
||||
// .btn {
|
||||
// display: flex;
|
||||
// width: 249px;
|
||||
// height: 39px;
|
||||
// justify-content: center;
|
||||
// align-items: center;
|
||||
// border-radius: 19px;
|
||||
// background: $themeBgLgColor;
|
||||
|
||||
// color: #fff;
|
||||
// font-size: 16px;
|
||||
// font-weight: 500;
|
||||
// margin: 0 auto;
|
||||
// }
|
||||
}
|
||||
|
||||
.play-list {
|
||||
@include categoryWrap();
|
||||
border: none;
|
||||
|
||||
margin-top: 0;
|
||||
padding-top: 16px;
|
||||
.title {
|
||||
padding: 0 14px;
|
||||
@include categoryTitle();
|
||||
}
|
||||
|
||||
.list {
|
||||
@include categoryList();
|
||||
padding-left: 14px;
|
||||
|
||||
.item {
|
||||
height: 118px;
|
||||
flex-shrink: 0;
|
||||
width: 97px;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 4px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: 97px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.icon {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
z-index: 1;
|
||||
width: 18px;
|
||||
height: 14px;
|
||||
}
|
||||
.count {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 97px;
|
||||
height: 26px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.play {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.title-box {
|
||||
width: 100%;
|
||||
height: 21px;
|
||||
padding: 0 5px;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
flex: 1;
|
||||
color: $textColor;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
@include textEllipsis();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.action {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 1) 0%, #000 100%);
|
||||
height: 68px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
padding: 0 16px;
|
||||
@include SafeArea('padding', 'bottom', 0);
|
||||
display: flex;
|
||||
.btn {
|
||||
width: 108px;
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
height: 44px;
|
||||
color: #989898;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
justify-content: center;
|
||||
&.go-page {
|
||||
background: $themeColor;
|
||||
width: 223px;
|
||||
color: #fff;
|
||||
}
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.play-list-box {
|
||||
$paddingBottom: 38px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 16px 16px 0px 0px;
|
||||
background: $popupBg;
|
||||
padding: 18px 16px $paddingBottom;
|
||||
@include SafeArea('padding', 'bottom', 0);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: $textColor;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
|
||||
> .title {
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
line-height: 25px; /* 140% */
|
||||
margin: 16px 0 0;
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
svg {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
margin-right: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
span {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
}
|
||||
}
|
||||
.line {
|
||||
width: 36px;
|
||||
height: 5px;
|
||||
border-radius: 100px;
|
||||
background: #989898;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.list-box {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.recommendedBox {
|
||||
padding: 0 16px;
|
||||
:deep(.van-tabs__wrap) {
|
||||
// top: 39px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,761 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="state.info.id"
|
||||
ref="dvCaricatureWrap"
|
||||
class="caricature-wrap overflow-y-auto"
|
||||
@touchstart="onTouchStart"
|
||||
@touchmove="onTouchMove"
|
||||
@scroll="onContentScroll"
|
||||
>
|
||||
<Transition name="top">
|
||||
<div v-show="visibleHead" class="nav" @click.stop="visibleHead = false">
|
||||
<SvgIcon name="back_white" class="back" @click.stop="$router.go(-1)" />
|
||||
<span class="title">{{ currentItem.name }}</span>
|
||||
<div class="right flex-vcenter">
|
||||
<div
|
||||
class="collect"
|
||||
:class="{ collected: historyData.mediaStatus?.hasCollected }"
|
||||
@click.stop="onToogleCollected"
|
||||
>
|
||||
<SvgIcon name="acg-collect-img-0" class="svg-icon" v-if="!historyData.mediaStatus?.hasCollected" />
|
||||
<SvgIcon name="acg-collect-img-1" class="svg-icon" style="color: #f52c56" v-else />
|
||||
</div>
|
||||
<SvgIcon class="share" name="acg-share1" @click.stop="openShare" />
|
||||
</div>
|
||||
<!-- <SvgIcon class="back right" name="acg_catalogue" @click.stop="visibleCatalogue = true" /> -->
|
||||
</div>
|
||||
</Transition>
|
||||
<div class="like-box" :class="isToolShow ? 'top150' : ''" v-show="visibleHead" @click.stop="onToogleLiked">
|
||||
<SvgIcon v-if="historyData?.mediaStatus?.hasLiked" name="acg-heart-1" />
|
||||
<SvgIcon v-else name="acg-heart-0" />
|
||||
喜欢
|
||||
</div>
|
||||
<div class="list" @click="onClick">
|
||||
<ImgObserver
|
||||
v-for="(url, index) in currentEpisode"
|
||||
class="item"
|
||||
:style="getImgHeight(url)"
|
||||
:class="{ move: visibleCatalogue }"
|
||||
:key="url"
|
||||
:index="index"
|
||||
:url="url"
|
||||
:init="index - currentProgress <= 1"
|
||||
@updateIndex="updateIndex"
|
||||
@updateSize="updateSize"
|
||||
></ImgObserver>
|
||||
</div>
|
||||
<div
|
||||
v-show="running"
|
||||
class="running-box"
|
||||
@click.stop="
|
||||
{
|
||||
running = false;
|
||||
visibleHead = true;
|
||||
}
|
||||
"
|
||||
>
|
||||
<img class="running" src="@/assets/gif/running.gif" />
|
||||
</div>
|
||||
<Transition name="bottom">
|
||||
<div v-show="visibleHead && !running" ref="bottomRef" class="action">
|
||||
<div class="box">
|
||||
<div class="func" @click.stop="visibleCatalogue = true">
|
||||
<SvgIcon class="icon" name="acg_catalogue" />
|
||||
目录
|
||||
</div>
|
||||
<div class="func" @click.stop="visibleComment = true">
|
||||
<SvgIcon class="icon" name="acg_comment" />
|
||||
评论
|
||||
</div>
|
||||
<div class="func" @click.stop="isToolShow = !isToolShow">
|
||||
<SvgIcon class="icon" :name="isToolShow ? 'ddown' : 'acg_progress'" />
|
||||
进度
|
||||
</div>
|
||||
<div class="func" @click.stop="running = true">
|
||||
<SvgIcon class="icon" name="acg_automatic" />
|
||||
自动
|
||||
</div>
|
||||
<!-- <div class="prev" @click.stop="switchEpisodeNumber(-1)">
|
||||
<SvgIcon class="icon" name="acg_prev" />
|
||||
上一话
|
||||
</div>
|
||||
<div class="next" @click.stop="switchEpisodeNumber(1)">
|
||||
<SvgIcon class="icon" name="acg_prev" />
|
||||
下一话
|
||||
</div> -->
|
||||
</div>
|
||||
<Transition name="van-slide-up">
|
||||
<div v-show="isToolShow && !running" class="tools">
|
||||
<div class="prev" @click.stop="switchEpisodeNumber(-1)">
|
||||
<SvgIcon class="icon" name="backWhite" />
|
||||
<span>上一篇</span>
|
||||
</div>
|
||||
<div class="slider">
|
||||
<van-slider
|
||||
style="--van-slider-button-background: #ffd300"
|
||||
:model-value="progressRatio"
|
||||
@update:model-value="(v) => (progressRatio = v)"
|
||||
active-color="#FFD300"
|
||||
inactive-color="#fff3"
|
||||
button-size="12px"
|
||||
bar-height="4px"
|
||||
@change="reposition"
|
||||
></van-slider>
|
||||
</div>
|
||||
<div class="next" @click.stop="switchEpisodeNumber(1)">
|
||||
<SvgIcon class="icon" name="backWhite" />
|
||||
<span>下一篇</span>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- <div v-show="!visibleHead" class="tips">
|
||||
第{{ state.episodeNumber }}话 {{ currentProgress }}/{{ currentEpisode.length }}
|
||||
</div> -->
|
||||
</div>
|
||||
<Loading v-else />
|
||||
<van-popup v-model:show="visibleCatalogue" position="left" :style="{ width: '50%', height: '100%' }">
|
||||
<div class="play-list-box">
|
||||
<div class="header">
|
||||
<!-- <SvgIcon class="close" name="popup_close" @click.stop="visibleCatalogue = false" /> -->
|
||||
<div class="header-title ellipsis">{{ historyData.title }}</div>
|
||||
<div class="sort flex-center-between">
|
||||
<span class="menu-info">
|
||||
共{{ historyData.totalEpisode }}话 {{ getUpdateStatusText(historyData.updateStatus) }}
|
||||
</span>
|
||||
<span class="btn" @click="sort = !sort">
|
||||
<SvgIcon v-show="sort" class="section" name="acg_sort_0" />
|
||||
<SvgIcon v-show="!sort" class="section" name="acg_sort_1" />
|
||||
<span>{{ sort ? '正序' : '倒序' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list overflow-y-auto">
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh" finishedText="">
|
||||
<div
|
||||
:id="`section_${item.episodeNumber}`"
|
||||
class="item"
|
||||
:class="{ active: item.episodeNumber === state.episodeNumber }"
|
||||
v-for="item in autoList"
|
||||
:key="item.id"
|
||||
@click="switchEpisodeNumberCatalogue(item)"
|
||||
>
|
||||
{{ item.episodeNumber }}.{{ item.name }}
|
||||
</div>
|
||||
</PullRefresh>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
<PayForWatch
|
||||
v-if="state.info.id"
|
||||
ref="dvPayForWatch"
|
||||
:isOpen="visiblePayForWatch"
|
||||
:info="state.validateItem.id ? state.validateItem : state.info"
|
||||
@close="closePayForWatch"
|
||||
@buySuccess="beforeBuySuccess"
|
||||
></PayForWatch>
|
||||
|
||||
<AppSharePopup
|
||||
:is-open="visibleShare"
|
||||
coverType="portrait"
|
||||
@close="visibleShare = false"
|
||||
:cover="historyData.verticalCover || state.info.cover"
|
||||
/>
|
||||
|
||||
<CommentBox
|
||||
v-if="state.info.id"
|
||||
ref="dvComment"
|
||||
class="comment"
|
||||
:isOpen="visibleComment"
|
||||
objType="cartoon"
|
||||
@close="visibleComment = false"
|
||||
:showCollected="false"
|
||||
></CommentBox>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AcgImageDetailWatch',
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed, onDeactivated, onUnmounted, onBeforeUnmount, provide, reactive, toRaw } from 'vue';
|
||||
import PayForWatch from '../components/pay/PayForWatch.vue';
|
||||
import mediaContentInfo from '../utils/mediaContentInfo';
|
||||
import ImgObserver from '../components/ImgObserver.vue';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import { showToast } from 'vant';
|
||||
import { Pay_emit } from '../utils/pay-mitt';
|
||||
import type { PayInfo } from '../utils/pay-mitt';
|
||||
import { handleCollected, Collected_emit } from '../utils/collect';
|
||||
import CommunityApi from '@/service/community';
|
||||
import { getUpdateStatusText } from '../utils/common';
|
||||
import { debounce } from '@/utils';
|
||||
// import { useGlobalStore } from '@/stores/global';
|
||||
// const globalStore = useGlobalStore();
|
||||
const running = ref(false);
|
||||
const isToolShow = ref(false);
|
||||
const visibleHead = ref(true);
|
||||
const progressRatio = ref(0);
|
||||
const contentMaxScrollTop = ref(0);
|
||||
const contentScrollTop = ref(0);
|
||||
const visibleSides = ref(true);
|
||||
const bottomRef = ref();
|
||||
const {
|
||||
state,
|
||||
currentItem,
|
||||
visibleCatalogue,
|
||||
dvPayForWatch,
|
||||
visiblePayForWatch,
|
||||
closePayForWatch,
|
||||
buySuccess,
|
||||
switchEpisodeNumber,
|
||||
autoList,
|
||||
moreParams,
|
||||
onLoad,
|
||||
onRefresh,
|
||||
switchEpisodeNumberCatalogue,
|
||||
historyData,
|
||||
addHistoryReadData,
|
||||
sort,
|
||||
} = mediaContentInfo({ isGetHistoryInit: true, dbType: HistoryVideoType.AcgImage });
|
||||
provide('historyData', historyData);
|
||||
const dvCaricatureWrap = ref();
|
||||
const currentProgress = ref(1);
|
||||
const currentEpisode = computed(() => {
|
||||
return state.info.urlSet || [];
|
||||
});
|
||||
const reposition = () => {
|
||||
if (dvCaricatureWrap.value) {
|
||||
dvCaricatureWrap.value.scrollTop = (contentMaxScrollTop.value * progressRatio.value) / 100;
|
||||
}
|
||||
};
|
||||
const currentHistoryReadData = computed(() => {
|
||||
const arr = historyData.value.historyReadData || [];
|
||||
return arr.find((item) => item.episodeNumber === state.episodeNumber) || { scrollTop: 0, imgs: {} };
|
||||
});
|
||||
watch(
|
||||
dvCaricatureWrap,
|
||||
() => {
|
||||
dvCaricatureWrap.value.scrollTop = currentHistoryReadData.value.scrollTop || 0;
|
||||
},
|
||||
{
|
||||
once: true,
|
||||
},
|
||||
);
|
||||
const onContentScroll = (e: Event) => {
|
||||
const target = e.target as HTMLElement;
|
||||
contentScrollTop.value = target.scrollTop;
|
||||
contentMaxScrollTop.value = target.scrollHeight - target.offsetHeight;
|
||||
if (contentMaxScrollTop.value > 0) {
|
||||
progressRatio.value = (contentScrollTop.value / contentMaxScrollTop.value) * 100;
|
||||
}
|
||||
const progress =
|
||||
contentMaxScrollTop.value <= 0 ? 100 : Math.max(1, Math.min(100, Math.ceil((contentScrollTop.value / contentMaxScrollTop.value) * 100)));
|
||||
addHistoryReadData(state.episodeNumber, {
|
||||
progress,
|
||||
scrollTop: contentScrollTop.value,
|
||||
});
|
||||
const isEnd = contentScrollTop.value + 10 >= contentMaxScrollTop.value;
|
||||
if (isEnd) {
|
||||
console.log('阅读结束')
|
||||
running.value = false;
|
||||
visibleSides.value = true;
|
||||
bottomRef.value?.showTool();
|
||||
return;
|
||||
}
|
||||
if (!running.value) {
|
||||
if (contentScrollTop.value < 100) {
|
||||
visibleSides.value = true;
|
||||
}
|
||||
// visibleSides.value = contentScrollTop.value < 100
|
||||
}
|
||||
};
|
||||
const getImgHeight = (url: string) => {
|
||||
const imgs = (currentHistoryReadData.value.imgs || {})[url];
|
||||
const { width = 0, height = 0 } = imgs || {};
|
||||
if (width && height) {
|
||||
return {
|
||||
height: (window.innerWidth / width) * height + 'px',
|
||||
};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
const updateIndex = ({ index, url }: { index: number; url: string }) => {
|
||||
let progress = 0;
|
||||
if (dvCaricatureWrap.value.scrollTop === 0) {
|
||||
progress = 1;
|
||||
} else {
|
||||
progress = Number(index) + 1;
|
||||
}
|
||||
currentProgress.value = progress;
|
||||
addHistoryReadData(state.episodeNumber, {
|
||||
progress: Math.ceil((progress / currentEpisode.value.length) * 100),
|
||||
progressUrl: url,
|
||||
scrollTop: dvCaricatureWrap.value.scrollTop,
|
||||
});
|
||||
};
|
||||
const updateSize = (imgObj: any) => {
|
||||
addHistoryReadData(state.episodeNumber, {}, imgObj);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => state.episodeNumber,
|
||||
(num) => {
|
||||
addHistoryReadData(num, { progress: 1 });
|
||||
if (dvCaricatureWrap.value) {
|
||||
dvCaricatureWrap.value.scrollTop = 0;
|
||||
}
|
||||
visibleHead.value = true;
|
||||
},
|
||||
);
|
||||
addHistoryReadData(state.episodeNumber + 1, {});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
addHistoryReadData(state.episodeNumber, {
|
||||
scrollTop: dvCaricatureWrap.value.scrollTop,
|
||||
});
|
||||
|
||||
addHistoryReadData(state.episodeNumber + 1, {});
|
||||
});
|
||||
// 点赞
|
||||
const onToogleLiked = () => {
|
||||
const zan = !historyData.value.mediaStatus.hasLiked;
|
||||
|
||||
(zan ? CommunityApi.zan : CommunityApi.cancleZan)(
|
||||
zan
|
||||
? {
|
||||
type: 'image',
|
||||
objID: historyData.value.id,
|
||||
}
|
||||
: {
|
||||
type: 'image',
|
||||
objIDs: [historyData.value.id],
|
||||
},
|
||||
).then(() => {
|
||||
historyData.value.mediaStatus.hasLiked = zan;
|
||||
});
|
||||
};
|
||||
const onClick = (e: MouseEvent) => {
|
||||
if (running.value) {
|
||||
visibleHead.value = !visibleHead.value;
|
||||
return;
|
||||
}
|
||||
const { clientY } = e;
|
||||
let scrollTop = 0;
|
||||
const OneThird = window.innerHeight / 3.5;
|
||||
if (clientY < OneThird) {
|
||||
scrollTop = -300;
|
||||
} else if (clientY > OneThird * 2) {
|
||||
scrollTop = 300;
|
||||
} else {
|
||||
visibleHead.value = !visibleHead.value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (scrollTop !== 0) {
|
||||
visibleHead.value = false;
|
||||
dvCaricatureWrap.value.scrollTo({ top: scrollTop + dvCaricatureWrap.value.scrollTop, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
let lastScrollTop = 0;
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
const { clientY } = e.targetTouches[0];
|
||||
lastScrollTop = clientY;
|
||||
};
|
||||
const onTouchMove = (e: TouchEvent) => {
|
||||
const { clientY } = e.targetTouches[0];
|
||||
const distance = lastScrollTop - clientY;
|
||||
if (distance > 30) {
|
||||
visibleHead.value = false;
|
||||
return;
|
||||
}
|
||||
if (distance < -30) {
|
||||
visibleHead.value = true;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// watch(visibleCatalogue, (bool) => {
|
||||
// if (bool) {
|
||||
// visibleHead.value = false;
|
||||
// }
|
||||
// });
|
||||
|
||||
const scrollSpeed = 1;
|
||||
let moveInterval = null as ReturnType<typeof setInterval> | null;
|
||||
|
||||
const cancelAutoMove = () => {
|
||||
moveInterval && clearInterval(moveInterval);
|
||||
moveInterval = null;
|
||||
running.value = false;
|
||||
};
|
||||
|
||||
const autoMove = () => {
|
||||
const { scrollTop, scrollHeight } = dvCaricatureWrap.value;
|
||||
|
||||
if (scrollTop >= scrollHeight) {
|
||||
// 停止自动移动
|
||||
cancelAutoMove();
|
||||
return;
|
||||
}
|
||||
|
||||
let newPosition = scrollTop + scrollSpeed;
|
||||
|
||||
dvCaricatureWrap.value.scrollTo(0, newPosition);
|
||||
};
|
||||
|
||||
onDeactivated(() => {
|
||||
cancelAutoMove();
|
||||
});
|
||||
onUnmounted(() => {
|
||||
cancelAutoMove();
|
||||
});
|
||||
|
||||
watch(running, (bool) => {
|
||||
if (bool) {
|
||||
moveInterval = setInterval(autoMove, 10);
|
||||
showToast({
|
||||
message: '自动阅读模式',
|
||||
onClose: () => {
|
||||
if (moveInterval) {
|
||||
visibleHead.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
cancelAutoMove();
|
||||
}
|
||||
});
|
||||
|
||||
const beforeBuySuccess = (obj: PayInfo) => {
|
||||
Pay_emit(historyData.value.id, obj);
|
||||
buySuccess(obj.type);
|
||||
};
|
||||
|
||||
// 收藏
|
||||
const onToogleCollected = async () => {
|
||||
try {
|
||||
const {
|
||||
id,
|
||||
mediaStatus: { hasCollected },
|
||||
} = historyData.value;
|
||||
const tempIsCollect = !hasCollected;
|
||||
handleCollected(id, tempIsCollect).then(() => {
|
||||
historyData.value.mediaStatus.hasCollected = tempIsCollect;
|
||||
Collected_emit(id, tempIsCollect);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
const visibleShare = ref(false);
|
||||
const openShare = () => {
|
||||
const { id, verticalCover, horizontalCover } = historyData.value;
|
||||
let obj = {
|
||||
id,
|
||||
cover: verticalCover || horizontalCover,
|
||||
};
|
||||
// globalStore.setShareInfo(obj);
|
||||
visibleShare.value = true;
|
||||
};
|
||||
|
||||
const visibleComment = ref(false);
|
||||
const dvComment = ref();
|
||||
watch(dvComment, (el) => {
|
||||
if (!el) return;
|
||||
if (!state.info.id || !dvComment.value?.state) return;
|
||||
console.log(state, 'state.info', state.validateItem);
|
||||
dvComment.value.state.videoInfo = toRaw({
|
||||
...state.info,
|
||||
id: state.id,
|
||||
});
|
||||
dvComment.value.getCommentList();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
.caricature-wrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// @include SafeArea('padding', 'bottom', 60px);
|
||||
.like-box {
|
||||
position: fixed;
|
||||
bottom: 81px;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
border-radius: 999px 0px 0px 999px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
width: 86px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
svg {
|
||||
margin-right: 5px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
.top150 {
|
||||
bottom: 150px !important;
|
||||
}
|
||||
.nav {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
background: #181824;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
.back {
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
color: #fff;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.title {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
.right {
|
||||
flex-shrink: 0;
|
||||
margin-left: 4px;
|
||||
.collect {
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
.share {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
margin: 0 16px 0 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .list {
|
||||
padding-top: 1px;
|
||||
.item {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
height: auto;
|
||||
|
||||
:deep(.animationImg) {
|
||||
overflow: visible;
|
||||
.loading {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
position: fixed;
|
||||
box-sizing: content-box;
|
||||
@include SafeArea('padding', 'bottom', 0);
|
||||
// left: 88px;
|
||||
// right: 88px;
|
||||
height: 48px;
|
||||
background: #000;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
transition: all 0.3s;
|
||||
|
||||
.box {
|
||||
z-index: 2;
|
||||
border-radius: 100px;
|
||||
padding: 12px 16px;
|
||||
// background: rgba(0, 0, 0, 0.60)
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
font-size: 12px;
|
||||
.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
&.next .icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// .tips {
|
||||
// position: fixed;
|
||||
// @include SafeAreaFixed('bottom', 40px);
|
||||
// right: 20px;
|
||||
// border-radius: 8px;
|
||||
// background: rgba(0, 0, 0, 0.6);
|
||||
// padding: 8px 12px;
|
||||
// color: #f1f1f1;
|
||||
// font-size: 14px;
|
||||
// font-weight: 500;
|
||||
// }
|
||||
}
|
||||
|
||||
.play-list-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #0f0f0f;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
.header {
|
||||
padding-top: 85px;
|
||||
width: 100%;
|
||||
.close {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.header-title {
|
||||
width: 100%;
|
||||
text-indent: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
font-family: Roboto;
|
||||
}
|
||||
.sort {
|
||||
flex-shrink: 0;
|
||||
margin: 16px 0 14px;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #141414;
|
||||
background: #f68804;
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.section {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
.menu-info {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
flex: 1;
|
||||
.item {
|
||||
width: 100%;
|
||||
height: 19px;
|
||||
line-height: 19px;
|
||||
padding: 0 16px;
|
||||
margin-bottom: 11px;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
@include textEllipsis();
|
||||
&.active {
|
||||
color: #f68804;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.running-box {
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
border-radius: 50%;
|
||||
position: fixed;
|
||||
bottom: 50px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 100;
|
||||
.running {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
.tools {
|
||||
position: absolute;
|
||||
top: -70px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
padding: 0 16px;
|
||||
color: #fff;
|
||||
background-color: #000;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.prev,
|
||||
.next {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
|
||||
.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.next .icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 245px;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
.top-enter-active,
|
||||
.top-leave-active,
|
||||
.bottom-enter-active,
|
||||
.bottom-leave-active {
|
||||
will-change: transform;
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
|
||||
.top-enter-from,
|
||||
.top-leave-to {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.bottom-enter-from,
|
||||
.bottom-leave-to {
|
||||
transform: translateY(200%);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,538 @@
|
||||
<template>
|
||||
<div class="detail-wrap overflow-y-auto" @scroll="onScroll">
|
||||
<template v-if="state.info.id">
|
||||
<div class="header flex-center-between" :class="{ header_bg: scrollTop > 100 }">
|
||||
<SvgIcon name="back1" class="back" @click="$router.go(-1)" />
|
||||
<!-- 详情 -->
|
||||
<div class="collect" :class="{ collected: state.info.mediaStatus.hasCollected }" @click="onToogleCollected">
|
||||
<SvgIcon name="acg-collect-star-text-0" class="svg-icon" v-if="!state.info.mediaStatus.hasCollected" />
|
||||
<SvgIcon name="acg-collect-star-text-1" class="svg-icon" v-else style="color: #f52c56" />
|
||||
</div>
|
||||
</div>
|
||||
<lazyImg class="detail_image_bg" :src="state.info.verticalCover || state.info.horizontalCover">
|
||||
<div class="detail_box">
|
||||
<div class="cover-box flex-vcenter">
|
||||
<lazyImg class="cover" :src="state.info.verticalCover || state.info.horizontalCover"></lazyImg>
|
||||
<div class="content">
|
||||
<h1 class="title ellipsis">{{ state.info.title }}</h1>
|
||||
<div class="actress flex-vcenter" v-if="state.info.actress">
|
||||
<lazyImg class="favicon" :src="state.info.actress.portrait"></lazyImg>
|
||||
{{ state.info.actress.name }}
|
||||
</div>
|
||||
<div class="episode">
|
||||
共{{ state.info.totalEpisode || state.info.currentEpisode }}话
|
||||
<span class="episode_line">/</span>
|
||||
<span :class="{ episode_status1: statusText == '连载中' }">{{ statusText }}</span>
|
||||
</div>
|
||||
|
||||
<div class="tags">
|
||||
<span
|
||||
class="item"
|
||||
v-for="item in state.info.tagDetails || []"
|
||||
:key="item.id"
|
||||
@click="goAcgTagPage(item.id, item.name, 'text')"
|
||||
>
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- <div class="summary ellipsis2">{{ state.info.summary }}</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</lazyImg>
|
||||
|
||||
<div class="episode-box">
|
||||
<Summary v-if="state.info.summary" class="summary" :text="state.info.summary"></Summary>
|
||||
<div class="title">
|
||||
共{{ state.info.totalEpisode }}章
|
||||
<div class="flex-vcenter" @click="visibleCatalogue = true">
|
||||
目录
|
||||
<SvgIcon class="icon" name="rightArrow"></SvgIcon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list">
|
||||
<div
|
||||
class="item flex-vcenter"
|
||||
:class="{ active: item.episodeNumber === childState.episodeNumber }"
|
||||
v-for="item in childState.list.slice(0, 4)"
|
||||
:key="item.id"
|
||||
@click="openWatch(item.episodeNumber)"
|
||||
>
|
||||
<span class="name">第{{ item.episodeNumber }}章</span>
|
||||
<SvgIcon
|
||||
name="moreIcon"
|
||||
class="back"
|
||||
:class="{ active: item.episodeNumber === childState.episodeNumber }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<RecommendList
|
||||
:media-id="state.info.id"
|
||||
:contentType="state.info.mediaType"
|
||||
title="相关推荐"
|
||||
:margin-hide="true"
|
||||
></RecommendList>
|
||||
</div>
|
||||
<!-- <AdvSwiper v-if="advList.length" class="adv" :list="advList" /> -->
|
||||
<div class="read-box">
|
||||
<div class="btn" @click="openWatch(currentEpisode)">
|
||||
{{ currentEpisodeName }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<Loading v-else />
|
||||
<van-popup v-model:show="visibleCatalogue" z-index="1000" position="bottom" round :style="{ width: '100%' }">
|
||||
<div class="play-list-box">
|
||||
<div class="line" />
|
||||
<div class="title">
|
||||
{{ state.info.title }}
|
||||
<div class="sort" @click="sort = !sort">
|
||||
<span :class="{ active: sort }">正序</span>
|
||||
/
|
||||
<span :class="{ active: !sort }">倒序</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list overflow-y-auto">
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
|
||||
<div
|
||||
:id="`section_${item.episodeNumber}`"
|
||||
class="item flex-vcenter"
|
||||
:class="{ active: item.episodeNumber === childState.episodeNumber }"
|
||||
v-for="item in autoList"
|
||||
:key="item.id"
|
||||
@click="openWatch(item.episodeNumber)"
|
||||
>
|
||||
<span class="name">第{{ item.episodeNumber }}章</span>
|
||||
<SvgIcon name="moreIcon" class="back" />
|
||||
</div>
|
||||
</PullRefresh>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
<PayForWatch
|
||||
v-if="childState.validateItem && visiblePayForWatch"
|
||||
ref="dvPayForWatch"
|
||||
:isOpen="visiblePayForWatch"
|
||||
:info="childState.validateItem"
|
||||
@close="visiblePayForWatch = false"
|
||||
@buySuccess="beforeBuySuccess"
|
||||
></PayForWatch>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AcgTextDetail',
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, provide, watch } from 'vue';
|
||||
import { watchCount, dateFormatYyyyMmDd } from '@/utils/index';
|
||||
import MediaInfo from '../utils/mediaInfo';
|
||||
// import { AdType } from '@/enums/app';
|
||||
// import { getAdItem } from '@/utils/getConfig';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { handleCollected, Collected_emit } from '../utils/collect';
|
||||
import RecommendList from '../components/recommendList.vue';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import mediaContentInfo from '../utils/mediaContentInfo';
|
||||
import PayForWatch from '../components/pay/PayForWatch.vue';
|
||||
import { Pay_parent_on, Pay_child_on, Pay_emit } from '../utils/pay-mitt';
|
||||
import type { PayInfo } from '../utils/pay-mitt';
|
||||
import { goAcgTagPage } from '../utils/common';
|
||||
import Summary from '../components/summary.vue';
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// const advList = ref(getAdItem(AdType.VIDEOSWIPER));
|
||||
|
||||
const { state, historyEpisodeNumber, historyData, addHistory, statusText } = MediaInfo({
|
||||
dbType: HistoryVideoType.AcgText,
|
||||
});
|
||||
Pay_parent_on({ id: route.query.id as string, state, historyData, addHistory });
|
||||
provide('historyData', historyData);
|
||||
const {
|
||||
state: childState,
|
||||
autoList,
|
||||
visibleCatalogue,
|
||||
moreParams,
|
||||
sort,
|
||||
onLoad,
|
||||
onRefresh,
|
||||
visiblePayForWatch,
|
||||
validateHasBuy,
|
||||
} = mediaContentInfo({ dbType: HistoryVideoType.AcgText, isGetHistoryInit: true, isGetInfo: false });
|
||||
|
||||
Pay_child_on(childState.id, childState);
|
||||
|
||||
const currentEpisode = computed(() => {
|
||||
if (historyEpisodeNumber.value === 0) return 0;
|
||||
return historyEpisodeNumber.value;
|
||||
});
|
||||
watch(currentEpisode, () => {
|
||||
childState.episodeNumber = currentEpisode.value;
|
||||
});
|
||||
const currentEpisodeName = computed(() => {
|
||||
if (currentEpisode.value === 0) return '开始阅读';
|
||||
let prefix = '';
|
||||
|
||||
if (state.episodeNumber > historyEpisodeNumber.value) {
|
||||
prefix = '观看';
|
||||
} else {
|
||||
prefix = '续看';
|
||||
}
|
||||
return prefix + currentEpisode.value.toString().padStart(3, '0') + '话';
|
||||
});
|
||||
// 收藏
|
||||
const onToogleCollected = async () => {
|
||||
try {
|
||||
const {
|
||||
id,
|
||||
mediaStatus: { hasCollected },
|
||||
} = state.info;
|
||||
const tempIsCollect = !hasCollected;
|
||||
handleCollected(id, tempIsCollect).then(() => {
|
||||
state.info.mediaStatus.hasCollected = tempIsCollect;
|
||||
Collected_emit(id, tempIsCollect);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
let tempEpisodeNumber = 0;
|
||||
const openWatch = (num: number) => {
|
||||
num = num === 0 ? 1 : num;
|
||||
tempEpisodeNumber = num;
|
||||
const item = childState.list.find((item) => item.episodeNumber === num);
|
||||
if (!item) {
|
||||
goWatch(num);
|
||||
return;
|
||||
}
|
||||
if (validateHasBuy(item)) return;
|
||||
goWatch(num);
|
||||
};
|
||||
|
||||
const goWatch = (num = 0) => {
|
||||
// mediaSubType: 0 | 1; //0:文本 1:有声
|
||||
const { mediaSubType } = state.info;
|
||||
router.push({
|
||||
name: `AcgTextDetailWatch${mediaSubType}`,
|
||||
query: {
|
||||
id: route.query.id,
|
||||
episodeNumber: num || historyEpisodeNumber.value || 1,
|
||||
title: state.info.title,
|
||||
},
|
||||
});
|
||||
|
||||
visibleCatalogue.value = false;
|
||||
};
|
||||
|
||||
const beforeBuySuccess = (obj: PayInfo) => {
|
||||
Pay_emit(historyData.value.id, obj);
|
||||
if (obj.type === 'all') {
|
||||
state.info.mediaStatus.hasPaid = true;
|
||||
historyData.value.mediaStatus.hasPaid = true;
|
||||
addHistory();
|
||||
childState.list.forEach((item) => {
|
||||
item.hasBuy = true;
|
||||
});
|
||||
}
|
||||
|
||||
buySuccess(obj);
|
||||
};
|
||||
const buySuccess = ({ type }: PayInfo) => {
|
||||
if (type === 'one') {
|
||||
const item = (childState.list as Types.Acg.MediaContentListItem[]).find(
|
||||
(item) => item.episodeNumber === tempEpisodeNumber,
|
||||
);
|
||||
if (item) {
|
||||
item.hasBuy = true;
|
||||
}
|
||||
}
|
||||
|
||||
visiblePayForWatch.value = false;
|
||||
goWatch(tempEpisodeNumber);
|
||||
};
|
||||
|
||||
const scrollTop = ref(0);
|
||||
const onScroll = (e: Event) => {
|
||||
scrollTop.value = (e.target as HTMLElement).scrollTop;
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
@import '../assets/style.scss';
|
||||
.detail-wrap {
|
||||
height: 100%;
|
||||
// background: url('@/assets/images/png/acg/text-bg.png') #11111a no-repeat;
|
||||
background: #000;
|
||||
.header {
|
||||
padding: 0 16px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
z-index: 10;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
.back {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
color: #fff;
|
||||
}
|
||||
.collect {
|
||||
svg {
|
||||
width: 62px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.header_bg {
|
||||
background: #000;
|
||||
}
|
||||
.detail_image_bg {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
height: 280px;
|
||||
overflow: hidden;
|
||||
:deep(img) {
|
||||
object-position: top;
|
||||
filter: brightness(0.5);
|
||||
}
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.45) 0%, rgba(0, 0, 0, 0.78) 65%, #000 100%);
|
||||
}
|
||||
.detail_box {
|
||||
padding: 60px 16px 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
.cover-box {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
.cover {
|
||||
flex-shrink: 0;
|
||||
width: 111px;
|
||||
height: 146px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
.title {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
}
|
||||
.actress {
|
||||
margin: 6px 0;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
.favicon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.episode {
|
||||
color: #989898;
|
||||
font-size: 12px;
|
||||
line-height: 18px; /* 150% */
|
||||
margin-top: 12px;
|
||||
.episode_status1 {
|
||||
color: $themeColor;
|
||||
}
|
||||
.episode_line {
|
||||
margin: 0 8px 0 6px;
|
||||
}
|
||||
}
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 10px;
|
||||
margin: 10px 0 12px;
|
||||
.item {
|
||||
flex-shrink: 0;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
background: #22252d;
|
||||
color: #fff;
|
||||
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.adv {
|
||||
height: 75px;
|
||||
:deep(.van-swipe-item) {
|
||||
height: 75px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.read-box {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 68px;
|
||||
//background: linear-gradient(180deg, rgba(34, 35, 50, 0) 0%, #222332 100%);
|
||||
@include SafeArea('padding', 'bottom', 21px);
|
||||
.btn {
|
||||
margin: 0 auto;
|
||||
width: 343px;
|
||||
padding: 11px 0;
|
||||
border-radius: 3px;
|
||||
background: #f68804;
|
||||
color: var(---f, #fff);
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
.episode-box {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
padding: 0 16px;
|
||||
margin-top: -72px;
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
> div {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
> svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.item {
|
||||
padding: 12px 0;
|
||||
justify-content: space-between;
|
||||
color: #989898;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
&.active {
|
||||
color: #f68804;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
flex: 1;
|
||||
@include textEllipsis();
|
||||
}
|
||||
.back {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
// transform: rotate(180deg);
|
||||
flex-shrink: 1;
|
||||
color: #fff;
|
||||
&.active {
|
||||
color: #f68804;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.play-list-box {
|
||||
width: 375px;
|
||||
height: 400px;
|
||||
border-radius: 18px 18px 0px 0px;
|
||||
background: #0f0f0f;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
padding: 13px 16px 0;
|
||||
@include SafeArea('padding', 'bottom', 18px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.line {
|
||||
width: 36px;
|
||||
height: 5px;
|
||||
border-radius: 99px;
|
||||
background: #989898;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.sort {
|
||||
color: #747b80;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
.active {
|
||||
color: #f68804;
|
||||
}
|
||||
}
|
||||
}
|
||||
.list {
|
||||
flex: 1;
|
||||
.item {
|
||||
padding: 18px 0;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
&.active {
|
||||
color: #f68804;
|
||||
}
|
||||
.name {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
flex: 1;
|
||||
@include textEllipsis();
|
||||
}
|
||||
.back {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,358 @@
|
||||
<template>
|
||||
<div class="text-wrap" :class="theme">
|
||||
<Transition name="top">
|
||||
<Header v-show="visibleAction" class="header" :title="currentItem.id ? currentItem.name : ''">
|
||||
<template v-slot:right>
|
||||
<div class="right">
|
||||
<SvgIcon v-if="theme === ThemeMode.dark" name="theme-mode-light" @click="switchTheme" />
|
||||
<SvgIcon v-else name="theme-mode-dark" @click="switchTheme" />
|
||||
<SvgIcon name="acg_catalogue" @click="visibleCatalogue = true" style="margin-left: 12px" />
|
||||
</div>
|
||||
</template>
|
||||
</Header>
|
||||
</Transition>
|
||||
<Transition name="top">
|
||||
<Header
|
||||
v-show="!visibleAction"
|
||||
class="header header2"
|
||||
leftIcon=""
|
||||
:title="currentItem.id ? `${state.title} / ${currentItem.name}` : ''"
|
||||
/>
|
||||
</Transition>
|
||||
<template v-if="state.info.id">
|
||||
<div
|
||||
ref="rootEl"
|
||||
class="content overflow-y-auto"
|
||||
v-html="autoText"
|
||||
@scroll="onScroll"
|
||||
@click="visibleAction = !visibleAction"
|
||||
></div>
|
||||
<Transition name="bottom">
|
||||
<div v-show="visibleAction" class="action">
|
||||
<div class="prev" @click="switchEpisodeNumber(-1)">
|
||||
<SvgIcon class="icon" name="acg_text_prev" />
|
||||
上一章
|
||||
</div>
|
||||
<div class="next" @click="switchEpisodeNumber(1)">
|
||||
下一章
|
||||
<SvgIcon class="icon" name="acg_text_prev" />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
<Loading v-else></Loading>
|
||||
</div>
|
||||
<van-popup
|
||||
v-model:show="visibleCatalogue"
|
||||
z-index="1000"
|
||||
position="right"
|
||||
:style="{ height: '100%', width: '210px' }"
|
||||
>
|
||||
<div class="play-list-box">
|
||||
<div class="title">{{ state.title }}</div>
|
||||
<div class="sort">
|
||||
<span>共{{ historyData.totalEpisode }}章</span>
|
||||
<span class="status">{{ statusText }}</span>
|
||||
<span class="btn" @click="sort = !sort">
|
||||
<SvgIcon v-show="sort" class="section" name="acg_sort_0" />
|
||||
<SvgIcon v-show="!sort" class="section" name="acg_sort_1" />
|
||||
<span>{{ sort ? '正序' : '倒序' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="list overflow-y-auto">
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
|
||||
<div
|
||||
:id="`section_${item.episodeNumber}`"
|
||||
class="item"
|
||||
:class="{ active: item.episodeNumber === state.episodeNumber }"
|
||||
v-for="item in autoList"
|
||||
:key="item.id"
|
||||
@click="switchEpisodeNumberCatalogue(item)"
|
||||
>
|
||||
{{ item.episodeNumber }}.{{ item.name }}
|
||||
</div>
|
||||
</PullRefresh>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
<PayForWatch
|
||||
v-if="state.info.id"
|
||||
ref="dvPayForWatch"
|
||||
:isOpen="visiblePayForWatch"
|
||||
:info="state.validateItem.id ? state.validateItem : state.info"
|
||||
@close="closePayForWatch"
|
||||
@buySuccess="beforeBuySuccess"
|
||||
></PayForWatch>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AcgTextDetailWatch0',
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, provide, watch } from 'vue';
|
||||
import mediaContentInfo from '../utils/mediaContentInfo';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import PayForWatch from '../components/pay/PayForWatch.vue';
|
||||
import { Pay_emit } from '../utils/pay-mitt';
|
||||
import type { PayInfo } from '../utils/pay-mitt';
|
||||
enum ThemeMode {
|
||||
light = 'light',
|
||||
dark = 'dark',
|
||||
}
|
||||
const theme = ref(localStorage.getItem('theme') || ThemeMode.light);
|
||||
|
||||
const {
|
||||
state,
|
||||
statusText,
|
||||
moreParams,
|
||||
onLoad,
|
||||
onRefresh,
|
||||
sort,
|
||||
autoList,
|
||||
visibleCatalogue,
|
||||
historyData,
|
||||
visiblePayForWatch,
|
||||
closePayForWatch,
|
||||
buySuccess,
|
||||
switchEpisodeNumberCatalogue,
|
||||
switchEpisodeNumber,
|
||||
currentItem,
|
||||
addHistoryReadData,
|
||||
} = mediaContentInfo({ dbType: HistoryVideoType.AcgText });
|
||||
provide('historyData', historyData);
|
||||
const autoText = computed(() => {
|
||||
return (state.info.text || '').replace(/\n/g, '<br>');
|
||||
});
|
||||
const rootEl = ref();
|
||||
watch(currentItem, () => {
|
||||
if (!rootEl.value) return;
|
||||
const list = historyData.value.historyReadData || [];
|
||||
const item = list.find((i) => i.episodeNumber === state.episodeNumber) || {};
|
||||
const progress = Number(item.progress);
|
||||
rootEl.value.scrollTop = item.scrollTop || 0;
|
||||
addHistoryReadData(state.episodeNumber, {
|
||||
progress: Number.isFinite(progress) ? progress : 0,
|
||||
scrollTop: item.scrollTop || 0,
|
||||
});
|
||||
});
|
||||
|
||||
const visibleAction = ref(true);
|
||||
let scrollTop = 0;
|
||||
const onScroll = (e: Event) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const currentTop = target.scrollTop;
|
||||
const maxScrollTop = target.scrollHeight - target.offsetHeight;
|
||||
const progress = maxScrollTop <= 0 ? 100 : Math.max(1, Math.min(100, Math.ceil((currentTop / maxScrollTop) * 100)));
|
||||
addHistoryReadData(state.episodeNumber, {
|
||||
progress,
|
||||
scrollTop: currentTop,
|
||||
});
|
||||
|
||||
if (currentTop - scrollTop > 20) {
|
||||
visibleAction.value = false;
|
||||
scrollTop = currentTop;
|
||||
return;
|
||||
}
|
||||
if (currentTop - scrollTop < -20) {
|
||||
visibleAction.value = true;
|
||||
scrollTop = currentTop;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const switchTheme = () => {
|
||||
theme.value = theme.value === ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
|
||||
};
|
||||
const beforeBuySuccess = (obj: PayInfo) => {
|
||||
Pay_emit(historyData.value.id, obj);
|
||||
buySuccess(obj.type);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
.text-wrap {
|
||||
padding: 18px 0;
|
||||
&.light {
|
||||
--bg-color: #fff;
|
||||
--icon-color: #000;
|
||||
--text-header-color: rgba(0, 0, 0, 0.9);
|
||||
--text-color: #333;
|
||||
}
|
||||
&.dark {
|
||||
--bg-color: #030f18;
|
||||
--icon-color: #fff;
|
||||
--text-header-color: #fff;
|
||||
--text-color: #ffffffe5;
|
||||
}
|
||||
height: 100%;
|
||||
background-color: var(--bg-color);
|
||||
.header {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
background-color: var(--bg-color) !important;
|
||||
color: var(--text-header-color);
|
||||
:deep(.svg-icon) {
|
||||
color: var(--icon-color);
|
||||
}
|
||||
:deep(.title) {
|
||||
color: var(--icon-color);
|
||||
left: 44px;
|
||||
transform: none;
|
||||
}
|
||||
.right {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 10px;
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.header2 {
|
||||
:deep(.title) {
|
||||
color: #525252;
|
||||
left: 16px;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
height: 100%;
|
||||
padding: 44px 12px 110px;
|
||||
color: var(--text-color);
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
.action {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
width: 375px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 10px;
|
||||
@include SafeArea('padding', 'bottom', 12px);
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-header-color);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
> div {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-header-color);
|
||||
font-size: 16px;
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&.next .icon {
|
||||
transform: rotate(180deg);
|
||||
margin-right: 0;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.top-enter-active,
|
||||
.top-leave-active,
|
||||
.bottom-enter-active,
|
||||
.bottom-leave-active {
|
||||
will-change: transform;
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
|
||||
.top-enter-from,
|
||||
.top-leave-to {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.bottom-enter-from,
|
||||
.bottom-leave-to {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.play-list-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #0f0f0f;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
|
||||
.title {
|
||||
flex-shrink: 0;
|
||||
width: inherit;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.sort {
|
||||
flex-shrink: 0;
|
||||
width: inherit;
|
||||
height: 24px;
|
||||
margin: 0 0 12px;
|
||||
padding: 2px 16px;
|
||||
background: #f68804;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
.status {
|
||||
flex: 1;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.section {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
flex: 1;
|
||||
.item {
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
padding: 0 16px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
@include textEllipsis();
|
||||
&.active {
|
||||
color: #f68804;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,626 @@
|
||||
<template>
|
||||
<div class="audio-wrap overflow-y-auto">
|
||||
<div class="page-nav" v-if="state.info.id">
|
||||
<SvgIcon name="back_white" class="back" @click="$router.go(-1)" />
|
||||
{{ state.title }}
|
||||
<span></span>
|
||||
</div>
|
||||
<div v-else class="page-nav">
|
||||
<SvgIcon name="back_white" class="back" @click="$router.go(-1)" />
|
||||
</div>
|
||||
<div class="content-wrap">
|
||||
<template v-if="state.info.id">
|
||||
<div class="cd flex-center-center">
|
||||
<audio
|
||||
ref="audio"
|
||||
@timeupdate="onTimeUpdate"
|
||||
@loadedmetadata="onLoadedmetadata"
|
||||
@ended="onEnded"
|
||||
style="display: none"
|
||||
></audio>
|
||||
<lazyImg class="cover" :src="currentItem.cover || historyData.verticalCover" />
|
||||
</div>
|
||||
<div class="content">正在播放:{{ state.info.name }}</div>
|
||||
<div class="bar flex-center-between" @touchstart.stop @touchmove.stop @touchend.stop>
|
||||
<SvgIcon class="icon" name="acg_audio_time_seek-10" @click="upadteProgress(-10)"></SvgIcon>
|
||||
<van-slider
|
||||
class="progress"
|
||||
v-model="currentProgress"
|
||||
@change="onChange"
|
||||
@drag-start="onDragStart"
|
||||
@drag-end="onDragEnd"
|
||||
/>
|
||||
<SvgIcon class="icon" name="acg_audio_time_seek+10" @click="upadteProgress(10)"></SvgIcon>
|
||||
</div>
|
||||
<div class="time-box flex-center-between">
|
||||
<span>{{ currentPlayTime }}</span>
|
||||
<span>{{ duration }}</span>
|
||||
</div>
|
||||
<div class="action">
|
||||
<div class="item">
|
||||
<SvgIcon :name="playMode" class="mode" @click="onChangePlayMode" />
|
||||
{{ playModeText }}
|
||||
</div>
|
||||
<SvgIcon name="acg_audio_action_prev" class="prev" @click="switchEpisodeNumber(-1)" />
|
||||
<SvgIcon v-show="!isPlay" name="acg_audio_action_play" class="play" @click="isPlay = true" />
|
||||
<SvgIcon v-show="isPlay" name="acg_audio_pause" class="play" @click="isPlay = false" />
|
||||
<SvgIcon name="acg_audio_action_prev" class="prev next" @click="switchEpisodeNumber(1)" />
|
||||
<div class="item">
|
||||
<SvgIcon name="acg_catalogue" class="mode" @click="visibleCatalogue = true" />
|
||||
播放列表
|
||||
</div>
|
||||
</div>
|
||||
<div class="split"></div>
|
||||
<RecommendList
|
||||
v-if="mediaId"
|
||||
title="相关推荐"
|
||||
:media-id="mediaId"
|
||||
:contentType="'text'"
|
||||
:marginHide="true"
|
||||
></RecommendList>
|
||||
</template>
|
||||
<Loading v-else />
|
||||
</div>
|
||||
</div>
|
||||
<van-popup v-model:show="visibleCatalogue" z-index="1000" position="bottom" round :style="{ width: '100%' }">
|
||||
<div class="play-list-box">
|
||||
<div class="line" />
|
||||
<div class="title">
|
||||
{{ state.title }}
|
||||
|
||||
<div class="sort" @click="sort = !sort">
|
||||
<span :class="{ active: sort }">正序</span>
|
||||
/
|
||||
<span :class="{ active: !sort }">倒序</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list overflow-y-auto">
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
|
||||
<div
|
||||
:id="`section_${item.episodeNumber}`"
|
||||
class="item flex-vcenter"
|
||||
:class="{ active: item.episodeNumber === state.episodeNumber }"
|
||||
v-for="item in autoList"
|
||||
:key="item.id"
|
||||
@click="switchEpisodeNumberCatalogue(item)"
|
||||
>
|
||||
<span class="name">第{{ item.episodeNumber }}章</span>
|
||||
<!-- <SvgIcon
|
||||
v-if="item.episodeNumber === state.episodeNumber"
|
||||
name="acg_audio_episode_active"
|
||||
class="episode_active"
|
||||
/> -->
|
||||
<SvgIcon name="moreIcon" class="back" />
|
||||
</div>
|
||||
</PullRefresh>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<PayForWatch
|
||||
v-if="state.info.id"
|
||||
ref="dvPayForWatch"
|
||||
:isOpen="visiblePayForWatch"
|
||||
:info="state.validateItem.id ? state.validateItem : state.info"
|
||||
@close="visiblePayForWatch = false"
|
||||
@buySuccess="beforeBuySuccess"
|
||||
></PayForWatch>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AcgTextDetailWatch1',
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed, nextTick, onUnmounted, provide, onActivated, onDeactivated } from 'vue';
|
||||
// import PopupList from './components/popup-list.vue';
|
||||
import { videoTime } from '@/utils/index';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
// import PayForWatch from '../components/PayForWatch.vue';
|
||||
import mediaContentInfo from '../utils/mediaContentInfo';
|
||||
import { showToast } from 'vant';
|
||||
import { useRoute } from 'vue-router';
|
||||
import RecommendList from '../components/recommendList.vue';
|
||||
import PayForWatch from '../components/pay/PayForWatch.vue';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import { handleCollected } from '../utils/collect';
|
||||
import { Pay_emit, Pay_child_on } from '../utils/pay-mitt';
|
||||
import type { PayInfo } from '../utils/pay-mitt';
|
||||
|
||||
const route = useRoute();
|
||||
const mediaId = (route.query.id || '') as string;
|
||||
|
||||
const {
|
||||
state,
|
||||
currentItem,
|
||||
autoList,
|
||||
visibleCatalogue,
|
||||
moreParams,
|
||||
onLoad,
|
||||
onRefresh,
|
||||
visiblePayForWatch,
|
||||
buySuccess,
|
||||
switchEpisodeNumberCatalogue,
|
||||
switchEpisodeNumber,
|
||||
validateHasBuy,
|
||||
historyData,
|
||||
addHistory,
|
||||
addHistoryReadData,
|
||||
sort,
|
||||
} = mediaContentInfo({ dbType: HistoryVideoType.AcgText, isGetHistoryInit: true });
|
||||
provide('historyData', historyData);
|
||||
|
||||
Pay_child_on(state.id, state);
|
||||
|
||||
const isPlay = ref(false);
|
||||
const audio = ref();
|
||||
const currentProgress = ref(0);
|
||||
const audioTotalTime = ref(0);
|
||||
const audioCurrentTime = ref(0);
|
||||
const initPlay = ref(false);
|
||||
|
||||
enum PlayMode {
|
||||
listCycle = 'acg_audio_action_mode1',
|
||||
singleCycle = 'acg_audio_action_mode2',
|
||||
}
|
||||
|
||||
enum PlayModeText {
|
||||
listCycle = '多集循环',
|
||||
singleCycle = '单集循环',
|
||||
}
|
||||
|
||||
const playMode = ref(localStorage.getItem('PlayMode') || PlayMode.listCycle);
|
||||
const playModeText = computed(() => {
|
||||
if (playMode.value === PlayMode.listCycle) {
|
||||
return PlayModeText.listCycle;
|
||||
} else {
|
||||
return PlayModeText.singleCycle;
|
||||
}
|
||||
});
|
||||
const onChangePlayMode = () => {
|
||||
if (playMode.value === PlayMode.listCycle) {
|
||||
showToast(PlayModeText.singleCycle);
|
||||
playMode.value = PlayMode.singleCycle;
|
||||
} else {
|
||||
showToast(PlayModeText.listCycle);
|
||||
playMode.value = PlayMode.listCycle;
|
||||
}
|
||||
};
|
||||
|
||||
const currentUrl = ref('');
|
||||
watch(
|
||||
() => state.info.audioUrl,
|
||||
(url) => {
|
||||
currentUrl.value = url;
|
||||
},
|
||||
);
|
||||
watch(currentUrl, () => {
|
||||
if (!currentUrl.value) return '';
|
||||
|
||||
// const url = `${baseUrl + currentUrl.value}?token=${useUserStore().token}&c=${globalStore.videoRoadLine.url}`;
|
||||
const url = useConfigStore().sourceInfo.AUDIO + '/' + currentUrl.value;
|
||||
nextTick(() => {
|
||||
audio.value.src = url;
|
||||
});
|
||||
});
|
||||
|
||||
watch(isPlay, (bool) => {
|
||||
if (bool && !initPlay.value) {
|
||||
initPlay.value = true;
|
||||
}
|
||||
if (bool) audio.value.play();
|
||||
else audio.value.pause();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => state.episodeNumber,
|
||||
() => {
|
||||
currentProgress.value = 0;
|
||||
audioCurrentTime.value = 0;
|
||||
|
||||
if (audio.value) {
|
||||
audio.value.currentTime = 0;
|
||||
}
|
||||
|
||||
addHistoryReadData(state.episodeNumber, {
|
||||
progress: 0,
|
||||
currentTime: 0,
|
||||
});
|
||||
addHistory(state.episodeNumber);
|
||||
},
|
||||
);
|
||||
|
||||
const onLoadedmetadata = () => {
|
||||
if (audio.value === null) return;
|
||||
audioTotalTime.value = audio.value.duration;
|
||||
const list = historyData.value.historyReadData || [];
|
||||
const item = list.find((i) => i.episodeNumber === state.episodeNumber) || {};
|
||||
const savedProgress = Number(item.progress || 0);
|
||||
if (audioTotalTime.value > 0 && savedProgress > 0 && savedProgress < 100) {
|
||||
const restoreTime = (savedProgress / 100) * audioTotalTime.value;
|
||||
audio.value.currentTime = restoreTime;
|
||||
audioCurrentTime.value = restoreTime;
|
||||
currentProgress.value = savedProgress;
|
||||
} else if (savedProgress >= 100) {
|
||||
currentProgress.value = 0;
|
||||
audioCurrentTime.value = 0;
|
||||
} else {
|
||||
currentProgress.value = 0;
|
||||
audioCurrentTime.value = 0;
|
||||
}
|
||||
try {
|
||||
audio.value.play();
|
||||
isPlay.value = !audio.value.paused;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
addHistory(state.episodeNumber);
|
||||
};
|
||||
|
||||
const currentPlayTime = computed(() => {
|
||||
return videoTime(parseInt(audioCurrentTime.value.toString()));
|
||||
});
|
||||
const duration = computed(() => {
|
||||
return videoTime(parseInt(audioTotalTime.value.toString()));
|
||||
});
|
||||
|
||||
let isDrag = false;
|
||||
const onDragStart = () => {
|
||||
isDrag = true;
|
||||
};
|
||||
const onDragEnd = () => {
|
||||
isDrag = false;
|
||||
};
|
||||
const onTimeUpdate = () => {
|
||||
if (audio.value === null) return;
|
||||
if (visiblePayForWatch.value) {
|
||||
audio.value.pause();
|
||||
isPlay.value = false;
|
||||
return;
|
||||
}
|
||||
if (validateHasBuy(currentItem.value)) {
|
||||
audio.value.pause();
|
||||
isPlay.value = false;
|
||||
}
|
||||
const { currentTime } = audio.value;
|
||||
const duration = Number(audio.value.duration || 0);
|
||||
if (duration <= 0) return;
|
||||
const progress = Math.max(0, Math.min(100, (currentTime / duration) * 100));
|
||||
if (!isDrag) {
|
||||
currentProgress.value = progress;
|
||||
}
|
||||
audioCurrentTime.value = currentTime;
|
||||
addHistoryReadData(state.episodeNumber, {
|
||||
progress: Math.round(progress),
|
||||
currentTime,
|
||||
});
|
||||
};
|
||||
|
||||
const onEnded = async () => {
|
||||
addHistoryReadData(state.episodeNumber, {
|
||||
progress: 100,
|
||||
currentTime: audio.value?.duration || audioCurrentTime.value,
|
||||
});
|
||||
if (playMode.value === PlayMode.singleCycle) {
|
||||
currentProgress.value = 0;
|
||||
audio.value.play();
|
||||
} else {
|
||||
const bool = await switchEpisodeNumber(1);
|
||||
if (bool) {
|
||||
currentProgress.value = 0;
|
||||
audio.value.play();
|
||||
} else {
|
||||
isPlay.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (progress: number) => {
|
||||
const currentTime = (progress * audio.value.duration) / 100;
|
||||
audio.value.currentTime = currentTime;
|
||||
};
|
||||
|
||||
const upadteProgress = (num: number) => {
|
||||
if (!isPlay.value) return;
|
||||
const currentTime = audio.value.currentTime + num;
|
||||
audio.value.currentTime = currentTime;
|
||||
};
|
||||
|
||||
const beforeBuySuccess = (obj: PayInfo) => {
|
||||
Pay_emit(historyData.value.id, obj);
|
||||
buySuccess(obj.type);
|
||||
if (obj.type === 'all') {
|
||||
historyData.value.mediaStatus.hasPaid = true;
|
||||
addHistory(state.episodeNumber);
|
||||
}
|
||||
};
|
||||
|
||||
// 收藏
|
||||
// const onToogleCollected = async () => {
|
||||
// try {
|
||||
// const {
|
||||
// id,
|
||||
// mediaStatus: { hasCollected },
|
||||
// } = historyData.value;
|
||||
// const tempIsCollect = !hasCollected;
|
||||
// handleCollected(id, tempIsCollect).then(() => {
|
||||
// historyData.value.mediaStatus.hasCollected = tempIsCollect;
|
||||
// // Collected_emit(id, tempIsCollect);
|
||||
// });
|
||||
// } catch (e) {
|
||||
// console.log(e);
|
||||
// }
|
||||
// };
|
||||
|
||||
onUnmounted(() => {
|
||||
localStorage.setItem('PlayMode', playMode.value);
|
||||
});
|
||||
onActivated(() => {
|
||||
const episodeNumber = Number((route.query.episodeNumber || '0') as string);
|
||||
if (episodeNumber && episodeNumber != state.episodeNumber) {
|
||||
state.episodeNumber = episodeNumber;
|
||||
} else {
|
||||
if (audio.value) audio.value.play();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0% {
|
||||
transform-origin: top center;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform-origin: top center;
|
||||
transform: rotate(-10deg);
|
||||
}
|
||||
100% {
|
||||
transform-origin: top center;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
.audio-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
--van-slider-active-background: #e1351f;
|
||||
--van-slider-inactive-background: #3d3d3d;
|
||||
--van-slider-button-width: 12px;
|
||||
--van-slider-button-height: 12px;
|
||||
--van-slider-button-shadow: 0 0 0 4px rgb(245, 44, 86, 61%);
|
||||
.page-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// justify-content: space-between;
|
||||
height: 44px;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
padding: 0 16px;
|
||||
font-weight: 600;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
|
||||
background: #000;
|
||||
|
||||
.back {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.more {
|
||||
width: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.content-wrap {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 0 16px;
|
||||
height: 100%;
|
||||
@include SafeArea('padding', 'bottom', 44px);
|
||||
.cd {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
height: 188px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
margin-top: 28px;
|
||||
position: relative;
|
||||
.cover {
|
||||
width: 111px;
|
||||
height: 148px;
|
||||
border-radius: 9px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
$margin: 0px;
|
||||
.content {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 18px $margin;
|
||||
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
.bar {
|
||||
flex-shrink: 0;
|
||||
width: calc(100% - $margin * 2);
|
||||
height: 24px;
|
||||
margin: 0 $margin 10px;
|
||||
position: relative;
|
||||
|
||||
.progress {
|
||||
width: 251px;
|
||||
height: 2px;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
:deep(.van-slider__bar) {
|
||||
background: #f68804;
|
||||
}
|
||||
:deep(.van-slider__button) {
|
||||
box-shadow: 0 0 0 3px rgba(246, 136, 4, 0.7);
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.time {
|
||||
flex-shrink: 0;
|
||||
width: 50px;
|
||||
color: #fff;
|
||||
font-size: 9px;
|
||||
|
||||
margin-right: 0;
|
||||
margin-left: 6px;
|
||||
text-align: right;
|
||||
|
||||
&.current {
|
||||
margin-right: 6px;
|
||||
margin-left: 0;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
.time-box {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin: 10px 0 18px;
|
||||
}
|
||||
.action {
|
||||
flex-shrink: 0;
|
||||
width: calc(100% - $margin * 2);
|
||||
height: 58px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 0 auto;
|
||||
.item {
|
||||
// width: 68px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
.mode {
|
||||
width: 29px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.prev,
|
||||
.next {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
|
||||
&.next {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
.play {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.split {
|
||||
display: flex;
|
||||
width: 363px;
|
||||
height: 1px;
|
||||
margin: 18px auto;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.play-list-box {
|
||||
width: 375px;
|
||||
height: 400px;
|
||||
border-radius: 18px 18px 0px 0px;
|
||||
background: #0f0f0f;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
padding: 13px 16px 0;
|
||||
@include SafeArea('padding', 'bottom', 18px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.line {
|
||||
width: 36px;
|
||||
height: 5px;
|
||||
border-radius: 99px;
|
||||
background: #989898;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.sort {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
.active {
|
||||
color: #f68804;
|
||||
}
|
||||
}
|
||||
}
|
||||
.list {
|
||||
flex: 1;
|
||||
.item {
|
||||
padding: 18px 0;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #ffffff1a;
|
||||
&.active {
|
||||
color: #f68804;
|
||||
}
|
||||
.name {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
flex: 1;
|
||||
@include textEllipsis();
|
||||
}
|
||||
.back {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,411 @@
|
||||
<template>
|
||||
<div class="introduction-wrap overflow-y-auto">
|
||||
<div class="titleBox ellipsis2">
|
||||
{{ props.info.title }}
|
||||
</div>
|
||||
<div class="tags">
|
||||
<span
|
||||
class="tag"
|
||||
@click="goAcgTagPage(item.id, item.name, 'video')"
|
||||
v-for="item in info.tagDetails || []"
|
||||
:key="item.id"
|
||||
>
|
||||
#{{ item.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="videoInfoBox flex-vcenter">
|
||||
<div class="item margin-r-88">
|
||||
<span>{{ watchCount(props.info.countBrowse) }} 观看量</span>
|
||||
</div>
|
||||
<div class="item" @click="onToogleLiked">
|
||||
<SvgIcon v-if="props.info.mediaStatus.hasLiked" name="acg-like-1" />
|
||||
<SvgIcon v-else name="acg-like-0" />
|
||||
<span>{{ watchCount(props.info.countLike) || '点赞' }}</span>
|
||||
</div>
|
||||
<div class="item" @click="onToogleCollected">
|
||||
<SvgIcon name="acg-collect-star-1" v-if="props.info.mediaStatus.hasCollected" style="color: #f52c56"></SvgIcon>
|
||||
<SvgIcon name="acg-collect-star-0" v-else></SvgIcon>
|
||||
<span>{{ watchCount(props.info.countCollect) || '收藏' }}</span>
|
||||
</div>
|
||||
<div class="item" @click="openShare">
|
||||
<SvgIcon name="acg-share" />
|
||||
<span>分享</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="actress-box flex-center-between"
|
||||
v-if="props.info.actress && props.info.actress.id"
|
||||
@click="
|
||||
$router.push({
|
||||
name: 'AcgActress',
|
||||
query: {
|
||||
id: props.info.actress.id,
|
||||
},
|
||||
})
|
||||
"
|
||||
>
|
||||
<lazyImg class="portrait" :src="props.info.actress.portrait"></lazyImg>
|
||||
<div class="info">
|
||||
<p class="name">{{ props.info.actress.name }}</p>
|
||||
<div class="count-box flex-vcenter">
|
||||
<SvgIcon name="acg-video-icon-a-video" />
|
||||
{{ watchCount(props.info.actress.acgCount) }}部
|
||||
<SvgIcon name="acg-video-icon-a-collect" style="margin-left: 12px" />
|
||||
{{ watchCount(props.info.actress.collectCount) }}
|
||||
</div>
|
||||
</div>
|
||||
<SvgIcon class="icon" name="moreIcon" />
|
||||
</div>
|
||||
<!-- 选集区域 -->
|
||||
<div class="episodeArea">
|
||||
<div class="episodeHeader flex-center-between padding16">
|
||||
<div class="episodeTitle">选集</div>
|
||||
<div class="episodeAll flex-vcenter" @click="visibleCatalogue = true">
|
||||
全{{ props.info.totalEpisode }}话
|
||||
<SvgIcon class="icon" name="moreIcon" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="episodeBox" @touchstart.stop @touchmove.stop @touchend.stop>
|
||||
<Episode
|
||||
class="item"
|
||||
@click="switchEpisodeNumberCatalogue(item)"
|
||||
v-for="item in autoList"
|
||||
:key="item.id"
|
||||
:info="item"
|
||||
:episode-number="state.episodeNumber"
|
||||
></Episode>
|
||||
</div>
|
||||
</div>
|
||||
<HomeHeader :advType="AdType.VIDEOADV" />
|
||||
<!-- <RecommendList
|
||||
:media-id="props.info.id"
|
||||
:contentType="props.info.mediaType"
|
||||
title="猜你喜欢"
|
||||
:marginHide="true"
|
||||
></RecommendList> -->
|
||||
<RecommendedList :type="info.mediaType" :tagId="info.tagDetails?.[0]?.id" />
|
||||
<!-- 分享弹窗 -->
|
||||
<AppSharePopup
|
||||
:is-open="visibleShare"
|
||||
@close="visibleShare = false"
|
||||
coverType="portrait"
|
||||
:cover="props.info.verticalCover || props.info.horizontalCover"
|
||||
/>
|
||||
|
||||
<Teleport to=".video-detail-wrap">
|
||||
<van-popup v-model:show="visibleCatalogue" position="bottom" z-index="1001" :style="{ height: '438px' }">
|
||||
<div class="play-list-box">
|
||||
<div class="line"></div>
|
||||
<div class="title flex-center-between">
|
||||
剧集列表
|
||||
<!-- <span @click="sort = !sort">
|
||||
{{ sort ? '正序' : '倒序' }}
|
||||
</span> -->
|
||||
</div>
|
||||
<div class="grading-group" v-if="gradingGroup.length > 1">
|
||||
<div
|
||||
class="grading-group-item"
|
||||
v-for="(item, index) in gradingGroup"
|
||||
:key="index"
|
||||
@click.stop="state.gradingGroupIndex = index"
|
||||
>
|
||||
{{ item[0].episodeNumber }}-{{ item[item.length - 1].episodeNumber }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-box overflow-y-auto">
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
|
||||
<div class="list">
|
||||
<Episode
|
||||
class="item"
|
||||
:episodeNumber="state.episodeNumber"
|
||||
:info="item"
|
||||
v-for="item in gradingGroup[state.gradingGroupIndex]"
|
||||
:key="item.id"
|
||||
@click="state.episodeNumber = item.episodeNumber"
|
||||
></Episode>
|
||||
</div>
|
||||
</PullRefresh>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed, watchEffect, Ref } from 'vue';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import { goAcgTagPage } from '../../utils/common';
|
||||
import { watchCount, videoTime } from '@/utils/index';
|
||||
// import RecommendList from '../../components/recommendList.vue';
|
||||
import { handleCollected, Collected_emit } from '../../utils/collect';
|
||||
import { AdType } from '@/enums/app';
|
||||
import mediaContentInfo from '../../utils/mediaContentInfo';
|
||||
import CommunityApi from '@/service/community';
|
||||
import { useGlobalStore } from '@/stores/global';
|
||||
import Episode from '../../components/episode.vue';
|
||||
const emits = defineEmits(['updateLiked', 'updateCollected', 'updateItem', 'openRoadLine']);
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
info: Types.Media.MediaInfo;
|
||||
}>(),
|
||||
{
|
||||
info: {},
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
state,
|
||||
currentItem,
|
||||
autoList,
|
||||
// switchEpisodeNumberCatalogue,
|
||||
visibleCatalogue,
|
||||
sort,
|
||||
moreParams,
|
||||
onLoad,
|
||||
onRefresh,
|
||||
getList,
|
||||
buySuccess,
|
||||
replaceRouter,
|
||||
} = mediaContentInfo({
|
||||
isGetInfo: false,
|
||||
isGetHistoryInit: true,
|
||||
pageSize: 30,
|
||||
dbType: HistoryVideoType.AcgVideo,
|
||||
isScrollTop: false,
|
||||
});
|
||||
const gradingGroup = computed(() => {
|
||||
let arr = autoList.value || [];
|
||||
if (arr.length > 30) {
|
||||
const result = [];
|
||||
for (let i = 0; i < arr.length; i += 30) {
|
||||
result.push(arr.slice(i, i + 30));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return [arr];
|
||||
});
|
||||
watch(
|
||||
() => currentItem.value.id,
|
||||
() => {
|
||||
emits('updateItem', currentItem.value);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
defineExpose({
|
||||
buySuccess,
|
||||
});
|
||||
const switchEpisodeNumberCatalogue = (item: Types.Acg.MediaContentListItem) => {
|
||||
visibleCatalogue.value = false;
|
||||
state.episodeNumber = item.episodeNumber;
|
||||
replaceRouter(item.episodeNumber);
|
||||
};
|
||||
|
||||
// 打开分享
|
||||
const visibleShare = ref(false);
|
||||
const openShare = () => {
|
||||
const { id, verticalCover, horizontalCover } = props.info;
|
||||
let obj = {
|
||||
id,
|
||||
cover: verticalCover || horizontalCover,
|
||||
};
|
||||
// globalStore.setShareInfo(obj);
|
||||
visibleShare.value = true;
|
||||
};
|
||||
const onToogleCollected = () => {
|
||||
try {
|
||||
const {
|
||||
id,
|
||||
mediaStatus: { hasCollected },
|
||||
} = props.info;
|
||||
const tempIsCollect = !hasCollected;
|
||||
handleCollected(id, tempIsCollect).then(() => {
|
||||
emits('updateCollected', tempIsCollect);
|
||||
Collected_emit(id, tempIsCollect);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
const onToogleLiked = () => {
|
||||
const zan = !props.info.mediaStatus.hasLiked;
|
||||
(zan ? CommunityApi.zan : CommunityApi.cancleZan)(
|
||||
zan
|
||||
? {
|
||||
type: 'video',
|
||||
objID: props.info.id,
|
||||
}
|
||||
: {
|
||||
type: 'video',
|
||||
objIDs: [props.info.id],
|
||||
},
|
||||
).then(() => {
|
||||
emits('updateLiked', zan);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
.introduction-wrap {
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
.titleBox {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
margin: 18px 0;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 10px;
|
||||
margin: 10px 0 14px;
|
||||
.tag {
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
}
|
||||
|
||||
.videoInfoBox {
|
||||
color: #989898;
|
||||
font-size: 12px;
|
||||
margin: 10px 0 18px;
|
||||
justify-content: space-between;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.actress-box {
|
||||
height: 60px;
|
||||
margin: 24px 0;
|
||||
.portrait {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.info {
|
||||
flex: 1;
|
||||
.name {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.count-box {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.episodeArea {
|
||||
margin-bottom: 14px;
|
||||
border-top: 0.5px solid rgba(255, 255, 255, 0.1);
|
||||
border-bottom: 0.5px solid rgba(255, 255, 255, 0.1);
|
||||
padding-top: 10px;
|
||||
.episodeHeader {
|
||||
font-weight: 500;
|
||||
.episodeTitle {
|
||||
font-size: 16px;
|
||||
}
|
||||
.episodeAll {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.episodeBox {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
.item {
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.play-list-box {
|
||||
$paddingBottom: 38px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 20px 20px 0px 0px;
|
||||
background: #0f0f0f;
|
||||
padding: 18px 16px $paddingBottom;
|
||||
@include SafeArea('padding', 'bottom', 0);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: $textColor;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
> .title {
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 25px; /* 140% */
|
||||
margin: 16px 0;
|
||||
span {
|
||||
color: #e0e0e0;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
}
|
||||
.line {
|
||||
width: 36px;
|
||||
height: 5px;
|
||||
border-radius: 100px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.list-box {
|
||||
flex: 1;
|
||||
.list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
|
||||
grid-column-gap: 5px;
|
||||
grid-row-gap: 9px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.margin-r-88 {
|
||||
margin-right: 88px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,468 @@
|
||||
<template>
|
||||
<div ref="rootRef" class="video-detail-wrap">
|
||||
<SvgIcon name="back_white_round" class="back" @click="$router.go(-1)" />
|
||||
<template v-if="state.info.id">
|
||||
<div class="movieBox">
|
||||
<div class="videoAdv" v-if="state.advsList?.length && state.isShowAdv">
|
||||
<div class="advVideoBox">
|
||||
<AdvSwiper :list="state.advsList" :autoplay="2000" v-if="state.advsList.length" class="adv" />
|
||||
<div class="timer_close" v-if="isVip">
|
||||
<div v-if="state.timer > 0">
|
||||
{{ state.timer }}s |
|
||||
<span @click="state.isShowAdv = false">关闭广告</span>
|
||||
</div>
|
||||
<div v-else><span @click="state.isShowAdv = false">关闭</span></div>
|
||||
</div>
|
||||
<div class="timer_close" v-else>
|
||||
<div v-if="state.timer > 0">
|
||||
{{ state.timer }}s |
|
||||
<span @click="vipJump">VIP可关闭广告</span>
|
||||
</div>
|
||||
<div v-else><span @click="state.isShowAdv = false">关闭</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<AnimationHorizontalVideoInfo
|
||||
:videoInfo="currentItem"
|
||||
:mediaInfo="state.info"
|
||||
v-if="currentItem?.videoUrl && showVideo"
|
||||
:key="currentItem?.episodeNumber"
|
||||
@buySuccess="beforeBuySuccess"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<CountdownBanner
|
||||
@openMemberPay="showPaymentStatusPopup = true"
|
||||
/>
|
||||
<div class="changeLineBox flex-vcenter" :class="!isVip ? 'top258' : ''">
|
||||
<div class="flex-vcenter" @click.stop="openChangeRoadLine">
|
||||
<SvgIcon name="changeLineIcon" />
|
||||
<span>线路切换</span>
|
||||
</div>
|
||||
</div>
|
||||
<van-tabs
|
||||
v-model:active="tabActive"
|
||||
:animated="true"
|
||||
@rendered="onTabRendered"
|
||||
class="tabs"
|
||||
:class="{ newUser: !isVip }"
|
||||
>
|
||||
<van-tab :title="item.label" v-for="item in itemList" :key="item.value" :name="prefixKey + item.value">
|
||||
<template v-slot:title>
|
||||
{{ item.label }}
|
||||
<Teleport to=".van-tabs__nav">
|
||||
<span v-if="item.value === 2" class="countComment" :class="{ active: tabActive === prefixKey + 2 }">
|
||||
{{ watchCount(state.info.countComment) }}
|
||||
</span>
|
||||
</Teleport>
|
||||
</template>
|
||||
<!--
|
||||
@selectEpisode="selectEpisode" -->
|
||||
<Introduction
|
||||
v-if="tabIsInit(prefixKey + item.value) && item.value == 1 && state.info.id"
|
||||
ref="introductionRef"
|
||||
:info="state.info"
|
||||
@updateCollected="updateCollected"
|
||||
@updateLiked="updateLiked"
|
||||
@updateItem="updateItem"
|
||||
></Introduction>
|
||||
<CommentBox
|
||||
v-else-if="tabIsInit(prefixKey + item.value) && item.value == 2 && state.info.id"
|
||||
:id="prefixKey + item.value"
|
||||
ref="dvComment"
|
||||
:isOpen="true"
|
||||
renderType="normal"
|
||||
objType="cartoon"
|
||||
theme="black"
|
||||
class="comment-comp"
|
||||
@addComment="state.info.countComment += 1"
|
||||
:showCollected="false"
|
||||
></CommentBox>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</template>
|
||||
<Loading v-else />
|
||||
</div>
|
||||
<!-- 切换路线框 -->
|
||||
<SelectionLinePopup :is-open="showChangeRoadLine" @closeSelectLine="closeSelectLine" @close="closeChangeRoadLine" />
|
||||
<!-- 开通会员 -->
|
||||
<PaymentStatusPopup
|
||||
:show="showPaymentStatusPopup"
|
||||
:video-id="String(state.info?.id || '')"
|
||||
@close="closePaymentStatusPopup"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AcgVideoDetail',
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { ref, onUnmounted, nextTick, provide, computed, toRaw, watch, Ref, onMounted, defineAsyncComponent } from 'vue';
|
||||
import { HistoryVideoType, AdType } from '@/enums/app';
|
||||
import mediaInfo from '../utils/mediaInfo';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import MineApi from '@/service/mine';
|
||||
import { watchCount } from '@/utils/index';
|
||||
// import { Pay_emit } from '../utils/pay-mitt';
|
||||
import type { PayInfo } from '../utils/pay-mitt';
|
||||
import Introduction from './components/introduction.vue';
|
||||
import tabAction from '@/utils/tabAction';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getAdItem } from '@/utils/getConfig';
|
||||
import CountdownBanner from '@/components/CountdownBanner/index.vue';
|
||||
|
||||
const PaymentStatusPopup = defineAsyncComponent(() => import('@/components/paymentStatusPopup/index.vue'));
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const currentItem = ref({}) as Ref<Types.Acg.MediaContentListItem>;
|
||||
const itemList = [
|
||||
{
|
||||
label: '简介',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '评论',
|
||||
value: 2,
|
||||
},
|
||||
];
|
||||
const showPaymentStatusPopup = ref(false);
|
||||
const closePaymentStatusPopup = () => {
|
||||
showPaymentStatusPopup.value = false;
|
||||
};
|
||||
const prefixKey = 'acg_video_';
|
||||
const { tabActive, tabIsInit, onTabRendered } = tabAction(true);
|
||||
|
||||
const { isVip } = useUserStore();
|
||||
|
||||
const showVideo = ref(true);
|
||||
const { state, historyData, addHistory } = mediaInfo({ dbType: HistoryVideoType.AcgVideo });
|
||||
|
||||
provide('historyData', historyData);
|
||||
|
||||
const showChangeRoadLine = ref(false);
|
||||
// ------------------- 广告相关逻辑完善 -------------------
|
||||
const vipJump = () => {
|
||||
router.push('/member');
|
||||
};
|
||||
|
||||
// 获取广告
|
||||
const getRandomAdv = () => {
|
||||
state.advsList = <any>[];
|
||||
let advs = getAdItem(AdType.NEW_FREE_VIDEO).filter((item: any) => item.watchTime > 0);
|
||||
if (advs.length > 0) {
|
||||
state.advsList = advs;
|
||||
if (!(isVip || historyData.value?.mediaStatus?.hasPaid)) {
|
||||
state.isShowAdv = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
state.isShowAdv = false;
|
||||
};
|
||||
|
||||
// 倒计时
|
||||
const countdown = () => {
|
||||
let advs = getAdItem(AdType.NEW_FREE_VIDEO);
|
||||
if (advs && advs.length) {
|
||||
state.timer = getAdItem(AdType.NEW_FREE_VIDEO)[0].watchTime;
|
||||
} else {
|
||||
state.timer = 0;
|
||||
}
|
||||
if (state.countdowner) {
|
||||
clearInterval(state.countdowner);
|
||||
}
|
||||
state.countdowner = setInterval(() => {
|
||||
state.timer--;
|
||||
if (state.timer <= 0) {
|
||||
clearInterval(state.countdowner);
|
||||
state.countdowner = null;
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
onUnmounted(() => {
|
||||
state.countdowner && clearInterval(state.countdowner);
|
||||
});
|
||||
// 只在动漫数据获取到后再处理广告
|
||||
watch(
|
||||
() => currentItem.value,
|
||||
(val) => {
|
||||
if (val) {
|
||||
getRandomAdv();
|
||||
if (state.advsList.length > 0) {
|
||||
countdown();
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
const openChangeRoadLine = () => {
|
||||
showChangeRoadLine.value = true;
|
||||
};
|
||||
const closeChangeRoadLine = () => {
|
||||
showChangeRoadLine.value = false;
|
||||
};
|
||||
|
||||
// 关闭线路选择
|
||||
const closeSelectLine = () => {
|
||||
showVideo.value = false;
|
||||
showChangeRoadLine.value = false;
|
||||
nextTick(() => {
|
||||
showVideo.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
const dvComment = ref();
|
||||
|
||||
const initComment = () => {
|
||||
dvComment.value[0].state.videoInfo = toRaw(state.info);
|
||||
dvComment.value[0].getCommentList();
|
||||
};
|
||||
|
||||
watch(
|
||||
dvComment,
|
||||
(el) => {
|
||||
if (!el) return;
|
||||
initComment();
|
||||
},
|
||||
{
|
||||
once: true,
|
||||
},
|
||||
);
|
||||
const updateCollected = (collected: boolean) => {
|
||||
state.info.mediaStatus.hasCollected = collected;
|
||||
state.info.countCollect = state.info.countCollect + (collected ? +1 : -1);
|
||||
};
|
||||
|
||||
const updateLiked = (liked: boolean) => {
|
||||
state.info.mediaStatus.hasLiked = liked;
|
||||
state.info.countLike = state.info.countLike + (liked ? +1 : -1);
|
||||
};
|
||||
|
||||
const updateItem = (item: any) => {
|
||||
currentItem.value = item;
|
||||
if (route.query.tabActive) {
|
||||
tabActive.value = (prefixKey + route.query.tabActive) as string;
|
||||
}
|
||||
};
|
||||
|
||||
const introductionRef = ref();
|
||||
const beforeBuySuccess = (obj: PayInfo) => {
|
||||
if (obj.type === 'all') {
|
||||
state.info.mediaStatus.hasPaid = true;
|
||||
historyData.value.mediaStatus.hasPaid = true;
|
||||
addHistory(Number(route.query.episodeNumber || 1));
|
||||
}
|
||||
introductionRef.value[0].buySuccess(obj.type);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.video-detail-wrap {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
.back {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
z-index: 800;
|
||||
}
|
||||
|
||||
.movieBox {
|
||||
width: 375px;
|
||||
height: 212px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
//margin-left: -6px;
|
||||
z-index: 2;
|
||||
.videoAdv {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 99999;
|
||||
width: 100%;
|
||||
height: 212px;
|
||||
background: rgba(0, 0, 0, 1);
|
||||
.advVideoBox {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
.timer_close {
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 10;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
padding: 4px 12px;
|
||||
border-radius: 40px;
|
||||
color: white;
|
||||
|
||||
// span {
|
||||
// color: #ff7f0f;
|
||||
// }
|
||||
}
|
||||
|
||||
.adv {
|
||||
height: 211px;
|
||||
width: 375px;
|
||||
overflow: hidden;
|
||||
:deep(.van-swipe-item) {
|
||||
height: 211px !important;
|
||||
width: 375px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.newUserBg {
|
||||
height: 46px;
|
||||
background: url('@/assets/images/png/newUserBg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding-left: 98px;
|
||||
padding-right: 9px;
|
||||
color: #fff;
|
||||
&.noNewUser {
|
||||
// background-image: url('@/assets/images/png/newUserBg2.png');
|
||||
background-image: url('@/assets/images/png/newUserBg.png');
|
||||
}
|
||||
.nowOpenBtn {
|
||||
color: #000;
|
||||
background: linear-gradient(270deg, #e3b284 0%, #feeede 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.changeLineBox {
|
||||
position: absolute;
|
||||
top: 212px;
|
||||
right: 16px;
|
||||
z-index: 2;
|
||||
height: 44px;
|
||||
width: 100px;
|
||||
justify-content: flex-end;
|
||||
|
||||
color: #989898;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
.countComment {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 112px;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
&.active {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
.tabs {
|
||||
--van-tab-active-text-color: rgba(255, 255, 255, 0.9);
|
||||
--van-tab-text-color: rgba(255, 255, 255, 0.45);
|
||||
height: calc(100% - 269px + 11px);
|
||||
:deep(> .van-tabs__wrap) {
|
||||
background-color: unset;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
.van-tabs__nav {
|
||||
background: unset;
|
||||
.van-tab {
|
||||
font-size: 14px;
|
||||
padding: unset;
|
||||
width: 60px;
|
||||
flex: unset;
|
||||
}
|
||||
|
||||
.van-tabs__line {
|
||||
background-color: $themeColor;
|
||||
height: 4px;
|
||||
width: 16px;
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(> .van-tabs__content) {
|
||||
height: calc(100% - 44px);
|
||||
overflow-x: hidden;
|
||||
.van-tab__panel {
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
&.newUser {
|
||||
height: calc(100% - 269px + 11px);
|
||||
}
|
||||
}
|
||||
|
||||
.comment-comp {
|
||||
height: 100%;
|
||||
:deep(.comment-wrap) {
|
||||
height: 100%;
|
||||
.wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
.headerBox {
|
||||
display: none;
|
||||
}
|
||||
.slider {
|
||||
display: none;
|
||||
}
|
||||
.main {
|
||||
background: transparent;
|
||||
// border-top: 1px solid #e0e0e0;
|
||||
height: 100%;
|
||||
}
|
||||
.bottomBox {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
.commentBtnBox {
|
||||
.commentInput {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.top258 {
|
||||
top: 258px !important;
|
||||
}
|
||||
:deep(.adv_swiper) {
|
||||
border-radius: unset !important;
|
||||
.adv-container {
|
||||
.adv-swiper-slide {
|
||||
border-radius: unset !important;
|
||||
.cover {
|
||||
border-radius: unset !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div :id="`acg_topic_sortType_${props.sortType}`" class="topic-list overflow-y-auto">
|
||||
<template v-if="!state.loading">
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
|
||||
<CategoryContent
|
||||
class="content"
|
||||
coverDirection="portrait"
|
||||
:contentType="props.contentType"
|
||||
:contentWrap="true"
|
||||
:list="state.list"
|
||||
></CategoryContent>
|
||||
</PullRefresh>
|
||||
</template>
|
||||
<Loading v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRaw, reactive, watch, computed } from 'vue';
|
||||
import CategoryContent from '../../components/categoryContent.vue';
|
||||
import MediaApi from '@/service/media';
|
||||
import { useRoute } from 'vue-router';
|
||||
const route = useRoute();
|
||||
const sId = route.query.id || '';
|
||||
const kind = route.query.kind || '';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
sortType: number;
|
||||
contentType: string;
|
||||
}>(),
|
||||
{
|
||||
sortType: () => 3,
|
||||
contentType: 'video',
|
||||
},
|
||||
);
|
||||
|
||||
const showType = computed(() => {
|
||||
return Number(route.query.showType || '0');
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
loading: true,
|
||||
list: <any>[],
|
||||
});
|
||||
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: 12,
|
||||
};
|
||||
|
||||
const moreParams = reactive({
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
finished: false,
|
||||
isNoData: false,
|
||||
error: false,
|
||||
isHigehtMax: false,
|
||||
});
|
||||
|
||||
const getList = async (type = '') => {
|
||||
try {
|
||||
const TempfilterParams = Object.assign({}, params, { sId, sortType: props.sortType, kind });
|
||||
let res: any = await MediaApi.getMediaTopic(TempfilterParams);
|
||||
state.loading = false;
|
||||
moreParams.loading = false;
|
||||
moreParams.refreshing = false;
|
||||
|
||||
let list = res.list || [];
|
||||
if (params.pageNumber === 1) {
|
||||
moreParams.isNoData = list.length ? false : true;
|
||||
}
|
||||
if (type === 'refresh') {
|
||||
state.list = list;
|
||||
} else {
|
||||
state.list = [...state.list, ...list];
|
||||
}
|
||||
|
||||
if (!res.hasNext) {
|
||||
moreParams.finished = true;
|
||||
}
|
||||
} catch (e) {
|
||||
moreParams.loading = false;
|
||||
moreParams.error = true;
|
||||
}
|
||||
};
|
||||
getList();
|
||||
|
||||
const onLoad = () => {
|
||||
moreParams.loading = true;
|
||||
params.pageNumber++;
|
||||
getList();
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
params.pageNumber = 1;
|
||||
moreParams.refreshing = true;
|
||||
moreParams.finished = false;
|
||||
getList('refresh');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
.topic-list {
|
||||
height: calc(100vh - 44px * 2);
|
||||
padding: 0 16px;
|
||||
@include SafeArea('padding', 'bottom', 0);
|
||||
|
||||
.content {
|
||||
border-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<!-- ACG大类列表,漫画、动漫、有声小说 -->
|
||||
<template>
|
||||
<div class="topic-wrap">
|
||||
<Header :title="route.query.title" />
|
||||
<van-tabs
|
||||
v-model:active="tabActive"
|
||||
title-active-color="#fff"
|
||||
title-inactive-color="#747B80"
|
||||
:animated="true"
|
||||
@rendered="onTabRendered"
|
||||
class="tabs"
|
||||
>
|
||||
<van-tab v-for="item in itemList" :key="item.key" :title="item.title" :name="prefixKey + item.key">
|
||||
<List v-if="tabIsInit(prefixKey + item.key)" :sortType="item.key" :contentType="contentType"></List>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AcgTopicList',
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import List from './components/list.vue';
|
||||
import tabAction from '@/utils/tabAction';
|
||||
import { useRoute } from 'vue-router';
|
||||
const route = useRoute();
|
||||
const contentType = route.query.type as string;
|
||||
const itemList = [
|
||||
{ title: '热门推荐', key: 3 },
|
||||
{ title: '最新上架', key: 1 },
|
||||
{ title: '最新热评', key: 4 },
|
||||
];
|
||||
|
||||
const prefixKey = 'acg_topic_sortType_';
|
||||
const { tabActive, tabIsInit, onTabRendered } = tabAction(true);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.topic-wrap {
|
||||
.tabs {
|
||||
:deep(.van-tabs__wrap) {
|
||||
border-radius: 46px;
|
||||
margin: 0 auto;
|
||||
//background: #030f18;
|
||||
padding: 0 40px;
|
||||
.van-tabs__nav {
|
||||
background: unset;
|
||||
.van-tab {
|
||||
font-size: 14px;
|
||||
|
||||
&:not(:nth-last-child(2))::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
right: 0px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
// .van-tab:nth-of-type(3) {
|
||||
// &::after {
|
||||
// display: none;
|
||||
// }
|
||||
// }
|
||||
.van-tabs__line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
import MediaApi from '@/service/media';
|
||||
import { showToast } from 'vant';
|
||||
import { onMounted, onUnmounted, Ref } from 'vue';
|
||||
import eventBus from '@/utils/eventBus';
|
||||
import { AcgEventBusKey } from '@/enums/app';
|
||||
|
||||
/**
|
||||
* @Description 收藏acg
|
||||
* @param {string} objID
|
||||
* @param {string} mediaType
|
||||
* @param {boolean} isCollect
|
||||
* @return {*}
|
||||
*/
|
||||
const handleCollected = async (objID: string, isCollect: boolean) => {
|
||||
try {
|
||||
return (isCollect ? MediaApi.postBookshelfAdd : MediaApi.postBookshelfDel)({
|
||||
id: objID,
|
||||
}).then(() => {
|
||||
showToast(isCollect ? '成功收藏' : '成功取消');
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @Description eventBus监听
|
||||
* @param {string} id
|
||||
* @param {Ref} hasCollected
|
||||
* @return {*}
|
||||
*/
|
||||
const Collected_on = (id: string, hasCollected: Ref<boolean>, callback?: any) => {
|
||||
const update = (bool: any) => {
|
||||
if (callback) {
|
||||
callback(bool);
|
||||
return;
|
||||
}
|
||||
hasCollected.value = bool;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on(AcgEventBusKey.Collected + id, update);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
eventBus.off(AcgEventBusKey.Collected + id, update);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @Description eventBus赋值
|
||||
* @param {string} id
|
||||
* @param {boolean} bool
|
||||
* @return {*}
|
||||
*/
|
||||
const Collected_emit = (id: string, bool: boolean) => {
|
||||
eventBus.emit(AcgEventBusKey.Collected + id, bool);
|
||||
};
|
||||
|
||||
export { handleCollected, Collected_on, Collected_emit };
|
||||
@@ -0,0 +1,129 @@
|
||||
import router from '@/router';
|
||||
const goTopicPage = (id: string, type: string | number, title: string, showType?: number) => {
|
||||
if (typeof type === 'number') {
|
||||
type = getMediaTypeKey(type);
|
||||
}
|
||||
router.push({ path: '/AcgTopicList', query: { id, type, title, showType } });
|
||||
};
|
||||
|
||||
const goAcgTagPage = (id: string, tagName: string, newsType?: string) => {
|
||||
router.push({ path: '/tagVideoList', query: { id, tagName, newsType } });
|
||||
};
|
||||
// kind的id对应字符串
|
||||
enum MediaType {
|
||||
video = 4,
|
||||
image = 5,
|
||||
text = 6,
|
||||
}
|
||||
type MediaTypeKey = keyof typeof MediaType;
|
||||
type MediaTypeValue = { [key in MediaType]: number };
|
||||
|
||||
function getMediaTypeKey(value: number): string {
|
||||
if (value === MediaType.video) {
|
||||
return 'video';
|
||||
} else if (value === MediaType.image) {
|
||||
return 'image';
|
||||
} else if (value === MediaType.text) {
|
||||
return 'text';
|
||||
// } else if (value === MediaType.theatre) {
|
||||
// return 'theatre';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getMediaTypeValue(value: string): MediaType | number {
|
||||
const mediaTypeKeys = Object.keys(MediaType).filter((key) => isNaN(Number(key)));
|
||||
const mediaTypeValues = mediaTypeKeys.map((key) => MediaType[key]);
|
||||
const index = mediaTypeKeys.indexOf(value);
|
||||
|
||||
if (index !== -1) {
|
||||
return mediaTypeValues[index];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const goFilterPage = () => {
|
||||
const route = router.currentRoute;
|
||||
const { mediaType = 'video' } = route.value.query;
|
||||
router.push({ path: '/AcgSearchFilter', query: { mediaType } });
|
||||
};
|
||||
//获取当前时间
|
||||
const getNewDate = () => {
|
||||
return new Date(new Date().getTime() + (window._diffTime || 0));
|
||||
};
|
||||
|
||||
const rankTabList = [
|
||||
{ title: '日榜', key: 1 },
|
||||
{ title: '周榜', key: 2 },
|
||||
{ title: '人气榜', key: 3 },
|
||||
{ title: '热度榜', key: 4 },
|
||||
{ title: '钻石榜', key: 5 },
|
||||
{ title: '连载榜', key: 6 },
|
||||
];
|
||||
|
||||
function getElementDistance(element: HTMLElement, parent: HTMLElement) {
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
|
||||
const distance = {
|
||||
top: elementRect.top - parentRect.top,
|
||||
right: parentRect.right - elementRect.right,
|
||||
bottom: parentRect.bottom - elementRect.bottom,
|
||||
left: elementRect.left - parentRect.left,
|
||||
};
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
const goDetailPage = (mediaType: MediaTypeKey, id: string, mediaSubType?: number) => {
|
||||
let name = 'AcgImageDetail';
|
||||
if (mediaType === 'text') {
|
||||
name = 'AcgTextDetail';
|
||||
} else if (mediaType === 'video') {
|
||||
name = 'AcgVideoDetail';
|
||||
}
|
||||
|
||||
router.push({
|
||||
name,
|
||||
query: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const freeSet = new Set();
|
||||
|
||||
const getUpdateStatusText = (updateStatus?: number) => {
|
||||
if (updateStatus === 2) return '已完结';
|
||||
return '连载中';
|
||||
};
|
||||
|
||||
/** 按 id(无 id 则按 name)去重,保留首次出现顺序 */
|
||||
const dedupeTagDetails = (tags?: Types.Media.TagDetailsItem[] | null): Types.Media.TagDetailsItem[] => {
|
||||
if (!tags?.length) return [];
|
||||
const seen = new Set<string>();
|
||||
return tags.filter((item) => {
|
||||
const key = String(item.name || '').trim();
|
||||
if (!key || seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
goTopicPage,
|
||||
goFilterPage,
|
||||
getMediaTypeKey,
|
||||
getMediaTypeValue,
|
||||
getNewDate,
|
||||
rankTabList,
|
||||
getElementDistance,
|
||||
goDetailPage,
|
||||
freeSet,
|
||||
getUpdateStatusText,
|
||||
goAcgTagPage,
|
||||
dedupeTagDetails,
|
||||
};
|
||||
export type { MediaTypeKey, MediaTypeValue };
|
||||
@@ -0,0 +1,66 @@
|
||||
import { ref, Ref } from 'vue';
|
||||
import { getVideoHistoryByKey, addVideoHistory } from '@/utils/index';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import eventBus from '@/utils/eventBus';
|
||||
|
||||
const AcgHistoryData = {} as {
|
||||
[key: string]: {
|
||||
[key: string]: Types.Media.MediaInfo;
|
||||
};
|
||||
};
|
||||
|
||||
const key = 'AcgHistoryData_';
|
||||
|
||||
const getData = async (id: string, dbType = HistoryVideoType.AcgImage, isParent: boolean) => {
|
||||
if (!AcgHistoryData[dbType]) {
|
||||
AcgHistoryData[dbType] = {};
|
||||
}
|
||||
let item = AcgHistoryData[dbType][id];
|
||||
if (item && item.id) {
|
||||
return item;
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
AcgHistoryData[dbType][id] = { id: '' } as any;
|
||||
item = (await getVideoHistoryByKey(id, dbType)) as any;
|
||||
} else {
|
||||
item = (await new Promise((resolve) => {
|
||||
eventBus.on(key + id, (obj: Types.Media.MediaInfo) => {
|
||||
eventBus.off(key + id);
|
||||
resolve(obj);
|
||||
});
|
||||
})) as any;
|
||||
}
|
||||
|
||||
if (item && item.id) {
|
||||
AcgHistoryData[dbType][id] = item;
|
||||
eventBus.emit(key + id, item);
|
||||
}
|
||||
return item;
|
||||
};
|
||||
|
||||
export default (dbType = HistoryVideoType.AcgImage, isParent: boolean) => {
|
||||
const historyEpisodeNumber = ref(0);
|
||||
const historyData = ref({}) as Ref<Types.Media.MediaInfo>;
|
||||
|
||||
const getHistoryData = async (id: string) => {
|
||||
const history = (await getData(id, dbType, isParent)) as any;
|
||||
if (history) {
|
||||
historyData.value = history;
|
||||
if (history.historyEpisodeNumber) {
|
||||
historyEpisodeNumber.value = history.historyEpisodeNumber;
|
||||
}
|
||||
} else {
|
||||
historyEpisodeNumber.value = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const setHistoryData = (data: any, dbType: HistoryVideoType) => {
|
||||
const item = (AcgHistoryData[dbType] && AcgHistoryData[dbType][data.id]) || {};
|
||||
const newItem = Object.assign(item, data);
|
||||
AcgHistoryData[dbType][data.id] = newItem;
|
||||
eventBus.emit(key + data.id, newItem);
|
||||
addVideoHistory(newItem, dbType, new Date().getTime());
|
||||
};
|
||||
return { historyData, historyEpisodeNumber, getHistoryData, setHistoryData };
|
||||
};
|
||||
@@ -0,0 +1,542 @@
|
||||
import { reactive, ref, ComputedRef, watchEffect, watch, nextTick, computed, onBeforeUnmount } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import indexDB from './indexDB';
|
||||
import { showToast } from 'vant';
|
||||
import AcgApi from '@/service/acg';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { freeSet } from './common';
|
||||
interface ReadData {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export default ({
|
||||
mediaId = '',
|
||||
isScrollTop = true,
|
||||
isGetInfo = true,
|
||||
isUpdate = true,
|
||||
isNeedHistory = true,
|
||||
isGetHistoryInit = false,
|
||||
pageSize = 30,
|
||||
dbType = HistoryVideoType.AcgImage,
|
||||
}) => {
|
||||
const userStore = useUserStore();
|
||||
const { openLoading, closeLoading } = useLoading();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { id, episodeNumber, title } = route.query;
|
||||
|
||||
const state = reactive({
|
||||
list: [] as Types.Acg.MediaContentListItem[],
|
||||
info: {} as Types.Acg.MediaContentInfo,
|
||||
validateItem: {} as Types.Acg.MediaContentListItem,
|
||||
episodeNumber: Number(episodeNumber || '1'),
|
||||
id: (mediaId || id) as string,
|
||||
title,
|
||||
gradingGroupIndex: 0,
|
||||
});
|
||||
|
||||
const { historyData, historyEpisodeNumber, getHistoryData, setHistoryData } = indexDB(dbType, true);
|
||||
const addHistory = (num: number) => {
|
||||
if (num === -1) {
|
||||
num = state.episodeNumber;
|
||||
}
|
||||
const data = { ...historyData.value, historyEpisodeNumber: num, historyReadData };
|
||||
setHistoryData(data, dbType);
|
||||
};
|
||||
|
||||
const historyReadData = [] as Types.Acg.ReadData[];
|
||||
|
||||
const addHistoryReadData = (num: number, obj: ReadData, imgObj?: { url: string; width: number; height: number }) => {
|
||||
const listItem = state.list.find((item) => item.episodeNumber === num);
|
||||
if (!listItem) return;
|
||||
|
||||
const index = (historyReadData || []).findIndex((item) => item.episodeNumber === num);
|
||||
const isHas = index > -1;
|
||||
|
||||
const imgs = isHas ? historyReadData[index].imgs || {} : {};
|
||||
if (imgObj?.url) {
|
||||
imgs[imgObj?.url] = {
|
||||
width: imgObj?.width,
|
||||
height: imgObj?.height,
|
||||
};
|
||||
}
|
||||
let item = {};
|
||||
if (isHas) {
|
||||
item = historyReadData.splice(index, 1)[0];
|
||||
}
|
||||
|
||||
const newItem = Object.assign({}, { ...item }, { ...listItem, ...obj, imgs });
|
||||
historyReadData.push(newItem);
|
||||
};
|
||||
|
||||
if (isUpdate) {
|
||||
onBeforeUnmount(() => {
|
||||
nextTick(() => {
|
||||
addHistory(-1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const currentItem = computed(() => {
|
||||
if (state.list.length === 0) return {};
|
||||
return state.list.find((item) => item.episodeNumber === state.episodeNumber) || {};
|
||||
}) as ComputedRef<Types.Acg.MediaContentListItem>;
|
||||
|
||||
let initScrollLeft = true;
|
||||
watch(currentItem, () => {
|
||||
if (initScrollLeft) {
|
||||
goSectionScrollLeft();
|
||||
initScrollLeft = false;
|
||||
}
|
||||
if (['AcgTextDetail', 'AcgImageDetail'].includes((route.name as string) || '')) {
|
||||
return;
|
||||
}
|
||||
validateHasBuy(currentItem.value);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => state.episodeNumber,
|
||||
() => {
|
||||
addHistory(state.episodeNumber);
|
||||
},
|
||||
);
|
||||
|
||||
const getPageNumber = () => {
|
||||
return Math.ceil(state.episodeNumber / 30);
|
||||
};
|
||||
const pageNumber = getPageNumber();
|
||||
|
||||
const params = {
|
||||
prevPageNumber: pageNumber,
|
||||
nextPageNumber: pageNumber,
|
||||
pageSize,
|
||||
};
|
||||
|
||||
const moreParams = reactive({
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
finished: false,
|
||||
isNoData: false,
|
||||
error: false,
|
||||
isHigehtMax: false,
|
||||
});
|
||||
|
||||
const moreParams_finished = {
|
||||
prev: false,
|
||||
next: false,
|
||||
};
|
||||
|
||||
const getList = async (type: 'prev' | 'next', pageNumber?: number) => {
|
||||
try {
|
||||
const tempParams = {
|
||||
mediaId: state.id,
|
||||
pageSize: params.pageSize,
|
||||
pageNumber: pageNumber ? pageNumber : type === 'prev' ? params.prevPageNumber : params.nextPageNumber,
|
||||
};
|
||||
let res: any = await AcgApi.getMediaContentList(tempParams);
|
||||
if ((res.list || []).length === 0 && state.list.length === 0 && tempParams.pageNumber !== 1) {
|
||||
params.prevPageNumber = 1;
|
||||
params.nextPageNumber = 1;
|
||||
tempParams.pageNumber = 1;
|
||||
res = await AcgApi.getMediaContentList(tempParams);
|
||||
}
|
||||
|
||||
historyData.value.currentEpisode = res.total;
|
||||
|
||||
moreParams.loading = false;
|
||||
moreParams.refreshing = false;
|
||||
|
||||
let list = res.list || [];
|
||||
if (list.length > 0) {
|
||||
const reList = (list || []).reduce(
|
||||
(acc: Types.Acg.MediaContentListItem[], current: Types.Acg.MediaContentListItem) => {
|
||||
const x = acc.find((item) => item.episodeNumber === current.episodeNumber);
|
||||
if (!x) {
|
||||
return acc.concat([current]);
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
if (isInitGetList && res.hasNext && list.length !== reList.length) {
|
||||
list = reList;
|
||||
onLoad();
|
||||
}
|
||||
}
|
||||
isInitGetList = false;
|
||||
|
||||
if (state.list.length === 0) {
|
||||
state.list = list;
|
||||
} else {
|
||||
state.list = state.list.concat(list);
|
||||
}
|
||||
if (tempParams.pageNumber == 1 && state.list.length == 0) {
|
||||
moreParams.isNoData = true;
|
||||
}
|
||||
if (!res.hasNext) {
|
||||
moreParams_finished[type] = true;
|
||||
moreParams.finished = type === 'next' && sort.value;
|
||||
}
|
||||
return list;
|
||||
} catch (e) {
|
||||
moreParams.loading = false;
|
||||
moreParams.error = true;
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const onLoad = () => {
|
||||
moreParams.loading = true;
|
||||
if (sort.value) {
|
||||
params.nextPageNumber += 1;
|
||||
getList('next');
|
||||
} else {
|
||||
if (params.prevPageNumber === 1) {
|
||||
moreParams.loading = false;
|
||||
return;
|
||||
}
|
||||
params.prevPageNumber -= 1;
|
||||
getList('prev');
|
||||
}
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
if (sort.value) {
|
||||
if (params.prevPageNumber == 1) {
|
||||
moreParams.refreshing = false;
|
||||
return;
|
||||
}
|
||||
params.prevPageNumber -= 1;
|
||||
moreParams.refreshing = true;
|
||||
moreParams.finished = false;
|
||||
getList('prev');
|
||||
} else {
|
||||
if (moreParams_finished['next']) {
|
||||
moreParams.refreshing = false;
|
||||
return;
|
||||
}
|
||||
params.nextPageNumber += 1;
|
||||
moreParams.refreshing = true;
|
||||
getList('next');
|
||||
}
|
||||
};
|
||||
let isInitGetList = true;
|
||||
const init = () => {
|
||||
getList('next');
|
||||
};
|
||||
|
||||
if (isNeedHistory) {
|
||||
getHistoryData(state.id).then(() => {
|
||||
historyReadData.push(...JSON.parse(JSON.stringify(historyData.value.historyReadData || '[]')));
|
||||
if (isGetHistoryInit) {
|
||||
if (!episodeNumber) {
|
||||
state.episodeNumber = historyData.value.historyEpisodeNumber || state.episodeNumber;
|
||||
}
|
||||
|
||||
const pageNumber = getPageNumber();
|
||||
|
||||
params.prevPageNumber = pageNumber;
|
||||
params.nextPageNumber = pageNumber;
|
||||
}
|
||||
|
||||
init();
|
||||
});
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
const getContent = async () => {
|
||||
if (state.info.id) {
|
||||
openLoading();
|
||||
}
|
||||
|
||||
await AcgApi.getMediaContentInfo({ id: currentItem.value.id }).then((res) => {
|
||||
state.info = res;
|
||||
});
|
||||
|
||||
closeLoading();
|
||||
};
|
||||
|
||||
if (isGetInfo) {
|
||||
watch(
|
||||
() => currentItem.value.id,
|
||||
(newId, oldId) => {
|
||||
if (!newId || newId === oldId) return;
|
||||
getContent();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const statusText = computed(() => {
|
||||
switch (historyData.value.updateStatus) {
|
||||
case 1:
|
||||
return '连载中';
|
||||
case 2:
|
||||
return '已完结';
|
||||
default:
|
||||
return '已完结';
|
||||
}
|
||||
});
|
||||
|
||||
//目录
|
||||
const visibleCatalogue = ref(false);
|
||||
const sort = ref(true);
|
||||
const autoList = computed(() => {
|
||||
return (state.list || []).sort(
|
||||
sort.value ? (a, b) => a.episodeNumber - b.episodeNumber : (a, b) => b.episodeNumber - a.episodeNumber,
|
||||
);
|
||||
});
|
||||
|
||||
const scrollToElement = (elementId: string) => {
|
||||
const element = document.getElementById(elementId) as HTMLElement;
|
||||
if (!element) return;
|
||||
const parent = element.closest('.list') as HTMLElement;
|
||||
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
|
||||
const offsetTop = elementRect.top - parentRect.top;
|
||||
const offsetBottom = elementRect.bottom - parentRect.bottom;
|
||||
|
||||
if (offsetTop < 0 || offsetBottom > 0) {
|
||||
// parent.scrollTop = element.offsetTop - (parent.offsetHeight - element.offsetHeight) / 2;
|
||||
parent.scrollTop = element.offsetTop - parent.offsetHeight / 2;
|
||||
}
|
||||
};
|
||||
const goSectionScrollTop = () => {
|
||||
if (!isScrollTop) return;
|
||||
nextTick(() => {
|
||||
scrollToElement(`section_${state.episodeNumber}`);
|
||||
});
|
||||
};
|
||||
|
||||
let isInit = false;
|
||||
|
||||
watchEffect(() => {
|
||||
if (!visibleCatalogue.value) return;
|
||||
|
||||
if (isInit) {
|
||||
return;
|
||||
}
|
||||
|
||||
isInit = true;
|
||||
|
||||
goSectionScrollTop();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => sort.value,
|
||||
() => {
|
||||
moreParams.finished = moreParams_finished[sort.value ? 'next' : 'prev'];
|
||||
goSectionScrollTop();
|
||||
},
|
||||
);
|
||||
|
||||
//支付
|
||||
const dvPayForWatch = ref();
|
||||
const visiblePayForWatch = ref(false);
|
||||
|
||||
const validatePermission = () => {
|
||||
// 购买优先
|
||||
if (historyData.value.id && historyData.value.mediaStatus && historyData.value.mediaStatus.hasPaid) return true;
|
||||
// 会员
|
||||
if (historyData.value.permission === 0 && userStore.isVip) return true;
|
||||
// 付费
|
||||
if (historyData.value.permission === 1) {
|
||||
if (!historyData.value.price) {
|
||||
return true;
|
||||
}
|
||||
if (userStore.userInfo.vipLevel >= 2) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 免费 || 前几集免费
|
||||
if (historyData.value.permission === 2 || historyData.value.freeEpisode >= state.validateItem.episodeNumber)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const validateHasBuy = (item: Types.Acg.MediaContentListItem) => {
|
||||
state.validateItem = item;
|
||||
if (!validatePermission()) {
|
||||
visiblePayForWatch.value = true;
|
||||
// showToast('付费章节,购买后才能观看喔');
|
||||
return true;
|
||||
}
|
||||
visiblePayForWatch.value = false;
|
||||
return false;
|
||||
};
|
||||
|
||||
const buySuccess = (type = 'one') => {
|
||||
state.info.hasBuy = true;
|
||||
if (state.validateItem.id) {
|
||||
state.validateItem.hasBuy = true;
|
||||
state.episodeNumber = state.validateItem.episodeNumber;
|
||||
}
|
||||
visiblePayForWatch.value = false;
|
||||
visibleCatalogue.value = false;
|
||||
if (type === 'all') {
|
||||
state.list.forEach((item) => {
|
||||
item.hasBuy = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// watch(
|
||||
// dvPayForWatch,
|
||||
// (el) => {
|
||||
// debugger;
|
||||
// if (!el) return;
|
||||
// validateHasBuy(currentItem.value);
|
||||
// },
|
||||
// // {
|
||||
// // once: true,
|
||||
// // },
|
||||
// );
|
||||
|
||||
const closePayForWatch = () => {
|
||||
if (validatePermission()) {
|
||||
visiblePayForWatch.value = false;
|
||||
return;
|
||||
}
|
||||
router.back();
|
||||
};
|
||||
|
||||
const loadEpisodeList = async (num: number, type: 'prev' | 'next') => {
|
||||
const item = state.list.find((item) => item.episodeNumber === num);
|
||||
if (item) return item;
|
||||
|
||||
openLoading();
|
||||
await getList(type);
|
||||
closeLoading();
|
||||
|
||||
return state.list.find((item) => item.episodeNumber === num);
|
||||
};
|
||||
|
||||
const replaceRouter = (num: number) => {
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
episodeNumber: num,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const switchEpisodeNumber = async (num: number, isValidate = true) => {
|
||||
|
||||
if (num === -1) {
|
||||
if (state.episodeNumber !== 1) {
|
||||
const prevNum = state.episodeNumber - 1;
|
||||
const item = await loadEpisodeList(prevNum, 'prev');
|
||||
if (isValidate) {
|
||||
if (validateHasBuy(item as Types.Acg.MediaContentListItem)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
state.episodeNumber = prevNum;
|
||||
if (dbType === HistoryVideoType.AcgText) {
|
||||
addHistoryReadData(prevNum, { progress: 0, scrollTop: 0 });
|
||||
addHistory(prevNum);
|
||||
}
|
||||
replaceRouter(prevNum);
|
||||
return true;
|
||||
} else {
|
||||
showToast('这是第一章了喔~');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (state.episodeNumber < historyData.value.currentEpisode) {
|
||||
const nextNum = state.episodeNumber + 1;
|
||||
const item = await loadEpisodeList(nextNum, 'next');
|
||||
if (isValidate) {
|
||||
if (validateHasBuy(item as Types.Acg.MediaContentListItem)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
state.episodeNumber = nextNum;
|
||||
if (dbType === HistoryVideoType.AcgText) {
|
||||
addHistoryReadData(nextNum, { progress: 0, scrollTop: 0 });
|
||||
addHistory(nextNum);
|
||||
}
|
||||
replaceRouter(nextNum);
|
||||
return true;
|
||||
} else {
|
||||
showToast('已经到最后一章了喔~');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const switchEpisodeNumberCatalogue = (item: Types.Acg.MediaContentListItem) => {
|
||||
if (validateHasBuy(item)) return;
|
||||
|
||||
visibleCatalogue.value = false;
|
||||
if (dbType === HistoryVideoType.AcgText) {
|
||||
addHistoryReadData(item.episodeNumber, { progress: 0, scrollTop: 0 });
|
||||
addHistory(item.episodeNumber);
|
||||
}
|
||||
state.episodeNumber = item.episodeNumber;
|
||||
replaceRouter(item.episodeNumber);
|
||||
};
|
||||
|
||||
const scrollToElement2 = (elementId: string) => {
|
||||
const element = document.getElementById(elementId) as HTMLElement;
|
||||
if (!element) return;
|
||||
const parent = element.closest('.episode-box') as HTMLElement;
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
|
||||
const offsetLeft = elementRect.left - parentRect.left;
|
||||
const offsetRight = elementRect.right - parentRect.right;
|
||||
|
||||
if (offsetLeft < 0 || offsetRight > 0) {
|
||||
parent.scrollLeft = element.offsetLeft - parent.offsetWidth / 2;
|
||||
}
|
||||
};
|
||||
const goSectionScrollLeft = () => {
|
||||
const item = state.list.find((item) => item.episodeNumber === state.episodeNumber);
|
||||
if (!item) return;
|
||||
nextTick(() => {
|
||||
scrollToElement2(`episode-${item.id}`);
|
||||
});
|
||||
};
|
||||
const handlePopupSwitchEpisode = (num: number) => {
|
||||
state.episodeNumber = num;
|
||||
visibleCatalogue.value = false;
|
||||
|
||||
goSectionScrollLeft();
|
||||
};
|
||||
|
||||
return {
|
||||
state,
|
||||
currentItem,
|
||||
statusText,
|
||||
moreParams,
|
||||
onLoad,
|
||||
onRefresh,
|
||||
getList,
|
||||
visibleCatalogue,
|
||||
handlePopupSwitchEpisode,
|
||||
sort,
|
||||
autoList,
|
||||
dvPayForWatch,
|
||||
visiblePayForWatch,
|
||||
validateHasBuy,
|
||||
buySuccess,
|
||||
closePayForWatch,
|
||||
switchEpisodeNumber,
|
||||
historyData,
|
||||
historyEpisodeNumber,
|
||||
addHistory,
|
||||
addHistoryReadData,
|
||||
switchEpisodeNumberCatalogue,
|
||||
validatePermission,
|
||||
goSectionScrollTop,
|
||||
replaceRouter,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { reactive, computed, onActivated, onUnmounted, provide } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import MediaApi from '@/service/media';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import indexDB from './indexDB';
|
||||
import { dedupeTagDetails } from './common';
|
||||
// import { showToast } from 'vant';
|
||||
|
||||
export default ({ id = '', dbType = HistoryVideoType.AcgImage }) => {
|
||||
const route = useRoute();
|
||||
const madieId = id || (route.query.id as string);
|
||||
//内容
|
||||
const state = reactive({
|
||||
info: {} as Types.Media.MediaInfo,
|
||||
episodeNumber: Number(route.query.episode) || 1,
|
||||
advsList: <any>[],
|
||||
timer: 0,
|
||||
isShowAdv: true,
|
||||
countdowner: <any>null,
|
||||
});
|
||||
|
||||
const { historyData, historyEpisodeNumber, getHistoryData, setHistoryData } = indexDB(dbType, true);
|
||||
provide('historyData', historyData);
|
||||
const addHistory = (num = 0) => {
|
||||
const historyReadData = historyData.value.historyReadData || [];
|
||||
const obj = { ...historyData.value, ...state.info, historyReadData };
|
||||
if (num > 0) {
|
||||
obj.historyEpisodeNumber = num;
|
||||
}
|
||||
setHistoryData(obj, dbType);
|
||||
};
|
||||
|
||||
onActivated(() => {
|
||||
if (!state.info.id) return;
|
||||
setTimeout(() => {
|
||||
getHistoryData(madieId);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
getHistoryData(madieId).then(() => {
|
||||
addHistory();
|
||||
});
|
||||
});
|
||||
|
||||
const getData = async () => {
|
||||
await getHistoryData(madieId);
|
||||
const res = await MediaApi.getMediaInfo({ id: madieId });
|
||||
const info = res as Types.Media.MediaInfo;
|
||||
if (info.tagDetails?.length) {
|
||||
info.tagDetails = dedupeTagDetails(info.tagDetails);
|
||||
}
|
||||
state.info = info;
|
||||
|
||||
addHistory();
|
||||
if (!historyData.value.id) {
|
||||
historyData.value = state.info;
|
||||
}
|
||||
};
|
||||
getData();
|
||||
|
||||
const statusText = computed(() => {
|
||||
switch (state.info.updateStatus) {
|
||||
case 1:
|
||||
return '连载中';
|
||||
case 2:
|
||||
return '已完结';
|
||||
default:
|
||||
return '已完结';
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
state,
|
||||
historyData,
|
||||
addHistory,
|
||||
historyEpisodeNumber,
|
||||
statusText,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { onMounted, onUnmounted, Ref } from 'vue';
|
||||
import eventBus from '@/utils/eventBus';
|
||||
import { AcgEventBusKey } from '@/enums/app';
|
||||
|
||||
interface PayInfo {
|
||||
type: 'one' | 'all';
|
||||
productID: string;
|
||||
isFree: boolean;
|
||||
}
|
||||
|
||||
const Pay_parent_on = ({
|
||||
id,
|
||||
state,
|
||||
historyData,
|
||||
addHistory,
|
||||
}: {
|
||||
id: string;
|
||||
state: {
|
||||
info: Types.Media.MediaInfo;
|
||||
};
|
||||
historyData: Ref<Types.Media.MediaInfo>;
|
||||
addHistory: any;
|
||||
}) => {
|
||||
const update = () => {
|
||||
state.info.mediaStatus.hasPaid = true;
|
||||
historyData.value.mediaStatus.hasPaid = true;
|
||||
addHistory();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on(AcgEventBusKey.HasPaid + id, update as any);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
eventBus.off(AcgEventBusKey.HasPaid + id, update as any);
|
||||
});
|
||||
};
|
||||
|
||||
const Pay_child_on = (parentId: string, state: any) => {
|
||||
const update = (id: any) => {
|
||||
const item = (state.list as Types.Acg.MediaContentListItem[]).find((item) => item.id === id);
|
||||
if (item) {
|
||||
item.hasBuy = true;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on(AcgEventBusKey.HasBuy + parentId, update);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
eventBus.off(AcgEventBusKey.HasBuy + parentId, update);
|
||||
});
|
||||
};
|
||||
|
||||
const Pay_emit = (id: string, obj: PayInfo) => {
|
||||
if (obj.type === 'one') {
|
||||
eventBus.emit(AcgEventBusKey.HasBuy + id, obj.productID);
|
||||
} else {
|
||||
eventBus.emit(AcgEventBusKey.HasPaid + id);
|
||||
}
|
||||
};
|
||||
|
||||
export { Pay_parent_on, Pay_child_on, Pay_emit };
|
||||
export type { PayInfo };
|
||||
@@ -0,0 +1,242 @@
|
||||
interface Rules {
|
||||
toBottom: number;
|
||||
toTop: number;
|
||||
toLeft: number;
|
||||
toRight: number;
|
||||
isClickCallback?: boolean;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
time: number;
|
||||
}
|
||||
export interface Result {
|
||||
toTop: boolean;
|
||||
toBottom: boolean;
|
||||
toLeft: boolean;
|
||||
toRight: boolean;
|
||||
}
|
||||
class TouchSlide {
|
||||
element;
|
||||
startCallback: ((event: TouchEvent) => void) | undefined;
|
||||
moveCallback: ((event: TouchEvent) => void) | undefined;
|
||||
endCallback: ((event: TouchEvent) => void) | undefined;
|
||||
isTouch;
|
||||
isTouchMove;
|
||||
|
||||
event: TouchEvent | undefined;
|
||||
rules = {
|
||||
toBottom: 0,
|
||||
toTop: 0,
|
||||
toLeft: 0,
|
||||
toRight: 0,
|
||||
isClickCallback: false,
|
||||
} as Rules;
|
||||
scrollDirection: number | undefined;
|
||||
startPos = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
time: 0,
|
||||
} as Position;
|
||||
movePos = [] as Position[];
|
||||
endPos = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
time: 0,
|
||||
} as Position;
|
||||
isInit = false;
|
||||
scrollDirectionSibling = 0;
|
||||
result = {
|
||||
toTop: false,
|
||||
toBottom: false,
|
||||
toLeft: false,
|
||||
toRight: false,
|
||||
};
|
||||
eventType = 'click';
|
||||
|
||||
constructor({
|
||||
element,
|
||||
rules,
|
||||
startCallback,
|
||||
moveCallback,
|
||||
endCallback,
|
||||
}: {
|
||||
element: HTMLElement;
|
||||
rules?: Rules;
|
||||
startCallback?: (event: TouchEvent) => void;
|
||||
moveCallback?: (event: TouchEvent) => void;
|
||||
endCallback?: (event: TouchEvent) => void;
|
||||
startPos?: Position;
|
||||
movePos?: Position[];
|
||||
endPos?: Position;
|
||||
scrollDirection?: number;
|
||||
scrollDirectionSibling?: number;
|
||||
result?: Result;
|
||||
eventType?: 'click' | 'slide';
|
||||
}) {
|
||||
this.element = element;
|
||||
this.startCallback = startCallback;
|
||||
this.moveCallback = moveCallback;
|
||||
this.endCallback = endCallback;
|
||||
|
||||
this.isTouch = false;
|
||||
this.isTouchMove = false;
|
||||
this.initRules(rules || {});
|
||||
|
||||
const options = {
|
||||
capture: false,
|
||||
passive: false,
|
||||
};
|
||||
|
||||
this.element.addEventListener('touchstart', this.eventStart.bind(this), options);
|
||||
this.element.addEventListener('touchmove', this.eventMove.bind(this), options);
|
||||
this.element.addEventListener('touchend', this.eventEnd.bind(this), options);
|
||||
}
|
||||
// 初始化规则,如果没有传参,则返默认.5
|
||||
initRules({ toBottom = 0, toTop = 0, toLeft = 0, toRight = 0 }) {
|
||||
if (!toRight && !toLeft && !toTop && !toBottom) {
|
||||
toRight = toLeft = toTop = toBottom = 0.3;
|
||||
}
|
||||
|
||||
this.rules = {
|
||||
toBottom,
|
||||
toTop,
|
||||
toLeft,
|
||||
toRight,
|
||||
};
|
||||
}
|
||||
isPassRules() {
|
||||
if (this.scrollDirection && (this.rules.toBottom || this.rules.toTop)) {
|
||||
return true;
|
||||
} else if (!this.scrollDirection && (this.rules.toRight || this.rules.toLeft)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getTargetTouche(event: TouchEvent) {
|
||||
return event.targetTouches[0];
|
||||
}
|
||||
|
||||
eventStart(event: TouchEvent) {
|
||||
this.event = event;
|
||||
const touch = this.getTargetTouche(event);
|
||||
|
||||
this.startPos = {
|
||||
x: touch.screenX,
|
||||
y: touch.screenY,
|
||||
time: +new Date(),
|
||||
};
|
||||
|
||||
this.movePos = [];
|
||||
this.endPos = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
time: 0,
|
||||
};
|
||||
this.scrollDirection = 0;
|
||||
this.isInit = false;
|
||||
this.isTouch = true;
|
||||
this.isTouchMove = false;
|
||||
|
||||
if (this.startCallback) {
|
||||
this.startCallback(event);
|
||||
}
|
||||
}
|
||||
|
||||
eventMove(event: TouchEvent) {
|
||||
this.event = event;
|
||||
|
||||
this.isTouchMove = true;
|
||||
|
||||
if (!this.isTouch) {
|
||||
return;
|
||||
}
|
||||
if (event.targetTouches.length > 1 || (event.scale && event.scale !== 1)) {
|
||||
return;
|
||||
}
|
||||
const touch = this.getTargetTouche(event);
|
||||
|
||||
this.endPos = {
|
||||
x: touch.screenX - this.startPos.x,
|
||||
y: touch.screenY - this.startPos.y,
|
||||
time: +new Date(),
|
||||
};
|
||||
|
||||
this.movePos.push(JSON.parse(JSON.stringify(this.endPos)));
|
||||
|
||||
if (!this.isInit) {
|
||||
this.isInit = true;
|
||||
this.scrollDirection = Math.abs(this.endPos.x) < Math.abs(this.endPos.y) ? 1 : 0;
|
||||
}
|
||||
|
||||
this.scrollDirectionSibling = this.scrollDirection ? (this.endPos.y < 0 ? 1 : -1) : this.endPos.x < 0 ? 1 : -1;
|
||||
|
||||
if (this.moveCallback && this.isPassRules()) {
|
||||
this.moveCallback(event);
|
||||
}
|
||||
}
|
||||
eventEnd(event: TouchEvent) {
|
||||
this.isTouch = false;
|
||||
if (!this.isPassRules() || !this.isTouchMove) {
|
||||
if (!this.isTouchMove) {
|
||||
this.eventType = 'click';
|
||||
} else {
|
||||
this.eventType = 'slide';
|
||||
}
|
||||
|
||||
if (this.rules.isClickCallback && this.endCallback) {
|
||||
this.endCallback(event);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.eventType = 'slide';
|
||||
|
||||
const movePos = this.movePos,
|
||||
movePosLen = movePos.length,
|
||||
endMovePos = movePos[movePosLen - 1],
|
||||
end2Second = movePos[movePosLen - 2];
|
||||
|
||||
let [toTop, toBottom, toLeft, toRight] = [false, false, false, false];
|
||||
if (endMovePos) {
|
||||
const rules = this.rules;
|
||||
const speed = end2Second
|
||||
? Math.abs(this.scrollDirection ? endMovePos.y - end2Second.y : endMovePos.x - end2Second.x) /
|
||||
(endMovePos.time - end2Second.time)
|
||||
: 0,
|
||||
slideSpeed = 2;
|
||||
const { innerHeight, innerWidth } = window;
|
||||
|
||||
if (this.scrollDirection) {
|
||||
if (this.scrollDirectionSibling >= 0) {
|
||||
toTop = speed > slideSpeed || rules['toTop'] * innerHeight <= Math.abs(endMovePos.y);
|
||||
} else {
|
||||
toBottom = speed > slideSpeed || rules['toBottom'] * innerHeight <= Math.abs(endMovePos.y);
|
||||
}
|
||||
} else {
|
||||
if (this.scrollDirectionSibling <= 0) {
|
||||
toRight = speed > slideSpeed || rules['toRight'] * innerWidth <= Math.abs(endMovePos.x);
|
||||
} else {
|
||||
toLeft = speed > slideSpeed || rules['toLeft'] * innerWidth <= Math.abs(endMovePos.x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.result = {
|
||||
toTop,
|
||||
toBottom,
|
||||
toLeft,
|
||||
toRight,
|
||||
};
|
||||
|
||||
if (this.endCallback) {
|
||||
this.endCallback(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TouchSlide;
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
Reference in New Issue
Block a user