yiyan
This commit is contained in:
@@ -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 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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user