- Implemented SharePanel component in `SharePanel.astro` for sharing content on social media platforms. - Integrated QR code generation for WeChat sharing using the `qrcode` library. - Added localization support for English and Chinese languages. - Created utility functions in `seo.ts` for building article summaries and FAQs. - Introduced API routes for serving IndexNow key and generating full LLM catalog and summaries. - Enhanced SEO capabilities with structured data for articles and pages.
331 lines
9.9 KiB
Plaintext
331 lines
9.9 KiB
Plaintext
---
|
||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||
import {
|
||
createApiClient,
|
||
resolvePublicApiBaseUrl,
|
||
type PublicManagedSubscription,
|
||
} from '../../lib/api/client';
|
||
|
||
const token = Astro.url.searchParams.get('token')?.trim() ?? '';
|
||
const api = createApiClient({ requestUrl: Astro.url });
|
||
const apiBaseUrl = resolvePublicApiBaseUrl(Astro.url);
|
||
const EVENT_OPTIONS = [
|
||
{ value: 'post.published', label: '新文章通知' },
|
||
{ value: 'digest.weekly', label: '每周简报' },
|
||
{ value: 'digest.monthly', label: '每月简报' },
|
||
{ value: 'comment.created', label: '评论通知' },
|
||
{ value: 'friend_link.created', label: '友链申请通知' },
|
||
] as const;
|
||
|
||
const asObject = (value: unknown) => {
|
||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||
return {} as Record<string, unknown>;
|
||
}
|
||
return value as Record<string, unknown>;
|
||
};
|
||
|
||
const normalizeStringList = (value: unknown) => {
|
||
if (!Array.isArray(value)) {
|
||
return [] as string[];
|
||
}
|
||
return value
|
||
.map((item) => String(item).trim())
|
||
.filter(Boolean);
|
||
};
|
||
|
||
let subscription: PublicManagedSubscription | null = null;
|
||
let errorMessage = '';
|
||
|
||
if (token) {
|
||
try {
|
||
const response = await api.getManagedSubscription(token);
|
||
subscription = response.subscription;
|
||
} catch (error) {
|
||
errorMessage = error instanceof Error ? error.message : '无法加载订阅信息。';
|
||
}
|
||
} else {
|
||
errorMessage = '缺少管理令牌。';
|
||
}
|
||
|
||
const filters = asObject(subscription?.filters);
|
||
const initialEvents = normalizeStringList(filters.event_types);
|
||
const initialCategories = normalizeStringList(filters.categories).join(', ');
|
||
const initialTags = normalizeStringList(filters.tags).join(', ');
|
||
const initialDisplayName = subscription?.display_name ?? '';
|
||
const initialStatus = subscription?.status === 'paused' ? 'paused' : 'active';
|
||
---
|
||
|
||
<BaseLayout title="管理订阅偏好" description="调整订阅偏好、暂停订阅或查看当前订阅状态。" noindex>
|
||
<section class="subscription-shell">
|
||
<div class="subscription-card" data-subscription-manage-root data-api-base={apiBaseUrl}>
|
||
<p class="subscription-kicker">subscriptions / manage</p>
|
||
<h1>管理订阅偏好</h1>
|
||
{subscription ? (
|
||
<>
|
||
<p class="subscription-copy">当前目标:<strong>{subscription.target}</strong> · 频道:{subscription.channel_type}</p>
|
||
<p class="subscription-copy">验证状态:{subscription.verified_at ? '已确认' : '待确认'} · 当前状态:{subscription.status}</p>
|
||
|
||
<form class="subscription-form" data-manage-form>
|
||
<input type="hidden" name="token" value={token} />
|
||
<label>
|
||
<span>称呼</span>
|
||
<input type="text" name="displayName" value={initialDisplayName} placeholder="例如:主邮箱 / 运营邮箱" />
|
||
</label>
|
||
|
||
<fieldset>
|
||
<legend>通知类型</legend>
|
||
<div class="subscription-grid">
|
||
{EVENT_OPTIONS.map((item) => (
|
||
<label class="subscription-checkbox">
|
||
<input type="checkbox" name="eventTypes" value={item.value} checked={initialEvents.includes(item.value)} />
|
||
<span>{item.label}</span>
|
||
</label>
|
||
))}
|
||
</div>
|
||
</fieldset>
|
||
|
||
<div class="subscription-split">
|
||
<label>
|
||
<span>只看这些分类(可选)</span>
|
||
<input type="text" name="categories" value={initialCategories} placeholder="Rust, Astro, 日志" />
|
||
</label>
|
||
<label>
|
||
<span>只看这些标签(可选)</span>
|
||
<input type="text" name="tags" value={initialTags} placeholder="web, ai, deploy" />
|
||
</label>
|
||
</div>
|
||
|
||
<fieldset>
|
||
<legend>订阅状态</legend>
|
||
<div class="subscription-grid compact">
|
||
<label class="subscription-checkbox">
|
||
<input type="radio" name="status" value="active" checked={initialStatus === 'active'} />
|
||
<span>继续接收</span>
|
||
</label>
|
||
<label class="subscription-checkbox">
|
||
<input type="radio" name="status" value="paused" checked={initialStatus === 'paused'} />
|
||
<span>先暂停</span>
|
||
</label>
|
||
</div>
|
||
</fieldset>
|
||
|
||
<div class="subscription-actions">
|
||
<button type="submit">保存偏好</button>
|
||
<a href={`/subscriptions/unsubscribe?token=${encodeURIComponent(token)}`}>去退订</a>
|
||
</div>
|
||
</form>
|
||
|
||
<p class="subscription-status" data-manage-status>你可以随时在这里调整通知类型、暂停或退订。</p>
|
||
</>
|
||
) : (
|
||
<p class="subscription-status is-warning">{errorMessage}</p>
|
||
)}
|
||
</div>
|
||
</section>
|
||
</BaseLayout>
|
||
|
||
{subscription && (
|
||
<script>
|
||
const root = document.querySelector('[data-subscription-manage-root]');
|
||
const form = root?.querySelector?.('[data-manage-form]');
|
||
const status = root?.querySelector?.('[data-manage-status]');
|
||
const apiBase = root?.getAttribute?.('data-api-base') || '';
|
||
|
||
if (form instanceof HTMLFormElement && status instanceof HTMLElement && apiBase) {
|
||
form.addEventListener('submit', async (event) => {
|
||
event.preventDefault();
|
||
const formData = new FormData(form);
|
||
const token = String(formData.get('token') || '').trim();
|
||
const displayName = String(formData.get('displayName') || '').trim();
|
||
const statusValue = String(formData.get('status') || 'active').trim();
|
||
const categories = String(formData.get('categories') || '')
|
||
.split(/[,,]/)
|
||
.map((item) => item.trim())
|
||
.filter(Boolean);
|
||
const tags = String(formData.get('tags') || '')
|
||
.split(/[,,]/)
|
||
.map((item) => item.trim())
|
||
.filter(Boolean);
|
||
const eventTypes = formData
|
||
.getAll('eventTypes')
|
||
.map((item) => String(item).trim())
|
||
.filter(Boolean);
|
||
|
||
status.textContent = '保存中...';
|
||
|
||
const filters = {
|
||
event_types: eventTypes,
|
||
categories,
|
||
tags,
|
||
};
|
||
|
||
try {
|
||
const response = await fetch(`${apiBase}/subscriptions/manage`, {
|
||
method: 'PATCH',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
token,
|
||
displayName: displayName || null,
|
||
status: statusValue,
|
||
filters,
|
||
}),
|
||
});
|
||
const payload = await response.json().catch(() => ({}));
|
||
if (!response.ok) {
|
||
throw new Error(payload?.description || payload?.message || '保存失败,请稍后重试。');
|
||
}
|
||
status.textContent = '偏好已保存。';
|
||
} catch (error) {
|
||
status.textContent = error instanceof Error ? error.message : '保存失败,请稍后重试。';
|
||
}
|
||
});
|
||
}
|
||
</script>
|
||
)}
|
||
|
||
<style>
|
||
.subscription-shell {
|
||
max-width: 54rem;
|
||
margin: 0 auto;
|
||
padding: 2rem 1rem 4rem;
|
||
}
|
||
|
||
.subscription-card {
|
||
border: 1px solid rgba(94, 234, 212, 0.16);
|
||
background: linear-gradient(135deg, rgba(15, 23, 42, 0.86), rgba(15, 23, 42, 0.72));
|
||
border-radius: 1.25rem;
|
||
padding: 1.5rem;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.subscription-kicker {
|
||
margin: 0 0 0.5rem;
|
||
color: var(--primary);
|
||
font-size: 0.72rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.22em;
|
||
}
|
||
|
||
h1 {
|
||
margin: 0;
|
||
font-size: 1.75rem;
|
||
}
|
||
|
||
.subscription-copy {
|
||
margin: 0.65rem 0 0;
|
||
color: var(--text-secondary);
|
||
line-height: 1.75;
|
||
}
|
||
|
||
.subscription-form {
|
||
display: grid;
|
||
gap: 1rem;
|
||
margin-top: 1.4rem;
|
||
}
|
||
|
||
.subscription-form label,
|
||
.subscription-form fieldset {
|
||
display: grid;
|
||
gap: 0.5rem;
|
||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||
border-radius: 1rem;
|
||
padding: 1rem;
|
||
background: rgba(15, 23, 42, 0.24);
|
||
}
|
||
|
||
.subscription-form span,
|
||
.subscription-form legend {
|
||
color: var(--text-secondary);
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.subscription-form input[type='text'] {
|
||
width: 100%;
|
||
border-radius: 0.8rem;
|
||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||
background: rgba(15, 23, 42, 0.45);
|
||
color: var(--text-primary);
|
||
padding: 0.85rem 0.95rem;
|
||
}
|
||
|
||
.subscription-grid {
|
||
display: grid;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.subscription-grid.compact {
|
||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||
}
|
||
|
||
.subscription-checkbox {
|
||
display: flex !important;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
padding: 0.85rem 0.95rem !important;
|
||
}
|
||
|
||
.subscription-checkbox input {
|
||
accent-color: var(--primary);
|
||
}
|
||
|
||
.subscription-split {
|
||
display: grid;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.subscription-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.85rem;
|
||
margin-top: 0.25rem;
|
||
}
|
||
|
||
.subscription-actions button,
|
||
.subscription-actions a {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 999px;
|
||
border: 0;
|
||
padding: 0.9rem 1.05rem;
|
||
font-weight: 600;
|
||
text-decoration: none;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.subscription-actions button {
|
||
color: #08111f;
|
||
background: linear-gradient(135deg, var(--primary), #8b5cf6);
|
||
}
|
||
|
||
.subscription-actions a {
|
||
color: var(--text-primary);
|
||
border: 1px solid rgba(148, 163, 184, 0.24);
|
||
}
|
||
|
||
.subscription-status {
|
||
margin: 1rem 0 0;
|
||
color: var(--text-secondary);
|
||
line-height: 1.8;
|
||
}
|
||
|
||
.subscription-status.is-warning {
|
||
padding: 0.95rem 1rem;
|
||
border-radius: 0.95rem;
|
||
background: rgba(245, 158, 11, 0.14);
|
||
color: #fde68a;
|
||
}
|
||
|
||
@media (min-width: 900px) {
|
||
.subscription-grid {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.subscription-split {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
}
|
||
</style>
|