feat: ship public ops features and cache docker builds
Some checks failed
docker-images / build-and-push (admin, admin, termi-astro-admin, admin/Dockerfile) (push) Failing after 13s
docker-images / build-and-push (frontend, frontend, termi-astro-frontend, frontend/Dockerfile) (push) Has been cancelled
docker-images / build-and-push (backend, backend, termi-astro-backend, backend/Dockerfile) (push) Has been cancelled
Some checks failed
docker-images / build-and-push (admin, admin, termi-astro-admin, admin/Dockerfile) (push) Failing after 13s
docker-images / build-and-push (frontend, frontend, termi-astro-frontend, frontend/Dockerfile) (push) Has been cancelled
docker-images / build-and-push (backend, backend, termi-astro-backend, backend/Dockerfile) (push) Has been cancelled
This commit is contained in:
@@ -1,16 +1,25 @@
|
||||
---
|
||||
import { apiClient, resolvePublicApiBaseUrl } from '../lib/api/client';
|
||||
import {
|
||||
apiClient,
|
||||
resolvePublicApiBaseUrl,
|
||||
resolvePublicCommentTurnstileSiteKey,
|
||||
} from '../lib/api/client';
|
||||
import { getI18n } from '../lib/i18n';
|
||||
import type { Comment } from '../lib/api/client';
|
||||
import type { SiteSettings } from '../lib/types';
|
||||
|
||||
interface Props {
|
||||
postSlug: string;
|
||||
class?: string;
|
||||
siteSettings: SiteSettings;
|
||||
}
|
||||
|
||||
const { postSlug, class: className = '' } = Astro.props;
|
||||
const { postSlug, class: className = '', siteSettings } = Astro.props as Props;
|
||||
const { locale, t } = getI18n(Astro);
|
||||
const publicApiBaseUrl = resolvePublicApiBaseUrl(Astro.url);
|
||||
const turnstileSiteKey = siteSettings.comments.turnstileEnabled
|
||||
? siteSettings.comments.turnstileSiteKey || resolvePublicCommentTurnstileSiteKey()
|
||||
: '';
|
||||
|
||||
let comments: Comment[] = [];
|
||||
let error: string | null = null;
|
||||
@@ -36,7 +45,12 @@ function formatCommentDate(dateStr: string): string {
|
||||
}
|
||||
---
|
||||
|
||||
<div class={`terminal-comments ${className}`} data-post-slug={postSlug} data-api-base={publicApiBaseUrl}>
|
||||
<div
|
||||
class={`terminal-comments ${className}`}
|
||||
data-post-slug={postSlug}
|
||||
data-api-base={publicApiBaseUrl}
|
||||
data-turnstile-site-key={turnstileSiteKey || undefined}
|
||||
>
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="space-y-3">
|
||||
<span class="terminal-kicker">
|
||||
@@ -115,23 +129,38 @@ function formatCommentDate(dateStr: string): string {
|
||||
<div class="rounded-2xl border border-[var(--border-color)] bg-[var(--header-bg)]/60 px-4 py-3">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-[var(--text-tertiary)]">
|
||||
验证码
|
||||
{t('common.humanVerification')}
|
||||
</p>
|
||||
<button type="button" id="refresh-captcha" class="terminal-action-button px-3 py-2 text-xs">
|
||||
<i class="fas fa-rotate-right"></i>
|
||||
<span>刷新</span>
|
||||
</button>
|
||||
{turnstileSiteKey ? (
|
||||
<span class="text-xs text-[var(--text-tertiary)]">Cloudflare Turnstile</span>
|
||||
) : (
|
||||
<button type="button" id="refresh-captcha" class="terminal-action-button px-3 py-2 text-xs">
|
||||
<i class="fas fa-rotate-right"></i>
|
||||
<span>{t('common.refresh')}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p id="captcha-question" class="mt-2 text-sm text-[var(--text-secondary)]">加载中...</p>
|
||||
<input type="hidden" name="captchaToken" />
|
||||
<input
|
||||
type="text"
|
||||
name="captchaAnswer"
|
||||
required
|
||||
inputmode="numeric"
|
||||
placeholder="请输入上方答案"
|
||||
class="mt-3 terminal-form-input"
|
||||
/>
|
||||
|
||||
{turnstileSiteKey ? (
|
||||
<>
|
||||
<div class="mt-3" data-turnstile-container></div>
|
||||
<input type="hidden" name="turnstileToken" />
|
||||
<p class="mt-3 text-sm text-[var(--text-secondary)]">{t('common.turnstileHint')}</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p id="captcha-question" class="mt-2 text-sm text-[var(--text-secondary)]">加载中...</p>
|
||||
<input type="hidden" name="captchaToken" />
|
||||
<input
|
||||
type="text"
|
||||
name="captchaAnswer"
|
||||
required
|
||||
inputmode="numeric"
|
||||
placeholder="请输入上方答案"
|
||||
class="mt-3 terminal-form-input"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div id="replying-to" class="terminal-panel-muted hidden items-center justify-between gap-3 py-3">
|
||||
@@ -228,6 +257,8 @@ function formatCommentDate(dateStr: string): string {
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import { mountTurnstile, type MountedTurnstile } from '../lib/utils/turnstile';
|
||||
|
||||
const t = window.__termiTranslate;
|
||||
const wrapper = document.querySelector('.terminal-comments');
|
||||
const toggleBtn = document.getElementById('toggle-comment-form');
|
||||
@@ -243,8 +274,12 @@ function formatCommentDate(dateStr: string): string {
|
||||
const refreshCaptchaBtn = document.getElementById('refresh-captcha');
|
||||
const postSlug = wrapper?.getAttribute('data-post-slug') || '';
|
||||
const apiBase = wrapper?.getAttribute('data-api-base') || '/api';
|
||||
const captchaTokenInput = form?.querySelector('input[name=\"captchaToken\"]') as HTMLInputElement | null;
|
||||
const captchaAnswerInput = form?.querySelector('input[name=\"captchaAnswer\"]') as HTMLInputElement | null;
|
||||
const turnstileSiteKey = wrapper?.getAttribute('data-turnstile-site-key') || '';
|
||||
const turnstileContainer = form?.querySelector('[data-turnstile-container]') as HTMLElement | null;
|
||||
const turnstileTokenInput = form?.querySelector('input[name="turnstileToken"]') as HTMLInputElement | null;
|
||||
const captchaTokenInput = form?.querySelector('input[name="captchaToken"]') as HTMLInputElement | null;
|
||||
const captchaAnswerInput = form?.querySelector('input[name="captchaAnswer"]') as HTMLInputElement | null;
|
||||
let turnstileWidget: MountedTurnstile | null = null;
|
||||
|
||||
function showMessage(message: string, type: 'success' | 'error' | 'info') {
|
||||
if (!messageBox) return;
|
||||
@@ -316,6 +351,54 @@ function formatCommentDate(dateStr: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureTurnstile(showError = true) {
|
||||
if (!turnstileSiteKey || !turnstileContainer || !turnstileTokenInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
turnstileTokenInput.value = '';
|
||||
|
||||
if (turnstileWidget) {
|
||||
turnstileWidget.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
turnstileWidget = await mountTurnstile(turnstileContainer, {
|
||||
siteKey: turnstileSiteKey,
|
||||
onToken(token) {
|
||||
turnstileTokenInput.value = token;
|
||||
},
|
||||
onExpire() {
|
||||
turnstileTokenInput.value = '';
|
||||
},
|
||||
onError() {
|
||||
turnstileTokenInput.value = '';
|
||||
if (showError) {
|
||||
showMessage(t('common.turnstileLoadFailed'), 'error');
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (showError) {
|
||||
showMessage(
|
||||
error instanceof Error ? error.message : t('common.turnstileLoadFailed'),
|
||||
'error',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetHumanCheck() {
|
||||
if (turnstileSiteKey) {
|
||||
turnstileTokenInput && (turnstileTokenInput.value = '');
|
||||
turnstileWidget?.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
void loadCaptcha(false);
|
||||
}
|
||||
|
||||
toggleBtn?.addEventListener('click', () => {
|
||||
formContainer?.classList.toggle('hidden');
|
||||
if (!formContainer?.classList.contains('hidden')) {
|
||||
@@ -360,6 +443,14 @@ function formatCommentDate(dateStr: string): string {
|
||||
const formData = new FormData(form);
|
||||
const replyToId = replyingTo?.getAttribute('data-reply-to');
|
||||
|
||||
if (turnstileSiteKey) {
|
||||
const token = String(formData.get('turnstileToken') || '').trim();
|
||||
if (!token) {
|
||||
showMessage(t('common.turnstileRequired'), 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
showMessage(t('comments.submitting'), 'info');
|
||||
|
||||
@@ -375,6 +466,7 @@ function formatCommentDate(dateStr: string): string {
|
||||
content: formData.get('content'),
|
||||
scope: 'article',
|
||||
replyToCommentId: replyToId ? Number(replyToId) : null,
|
||||
turnstileToken: formData.get('turnstileToken'),
|
||||
captchaToken: formData.get('captchaToken'),
|
||||
captchaAnswer: formData.get('captchaAnswer'),
|
||||
website: formData.get('website'),
|
||||
@@ -390,10 +482,10 @@ function formatCommentDate(dateStr: string): string {
|
||||
resetReply();
|
||||
formContainer?.classList.add('hidden');
|
||||
showMessage(t('comments.submitSuccess'), 'success');
|
||||
void loadCaptcha(false);
|
||||
resetHumanCheck();
|
||||
} catch (error) {
|
||||
showMessage(t('comments.submitFailed', { message: error instanceof Error ? error.message : t('common.unknownError') }), 'error');
|
||||
void loadCaptcha(false);
|
||||
resetHumanCheck();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -410,5 +502,9 @@ function formatCommentDate(dateStr: string): string {
|
||||
});
|
||||
});
|
||||
|
||||
void loadCaptcha(false);
|
||||
if (turnstileSiteKey) {
|
||||
void ensureTurnstile(false);
|
||||
} else {
|
||||
void loadCaptcha(false);
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user