You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

280 lines
11 KiB
Vue

<script>
const { ref, watch, computed, onMounted, nextTick } = require('vue');
function throttle(fn, delay) {
let timer = null;
return function () {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay);
}
}
}
function isMobile() {
return window.innerWidth <= 576;
}
module.exports = {
compatConfig: {
MODE: 3
},
compilerOptions: {
whitespace: 'condense'
},
props: {
pageListLoader: {
type: Object,
default: null,
},
autoFocus: {
type: Boolean,
default: false,
},
lazyLoad: {
type: Boolean,
default: false
}
},
setup(props) {
const containerRef = ref();
let initLoaded = false;
const pageListLoading = ref(true);
const contentLoading = ref(false);
const pageList = ref([]);
const selectedPageIdx = ref(null);
const previewPageContent = ref('');
const selectedPageUrl = ref('#');
const bookletClassList = ref([]);
const onSelectPage = (idx) => {
selectedPageIdx.value = idx;
}
const selectedPageInfo = computed(() => {
if (selectedPageIdx.value !== null && selectedPageIdx.value < pageList.value.length) {
return pageList.value[selectedPageIdx.value];
} else {
return '';
}
});
const loadPageList = () => {
props.pageListLoader.loadPageList().then((loadedPageList) => {
pageListLoading.value = false;
pageList.value = loadedPageList;
initLoaded = true;
// 自动选中第一项
if (!isMobile() && selectedPageIdx.value === null && props.autoFocus && pageList.value.length > 0) {
selectedPageIdx.value = 0;
}
}).catch((err) => {
console.error(err);
});
}
const loadPageContent = () => {
contentLoading.value = true;
selectedPageUrl.value = '#';
props.pageListLoader.loadPageContent(selectedPageInfo.value).then((pageContent) => {
contentLoading.value = false;
previewPageContent.value = pageContent;
props.pageListLoader.getPageUrl(selectedPageInfo.value).then((pageUrl) => {
selectedPageUrl.value = pageUrl;
}).catch((err) => {
console.error(err);
});
}).catch((err) => {
contentLoading.value = false;
console.error(err);
});
}
// 处理Lazy Load
const onWindowScroll = throttle(() => {
if (initLoaded) {
window.removeEventListener('scroll', onWindowScroll, { passive: true });
return;
}
let container = containerRef.value;
let containerRect = container.getBoundingClientRect();
let containerTop = containerRect.top;
let containerBottom = containerRect.bottom;
let windowHeight = window.innerHeight;
if (containerTop < windowHeight && containerBottom > 0) {
loadPageList();
window.removeEventListener('scroll', onWindowScroll, { passive: true });
}
}, 100);
const onClickBack = () => {
focusMenuTransition().then(() => {
selectedPageIdx.value = null;
});
}
const focusContentTransition = () => {
return new Promise((resolve, reject) => {
// 进入动画
bookletClassList.value = ['animating', 'focus-menu-transition'];
nextTick(() => {
setTimeout(() => {
bookletClassList.value = ['animating', 'focus-content-transition'];
}, 10);
setTimeout(() => {
bookletClassList.value = ['focus-content'];
resolve();
}, 200);
});
});
}
const focusMenuTransition = () => {
return new Promise((resolve, reject) => {
// 退出动画
bookletClassList.value = ['animating', 'focus-content-transition'];
nextTick(() => {
setTimeout(() => {
bookletClassList.value = ['animating', 'focus-menu-transition'];
}, 10);
setTimeout(() => {
bookletClassList.value = ['focus-menu'];
resolve();
}, 200);
});
});
}
watch(() => selectedPageIdx.value, (newValue, oldValue) => {
// 选中页面变化时,加载页面内容
if (newValue !== null) {
loadPageContent();
}
// 手机端页面动画
if (oldValue === null && newValue !== null) {
focusContentTransition();
}
});
onMounted(() => {
if (!props.pageListLoader) {
console.error('pageListLoader is not set');
return;
}
if (!props.lazyLoad) {
loadPageList();
} else {
window.addEventListener('scroll', onWindowScroll, { passive: true });
nextTick(() => {
onWindowScroll(); // init
});
}
});
return {
// global mw object
mw,
// states
containerRef,
pageListLoading,
contentLoading,
pageList,
selectedPageIdx,
previewPageContent,
selectedPageInfo,
selectedPageUrl,
bookletAnimateClassList: bookletClassList,
// methods
onSelectPage,
onClickBack
};
}
};
</script>
<template>
<div class="isekai-preview-page-list-container" ref="containerRef">
<div v-if="pageListLoading" class="loading">
<div class="spinner">
<div class="oo-ui-widget oo-ui-widget-enabled oo-ui-progressBarWidget-indeterminate oo-ui-progressBarWidget"
aria-disabled="false" role="progressbar" aria-valuemin="0" aria-valuemax="100">
<div class="oo-ui-progressBarWidget-bar"></div>
</div>
</div>
</div>
<div v-else class="isekai-booklet-layout" :class="bookletAnimateClassList">
<div class="isekai-booklet-menu">
<ul class="isekai-list isekai-preview-page-list isekai-thin-scrollbar">
<a class="isekai-list-item" v-for="(pageInfo, idx) in pageList" :key="idx" href="javascript:;"
:class="{ active: idx === selectedPageIdx }" @click.prevent="onSelectPage(idx)">
<div class="isekai-list-item-content">
<div class="isekai-list-item-subtitle" v-if="pageInfo.subTitle">{{ pageInfo.subTitle }}</div>
<div class="isekai-list-item-title">
<div>{{ pageInfo.title }}</div>
</div>
</div>
<div class="isekai-list-item-icon">
<span
class="oo-ui-widget oo-ui-widget-enabled oo-ui-iconElement oo-ui-iconElement-icon oo-ui-icon-next oo-ui-labelElement-invisible oo-ui-iconWidget"
:class="{ 'oo-ui-image-invert': idx === selectedPageIdx, 'oo-ui-image-progressive': idx !== selectedPageIdx }"
aria-disabled="false"></span>
</div>
</a>
</ul>
</div>
<div class="isekai-booklet-content" v-if="selectedPageIdx === null">
<div class="isekai-preview-page-list-placeholder">
{{ mw.msg('isekai-preview-page-list-preview-placeholder') }}
</div>
</div>
<div class="isekai-booklet-content" v-else>
<div class="card-header">
<div class="card-header-buttons back-button-container">
<span class="oo-ui-widget oo-ui-widget-enabled oo-ui-buttonElement oo-ui-buttonElement-frameless oo-ui-iconElement oo-ui-buttonWidget">
<a class="oo-ui-buttonElement-button" role="button" tabindex="0" href="javascript:;" @click.prevent="onClickBack">
<span class="oo-ui-iconElement-icon oo-ui-icon-previous"></span>
<span class="oo-ui-labelElement-label oo-ui-labelElement-invisible">{{ mw.msg('isekai-preview-page-list-back-btn') }}</span>
<span class="oo-ui-indicatorElement-indicator oo-ui-indicatorElement-noIndicator"></span>
</a>
</span>
</div>
<div class="card-header-text">{{ selectedPageInfo && selectedPageInfo.title }}</div>
<div class="card-header-buttons">
<span class="oo-ui-widget oo-ui-widget-enabled oo-ui-buttonElement oo-ui-buttonElement-framed oo-ui-iconElement oo-ui-labelElement oo-ui-flaggedElement-primary oo-ui-flaggedElement-progressive oo-ui-buttonWidget">
<a class="oo-ui-buttonElement-button" role="button" tabindex="0" :href="selectedPageUrl" target="_blank">
<span class="oo-ui-iconElement-icon oo-ui-icon-ellipsis oo-ui-image-invert"></span>
<span class="oo-ui-labelElement-label">{{ mw.msg('isekai-preview-page-list-readmore-btn') }}</span>
<span class="oo-ui-indicatorElement-indicator oo-ui-indicatorElement-noIndicator oo-ui-image-invert"></span>
</a>
</span>
</div>
</div>
<div class="isekai-preview-container">
<div v-if="contentLoading" class="loading">
<div class="spinner">
<div class="oo-ui-widget oo-ui-widget-enabled oo-ui-progressBarWidget-indeterminate oo-ui-progressBarWidget"
aria-disabled="false" role="progressbar" aria-valuemin="0" aria-valuemax="100">
<div class="oo-ui-progressBarWidget-bar"></div>
</div>
</div>
</div>
<div v-else class="isekai-preview-content" v-html="previewPageContent"></div>
</div>
</div>
</div>
</div>
</template>