1725 功能大改
This commit is contained in:
1 parent
21de080b39
commit
af41bcd2bb
10 files changed
+1743
-10
No files matched your search
@@ -0,0 +1,345 @@
|
||||
/* ============================================================
|
||||
Shortcodes 样式 - 实况照片、语音、音乐卡片
|
||||
移植自 Hugo-Theme-Amigo
|
||||
============================================================ */
|
||||
|
||||
/* ---- Live Photo ---- */
|
||||
.live-photo {
|
||||
--border-radius: 4px;
|
||||
--live-photo-aspect: 4 / 5;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: var(--border-radius);
|
||||
width: 100%;
|
||||
max-width: min(360px, 100%);
|
||||
margin: 12px auto;
|
||||
aspect-ratio: var(--live-photo-aspect);
|
||||
background: #f5f5f5;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.live-photo .live-photo-video {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.live-photo .live-photo-poster {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
opacity: 1;
|
||||
transition: opacity 0.25s ease;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.live-photo.is-playing .live-photo-video {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.live-photo.is-playing .live-photo-poster {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.live-photo .live-photo-controls-group {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.live-photo .live-photo-control-btn {
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--border-radius);
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background-color: transparent;
|
||||
color: #606060;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease;
|
||||
}
|
||||
|
||||
.live-photo .live-photo-control-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.live-photo .live-photo-toggle-btn {
|
||||
background-color: rgba(44, 44, 46, 0.62);
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
color: rgb(205 205 205 / 92%);
|
||||
}
|
||||
|
||||
.live-photo .live-photo-toggle-btn:hover {
|
||||
background-color: rgba(44, 44, 46, 0.72);
|
||||
}
|
||||
|
||||
.live-photo .live-photo-mute-btn {
|
||||
background-color: rgba(44, 44, 46, 0.35);
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
color: rgb(205 205 205 / 92%);
|
||||
}
|
||||
|
||||
.live-photo .live-photo-mute-btn:hover {
|
||||
background-color: rgba(44, 44, 46, 0.42);
|
||||
}
|
||||
|
||||
.live-photo-mute-btn .live-photo-mute-icon {
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
font-size: 15px;
|
||||
line-height: 19px;
|
||||
color: rgb(205 205 205 / 92%);
|
||||
}
|
||||
|
||||
.live-photo-mute-btn[data-muted="true"] .icon-unmuted {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.live-photo-mute-btn[data-muted="false"] .icon-muted {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ---- Voice Bubble ---- */
|
||||
.amigo-voice-bubble {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, #95ec69 0%, #7dd652 100%);
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
max-width: 200px;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.amigo-voice-bubble:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.amigo-voice-bubble:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.amigo-voice-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.amigo-voice-icon span {
|
||||
display: block;
|
||||
width: 3px;
|
||||
background: #2d2d2d;
|
||||
border-radius: 2px;
|
||||
animation: voiceWave 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.amigo-voice-icon span:nth-child(1) {
|
||||
height: 8px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.amigo-voice-icon span:nth-child(2) {
|
||||
height: 14px;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.amigo-voice-icon span:nth-child(3) {
|
||||
height: 10px;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.amigo-voice-bubble.playing .amigo-voice-icon span {
|
||||
animation: voiceWaveActive 0.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.amigo-voice-duration {
|
||||
font-size: 14px;
|
||||
color: #2d2d2d;
|
||||
font-weight: 500;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.amigo-voice-unread {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #ff4d4f;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
}
|
||||
|
||||
@keyframes voiceWave {
|
||||
0%, 100% { transform: scaleY(0.5); }
|
||||
50% { transform: scaleY(1); }
|
||||
}
|
||||
|
||||
@keyframes voiceWaveActive {
|
||||
0%, 100% { transform: scaleY(0.3); }
|
||||
50% { transform: scaleY(1); }
|
||||
}
|
||||
|
||||
/* ---- Music Card ---- */
|
||||
.amigo-music-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
max-width: 400px;
|
||||
margin: 16px 0;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.amigo-music-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.6);
|
||||
}
|
||||
|
||||
.amigo-music-card__art {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.amigo-music-card__cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.amigo-music-card__play {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.amigo-music-card__art:hover .amigo-music-card__play {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.amigo-music-card__play-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M8 5v14l11-7z'/%3E%3C/svg%3E") no-repeat center;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.amigo-music-card__meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.amigo-music-card__title {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.amigo-music-card__artist {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.amigo-music-card__timeline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.amigo-music-card__time {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
min-width: 36px;
|
||||
}
|
||||
|
||||
.amigo-music-card__seek-wrap {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.amigo-music-card__seek {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.amigo-music-card__seek::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Dark theme support */
|
||||
[data-theme="dark"] .live-photo {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .amigo-voice-bubble {
|
||||
background: linear-gradient(135deg, #3d3d3d 0%, #2d2d2d 100%);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .amigo-voice-icon span,
|
||||
[data-theme="dark"] .amigo-voice-duration {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .amigo-music-card {
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* Shortcodes JavaScript - 实况照片、语音、音乐卡片
|
||||
* 移植自 Hugo-Theme-Amigo
|
||||
*/
|
||||
|
||||
// ============================================================
|
||||
// Live Photo
|
||||
// ============================================================
|
||||
|
||||
function initLivePhotoShortcodes() {
|
||||
document.querySelectorAll('.live-photo').forEach(function(livePhoto) {
|
||||
if (livePhoto.__liveBound) return;
|
||||
livePhoto.__liveBound = true;
|
||||
|
||||
var video = livePhoto.querySelector('video.live-photo-video') || livePhoto.querySelector('video');
|
||||
var posterImg = livePhoto.querySelector('img.live-photo-poster') || livePhoto.querySelector('img');
|
||||
var toggleBtn = livePhoto.querySelector('.live-photo-toggle-btn');
|
||||
var muteBtn = livePhoto.querySelector('.live-photo-mute-btn');
|
||||
|
||||
if (!video || !toggleBtn || !muteBtn) return;
|
||||
|
||||
var HOVER_DELAY = 500;
|
||||
var hoverTimer = null;
|
||||
var isLoaded = false;
|
||||
|
||||
function syncAspectFromPoster() {
|
||||
if (!posterImg) return;
|
||||
var w = posterImg.naturalWidth || 0;
|
||||
var h = posterImg.naturalHeight || 0;
|
||||
if (!w || !h) return;
|
||||
livePhoto.style.setProperty('--live-photo-aspect', w + ' / ' + h);
|
||||
}
|
||||
|
||||
if (posterImg && posterImg.complete) {
|
||||
syncAspectFromPoster();
|
||||
} else if (posterImg) {
|
||||
posterImg.addEventListener('load', syncAspectFromPoster, { once: true });
|
||||
}
|
||||
|
||||
function ensureLoaded() {
|
||||
if (isLoaded) return;
|
||||
isLoaded = true;
|
||||
var src = video.dataset.src || '';
|
||||
if (src && !video.getAttribute('src')) {
|
||||
video.setAttribute('src', src);
|
||||
video.src = src;
|
||||
}
|
||||
try { video.load(); } catch (e) {}
|
||||
}
|
||||
|
||||
function setMuted(isMuted) {
|
||||
video.muted = !!isMuted;
|
||||
if (isMuted) video.setAttribute('muted', '');
|
||||
else video.removeAttribute('muted');
|
||||
muteBtn.setAttribute('data-muted', isMuted ? 'true' : 'false');
|
||||
}
|
||||
|
||||
setMuted(true);
|
||||
|
||||
function playVideo() {
|
||||
ensureLoaded();
|
||||
livePhoto.classList.add('is-playing');
|
||||
toggleBtn.setAttribute('data-state', 'live');
|
||||
try { video.play(); } catch (e) {}
|
||||
}
|
||||
|
||||
function stopVideo() {
|
||||
livePhoto.classList.remove('is-playing');
|
||||
toggleBtn.setAttribute('data-state', 'static');
|
||||
try { video.pause(); video.currentTime = 0; } catch (e) {}
|
||||
}
|
||||
|
||||
// Toggle button click
|
||||
toggleBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
if (livePhoto.classList.contains('is-playing')) {
|
||||
stopVideo();
|
||||
} else {
|
||||
playVideo();
|
||||
}
|
||||
});
|
||||
|
||||
// Mute button click
|
||||
muteBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
setMuted(!video.muted);
|
||||
});
|
||||
|
||||
// Hover to play
|
||||
livePhoto.addEventListener('mouseenter', function() {
|
||||
hoverTimer = setTimeout(playVideo, HOVER_DELAY);
|
||||
});
|
||||
|
||||
livePhoto.addEventListener('mouseleave', function() {
|
||||
clearTimeout(hoverTimer);
|
||||
if (toggleBtn.getAttribute('data-state') !== 'live') {
|
||||
stopVideo();
|
||||
}
|
||||
});
|
||||
|
||||
// Mobile touch
|
||||
livePhoto.addEventListener('touchstart', function() {
|
||||
if (livePhoto.classList.contains('is-playing')) {
|
||||
stopVideo();
|
||||
} else {
|
||||
playVideo();
|
||||
}
|
||||
});
|
||||
|
||||
// Video ended
|
||||
video.addEventListener('ended', function() {
|
||||
if (toggleBtn.getAttribute('data-state') !== 'live') {
|
||||
stopVideo();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Voice Bubble
|
||||
// ============================================================
|
||||
|
||||
function initVoiceBubbles() {
|
||||
document.querySelectorAll('.amigo-voice-bubble').forEach(function(bubble) {
|
||||
if (bubble.__voiceBound) return;
|
||||
bubble.__voiceBound = true;
|
||||
|
||||
var audioSrc = bubble.dataset.src;
|
||||
var audio = null;
|
||||
var isPlaying = false;
|
||||
var durationSpan = bubble.querySelector('.amigo-voice-duration');
|
||||
|
||||
// Load audio duration
|
||||
if (audioSrc) {
|
||||
audio = new Audio(audioSrc);
|
||||
audio.addEventListener('loadedmetadata', function() {
|
||||
var duration = Math.round(audio.duration);
|
||||
if (durationSpan) {
|
||||
durationSpan.textContent = duration + '"';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bubble.addEventListener('click', function() {
|
||||
if (!audio) return;
|
||||
|
||||
if (isPlaying) {
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
bubble.classList.remove('playing');
|
||||
isPlaying = false;
|
||||
} else {
|
||||
// Stop other playing voices
|
||||
document.querySelectorAll('.amigo-voice-bubble.playing').forEach(function(other) {
|
||||
if (other !== bubble) {
|
||||
other.classList.remove('playing');
|
||||
}
|
||||
});
|
||||
|
||||
audio.play();
|
||||
bubble.classList.add('playing');
|
||||
isPlaying = true;
|
||||
}
|
||||
});
|
||||
|
||||
audio.addEventListener('ended', function() {
|
||||
bubble.classList.remove('playing');
|
||||
isPlaying = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Music Card
|
||||
// ============================================================
|
||||
|
||||
function initMusicCards() {
|
||||
document.querySelectorAll('.amigo-music-card').forEach(function(card) {
|
||||
if (card.__musicBound) return;
|
||||
card.__musicBound = true;
|
||||
|
||||
var audioSrc = card.dataset.src;
|
||||
var playBtn = card.querySelector('.amigo-music-card__play');
|
||||
var seekInput = card.querySelector('.amigo-music-card__seek');
|
||||
var currentTime = card.querySelector('.amigo-music-card__time--current');
|
||||
var durationTime = card.querySelector('.amigo-music-card__time--duration');
|
||||
var audio = null;
|
||||
var isPlaying = false;
|
||||
|
||||
if (!audioSrc) return;
|
||||
|
||||
audio = new Audio(audioSrc);
|
||||
|
||||
// Load metadata
|
||||
audio.addEventListener('loadedmetadata', function() {
|
||||
var duration = formatTime(audio.duration);
|
||||
if (durationTime) durationTime.textContent = duration;
|
||||
});
|
||||
|
||||
// Update progress
|
||||
audio.addEventListener('timeupdate', function() {
|
||||
if (currentTime) currentTime.textContent = formatTime(audio.currentTime);
|
||||
if (seekInput && audio.duration) {
|
||||
seekInput.value = (audio.currentTime / audio.duration * 1000).toString();
|
||||
}
|
||||
});
|
||||
|
||||
// Play/Pause
|
||||
function togglePlay() {
|
||||
if (isPlaying) {
|
||||
audio.pause();
|
||||
card.classList.remove('playing');
|
||||
isPlaying = false;
|
||||
} else {
|
||||
// Stop other playing music
|
||||
document.querySelectorAll('.amigo-music-card.playing').forEach(function(other) {
|
||||
if (other !== card) {
|
||||
other.classList.remove('playing');
|
||||
}
|
||||
});
|
||||
|
||||
audio.play();
|
||||
card.classList.add('playing');
|
||||
isPlaying = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (playBtn) {
|
||||
playBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
togglePlay();
|
||||
});
|
||||
}
|
||||
|
||||
card.addEventListener('click', togglePlay);
|
||||
|
||||
// Seek
|
||||
if (seekInput) {
|
||||
seekInput.addEventListener('input', function() {
|
||||
if (audio.duration) {
|
||||
audio.currentTime = (parseFloat(seekInput.value) / 1000) * audio.duration;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Ended
|
||||
audio.addEventListener('ended', function() {
|
||||
card.classList.remove('playing');
|
||||
isPlaying = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function formatTime(seconds) {
|
||||
var m = Math.floor(seconds / 60);
|
||||
var s = Math.floor(seconds % 60);
|
||||
return m + ':' + (s < 10 ? '0' : '') + s;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Initialize all shortcodes
|
||||
// ============================================================
|
||||
|
||||
function initAllShortcodes() {
|
||||
initLivePhotoShortcodes();
|
||||
initVoiceBubbles();
|
||||
initMusicCards();
|
||||
}
|
||||
|
||||
// DOMContentLoaded
|
||||
document.addEventListener('DOMContentLoaded', initAllShortcodes);
|
||||
|
||||
// Page load
|
||||
window.addEventListener('load', initAllShortcodes);
|
||||
|
||||
// PJAX support - re-initialize after page transition
|
||||
document.addEventListener('pjax:complete', initAllShortcodes);
|
||||
document.addEventListener('pjax:success', initAllShortcodes);
|
||||
|
||||
// Also listen for custom events that some PJAX libraries fire
|
||||
document.addEventListener('page:loaded', initAllShortcodes);
|
||||
document.addEventListener('page:change', initAllShortcodes);
|
||||
@@ -170,8 +170,9 @@
|
||||
{{/* ====== 5. 延迟加载的非关键JS ====== */}}
|
||||
{{ $toast := resources.Get "js/modules/toast.js" }}
|
||||
{{ $easylazyload := resources.Get "js/modules/lazyload.js" }}
|
||||
{{ $shortcodesJs := resources.Get "js/shortcodes.js" }}
|
||||
|
||||
{{ $deferredScripts := slice $toast $easylazyload | resources.Concat "js/deferred.js" | resources.Minify | resources.Fingerprint }}
|
||||
{{ $deferredScripts := slice $toast $easylazyload $shortcodesJs | resources.Concat "js/deferred.js" | resources.Minify | resources.Fingerprint }}
|
||||
|
||||
<script>
|
||||
// 使用requestIdleCallback在浏览器空闲时加载
|
||||
|
||||
@@ -131,8 +131,9 @@
|
||||
{{ $nprogress := resources.Get "css/libs/nprogress.min.css" }}
|
||||
{{ $message := resources.Get "css/libs/message.min.css" }}
|
||||
{{ $main := resources.Get "css/main.css" }}
|
||||
{{ $shortcodes := resources.Get "css/shortcodes.css" }}
|
||||
|
||||
{{ $style := slice $normalize $remixicon $uikit $nprogress $message $main | resources.Concat "css/style.css" | resources.Minify | resources.Fingerprint }}
|
||||
{{ $style := slice $normalize $remixicon $uikit $nprogress $message $main $shortcodes | resources.Concat "css/style.css" | resources.Minify | resources.Fingerprint }}
|
||||
<link rel="stylesheet" href="{{ $style.RelPermalink }}">
|
||||
|
||||
{{- if and .Site.Params.style.enableBackgroundImage .Site.Params.style.backgroundImageUrl -}}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{{- $imageRaw := .Get "image" | default (.Get "src") -}}
|
||||
{{- $alt := .Get "alt" | default "Live Photo" -}}
|
||||
{{- $videoRaw := .Get "video" | default (replaceRE "\\.(avif|webp|jpg|jpeg|png|gif)$" ".mp4" $imageRaw) -}}
|
||||
{{- $image := $imageRaw -}}
|
||||
{{- $video := $videoRaw -}}
|
||||
|
||||
{{- if and $imageRaw (not (findRE "^(https?://|/)" $imageRaw)) -}}
|
||||
{{- with .Page.Resources.GetMatch $imageRaw -}}
|
||||
{{- $image = .RelPermalink -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- if and $videoRaw (not (findRE "^(https?://|/)" $videoRaw)) -}}
|
||||
{{- with .Page.Resources.GetMatch $videoRaw -}}
|
||||
{{- $video = .RelPermalink -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $style := .Get "style" -}}
|
||||
|
||||
<div class="live-photo"{{ with $style }}{{ printf " style=%q" (. | safeCSS) | safeHTMLAttr }}{{ end }}>
|
||||
<img class="live-photo-poster" src="{{ $image }}" alt="{{ $alt }}" loading="lazy" data-keep="1" no-view>
|
||||
<video class="live-photo-video" data-src="{{ $video }}" poster="{{ $image }}" muted playsinline loop preload="metadata" aria-label="{{ $alt }}"></video>
|
||||
<div class="live-photo-controls-group">
|
||||
<button type="button" class="live-photo-control-btn live-photo-toggle-btn" data-state="static" aria-label="Toggle Live Photo">
|
||||
<i class="icon-live" aria-hidden="true"></i>
|
||||
<span class="live-photo-live-label">实况</span>
|
||||
</button>
|
||||
<button type="button" class="live-photo-control-btn live-photo-mute-btn" data-muted="true" aria-label="Toggle Mute">
|
||||
<i class="ri-volume-up-line live-photo-mute-icon icon-unmuted" aria-hidden="true"></i>
|
||||
<i class="ri-volume-mute-line live-photo-mute-icon icon-muted" aria-hidden="true"></i>
|
||||
<span class="live-photo-mute-label">静音</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="warning" style="opacity: 0;"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
{{ $src := .Get "src" | default "" }}
|
||||
{{ $cover := .Get "cover" | default "/images/default-cover.jpg" }}
|
||||
{{ $name := .Get "name" | default "未知歌曲" }}
|
||||
{{ $artist := .Get "artist" | default "未知艺术家" }}
|
||||
{{ $accent := .Get "accent" | default "#d43c33" }}
|
||||
{{ $isApi := strings.HasPrefix $src "https://open.motues.top/music" }}
|
||||
|
||||
<div class="amigo-music-card-mark amigo-music-card" data-amigo-music-card="1" data-src="{{ $src }}" data-name="{{ $name }}" data-artist="{{ $artist }}" data-is-api="{{ if $isApi }}true{{ else }}false{{ end }}" style="--music-accent:{{ $accent }}">
|
||||
<div class="amigo-music-card__hero">
|
||||
<div class="amigo-music-card__art">
|
||||
<img class="amigo-music-card__cover amigo-music-card__cover--image" src="{{ $cover }}" alt="{{ $name }} cover" loading="lazy" data-keep="1">
|
||||
<button type="button" class="amigo-music-card__play" aria-label="播放《{{ $name }}》" aria-pressed="false">
|
||||
<span class="amigo-music-card__play-icon" aria-hidden="true"></span>
|
||||
</button>
|
||||
<span class="amigo-music-card__equalizer" aria-hidden="true"><span></span><span></span><span></span></span>
|
||||
</div>
|
||||
<div class="amigo-music-card__meta">
|
||||
<strong class="amigo-music-card__title">{{ $name }}</strong>
|
||||
<span class="amigo-music-card__subtitle">
|
||||
<span class="amigo-music-card__artist">{{ $artist }}</span>
|
||||
</span>
|
||||
<div class="amigo-music-card__timeline">
|
||||
<span class="amigo-music-card__time amigo-music-card__time--current">0:00</span>
|
||||
<label class="amigo-music-card__seek-wrap">
|
||||
<span class="sr-only">调整《{{ $name }}》的播放进度</span>
|
||||
<input class="amigo-music-card__seek" type="range" min="0" max="1000" step="1" value="0" aria-label="调整《{{ $name }}》的播放进度">
|
||||
<span class="amigo-music-card__seek-thumb" aria-hidden="true"></span>
|
||||
</label>
|
||||
<span class="amigo-music-card__time amigo-music-card__time--duration">--:--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
{{- $src := .Get "src" | default "" -}}
|
||||
|
||||
{{- if and $src (not (findRE "^(https?://|/)" $src)) -}}
|
||||
{{- with .Page.Resources.GetMatch $src -}}
|
||||
{{- $src = .RelPermalink -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- $isToday := eq (.Page.Date.Format "2006-01-02") (now.Format "2006-01-02") -}}
|
||||
|
||||
<div class="amigo-voice-bubble" data-src="{{ $src }}">
|
||||
<div class="amigo-voice-icon">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<span class="amigo-voice-duration">″</span>
|
||||
{{ if $isToday }}<i class="amigo-voice-unread"></i>{{ end }}
|
||||
</div>
|
||||
@@ -0,0 +1,238 @@
|
||||
# Hugo 短代码功能 & Telegram Bot 适配
|
||||
|
||||
## ✅ 已实现功能
|
||||
|
||||
### 1️⃣ Hugo 短代码
|
||||
|
||||
#### 实况照片 (Live Photo)
|
||||
```markdown
|
||||
{{< livephoto image="photo.jpg" video="photo.mp4" >}}
|
||||
```
|
||||
- 悬停播放视频
|
||||
- 点击切换播放/暂停
|
||||
- 静音控制
|
||||
- 自动检测图片比例
|
||||
|
||||
#### 语音气泡
|
||||
```markdown
|
||||
{{< voice src="voice.ogg" >}}
|
||||
```
|
||||
- 微信风格设计
|
||||
- 点击播放语音
|
||||
- 自动显示时长
|
||||
- 未读标记(当天发布)
|
||||
|
||||
#### 音乐卡片
|
||||
```markdown
|
||||
{{< music-card src="song.mp3" name="歌曲名" artist="歌手" cover="cover.jpg" >}}
|
||||
```
|
||||
- 精美卡片设计
|
||||
- 播放/暂停控制
|
||||
- 进度条拖拽
|
||||
- 时间显示
|
||||
|
||||
---
|
||||
|
||||
## 📱 Telegram Bot 使用指南
|
||||
|
||||
### 发送语音消息
|
||||
|
||||
**操作**:
|
||||
1. 在写作模式下(`/new` 或 `/edit`)
|
||||
2. 发送语音消息
|
||||
|
||||
**Bot 响应**:
|
||||
```
|
||||
🎙️ 正在处理语音...
|
||||
✅ 语音已保存:1719234567-voice.ogg
|
||||
📝 转文字:今天天气真好...
|
||||
```
|
||||
|
||||
**生成内容**:
|
||||
```markdown
|
||||
{{< voice src="1719234567-voice.ogg" >}}
|
||||
|
||||
> 今天天气真好...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 发送音频文件(MP3)
|
||||
|
||||
**操作**:
|
||||
1. 在写作模式下
|
||||
2. 发送 MP3 文件
|
||||
|
||||
**Bot 响应**:
|
||||
```
|
||||
🎵 正在处理音频文件...
|
||||
✅ 音频已保存:1719234567-music.mp3
|
||||
|
||||
请输入歌曲信息(格式:歌名 - 歌手):
|
||||
```
|
||||
|
||||
**用户回复**:
|
||||
```
|
||||
晴天 - 周杰伦
|
||||
```
|
||||
|
||||
**Bot 响应**:
|
||||
```
|
||||
🎵 歌曲:晴天
|
||||
🎤 歌手:周杰伦
|
||||
|
||||
是否添加封面图?
|
||||
[📷 添加封面图] [⏭️ 跳过]
|
||||
```
|
||||
|
||||
**选项 A - 添加封面图**:
|
||||
1. 点击 "添加封面图"
|
||||
2. 发送图片
|
||||
3. Bot:✅ 封面图已保存:cover.jpg
|
||||
4. 生成带封面的音乐卡片
|
||||
|
||||
**选项 B - 跳过**:
|
||||
1. 点击 "跳过"
|
||||
2. 直接生成不带封面的音乐卡片
|
||||
|
||||
**生成内容**:
|
||||
```markdown
|
||||
{{< music-card src="1719234567-music.mp3" name="晴天" artist="周杰伦" cover="cover.jpg" >}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 发送图片
|
||||
|
||||
**操作**:
|
||||
1. 在写作模式下
|
||||
2. 发送图片
|
||||
|
||||
**Bot 响应**:
|
||||
```
|
||||
📷 图片已保存:1719234567-photo.jpg
|
||||
|
||||
💡 提示:如果您发送的是 iOS 实况照片(Live Photo),
|
||||
动态效果无法自动提取。如需保留动态效果,请:
|
||||
|
||||
1. 将实况照片转为视频/GIF 后重新发送
|
||||
2. 或者单独发送视频部分
|
||||
|
||||
继续发送其他内容...
|
||||
```
|
||||
|
||||
**生成内容**:
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 功能特性
|
||||
|
||||
### PJAX 支持
|
||||
- 所有短代码支持 PJAX 无刷新页面切换
|
||||
- 自动重新初始化交互逻辑
|
||||
- 保持播放状态
|
||||
|
||||
### 暗黑模式
|
||||
- 自动适配暗黑主题
|
||||
- 深色背景优化
|
||||
- 高对比度设计
|
||||
|
||||
### 响应式设计
|
||||
- 移动端友好
|
||||
- 触摸操作支持
|
||||
- 自适应布局
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 技术实现
|
||||
|
||||
### 文件结构
|
||||
|
||||
**Hugo 主题 (ying)**:
|
||||
```
|
||||
themes/ying/
|
||||
├── layouts/shortcodes/
|
||||
│ ├── livephoto.html
|
||||
│ ├── voice.html
|
||||
│ └── music-card.html
|
||||
├── assets/css/
|
||||
│ └── shortcodes.css
|
||||
└── assets/js/
|
||||
└── shortcodes.js
|
||||
```
|
||||
|
||||
**Telegram Bot (write-server)**:
|
||||
```
|
||||
write-server/src/lib/bot/
|
||||
├── poll.ts
|
||||
│ ├── handleVoiceEnhanced() # 语音增强
|
||||
│ ├── handleAudioEnhanced() # 音频处理
|
||||
│ ├── handlePhotoEnhanced() # 图片增强
|
||||
│ ├── handleAudioInfoEnhanced() # 音频信息
|
||||
│ └── handleAudioCoverCallback() # 封面图回调
|
||||
└── handlers.ts
|
||||
```
|
||||
|
||||
### API 集成
|
||||
- Artalk 评论系统
|
||||
- Telegram Bot API
|
||||
- Hugo 静态生成
|
||||
|
||||
---
|
||||
|
||||
## 📝 部署检查清单
|
||||
|
||||
- [ ] 提交 ying 主题修改
|
||||
- [ ] 提交 write-server 修改
|
||||
- [ ] 推送到远程仓库
|
||||
- [ ] 重新构建 Hugo 站点
|
||||
- [ ] 部署 write-server
|
||||
- [ ] 测试语音消息
|
||||
- [ ] 测试音频文件
|
||||
- [ ] 测试图片上传
|
||||
- [ ] 验证短代码渲染
|
||||
- [ ] 测试 PJAX 页面切换
|
||||
- [ ] 测试暗黑模式
|
||||
|
||||
---
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 语音不播放
|
||||
- 检查音频文件路径
|
||||
- 确认文件格式支持(OGG, MP3)
|
||||
- 查看浏览器控制台错误
|
||||
|
||||
### 音乐卡片无响应
|
||||
- 检查 JS 是否正确加载
|
||||
- 确认 PJAX 初始化
|
||||
- 刷新页面重试
|
||||
|
||||
### 封面图不显示
|
||||
- 检查图片路径
|
||||
- 确认文件已保存
|
||||
- 查看 Hugo 构建日志
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关链接
|
||||
|
||||
- [Hugo 短代码文档](https://gohugo.io/content-management/shortcodes/)
|
||||
- [Telegram Bot API](https://core.telegram.org/bots/api)
|
||||
- [Amigo 主题](https://github.com/zqlit/Hugo-Theme-Amigo)
|
||||
|
||||
---
|
||||
|
||||
## 📊 版本历史
|
||||
|
||||
**v1.0.0** (2026-06-24)
|
||||
- ✅ 移植 Amigo 主题短代码
|
||||
- ✅ Telegram Bot 语音增强
|
||||
- ✅ Telegram Bot 音频文件支持
|
||||
- ✅ 封面图上传功能
|
||||
- ✅ 实况照片引导提示
|
||||
- ✅ PJAX 支持
|
||||
- ✅ 暗黑模式适配
|
||||
@@ -79,18 +79,21 @@ export async function startPolling(): Promise<void> {
|
||||
const chatId = msg.chat.id;
|
||||
if (!allowed(chatId)) continue;
|
||||
|
||||
// 处理语音消息(转文字写入文章)
|
||||
if ((msg.voice || msg.audio) && hasSession(chatId)) {
|
||||
const voiceFile = msg.voice || msg.audio;
|
||||
if (voiceFile) {
|
||||
await handleVoice(chatId, voiceFile.file_id);
|
||||
}
|
||||
// 处理语音消息(增强版:保存文件 + 转文字 + 生成短代码)
|
||||
if (msg.voice && hasSession(chatId)) {
|
||||
await handleVoiceEnhanced(chatId, msg.voice.file_id, msg.voice.duration);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理图片
|
||||
// 处理音频文件(增强版:支持封面图)
|
||||
if (msg.audio && hasSession(chatId)) {
|
||||
await handleAudioEnhanced(chatId, msg.audio.file_id, msg.audio.file_name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理图片(增强版:支持封面图上传 + 实况照片引导)
|
||||
if (msg.photo && hasSession(chatId)) {
|
||||
await handlePhoto(chatId, msg.photo, msg.caption);
|
||||
await handlePhotoEnhanced(chatId, msg.photo, msg.caption);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -110,6 +113,13 @@ export async function startPolling(): Promise<void> {
|
||||
// ForceReply routing
|
||||
if (msg.reply_to_message?.text) {
|
||||
const original = msg.reply_to_message.text;
|
||||
|
||||
// 处理音频信息回复(音乐卡片)
|
||||
if (original.includes("请输入歌曲信息")) {
|
||||
const handled = await handleAudioInfo(chatId, fullText);
|
||||
if (handled) continue;
|
||||
}
|
||||
|
||||
if (original.includes("请输入文章标题")) { await handleCommand(chatId, "new", fullText); continue; }
|
||||
if (original.includes("请输入新标题")) { await handleCommand(chatId, "title", fullText); continue; }
|
||||
if (original.includes("请输入分类")) { await handleCommand(chatId, "categories", fullText); continue; }
|
||||
@@ -351,6 +361,13 @@ async function handleCommand(chatId: number, cmd: string, arg?: string) {
|
||||
}
|
||||
|
||||
async function handleInlineCallback(chatId: number, queryId: string, data: string) {
|
||||
// 处理音频封面图回调
|
||||
if (data === "audio_add_cover" || data === "audio_skip_cover") {
|
||||
await handleAudioCoverCallback(chatId, data);
|
||||
try { await tg("answerCallbackQuery", { callback_query_id: queryId }); } catch (_e) {}
|
||||
return;
|
||||
}
|
||||
|
||||
let prefix = "";
|
||||
let arg = "";
|
||||
// First try prefix match (e.g. "edit_slug", "list_2")
|
||||
@@ -373,3 +390,391 @@ async function handleInlineCallback(chatId: number, queryId: string, data: strin
|
||||
await handleCommand(chatId, cmd, arg);
|
||||
try { await tg("answerCallbackQuery", { callback_query_id: queryId }); } catch (_e) {}
|
||||
}
|
||||
| ||||