初始化
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import MediaApi from '@/service/media';
|
||||
import { showToast } from 'vant';
|
||||
import { onMounted, onUnmounted, Ref } from 'vue';
|
||||
import eventBus from '@/utils/eventBus';
|
||||
import { AcgEventBusKey } from '@/enums/app';
|
||||
|
||||
/**
|
||||
* @Description 收藏acg
|
||||
* @param {string} objID
|
||||
* @param {string} mediaType
|
||||
* @param {boolean} isCollect
|
||||
* @return {*}
|
||||
*/
|
||||
const handleCollected = async (objID: string, isCollect: boolean) => {
|
||||
try {
|
||||
return (isCollect ? MediaApi.postBookshelfAdd : MediaApi.postBookshelfDel)({
|
||||
id: objID,
|
||||
}).then(() => {
|
||||
showToast(isCollect ? '成功收藏' : '成功取消');
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @Description eventBus监听
|
||||
* @param {string} id
|
||||
* @param {Ref} hasCollected
|
||||
* @return {*}
|
||||
*/
|
||||
const Collected_on = (id: string, hasCollected: Ref<boolean>, callback?: any) => {
|
||||
const update = (bool: any) => {
|
||||
if (callback) {
|
||||
callback(bool);
|
||||
return;
|
||||
}
|
||||
hasCollected.value = bool;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on(AcgEventBusKey.Collected + id, update);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
eventBus.off(AcgEventBusKey.Collected + id, update);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @Description eventBus赋值
|
||||
* @param {string} id
|
||||
* @param {boolean} bool
|
||||
* @return {*}
|
||||
*/
|
||||
const Collected_emit = (id: string, bool: boolean) => {
|
||||
eventBus.emit(AcgEventBusKey.Collected + id, bool);
|
||||
};
|
||||
|
||||
export { handleCollected, Collected_on, Collected_emit };
|
||||
@@ -0,0 +1,129 @@
|
||||
import router from '@/router';
|
||||
const goTopicPage = (id: string, type: string | number, title: string, showType?: number) => {
|
||||
if (typeof type === 'number') {
|
||||
type = getMediaTypeKey(type);
|
||||
}
|
||||
router.push({ path: '/AcgTopicList', query: { id, type, title, showType } });
|
||||
};
|
||||
|
||||
const goAcgTagPage = (id: string, tagName: string, newsType?: string) => {
|
||||
router.push({ path: '/tagVideoList', query: { id, tagName, newsType } });
|
||||
};
|
||||
// kind的id对应字符串
|
||||
enum MediaType {
|
||||
video = 4,
|
||||
image = 5,
|
||||
text = 6,
|
||||
}
|
||||
type MediaTypeKey = keyof typeof MediaType;
|
||||
type MediaTypeValue = { [key in MediaType]: number };
|
||||
|
||||
function getMediaTypeKey(value: number): string {
|
||||
if (value === MediaType.video) {
|
||||
return 'video';
|
||||
} else if (value === MediaType.image) {
|
||||
return 'image';
|
||||
} else if (value === MediaType.text) {
|
||||
return 'text';
|
||||
// } else if (value === MediaType.theatre) {
|
||||
// return 'theatre';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getMediaTypeValue(value: string): MediaType | number {
|
||||
const mediaTypeKeys = Object.keys(MediaType).filter((key) => isNaN(Number(key)));
|
||||
const mediaTypeValues = mediaTypeKeys.map((key) => MediaType[key]);
|
||||
const index = mediaTypeKeys.indexOf(value);
|
||||
|
||||
if (index !== -1) {
|
||||
return mediaTypeValues[index];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const goFilterPage = () => {
|
||||
const route = router.currentRoute;
|
||||
const { mediaType = 'video' } = route.value.query;
|
||||
router.push({ path: '/AcgSearchFilter', query: { mediaType } });
|
||||
};
|
||||
//获取当前时间
|
||||
const getNewDate = () => {
|
||||
return new Date(new Date().getTime() + (window._diffTime || 0));
|
||||
};
|
||||
|
||||
const rankTabList = [
|
||||
{ title: '日榜', key: 1 },
|
||||
{ title: '周榜', key: 2 },
|
||||
{ title: '人气榜', key: 3 },
|
||||
{ title: '热度榜', key: 4 },
|
||||
{ title: '钻石榜', key: 5 },
|
||||
{ title: '连载榜', key: 6 },
|
||||
];
|
||||
|
||||
function getElementDistance(element: HTMLElement, parent: HTMLElement) {
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
|
||||
const distance = {
|
||||
top: elementRect.top - parentRect.top,
|
||||
right: parentRect.right - elementRect.right,
|
||||
bottom: parentRect.bottom - elementRect.bottom,
|
||||
left: elementRect.left - parentRect.left,
|
||||
};
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
const goDetailPage = (mediaType: MediaTypeKey, id: string, mediaSubType?: number) => {
|
||||
let name = 'AcgImageDetail';
|
||||
if (mediaType === 'text') {
|
||||
name = 'AcgTextDetail';
|
||||
} else if (mediaType === 'video') {
|
||||
name = 'AcgVideoDetail';
|
||||
}
|
||||
|
||||
router.push({
|
||||
name,
|
||||
query: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const freeSet = new Set();
|
||||
|
||||
const getUpdateStatusText = (updateStatus?: number) => {
|
||||
if (updateStatus === 2) return '已完结';
|
||||
return '连载中';
|
||||
};
|
||||
|
||||
/** 按 id(无 id 则按 name)去重,保留首次出现顺序 */
|
||||
const dedupeTagDetails = (tags?: Types.Media.TagDetailsItem[] | null): Types.Media.TagDetailsItem[] => {
|
||||
if (!tags?.length) return [];
|
||||
const seen = new Set<string>();
|
||||
return tags.filter((item) => {
|
||||
const key = String(item.name || '').trim();
|
||||
if (!key || seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
goTopicPage,
|
||||
goFilterPage,
|
||||
getMediaTypeKey,
|
||||
getMediaTypeValue,
|
||||
getNewDate,
|
||||
rankTabList,
|
||||
getElementDistance,
|
||||
goDetailPage,
|
||||
freeSet,
|
||||
getUpdateStatusText,
|
||||
goAcgTagPage,
|
||||
dedupeTagDetails,
|
||||
};
|
||||
export type { MediaTypeKey, MediaTypeValue };
|
||||
@@ -0,0 +1,66 @@
|
||||
import { ref, Ref } from 'vue';
|
||||
import { getVideoHistoryByKey, addVideoHistory } from '@/utils/index';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import eventBus from '@/utils/eventBus';
|
||||
|
||||
const AcgHistoryData = {} as {
|
||||
[key: string]: {
|
||||
[key: string]: Types.Media.MediaInfo;
|
||||
};
|
||||
};
|
||||
|
||||
const key = 'AcgHistoryData_';
|
||||
|
||||
const getData = async (id: string, dbType = HistoryVideoType.AcgImage, isParent: boolean) => {
|
||||
if (!AcgHistoryData[dbType]) {
|
||||
AcgHistoryData[dbType] = {};
|
||||
}
|
||||
let item = AcgHistoryData[dbType][id];
|
||||
if (item && item.id) {
|
||||
return item;
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
AcgHistoryData[dbType][id] = { id: '' } as any;
|
||||
item = (await getVideoHistoryByKey(id, dbType)) as any;
|
||||
} else {
|
||||
item = (await new Promise((resolve) => {
|
||||
eventBus.on(key + id, (obj: Types.Media.MediaInfo) => {
|
||||
eventBus.off(key + id);
|
||||
resolve(obj);
|
||||
});
|
||||
})) as any;
|
||||
}
|
||||
|
||||
if (item && item.id) {
|
||||
AcgHistoryData[dbType][id] = item;
|
||||
eventBus.emit(key + id, item);
|
||||
}
|
||||
return item;
|
||||
};
|
||||
|
||||
export default (dbType = HistoryVideoType.AcgImage, isParent: boolean) => {
|
||||
const historyEpisodeNumber = ref(0);
|
||||
const historyData = ref({}) as Ref<Types.Media.MediaInfo>;
|
||||
|
||||
const getHistoryData = async (id: string) => {
|
||||
const history = (await getData(id, dbType, isParent)) as any;
|
||||
if (history) {
|
||||
historyData.value = history;
|
||||
if (history.historyEpisodeNumber) {
|
||||
historyEpisodeNumber.value = history.historyEpisodeNumber;
|
||||
}
|
||||
} else {
|
||||
historyEpisodeNumber.value = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const setHistoryData = (data: any, dbType: HistoryVideoType) => {
|
||||
const item = (AcgHistoryData[dbType] && AcgHistoryData[dbType][data.id]) || {};
|
||||
const newItem = Object.assign(item, data);
|
||||
AcgHistoryData[dbType][data.id] = newItem;
|
||||
eventBus.emit(key + data.id, newItem);
|
||||
addVideoHistory(newItem, dbType, new Date().getTime());
|
||||
};
|
||||
return { historyData, historyEpisodeNumber, getHistoryData, setHistoryData };
|
||||
};
|
||||
@@ -0,0 +1,542 @@
|
||||
import { reactive, ref, ComputedRef, watchEffect, watch, nextTick, computed, onBeforeUnmount } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import indexDB from './indexDB';
|
||||
import { showToast } from 'vant';
|
||||
import AcgApi from '@/service/acg';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { freeSet } from './common';
|
||||
interface ReadData {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export default ({
|
||||
mediaId = '',
|
||||
isScrollTop = true,
|
||||
isGetInfo = true,
|
||||
isUpdate = true,
|
||||
isNeedHistory = true,
|
||||
isGetHistoryInit = false,
|
||||
pageSize = 30,
|
||||
dbType = HistoryVideoType.AcgImage,
|
||||
}) => {
|
||||
const userStore = useUserStore();
|
||||
const { openLoading, closeLoading } = useLoading();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { id, episodeNumber, title } = route.query;
|
||||
|
||||
const state = reactive({
|
||||
list: [] as Types.Acg.MediaContentListItem[],
|
||||
info: {} as Types.Acg.MediaContentInfo,
|
||||
validateItem: {} as Types.Acg.MediaContentListItem,
|
||||
episodeNumber: Number(episodeNumber || '1'),
|
||||
id: (mediaId || id) as string,
|
||||
title,
|
||||
gradingGroupIndex: 0,
|
||||
});
|
||||
|
||||
const { historyData, historyEpisodeNumber, getHistoryData, setHistoryData } = indexDB(dbType, true);
|
||||
const addHistory = (num: number) => {
|
||||
if (num === -1) {
|
||||
num = state.episodeNumber;
|
||||
}
|
||||
const data = { ...historyData.value, historyEpisodeNumber: num, historyReadData };
|
||||
setHistoryData(data, dbType);
|
||||
};
|
||||
|
||||
const historyReadData = [] as Types.Acg.ReadData[];
|
||||
|
||||
const addHistoryReadData = (num: number, obj: ReadData, imgObj?: { url: string; width: number; height: number }) => {
|
||||
const listItem = state.list.find((item) => item.episodeNumber === num);
|
||||
if (!listItem) return;
|
||||
|
||||
const index = (historyReadData || []).findIndex((item) => item.episodeNumber === num);
|
||||
const isHas = index > -1;
|
||||
|
||||
const imgs = isHas ? historyReadData[index].imgs || {} : {};
|
||||
if (imgObj?.url) {
|
||||
imgs[imgObj?.url] = {
|
||||
width: imgObj?.width,
|
||||
height: imgObj?.height,
|
||||
};
|
||||
}
|
||||
let item = {};
|
||||
if (isHas) {
|
||||
item = historyReadData.splice(index, 1)[0];
|
||||
}
|
||||
|
||||
const newItem = Object.assign({}, { ...item }, { ...listItem, ...obj, imgs });
|
||||
historyReadData.push(newItem);
|
||||
};
|
||||
|
||||
if (isUpdate) {
|
||||
onBeforeUnmount(() => {
|
||||
nextTick(() => {
|
||||
addHistory(-1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const currentItem = computed(() => {
|
||||
if (state.list.length === 0) return {};
|
||||
return state.list.find((item) => item.episodeNumber === state.episodeNumber) || {};
|
||||
}) as ComputedRef<Types.Acg.MediaContentListItem>;
|
||||
|
||||
let initScrollLeft = true;
|
||||
watch(currentItem, () => {
|
||||
if (initScrollLeft) {
|
||||
goSectionScrollLeft();
|
||||
initScrollLeft = false;
|
||||
}
|
||||
if (['AcgTextDetail', 'AcgImageDetail'].includes((route.name as string) || '')) {
|
||||
return;
|
||||
}
|
||||
validateHasBuy(currentItem.value);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => state.episodeNumber,
|
||||
() => {
|
||||
addHistory(state.episodeNumber);
|
||||
},
|
||||
);
|
||||
|
||||
const getPageNumber = () => {
|
||||
return Math.ceil(state.episodeNumber / 30);
|
||||
};
|
||||
const pageNumber = getPageNumber();
|
||||
|
||||
const params = {
|
||||
prevPageNumber: pageNumber,
|
||||
nextPageNumber: pageNumber,
|
||||
pageSize,
|
||||
};
|
||||
|
||||
const moreParams = reactive({
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
finished: false,
|
||||
isNoData: false,
|
||||
error: false,
|
||||
isHigehtMax: false,
|
||||
});
|
||||
|
||||
const moreParams_finished = {
|
||||
prev: false,
|
||||
next: false,
|
||||
};
|
||||
|
||||
const getList = async (type: 'prev' | 'next', pageNumber?: number) => {
|
||||
try {
|
||||
const tempParams = {
|
||||
mediaId: state.id,
|
||||
pageSize: params.pageSize,
|
||||
pageNumber: pageNumber ? pageNumber : type === 'prev' ? params.prevPageNumber : params.nextPageNumber,
|
||||
};
|
||||
let res: any = await AcgApi.getMediaContentList(tempParams);
|
||||
if ((res.list || []).length === 0 && state.list.length === 0 && tempParams.pageNumber !== 1) {
|
||||
params.prevPageNumber = 1;
|
||||
params.nextPageNumber = 1;
|
||||
tempParams.pageNumber = 1;
|
||||
res = await AcgApi.getMediaContentList(tempParams);
|
||||
}
|
||||
|
||||
historyData.value.currentEpisode = res.total;
|
||||
|
||||
moreParams.loading = false;
|
||||
moreParams.refreshing = false;
|
||||
|
||||
let list = res.list || [];
|
||||
if (list.length > 0) {
|
||||
const reList = (list || []).reduce(
|
||||
(acc: Types.Acg.MediaContentListItem[], current: Types.Acg.MediaContentListItem) => {
|
||||
const x = acc.find((item) => item.episodeNumber === current.episodeNumber);
|
||||
if (!x) {
|
||||
return acc.concat([current]);
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
if (isInitGetList && res.hasNext && list.length !== reList.length) {
|
||||
list = reList;
|
||||
onLoad();
|
||||
}
|
||||
}
|
||||
isInitGetList = false;
|
||||
|
||||
if (state.list.length === 0) {
|
||||
state.list = list;
|
||||
} else {
|
||||
state.list = state.list.concat(list);
|
||||
}
|
||||
if (tempParams.pageNumber == 1 && state.list.length == 0) {
|
||||
moreParams.isNoData = true;
|
||||
}
|
||||
if (!res.hasNext) {
|
||||
moreParams_finished[type] = true;
|
||||
moreParams.finished = type === 'next' && sort.value;
|
||||
}
|
||||
return list;
|
||||
} catch (e) {
|
||||
moreParams.loading = false;
|
||||
moreParams.error = true;
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const onLoad = () => {
|
||||
moreParams.loading = true;
|
||||
if (sort.value) {
|
||||
params.nextPageNumber += 1;
|
||||
getList('next');
|
||||
} else {
|
||||
if (params.prevPageNumber === 1) {
|
||||
moreParams.loading = false;
|
||||
return;
|
||||
}
|
||||
params.prevPageNumber -= 1;
|
||||
getList('prev');
|
||||
}
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
if (sort.value) {
|
||||
if (params.prevPageNumber == 1) {
|
||||
moreParams.refreshing = false;
|
||||
return;
|
||||
}
|
||||
params.prevPageNumber -= 1;
|
||||
moreParams.refreshing = true;
|
||||
moreParams.finished = false;
|
||||
getList('prev');
|
||||
} else {
|
||||
if (moreParams_finished['next']) {
|
||||
moreParams.refreshing = false;
|
||||
return;
|
||||
}
|
||||
params.nextPageNumber += 1;
|
||||
moreParams.refreshing = true;
|
||||
getList('next');
|
||||
}
|
||||
};
|
||||
let isInitGetList = true;
|
||||
const init = () => {
|
||||
getList('next');
|
||||
};
|
||||
|
||||
if (isNeedHistory) {
|
||||
getHistoryData(state.id).then(() => {
|
||||
historyReadData.push(...JSON.parse(JSON.stringify(historyData.value.historyReadData || '[]')));
|
||||
if (isGetHistoryInit) {
|
||||
if (!episodeNumber) {
|
||||
state.episodeNumber = historyData.value.historyEpisodeNumber || state.episodeNumber;
|
||||
}
|
||||
|
||||
const pageNumber = getPageNumber();
|
||||
|
||||
params.prevPageNumber = pageNumber;
|
||||
params.nextPageNumber = pageNumber;
|
||||
}
|
||||
|
||||
init();
|
||||
});
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
const getContent = async () => {
|
||||
if (state.info.id) {
|
||||
openLoading();
|
||||
}
|
||||
|
||||
await AcgApi.getMediaContentInfo({ id: currentItem.value.id }).then((res) => {
|
||||
state.info = res;
|
||||
});
|
||||
|
||||
closeLoading();
|
||||
};
|
||||
|
||||
if (isGetInfo) {
|
||||
watch(
|
||||
() => currentItem.value.id,
|
||||
(newId, oldId) => {
|
||||
if (!newId || newId === oldId) return;
|
||||
getContent();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const statusText = computed(() => {
|
||||
switch (historyData.value.updateStatus) {
|
||||
case 1:
|
||||
return '连载中';
|
||||
case 2:
|
||||
return '已完结';
|
||||
default:
|
||||
return '已完结';
|
||||
}
|
||||
});
|
||||
|
||||
//目录
|
||||
const visibleCatalogue = ref(false);
|
||||
const sort = ref(true);
|
||||
const autoList = computed(() => {
|
||||
return (state.list || []).sort(
|
||||
sort.value ? (a, b) => a.episodeNumber - b.episodeNumber : (a, b) => b.episodeNumber - a.episodeNumber,
|
||||
);
|
||||
});
|
||||
|
||||
const scrollToElement = (elementId: string) => {
|
||||
const element = document.getElementById(elementId) as HTMLElement;
|
||||
if (!element) return;
|
||||
const parent = element.closest('.list') as HTMLElement;
|
||||
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
|
||||
const offsetTop = elementRect.top - parentRect.top;
|
||||
const offsetBottom = elementRect.bottom - parentRect.bottom;
|
||||
|
||||
if (offsetTop < 0 || offsetBottom > 0) {
|
||||
// parent.scrollTop = element.offsetTop - (parent.offsetHeight - element.offsetHeight) / 2;
|
||||
parent.scrollTop = element.offsetTop - parent.offsetHeight / 2;
|
||||
}
|
||||
};
|
||||
const goSectionScrollTop = () => {
|
||||
if (!isScrollTop) return;
|
||||
nextTick(() => {
|
||||
scrollToElement(`section_${state.episodeNumber}`);
|
||||
});
|
||||
};
|
||||
|
||||
let isInit = false;
|
||||
|
||||
watchEffect(() => {
|
||||
if (!visibleCatalogue.value) return;
|
||||
|
||||
if (isInit) {
|
||||
return;
|
||||
}
|
||||
|
||||
isInit = true;
|
||||
|
||||
goSectionScrollTop();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => sort.value,
|
||||
() => {
|
||||
moreParams.finished = moreParams_finished[sort.value ? 'next' : 'prev'];
|
||||
goSectionScrollTop();
|
||||
},
|
||||
);
|
||||
|
||||
//支付
|
||||
const dvPayForWatch = ref();
|
||||
const visiblePayForWatch = ref(false);
|
||||
|
||||
const validatePermission = () => {
|
||||
// 购买优先
|
||||
if (historyData.value.id && historyData.value.mediaStatus && historyData.value.mediaStatus.hasPaid) return true;
|
||||
// 会员
|
||||
if (historyData.value.permission === 0 && userStore.isVip) return true;
|
||||
// 付费
|
||||
if (historyData.value.permission === 1) {
|
||||
if (!historyData.value.price) {
|
||||
return true;
|
||||
}
|
||||
if (userStore.userInfo.vipLevel >= 2) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 免费 || 前几集免费
|
||||
if (historyData.value.permission === 2 || historyData.value.freeEpisode >= state.validateItem.episodeNumber)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const validateHasBuy = (item: Types.Acg.MediaContentListItem) => {
|
||||
state.validateItem = item;
|
||||
if (!validatePermission()) {
|
||||
visiblePayForWatch.value = true;
|
||||
// showToast('付费章节,购买后才能观看喔');
|
||||
return true;
|
||||
}
|
||||
visiblePayForWatch.value = false;
|
||||
return false;
|
||||
};
|
||||
|
||||
const buySuccess = (type = 'one') => {
|
||||
state.info.hasBuy = true;
|
||||
if (state.validateItem.id) {
|
||||
state.validateItem.hasBuy = true;
|
||||
state.episodeNumber = state.validateItem.episodeNumber;
|
||||
}
|
||||
visiblePayForWatch.value = false;
|
||||
visibleCatalogue.value = false;
|
||||
if (type === 'all') {
|
||||
state.list.forEach((item) => {
|
||||
item.hasBuy = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// watch(
|
||||
// dvPayForWatch,
|
||||
// (el) => {
|
||||
// debugger;
|
||||
// if (!el) return;
|
||||
// validateHasBuy(currentItem.value);
|
||||
// },
|
||||
// // {
|
||||
// // once: true,
|
||||
// // },
|
||||
// );
|
||||
|
||||
const closePayForWatch = () => {
|
||||
if (validatePermission()) {
|
||||
visiblePayForWatch.value = false;
|
||||
return;
|
||||
}
|
||||
router.back();
|
||||
};
|
||||
|
||||
const loadEpisodeList = async (num: number, type: 'prev' | 'next') => {
|
||||
const item = state.list.find((item) => item.episodeNumber === num);
|
||||
if (item) return item;
|
||||
|
||||
openLoading();
|
||||
await getList(type);
|
||||
closeLoading();
|
||||
|
||||
return state.list.find((item) => item.episodeNumber === num);
|
||||
};
|
||||
|
||||
const replaceRouter = (num: number) => {
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
episodeNumber: num,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const switchEpisodeNumber = async (num: number, isValidate = true) => {
|
||||
|
||||
if (num === -1) {
|
||||
if (state.episodeNumber !== 1) {
|
||||
const prevNum = state.episodeNumber - 1;
|
||||
const item = await loadEpisodeList(prevNum, 'prev');
|
||||
if (isValidate) {
|
||||
if (validateHasBuy(item as Types.Acg.MediaContentListItem)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
state.episodeNumber = prevNum;
|
||||
if (dbType === HistoryVideoType.AcgText) {
|
||||
addHistoryReadData(prevNum, { progress: 0, scrollTop: 0 });
|
||||
addHistory(prevNum);
|
||||
}
|
||||
replaceRouter(prevNum);
|
||||
return true;
|
||||
} else {
|
||||
showToast('这是第一章了喔~');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (state.episodeNumber < historyData.value.currentEpisode) {
|
||||
const nextNum = state.episodeNumber + 1;
|
||||
const item = await loadEpisodeList(nextNum, 'next');
|
||||
if (isValidate) {
|
||||
if (validateHasBuy(item as Types.Acg.MediaContentListItem)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
state.episodeNumber = nextNum;
|
||||
if (dbType === HistoryVideoType.AcgText) {
|
||||
addHistoryReadData(nextNum, { progress: 0, scrollTop: 0 });
|
||||
addHistory(nextNum);
|
||||
}
|
||||
replaceRouter(nextNum);
|
||||
return true;
|
||||
} else {
|
||||
showToast('已经到最后一章了喔~');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const switchEpisodeNumberCatalogue = (item: Types.Acg.MediaContentListItem) => {
|
||||
if (validateHasBuy(item)) return;
|
||||
|
||||
visibleCatalogue.value = false;
|
||||
if (dbType === HistoryVideoType.AcgText) {
|
||||
addHistoryReadData(item.episodeNumber, { progress: 0, scrollTop: 0 });
|
||||
addHistory(item.episodeNumber);
|
||||
}
|
||||
state.episodeNumber = item.episodeNumber;
|
||||
replaceRouter(item.episodeNumber);
|
||||
};
|
||||
|
||||
const scrollToElement2 = (elementId: string) => {
|
||||
const element = document.getElementById(elementId) as HTMLElement;
|
||||
if (!element) return;
|
||||
const parent = element.closest('.episode-box') as HTMLElement;
|
||||
const parentRect = parent.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
|
||||
const offsetLeft = elementRect.left - parentRect.left;
|
||||
const offsetRight = elementRect.right - parentRect.right;
|
||||
|
||||
if (offsetLeft < 0 || offsetRight > 0) {
|
||||
parent.scrollLeft = element.offsetLeft - parent.offsetWidth / 2;
|
||||
}
|
||||
};
|
||||
const goSectionScrollLeft = () => {
|
||||
const item = state.list.find((item) => item.episodeNumber === state.episodeNumber);
|
||||
if (!item) return;
|
||||
nextTick(() => {
|
||||
scrollToElement2(`episode-${item.id}`);
|
||||
});
|
||||
};
|
||||
const handlePopupSwitchEpisode = (num: number) => {
|
||||
state.episodeNumber = num;
|
||||
visibleCatalogue.value = false;
|
||||
|
||||
goSectionScrollLeft();
|
||||
};
|
||||
|
||||
return {
|
||||
state,
|
||||
currentItem,
|
||||
statusText,
|
||||
moreParams,
|
||||
onLoad,
|
||||
onRefresh,
|
||||
getList,
|
||||
visibleCatalogue,
|
||||
handlePopupSwitchEpisode,
|
||||
sort,
|
||||
autoList,
|
||||
dvPayForWatch,
|
||||
visiblePayForWatch,
|
||||
validateHasBuy,
|
||||
buySuccess,
|
||||
closePayForWatch,
|
||||
switchEpisodeNumber,
|
||||
historyData,
|
||||
historyEpisodeNumber,
|
||||
addHistory,
|
||||
addHistoryReadData,
|
||||
switchEpisodeNumberCatalogue,
|
||||
validatePermission,
|
||||
goSectionScrollTop,
|
||||
replaceRouter,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { reactive, computed, onActivated, onUnmounted, provide } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import MediaApi from '@/service/media';
|
||||
import { HistoryVideoType } from '@/enums/app';
|
||||
import indexDB from './indexDB';
|
||||
import { dedupeTagDetails } from './common';
|
||||
// import { showToast } from 'vant';
|
||||
|
||||
export default ({ id = '', dbType = HistoryVideoType.AcgImage }) => {
|
||||
const route = useRoute();
|
||||
const madieId = id || (route.query.id as string);
|
||||
//内容
|
||||
const state = reactive({
|
||||
info: {} as Types.Media.MediaInfo,
|
||||
episodeNumber: Number(route.query.episode) || 1,
|
||||
advsList: <any>[],
|
||||
timer: 0,
|
||||
isShowAdv: true,
|
||||
countdowner: <any>null,
|
||||
});
|
||||
|
||||
const { historyData, historyEpisodeNumber, getHistoryData, setHistoryData } = indexDB(dbType, true);
|
||||
provide('historyData', historyData);
|
||||
const addHistory = (num = 0) => {
|
||||
const historyReadData = historyData.value.historyReadData || [];
|
||||
const obj = { ...historyData.value, ...state.info, historyReadData };
|
||||
if (num > 0) {
|
||||
obj.historyEpisodeNumber = num;
|
||||
}
|
||||
setHistoryData(obj, dbType);
|
||||
};
|
||||
|
||||
onActivated(() => {
|
||||
if (!state.info.id) return;
|
||||
setTimeout(() => {
|
||||
getHistoryData(madieId);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
getHistoryData(madieId).then(() => {
|
||||
addHistory();
|
||||
});
|
||||
});
|
||||
|
||||
const getData = async () => {
|
||||
await getHistoryData(madieId);
|
||||
const res = await MediaApi.getMediaInfo({ id: madieId });
|
||||
const info = res as Types.Media.MediaInfo;
|
||||
if (info.tagDetails?.length) {
|
||||
info.tagDetails = dedupeTagDetails(info.tagDetails);
|
||||
}
|
||||
state.info = info;
|
||||
|
||||
addHistory();
|
||||
if (!historyData.value.id) {
|
||||
historyData.value = state.info;
|
||||
}
|
||||
};
|
||||
getData();
|
||||
|
||||
const statusText = computed(() => {
|
||||
switch (state.info.updateStatus) {
|
||||
case 1:
|
||||
return '连载中';
|
||||
case 2:
|
||||
return '已完结';
|
||||
default:
|
||||
return '已完结';
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
state,
|
||||
historyData,
|
||||
addHistory,
|
||||
historyEpisodeNumber,
|
||||
statusText,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { onMounted, onUnmounted, Ref } from 'vue';
|
||||
import eventBus from '@/utils/eventBus';
|
||||
import { AcgEventBusKey } from '@/enums/app';
|
||||
|
||||
interface PayInfo {
|
||||
type: 'one' | 'all';
|
||||
productID: string;
|
||||
isFree: boolean;
|
||||
}
|
||||
|
||||
const Pay_parent_on = ({
|
||||
id,
|
||||
state,
|
||||
historyData,
|
||||
addHistory,
|
||||
}: {
|
||||
id: string;
|
||||
state: {
|
||||
info: Types.Media.MediaInfo;
|
||||
};
|
||||
historyData: Ref<Types.Media.MediaInfo>;
|
||||
addHistory: any;
|
||||
}) => {
|
||||
const update = () => {
|
||||
state.info.mediaStatus.hasPaid = true;
|
||||
historyData.value.mediaStatus.hasPaid = true;
|
||||
addHistory();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on(AcgEventBusKey.HasPaid + id, update as any);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
eventBus.off(AcgEventBusKey.HasPaid + id, update as any);
|
||||
});
|
||||
};
|
||||
|
||||
const Pay_child_on = (parentId: string, state: any) => {
|
||||
const update = (id: any) => {
|
||||
const item = (state.list as Types.Acg.MediaContentListItem[]).find((item) => item.id === id);
|
||||
if (item) {
|
||||
item.hasBuy = true;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on(AcgEventBusKey.HasBuy + parentId, update);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
eventBus.off(AcgEventBusKey.HasBuy + parentId, update);
|
||||
});
|
||||
};
|
||||
|
||||
const Pay_emit = (id: string, obj: PayInfo) => {
|
||||
if (obj.type === 'one') {
|
||||
eventBus.emit(AcgEventBusKey.HasBuy + id, obj.productID);
|
||||
} else {
|
||||
eventBus.emit(AcgEventBusKey.HasPaid + id);
|
||||
}
|
||||
};
|
||||
|
||||
export { Pay_parent_on, Pay_child_on, Pay_emit };
|
||||
export type { PayInfo };
|
||||
@@ -0,0 +1,242 @@
|
||||
interface Rules {
|
||||
toBottom: number;
|
||||
toTop: number;
|
||||
toLeft: number;
|
||||
toRight: number;
|
||||
isClickCallback?: boolean;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
time: number;
|
||||
}
|
||||
export interface Result {
|
||||
toTop: boolean;
|
||||
toBottom: boolean;
|
||||
toLeft: boolean;
|
||||
toRight: boolean;
|
||||
}
|
||||
class TouchSlide {
|
||||
element;
|
||||
startCallback: ((event: TouchEvent) => void) | undefined;
|
||||
moveCallback: ((event: TouchEvent) => void) | undefined;
|
||||
endCallback: ((event: TouchEvent) => void) | undefined;
|
||||
isTouch;
|
||||
isTouchMove;
|
||||
|
||||
event: TouchEvent | undefined;
|
||||
rules = {
|
||||
toBottom: 0,
|
||||
toTop: 0,
|
||||
toLeft: 0,
|
||||
toRight: 0,
|
||||
isClickCallback: false,
|
||||
} as Rules;
|
||||
scrollDirection: number | undefined;
|
||||
startPos = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
time: 0,
|
||||
} as Position;
|
||||
movePos = [] as Position[];
|
||||
endPos = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
time: 0,
|
||||
} as Position;
|
||||
isInit = false;
|
||||
scrollDirectionSibling = 0;
|
||||
result = {
|
||||
toTop: false,
|
||||
toBottom: false,
|
||||
toLeft: false,
|
||||
toRight: false,
|
||||
};
|
||||
eventType = 'click';
|
||||
|
||||
constructor({
|
||||
element,
|
||||
rules,
|
||||
startCallback,
|
||||
moveCallback,
|
||||
endCallback,
|
||||
}: {
|
||||
element: HTMLElement;
|
||||
rules?: Rules;
|
||||
startCallback?: (event: TouchEvent) => void;
|
||||
moveCallback?: (event: TouchEvent) => void;
|
||||
endCallback?: (event: TouchEvent) => void;
|
||||
startPos?: Position;
|
||||
movePos?: Position[];
|
||||
endPos?: Position;
|
||||
scrollDirection?: number;
|
||||
scrollDirectionSibling?: number;
|
||||
result?: Result;
|
||||
eventType?: 'click' | 'slide';
|
||||
}) {
|
||||
this.element = element;
|
||||
this.startCallback = startCallback;
|
||||
this.moveCallback = moveCallback;
|
||||
this.endCallback = endCallback;
|
||||
|
||||
this.isTouch = false;
|
||||
this.isTouchMove = false;
|
||||
this.initRules(rules || {});
|
||||
|
||||
const options = {
|
||||
capture: false,
|
||||
passive: false,
|
||||
};
|
||||
|
||||
this.element.addEventListener('touchstart', this.eventStart.bind(this), options);
|
||||
this.element.addEventListener('touchmove', this.eventMove.bind(this), options);
|
||||
this.element.addEventListener('touchend', this.eventEnd.bind(this), options);
|
||||
}
|
||||
// 初始化规则,如果没有传参,则返默认.5
|
||||
initRules({ toBottom = 0, toTop = 0, toLeft = 0, toRight = 0 }) {
|
||||
if (!toRight && !toLeft && !toTop && !toBottom) {
|
||||
toRight = toLeft = toTop = toBottom = 0.3;
|
||||
}
|
||||
|
||||
this.rules = {
|
||||
toBottom,
|
||||
toTop,
|
||||
toLeft,
|
||||
toRight,
|
||||
};
|
||||
}
|
||||
isPassRules() {
|
||||
if (this.scrollDirection && (this.rules.toBottom || this.rules.toTop)) {
|
||||
return true;
|
||||
} else if (!this.scrollDirection && (this.rules.toRight || this.rules.toLeft)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getTargetTouche(event: TouchEvent) {
|
||||
return event.targetTouches[0];
|
||||
}
|
||||
|
||||
eventStart(event: TouchEvent) {
|
||||
this.event = event;
|
||||
const touch = this.getTargetTouche(event);
|
||||
|
||||
this.startPos = {
|
||||
x: touch.screenX,
|
||||
y: touch.screenY,
|
||||
time: +new Date(),
|
||||
};
|
||||
|
||||
this.movePos = [];
|
||||
this.endPos = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
time: 0,
|
||||
};
|
||||
this.scrollDirection = 0;
|
||||
this.isInit = false;
|
||||
this.isTouch = true;
|
||||
this.isTouchMove = false;
|
||||
|
||||
if (this.startCallback) {
|
||||
this.startCallback(event);
|
||||
}
|
||||
}
|
||||
|
||||
eventMove(event: TouchEvent) {
|
||||
this.event = event;
|
||||
|
||||
this.isTouchMove = true;
|
||||
|
||||
if (!this.isTouch) {
|
||||
return;
|
||||
}
|
||||
if (event.targetTouches.length > 1 || (event.scale && event.scale !== 1)) {
|
||||
return;
|
||||
}
|
||||
const touch = this.getTargetTouche(event);
|
||||
|
||||
this.endPos = {
|
||||
x: touch.screenX - this.startPos.x,
|
||||
y: touch.screenY - this.startPos.y,
|
||||
time: +new Date(),
|
||||
};
|
||||
|
||||
this.movePos.push(JSON.parse(JSON.stringify(this.endPos)));
|
||||
|
||||
if (!this.isInit) {
|
||||
this.isInit = true;
|
||||
this.scrollDirection = Math.abs(this.endPos.x) < Math.abs(this.endPos.y) ? 1 : 0;
|
||||
}
|
||||
|
||||
this.scrollDirectionSibling = this.scrollDirection ? (this.endPos.y < 0 ? 1 : -1) : this.endPos.x < 0 ? 1 : -1;
|
||||
|
||||
if (this.moveCallback && this.isPassRules()) {
|
||||
this.moveCallback(event);
|
||||
}
|
||||
}
|
||||
eventEnd(event: TouchEvent) {
|
||||
this.isTouch = false;
|
||||
if (!this.isPassRules() || !this.isTouchMove) {
|
||||
if (!this.isTouchMove) {
|
||||
this.eventType = 'click';
|
||||
} else {
|
||||
this.eventType = 'slide';
|
||||
}
|
||||
|
||||
if (this.rules.isClickCallback && this.endCallback) {
|
||||
this.endCallback(event);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.eventType = 'slide';
|
||||
|
||||
const movePos = this.movePos,
|
||||
movePosLen = movePos.length,
|
||||
endMovePos = movePos[movePosLen - 1],
|
||||
end2Second = movePos[movePosLen - 2];
|
||||
|
||||
let [toTop, toBottom, toLeft, toRight] = [false, false, false, false];
|
||||
if (endMovePos) {
|
||||
const rules = this.rules;
|
||||
const speed = end2Second
|
||||
? Math.abs(this.scrollDirection ? endMovePos.y - end2Second.y : endMovePos.x - end2Second.x) /
|
||||
(endMovePos.time - end2Second.time)
|
||||
: 0,
|
||||
slideSpeed = 2;
|
||||
const { innerHeight, innerWidth } = window;
|
||||
|
||||
if (this.scrollDirection) {
|
||||
if (this.scrollDirectionSibling >= 0) {
|
||||
toTop = speed > slideSpeed || rules['toTop'] * innerHeight <= Math.abs(endMovePos.y);
|
||||
} else {
|
||||
toBottom = speed > slideSpeed || rules['toBottom'] * innerHeight <= Math.abs(endMovePos.y);
|
||||
}
|
||||
} else {
|
||||
if (this.scrollDirectionSibling <= 0) {
|
||||
toRight = speed > slideSpeed || rules['toRight'] * innerWidth <= Math.abs(endMovePos.x);
|
||||
} else {
|
||||
toLeft = speed > slideSpeed || rules['toLeft'] * innerWidth <= Math.abs(endMovePos.x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.result = {
|
||||
toTop,
|
||||
toBottom,
|
||||
toLeft,
|
||||
toRight,
|
||||
};
|
||||
|
||||
if (this.endCallback) {
|
||||
this.endCallback(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TouchSlide;
|
||||
Reference in New Issue
Block a user