更换到Astro #9

Merged
biss merged 30 commits from switch-to-astro into master 2026-06-19 13:23:31 +08:00
3 changed files with 347 additions and 2 deletions
Showing only changes of commit e2d95d08c1 - Show all commits
+343
View File
@@ -0,0 +1,343 @@
<aside class="hitokoto-card" aria-label="一言" data-hitokoto-card>
<div class="hitokoto-card__head">
<button class="hitokoto-card__toggle" type="button" aria-label="展开一言" aria-expanded="true" title="一言" data-hitokoto-toggle>
<i class="fa-solid fa-quote-left" aria-hidden="true"></i>
<span>一言</span>
</button>
<div class="hitokoto-card__actions">
<button class="hitokoto-card__refresh" type="button" aria-label="换一句" title="换一句" data-hitokoto-refresh>
<i class="fa-solid fa-rotate" aria-hidden="true"></i>
</button>
<button class="hitokoto-card__collapse" type="button" aria-label="收起一言" title="收起" data-hitokoto-collapse>
<i class="fa-solid fa-chevron-down" aria-hidden="true"></i>
</button>
</div>
</div>
<a class="hitokoto-card__text" href="https://hitokoto.cn/" target="_blank" rel="noreferrer" data-hitokoto-text>
获取中...
</a>
<p class="hitokoto-card__from" data-hitokoto-from>来自 Hitokoto</p>
</aside>
<script>
(() => {
const card = document.querySelector('[data-hitokoto-card]');
const toggle = card?.querySelector('[data-hitokoto-toggle]');
const text = card?.querySelector('[data-hitokoto-text]');
const from = card?.querySelector('[data-hitokoto-from]');
const refresh = card?.querySelector('[data-hitokoto-refresh]');
const collapse = card?.querySelector('[data-hitokoto-collapse]');
if (
!(card instanceof HTMLElement) ||
!(toggle instanceof HTMLButtonElement) ||
!(text instanceof HTMLAnchorElement) ||
!(from instanceof HTMLElement)
) {
return;
}
const endpoint = 'https://yiyan.biss.click/api?encode=json';
const cacheKey = 'hitokoto-card-cache';
const cacheMaxAge = 6 * 60 * 60 * 1000;
const fallback = {
sentence: '生活明朗,万物可爱。',
source: '本地缓存',
url: 'https://hitokoto.cn/',
};
const isHomePage = ['/', '/index.html'].includes(window.location.pathname);
function setExpanded(expanded) {
card.classList.toggle('is-collapsed', !expanded);
toggle.setAttribute('aria-expanded', String(expanded));
toggle.setAttribute('aria-label', expanded ? '收起一言' : '展开一言');
}
function readCache() {
try {
const cached = localStorage.getItem(cacheKey);
if (!cached) return null;
const parsed = JSON.parse(cached);
if (!parsed || typeof parsed.expiresAt !== 'number' || parsed.expiresAt <= Date.now()) return null;
return parsed.data;
} catch {
return null;
}
}
function writeCache(data) {
try {
localStorage.setItem(
cacheKey,
JSON.stringify({
data,
expiresAt: Date.now() + cacheMaxAge,
}),
);
} catch {
// localStorage can be unavailable in strict privacy modes.
}
}
function renderHitokoto(data) {
text.textContent = data.sentence;
text.href = data.url || 'https://hitokoto.cn/';
from.textContent = data.source ? `出自 ${data.source}` : '来自 Hitokoto';
}
async function fetchHitokoto({ force = false } = {}) {
if (!force) {
const cached = readCache();
if (cached) {
renderHitokoto(cached);
return;
}
}
card.classList.add('is-loading');
text.textContent = '获取中...';
try {
const response = await fetch(endpoint);
if (!response.ok) throw new Error(`Hitokoto request failed: ${response.status}`);
const data = await response.json();
const rawSentence = data.hitokoto || data.content || data.sentence || data.text;
const sentence = typeof rawSentence === 'string' && rawSentence.trim() ? rawSentence.trim() : fallback.sentence;
const source = typeof data.from === 'string' && data.from.trim() ? data.from.trim() : 'Hitokoto';
const url = data.uuid ? `https://hitokoto.cn/?uuid=${encodeURIComponent(data.uuid)}` : 'https://hitokoto.cn/';
const nextData = { sentence, source, url };
writeCache(nextData);
renderHitokoto(nextData);
} catch (error) {
console.error(error);
renderHitokoto(readCache() || fallback);
} finally {
card.classList.remove('is-loading');
}
}
setExpanded(isHomePage);
toggle.addEventListener('click', () => {
setExpanded(card.classList.contains('is-collapsed'));
});
refresh?.addEventListener('click', (event) => {
event.stopPropagation();
setExpanded(true);
fetchHitokoto({ force: true });
});
collapse?.addEventListener('click', (event) => {
event.stopPropagation();
setExpanded(false);
});
card.addEventListener('click', (event) => {
if (!card.classList.contains('is-collapsed')) return;
if (event.target instanceof Element && event.target.closest('a, button')) return;
setExpanded(true);
});
fetchHitokoto();
})();
</script>
<style>
.hitokoto-card {
position: fixed;
left: max(18px, env(safe-area-inset-left));
bottom: max(18px, env(safe-area-inset-bottom));
z-index: 30;
width: min(340px, calc(100vw - 36px));
min-height: 48px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.58);
border-radius: 8px;
padding: 14px 15px 13px;
color: #40505a;
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.74), rgba(236, 253, 245, 0.58)),
rgba(255, 253, 248, 0.76);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.8),
0 18px 44px rgba(44, 55, 63, 0.16);
backdrop-filter: blur(18px) saturate(165%);
-webkit-backdrop-filter: blur(18px) saturate(165%);
transition:
width 0.2s ease,
padding 0.2s ease,
transform 0.2s ease;
}
.hitokoto-card.is-collapsed {
width: 48px;
padding: 8px;
cursor: pointer;
}
.hitokoto-card__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 9px;
color: var(--accent);
font-size: 0.84rem;
font-weight: 800;
}
.hitokoto-card.is-collapsed .hitokoto-card__head {
justify-content: center;
margin-bottom: 0;
}
.hitokoto-card__toggle,
.hitokoto-card__actions {
display: inline-flex;
align-items: center;
gap: 7px;
}
.hitokoto-card__toggle,
.hitokoto-card__refresh,
.hitokoto-card__collapse {
appearance: none;
-webkit-appearance: none;
border: 0;
color: inherit;
font: inherit;
background: transparent;
cursor: pointer;
}
.hitokoto-card__toggle {
display: inline-flex;
align-items: center;
gap: 7px;
min-width: 0;
border-radius: 999px;
padding: 0;
}
.hitokoto-card__toggle:hover {
color: var(--accent-strong);
}
.hitokoto-card__refresh,
.hitokoto-card__collapse {
display: grid;
width: 30px;
min-width: 30px;
height: 30px;
place-items: center;
border: 1px solid rgba(15, 118, 110, 0.16);
border-radius: 999px;
background: rgba(236, 253, 245, 0.58);
transition:
color 0.2s ease,
background-color 0.2s ease,
transform 0.2s ease;
}
.hitokoto-card__refresh:hover {
color: #fff;
background: #3eb8be;
transform: translateY(-1px);
}
.hitokoto-card__collapse:hover {
color: #fff;
background: #3eb8be;
transform: translateY(1px);
}
.hitokoto-card.is-collapsed .hitokoto-card__toggle {
width: 32px;
height: 32px;
justify-content: center;
}
.hitokoto-card.is-collapsed .hitokoto-card__toggle span,
.hitokoto-card.is-collapsed .hitokoto-card__actions,
.hitokoto-card.is-collapsed .hitokoto-card__text,
.hitokoto-card.is-collapsed .hitokoto-card__from {
display: none;
}
.hitokoto-card.is-loading .hitokoto-card__refresh i {
animation: hitokoto-spin 0.9s linear infinite;
}
.hitokoto-card__text {
display: block;
color: #2f4358;
font-size: 0.95rem;
font-weight: 700;
line-height: 1.65;
overflow-wrap: anywhere;
}
.hitokoto-card__text:hover {
color: var(--accent);
}
.hitokoto-card__from {
margin: 8px 0 0;
color: var(--muted);
font-size: 0.78rem;
line-height: 1.45;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:root[data-theme='dark'] .hitokoto-card {
border-color: rgba(174, 205, 197, 0.18);
color: #dce9e6;
background:
radial-gradient(circle at 14% 0%, rgba(114, 222, 210, 0.1), transparent 34%),
linear-gradient(145deg, rgba(28, 43, 47, 0.82), rgba(12, 22, 25, 0.72)),
rgba(17, 28, 31, 0.74);
box-shadow: var(--shadow);
}
:root[data-theme='dark'] .hitokoto-card__text {
color: #edf7f5;
}
:root[data-theme='dark'] .hitokoto-card__refresh,
:root[data-theme='dark'] .hitokoto-card__collapse {
border-color: rgba(114, 222, 210, 0.22);
background: rgba(114, 222, 210, 0.1);
}
@keyframes hitokoto-spin {
to {
transform: rotate(360deg);
}
}
@media (max-width: 720px) {
.hitokoto-card {
left: 12px;
bottom: 12px;
width: min(300px, calc(100vw - 24px));
padding: 12px;
}
.hitokoto-card.is-collapsed {
width: 46px;
padding: 7px;
}
.hitokoto-card__text {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
font-size: 0.9rem;
}
}
</style>
+2
View File
@@ -1,6 +1,7 @@
---
import '../styles/site.css';
import CustomContextMenu from '@/components/CustomContextMenu.astro';
import HitokotoCard from '@/components/HitokotoCard.astro';
import { publicSiteConfig, siteConfig } from '../../site.config.mjs';
interface Props {
@@ -119,6 +120,7 @@ const publicConfigJson = JSON.stringify(publicSiteConfig).replace(/</g, '\\u003c
</section>
</div>
</footer>
<HitokotoCard />
<CustomContextMenu />
<script is:inline src={siteConfig.cdn.instantsearchJs}></script>
<script is:inline src={siteConfig.cdn.typesenseAdapterJs}></script>
+2 -2
View File
@@ -115,9 +115,9 @@ const relatedPosts = allPosts
<div class="article-summary-footer">
<span class="article-summary-brand">
<span aria-hidden="true">✣</span>
清羽のAI摘要
AI摘要
</span>
<span class="article-summary-model">KIMI-K2.5</span>
<span class="article-summary-model">QWEN</span>
</div>
</section>
)}