初始化

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