Files
termi-blog/frontend/src/pages/subscriptions/manage.astro
limitcool 3628a46ed1
Some checks failed
docker-images / resolve-build-targets (push) Successful in 7s
ui-regression / playwright-regression (push) Failing after 13m47s
docker-images / build-and-push (push) Failing after 7s
docker-images / submit-indexnow (push) Has been skipped
feat: add SharePanel component for social sharing with QR code support
- 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.
2026-04-02 14:15:21 +08:00

331 lines
9.9 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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>