更换到Astro #9
@@ -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>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import '../styles/site.css';
|
import '../styles/site.css';
|
||||||
import CustomContextMenu from '@/components/CustomContextMenu.astro';
|
import CustomContextMenu from '@/components/CustomContextMenu.astro';
|
||||||
|
import HitokotoCard from '@/components/HitokotoCard.astro';
|
||||||
import { publicSiteConfig, siteConfig } from '../../site.config.mjs';
|
import { publicSiteConfig, siteConfig } from '../../site.config.mjs';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -119,6 +120,7 @@ const publicConfigJson = JSON.stringify(publicSiteConfig).replace(/</g, '\\u003c
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
<HitokotoCard />
|
||||||
<CustomContextMenu />
|
<CustomContextMenu />
|
||||||
<script is:inline src={siteConfig.cdn.instantsearchJs}></script>
|
<script is:inline src={siteConfig.cdn.instantsearchJs}></script>
|
||||||
<script is:inline src={siteConfig.cdn.typesenseAdapterJs}></script>
|
<script is:inline src={siteConfig.cdn.typesenseAdapterJs}></script>
|
||||||
|
|||||||
@@ -115,9 +115,9 @@ const relatedPosts = allPosts
|
|||||||
<div class="article-summary-footer">
|
<div class="article-summary-footer">
|
||||||
<span class="article-summary-brand">
|
<span class="article-summary-brand">
|
||||||
<span aria-hidden="true">✣</span>
|
<span aria-hidden="true">✣</span>
|
||||||
清羽のAI摘要
|
AI摘要
|
||||||
</span>
|
</span>
|
||||||
<span class="article-summary-model">KIMI-K2.5</span>
|
<span class="article-summary-model">QWEN</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user