543 lines
14 KiB
TypeScript
543 lines
14 KiB
TypeScript
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,
|
|
};
|
|
};
|