add theme
This commit is contained in:
65
themes/butterfly/source/js/ai-summary.js
Normal file
65
themes/butterfly/source/js/ai-summary.js
Normal file
@@ -0,0 +1,65 @@
|
||||
// 打字机效果
|
||||
function typeTextMachineStyle(text, targetSelector, options = {}) {
|
||||
const {
|
||||
delay = 50,
|
||||
startDelay = 2000,
|
||||
onComplete = null,
|
||||
clearBefore = true,
|
||||
eraseBefore = true, // 新增:是否以打字机方式清除原文本
|
||||
eraseDelay = 30, // 新增:删除每个字符的间隔
|
||||
} = options;
|
||||
|
||||
const el = document.querySelector(targetSelector);
|
||||
if (!el || typeof text !== "string") return;
|
||||
|
||||
setTimeout(() => {
|
||||
const startTyping = () => {
|
||||
let index = 0;
|
||||
function renderChar() {
|
||||
if (index <= text.length) {
|
||||
el.textContent = text.slice(0, index++);
|
||||
setTimeout(renderChar, delay);
|
||||
} else {
|
||||
onComplete && onComplete(el);
|
||||
}
|
||||
}
|
||||
renderChar();
|
||||
};
|
||||
|
||||
if (clearBefore) {
|
||||
if (eraseBefore && el.textContent.length > 0) {
|
||||
let currentText = el.textContent;
|
||||
let eraseIndex = currentText.length;
|
||||
|
||||
function eraseChar() {
|
||||
if (eraseIndex > 0) {
|
||||
el.textContent = currentText.slice(0, --eraseIndex);
|
||||
setTimeout(eraseChar, eraseDelay);
|
||||
} else {
|
||||
startTyping(); // 删除完毕后开始打字
|
||||
}
|
||||
}
|
||||
|
||||
eraseChar();
|
||||
} else {
|
||||
el.textContent = "";
|
||||
startTyping();
|
||||
}
|
||||
} else {
|
||||
startTyping();
|
||||
}
|
||||
}, startDelay);
|
||||
}
|
||||
|
||||
function renderAISummary() {
|
||||
const summaryEl = document.querySelector('.ai-summary .ai-explanation');
|
||||
if (!summaryEl) return;
|
||||
|
||||
const summaryText = summaryEl.getAttribute('data-summary');
|
||||
if (summaryText) {
|
||||
typeTextMachineStyle(summaryText, ".ai-summary .ai-explanation"); // 如果需要切换,在这里调用另一个函数即可
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('pjax:complete', renderAISummary);
|
||||
document.addEventListener('DOMContentLoaded', renderAISummary);
|
||||
111
themes/butterfly/source/js/calendar.js
Normal file
111
themes/butterfly/source/js/calendar.js
Normal file
@@ -0,0 +1,111 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initializeCard();
|
||||
});
|
||||
|
||||
document.addEventListener("pjax:complete", () => {
|
||||
initializeCard();
|
||||
});
|
||||
|
||||
function initializeCard() {
|
||||
cardTimes();
|
||||
cardRefreshTimes();
|
||||
}
|
||||
|
||||
let year, month, week, date, dates, weekStr, monthStr, asideTime, asideDay, asideDayNum, animalYear, ganzhiYear, lunarMon, lunarDay;
|
||||
const now = new Date();
|
||||
|
||||
function cardRefreshTimes() {
|
||||
const e = document.getElementById("card-widget-schedule");
|
||||
if (e) {
|
||||
asideDay = (now - asideTime) / 1e3 / 60 / 60 / 24;
|
||||
e.querySelector("#pBar_year").value = asideDay;
|
||||
e.querySelector("#p_span_year").innerHTML = (asideDay / 365 * 100).toFixed(1) + "%";
|
||||
e.querySelector(".schedule-r0 .schedule-d1 .aside-span2").innerHTML = `还剩<a> ${(365 - asideDay).toFixed(0)} </a>天`;
|
||||
e.querySelector("#pBar_month").value = date;
|
||||
e.querySelector("#pBar_month").max = dates;
|
||||
e.querySelector("#p_span_month").innerHTML = (date / dates * 100).toFixed(1) + "%";
|
||||
e.querySelector(".schedule-r1 .schedule-d1 .aside-span2").innerHTML = `还剩<a> ${(dates - date)} </a>天`;
|
||||
e.querySelector("#pBar_week").value = week === 0 ? 7 : week;
|
||||
e.querySelector("#p_span_week").innerHTML = ((week === 0 ? 7 : week) / 7 * 100).toFixed(1) + "%";
|
||||
e.querySelector(".schedule-r2 .schedule-d1 .aside-span2").innerHTML = `还剩<a> ${(7 - (week === 0 ? 7 : week))} </a>天`;
|
||||
}
|
||||
}
|
||||
|
||||
function cardTimes() {
|
||||
year = now.getFullYear();
|
||||
month = now.getMonth();
|
||||
week = now.getDay();
|
||||
date = now.getDate();
|
||||
|
||||
const e = document.getElementById("card-widget-calendar");
|
||||
if (e) {
|
||||
const isLeapYear = year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
|
||||
weekStr = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"][week];
|
||||
const monthData = [
|
||||
{ month: "1月", days: 31 },
|
||||
{ month: "2月", days: isLeapYear ? 29 : 28 },
|
||||
{ month: "3月", days: 31 },
|
||||
{ month: "4月", days: 30 },
|
||||
{ month: "5月", days: 31 },
|
||||
{ month: "6月", days: 30 },
|
||||
{ month: "7月", days: 31 },
|
||||
{ month: "8月", days: 31 },
|
||||
{ month: "9月", days: 30 },
|
||||
{ month: "10月", days: 31 },
|
||||
{ month: "11月", days: 30 },
|
||||
{ month: "12月", days: 31 }
|
||||
];
|
||||
monthStr = monthData[month].month;
|
||||
dates = monthData[month].days;
|
||||
|
||||
const t = (week + 8 - date % 7) % 7;
|
||||
let n = "", d = false, s = 7 - t;
|
||||
const o = (dates - s) % 7 === 0 ? Math.floor((dates - s) / 7) + 1 : Math.floor((dates - s) / 7) + 2;
|
||||
const c = e.querySelector("#calendar-main");
|
||||
const l = e.querySelector("#calendar-date");
|
||||
|
||||
l.style.fontSize = ["64px", "48px", "36px"][Math.min(o - 3, 2)];
|
||||
|
||||
for (let i = 0; i < o; i++) {
|
||||
if (!c.querySelector(`.calendar-r${i}`)) {
|
||||
c.innerHTML += `<div class='calendar-r${i}'></div>`;
|
||||
}
|
||||
for (let j = 0; j < 7; j++) {
|
||||
if (i === 0 && j === t) {
|
||||
n = 1;
|
||||
d = true;
|
||||
}
|
||||
const r = n === date ? " class='now'" : "";
|
||||
if (!c.querySelector(`.calendar-r${i} .calendar-d${j} a`)) {
|
||||
c.querySelector(`.calendar-r${i}`).innerHTML += `<div class='calendar-d${j}'><a${r}>${n}</a></div>`;
|
||||
}
|
||||
if (n >= dates) {
|
||||
n = "";
|
||||
d = false;
|
||||
}
|
||||
if (d) {
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lunarDate = chineseLunar.solarToLunar(new Date(year, month, date));
|
||||
animalYear = chineseLunar.format(lunarDate, "A");
|
||||
ganzhiYear = chineseLunar.format(lunarDate, "T").slice(0, -1);
|
||||
lunarMon = chineseLunar.format(lunarDate, "M");
|
||||
lunarDay = chineseLunar.format(lunarDate, "d");
|
||||
|
||||
const newYearDate = new Date("2026/02/16 00:00:00");
|
||||
const daysUntilNewYear = Math.floor((newYearDate - now) / 1e3 / 60 / 60 / 24);
|
||||
asideTime = new Date(`${new Date().getFullYear()}/01/01 00:00:00`);
|
||||
asideDay = (now - asideTime) / 1e3 / 60 / 60 / 24;
|
||||
asideDayNum = Math.floor(asideDay);
|
||||
const weekNum = week - asideDayNum % 7 >= 0 ? Math.ceil(asideDayNum / 7) : Math.ceil(asideDayNum / 7) + 1;
|
||||
|
||||
e.querySelector("#calendar-week").innerHTML = `第${weekNum}周 ${weekStr}`;
|
||||
e.querySelector("#calendar-date").innerHTML = date.toString().padStart(2, "0");
|
||||
e.querySelector("#calendar-solar").innerHTML = `${year}年${monthStr} 第${asideDay.toFixed(0)}天`;
|
||||
e.querySelector("#calendar-lunar").innerHTML = `${ganzhiYear}${animalYear}年 ${lunarMon}${lunarDay}`;
|
||||
document.getElementById("schedule-days").innerHTML = daysUntilNewYear;
|
||||
}
|
||||
}
|
||||
930
themes/butterfly/source/js/main.js
Normal file
930
themes/butterfly/source/js/main.js
Normal file
@@ -0,0 +1,930 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
let headerContentWidth, $nav
|
||||
let mobileSidebarOpen = false
|
||||
|
||||
const adjustMenu = init => {
|
||||
const getAllWidth = ele => Array.from(ele).reduce((width, i) => width + i.offsetWidth, 0)
|
||||
|
||||
if (init) {
|
||||
const blogInfoWidth = getAllWidth(document.querySelector('#blog-info > a').children)
|
||||
const menusWidth = getAllWidth(document.getElementById('menus').children)
|
||||
headerContentWidth = blogInfoWidth + menusWidth
|
||||
$nav = document.getElementById('nav')
|
||||
}
|
||||
|
||||
const hideMenuIndex = window.innerWidth <= 768 || headerContentWidth > $nav.offsetWidth - 120
|
||||
$nav.classList.toggle('hide-menu', hideMenuIndex)
|
||||
}
|
||||
|
||||
// 初始化header
|
||||
const initAdjust = () => {
|
||||
adjustMenu(true)
|
||||
$nav.classList.add('show')
|
||||
}
|
||||
|
||||
// sidebar menus
|
||||
const sidebarFn = {
|
||||
open: () => {
|
||||
btf.overflowPaddingR.add()
|
||||
btf.animateIn(document.getElementById('menu-mask'), 'to_show 0.5s')
|
||||
document.getElementById('sidebar-menus').classList.add('open')
|
||||
mobileSidebarOpen = true
|
||||
},
|
||||
close: () => {
|
||||
btf.overflowPaddingR.remove()
|
||||
btf.animateOut(document.getElementById('menu-mask'), 'to_hide 0.5s')
|
||||
document.getElementById('sidebar-menus').classList.remove('open')
|
||||
mobileSidebarOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 首頁top_img底下的箭頭
|
||||
*/
|
||||
const scrollDownInIndex = () => {
|
||||
const handleScrollToDest = () => {
|
||||
btf.scrollToDest(document.getElementById('content-inner').offsetTop, 300)
|
||||
}
|
||||
|
||||
const $scrollDownEle = document.getElementById('scroll-down')
|
||||
$scrollDownEle && btf.addEventListenerPjax($scrollDownEle, 'click', handleScrollToDest)
|
||||
}
|
||||
|
||||
/**
|
||||
* 代碼
|
||||
* 只適用於Hexo默認的代碼渲染
|
||||
*/
|
||||
const addHighlightTool = () => {
|
||||
const highLight = GLOBAL_CONFIG.highlight
|
||||
if (!highLight) return
|
||||
|
||||
const { highlightCopy, highlightLang, highlightHeightLimit, highlightFullpage, highlightMacStyle, plugin } = highLight
|
||||
const isHighlightShrink = GLOBAL_CONFIG_SITE.isHighlightShrink
|
||||
const isShowTool = highlightCopy || highlightLang || isHighlightShrink !== undefined || highlightFullpage || highlightMacStyle
|
||||
const $figureHighlight = plugin === 'highlight.js' ? document.querySelectorAll('figure.highlight') : document.querySelectorAll('pre[class*="language-"]')
|
||||
|
||||
if (!((isShowTool || highlightHeightLimit) && $figureHighlight.length)) return
|
||||
|
||||
const isPrismjs = plugin === 'prismjs'
|
||||
const highlightShrinkClass = isHighlightShrink === true ? 'closed' : ''
|
||||
const highlightShrinkEle = isHighlightShrink !== undefined ? '<i class="fas fa-angle-down expand"></i>' : ''
|
||||
const highlightCopyEle = highlightCopy ? '<div class="copy-notice"></div><i class="fas fa-paste copy-button"></i>' : ''
|
||||
const highlightMacStyleEle = '<div class="macStyle"><div class="mac-close"></div><div class="mac-minimize"></div><div class="mac-maximize"></div></div>'
|
||||
const highlightFullpageEle = highlightFullpage ? '<i class="fa-solid fa-up-right-and-down-left-from-center fullpage-button"></i>' : ''
|
||||
|
||||
const alertInfo = (ele, text) => {
|
||||
if (GLOBAL_CONFIG.Snackbar !== undefined) {
|
||||
btf.snackbarShow(text)
|
||||
} else {
|
||||
ele.textContent = text
|
||||
ele.style.opacity = 1
|
||||
setTimeout(() => { ele.style.opacity = 0 }, 800)
|
||||
}
|
||||
}
|
||||
|
||||
const copy = async (text, ctx) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
alertInfo(ctx, GLOBAL_CONFIG.copy.success)
|
||||
} catch (err) {
|
||||
console.error('Failed to copy: ', err)
|
||||
alertInfo(ctx, GLOBAL_CONFIG.copy.noSupport)
|
||||
}
|
||||
}
|
||||
|
||||
// click events
|
||||
const highlightCopyFn = (ele, clickEle) => {
|
||||
const $buttonParent = ele.parentNode
|
||||
$buttonParent.classList.add('copy-true')
|
||||
const preCodeSelector = isPrismjs ? 'pre code' : 'table .code pre'
|
||||
const codeElement = $buttonParent.querySelector(preCodeSelector)
|
||||
if (!codeElement) return
|
||||
copy(codeElement.innerText, clickEle.previousElementSibling)
|
||||
$buttonParent.classList.remove('copy-true')
|
||||
}
|
||||
|
||||
const highlightShrinkFn = ele => ele.classList.toggle('closed')
|
||||
|
||||
const codeFullpage = (item, clickEle) => {
|
||||
const wrapEle = item.closest('figure.highlight')
|
||||
const isFullpage = wrapEle.classList.toggle('code-fullpage')
|
||||
|
||||
document.body.style.overflow = isFullpage ? 'hidden' : ''
|
||||
clickEle.classList.toggle('fa-down-left-and-up-right-to-center', isFullpage)
|
||||
clickEle.classList.toggle('fa-up-right-and-down-left-from-center', !isFullpage)
|
||||
}
|
||||
|
||||
const highlightToolsFn = e => {
|
||||
const $target = e.target.classList
|
||||
const currentElement = e.currentTarget
|
||||
if ($target.contains('expand')) highlightShrinkFn(currentElement)
|
||||
else if ($target.contains('copy-button')) highlightCopyFn(currentElement, e.target)
|
||||
else if ($target.contains('fullpage-button')) codeFullpage(currentElement, e.target)
|
||||
}
|
||||
|
||||
const expandCode = e => e.currentTarget.classList.toggle('expand-done')
|
||||
|
||||
// 獲取隱藏狀態下元素的真實高度
|
||||
const getActualHeight = item => {
|
||||
const hiddenElements = new Map()
|
||||
|
||||
const fix = () => {
|
||||
let current = item
|
||||
while (current !== document.body && current != null) {
|
||||
if (window.getComputedStyle(current).display === 'none') {
|
||||
hiddenElements.set(current, current.getAttribute('style') || '')
|
||||
}
|
||||
current = current.parentNode
|
||||
}
|
||||
|
||||
const style = 'visibility: hidden !important; display: block !important;'
|
||||
hiddenElements.forEach((originalStyle, elem) => {
|
||||
elem.setAttribute('style', originalStyle ? originalStyle + ';' + style : style)
|
||||
})
|
||||
}
|
||||
|
||||
const restore = () => {
|
||||
hiddenElements.forEach((originalStyle, elem) => {
|
||||
if (originalStyle === '') elem.removeAttribute('style')
|
||||
else elem.setAttribute('style', originalStyle)
|
||||
})
|
||||
}
|
||||
|
||||
fix()
|
||||
const height = item.offsetHeight
|
||||
restore()
|
||||
return height
|
||||
}
|
||||
|
||||
const createEle = (lang, item) => {
|
||||
const fragment = document.createDocumentFragment()
|
||||
|
||||
if (isShowTool) {
|
||||
const hlTools = document.createElement('div')
|
||||
hlTools.className = `highlight-tools ${highlightShrinkClass}`
|
||||
hlTools.innerHTML = highlightMacStyleEle + highlightShrinkEle + lang + highlightCopyEle + highlightFullpageEle
|
||||
btf.addEventListenerPjax(hlTools, 'click', highlightToolsFn)
|
||||
fragment.appendChild(hlTools)
|
||||
}
|
||||
|
||||
if (highlightHeightLimit && getActualHeight(item) > highlightHeightLimit + 30) {
|
||||
const ele = document.createElement('div')
|
||||
ele.className = 'code-expand-btn'
|
||||
ele.innerHTML = '<i class="fas fa-angle-double-down"></i>'
|
||||
btf.addEventListenerPjax(ele, 'click', expandCode)
|
||||
fragment.appendChild(ele)
|
||||
}
|
||||
|
||||
isPrismjs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild)
|
||||
}
|
||||
|
||||
$figureHighlight.forEach(item => {
|
||||
let langName = ''
|
||||
if (isPrismjs) btf.wrap(item, 'figure', { class: 'highlight' })
|
||||
|
||||
if (!highlightLang) {
|
||||
createEle('', item)
|
||||
return
|
||||
}
|
||||
|
||||
if (isPrismjs) {
|
||||
langName = item.getAttribute('data-language') || 'Code'
|
||||
} else {
|
||||
langName = item.getAttribute('class').split(' ')[1]
|
||||
if (langName === 'plain' || langName === undefined) langName = 'Code'
|
||||
}
|
||||
createEle(`<div class="code-lang">${langName}</div>`, item)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* PhotoFigcaption
|
||||
*/
|
||||
const addPhotoFigcaption = () => {
|
||||
if (!GLOBAL_CONFIG.isPhotoFigcaption) return
|
||||
document.querySelectorAll('#article-container img').forEach(item => {
|
||||
const altValue = item.title || item.alt
|
||||
if (!altValue) return
|
||||
const ele = document.createElement('div')
|
||||
ele.className = 'img-alt text-center'
|
||||
ele.textContent = altValue
|
||||
item.insertAdjacentElement('afterend', ele)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightbox
|
||||
*/
|
||||
const runLightbox = () => {
|
||||
btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)'))
|
||||
}
|
||||
|
||||
/**
|
||||
* justified-gallery 圖庫排版
|
||||
*/
|
||||
|
||||
const fetchUrl = async url => {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch URL:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const runJustifiedGallery = (container, data, config) => {
|
||||
const { isButton, limit, firstLimit, tabs } = config
|
||||
|
||||
const dataLength = data.length
|
||||
const maxGroupKey = Math.ceil((dataLength - firstLimit) / limit + 1)
|
||||
|
||||
// Gallery configuration
|
||||
const igConfig = {
|
||||
gap: 5,
|
||||
isConstantSize: true,
|
||||
sizeRange: [150, 600],
|
||||
// useResizeObserver: true,
|
||||
// observeChildren: true,
|
||||
useTransform: true
|
||||
// useRecycle: false
|
||||
}
|
||||
|
||||
const ig = new InfiniteGrid.JustifiedInfiniteGrid(container, igConfig)
|
||||
let isLayoutHidden = false
|
||||
|
||||
// Utility functions
|
||||
const sanitizeString = str => (str && str.replace(/"/g, '"')) || ''
|
||||
|
||||
const createImageItem = item => {
|
||||
const alt = item.alt ? `alt="${sanitizeString(item.alt)}"` : ''
|
||||
const title = item.title ? `title="${sanitizeString(item.title)}"` : ''
|
||||
return `<div class="item">
|
||||
<img src="${item.url}" data-grid-maintained-target="true" ${alt} ${title} />
|
||||
</div>`
|
||||
}
|
||||
|
||||
const getItems = (nextGroupKey, count, isFirst = false) => {
|
||||
const startIndex = isFirst ? (nextGroupKey - 1) * count : (nextGroupKey - 2) * count + firstLimit
|
||||
return data.slice(startIndex, startIndex + count).map(createImageItem)
|
||||
}
|
||||
|
||||
// Load more button
|
||||
const addLoadMoreButton = container => {
|
||||
const button = document.createElement('button')
|
||||
button.innerHTML = `${GLOBAL_CONFIG.infinitegrid.buttonText}<i class="fa-solid fa-arrow-down"></i>`
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
button.remove()
|
||||
btf.setLoading.add(container)
|
||||
appendItems(ig.getGroups().length + 1, limit)
|
||||
}, { once: true })
|
||||
|
||||
container.insertAdjacentElement('afterend', button)
|
||||
}
|
||||
|
||||
const appendItems = (nextGroupKey, count, isFirst) => {
|
||||
ig.append(getItems(nextGroupKey, count, isFirst), nextGroupKey)
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
const handleRenderComplete = e => {
|
||||
if (tabs) {
|
||||
const parentNode = container.parentNode
|
||||
if (isLayoutHidden) {
|
||||
parentNode.style.visibility = 'visible'
|
||||
}
|
||||
if (container.offsetHeight === 0) {
|
||||
parentNode.style.visibility = 'hidden'
|
||||
isLayoutHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
const { updated, isResize, mounted } = e
|
||||
if (!updated.length || !mounted.length || isResize) return
|
||||
|
||||
btf.loadLightbox(container.querySelectorAll('img:not(.medium-zoom-image)'))
|
||||
|
||||
if (ig.getGroups().length === maxGroupKey) {
|
||||
btf.setLoading.remove(container)
|
||||
!tabs && ig.off('renderComplete', handleRenderComplete)
|
||||
return
|
||||
}
|
||||
|
||||
if (isButton) {
|
||||
btf.setLoading.remove(container)
|
||||
addLoadMoreButton(container)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRequestAppend = btf.debounce(e => {
|
||||
const nextGroupKey = (+e.groupKey || 0) + 1
|
||||
|
||||
if (nextGroupKey === 1) appendItems(nextGroupKey, firstLimit, true)
|
||||
else appendItems(nextGroupKey, limit)
|
||||
|
||||
if (nextGroupKey === maxGroupKey) ig.off('requestAppend', handleRequestAppend)
|
||||
}, 300)
|
||||
|
||||
btf.setLoading.add(container)
|
||||
ig.on('renderComplete', handleRenderComplete)
|
||||
|
||||
if (isButton) {
|
||||
appendItems(1, firstLimit, true)
|
||||
} else {
|
||||
ig.on('requestAppend', handleRequestAppend)
|
||||
ig.renderItems()
|
||||
}
|
||||
|
||||
btf.addGlobalFn('pjaxSendOnce', () => ig.destroy())
|
||||
}
|
||||
|
||||
const addJustifiedGallery = async (elements, tabs = false) => {
|
||||
if (!elements.length) return
|
||||
|
||||
const initGallery = async () => {
|
||||
for (const element of elements) {
|
||||
if (btf.isHidden(element) || element.classList.contains('loaded')) continue
|
||||
|
||||
const config = {
|
||||
isButton: element.getAttribute('data-button') === 'true',
|
||||
limit: parseInt(element.getAttribute('data-limit'), 10),
|
||||
firstLimit: parseInt(element.getAttribute('data-first'), 10),
|
||||
tabs
|
||||
}
|
||||
|
||||
const container = element.firstElementChild
|
||||
const content = container.textContent
|
||||
container.textContent = ''
|
||||
element.classList.add('loaded')
|
||||
|
||||
try {
|
||||
const data = element.getAttribute('data-type') === 'url' ? await fetchUrl(content) : JSON.parse(content)
|
||||
runJustifiedGallery(container, data, config)
|
||||
} catch (error) {
|
||||
console.error('Gallery data parsing failed:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof InfiniteGrid === 'function') {
|
||||
await initGallery()
|
||||
} else {
|
||||
await btf.getScript(GLOBAL_CONFIG.infinitegrid.js)
|
||||
await initGallery()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* rightside scroll percent
|
||||
*/
|
||||
const rightsideScrollPercent = currentTop => {
|
||||
const scrollPercent = btf.getScrollPercent(currentTop, document.body)
|
||||
const goUpElement = document.getElementById('go-up')
|
||||
|
||||
if (scrollPercent < 95) {
|
||||
goUpElement.classList.add('show-percent')
|
||||
goUpElement.querySelector('.scroll-percent').textContent = scrollPercent
|
||||
} else {
|
||||
goUpElement.classList.remove('show-percent')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 滾動處理
|
||||
*/
|
||||
const scrollFn = () => {
|
||||
const $rightside = document.getElementById('rightside')
|
||||
const innerHeight = window.innerHeight + 56
|
||||
let initTop = 0
|
||||
const $header = document.getElementById('page-header')
|
||||
const isChatBtn = typeof chatBtn !== 'undefined'
|
||||
const isShowPercent = GLOBAL_CONFIG.percent.rightside
|
||||
|
||||
// 檢查文檔高度是否小於視窗高度
|
||||
const checkDocumentHeight = () => {
|
||||
if (document.body.scrollHeight <= innerHeight) {
|
||||
$rightside.classList.add('rightside-show')
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果文檔高度小於視窗高度,直接返回
|
||||
if (checkDocumentHeight()) return
|
||||
|
||||
// find the scroll direction
|
||||
const scrollDirection = currentTop => {
|
||||
const result = currentTop > initTop // true is down & false is up
|
||||
initTop = currentTop
|
||||
return result
|
||||
}
|
||||
|
||||
let flag = ''
|
||||
const scrollTask = btf.throttle(() => {
|
||||
const currentTop = window.scrollY || document.documentElement.scrollTop
|
||||
const isDown = scrollDirection(currentTop)
|
||||
if (currentTop > 56) {
|
||||
if (flag === '') {
|
||||
$header.classList.add('nav-fixed')
|
||||
$rightside.classList.add('rightside-show')
|
||||
}
|
||||
|
||||
if (isDown) {
|
||||
if (flag !== 'down') {
|
||||
$header.classList.remove('nav-visible')
|
||||
isChatBtn && window.chatBtn.hide()
|
||||
flag = 'down'
|
||||
}
|
||||
} else {
|
||||
if (flag !== 'up') {
|
||||
$header.classList.add('nav-visible')
|
||||
isChatBtn && window.chatBtn.show()
|
||||
flag = 'up'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
flag = ''
|
||||
if (currentTop === 0) {
|
||||
$header.classList.remove('nav-fixed', 'nav-visible')
|
||||
}
|
||||
$rightside.classList.remove('rightside-show')
|
||||
}
|
||||
|
||||
isShowPercent && rightsideScrollPercent(currentTop)
|
||||
checkDocumentHeight()
|
||||
}, 300)
|
||||
|
||||
btf.addEventListenerPjax(window, 'scroll', scrollTask, { passive: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* toc,anchor
|
||||
*/
|
||||
const scrollFnToDo = () => {
|
||||
const isToc = GLOBAL_CONFIG_SITE.isToc
|
||||
const isAnchor = GLOBAL_CONFIG.isAnchor
|
||||
const $article = document.getElementById('article-container')
|
||||
|
||||
if (!($article && (isToc || isAnchor))) return
|
||||
|
||||
let $tocLink, $cardToc, autoScrollToc, $tocPercentage, isExpand
|
||||
|
||||
if (isToc) {
|
||||
const $cardTocLayout = document.getElementById('card-toc')
|
||||
$cardToc = $cardTocLayout.querySelector('.toc-content')
|
||||
$tocLink = $cardToc.querySelectorAll('.toc-link')
|
||||
$tocPercentage = $cardTocLayout.querySelector('.toc-percentage')
|
||||
isExpand = $cardToc.classList.contains('is-expand')
|
||||
|
||||
// toc元素點擊
|
||||
const tocItemClickFn = e => {
|
||||
const target = e.target.closest('.toc-link')
|
||||
if (!target) return
|
||||
|
||||
e.preventDefault()
|
||||
btf.scrollToDest(btf.getEleTop(document.getElementById(decodeURI(target.getAttribute('href')).replace('#', ''))), 300)
|
||||
if (window.innerWidth < 900) {
|
||||
$cardTocLayout.classList.remove('open')
|
||||
}
|
||||
}
|
||||
|
||||
btf.addEventListenerPjax($cardToc, 'click', tocItemClickFn)
|
||||
|
||||
autoScrollToc = item => {
|
||||
const sidebarHeight = $cardToc.clientHeight
|
||||
const itemOffsetTop = item.offsetTop
|
||||
const itemHeight = item.clientHeight
|
||||
const scrollTop = $cardToc.scrollTop
|
||||
const offset = itemOffsetTop - scrollTop
|
||||
const middlePosition = (sidebarHeight - itemHeight) / 2
|
||||
|
||||
if (offset !== middlePosition) {
|
||||
$cardToc.scrollTop = scrollTop + (offset - middlePosition)
|
||||
}
|
||||
}
|
||||
|
||||
// 處理 hexo-blog-encrypt 事件
|
||||
$cardToc.style.display = 'block'
|
||||
}
|
||||
|
||||
// find head position & add active class
|
||||
const $articleList = $article.querySelectorAll('h1,h2,h3,h4,h5,h6')
|
||||
let detectItem = ''
|
||||
|
||||
const findHeadPosition = top => {
|
||||
if (top === 0) return false
|
||||
|
||||
let currentId = ''
|
||||
let currentIndex = ''
|
||||
|
||||
for (let i = 0; i < $articleList.length; i++) {
|
||||
const ele = $articleList[i]
|
||||
if (top > btf.getEleTop(ele) - 80) {
|
||||
const id = ele.id
|
||||
currentId = id ? '#' + encodeURI(id) : ''
|
||||
currentIndex = i
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (detectItem === currentIndex) return
|
||||
|
||||
if (isAnchor) btf.updateAnchor(currentId)
|
||||
|
||||
detectItem = currentIndex
|
||||
|
||||
if (isToc) {
|
||||
$cardToc.querySelectorAll('.active').forEach(i => i.classList.remove('active'))
|
||||
|
||||
if (currentId) {
|
||||
const currentActive = $tocLink[currentIndex]
|
||||
currentActive.classList.add('active')
|
||||
|
||||
setTimeout(() => autoScrollToc(currentActive), 0)
|
||||
|
||||
if (!isExpand) {
|
||||
let parent = currentActive.parentNode
|
||||
while (!parent.matches('.toc')) {
|
||||
if (parent.matches('li')) parent.classList.add('active')
|
||||
parent = parent.parentNode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// main of scroll
|
||||
const tocScrollFn = btf.throttle(() => {
|
||||
const currentTop = window.scrollY || document.documentElement.scrollTop
|
||||
if (isToc && GLOBAL_CONFIG.percent.toc) {
|
||||
$tocPercentage.textContent = btf.getScrollPercent(currentTop, $article)
|
||||
}
|
||||
findHeadPosition(currentTop)
|
||||
}, 100)
|
||||
|
||||
btf.addEventListenerPjax(window, 'scroll', tocScrollFn, { passive: true })
|
||||
}
|
||||
|
||||
const handleThemeChange = mode => {
|
||||
const globalFn = window.globalFn || {}
|
||||
const themeChange = globalFn.themeChange || {}
|
||||
if (!themeChange) {
|
||||
return
|
||||
}
|
||||
|
||||
Object.keys(themeChange).forEach(key => {
|
||||
const themeChangeFn = themeChange[key]
|
||||
if (['disqus', 'disqusjs'].includes(key)) {
|
||||
setTimeout(() => themeChangeFn(mode), 300)
|
||||
} else {
|
||||
themeChangeFn(mode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Rightside
|
||||
*/
|
||||
const rightSideFn = {
|
||||
readmode: () => { // read mode
|
||||
const $body = document.body
|
||||
const newEle = document.createElement('button')
|
||||
|
||||
const exitReadMode = () => {
|
||||
$body.classList.remove('read-mode')
|
||||
newEle.remove()
|
||||
newEle.removeEventListener('click', exitReadMode)
|
||||
}
|
||||
|
||||
$body.classList.add('read-mode')
|
||||
newEle.type = 'button'
|
||||
newEle.className = 'fas fa-sign-out-alt exit-readmode'
|
||||
newEle.addEventListener('click', exitReadMode)
|
||||
$body.appendChild(newEle)
|
||||
},
|
||||
darkmode: () => { // switch between light and dark mode
|
||||
const willChangeMode = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'
|
||||
if (willChangeMode === 'dark') {
|
||||
btf.activateDarkMode()
|
||||
GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.day_to_night)
|
||||
} else {
|
||||
btf.activateLightMode()
|
||||
GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.night_to_day)
|
||||
}
|
||||
btf.saveToLocal.set('theme', willChangeMode, 2)
|
||||
handleThemeChange(willChangeMode)
|
||||
},
|
||||
'rightside-config': item => { // Show or hide rightside-hide-btn
|
||||
const hideLayout = item.firstElementChild
|
||||
if (hideLayout.classList.contains('show')) {
|
||||
hideLayout.classList.add('status')
|
||||
setTimeout(() => {
|
||||
hideLayout.classList.remove('status')
|
||||
}, 300)
|
||||
}
|
||||
|
||||
hideLayout.classList.toggle('show')
|
||||
},
|
||||
'go-up': () => { // Back to top
|
||||
btf.scrollToDest(0, 500)
|
||||
},
|
||||
'hide-aside-btn': () => { // Hide aside
|
||||
const $htmlDom = document.documentElement.classList
|
||||
const saveStatus = $htmlDom.contains('hide-aside') ? 'show' : 'hide'
|
||||
btf.saveToLocal.set('aside-status', saveStatus, 2)
|
||||
$htmlDom.toggle('hide-aside')
|
||||
},
|
||||
'mobile-toc-button': (p, item) => { // Show mobile toc
|
||||
const tocEle = document.getElementById('card-toc')
|
||||
tocEle.style.transition = 'transform 0.3s ease-in-out'
|
||||
|
||||
const tocEleHeight = tocEle.clientHeight
|
||||
const btData = item.getBoundingClientRect()
|
||||
|
||||
const tocEleBottom = window.innerHeight - btData.bottom - 30
|
||||
if (tocEleHeight > tocEleBottom) {
|
||||
tocEle.style.transformOrigin = `right ${tocEleHeight - tocEleBottom - btData.height / 2}px`
|
||||
}
|
||||
|
||||
tocEle.classList.toggle('open')
|
||||
tocEle.addEventListener('transitionend', () => {
|
||||
tocEle.style.cssText = ''
|
||||
}, { once: true })
|
||||
},
|
||||
'chat-btn': () => { // Show chat
|
||||
window.chatBtnFn()
|
||||
},
|
||||
translateLink: () => { // switch between traditional and simplified chinese
|
||||
window.translateFn.translatePage()
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('rightside').addEventListener('click', e => {
|
||||
const $target = e.target.closest('[id]')
|
||||
if ($target && rightSideFn[$target.id]) {
|
||||
rightSideFn[$target.id](e.currentTarget, $target)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* menu
|
||||
* 側邊欄sub-menu 展開/收縮
|
||||
*/
|
||||
const clickFnOfSubMenu = () => {
|
||||
const handleClickOfSubMenu = e => {
|
||||
const target = e.target.closest('.site-page.group')
|
||||
if (!target) return
|
||||
target.classList.toggle('hide')
|
||||
}
|
||||
|
||||
const menusItems = document.querySelector('#sidebar-menus .menus_items')
|
||||
menusItems && menusItems.addEventListener('click', handleClickOfSubMenu)
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机端目录点击
|
||||
*/
|
||||
const openMobileMenu = () => {
|
||||
const toggleMenu = document.getElementById('toggle-menu')
|
||||
if (!toggleMenu) return
|
||||
btf.addEventListenerPjax(toggleMenu, 'click', () => { sidebarFn.open() })
|
||||
}
|
||||
|
||||
/**
|
||||
* 複製時加上版權信息
|
||||
*/
|
||||
const addCopyright = () => {
|
||||
const { limitCount, languages } = GLOBAL_CONFIG.copyright
|
||||
|
||||
const handleCopy = (e) => {
|
||||
e.preventDefault()
|
||||
const copyFont = window.getSelection(0).toString()
|
||||
let textFont = copyFont
|
||||
if (copyFont.length > limitCount) {
|
||||
textFont = `${copyFont}\n\n\n${languages.author}\n${languages.link}${window.location.href}\n${languages.source}\n${languages.info}`
|
||||
}
|
||||
if (e.clipboardData) {
|
||||
return e.clipboardData.setData('text', textFont)
|
||||
} else {
|
||||
return window.clipboardData.setData('text', textFont)
|
||||
}
|
||||
}
|
||||
|
||||
document.body.addEventListener('copy', handleCopy)
|
||||
}
|
||||
|
||||
/**
|
||||
* 網頁運行時間
|
||||
*/
|
||||
const addRuntime = () => {
|
||||
const $runtimeCount = document.getElementById('runtimeshow')
|
||||
if ($runtimeCount) {
|
||||
const publishDate = $runtimeCount.getAttribute('data-publishDate')
|
||||
$runtimeCount.textContent = `${btf.diffDate(publishDate)} ${GLOBAL_CONFIG.runtime}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 最後一次更新時間
|
||||
*/
|
||||
const addLastPushDate = () => {
|
||||
const $lastPushDateItem = document.getElementById('last-push-date')
|
||||
if ($lastPushDateItem) {
|
||||
const lastPushDate = $lastPushDateItem.getAttribute('data-lastPushDate')
|
||||
$lastPushDateItem.textContent = btf.diffDate(lastPushDate, true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* table overflow
|
||||
*/
|
||||
const addTableWrap = () => {
|
||||
const $table = document.querySelectorAll('#article-container table')
|
||||
if (!$table.length) return
|
||||
|
||||
$table.forEach(item => {
|
||||
if (!item.closest('.highlight')) {
|
||||
btf.wrap(item, 'div', { class: 'table-wrap' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* tag-hide
|
||||
*/
|
||||
const clickFnOfTagHide = () => {
|
||||
const hideButtons = document.querySelectorAll('#article-container .hide-button')
|
||||
if (!hideButtons.length) return
|
||||
hideButtons.forEach(item => item.addEventListener('click', e => {
|
||||
const currentTarget = e.currentTarget
|
||||
currentTarget.classList.add('open')
|
||||
addJustifiedGallery(currentTarget.nextElementSibling.querySelectorAll('.gallery-container'))
|
||||
}, { once: true }))
|
||||
}
|
||||
|
||||
const tabsFn = () => {
|
||||
const navTabsElements = document.querySelectorAll('#article-container .tabs')
|
||||
if (!navTabsElements.length) return
|
||||
|
||||
const setActiveClass = (elements, activeIndex) => {
|
||||
elements.forEach((el, index) => {
|
||||
el.classList.toggle('active', index === activeIndex)
|
||||
})
|
||||
}
|
||||
|
||||
const handleNavClick = e => {
|
||||
const target = e.target.closest('button')
|
||||
if (!target || target.classList.contains('active')) return
|
||||
|
||||
const navItems = [...e.currentTarget.children]
|
||||
const tabContents = [...e.currentTarget.nextElementSibling.children]
|
||||
const indexOfButton = navItems.indexOf(target)
|
||||
setActiveClass(navItems, indexOfButton)
|
||||
e.currentTarget.classList.remove('no-default')
|
||||
setActiveClass(tabContents, indexOfButton)
|
||||
addJustifiedGallery(tabContents[indexOfButton].querySelectorAll('.gallery-container'), true)
|
||||
}
|
||||
|
||||
const handleToTopClick = tabElement => e => {
|
||||
if (e.target.closest('button')) {
|
||||
btf.scrollToDest(btf.getEleTop(tabElement), 300)
|
||||
}
|
||||
}
|
||||
|
||||
navTabsElements.forEach(tabElement => {
|
||||
btf.addEventListenerPjax(tabElement.firstElementChild, 'click', handleNavClick)
|
||||
btf.addEventListenerPjax(tabElement.lastElementChild, 'click', handleToTopClick(tabElement))
|
||||
})
|
||||
}
|
||||
|
||||
const toggleCardCategory = () => {
|
||||
const cardCategory = document.querySelector('#aside-cat-list.expandBtn')
|
||||
if (!cardCategory) return
|
||||
|
||||
const handleToggleBtn = e => {
|
||||
const target = e.target
|
||||
if (target.nodeName === 'I') {
|
||||
e.preventDefault()
|
||||
target.parentNode.classList.toggle('expand')
|
||||
}
|
||||
}
|
||||
btf.addEventListenerPjax(cardCategory, 'click', handleToggleBtn, true)
|
||||
}
|
||||
|
||||
const addPostOutdateNotice = () => {
|
||||
const ele = document.getElementById('post-outdate-notice')
|
||||
if (!ele) return
|
||||
|
||||
const { limitDay, messagePrev, messageNext, postUpdate } = JSON.parse(ele.getAttribute('data'))
|
||||
const diffDay = btf.diffDate(postUpdate)
|
||||
if (diffDay >= limitDay) {
|
||||
ele.textContent = `${messagePrev} ${diffDay} ${messageNext}`
|
||||
ele.hidden = false
|
||||
}
|
||||
}
|
||||
|
||||
const lazyloadImg = () => {
|
||||
window.lazyLoadInstance = new LazyLoad({
|
||||
elements_selector: 'img',
|
||||
threshold: 0,
|
||||
data_src: 'lazy-src'
|
||||
})
|
||||
|
||||
btf.addGlobalFn('pjaxComplete', () => {
|
||||
window.lazyLoadInstance.update()
|
||||
}, 'lazyload')
|
||||
}
|
||||
|
||||
const relativeDate = selector => {
|
||||
selector.forEach(item => {
|
||||
item.textContent = btf.diffDate(item.getAttribute('datetime'), true)
|
||||
item.style.display = 'inline'
|
||||
})
|
||||
}
|
||||
|
||||
const justifiedIndexPostUI = () => {
|
||||
const recentPostsElement = document.getElementById('recent-posts')
|
||||
if (!(recentPostsElement && recentPostsElement.classList.contains('masonry'))) return
|
||||
|
||||
const init = () => {
|
||||
const masonryItem = new InfiniteGrid.MasonryInfiniteGrid('.recent-post-items', {
|
||||
gap: { horizontal: 10, vertical: 20 },
|
||||
useTransform: true,
|
||||
useResizeObserver: true
|
||||
})
|
||||
masonryItem.renderItems()
|
||||
btf.addGlobalFn('pjaxCompleteOnce', () => { masonryItem.destroy() }, 'removeJustifiedIndexPostUI')
|
||||
}
|
||||
|
||||
typeof InfiniteGrid === 'function' ? init() : btf.getScript(`${GLOBAL_CONFIG.infinitegrid.js}`).then(init)
|
||||
}
|
||||
|
||||
const unRefreshFn = () => {
|
||||
window.addEventListener('resize', () => {
|
||||
adjustMenu(false)
|
||||
mobileSidebarOpen && btf.isHidden(document.getElementById('toggle-menu')) && sidebarFn.close()
|
||||
})
|
||||
|
||||
const menuMask = document.getElementById('menu-mask')
|
||||
menuMask && menuMask.addEventListener('click', () => { sidebarFn.close() })
|
||||
|
||||
clickFnOfSubMenu()
|
||||
GLOBAL_CONFIG.islazyloadPlugin && lazyloadImg()
|
||||
GLOBAL_CONFIG.copyright !== undefined && addCopyright()
|
||||
|
||||
if (GLOBAL_CONFIG.autoDarkmode) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
||||
if (btf.saveToLocal.get('theme') !== undefined) return
|
||||
e.matches ? handleThemeChange('dark') : handleThemeChange('light')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const forPostFn = () => {
|
||||
addHighlightTool()
|
||||
addPhotoFigcaption()
|
||||
addJustifiedGallery(document.querySelectorAll('#article-container .gallery-container'))
|
||||
runLightbox()
|
||||
scrollFnToDo()
|
||||
addTableWrap()
|
||||
clickFnOfTagHide()
|
||||
tabsFn()
|
||||
}
|
||||
|
||||
const refreshFn = () => {
|
||||
initAdjust()
|
||||
justifiedIndexPostUI()
|
||||
|
||||
if (GLOBAL_CONFIG_SITE.pageType === 'post') {
|
||||
addPostOutdateNotice()
|
||||
GLOBAL_CONFIG.relativeDate.post && relativeDate(document.querySelectorAll('#post-meta time'))
|
||||
} else {
|
||||
GLOBAL_CONFIG.relativeDate.homepage && relativeDate(document.querySelectorAll('#recent-posts time'))
|
||||
GLOBAL_CONFIG.runtime && addRuntime()
|
||||
addLastPushDate()
|
||||
toggleCardCategory()
|
||||
}
|
||||
|
||||
GLOBAL_CONFIG_SITE.pageType === 'home' && scrollDownInIndex()
|
||||
scrollFn()
|
||||
|
||||
forPostFn()
|
||||
GLOBAL_CONFIG_SITE.pageType !== 'shuoshuo' && btf.switchComments(document)
|
||||
openMobileMenu()
|
||||
}
|
||||
|
||||
btf.addGlobalFn('pjaxComplete', refreshFn, 'refreshFn')
|
||||
refreshFn()
|
||||
unRefreshFn()
|
||||
|
||||
// 處理 hexo-blog-encrypt 事件
|
||||
window.addEventListener('hexo-blog-decrypt', e => {
|
||||
forPostFn()
|
||||
window.translateFn.translateInitialization()
|
||||
Object.values(window.globalFn.encrypt).forEach(fn => {
|
||||
fn()
|
||||
})
|
||||
})
|
||||
})
|
||||
13
themes/butterfly/source/js/random.js
Normal file
13
themes/butterfly/source/js/random.js
Normal file
@@ -0,0 +1,13 @@
|
||||
function randomPost() {
|
||||
fetch('/sitemap.xml').then(res => res.text()).then(str => (new window.DOMParser()).parseFromString(str, "text/xml")).then(data => {
|
||||
let ls = data.querySelectorAll('url loc');
|
||||
let locationHref,locSplit;
|
||||
do {
|
||||
locationHref = ls[Math.floor(Math.random() * ls.length)].innerHTML
|
||||
locSplit = locationHref.split('/')[3] || ''
|
||||
} while (locSplit !== 'posts');
|
||||
//若所有文章都如 https://…….com/posts/2022/07/…… 格式,主域名后字符是 posts,则循环条件改为:
|
||||
//while (locSplit !== 'posts');
|
||||
location.href = locationHref
|
||||
})
|
||||
}
|
||||
332
themes/butterfly/source/js/rightmenu.js
Normal file
332
themes/butterfly/source/js/rightmenu.js
Normal file
@@ -0,0 +1,332 @@
|
||||
function setMask() {//设置遮罩层
|
||||
if (document.getElementsByClassName("rmMask")[0] !== undefined) {
|
||||
return document.getElementsByClassName("rmMask")[0];
|
||||
}
|
||||
mask = document.createElement('div');
|
||||
mask.className = "rmMask";
|
||||
mask.style.width = window.innerWidth + 'px';
|
||||
mask.style.height = window.innerHeight + 'px';
|
||||
mask.style.background = '#fff';
|
||||
mask.style.opacity = '.0';
|
||||
mask.style.position = 'fixed';
|
||||
mask.style.top = '0';
|
||||
mask.style.left = '0';
|
||||
mask.style.zIndex = 998;
|
||||
document.body.appendChild(mask);
|
||||
document.getElementById("rightMenu").style.zIndex = 19198;
|
||||
return mask;
|
||||
}
|
||||
|
||||
function insertAtCursor(myField, myValue) {
|
||||
|
||||
//IE 浏览器
|
||||
if (document.selection) {
|
||||
myField.focus();
|
||||
sel = document.selection.createRange();
|
||||
sel.text = myValue;
|
||||
sel.select();
|
||||
}
|
||||
|
||||
//FireFox、Chrome等
|
||||
else if (myField.selectionStart || myField.selectionStart === '0') {
|
||||
var startPos = myField.selectionStart;
|
||||
var endPos = myField.selectionEnd;
|
||||
|
||||
// 保存滚动条
|
||||
var restoreTop = myField.scrollTop;
|
||||
myField.value = myField.value.substring(0, startPos) + myValue + myField.value.substring(endPos, myField.value.length);
|
||||
|
||||
if (restoreTop > 0) {
|
||||
myField.scrollTop = restoreTop;
|
||||
}
|
||||
|
||||
myField.focus();
|
||||
myField.selectionStart = startPos + myValue.length;
|
||||
myField.selectionEnd = startPos + myValue.length;
|
||||
} else {
|
||||
myField.value += myValue;
|
||||
myField.focus();
|
||||
}
|
||||
}
|
||||
|
||||
let rmf = {};
|
||||
rmf.showRightMenu = function (isTrue, x = 0, y = 0) {
|
||||
let $rightMenu = $('#rightMenu');
|
||||
$rightMenu.css('top', x + 'px').css('left', y + 'px');
|
||||
|
||||
if (isTrue) {
|
||||
$rightMenu.show();
|
||||
} else {
|
||||
$rightMenu.hide();
|
||||
}
|
||||
}
|
||||
rmf.switchDarkMode = function () {
|
||||
const nowMode = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'
|
||||
if (nowMode === 'light') {
|
||||
activateDarkMode()
|
||||
saveToLocal.set('theme', 'dark', 2)
|
||||
GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.day_to_night)
|
||||
} else {
|
||||
activateLightMode()
|
||||
saveToLocal.set('theme', 'light', 2)
|
||||
GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.night_to_day)
|
||||
}
|
||||
// handle some cases
|
||||
typeof utterancesTheme === 'function' && utterancesTheme()
|
||||
typeof FB === 'object' && window.loadFBComment()
|
||||
window.DISQUS && document.getElementById('disqus_thread').children.length && setTimeout(() => window.disqusReset(), 200)
|
||||
switchPostChart();
|
||||
};
|
||||
|
||||
rmf.copyWordsLink = function () {
|
||||
const decodedUrl = decodeURIComponent(window.location.href); // 解码 URL
|
||||
navigator.clipboard.writeText(decodedUrl)
|
||||
.then(() => {
|
||||
Snackbar.show({
|
||||
text: '链接复制成功!快去分享吧!',
|
||||
pos: 'top-right',
|
||||
showAction: false
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
rmf.switchReadMode = function () {
|
||||
const $body = document.body
|
||||
$body.classList.add('read-mode')
|
||||
const newEle = document.createElement('button')
|
||||
newEle.type = 'button'
|
||||
newEle.className = 'fas fa-sign-out-alt exit-readmode'
|
||||
$body.appendChild(newEle)
|
||||
|
||||
function clickFn() {
|
||||
$body.classList.remove('read-mode')
|
||||
newEle.remove()
|
||||
newEle.removeEventListener('click', clickFn)
|
||||
}
|
||||
|
||||
newEle.addEventListener('click', clickFn)
|
||||
}
|
||||
|
||||
//复制选中文字
|
||||
rmf.copySelect = function () {
|
||||
navigator.clipboard.writeText(document.getSelection().toString()).then(() => {
|
||||
Snackbar.show({
|
||||
text: '已复制选中文字!',
|
||||
pos: 'top-right',
|
||||
showAction: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//回到顶部
|
||||
rmf.scrollToTop = function () {
|
||||
document.getElementsByClassName("menus_items")[1].setAttribute("style", "");
|
||||
document.getElementById("name-container").setAttribute("style", "display:none");
|
||||
btf.scrollToDest(0, 500);
|
||||
}
|
||||
rmf.translate = function () {
|
||||
document.getElementById("translateLink").click();
|
||||
}
|
||||
rmf.searchinThisPage = () => {
|
||||
let mask = setMask(); // 确保 mask 元素存在于 document.body 中
|
||||
document.getElementsByClassName("local-search-box--input")[0].value = window.getSelection().toString();
|
||||
document.getElementsByClassName("search")[0].click();
|
||||
var evt = document.createEvent("HTMLEvents");
|
||||
evt.initEvent("input", false, false);
|
||||
document.getElementsByClassName("local-search-box--input")[0].dispatchEvent(evt);
|
||||
|
||||
// 在尝试移除 mask 元素之前检查它是否存在于 document.body 中
|
||||
if (document.body.contains(mask)) {
|
||||
document.body.removeChild(mask);
|
||||
}
|
||||
}
|
||||
|
||||
document.body.addEventListener('touchmove', function (e) {
|
||||
|
||||
}, {passive: false});
|
||||
|
||||
function popupMenu() {
|
||||
//window.oncontextmenu=function(){return false;}
|
||||
window.oncontextmenu = function (event) {
|
||||
Snackbar.show({
|
||||
text: '按住 Ctrl 再点击右键,即可恢复原界面哦',
|
||||
pos: 'bottom-left',
|
||||
showAction: false
|
||||
});
|
||||
if (event.ctrlKey || document.body.clientWidth < 900) return true;
|
||||
$('.rightMenu-group.hide').hide();
|
||||
if (document.getSelection().toString()) {
|
||||
$('#menu-text').show();
|
||||
}
|
||||
if (document.getElementById('post')) {
|
||||
$('#menu-post').show();
|
||||
} else {
|
||||
if (document.getElementById('page')) {
|
||||
$('#menu-post').show();
|
||||
}
|
||||
}
|
||||
var el = window.document.body;
|
||||
el = event.target;
|
||||
var a = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w.-]+)+[\w\-._~:\/?#[\]@!$&'*+,;=]+$/
|
||||
if (a.test(window.getSelection().toString()) && el.tagName !== "A") {
|
||||
$('#menu-too').show()
|
||||
}
|
||||
if (el.tagName === 'A') {
|
||||
$('#menu-to').show()
|
||||
rmf.open = function () {
|
||||
if (el.href.indexOf("http://") === -1 && el.href.indexOf("https://") === -1 || el.href.indexOf("blog.june-pj.cn") !== -1) {
|
||||
pjax.loadUrl(el.href)
|
||||
} else {
|
||||
location.href = el.href
|
||||
}
|
||||
}
|
||||
rmf.openWithNewTab = function () {
|
||||
window.open(el.href);
|
||||
// window.location.reload();
|
||||
}
|
||||
rmf.copyLink = function () {
|
||||
const url = el.href;
|
||||
navigator.clipboard.writeText(url);
|
||||
Snackbar.show({
|
||||
text: '链接复制成功!快去分享吧!',
|
||||
pos: 'top-right',
|
||||
showAction: false
|
||||
});
|
||||
};
|
||||
}
|
||||
if (el.tagName === 'IMG') {
|
||||
$('#menu-img').show()
|
||||
rmf.openWithNewTab = function () {
|
||||
window.open(el.src);
|
||||
// window.location.reload();
|
||||
}
|
||||
rmf.click = function () {
|
||||
el.click()
|
||||
}
|
||||
rmf.copyLink = function () {
|
||||
const url = el.src
|
||||
navigator.clipboard.writeText(url);
|
||||
Snackbar.show({
|
||||
text: '链接复制成功!快去分享吧!',
|
||||
pos: 'top-right',
|
||||
showAction: false
|
||||
});
|
||||
}
|
||||
rmf.saveAs = function () {
|
||||
var a = document.createElement('a');
|
||||
a.href = el.src;
|
||||
// 获取图片的文件名部分
|
||||
a.download = el.src.split('/').pop(); // 使用图片的文件名作为下载文件名
|
||||
a.style.display = 'none'; // 隐藏下载链接
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
} else if (el.tagName === "TEXTAREA" || el.tagName === "INPUT") {
|
||||
$('#menu-paste').show();
|
||||
// rmf.paste=function(){
|
||||
// input.addEventListener('paste', async event => {
|
||||
// event.preventDefault();
|
||||
// const text = await navigator.clipboard.readText();
|
||||
// el.value+=text;
|
||||
// });
|
||||
// }
|
||||
rmf.paste = function () {
|
||||
navigator.permissions
|
||||
.query({
|
||||
name: 'clipboard-read'
|
||||
})
|
||||
.then(result => {
|
||||
if (result.state === 'granted' || result.state === 'prompt') {
|
||||
//读取剪贴板
|
||||
navigator.clipboard.readText().then(text => {
|
||||
console.log(text)
|
||||
insertAtCursor(el, text)
|
||||
})
|
||||
} else {
|
||||
Snackbar.show({
|
||||
text: '请允许读取剪贴板!',
|
||||
pos: 'top-center',
|
||||
showAction: false,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
let pageX = event.clientX + 10;
|
||||
let pageY = event.clientY;
|
||||
let rmWidth = $('#rightMenu').width();
|
||||
let rmHeight = $('#rightMenu').height();
|
||||
if (pageX + rmWidth > window.innerWidth) {
|
||||
pageX -= rmWidth + 10;
|
||||
}
|
||||
if (pageY + rmHeight > window.innerHeight) {
|
||||
pageY -= pageY + rmHeight - window.innerHeight;
|
||||
}
|
||||
mask = setMask();
|
||||
window.onscroll = () => {
|
||||
rmf.showRightMenu(false);
|
||||
window.onscroll = () => {
|
||||
}
|
||||
if (document.body.contains(mask)) {
|
||||
document.body.removeChild(mask);
|
||||
}
|
||||
}
|
||||
|
||||
$(".rightMenu-item").click(() => {
|
||||
if (document.body.contains(mask)) {
|
||||
document.body.removeChild(mask);
|
||||
}
|
||||
});
|
||||
|
||||
$(window).resize(() => {
|
||||
rmf.showRightMenu(false);
|
||||
if (document.body.contains(mask)) {
|
||||
document.body.removeChild(mask);
|
||||
}
|
||||
});
|
||||
|
||||
mask.onclick = () => {
|
||||
if (document.body.contains(mask)) {
|
||||
document.body.removeChild(mask);
|
||||
}
|
||||
};
|
||||
|
||||
rmf.showRightMenu(true, pageY, pageX);
|
||||
return false;
|
||||
};
|
||||
|
||||
window.addEventListener('click', function () {
|
||||
rmf.showRightMenu(false);
|
||||
});
|
||||
}
|
||||
|
||||
if (!(navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i))) {
|
||||
popupMenu()
|
||||
}
|
||||
const box = document.documentElement
|
||||
|
||||
function addLongtabListener(target, callback) {
|
||||
let timer = 0 // 初始化timer
|
||||
|
||||
target.ontouchstart = () => {
|
||||
timer = 0 // 重置timer
|
||||
timer = setTimeout(() => {
|
||||
callback();
|
||||
timer = 0
|
||||
}, 380) // 超时器能成功执行,说明是长按
|
||||
}
|
||||
|
||||
target.ontouchmove = () => {
|
||||
clearTimeout(timer) // 如果来到这里,说明是滑动
|
||||
timer = 0
|
||||
}
|
||||
|
||||
target.ontouchend = () => { // 到这里如果timer有值,说明此触摸时间不足380ms,是点击
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addLongtabListener(box, popupMenu)
|
||||
174
themes/butterfly/source/js/search/algolia.js
Normal file
174
themes/butterfly/source/js/search/algolia.js
Normal file
@@ -0,0 +1,174 @@
|
||||
window.addEventListener('load', () => {
|
||||
const { algolia } = GLOBAL_CONFIG
|
||||
const { appId, apiKey, indexName, hitsPerPage = 5, languages } = algolia
|
||||
|
||||
if (!appId || !apiKey || !indexName) {
|
||||
return console.error('Algolia setting is invalid!')
|
||||
}
|
||||
|
||||
const $searchMask = document.getElementById('search-mask')
|
||||
const $searchDialog = document.querySelector('#algolia-search .search-dialog')
|
||||
|
||||
const animateElements = show => {
|
||||
const action = show ? 'animateIn' : 'animateOut'
|
||||
const maskAnimation = show ? 'to_show 0.5s' : 'to_hide 0.5s'
|
||||
const dialogAnimation = show ? 'titleScale 0.5s' : 'search_close .5s'
|
||||
btf[action]($searchMask, maskAnimation)
|
||||
btf[action]($searchDialog, dialogAnimation)
|
||||
}
|
||||
|
||||
const fixSafariHeight = () => {
|
||||
if (window.innerWidth < 768) {
|
||||
$searchDialog.style.setProperty('--search-height', `${window.innerHeight}px`)
|
||||
}
|
||||
}
|
||||
|
||||
const openSearch = () => {
|
||||
btf.overflowPaddingR.add()
|
||||
animateElements(true)
|
||||
setTimeout(() => { document.querySelector('#algolia-search .ais-SearchBox-input').focus() }, 100)
|
||||
|
||||
const handleEscape = event => {
|
||||
if (event.code === 'Escape') {
|
||||
closeSearch()
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
fixSafariHeight()
|
||||
window.addEventListener('resize', fixSafariHeight)
|
||||
}
|
||||
|
||||
const closeSearch = () => {
|
||||
btf.overflowPaddingR.remove()
|
||||
animateElements(false)
|
||||
window.removeEventListener('resize', fixSafariHeight)
|
||||
}
|
||||
|
||||
const searchClickFn = () => {
|
||||
btf.addEventListenerPjax(document.querySelector('#search-button > .search'), 'click', openSearch)
|
||||
}
|
||||
|
||||
const searchFnOnce = () => {
|
||||
$searchMask.addEventListener('click', closeSearch)
|
||||
document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch)
|
||||
}
|
||||
|
||||
const cutContent = (content) => {
|
||||
if (!content) return ''
|
||||
const firstOccur = content.indexOf('<mark>')
|
||||
let start = firstOccur - 30
|
||||
let end = firstOccur + 120
|
||||
let pre = ''
|
||||
let post = ''
|
||||
|
||||
if (start <= 0) {
|
||||
start = 0
|
||||
end = 140
|
||||
} else {
|
||||
pre = '...'
|
||||
}
|
||||
|
||||
if (end > content.length) {
|
||||
end = content.length
|
||||
} else {
|
||||
post = '...'
|
||||
}
|
||||
|
||||
return `${pre}${content.substring(start, end)}${post}`
|
||||
}
|
||||
|
||||
const disableDiv = [
|
||||
document.getElementById('algolia-hits'),
|
||||
document.getElementById('algolia-pagination'),
|
||||
document.querySelector('#algolia-info .algolia-stats')
|
||||
]
|
||||
|
||||
const searchClient = typeof algoliasearch === 'function' ? algoliasearch : window['algoliasearch/lite'].liteClient
|
||||
const search = instantsearch({
|
||||
indexName,
|
||||
searchClient: searchClient(appId, apiKey),
|
||||
searchFunction (helper) {
|
||||
disableDiv.forEach(item => {
|
||||
item.style.display = helper.state.query ? '' : 'none'
|
||||
})
|
||||
if (helper.state.query) helper.search()
|
||||
}
|
||||
})
|
||||
|
||||
const widgets = [
|
||||
instantsearch.widgets.configure({ hitsPerPage }),
|
||||
instantsearch.widgets.searchBox({
|
||||
container: '#algolia-search-input',
|
||||
showReset: false,
|
||||
showSubmit: false,
|
||||
placeholder: languages.input_placeholder,
|
||||
showLoadingIndicator: true
|
||||
}),
|
||||
instantsearch.widgets.hits({
|
||||
container: '#algolia-hits',
|
||||
templates: {
|
||||
item (data) {
|
||||
const link = data.permalink || (GLOBAL_CONFIG.root + data.path)
|
||||
const result = data._highlightResult
|
||||
const content = result.contentStripTruncate
|
||||
? cutContent(result.contentStripTruncate.value)
|
||||
: result.contentStrip
|
||||
? cutContent(result.contentStrip.value)
|
||||
: result.content
|
||||
? cutContent(result.content.value)
|
||||
: ''
|
||||
return `
|
||||
<a href="${link}" class="algolia-hit-item-link">
|
||||
<span class="algolia-hits-item-title">${result.title.value || 'no-title'}</span>
|
||||
${content ? `<div class="algolia-hit-item-content">${content}</div>` : ''}
|
||||
</a>`
|
||||
},
|
||||
empty (data) {
|
||||
return `<div id="algolia-hits-empty">${languages.hits_empty.replace(/\$\{query}/, data.query)}</div>`
|
||||
}
|
||||
}
|
||||
}),
|
||||
instantsearch.widgets.stats({
|
||||
container: '#algolia-info > .algolia-stats',
|
||||
templates: {
|
||||
text (data) {
|
||||
const stats = languages.hits_stats
|
||||
.replace(/\$\{hits}/, data.nbHits)
|
||||
.replace(/\$\{time}/, data.processingTimeMS)
|
||||
return `<hr>${stats}`
|
||||
}
|
||||
}
|
||||
}),
|
||||
instantsearch.widgets.poweredBy({
|
||||
container: '#algolia-info > .algolia-poweredBy'
|
||||
}),
|
||||
instantsearch.widgets.pagination({
|
||||
container: '#algolia-pagination',
|
||||
totalPages: 5,
|
||||
templates: {
|
||||
first: '<i class="fas fa-angle-double-left"></i>',
|
||||
last: '<i class="fas fa-angle-double-right"></i>',
|
||||
previous: '<i class="fas fa-angle-left"></i>',
|
||||
next: '<i class="fas fa-angle-right"></i>'
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
search.addWidgets(widgets)
|
||||
search.start()
|
||||
searchClickFn()
|
||||
searchFnOnce()
|
||||
|
||||
window.addEventListener('pjax:complete', () => {
|
||||
if (!btf.isHidden($searchMask)) closeSearch()
|
||||
searchClickFn()
|
||||
})
|
||||
|
||||
if (window.pjax) {
|
||||
search.on('render', () => {
|
||||
window.pjax.refresh(document.getElementById('algolia-hits'))
|
||||
})
|
||||
}
|
||||
})
|
||||
360
themes/butterfly/source/js/search/local-search.js
Normal file
360
themes/butterfly/source/js/search/local-search.js
Normal file
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* Refer to hexo-generator-searchdb
|
||||
* https://github.com/next-theme/hexo-generator-searchdb/blob/main/dist/search.js
|
||||
* Modified by hexo-theme-butterfly
|
||||
*/
|
||||
|
||||
class LocalSearch {
|
||||
constructor ({
|
||||
path = '',
|
||||
unescape = false,
|
||||
top_n_per_article = 1
|
||||
}) {
|
||||
this.path = path
|
||||
this.unescape = unescape
|
||||
this.top_n_per_article = top_n_per_article
|
||||
this.isfetched = false
|
||||
this.datas = null
|
||||
}
|
||||
|
||||
getIndexByWord (words, text, caseSensitive = false) {
|
||||
const index = []
|
||||
const included = new Set()
|
||||
|
||||
if (!caseSensitive) {
|
||||
text = text.toLowerCase()
|
||||
}
|
||||
words.forEach(word => {
|
||||
if (this.unescape) {
|
||||
const div = document.createElement('div')
|
||||
div.innerText = word
|
||||
word = div.innerHTML
|
||||
}
|
||||
const wordLen = word.length
|
||||
if (wordLen === 0) return
|
||||
let startPosition = 0
|
||||
let position = -1
|
||||
if (!caseSensitive) {
|
||||
word = word.toLowerCase()
|
||||
}
|
||||
while ((position = text.indexOf(word, startPosition)) > -1) {
|
||||
index.push({ position, word })
|
||||
included.add(word)
|
||||
startPosition = position + wordLen
|
||||
}
|
||||
})
|
||||
// Sort index by position of keyword
|
||||
index.sort((left, right) => {
|
||||
if (left.position !== right.position) {
|
||||
return left.position - right.position
|
||||
}
|
||||
return right.word.length - left.word.length
|
||||
})
|
||||
return [index, included]
|
||||
}
|
||||
|
||||
// Merge hits into slices
|
||||
mergeIntoSlice (start, end, index) {
|
||||
let item = index[0]
|
||||
let { position, word } = item
|
||||
const hits = []
|
||||
const count = new Set()
|
||||
while (position + word.length <= end && index.length !== 0) {
|
||||
count.add(word)
|
||||
hits.push({
|
||||
position,
|
||||
length: word.length
|
||||
})
|
||||
const wordEnd = position + word.length
|
||||
|
||||
// Move to next position of hit
|
||||
index.shift()
|
||||
while (index.length !== 0) {
|
||||
item = index[0]
|
||||
position = item.position
|
||||
word = item.word
|
||||
if (wordEnd > position) {
|
||||
index.shift()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
hits,
|
||||
start,
|
||||
end,
|
||||
count: count.size
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight title and content
|
||||
highlightKeyword (val, slice) {
|
||||
let result = ''
|
||||
let index = slice.start
|
||||
for (const { position, length } of slice.hits) {
|
||||
result += val.substring(index, position)
|
||||
index = position + length
|
||||
result += `<mark class="search-keyword">${val.substr(position, length)}</mark>`
|
||||
}
|
||||
result += val.substring(index, slice.end)
|
||||
return result
|
||||
}
|
||||
|
||||
getResultItems (keywords) {
|
||||
const resultItems = []
|
||||
this.datas.forEach(({ title, content, url }) => {
|
||||
// The number of different keywords included in the article.
|
||||
const [indexOfTitle, keysOfTitle] = this.getIndexByWord(keywords, title)
|
||||
const [indexOfContent, keysOfContent] = this.getIndexByWord(keywords, content)
|
||||
const includedCount = new Set([...keysOfTitle, ...keysOfContent]).size
|
||||
|
||||
// Show search results
|
||||
const hitCount = indexOfTitle.length + indexOfContent.length
|
||||
if (hitCount === 0) return
|
||||
|
||||
const slicesOfTitle = []
|
||||
if (indexOfTitle.length !== 0) {
|
||||
slicesOfTitle.push(this.mergeIntoSlice(0, title.length, indexOfTitle))
|
||||
}
|
||||
|
||||
let slicesOfContent = []
|
||||
while (indexOfContent.length !== 0) {
|
||||
const item = indexOfContent[0]
|
||||
const { position } = item
|
||||
// Cut out 120 characters. The maxlength of .search-input is 80.
|
||||
const start = Math.max(0, position - 20)
|
||||
const end = Math.min(content.length, position + 100)
|
||||
slicesOfContent.push(this.mergeIntoSlice(start, end, indexOfContent))
|
||||
}
|
||||
|
||||
// Sort slices in content by included keywords' count and hits' count
|
||||
slicesOfContent.sort((left, right) => {
|
||||
if (left.count !== right.count) {
|
||||
return right.count - left.count
|
||||
} else if (left.hits.length !== right.hits.length) {
|
||||
return right.hits.length - left.hits.length
|
||||
}
|
||||
return left.start - right.start
|
||||
})
|
||||
|
||||
// Select top N slices in content
|
||||
const upperBound = parseInt(this.top_n_per_article, 10)
|
||||
if (upperBound >= 0) {
|
||||
slicesOfContent = slicesOfContent.slice(0, upperBound)
|
||||
}
|
||||
|
||||
let resultItem = ''
|
||||
|
||||
url = new URL(url, location.origin)
|
||||
url.searchParams.append('highlight', keywords.join(' '))
|
||||
|
||||
if (slicesOfTitle.length !== 0) {
|
||||
resultItem += `<li class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${this.highlightKeyword(title, slicesOfTitle[0])}</span>`
|
||||
} else {
|
||||
resultItem += `<li class="local-search-hit-item"><a href="${url.href}"><span class="search-result-title">${title}</span>`
|
||||
}
|
||||
|
||||
slicesOfContent.forEach(slice => {
|
||||
resultItem += `<p class="search-result">${this.highlightKeyword(content, slice)}...</p></a>`
|
||||
})
|
||||
|
||||
resultItem += '</li>'
|
||||
resultItems.push({
|
||||
item: resultItem,
|
||||
id: resultItems.length,
|
||||
hitCount,
|
||||
includedCount
|
||||
})
|
||||
})
|
||||
return resultItems
|
||||
}
|
||||
|
||||
fetchData () {
|
||||
const isXml = !this.path.endsWith('json')
|
||||
fetch(this.path)
|
||||
.then(response => response.text())
|
||||
.then(res => {
|
||||
// Get the contents from search data
|
||||
this.isfetched = true
|
||||
this.datas = isXml
|
||||
? [...new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map(element => ({
|
||||
title: element.querySelector('title').textContent,
|
||||
content: element.querySelector('content').textContent,
|
||||
url: element.querySelector('url').textContent
|
||||
}))
|
||||
: JSON.parse(res)
|
||||
// Only match articles with non-empty titles
|
||||
this.datas = this.datas.filter(data => data.title).map(data => {
|
||||
data.title = data.title.trim()
|
||||
data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : ''
|
||||
data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/')
|
||||
return data
|
||||
})
|
||||
// Remove loading animation
|
||||
window.dispatchEvent(new Event('search:loaded'))
|
||||
})
|
||||
}
|
||||
|
||||
// Highlight by wrapping node in mark elements with the given class name
|
||||
highlightText (node, slice, className) {
|
||||
const val = node.nodeValue
|
||||
let index = slice.start
|
||||
const children = []
|
||||
for (const { position, length } of slice.hits) {
|
||||
const text = document.createTextNode(val.substring(index, position))
|
||||
index = position + length
|
||||
const mark = document.createElement('mark')
|
||||
mark.className = className
|
||||
mark.appendChild(document.createTextNode(val.substr(position, length)))
|
||||
children.push(text, mark)
|
||||
}
|
||||
node.nodeValue = val.substring(index, slice.end)
|
||||
children.forEach(element => {
|
||||
node.parentNode.insertBefore(element, node)
|
||||
})
|
||||
}
|
||||
|
||||
// Highlight the search words provided in the url in the text
|
||||
highlightSearchWords (body) {
|
||||
const params = new URL(location.href).searchParams.get('highlight')
|
||||
const keywords = params ? params.split(' ') : []
|
||||
if (!keywords.length || !body) return
|
||||
const walk = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null)
|
||||
const allNodes = []
|
||||
while (walk.nextNode()) {
|
||||
if (!walk.currentNode.parentNode.matches('button, select, textarea, .mermaid')) allNodes.push(walk.currentNode)
|
||||
}
|
||||
allNodes.forEach(node => {
|
||||
const [indexOfNode] = this.getIndexByWord(keywords, node.nodeValue)
|
||||
if (!indexOfNode.length) return
|
||||
const slice = this.mergeIntoSlice(0, node.nodeValue.length, indexOfNode)
|
||||
this.highlightText(node, slice, 'search-keyword')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
// Search
|
||||
const { path, top_n_per_article, unescape, languages } = GLOBAL_CONFIG.localSearch
|
||||
const localSearch = new LocalSearch({
|
||||
path,
|
||||
top_n_per_article,
|
||||
unescape
|
||||
})
|
||||
|
||||
const input = document.querySelector('#local-search-input input')
|
||||
const statsItem = document.getElementById('local-search-stats-wrap')
|
||||
const $loadingStatus = document.getElementById('loading-status')
|
||||
const isXml = !path.endsWith('json')
|
||||
|
||||
const inputEventFunction = () => {
|
||||
if (!localSearch.isfetched) return
|
||||
let searchText = input.value.trim().toLowerCase()
|
||||
isXml && (searchText = searchText.replace(/</g, '<').replace(/>/g, '>'))
|
||||
if (searchText !== '') $loadingStatus.innerHTML = '<i class="fas fa-spinner fa-pulse"></i>'
|
||||
const keywords = searchText.split(/[-\s]+/)
|
||||
const container = document.getElementById('local-search-results')
|
||||
let resultItems = []
|
||||
if (searchText.length > 0) {
|
||||
// Perform local searching
|
||||
resultItems = localSearch.getResultItems(keywords)
|
||||
}
|
||||
if (keywords.length === 1 && keywords[0] === '') {
|
||||
container.textContent = ''
|
||||
statsItem.textContent = ''
|
||||
} else if (resultItems.length === 0) {
|
||||
container.textContent = ''
|
||||
const statsDiv = document.createElement('div')
|
||||
statsDiv.className = 'search-result-stats'
|
||||
statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText)
|
||||
statsItem.innerHTML = statsDiv.outerHTML
|
||||
} else {
|
||||
resultItems.sort((left, right) => {
|
||||
if (left.includedCount !== right.includedCount) {
|
||||
return right.includedCount - left.includedCount
|
||||
} else if (left.hitCount !== right.hitCount) {
|
||||
return right.hitCount - left.hitCount
|
||||
}
|
||||
return right.id - left.id
|
||||
})
|
||||
|
||||
const stats = languages.hits_stats.replace(/\$\{hits}/, resultItems.length)
|
||||
|
||||
container.innerHTML = `<ol class="search-result-list">${resultItems.map(result => result.item).join('')}</ol>`
|
||||
statsItem.innerHTML = `<hr><div class="search-result-stats">${stats}</div>`
|
||||
window.pjax && window.pjax.refresh(container)
|
||||
}
|
||||
|
||||
$loadingStatus.textContent = ''
|
||||
}
|
||||
|
||||
let loadFlag = false
|
||||
const $searchMask = document.getElementById('search-mask')
|
||||
const $searchDialog = document.querySelector('#local-search .search-dialog')
|
||||
|
||||
// fix safari
|
||||
const fixSafariHeight = () => {
|
||||
if (window.innerWidth < 768) {
|
||||
$searchDialog.style.setProperty('--search-height', window.innerHeight + 'px')
|
||||
}
|
||||
}
|
||||
|
||||
const openSearch = () => {
|
||||
btf.overflowPaddingR.add()
|
||||
btf.animateIn($searchMask, 'to_show 0.5s')
|
||||
btf.animateIn($searchDialog, 'titleScale 0.5s')
|
||||
setTimeout(() => { input.focus() }, 300)
|
||||
if (!loadFlag) {
|
||||
!localSearch.isfetched && localSearch.fetchData()
|
||||
input.addEventListener('input', inputEventFunction)
|
||||
loadFlag = true
|
||||
}
|
||||
// shortcut: ESC
|
||||
document.addEventListener('keydown', function f (event) {
|
||||
if (event.code === 'Escape') {
|
||||
closeSearch()
|
||||
document.removeEventListener('keydown', f)
|
||||
}
|
||||
})
|
||||
|
||||
fixSafariHeight()
|
||||
window.addEventListener('resize', fixSafariHeight)
|
||||
}
|
||||
|
||||
const closeSearch = () => {
|
||||
btf.overflowPaddingR.remove()
|
||||
btf.animateOut($searchDialog, 'search_close .5s')
|
||||
btf.animateOut($searchMask, 'to_hide 0.5s')
|
||||
window.removeEventListener('resize', fixSafariHeight)
|
||||
}
|
||||
|
||||
const searchClickFn = () => {
|
||||
btf.addEventListenerPjax(document.querySelector('#search-button > .search'), 'click', openSearch)
|
||||
}
|
||||
|
||||
const searchFnOnce = () => {
|
||||
document.querySelector('#local-search .search-close-button').addEventListener('click', closeSearch)
|
||||
$searchMask.addEventListener('click', closeSearch)
|
||||
if (GLOBAL_CONFIG.localSearch.preload) {
|
||||
localSearch.fetchData()
|
||||
}
|
||||
localSearch.highlightSearchWords(document.getElementById('article-container'))
|
||||
}
|
||||
|
||||
window.addEventListener('search:loaded', () => {
|
||||
const $loadDataItem = document.getElementById('loading-database')
|
||||
$loadDataItem.nextElementSibling.style.display = 'block'
|
||||
$loadDataItem.remove()
|
||||
})
|
||||
|
||||
searchClickFn()
|
||||
searchFnOnce()
|
||||
|
||||
// pjax
|
||||
window.addEventListener('pjax:complete', () => {
|
||||
!btf.isHidden($searchMask) && closeSearch()
|
||||
localSearch.highlightSearchWords(document.getElementById('article-container'))
|
||||
searchClickFn()
|
||||
})
|
||||
})
|
||||
415
themes/butterfly/source/js/shuoshuo.js
Normal file
415
themes/butterfly/source/js/shuoshuo.js
Normal file
@@ -0,0 +1,415 @@
|
||||
function renderTalks() {
|
||||
const talkContainer = document.querySelector('#talk');
|
||||
const domain = 'https://mm.biss.click';
|
||||
if (!talkContainer) return;
|
||||
talkContainer.innerHTML = '';
|
||||
const generateIconSVG = () => {
|
||||
return `<svg viewBox="0 0 512 512"xmlns="http://www.w3.org/2000/svg"class="is-badge icon"><path d="m512 268c0 17.9-4.3 34.5-12.9 49.7s-20.1 27.1-34.6 35.4c.4 2.7.6 6.9.6 12.6 0 27.1-9.1 50.1-27.1 69.1-18.1 19.1-39.9 28.6-65.4 28.6-11.4 0-22.3-2.1-32.6-6.3-8 16.4-19.5 29.6-34.6 39.7-15 10.2-31.5 15.2-49.4 15.2-18.3 0-34.9-4.9-49.7-14.9-14.9-9.9-26.3-23.2-34.3-40-10.3 4.2-21.1 6.3-32.6 6.3-25.5 0-47.4-9.5-65.7-28.6-18.3-19-27.4-42.1-27.4-69.1 0-3 .4-7.2 1.1-12.6-14.5-8.4-26-20.2-34.6-35.4-8.5-15.2-12.8-31.8-12.8-49.7 0-19 4.8-36.5 14.3-52.3s22.3-27.5 38.3-35.1c-4.2-11.4-6.3-22.9-6.3-34.3 0-27 9.1-50.1 27.4-69.1s40.2-28.6 65.7-28.6c11.4 0 22.3 2.1 32.6 6.3 8-16.4 19.5-29.6 34.6-39.7 15-10.1 31.5-15.2 49.4-15.2s34.4 5.1 49.4 15.1c15 10.1 26.6 23.3 34.6 39.7 10.3-4.2 21.1-6.3 32.6-6.3 25.5 0 47.3 9.5 65.4 28.6s27.1 42.1 27.1 69.1c0 12.6-1.9 24-5.7 34.3 16 7.6 28.8 19.3 38.3 35.1 9.5 15.9 14.3 33.4 14.3 52.4zm-266.9 77.1 105.7-158.3c2.7-4.2 3.5-8.8 2.6-13.7-1-4.9-3.5-8.8-7.7-11.4-4.2-2.7-8.8-3.6-13.7-2.9-5 .8-9 3.2-12 7.4l-93.1 140-42.9-42.8c-3.8-3.8-8.2-5.6-13.1-5.4-5 .2-9.3 2-13.1 5.4-3.4 3.4-5.1 7.7-5.1 12.9 0 5.1 1.7 9.4 5.1 12.9l58.9 58.9 2.9 2.3c3.4 2.3 6.9 3.4 10.3 3.4 6.7-.1 11.8-2.9 15.2-8.7z"fill="#1da1f2"></path></svg>`;
|
||||
}
|
||||
const waterfall = (a) => {
|
||||
function b(a, b) {
|
||||
var c = window.getComputedStyle(b);
|
||||
return parseFloat(c["margin" + a]) || 0
|
||||
}
|
||||
|
||||
function c(a) {
|
||||
return a + "px"
|
||||
}
|
||||
|
||||
function d(a) {
|
||||
return parseFloat(a.style.top)
|
||||
}
|
||||
|
||||
function e(a) {
|
||||
return parseFloat(a.style.left)
|
||||
}
|
||||
|
||||
function f(a) {
|
||||
return a.clientWidth
|
||||
}
|
||||
|
||||
function g(a) {
|
||||
return a.clientHeight
|
||||
}
|
||||
|
||||
function h(a) {
|
||||
return d(a) + g(a) + b("Bottom", a)
|
||||
}
|
||||
|
||||
function i(a) {
|
||||
return e(a) + f(a) + b("Right", a)
|
||||
}
|
||||
|
||||
function j(a) {
|
||||
a = a.sort(function (a, b) {
|
||||
return h(a) === h(b) ? e(b) - e(a) : h(b) - h(a)
|
||||
})
|
||||
}
|
||||
|
||||
function k(b) {
|
||||
f(a) != t && (b.target.removeEventListener(b.type, arguments.callee), waterfall(a))
|
||||
}
|
||||
"string" == typeof a && (a = document.querySelector(a));
|
||||
var l = [].map.call(a.children, function (a) {
|
||||
return a.style.position = "absolute", a
|
||||
});
|
||||
a.style.position = "relative";
|
||||
var m = [];
|
||||
l.length && (l[0].style.top = "0px", l[0].style.left = c(b("Left", l[0])), m.push(l[0]));
|
||||
for (var n = 1; n < l.length; n++) {
|
||||
var o = l[n - 1],
|
||||
p = l[n],
|
||||
q = i(o) + f(p) <= f(a);
|
||||
if (!q) break;
|
||||
p.style.top = o.style.top, p.style.left = c(i(o) + b("Left", p)), m.push(p)
|
||||
}
|
||||
for (; n < l.length; n++) {
|
||||
j(m);
|
||||
var p = l[n],
|
||||
r = m.pop();
|
||||
p.style.top = c(h(r) + b("Top", p)), p.style.left = c(e(r)), m.push(p)
|
||||
}
|
||||
j(m);
|
||||
var s = m[0];
|
||||
a.style.height = c(h(s) + b("Bottom", s));
|
||||
var t = f(a);
|
||||
window.addEventListener ? window.addEventListener("resize", k) : document.body.onresize = k
|
||||
};
|
||||
|
||||
const fetchAndRenderTalks = () => {
|
||||
const url = 'https://mm.biss.click/api/memo/list';
|
||||
const cacheKey = 'talksCache';
|
||||
const cacheTimeKey = 'talksCacheTime';
|
||||
const cacheDuration = 30 * 60 * 1000; // 半个小时 (30 分钟)
|
||||
|
||||
const cachedData = localStorage.getItem(cacheKey);
|
||||
const cachedTime = localStorage.getItem(cacheTimeKey);
|
||||
const currentTime = new Date().getTime();
|
||||
|
||||
// 判断缓存是否有效
|
||||
if (cachedData && cachedTime && (currentTime - cachedTime < cacheDuration)) {
|
||||
const data = JSON.parse(cachedData);
|
||||
renderTalks(data); // 使用缓存渲染数据
|
||||
} else {
|
||||
if (talkContainer) {
|
||||
talkContainer.innerHTML = '';
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
size: 30
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.code === 0 && data.data && Array.isArray(data.data.list)) {
|
||||
// 缓存数据
|
||||
localStorage.setItem(cacheKey, JSON.stringify(data.data.list));
|
||||
localStorage.setItem(cacheTimeKey, currentTime.toString());
|
||||
renderTalks(data.data.list); // 渲染数据
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染函数
|
||||
function renderTalks(list) {
|
||||
// 确保 data 是一个数组
|
||||
if (Array.isArray(list)) {
|
||||
let items = list.map(item => formatTalk(item, url));
|
||||
items.forEach(item => talkContainer.appendChild(generateTalkElement(item)));
|
||||
waterfall('#talk');
|
||||
} else {
|
||||
console.error('Data is not an array:', list);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const formatTalk = (item, url) => {
|
||||
let date = formatTime(new Date(item.createdAt).toString());
|
||||
let content = item.content;
|
||||
let imgs = item.imgs ? item.imgs.split(',') : [];
|
||||
let text = content;
|
||||
content = text.replace(/\[(.*?)\]\((.*?)\)/g, `<a href="$2">@$1</a>`)
|
||||
.replace(/- \[ \]/g, '⚪')
|
||||
.replace(/- \[x\]/g, '⚫');
|
||||
// 保留换行符,转换 \n 为 <br>
|
||||
content = content.replace(/\n/g, '<br>');
|
||||
// 将content用一个类包裹,便于后续处理
|
||||
content = `<div class="talk_content_text">${content}</div>`;
|
||||
if (imgs.length > 0) {
|
||||
const imgDiv = document.createElement('div');
|
||||
imgDiv.className = 'zone_imgbox';
|
||||
imgs.forEach(e => {
|
||||
const imgLink = document.createElement('a');
|
||||
const imgUrl = domain + e;
|
||||
imgLink.href = imgUrl;
|
||||
imgLink.setAttribute('data-fancybox', 'gallery');
|
||||
imgLink.className = 'fancybox';
|
||||
imgLink.setAttribute('data-thumb', e);
|
||||
const imgTag = document.createElement('img');
|
||||
imgTag.src = domain + e;
|
||||
imgLink.appendChild(imgTag);
|
||||
imgDiv.appendChild(imgLink);
|
||||
});
|
||||
content += imgDiv.outerHTML;
|
||||
}
|
||||
|
||||
// 外链分享功能
|
||||
if (item.externalUrl) {
|
||||
const externalUrl = item.externalUrl;
|
||||
const externalTitle = item.externalTitle;
|
||||
const externalFavicon = item.externalFavicon;
|
||||
|
||||
const externalContainer = `
|
||||
<div class="shuoshuo-external-link">
|
||||
<a class="external-link" href="${externalUrl}" target="_blank" rel="external nofollow noopener noreferrer">
|
||||
<div class="external-link-left" style="background-image: url(${externalFavicon})"></div>
|
||||
<div class="external-link-right">
|
||||
<div class="external-link-title">${externalTitle}</div>
|
||||
<div>点击跳转<i class="fa-solid fa-angle-right"></i></div>
|
||||
</div>
|
||||
</a>
|
||||
</div>`;
|
||||
|
||||
content += externalContainer;
|
||||
}
|
||||
|
||||
const ext = JSON.parse(item.ext || '{}');
|
||||
|
||||
if (ext.music && ext.music.id) {
|
||||
const music = ext.music;
|
||||
const musicUrl = music.api.replace(':server', music.server)
|
||||
.replace(':type', music.type)
|
||||
.replace(':id', music.id);
|
||||
content += `
|
||||
<meting-js server="${music.server}" type="${music.type}" id="${music.id}" api="${music.api}"></meting-js>
|
||||
`;
|
||||
}
|
||||
|
||||
if (ext.doubanMovie && ext.doubanMovie.id) {
|
||||
const doubanMovie = ext.doubanMovie;
|
||||
const doubanMovieUrl = doubanMovie.url;
|
||||
const doubanTitle = doubanMovie.title;
|
||||
// const doubanDesc = doubanMovie.desc || '暂无描述';
|
||||
const doubanImage = doubanMovie.image;
|
||||
const doubanDirector = doubanMovie.director || '未知导演';
|
||||
const doubanRating = doubanMovie.rating || '暂无评分';
|
||||
// const doubanReleaseDate = doubanMovie.releaseDate || '未知上映时间';
|
||||
// const doubanActors = doubanMovie.actors || '未知演员';
|
||||
const doubanRuntime = doubanMovie.runtime || '未知时长';
|
||||
|
||||
content += `
|
||||
<a class="douban-card" href="${doubanMovieUrl}" target="_blank">
|
||||
<div class="douban-card-bgimg" style="background-image: url('${doubanImage}');"></div>
|
||||
<div class="douban-card-left">
|
||||
<div class="douban-card-img" style="background-image: url('${doubanImage}');"></div>
|
||||
</div>
|
||||
<div class="douban-card-right">
|
||||
<div class="douban-card-item"><span>电影名: </span><strong>${doubanTitle}</strong></div>
|
||||
<div class="douban-card-item"><span>导演: </span><span>${doubanDirector}</span></div>
|
||||
<div class="douban-card-item"><span>评分: </span><span>${doubanRating}</span></div>
|
||||
<div class="douban-card-item"><span>时长: </span><span>${doubanRuntime}</span></div>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
if (ext.doubanBook && ext.doubanBook.id) {
|
||||
const doubanBook = ext.doubanBook;
|
||||
const bookUrl = doubanBook.url;
|
||||
const bookTitle = doubanBook.title;
|
||||
// const bookDesc = doubanBook.desc;
|
||||
const bookImage = doubanBook.image;
|
||||
const bookAuthor = doubanBook.author;
|
||||
const bookRating = doubanBook.rating;
|
||||
const bookPubDate = doubanBook.pubDate;
|
||||
|
||||
const bookTemplate = `
|
||||
<a class="douban-card" href="${bookUrl}" target="_blank">
|
||||
<div class="douban-card-bgimg" style="background-image: url('${bookImage}');"></div>
|
||||
<div class="douban-card-left">
|
||||
<div class="douban-card-img" style="background-image: url('${bookImage}');"></div>
|
||||
</div>
|
||||
<div class="douban-card-right">
|
||||
<div class="douban-card-item">
|
||||
<span>书名: </span><strong>${bookTitle}</strong>
|
||||
</div>
|
||||
<div class="douban-card-item">
|
||||
<span>作者: </span><span>${bookAuthor}</span>
|
||||
</div>
|
||||
<div class="douban-card-item">
|
||||
<span>出版年份: </span><span>${bookPubDate}</span>
|
||||
</div>
|
||||
<div class="douban-card-item">
|
||||
<span>评分: </span><span>${bookRating}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
|
||||
content += bookTemplate;
|
||||
}
|
||||
|
||||
if (ext.video && ext.video.type) {
|
||||
const videoType = ext.video.type;
|
||||
const videoUrl = ext.video.value;
|
||||
if (videoType === 'bilibili') {
|
||||
// Bilibili 视频模板
|
||||
// 从形如https://www.bilibili.com/video/BV1VGAPeAEMQ/?vd_source=91b3158d27d98ff41f842508c3794a13 的链接中提取视频 BV1VGAPeAEMQ
|
||||
const biliTemplate = `
|
||||
<div style="position: relative; padding: 30% 45%; margin-top: 10px;">
|
||||
<iframe
|
||||
style="position: absolute; width: 100%; height: 100%; left: 0; top: 0; border-radius: 12px;"
|
||||
src="${videoUrl}&autoplay=0"
|
||||
scrolling="no"
|
||||
frameborder="no"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
`;
|
||||
// 将模板插入到 DOM 中
|
||||
content += biliTemplate;
|
||||
|
||||
} else if (videoType === 'youtube') {
|
||||
// YouTube 视频模板
|
||||
// 从形如https://youtu.be/2V6lvCUPT8I?si=DVhUas6l6qlAr6Ru的链接中提取视频 ID2V6lvCUPT8I
|
||||
const youtubeTemplate = `
|
||||
<div style="position: relative; padding: 30% 45%; margin-top: 10px;">
|
||||
<iframe width="100%"
|
||||
style="position: absolute; width: 100%; height: 100%; left: 0; top: 0; border-radius: 12px;"
|
||||
src="${videoUrl}"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
`;
|
||||
// 将模板插入到 DOM 中
|
||||
content += youtubeTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: content,
|
||||
user: item.user.nickname || '匿名',
|
||||
avatar: item.user.avatarUrl || 'https://p.liiiu.cn/i/2024/03/29/66061417537af.png',
|
||||
date: date,
|
||||
location: item.location || '山西',
|
||||
tags: item.tags ? item.tags.split(',').filter(tag => tag.trim() !== '') : ['无标签'],
|
||||
text: content.replace(/\[(.*?)\]\((.*?)\)/g, '[链接]' + `${imgs.length ? '[图片]' : ''}`)
|
||||
};
|
||||
};
|
||||
|
||||
const generateTalkElement = (item) => {
|
||||
const talkItem = document.createElement('div');
|
||||
talkItem.className = 'talk_item';
|
||||
|
||||
const talkMeta = document.createElement('div');
|
||||
talkMeta.className = 'talk_meta';
|
||||
|
||||
const avatar = document.createElement('img');
|
||||
avatar.className = 'no-lightbox avatar';
|
||||
avatar.src = item.avatar;
|
||||
|
||||
const info = document.createElement('div');
|
||||
info.className = 'info';
|
||||
|
||||
const talkNick = document.createElement('span');
|
||||
talkNick.className = 'talk_nick';
|
||||
talkNick.innerHTML = `${item.user} ${generateIconSVG()}`;
|
||||
|
||||
const talkDate = document.createElement('span');
|
||||
talkDate.className = 'talk_date';
|
||||
talkDate.textContent = item.date;
|
||||
|
||||
const talkContent = document.createElement('div');
|
||||
talkContent.className = 'talk_content';
|
||||
talkContent.innerHTML = item.content;
|
||||
|
||||
const talkBottom = document.createElement('div');
|
||||
talkBottom.className = 'talk_bottom';
|
||||
|
||||
const TagContainer = document.createElement('div');
|
||||
|
||||
const talkTag = document.createElement('span');
|
||||
talkTag.className = 'talk_tag';
|
||||
talkTag.textContent = `🏷️${item.tags}`;
|
||||
|
||||
const locationTag = document.createElement('span');
|
||||
locationTag.className = 'location_tag';
|
||||
locationTag.textContent = `🌍${item.location}`;
|
||||
|
||||
TagContainer.appendChild(talkTag);
|
||||
TagContainer.appendChild(locationTag);
|
||||
|
||||
const commentLink = document.createElement('a');
|
||||
commentLink.href = 'javascript:;';
|
||||
commentLink.onclick = () => goComment(item.text);
|
||||
const commentIcon = document.createElement('span');
|
||||
commentIcon.className = 'icon';
|
||||
const commentIconInner = document.createElement('i');
|
||||
commentIconInner.className = 'fa-solid fa-message fa-fw';
|
||||
commentIcon.appendChild(commentIconInner);
|
||||
commentLink.appendChild(commentIcon);
|
||||
|
||||
talkMeta.appendChild(avatar);
|
||||
info.appendChild(talkNick);
|
||||
info.appendChild(talkDate);
|
||||
talkMeta.appendChild(info);
|
||||
talkItem.appendChild(talkMeta);
|
||||
talkItem.appendChild(talkContent);
|
||||
talkBottom.appendChild(TagContainer);
|
||||
talkBottom.appendChild(commentLink);
|
||||
talkItem.appendChild(talkBottom);
|
||||
|
||||
return talkItem;
|
||||
};
|
||||
|
||||
const goComment = (e) => {
|
||||
const match = e.match(/<div class="talk_content_text">([\s\S]*?)<\/div>/);
|
||||
const textContent = match ? match[1] : "";
|
||||
const n = document.querySelector(".atk-textarea");
|
||||
n.value = `> ${textContent}\n\n`;
|
||||
n.focus();
|
||||
btf.snackbarShow("已为您引用该说说,不删除空格效果更佳");
|
||||
// const n = document.querySelector(".atk-textarea");
|
||||
// n.value = `> ${e}\n\n`;
|
||||
// n.focus();
|
||||
// btf.snackbarShow("已为您引用该说说,不删除空格效果更佳");
|
||||
};
|
||||
|
||||
const formatTime = (time) => {
|
||||
const d = new Date(time);
|
||||
const ls = [
|
||||
d.getFullYear(),
|
||||
d.getMonth() + 1,
|
||||
d.getDate(),
|
||||
d.getHours(),
|
||||
d.getMinutes(),
|
||||
d.getSeconds(),
|
||||
];
|
||||
const r = ls.map((a) => (a.toString().length === 1 ? '0' + a : a));
|
||||
return `${r[0]}-${r[1]}-${r[2]} ${r[3]}:${r[4]}`;
|
||||
};
|
||||
|
||||
fetchAndRenderTalks();
|
||||
}
|
||||
|
||||
renderTalks();
|
||||
|
||||
// function whenDOMReady() {
|
||||
// const talkContainer = document.querySelector('#talk');
|
||||
// talkContainer.innerHTML = '';
|
||||
// fetchAndRenderTalks();
|
||||
// }
|
||||
// whenDOMReady();
|
||||
// document.addEventListener("pjax:complete", whenDOMReady);
|
||||
117
themes/butterfly/source/js/tw_cn.js
Normal file
117
themes/butterfly/source/js/tw_cn.js
Normal file
File diff suppressed because one or more lines are too long
668
themes/butterfly/source/js/txmap.js
Normal file
668
themes/butterfly/source/js/txmap.js
Normal file
@@ -0,0 +1,668 @@
|
||||
//get请求
|
||||
$.ajax({
|
||||
type: 'get',
|
||||
url: 'https://apis.map.qq.com/ws/location/v1/ip',
|
||||
data: {
|
||||
key: 'JOCBZ-5FCRV-CWTP7-5HXTF-OODC2-2PF6R',
|
||||
output: 'jsonp',
|
||||
callback: '?',
|
||||
},
|
||||
dataType: 'jsonp',
|
||||
success: function (res) {
|
||||
window.ipLocation = res;
|
||||
}
|
||||
})
|
||||
function getDistance(e1, n1, e2, n2) {
|
||||
const R = 6371
|
||||
const { sin, cos, asin, PI, hypot } = Math
|
||||
let getPoint = (e, n) => {
|
||||
e *= PI / 180
|
||||
n *= PI / 180
|
||||
return { x: cos(n) * cos(e), y: cos(n) * sin(e), z: sin(n) }
|
||||
}
|
||||
|
||||
let a = getPoint(e1, n1)
|
||||
let b = getPoint(e2, n2)
|
||||
let c = hypot(a.x - b.x, a.y - b.y, a.z - b.z)
|
||||
let r = asin(c / 2) * 2 * R
|
||||
return Math.round(r);
|
||||
}
|
||||
|
||||
function showWelcome() {
|
||||
|
||||
let dist = getDistance(112.92358, 35.79807, ipLocation.result.location.lng, ipLocation.result.location.lat); //这里记得换成自己的经纬度
|
||||
let pos = ipLocation.result.ad_info.nation;
|
||||
let ip;
|
||||
let posdesc;
|
||||
//根据国家、省份、城市信息自定义欢迎语
|
||||
switch (ipLocation.result.ad_info.nation) {
|
||||
case "日本":
|
||||
posdesc = "よろしく,一起去看樱花吗";
|
||||
break;
|
||||
case "美国":
|
||||
posdesc = "Let us live in peace!";
|
||||
break;
|
||||
case "英国":
|
||||
posdesc = "想同你一起夜乘伦敦眼";
|
||||
break;
|
||||
case "俄罗斯":
|
||||
posdesc = "干了这瓶伏特加!";
|
||||
break;
|
||||
case "法国":
|
||||
posdesc = "C'est La Vie";
|
||||
break;
|
||||
case "德国":
|
||||
posdesc = "Die Zeit verging im Fluge.";
|
||||
break;
|
||||
case "澳大利亚":
|
||||
posdesc = "一起去大堡礁吧!";
|
||||
break;
|
||||
case "加拿大":
|
||||
posdesc = "拾起一片枫叶赠予你";
|
||||
break;
|
||||
case "中国":
|
||||
pos = ipLocation.result.ad_info.province + " " + ipLocation.result.ad_info.city + " " + ipLocation.result.ad_info.district;
|
||||
ip = ipLocation.result.ip;
|
||||
switch (ipLocation.result.ad_info.province) {
|
||||
/* 4 直辖市 */
|
||||
case "北京市":
|
||||
posdesc = "北——京——欢迎你~~~";
|
||||
break;
|
||||
case "天津市":
|
||||
posdesc = "讲段相声吧";
|
||||
break;
|
||||
case "上海市":
|
||||
posdesc = "众所周知,中国只有两个城市";
|
||||
break;
|
||||
case "重庆市":
|
||||
posdesc = "8D魔幻城市,导航听了都摇头";
|
||||
break;
|
||||
|
||||
/* 河北 */
|
||||
case "河北省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "石家庄市": posdesc = "正宗安徽牛肉板面发源地!"; break;
|
||||
case "唐山市": posdesc = "烧烤配麻糖,工业风拿捏了"; break;
|
||||
case "秦皇岛市": posdesc = "阿那亚的孤独图书馆,假装在圣托里尼"; break;
|
||||
case "邯郸市": posdesc = "学步桥警告:别邯郸学步嗷"; break;
|
||||
case "邢台市": posdesc = "太行山最绿的地儿,懂行的都去天河山"; break;
|
||||
case "保定市": posdesc = "驴火宇宙中心,加焖子才够味"; break;
|
||||
case "张家口市": posdesc = "崇礼滑雪,冬天也要整点‘雪’业"; break;
|
||||
case "承德市": posdesc = "避暑山庄:皇上都说凉快"; break;
|
||||
case "沧州市": posdesc = "武术之乡,八极拳申请出战"; break;
|
||||
case "廊坊市": posdesc = "北京的后花园,通勤两小时"; break;
|
||||
case "衡水市": posdesc = "衡水老白干,一杯就上头"; break;
|
||||
default: posdesc = "山势巍巍成壁垒,天下雄关铁马金戈由此向,无限江山";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 山西 */
|
||||
case "山西省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "太原市": posdesc = "秋叶蓝不城"; break;
|
||||
case "大同市": posdesc = "刀削面配兔头,碳水快乐星球"; break;
|
||||
case "阳泉市": posdesc = "刘慈欣老家,三体人从这儿起飞"; break;
|
||||
case "长治市": posdesc = "上党从来天下脊,撸串配潞酒"; break;
|
||||
case "晋城市": posdesc = "一方水土养一方人,晋城话说给晋城人..."; break;
|
||||
case "朔州市": posdesc = "右玉羊肉,吃草羊的天花板"; break;
|
||||
case "晋中市": posdesc = "平遥古城拍拍照,晋商票号走一遭"; break;
|
||||
case "运城市": posdesc = "关公老家,天天‘义’起来"; break;
|
||||
case "忻州市": posdesc = "五台山拜佛,顺便许个愿"; break;
|
||||
case "临汾市": posdesc = "洪洞大槐树,寻根问祖集中地"; break;
|
||||
case "吕梁市": posdesc = "杏花村汾酒,喝出魏晋风骨"; break;
|
||||
default: posdesc = "展开坐具长三尺,已占山河五百余";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 内蒙古 */
|
||||
case "内蒙古自治区":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "呼和浩特市": posdesc = "来碗羊杂碎,草原的早晨醒啦"; break;
|
||||
case "包头市": posdesc = "稀土之都,钢铁侠看了都眼馋"; break;
|
||||
case "乌海市": posdesc = "沙漠里看海,赛博朋克即视感"; break;
|
||||
case "赤峰市": posdesc = "对夹夹一切,赤峰人的汉堡"; break;
|
||||
case "通辽市": posdesc = "科尔沁风干牛肉,越嚼越上头"; break;
|
||||
case "鄂尔多斯市": posdesc = "羊绒衫暖和,土豪也多"; break;
|
||||
case "呼伦贝尔市": posdesc = "大草原配套马杆,汉子诚不欺我"; break;
|
||||
case "巴彦淖尔市": posdesc = "河套面粉,馒头好吃到哭"; break;
|
||||
case "乌兰察布市": posdesc = "土豆开会,薯条自由"; break;
|
||||
case "兴安盟": posdesc = "阿尔山秋景,美到内存爆炸"; break;
|
||||
case "锡林郭勒盟": posdesc = "羊肉届鄙视链顶端"; break;
|
||||
case "阿拉善盟": posdesc = "左手沙漠右手胡杨,YYDS"; break;
|
||||
default: posdesc = "天苍苍,野茫茫,风吹草低见牛羊";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 辽宁 */
|
||||
case "辽宁省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "沈阳市": posdesc = "鸡架老雪花,沈阳人的快乐水"; break;
|
||||
case "大连市": posdesc = "浪漫之都,海鲜吃到扶墙"; break;
|
||||
case "鞍山市": posdesc = "鞍钢硬朗,千山更硬朗"; break;
|
||||
case "抚顺市": posdesc = "煤都往事,麻辣拌续命"; break;
|
||||
case "本溪市": posdesc = "本溪水洞,东北地下艺术宫殿"; break;
|
||||
case "丹东市": posdesc = "草莓超大颗,对岸就是朝鲜"; break;
|
||||
case "锦州市": posdesc = "锦州烧烤,小串卷一切"; break;
|
||||
case "营口市": posdesc = "东北小三亚,鲅鱼圈冲鸭"; break;
|
||||
case "阜新市": posdesc = "玛瑙之都,剁手也要买"; break;
|
||||
case "辽阳市": posdesc = "白塔青年,古城也很潮"; break;
|
||||
case "盘锦市": posdesc = "蟹稻共生,盘锦螃蟹YYDS"; break;
|
||||
case "铁岭市": posdesc = "宇宙的尽头,李雪琴盖章"; break;
|
||||
case "朝阳市": posdesc = "古生物化石,恐龙看了都点赞"; break;
|
||||
case "葫芦岛市": posdesc = "兴城海滨,东北人自己的马尔代夫"; break;
|
||||
default: posdesc = "我想吃烤鸡架!";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 吉林 */
|
||||
case "吉林省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "长春市": posdesc = "汽车之城,东北F4老大"; break;
|
||||
case "吉林市": posdesc = "雾凇奇观,冬日限定皮肤"; break;
|
||||
case "四平市": posdesc = "李连贵熏肉大饼,碳水+脂肪的双重暴击"; break;
|
||||
case "辽源市": posdesc = "袜子走天下,辽源制造"; break;
|
||||
case "通化市": posdesc = "葡萄酒之乡,干杯老铁"; break;
|
||||
case "白山市": posdesc = "长白山天池,水怪等你合影"; break;
|
||||
case "松原市": posdesc = "查干湖冬捕,一网几十万斤"; break;
|
||||
case "白城市": posdesc = "向海鹤舞,丹顶鹤的T台"; break;
|
||||
case "延边州": posdesc = "朝鲜族美食宇宙中心,冷面泡菜的快乐老家"; break;
|
||||
default: posdesc = "状元阁就是东北烧烤之王";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 黑龙江 */
|
||||
case "黑龙江省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "哈尔滨市": posdesc = "中央大街走一走,俄式风情拿捏"; break;
|
||||
case "齐齐哈尔市": posdesc = "BBQ烤肉配鹤舞,真·鹤城"; break;
|
||||
case "鸡西市": posdesc = "刀削面加辣,鸡西人的乡愁"; break;
|
||||
case "鹤岗市": posdesc = "房价白菜,躺平圣地"; break;
|
||||
case "双鸭山市": posdesc = "宝清大白板,瓜子界天花板"; break;
|
||||
case "大庆市": posdesc = "铁人精神+坑烤,香到犯规"; break;
|
||||
case "伊春市": posdesc = "林都氧吧,每一口都是洗肺"; break;
|
||||
case "佳木斯市": posdesc = "蔓越莓老家,洋气得很"; break;
|
||||
case "七台河市": posdesc = "短道速滑冠军制造机"; break;
|
||||
case "牡丹江市": posdesc = "镜泊湖跳水,瀑布下饺子"; break;
|
||||
case "黑河市": posdesc = "早市买大列巴,对岸俄国即视感"; break;
|
||||
case "绥化市": posdesc = "寒地黑土,东北粮仓"; break;
|
||||
case "大兴安岭地区": posdesc = "找北请到漠河,泼水成冰"; break;
|
||||
default: posdesc = "很喜欢哈尔滨大剧院";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 江苏 */
|
||||
case "江苏省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "南京市": posdesc = "这是我挺想去的城市啦"; break;
|
||||
case "无锡市": posdesc = "太湖明珠,酱排骨甜到心坎"; break;
|
||||
case "徐州市": posdesc = "地锅鸡+烧烤,苏北硬核碳水"; break;
|
||||
case "常州市": posdesc = "恐龙园冲鸭,暴龙陪你自拍"; break;
|
||||
case "苏州市": posdesc = "上有天堂,下有苏杭"; break;
|
||||
case "南通市": posdesc = "教育卷王,学霸生产线"; break;
|
||||
case "连云港市": posdesc = "花果山见猴哥,连岛看海"; break;
|
||||
case "淮安市": posdesc = "世界美食之都,盱眙龙虾冲"; break;
|
||||
case "盐城市": posdesc = "丹顶鹤与麋鹿的双厨狂喜"; break;
|
||||
case "扬州市": posdesc = "早上皮包水,晚上水包皮"; break;
|
||||
case "镇江市": posdesc = "香醋摆不坏,肴肉不当菜"; break;
|
||||
case "泰州市": posdesc = "早茶三巨头,烫干丝安排"; break;
|
||||
case "宿迁市": posdesc = "刘强东老家,客服之都"; break;
|
||||
default: posdesc = "散装是必须要散装的";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 浙江 */
|
||||
case "浙江省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "杭州市": posdesc = "西湖醋鱼警告,不好吃别打我"; break;
|
||||
case "宁波市": posdesc = "汤圆加海鲜,甜咸永动机"; break;
|
||||
case "温州市": posdesc = "江南皮革厂回归,老板没跑"; break;
|
||||
case "嘉兴市": posdesc = "粽子宇宙中心,肉粽yyds"; break;
|
||||
case "湖州市": posdesc = "安吉白茶配太湖蟹,双倍快乐"; break;
|
||||
case "绍兴市": posdesc = "孔乙己的茴香豆,加酒不加水"; break;
|
||||
case "金华市": posdesc = "义乌小商品,买全球卖全球"; break;
|
||||
case "衢州市": posdesc = "三头一掌,辣到灵魂出窍"; break;
|
||||
case "舟山市": posdesc = "东海小黄鱼,鲜到眉毛掉"; break;
|
||||
case "台州市": posdesc = "糯叽叽嵌糕,台州人的汉堡"; break;
|
||||
case "丽水市": posdesc = "云和梯田,摄影佬的卷王"; break;
|
||||
default: posdesc = "东风渐绿西湖柳,雁已还人未南归";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 安徽 */
|
||||
case "安徽省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "合肥市": posdesc = "风投之城,赌出来的霸都"; break;
|
||||
case "芜湖市": posdesc = "芜湖起飞,起飞~"; break;
|
||||
case "蚌埠市": posdesc = "蚌埠住了,真的住了"; break;
|
||||
case "淮南市": posdesc = "牛肉汤+烧饼,淮南人的早晨"; break;
|
||||
case "马鞍山市": posdesc = "因钢设市,李白终老于此"; break;
|
||||
case "淮北市": posdesc = "口子窖,喝出安徽的烈"; break;
|
||||
case "铜陵市": posdesc = "铜都,铜臭味儿香得很"; break;
|
||||
case "安庆市": posdesc = "黄梅戏一开嗓,谁不说家乡好"; break;
|
||||
case "黄山市": posdesc = "迎客松打卡,腿已断"; break;
|
||||
case "滁州市": posdesc = "琅琊山醉翁亭,欧阳修都说赞"; break;
|
||||
case "阜阳市": posdesc = "格拉条界的天花板,碳水炸弹"; break;
|
||||
case "宿州市": posdesc = "砀山梨,一口下去全是汁"; break;
|
||||
case "六安市": posdesc = "六安瓜片,茶香飘出皖西"; break;
|
||||
case "亳州市": posdesc = "华佗故里,药材香飘全国"; break;
|
||||
case "池州市": posdesc = "九华山许愿,佛系青年集合"; break;
|
||||
case "宣城市": posdesc = "文房四宝之乡,笔墨纸砚管够"; break;
|
||||
default: posdesc = "蚌埠住了,芜湖起飞";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 福建 */
|
||||
case "福建省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "福州市": posdesc = "佛跳墙警告,香到隔壁台湾"; break;
|
||||
case "厦门市": posdesc = "鼓浪屿挤爆,网红拍照机位排队"; break;
|
||||
case "莆田市": posdesc = "假鞋之都,真香定律"; break;
|
||||
case "三明市": posdesc = "沙县小吃出三明,全球开店"; break;
|
||||
case "泉州市": posdesc = "宋元东方第一大港,蟳蜅女簪花围"; break;
|
||||
case "漳州市": posdesc = "四果汤+卤面,闽南胃的天堂"; break;
|
||||
case "南平市": posdesc = "武夷山岩茶,一泡就破产"; break;
|
||||
case "龙岩市": posdesc = "客家土楼,大鱼海棠取景地"; break;
|
||||
case "宁德市": posdesc = "霞浦滩涂,摄影佬的卷王"; break;
|
||||
default: posdesc = "井邑白云间,岩城远带山";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 江西 */
|
||||
case "江西省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "南昌市": posdesc = "拌粉+瓦罐汤,南昌人的早晨"; break;
|
||||
case "景德镇市": posdesc = "千年瓷都,买瓷器按斤称"; break;
|
||||
case "萍乡市": posdesc = "武功山金顶,云海配帐篷"; break;
|
||||
case "九江市": posdesc = "庐山恋,爱情开始的地方"; break;
|
||||
case "新余市": posdesc = "钢铁之城,仙女湖许愿"; break;
|
||||
case "鹰潭市": posdesc = "龙虎山天师府,道家仙气"; break;
|
||||
case "赣州市": posdesc = "脐橙管饱,客家围屋走一圈"; break;
|
||||
case "吉安市": posdesc = "井冈山红色之旅,星星之火"; break;
|
||||
case "宜春市": posdesc = "月亮之都,温汤富硒"; break;
|
||||
case "抚州市": posdesc = "才子之乡,王安石汤显祖"; break;
|
||||
case "上饶市": posdesc = "婺源油菜花,摄影内存告急"; break;
|
||||
default: posdesc = "落霞与孤鹜齐飞,秋水共长天一色";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 山东 */
|
||||
case "山东省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "济南市": posdesc = "大明湖夏雨荷,你还记得吗"; break;
|
||||
case "青岛市": posdesc = "哈啤酒吃嘎啦,塑料袋打酒"; break;
|
||||
case "淄博市": posdesc = "烧烤三件套,灵魂蘸料"; break;
|
||||
case "枣庄市": posdesc = "台儿庄古城,辣子鸡真香"; break;
|
||||
case "东营市": posdesc = "黄河入海口,看鸳鸯锅"; break;
|
||||
case "烟台市": posdesc = "张裕葡萄酒,微醺在海边"; break;
|
||||
case "潍坊市": posdesc = "风筝之都,天上全是佩奇"; break;
|
||||
case "济宁市": posdesc = "曲阜三孔,拜见孔夫子"; break;
|
||||
case "泰安市": posdesc = "泰山十八盘,腿抖到明年"; break;
|
||||
case "威海市": posdesc = "干净到反光,韩餐便宜哭"; break;
|
||||
case "日照市": posdesc = "日出先照,海鲜吃到撑"; break;
|
||||
case "临沂市": posdesc = "物流之都,煎饼卷宇宙"; break;
|
||||
case "德州市": posdesc = "德州扒鸡,高铁必带"; break;
|
||||
case "聊城市": posdesc = "东昌湖配阿胶,补血又浪漫"; break;
|
||||
case "滨州市": posdesc = "沾化冬枣,甜过初恋"; break;
|
||||
case "菏泽市": posdesc = "牡丹甲天下,曹州烧饼酥掉渣"; break;
|
||||
default: posdesc = "遥望齐州九点烟,一泓海水杯中泻";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 河南 */
|
||||
case "河南省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "郑州市": posdesc = "豫州之域,天地之中"; break;
|
||||
case "开封市": posdesc = "刚正不阿包青天"; break;
|
||||
case "洛阳市": posdesc = "洛阳牡丹甲天下"; break;
|
||||
case "平顶山市": posdesc = "中原大佛,抬头颈椎病好了"; break;
|
||||
case "安阳市": posdesc = "甲骨文老家,文字博物馆走起"; break;
|
||||
case "鹤壁市": posdesc = "朝歌夜弦,封神榜起源"; break;
|
||||
case "新乡市": posdesc = "比干庙打卡,忠臣Buff"; break;
|
||||
case "焦作市": posdesc = "云台山瀑布,飞流直下三千尺"; break;
|
||||
case "濮阳市": posdesc = "中华龙乡,杂技之乡"; break;
|
||||
case "许昌市": posdesc = "曹魏故都,胖东来逛断腿"; break;
|
||||
case "漯河市": posdesc = "卫龙辣条,童年回忆杀"; break;
|
||||
case "三门峡市": posdesc = "天鹅之城,黄河第一坝"; break;
|
||||
case "南阳市": posdesc = "臣本布衣,躬耕于南阳此南阳非彼南阳!"; break;
|
||||
case "商丘市": posdesc = "火文化起源,燧人氏钻木取火"; break;
|
||||
case "信阳市": posdesc = "毛尖茶香,热干面河南分面"; break;
|
||||
case "周口市": posdesc = "老子故里,胡辣汤配油条"; break;
|
||||
case "驻马店市": posdesc = "峰峰有奇石,石石挟仙气嵖岈山的花很美哦!"; break;
|
||||
case "济源市": posdesc = "愚公移山,山还在人已富"; break;
|
||||
default: posdesc = "可否带我品尝河南烩面啦?";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 湖北 */
|
||||
case "湖北省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "武汉市": posdesc = "热干面配豆皮,过早天堂"; break;
|
||||
case "黄石市": posdesc = "矿冶古都,仙岛湖打卡"; break;
|
||||
case "十堰市": posdesc = "武当山修仙,问道金顶"; break;
|
||||
case "宜昌市": posdesc = "三峡大坝,国之重器"; break;
|
||||
case "襄阳市": posdesc = "郭靖守过的城,牛肉面管饱"; break;
|
||||
case "鄂州市": posdesc = "武昌鱼原产地,真香警告"; break;
|
||||
case "荆门市": posdesc = "明显陵+漳河鱼,历史与美食"; break;
|
||||
case "孝感市": posdesc = "麻糖米酒,孝感人自带甜味"; break;
|
||||
case "荆州市": posdesc = "荆州古城,关羽大意失荆州"; break;
|
||||
case "黄冈市": posdesc = "红安将军县!辈出将才!"; break;
|
||||
case "咸宁市": posdesc = "温泉泡到爽,桂花糕管够"; break;
|
||||
case "随州市": posdesc = "炎帝故里,编钟一响谁与争锋"; break;
|
||||
case "恩施州": posdesc = "恩施大峡谷,一炷香镇楼"; break;
|
||||
case "仙桃市": posdesc = "体操之乡,李小双李大双"; break;
|
||||
case "潜江市": posdesc = "油焖大虾,夜宵霸主"; break;
|
||||
case "天门市": posdesc = "蒸菜三蒸,香到邻居敲门"; break;
|
||||
case "神农架林区": posdesc = "野人出没,注意熊出没"; break;
|
||||
default: posdesc = "来碗热干面~";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 湖南 */
|
||||
case "湖南省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "长沙市": posdesc = "74751,长沙斯塔克"; break;
|
||||
case "株洲市": posdesc = "火车拖来的城市,湘菜也硬核"; break;
|
||||
case "湘潭市": posdesc = "毛氏红烧肉,伟人同款"; break;
|
||||
case "衡阳市": posdesc = "南岳衡山,寿比南山"; break;
|
||||
case "邵阳市": posdesc = "邵阳米粉,辣到灵魂出窍"; break;
|
||||
case "岳阳市": posdesc = "岳阳楼记,先天下之忧而忧"; break;
|
||||
case "常德市": posdesc = "常德牛肉粉,嗦粉人集合"; break;
|
||||
case "张家界市": posdesc = "阿凡达取景地,悬浮山真香"; break;
|
||||
case "益阳市": posdesc = "安化黑茶,越陈越香"; break;
|
||||
case "郴州市": posdesc = "雾漫小东江,摄影佬天堂"; break;
|
||||
case "永州市": posdesc = "柳宗元打call,永州之野产异蛇"; break;
|
||||
case "怀化市": posdesc = "芷江受降坊,历史不能忘"; break;
|
||||
case "娄底市": posdesc = "蚩尤故里,梅山文化"; break;
|
||||
case "湘西州": posdesc = "凤凰古城,沈从文笔下的边城"; break;
|
||||
default: posdesc = "74751,长沙斯塔克";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 广东 */
|
||||
case "广东省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "广州市": posdesc = "看小蛮腰,喝早茶了嘛~"; break;
|
||||
case "韶关市": posdesc = "丹霞山阳元石,脸红心跳"; break;
|
||||
case "深圳市": posdesc = "今天你逛商场了嘛~"; break;
|
||||
case "珠海市": posdesc = "情侣路走断腿,日月贝打卡"; break;
|
||||
case "汕头市": posdesc = "牛肉丸弹到飞起,粿条管够"; break;
|
||||
case "佛山市": posdesc = "无影脚黄飞鸿,顺德美食天堂"; break;
|
||||
case "江门市": posdesc = "开平碉楼,让子弹飞取景"; break;
|
||||
case "湛江市": posdesc = "生蚝按盆吃,快乐似神仙"; break;
|
||||
case "茂名市": posdesc = "荔枝之乡,杨贵妃同款"; break;
|
||||
case "肇庆市": posdesc = "七星岩+鼎湖山,天然氧吧"; break;
|
||||
case "惠州市": posdesc = "双月湾冲浪,巽寮湾发呆"; break;
|
||||
case "梅州市": posdesc = "客家娘酒,酿出乡愁"; break;
|
||||
case "汕尾市": posdesc = "二马路夜市,吃到扶墙"; break;
|
||||
case "河源市": posdesc = "万绿湖绿出屏,矿泉水直接喝"; break;
|
||||
case "阳江市": posdesc = "阳春合水!博主家乡~ 欢迎来玩~"; break;
|
||||
case "清远市": posdesc = "清远鸡滑到筷子夹不住"; break;
|
||||
case "东莞市": posdesc = "世界工厂,潮玩之都"; break;
|
||||
case "中山市": posdesc = "孙中山故里,乳鸽脆皮流油"; break;
|
||||
case "潮州市": posdesc = "牛肉火锅+粿条,潮汕味拉满"; break;
|
||||
case "揭阳市": posdesc = "普宁豆干,外酥里嫩"; break;
|
||||
case "云浮市": posdesc = "石材王国,云石艺术"; break;
|
||||
default: posdesc = "来两斤福建人~";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 广西 */
|
||||
case "广西壮族自治区":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "南宁市": posdesc = "友仔友女,撩螺咩?"; break;
|
||||
case "柳州市": posdesc = "螺蛳粉真香,鼻子先流泪"; break;
|
||||
case "桂林市": posdesc = "桂林山水甲天下"; break;
|
||||
case "梧州市": posdesc = "龟苓膏原产地,苦尽甘来"; break;
|
||||
case "北海市": posdesc = "银滩冲浪,涠洲岛潜水"; break;
|
||||
case "防城港市": posdesc = "京族三岛,中国最后的海上吉普赛"; break;
|
||||
case "钦州市": posdesc = "大蚝自由,烧烤配啤酒"; break;
|
||||
case "贵港市": posdesc = "荷美覃塘,莲藕排骨汤"; break;
|
||||
case "玉林市": posdesc = "狗肉节争议,牛腩粉真香"; break;
|
||||
case "百色市": posdesc = "芒果之乡,甜过初恋"; break;
|
||||
case "贺州市": posdesc = "黄姚古镇,发圈假装在江南"; break;
|
||||
case "河池市": posdesc = "巴马长寿村,吸氧续命"; break;
|
||||
case "来宾市": posdesc = "世界瑶都,瑶族风情"; break;
|
||||
case "崇左市": posdesc = "德天跨国瀑布,越南一步之遥"; break;
|
||||
default: posdesc = "桂林山水甲天下";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 海南 */
|
||||
case "海南省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "海口市": posdesc = "老爸茶一坐一下午,悠闲省"; break;
|
||||
case "三亚市": posdesc = "椰梦长廊,潜水看珊瑚"; break;
|
||||
case "三沙市": posdesc = "祖国最南端,海景房缺房"; break;
|
||||
case "儋州市": posdesc = "东坡书院,海南文化担当"; break;
|
||||
case "五指山市": posdesc = "黎族三月三,竹竿舞跳起来"; break;
|
||||
case "琼海市": posdesc = "博鳌论坛,高端大气上档次"; break;
|
||||
case "文昌市": posdesc = "航天发射场,看火箭飞天"; break;
|
||||
case "万宁市": posdesc = "日月湾冲浪,浪到飞起"; break;
|
||||
case "东方市": posdesc = "鱼鳞洲灯塔,最美晚霞"; break;
|
||||
case "定安县": posdesc = "仙沟牛肉,现切现涮"; break;
|
||||
case "屯昌县": posdesc = "油画之乡,艺术气息"; break;
|
||||
case "澄迈县": posdesc = "富硒福地,长寿老人扎堆"; break;
|
||||
case "临高县": posdesc = "临高角灯塔,海南最北端"; break;
|
||||
case "白沙县": posdesc = "绿茶飘香,天然氧吧"; break;
|
||||
case "昌江县": posdesc = "木棉花开,红满山坡"; break;
|
||||
case "乐东县": posdesc = "莺歌海盐场,天空之镜"; break;
|
||||
case "陵水县": posdesc = "清水湾会唱歌的沙滩"; break;
|
||||
case "保亭县": posdesc = "七仙岭温泉,泡到不想走"; break;
|
||||
case "琼中县": posdesc = "黎母山探秘,雨林徒步"; break;
|
||||
default: posdesc = "朝观日出逐白浪,夕看云起收霞光";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 四川 */
|
||||
case "四川省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "成都市": posdesc = "巴适得板,熊猫儿等你"; break;
|
||||
case "自贡市": posdesc = "恐龙之乡,盐帮菜辣哭"; break;
|
||||
case "攀枝花市": posdesc = "阳光花城,芒果甜到爆"; break;
|
||||
case "泸州市": posdesc = "1573国窖,喝出高级感"; break;
|
||||
case "德阳市": posdesc = "三星堆上新,外星人实锤"; break;
|
||||
case "绵阳市": posdesc = "科技城+米粉,文武双全"; break;
|
||||
case "广元市": posdesc = "剑门关鸟道,腿抖到明年"; break;
|
||||
case "遂宁市": posdesc = "观音故里,灵泉寺许愿"; break;
|
||||
case "内江市": posdesc = "大千故里,蜜饯甜到齁"; break;
|
||||
case "乐山市": posdesc = "大佛脚底下吃跷脚牛肉"; break;
|
||||
case "南充市": posdesc = "川北凉粉,辣到喷火"; break;
|
||||
case "眉山市": posdesc = "东坡肘子,肥而不腻"; break;
|
||||
case "宜宾市": posdesc = "五粮液配燃面,上头"; break;
|
||||
case "广安市": posdesc = "邓小平故里,红色之旅"; break;
|
||||
case "达州市": posdesc = "灯影牛肉,薄如纸片"; break;
|
||||
case "雅安市": posdesc = "熊猫老家,三雅文化"; break;
|
||||
case "巴中市": posdesc = "光雾山红叶,美到窒息"; break;
|
||||
case "资阳市": posdesc = "安岳柠檬,酸爽炸裂"; break;
|
||||
case "阿坝州": posdesc = "九寨沟归来不看水"; break;
|
||||
case "甘孜州": posdesc = "丁真家乡,理塘打卡"; break;
|
||||
case "凉山州": posdesc = "西昌烧烤,火盆边撸串"; break;
|
||||
default: posdesc = "康康川妹子";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 贵州 */
|
||||
case "贵州省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "贵阳市": posdesc = "避暑之都,丝娃娃卷一切"; break;
|
||||
case "六盘水市": posdesc = "凉都19℃,夏天盖被子"; break;
|
||||
case "遵义市": posdesc = "遵义会议,红色圣地"; break;
|
||||
case "安顺市": posdesc = "黄果树瀑布,水帘洞打卡"; break;
|
||||
case "毕节市": posdesc = "百里杜鹃,花海刷屏"; break;
|
||||
case "铜仁市": posdesc = "梵净山蘑菇石,天空之城"; break;
|
||||
case "黔西南州": posdesc = "万峰林骑行,最美喀斯特"; break;
|
||||
case "黔东南州": posdesc = "千户苗寨,银饰叮当"; break;
|
||||
case "黔南州": posdesc = "天眼FAST,找外星人"; break;
|
||||
default: posdesc = "茅台,学生,再塞200";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 云南 */
|
||||
case "云南省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "昆明市": posdesc = "春城无处不飞花,过桥米线管饱"; break;
|
||||
case "曲靖市": posdesc = "宣威火腿,云腿月饼香"; break;
|
||||
case "玉溪市": posdesc = "红塔山+抚仙湖,烟与湖"; break;
|
||||
case "保山市": posdesc = "腾冲温泉,火山热海"; break;
|
||||
case "昭通市": posdesc = "苹果之城,丑苹果甜到心"; break;
|
||||
case "丽江市": posdesc = "古城艳遇,玉龙雪山蓝月谷"; break;
|
||||
case "普洱市": posdesc = "左手咖啡右手茶,人生赢家"; break;
|
||||
case "临沧市": posdesc = "冰岛老寨,普洱茶天花板"; break;
|
||||
case "楚雄州": posdesc = "彝人古镇,火把节狂欢"; break;
|
||||
case "红河州": posdesc = "元阳梯田,光影天堂"; break;
|
||||
case "文山州": posdesc = "普者黑荷花,三生三世"; break;
|
||||
case "西双版纳州": posdesc = "星光夜市,傣味烧烤"; break;
|
||||
case "大理州": posdesc = "苍山雪洱海月,风花雪月"; break;
|
||||
case "德宏州": posdesc = "芒市泡鲁达,缅味十足"; break;
|
||||
case "怒江州": posdesc = "丙中洛人神共居,世外桃源"; break;
|
||||
case "迪庆州": posdesc = "香格里拉,心中的日月"; break;
|
||||
default: posdesc = "玉龙飞舞云缠绕,万仞冰川直耸天";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 西藏 */
|
||||
case "西藏自治区":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "拉萨市": posdesc = "日光之城,布达拉宫朝圣"; break;
|
||||
case "日喀则市": posdesc = "珠峰大本营,8848.86打卡"; break;
|
||||
case "昌都市": posdesc = "然乌湖倒影,318此生必驾"; break;
|
||||
case "林芝市": posdesc = "桃花沟十里桃林,三生三世"; break;
|
||||
case "山南市": posdesc = "羊卓雍措,天鹅之湖"; break;
|
||||
case "那曲市": posdesc = "羌塘草原,藏羚羊奔跑"; break;
|
||||
case "阿里地区": posdesc = "冈仁波齐转山,信仰之旅"; break;
|
||||
default: posdesc = "躺在茫茫草原上,仰望蓝天";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 陕西 */
|
||||
case "陕西省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "西安市": posdesc = "碳水之都,肉夹馍配冰峰"; break;
|
||||
case "铜川市": posdesc = "药王故里,孙思邈养生"; break;
|
||||
case "宝鸡市": posdesc = "青铜器博物院,何尊镇馆"; break;
|
||||
case "咸阳市": posdesc = "秦始皇老家,biangbiang面"; break;
|
||||
case "渭南市": posdesc = "华山论剑,险中求胜"; break;
|
||||
case "延安市": posdesc = "宝塔山+枣园,红色之旅"; break;
|
||||
case "汉中市": posdesc = "油菜花田,汉人老家"; break;
|
||||
case "榆林市": posdesc = "镇北台+榆林豆腐,塞上江南"; break;
|
||||
case "安康市": posdesc = "瀛湖烤鱼,陕南小江南"; break;
|
||||
case "商洛市": posdesc = "金丝峡漂流,天然空调"; break;
|
||||
default: posdesc = "来份臊子面加馍";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 甘肃 */
|
||||
case "甘肃省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "兰州市": posdesc = "一碗牛肉面,拉开甘肃序幕"; break;
|
||||
case "嘉峪关市": posdesc = "天下第一雄关,长城终点"; break;
|
||||
case "金昌市": posdesc = "镍都金昌,紫金花海"; break;
|
||||
case "白银市": posdesc = "黄河石林,大自然的鬼斧神工"; break;
|
||||
case "天水市": posdesc = "麦积山石窟,东方雕塑馆"; break;
|
||||
case "武威市": posdesc = "马踏飞燕,中国旅游标志"; break;
|
||||
case "张掖市": posdesc = "七彩丹霞,打翻调色盘"; break;
|
||||
case "平凉市": posdesc = "崆峒山论剑,武侠梦"; break;
|
||||
case "酒泉市": posdesc = "敦煌飞天,卫星发射"; break;
|
||||
case "庆阳市": posdesc = "香包刺绣,陇绣一绝"; break;
|
||||
case "定西市": posdesc = "马铃薯之乡,洋芋擦擦"; break;
|
||||
case "陇南市": posdesc = "官鹅沟秋景,陇上小九寨"; break;
|
||||
case "临夏州": posdesc = "刘家峡水库,蓝到犯规"; break;
|
||||
case "甘南州": posdesc = "扎尕那石城,神仙居住"; break;
|
||||
default: posdesc = "羌笛何须怨杨柳,春风不度玉门关";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 青海 */
|
||||
case "青海省":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "西宁市": posdesc = "青海湖骑行,塔尔寺转经"; break;
|
||||
case "海东市": posdesc = "喇家遗址,一碗4000年前的面条"; break;
|
||||
case "海北州": posdesc = "门源花海,金色海洋"; break;
|
||||
case "黄南州": posdesc = "热贡艺术,唐卡小镇"; break;
|
||||
case "海南州": posdesc = "龙羊峡大坝,黄河第一坝"; break;
|
||||
case "果洛州": posdesc = "年保玉则,天神后花园"; break;
|
||||
case "玉树州": posdesc = "三江之源,可可西里"; break;
|
||||
case "海西州": posdesc = "茶卡盐湖,天空之镜"; break;
|
||||
default: posdesc = "牛肉干和老酸奶都好好吃";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 宁夏 */
|
||||
case "宁夏回族自治区":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "银川市": posdesc = "塞上江南,手抓羊排"; break;
|
||||
case "石嘴山市": posdesc = "沙湖苇舟,鸟的天堂"; break;
|
||||
case "吴忠市": posdesc = "早茶拉面,吴忠人的早晨"; break;
|
||||
case "固原市": posdesc = "六盘山红军长征,红色之旅"; break;
|
||||
case "中卫市": posdesc = "沙坡头滑沙,黄河飞索"; break;
|
||||
default: posdesc = "大漠孤烟直,长河落日圆";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 新疆 */
|
||||
case "新疆维吾尔自治区":
|
||||
switch (ipLocation.result.ad_info.city) {
|
||||
case "乌鲁木齐市": posdesc = "国际大巴扎,烤包子真香"; break;
|
||||
case "克拉玛依市": posdesc = "黑油山,石油之城"; break;
|
||||
case "吐鲁番市": posdesc = "火焰山+葡萄沟,冰火两重天"; break;
|
||||
case "哈密市": posdesc = "哈密瓜原产地,甜到齁"; break;
|
||||
case "昌吉州": posdesc = "天山天池,王母娘娘洗脚盆"; break;
|
||||
case "博尔塔拉州": posdesc = "赛里木湖,大西洋最后一滴泪"; break;
|
||||
case "巴音郭楞州": posdesc = "巴音布鲁克,九曲十八弯"; break;
|
||||
case "阿克苏地区": posdesc = "冰糖心苹果,甜到心坎"; break;
|
||||
case "克孜勒苏州": posdesc = "帕米尔高原,冰山上的来客"; break;
|
||||
case "喀什地区": posdesc = "古城开城仪式,一秒穿越"; break;
|
||||
case "和田地区": posdesc = "和田玉买买提,剁手之旅"; break;
|
||||
case "伊犁州": posdesc = "杏花沟花海,美到窒息"; break;
|
||||
case "塔城地区": posdesc = "手抓肉大盘,吃肉不吃蒜"; break;
|
||||
case "阿勒泰地区": posdesc = "喀纳斯湖怪,等你来找"; break;
|
||||
default: posdesc = "驼铃古道丝绸路,胡马犹闻唐汉风";
|
||||
}
|
||||
break;
|
||||
|
||||
/* 港澳台 */
|
||||
case "台湾省":
|
||||
posdesc = "我在这头,大陆在那头";
|
||||
break;
|
||||
case "香港特别行政区":
|
||||
posdesc = "永定贼有残留地鬼嚎,迎击光非岁玉";
|
||||
break;
|
||||
case "澳门特别行政区":
|
||||
posdesc = "性感荷官,在线发牌";
|
||||
break;
|
||||
|
||||
/* 兜底 */
|
||||
default:
|
||||
posdesc = "带我去你的城市逛逛吧!";
|
||||
}
|
||||
break;
|
||||
default:
|
||||
posdesc = "带我去你的国家逛逛吧";
|
||||
break;
|
||||
}
|
||||
|
||||
//根据本地时间切换欢迎语
|
||||
let timeChange;
|
||||
let date = new Date();
|
||||
if (date.getHours() >= 5 && date.getHours() < 11) timeChange = "<span>🌤️ 早上好,一日之计在于晨</span>";
|
||||
else if (date.getHours() >= 11 && date.getHours() < 13) timeChange = "<span>☀️ 中午好,记得午休喔~</span>";
|
||||
else if (date.getHours() >= 13 && date.getHours() < 17) timeChange = "<span>🕞 下午好,饮茶先啦!</span>";
|
||||
else if (date.getHours() >= 17 && date.getHours() < 19) timeChange = "<span>🚶♂️ 即将下班,记得按时吃饭~</span>";
|
||||
else if (date.getHours() >= 19 && date.getHours() < 23) timeChange = "<span>🌙 晚上好,夜生活嗨起来!</span>";
|
||||
else timeChange = "夜深了,早点休息,少熬夜";
|
||||
|
||||
// 新增ipv6显示为指定内容
|
||||
if (ip.includes(":")) {
|
||||
ip = "<br>好复杂,咱看不懂~(ipv6)";
|
||||
}
|
||||
try {
|
||||
//自定义文本和需要放的位置
|
||||
document.getElementById("welcome-info").innerHTML =
|
||||
`欢迎来自 <b><span style="color: var(--kouseki-ip-color);font-size: var(--kouseki-gl-size)">${pos}</span></b> 的小友💖<br>${posdesc}🍂<br>当前位置距博主约 <b><span style="color: var(--kouseki-ip-color)">${dist}</span></b> 公里!<br>您的IP地址为:<b><span class="ip-mask">${ip}</span></b><br>${timeChange} <br>`;
|
||||
} catch (err) {
|
||||
console.log("Pjax无法获取元素")
|
||||
}
|
||||
}
|
||||
window.onload = showWelcome;
|
||||
// 如果使用了pjax在加上下面这行代码
|
||||
document.addEventListener('pjax:complete', showWelcome);
|
||||
350
themes/butterfly/source/js/utils.js
Normal file
350
themes/butterfly/source/js/utils.js
Normal file
@@ -0,0 +1,350 @@
|
||||
(() => {
|
||||
const btfFn = {
|
||||
debounce: (func, wait = 0, immediate = false) => {
|
||||
let timeout
|
||||
return (...args) => {
|
||||
const later = () => {
|
||||
timeout = null
|
||||
if (!immediate) func(...args)
|
||||
}
|
||||
const callNow = immediate && !timeout
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(later, wait)
|
||||
if (callNow) func(...args)
|
||||
}
|
||||
},
|
||||
|
||||
throttle: function (func, wait, options = {}) {
|
||||
let timeout, context, args
|
||||
let previous = 0
|
||||
|
||||
const later = () => {
|
||||
previous = options.leading === false ? 0 : new Date().getTime()
|
||||
timeout = null
|
||||
func.apply(context, args)
|
||||
if (!timeout) context = args = null
|
||||
}
|
||||
|
||||
const throttled = (...params) => {
|
||||
const now = new Date().getTime()
|
||||
if (!previous && options.leading === false) previous = now
|
||||
const remaining = wait - (now - previous)
|
||||
context = this
|
||||
args = params
|
||||
if (remaining <= 0 || remaining > wait) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
timeout = null
|
||||
}
|
||||
previous = now
|
||||
func.apply(context, args)
|
||||
if (!timeout) context = args = null
|
||||
} else if (!timeout && options.trailing !== false) {
|
||||
timeout = setTimeout(later, remaining)
|
||||
}
|
||||
}
|
||||
|
||||
return throttled
|
||||
},
|
||||
|
||||
overflowPaddingR: {
|
||||
add: () => {
|
||||
const paddingRight = window.innerWidth - document.body.clientWidth
|
||||
|
||||
if (paddingRight > 0) {
|
||||
document.body.style.paddingRight = `${paddingRight}px`
|
||||
document.body.style.overflow = 'hidden'
|
||||
const menuElement = document.querySelector('#page-header.nav-fixed #menus')
|
||||
if (menuElement) {
|
||||
menuElement.style.paddingRight = `${paddingRight}px`
|
||||
}
|
||||
}
|
||||
},
|
||||
remove: () => {
|
||||
document.body.style.paddingRight = ''
|
||||
document.body.style.overflow = ''
|
||||
const menuElement = document.querySelector('#page-header.nav-fixed #menus')
|
||||
if (menuElement) {
|
||||
menuElement.style.paddingRight = ''
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
snackbarShow: (text, showAction = false, duration = 2000) => {
|
||||
const { position, bgLight, bgDark } = GLOBAL_CONFIG.Snackbar
|
||||
const bg = document.documentElement.getAttribute('data-theme') === 'light' ? bgLight : bgDark
|
||||
Snackbar.show({
|
||||
text,
|
||||
backgroundColor: bg,
|
||||
showAction,
|
||||
duration,
|
||||
pos: position,
|
||||
customClass: 'snackbar-css'
|
||||
})
|
||||
},
|
||||
|
||||
diffDate: (inputDate, more = false) => {
|
||||
const dateNow = new Date()
|
||||
const datePost = new Date(inputDate)
|
||||
const diffMs = dateNow - datePost
|
||||
const diffSec = diffMs / 1000
|
||||
const diffMin = diffSec / 60
|
||||
const diffHour = diffMin / 60
|
||||
const diffDay = diffHour / 24
|
||||
const diffMonth = diffDay / 30
|
||||
const { dateSuffix } = GLOBAL_CONFIG
|
||||
|
||||
if (!more) return Math.floor(diffDay)
|
||||
|
||||
if (diffMonth > 12) return datePost.toISOString().slice(0, 10)
|
||||
if (diffMonth >= 1) return `${Math.floor(diffMonth)} ${dateSuffix.month}`
|
||||
if (diffDay >= 1) return `${Math.floor(diffDay)} ${dateSuffix.day}`
|
||||
if (diffHour >= 1) return `${Math.floor(diffHour)} ${dateSuffix.hour}`
|
||||
if (diffMin >= 1) return `${Math.floor(diffMin)} ${dateSuffix.min}`
|
||||
return dateSuffix.just
|
||||
},
|
||||
|
||||
loadComment: (dom, callback) => {
|
||||
if ('IntersectionObserver' in window) {
|
||||
const observerItem = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
callback()
|
||||
observerItem.disconnect()
|
||||
}
|
||||
}, { threshold: [0] })
|
||||
observerItem.observe(dom)
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
|
||||
scrollToDest: (pos, time = 500) => {
|
||||
const currentPos = window.scrollY
|
||||
const isNavFixed = document.getElementById('page-header').classList.contains('fixed')
|
||||
if (currentPos > pos || isNavFixed) pos = pos - 70
|
||||
|
||||
if ('scrollBehavior' in document.documentElement.style) {
|
||||
window.scrollTo({
|
||||
top: pos,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const startTime = performance.now()
|
||||
const animate = currentTime => {
|
||||
const timeElapsed = currentTime - startTime
|
||||
const progress = Math.min(timeElapsed / time, 1)
|
||||
window.scrollTo(0, currentPos + (pos - currentPos) * progress)
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(animate)
|
||||
},
|
||||
|
||||
animateIn: (ele, animation) => {
|
||||
ele.style.display = 'block'
|
||||
ele.style.animation = animation
|
||||
},
|
||||
|
||||
animateOut: (ele, animation) => {
|
||||
const handleAnimationEnd = () => {
|
||||
ele.style.display = ''
|
||||
ele.style.animation = ''
|
||||
ele.removeEventListener('animationend', handleAnimationEnd)
|
||||
}
|
||||
ele.addEventListener('animationend', handleAnimationEnd)
|
||||
ele.style.animation = animation
|
||||
},
|
||||
|
||||
wrap: (selector, eleType, options) => {
|
||||
const createEle = document.createElement(eleType)
|
||||
for (const [key, value] of Object.entries(options)) {
|
||||
createEle.setAttribute(key, value)
|
||||
}
|
||||
selector.parentNode.insertBefore(createEle, selector)
|
||||
createEle.appendChild(selector)
|
||||
},
|
||||
|
||||
isHidden: ele => ele.offsetHeight === 0 && ele.offsetWidth === 0,
|
||||
|
||||
getEleTop: ele => {
|
||||
let actualTop = ele.offsetTop
|
||||
let current = ele.offsetParent
|
||||
|
||||
while (current !== null) {
|
||||
actualTop += current.offsetTop
|
||||
current = current.offsetParent
|
||||
}
|
||||
|
||||
return actualTop
|
||||
},
|
||||
|
||||
loadLightbox: ele => {
|
||||
const service = GLOBAL_CONFIG.lightbox
|
||||
|
||||
if (service === 'medium_zoom') {
|
||||
mediumZoom(ele, { background: 'var(--zoom-bg)' })
|
||||
return
|
||||
}
|
||||
|
||||
if (service === 'fancybox') {
|
||||
Array.from(ele).forEach(i => {
|
||||
if (i.parentNode.tagName !== 'A') {
|
||||
const dataSrc = i.dataset.lazySrc || i.src
|
||||
const dataCaption = i.title || i.alt || ''
|
||||
btf.wrap(i, 'a', { href: dataSrc, 'data-fancybox': 'gallery', 'data-caption': dataCaption, 'data-thumb': dataSrc })
|
||||
}
|
||||
})
|
||||
|
||||
if (!window.fancyboxRun) {
|
||||
let options = ''
|
||||
if (Fancybox.version < '6') {
|
||||
options = {
|
||||
Hash: false,
|
||||
Thumbs: {
|
||||
showOnStart: false
|
||||
},
|
||||
Images: {
|
||||
Panzoom: {
|
||||
maxScale: 4
|
||||
}
|
||||
},
|
||||
Carousel: {
|
||||
transition: 'slide'
|
||||
},
|
||||
Toolbar: {
|
||||
display: {
|
||||
left: ['infobar'],
|
||||
middle: [
|
||||
'zoomIn',
|
||||
'zoomOut',
|
||||
'toggle1to1',
|
||||
'rotateCCW',
|
||||
'rotateCW',
|
||||
'flipX',
|
||||
'flipY'
|
||||
],
|
||||
right: ['slideshow', 'thumbs', 'close']
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
options = {
|
||||
Hash: false,
|
||||
Carousel: {
|
||||
transition: 'slide',
|
||||
Thumbs: {
|
||||
showOnStart: false
|
||||
},
|
||||
Toolbar: {
|
||||
display: {
|
||||
left: ['counter'],
|
||||
middle: [
|
||||
'zoomIn',
|
||||
'zoomOut',
|
||||
'toggle1to1',
|
||||
'rotateCCW',
|
||||
'rotateCW',
|
||||
'flipX',
|
||||
'flipY',
|
||||
"reset"
|
||||
],
|
||||
right: ['autoplay', 'thumbs', 'close']
|
||||
}
|
||||
},
|
||||
Zoomable: {
|
||||
Panzoom: {
|
||||
maxScale: 4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Fancybox.bind('[data-fancybox]', options)
|
||||
window.fancyboxRun = true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setLoading: {
|
||||
add: ele => {
|
||||
const html = `
|
||||
<div class="loading-container">
|
||||
<div class="loading-item">
|
||||
<div></div><div></div><div></div><div></div><div></div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
ele.insertAdjacentHTML('afterend', html)
|
||||
},
|
||||
remove: ele => {
|
||||
ele.nextElementSibling.remove()
|
||||
}
|
||||
},
|
||||
|
||||
updateAnchor: anchor => {
|
||||
if (anchor !== window.location.hash) {
|
||||
if (!anchor) anchor = location.pathname
|
||||
const title = GLOBAL_CONFIG_SITE.title
|
||||
window.history.replaceState({
|
||||
url: location.href,
|
||||
title
|
||||
}, title, anchor)
|
||||
}
|
||||
},
|
||||
|
||||
getScrollPercent: (() => {
|
||||
let docHeight, winHeight, headerHeight, contentMath
|
||||
|
||||
return (currentTop, ele) => {
|
||||
if (!docHeight || ele.clientHeight !== docHeight) {
|
||||
docHeight = ele.clientHeight
|
||||
winHeight = window.innerHeight
|
||||
headerHeight = ele.offsetTop
|
||||
contentMath = Math.max(docHeight - winHeight, document.documentElement.scrollHeight - winHeight)
|
||||
}
|
||||
|
||||
const scrollPercent = (currentTop - headerHeight) / contentMath
|
||||
return Math.max(0, Math.min(100, Math.round(scrollPercent * 100)))
|
||||
}
|
||||
})(),
|
||||
|
||||
addEventListenerPjax: (ele, event, fn, option = false) => {
|
||||
ele.addEventListener(event, fn, option)
|
||||
btf.addGlobalFn('pjaxSendOnce', () => {
|
||||
ele.removeEventListener(event, fn, option)
|
||||
})
|
||||
},
|
||||
|
||||
removeGlobalFnEvent: (key, parent = window) => {
|
||||
const globalFn = parent.globalFn || {}
|
||||
const keyObj = globalFn[key]
|
||||
if (!keyObj) return
|
||||
|
||||
Object.keys(keyObj).forEach(i => keyObj[i]())
|
||||
|
||||
delete globalFn[key]
|
||||
},
|
||||
|
||||
switchComments: (el = document, path) => {
|
||||
const switchBtn = el.querySelector('#switch-btn')
|
||||
if (!switchBtn) return
|
||||
|
||||
let switchDone = false
|
||||
const postComment = el.querySelector('#post-comment')
|
||||
const handleSwitchBtn = () => {
|
||||
postComment.classList.toggle('move')
|
||||
if (!switchDone && typeof loadOtherComment === 'function') {
|
||||
switchDone = true
|
||||
loadOtherComment(el, path)
|
||||
}
|
||||
}
|
||||
btf.addEventListenerPjax(switchBtn, 'click', handleSwitchBtn)
|
||||
}
|
||||
}
|
||||
|
||||
window.btf = { ...window.btf, ...btfFn }
|
||||
})()
|
||||
Reference in New Issue
Block a user