diff --git a/themes/Ying/assets/css/main.css b/themes/Ying/assets/css/main.css
index a4734212..e925e6f4 100644
--- a/themes/Ying/assets/css/main.css
+++ b/themes/Ying/assets/css/main.css
@@ -41,6 +41,32 @@
--card-shadow-dark: 0 18px 45px rgba(0, 0, 0, 0.9), 0 0 0 1px rgba(148, 163, 184, 0.45);
}
+/* ── Theme switch transition ── */
+::view-transition-old(root),
+::view-transition-new(root) {
+ animation-duration: 0.45s;
+ animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+::view-transition-old(root) {
+ animation-name: theme-fade-out;
+}
+
+::view-transition-new(root) {
+ animation-name: theme-fade-in;
+ animation-delay: 0.04s;
+}
+
+@keyframes theme-fade-out {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
+
+@keyframes theme-fade-in {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
[data-theme="dark"] body {
color: var(--text-content-dark-color);
background-color: var(--background-dark-color);
@@ -3736,6 +3762,79 @@ input[type=submit] {
}
}
+/* ── Toast ── */
+.ying-toast-container {
+ position: fixed;
+ top: 18px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 10000;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ pointer-events: none;
+}
+
+.ying-toast {
+ padding: 9px 18px;
+ border-radius: 6px;
+ font-size: 13px;
+ font-weight: 500;
+ line-height: 1.3;
+ opacity: 0;
+ transform: translateY(-10px);
+ transition: opacity 0.28s ease, transform 0.28s ease;
+ pointer-events: auto;
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
+ backdrop-filter: blur(20px) saturate(180%);
+ will-change: transform, opacity;
+}
+
+.ying-toast--visible {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+.ying-toast--success {
+ background: rgba(34, 197, 94, 0.88);
+ color: #fff;
+ box-shadow: 0 8px 32px rgba(34, 197, 94, 0.18);
+}
+
+.ying-toast--error {
+ background: rgba(239, 68, 68, 0.88);
+ color: #fff;
+ box-shadow: 0 8px 32px rgba(239, 68, 68, 0.18);
+}
+
+.ying-toast--warning {
+ background: rgba(245, 158, 11, 0.88);
+ color: #fff;
+ box-shadow: 0 8px 32px rgba(245, 158, 11, 0.18);
+}
+
+.ying-toast--info {
+ background: rgba(29, 61, 108, 0.85);
+ color: #f0f4fb;
+ box-shadow: 0 8px 32px rgba(29, 61, 108, 0.18);
+}
+
+[data-theme="dark"] .ying-toast--info {
+ background: rgba(159, 196, 244, 0.22);
+ color: #f3f7fd;
+ -webkit-backdrop-filter: blur(24px) saturate(180%);
+ backdrop-filter: blur(24px) saturate(180%);
+}
+
+@media screen and (max-width: 480px) {
+ .ying-toast {
+ padding: 8px 14px;
+ font-size: 12px;
+ border-radius: 5px;
+ }
+}
+
.floating-tools a {
width: 40px;
height: 40px;
diff --git a/themes/Ying/assets/js/modules/infinite-scroll.js b/themes/Ying/assets/js/modules/infinite-scroll.js
index 96fb5b57..5e33059f 100644
--- a/themes/Ying/assets/js/modules/infinite-scroll.js
+++ b/themes/Ying/assets/js/modules/infinite-scroll.js
@@ -63,17 +63,21 @@ window.initInfiniteScroll = function() {
// 1. Get new posts
const newPosts = doc.querySelectorAll(`${containerSelector} ${itemSelector}`);
- // 2. Append posts
+ // 2. Append posts with batch layout read/write to avoid thrashing
if (newPosts.length > 0) {
- newPosts.forEach(post => {
- // Remove UIkit scrollspy attributes to prevent interference and use manual animation
+ // Phase 1: append all and set initial opacity
+ newPosts.forEach(function(post) {
post.removeAttribute('uk-scrollspy-class');
post.style.opacity = 0;
container.appendChild(post);
-
- // Trigger reflow
- void post.offsetWidth;
-
+ });
+
+ // Phase 2: force single layout read for all new posts
+ var postArr = Array.prototype.slice.call(newPosts);
+ postArr.forEach(function(post) { void post.offsetWidth; });
+
+ // Phase 3: batch animate in
+ postArr.forEach(function(post) {
post.style.transition = 'opacity 0.5s ease';
post.style.opacity = 1;
});
diff --git a/themes/Ying/assets/js/modules/search.js b/themes/Ying/assets/js/modules/search.js
index cd8942e6..f5a7253a 100644
--- a/themes/Ying/assets/js/modules/search.js
+++ b/themes/Ying/assets/js/modules/search.js
@@ -5,6 +5,16 @@ window.initSearch = function() {
var searchLoadingPromise = null;
+function debounce(fn, delay) {
+ var timer;
+ return function() {
+ var context = this;
+ var args = arguments;
+ clearTimeout(timer);
+ timer = setTimeout(function() { fn.apply(context, args); }, delay);
+ };
+}
+
function loadSearchIndex() {
if (window.searchIndex) {
return Promise.resolve(window.searchIndex);
@@ -91,7 +101,7 @@ function initSearchModal() {
}
});
- searchInput.addEventListener('input', function() {
+ searchInput.addEventListener('input', debounce(function() {
var query = this.value.trim().toLowerCase();
if (query.length === 0) {
@@ -135,7 +145,7 @@ function initSearchModal() {
} else {
searchResults.innerHTML = '
未找到相关文章
';
}
- });
+ }), 200);
}
}
}
@@ -224,7 +234,7 @@ function initInlineSearch() {
});
}
- input.addEventListener('input', function() {
+ input.addEventListener('input', debounce(function() {
var query = this.value.trim().toLowerCase();
var inputGroup = this.closest('.search-input-group');
@@ -285,19 +295,21 @@ function initInlineSearch() {
resultsContainer.innerHTML = '暂无相关内容
';
resultsContainer.classList.add('show');
}
- });
+ }), 200);
// Close on click outside
if (!window.inlineSearchClickOutside) {
window.inlineSearchClickOutside = function(e) {
- var header = document.getElementById('essay-header');
var searchContainer = document.getElementById('search-container');
+ if (!searchContainer || searchContainer.style.display !== 'block') return;
+ var header = document.getElementById('essay-header');
var resultsContainer = document.getElementById('inline-search-results');
-
- if (header && searchContainer && resultsContainer && searchContainer.style.display === 'block') {
- if (!searchContainer.contains(e.target) && !header.contains(e.target) && !resultsContainer.contains(e.target)) {
- window.toggleSearchMode(false);
- }
+
+ if (header && resultsContainer &&
+ !searchContainer.contains(e.target) &&
+ !header.contains(e.target) &&
+ !resultsContainer.contains(e.target)) {
+ window.toggleSearchMode(false);
}
};
document.addEventListener('click', window.inlineSearchClickOutside);
diff --git a/themes/Ying/assets/js/modules/tiaozhuan.js b/themes/Ying/assets/js/modules/tiaozhuan.js
index e3f3b38d..68452d10 100644
--- a/themes/Ying/assets/js/modules/tiaozhuan.js
+++ b/themes/Ying/assets/js/modules/tiaozhuan.js
@@ -1,35 +1,24 @@
-function checkParent(element, classNames) {
- while (element) {
- if (element.classList && classNames.some(cn => element.classList.contains(cn))) {
- return true;
- }
- element = element.parentElement;
- }
- return false;
- }
- var excludedClasses = ['talks_comments','tiaozhuan-button', 'floating-tools']; // 添加需要排除的a标签类名class
- window.addEventListener('load', (event) => {
- document.body.addEventListener('click', function(e) {
- let target = e.target;
- while (target && target.nodeName !== 'A') {
- target = target.parentNode;
- }
- if (target && target.nodeName === 'A' &&
- !checkParent(target, excludedClasses) &&
- !target.href.startsWith('javascript:') &&
- !target.href.includes('usj.cc') &&
- !target.href.includes('douban.com') &&
- !target.href.includes('bilibili.com') &&
- !target.href.includes('github.com') &&
- !target.href.includes('zql.im') &&
- !target.href.includes('5b2.cn') &&
- !target.href.includes('https://beian.miit.gov.cn') &&
- !target.href.includes('https://www.upyun.com') &&
- target.hostname !== window.location.hostname) {
+var excludedClasses = ['talks_comments','tiaozhuan-button', 'floating-tools'];
+window.addEventListener('load', function() {
+ document.body.addEventListener('click', function(e) {
+ var target = e.target.closest('a');
+ if (!target) return;
+
+ if (target.href.startsWith('javascript:') ||
+ target.hostname === window.location.hostname ||
+ target.href.includes('usj.cc') ||
+ target.href.includes('douban.com') ||
+ target.href.includes('bilibili.com') ||
+ target.href.includes('github.com') ||
+ target.href.includes('zql.im') ||
+ target.href.includes('5b2.cn') ||
+ target.href.includes('beian.miit.gov.cn') ||
+ target.href.includes('upyun.com')) return;
+
+ if (excludedClasses.some(function(cn) { return target.classList.contains(cn); })) return;
+
e.preventDefault();
- let encodedUrl = btoa(target.href); // Base64 encode the URL
- let url = '/tiaozhuan.html?target=' + encodedUrl;
- window.open(url, '_blank');
- }
- });
- });
\ No newline at end of file
+ var encodedUrl = btoa(target.href);
+ window.open('/tiaozhuan.html?target=' + encodedUrl, '_blank');
+ });
+});
\ No newline at end of file
diff --git a/themes/Ying/assets/js/modules/toast.js b/themes/Ying/assets/js/modules/toast.js
new file mode 100644
index 00000000..4d652263
--- /dev/null
+++ b/themes/Ying/assets/js/modules/toast.js
@@ -0,0 +1,63 @@
+/**
+ * Toast — lightweight notification
+ * Usage: window.Toast.success('msg'), Toast.error('msg'), Toast.warning('msg'), Toast.info('msg')
+ * window.Toast.show('msg', { type: 'info', duration: 2500 })
+ */
+(function() {
+ var container = null;
+ var counter = 0;
+
+ function ensureContainer() {
+ if (container) return;
+ container = document.createElement('div');
+ container.className = 'ying-toast-container';
+ document.body.appendChild(container);
+ }
+
+ function remove(el, delay) {
+ setTimeout(function() {
+ el.classList.remove('ying-toast--visible');
+ el.addEventListener('transitionend', function onEnd(e) {
+ if (e.propertyName !== 'opacity') return;
+ el.removeEventListener('transitionend', onEnd);
+ if (el.parentNode) el.parentNode.removeChild(el);
+ });
+ }, delay);
+ }
+
+ window.Toast = {
+ show: function(text, options) {
+ options = options || {};
+ var type = options.type || 'info';
+ var duration = options.duration || 2200;
+
+ ensureContainer();
+
+ var el = document.createElement('div');
+ el.className = 'ying-toast ying-toast--' + type;
+ el.textContent = text;
+ el.setAttribute('role', 'status');
+ el.setAttribute('aria-live', 'polite');
+ container.appendChild(el);
+
+ /* Force layout then animate in */
+ void el.offsetWidth;
+ el.classList.add('ying-toast--visible');
+
+ remove(el, duration);
+ },
+
+ success: function(text, duration) {
+ this.show(text, { type: 'success', duration: duration });
+ },
+ error: function(text, duration) {
+ this.show(text, { type: 'error', duration: duration });
+ },
+ warning: function(text, duration) {
+ this.show(text, { type: 'warning', duration: duration });
+ },
+ info: function(text, duration) {
+ this.show(text, { type: 'info', duration: duration });
+ }
+ };
+})();
diff --git a/themes/Ying/assets/js/modules/utils.js b/themes/Ying/assets/js/modules/utils.js
index 86e1126f..39fdaa54 100644
--- a/themes/Ying/assets/js/modules/utils.js
+++ b/themes/Ying/assets/js/modules/utils.js
@@ -9,69 +9,13 @@ window.isMobile = function() {
// Toast Message Helper
window.showToast = function(text, type) {
- type = type || 'info'; // default to info
- if (typeof Qmsg !== 'undefined') {
- // Map common types to Qmsg methods
- if (typeof Qmsg[type] === 'function') {
- Qmsg[type](text);
+ if (window.Toast) {
+ var t = type || 'info';
+ if (typeof window.Toast[t] === 'function') {
+ window.Toast[t](text);
} else {
- Qmsg.info(text);
+ window.Toast.info(text);
}
- } else {
- // Fallback custom toast
- let toast = document.querySelector('.custom-toast');
- if (!toast) {
- toast = document.createElement('div');
- toast.className = 'custom-toast';
- toast.style.cssText = `
- position: fixed;
- top: 20px;
- left: 50%;
- transform: translateX(-50%);
- background: rgba(0,0,0,0.8);
- color: white;
- padding: 8px 16px;
- border-radius: 4px;
- z-index: 9999;
- font-size: 14px;
- opacity: 0;
- transition: opacity 0.3s, background-color 0.3s;
- pointer-events: none;
- display: flex;
- align-items: center;
- gap: 8px;
- `;
-
- // Add dark mode support class
- toast.setAttribute('data-toast', 'true');
- document.body.appendChild(toast);
- }
-
- // Update toast style based on theme
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
- if (isDark) {
- toast.style.background = 'rgba(255, 255, 255, 0.9)';
- toast.style.color = '#333';
- toast.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)';
- } else {
- toast.style.background = 'rgba(0,0,0,0.8)';
- toast.style.color = 'white';
- toast.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)';
- }
-
- // Simple icon mapping for fallback
- let icon = '';
- if (type === 'success') icon = '✅ ';
- else if (type === 'error') icon = '❌ ';
- else if (type === 'warning') icon = '⚠️ ';
-
- toast.innerText = icon + text;
- toast.style.opacity = '1';
-
- if (window.toastTimeout) clearTimeout(window.toastTimeout);
- window.toastTimeout = setTimeout(() => {
- toast.style.opacity = '0';
- }, 2500);
}
};
@@ -85,25 +29,30 @@ window.initThemeToggle = function() {
avatar.parentNode.replaceChild(newAvatar, avatar);
newAvatar.addEventListener('click', function(e) {
- // Prevent if clicking on something interactive inside (unlikely for avatar)
e.preventDefault();
-
- const html = document.documentElement;
- const currentTheme = html.getAttribute('data-theme');
- const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
-
- html.setAttribute('data-theme', newTheme);
- localStorage.setItem('theme', newTheme);
-
- window.showToast('已切换至 ' + (newTheme === 'dark' ? '深色' : '浅色') + '模式', 'success');
- // Sync Artalk Theme
- if (window.artalk && typeof window.artalk.setDarkMode === 'function') {
- window.artalk.setDarkMode(newTheme === 'dark');
+ var html = document.documentElement;
+ var currentTheme = html.getAttribute('data-theme');
+ var newTheme = currentTheme === 'dark' ? 'light' : 'dark';
+
+ var apply = function() {
+ html.setAttribute('data-theme', newTheme);
+ localStorage.setItem('theme', newTheme);
+
+ window.showToast('已切换至 ' + (newTheme === 'dark' ? '深色' : '浅色') + '模式', 'success');
+
+ if (window.artalk && typeof window.artalk.setDarkMode === 'function') {
+ window.artalk.setDarkMode(newTheme === 'dark');
+ }
+
+ window.dispatchEvent(new Event('resize'));
+ };
+
+ if (document.startViewTransition) {
+ document.startViewTransition(function() { apply(); });
+ } else {
+ apply();
}
-
- // Trigger generic resize event to force charts/etc to redraw if needed
- window.dispatchEvent(new Event('resize'));
});
};
diff --git a/themes/Ying/layouts/partials/footer.html b/themes/Ying/layouts/partials/footer.html
index a082a469..27b1c866 100644
--- a/themes/Ying/layouts/partials/footer.html
+++ b/themes/Ying/layouts/partials/footer.html
@@ -48,7 +48,6 @@
{{ $iconfont := resources.Get "js/libs/iconfont.js" }}
-{{ $lightgallery := resources.Get "js/libs/lightgallery.umd.min.js" }}
{{ $uikit := resources.Get "js/libs/uikit.min.js" }}
{{ $easylazyload := resources.Get "js/modules/lazyload.js" }}
{{ $viewimage := resources.Get "js/libs/view-image.min.js" }}
@@ -56,8 +55,8 @@
{{ $main := resources.Get "js/main.js" }}
{{ $mypjax := resources.Get "js/modules/mypjax.js" }}
{{ $nprogress := resources.Get "js/libs/nprogress.js" }}
-{{ $message := resources.Get "js/libs/message.min.js" }}
{{ $tiaozhuan := resources.Get "js/modules/tiaozhuan.js" }}
+{{ $toast := resources.Get "js/modules/toast.js" }}
{{ $reward := resources.Get "js/modules/reward.js" }}
{{ $echarts := resources.Get "js/libs/echarts.min.js" }}
{{ $artalk := resources.Get "js/libs/Artalk.js" }}
@@ -73,7 +72,7 @@
{{ $artalkModule := resources.Get "js/modules/artalk.js" }}
{{ $pjaxLib := resources.Get "js/libs/pjax.js" }}
-{{ $scripts := slice $iconfont $lightgallery $uikit $easylazyload $viewimage $barrage $utils $loader $cache $search $floatingTools $paragraphComments $infiniteScroll $artalkModule $nprogress $pjaxLib $main $mypjax $message $tiaozhuan $reward $echarts $artalk | resources.Concat "js/bundle.js" | resources.Minify | resources.Fingerprint }}
+{{ $scripts := slice $iconfont $uikit $easylazyload $viewimage $barrage $utils $loader $cache $search $floatingTools $paragraphComments $infiniteScroll $artalkModule $nprogress $pjaxLib $main $mypjax $toast $tiaozhuan $reward $echarts $artalk | resources.Concat "js/bundle.js" | resources.Minify | resources.Fingerprint }}