feat(frontend): 添加邮件模板编辑器
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
8cef9a7ab1
commit
11462a3e9f
@ -850,6 +850,105 @@ export async function sendTestEmail(
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Email Template Settings ====================
|
||||||
|
|
||||||
|
export interface EmailTemplateOption {
|
||||||
|
value: string;
|
||||||
|
label?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EmailTemplateEventOption = string | EmailTemplateOption;
|
||||||
|
|
||||||
|
export interface EmailTemplateSummary {
|
||||||
|
event: string;
|
||||||
|
locale: string;
|
||||||
|
subject: string;
|
||||||
|
is_custom?: boolean;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailTemplateListResponse {
|
||||||
|
events: EmailTemplateEventOption[];
|
||||||
|
locales: string[];
|
||||||
|
templates?: EmailTemplateSummary[];
|
||||||
|
placeholders?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailTemplateDetail {
|
||||||
|
event: string;
|
||||||
|
locale: string;
|
||||||
|
subject: string;
|
||||||
|
html: string;
|
||||||
|
is_custom?: boolean;
|
||||||
|
updated_at?: string;
|
||||||
|
placeholders?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateEmailTemplateRequest {
|
||||||
|
subject: string;
|
||||||
|
html: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PreviewEmailTemplateRequest extends UpdateEmailTemplateRequest {
|
||||||
|
event: string;
|
||||||
|
locale: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailTemplatePreviewResponse {
|
||||||
|
subject: string;
|
||||||
|
html: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getEmailTemplates(): Promise<EmailTemplateListResponse> {
|
||||||
|
const { data } = await apiClient.get<EmailTemplateListResponse>(
|
||||||
|
"/admin/settings/email-templates",
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getEmailTemplate(
|
||||||
|
event: string,
|
||||||
|
locale: string,
|
||||||
|
): Promise<EmailTemplateDetail> {
|
||||||
|
const { data } = await apiClient.get<EmailTemplateDetail>(
|
||||||
|
`/admin/settings/email-templates/${encodeURIComponent(event)}/${encodeURIComponent(locale)}`,
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateEmailTemplate(
|
||||||
|
event: string,
|
||||||
|
locale: string,
|
||||||
|
request: UpdateEmailTemplateRequest,
|
||||||
|
): Promise<EmailTemplateDetail> {
|
||||||
|
const { data } = await apiClient.put<EmailTemplateDetail>(
|
||||||
|
`/admin/settings/email-templates/${encodeURIComponent(event)}/${encodeURIComponent(locale)}`,
|
||||||
|
request,
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function restoreOfficialEmailTemplate(
|
||||||
|
event: string,
|
||||||
|
locale: string,
|
||||||
|
): Promise<EmailTemplateDetail> {
|
||||||
|
const { data } = await apiClient.post<EmailTemplateDetail>(
|
||||||
|
`/admin/settings/email-templates/${encodeURIComponent(event)}/${encodeURIComponent(locale)}/restore-official`,
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function previewEmailTemplate(
|
||||||
|
request: PreviewEmailTemplateRequest,
|
||||||
|
): Promise<EmailTemplatePreviewResponse> {
|
||||||
|
const { data } = await apiClient.post<EmailTemplatePreviewResponse>(
|
||||||
|
"/admin/settings/email-template-preview",
|
||||||
|
request,
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Admin API Key status response
|
* Admin API Key status response
|
||||||
*/
|
*/
|
||||||
@ -1156,6 +1255,11 @@ export const settingsAPI = {
|
|||||||
updateSettings,
|
updateSettings,
|
||||||
testSmtpConnection,
|
testSmtpConnection,
|
||||||
sendTestEmail,
|
sendTestEmail,
|
||||||
|
getEmailTemplates,
|
||||||
|
getEmailTemplate,
|
||||||
|
updateEmailTemplate,
|
||||||
|
restoreOfficialEmailTemplate,
|
||||||
|
previewEmailTemplate,
|
||||||
getAdminApiKey,
|
getAdminApiKey,
|
||||||
regenerateAdminApiKey,
|
regenerateAdminApiKey,
|
||||||
deleteAdminApiKey,
|
deleteAdminApiKey,
|
||||||
|
|||||||
@ -6193,6 +6193,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<EmailTemplateEditor />
|
||||||
|
|
||||||
<!-- Balance Low Notification -->
|
<!-- Balance Low Notification -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div
|
<div
|
||||||
@ -6450,6 +6453,7 @@ import Toggle from "@/components/common/Toggle.vue";
|
|||||||
import ProxySelector from "@/components/common/ProxySelector.vue";
|
import ProxySelector from "@/components/common/ProxySelector.vue";
|
||||||
import ImageUpload from "@/components/common/ImageUpload.vue";
|
import ImageUpload from "@/components/common/ImageUpload.vue";
|
||||||
import BackupSettings from "@/views/admin/BackupView.vue";
|
import BackupSettings from "@/views/admin/BackupView.vue";
|
||||||
|
import EmailTemplateEditor from "@/views/admin/settings/EmailTemplateEditor.vue";
|
||||||
import { useClipboard } from "@/composables/useClipboard";
|
import { useClipboard } from "@/composables/useClipboard";
|
||||||
import { affiliatesAPI, type AffiliateAdminEntry, type SimpleUser as AffiliateSimpleUser } from "@/api/admin/affiliates";
|
import { affiliatesAPI, type AffiliateAdminEntry, type SimpleUser as AffiliateSimpleUser } from "@/api/admin/affiliates";
|
||||||
import { extractApiErrorMessage, extractI18nErrorMessage } from "@/utils/apiError";
|
import { extractApiErrorMessage, extractI18nErrorMessage } from "@/utils/apiError";
|
||||||
|
|||||||
438
frontend/src/views/admin/settings/EmailTemplateEditor.vue
Normal file
438
frontend/src/views/admin/settings/EmailTemplateEditor.vue
Normal file
@ -0,0 +1,438 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card">
|
||||||
|
<div
|
||||||
|
class="flex flex-col gap-3 border-b border-gray-100 px-6 py-4 dark:border-dark-700 lg:flex-row lg:items-start lg:justify-between"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ t("admin.settings.emailTemplates.title") }}
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t("admin.settings.emailTemplates.description") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary btn-sm"
|
||||||
|
:disabled="loadingTemplate || previewing || !canPreview"
|
||||||
|
@click="refreshPreview"
|
||||||
|
>
|
||||||
|
{{ previewing ? t("admin.settings.emailTemplates.previewing") : t("admin.settings.emailTemplates.preview") }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary btn-sm"
|
||||||
|
:disabled="loadingTemplate || restoring || !selectedEvent || !selectedLocale"
|
||||||
|
@click="restoreOfficial"
|
||||||
|
>
|
||||||
|
{{ restoring ? t("admin.settings.emailTemplates.restoring") : t("admin.settings.emailTemplates.restoreOfficial") }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
:disabled="loadingTemplate || saving || !canSave"
|
||||||
|
@click="saveTemplate"
|
||||||
|
>
|
||||||
|
{{ saving ? t("admin.settings.emailTemplates.saving") : t("admin.settings.emailTemplates.save") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-6 p-6">
|
||||||
|
<div
|
||||||
|
v-if="loadingList"
|
||||||
|
class="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="h-4 w-4 animate-spin rounded-full border-b-2 border-primary-600"
|
||||||
|
></span>
|
||||||
|
{{ t("common.loading") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label class="input-label" for="email-template-event">
|
||||||
|
{{ t("admin.settings.emailTemplates.event") }}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="email-template-event"
|
||||||
|
v-model="selectedEvent"
|
||||||
|
class="input"
|
||||||
|
:disabled="loadingTemplate || eventOptions.length === 0"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="option in eventOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value"
|
||||||
|
>
|
||||||
|
{{ option.label || option.value }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<p v-if="selectedEventDescription" class="input-hint">
|
||||||
|
{{ selectedEventDescription }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="input-label" for="email-template-locale">
|
||||||
|
{{ t("admin.settings.emailTemplates.locale") }}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="email-template-locale"
|
||||||
|
v-model="selectedLocale"
|
||||||
|
class="input"
|
||||||
|
:disabled="loadingTemplate || localeOptions.length === 0"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="localeOption in localeOptions"
|
||||||
|
:key="localeOption"
|
||||||
|
:value="localeOption"
|
||||||
|
>
|
||||||
|
{{ formatLocale(localeOption) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!eventOptions.length || !localeOptions.length"
|
||||||
|
class="rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-700 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-300"
|
||||||
|
>
|
||||||
|
{{ t("admin.settings.emailTemplates.empty") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="grid grid-cols-1 gap-6 xl:grid-cols-2">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="input-label" for="email-template-subject">
|
||||||
|
{{ t("admin.settings.emailTemplates.subject") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email-template-subject"
|
||||||
|
v-model="subject"
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
:disabled="loadingTemplate"
|
||||||
|
:placeholder="t('admin.settings.emailTemplates.subjectPlaceholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="input-label" for="email-template-html">
|
||||||
|
{{ t("admin.settings.emailTemplates.html") }}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="email-template-html"
|
||||||
|
v-model="html"
|
||||||
|
rows="18"
|
||||||
|
class="input min-h-[28rem] resize-y font-mono text-sm leading-6"
|
||||||
|
:disabled="loadingTemplate"
|
||||||
|
:placeholder="t('admin.settings.emailTemplates.htmlPlaceholder')"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-dark-700 dark:bg-dark-800/60"
|
||||||
|
>
|
||||||
|
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{{ t("admin.settings.emailTemplates.placeholders") }}
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t("admin.settings.emailTemplates.placeholdersHelp") }}
|
||||||
|
</p>
|
||||||
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
v-for="placeholder in placeholderList"
|
||||||
|
:key="placeholder"
|
||||||
|
type="button"
|
||||||
|
class="rounded-full border border-gray-200 bg-white px-3 py-1 font-mono text-xs text-gray-700 transition-colors hover:border-primary-300 hover:text-primary-600 dark:border-dark-600 dark:bg-dark-700 dark:text-gray-200 dark:hover:border-primary-500 dark:hover:text-primary-300"
|
||||||
|
@click="copyPlaceholder(placeholder)"
|
||||||
|
>
|
||||||
|
{{ placeholder }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div
|
||||||
|
class="rounded-lg border border-gray-200 bg-white dark:border-dark-700 dark:bg-dark-800"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between border-b border-gray-100 px-4 py-3 dark:border-dark-700"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{{ t("admin.settings.emailTemplates.livePreview") }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ previewSubject || t("admin.settings.emailTemplates.noPreview") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="isCustomTemplate"
|
||||||
|
class="rounded-full bg-primary-50 px-2.5 py-1 text-xs font-medium text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
|
||||||
|
>
|
||||||
|
{{ t("admin.settings.emailTemplates.customized") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-100 p-3 dark:bg-dark-900">
|
||||||
|
<iframe
|
||||||
|
class="h-[36rem] w-full rounded-md border border-gray-200 bg-white dark:border-dark-700"
|
||||||
|
sandbox=""
|
||||||
|
:srcdoc="previewHtml"
|
||||||
|
:title="t('admin.settings.emailTemplates.livePreview')"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t("admin.settings.emailTemplates.previewSecurityHint") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref, watch } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { adminAPI } from "@/api";
|
||||||
|
import type {
|
||||||
|
EmailTemplateEventOption,
|
||||||
|
EmailTemplateOption,
|
||||||
|
} from "@/api/admin/settings";
|
||||||
|
import { useAppStore } from "@/stores";
|
||||||
|
import { extractApiErrorMessage } from "@/utils/apiError";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
const fallbackPlaceholders = [
|
||||||
|
"{{site_name}}",
|
||||||
|
"{{recipient_name}}",
|
||||||
|
"{{recipient_email}}",
|
||||||
|
"{{subscription_group}}",
|
||||||
|
"{{expiry_time}}",
|
||||||
|
"{{days_remaining}}",
|
||||||
|
"{{current_balance}}",
|
||||||
|
"{{threshold}}",
|
||||||
|
"{{recharge_url}}",
|
||||||
|
"{{recharge_amount}}",
|
||||||
|
"{{order_id}}",
|
||||||
|
"{{unsubscribe_url}}",
|
||||||
|
];
|
||||||
|
|
||||||
|
const loadingList = ref(true);
|
||||||
|
const loadingTemplate = ref(false);
|
||||||
|
const saving = ref(false);
|
||||||
|
const previewing = ref(false);
|
||||||
|
const restoring = ref(false);
|
||||||
|
const eventOptions = ref<EmailTemplateOption[]>([]);
|
||||||
|
const localeOptions = ref<string[]>([]);
|
||||||
|
const selectedEvent = ref("");
|
||||||
|
const selectedLocale = ref("");
|
||||||
|
const subject = ref("");
|
||||||
|
const html = ref("");
|
||||||
|
const isCustomTemplate = ref(false);
|
||||||
|
const placeholders = ref<string[]>([]);
|
||||||
|
const previewSubject = ref("");
|
||||||
|
const previewHtml = ref("");
|
||||||
|
const initializingSelection = ref(false);
|
||||||
|
|
||||||
|
function normalizeEventOption(option: EmailTemplateEventOption): EmailTemplateOption {
|
||||||
|
if (typeof option === "string") {
|
||||||
|
return { value: option };
|
||||||
|
}
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedEventDescription = computed(() => {
|
||||||
|
return (
|
||||||
|
eventOptions.value.find((option) => option.value === selectedEvent.value)
|
||||||
|
?.description || ""
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const placeholderList = computed(() => {
|
||||||
|
const combined = [...placeholders.value, ...fallbackPlaceholders];
|
||||||
|
return Array.from(
|
||||||
|
new Set(
|
||||||
|
combined
|
||||||
|
.map((item) => formatPlaceholder(item))
|
||||||
|
.filter((item) => item.length > 0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatPlaceholder(placeholder: string): string {
|
||||||
|
const trimmed = placeholder.trim();
|
||||||
|
if (!trimmed) return "";
|
||||||
|
if (trimmed.startsWith("{{") && trimmed.endsWith("}}")) return trimmed;
|
||||||
|
return `{{${trimmed}}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canSave = computed(
|
||||||
|
() =>
|
||||||
|
Boolean(selectedEvent.value && selectedLocale.value) &&
|
||||||
|
subject.value.trim().length > 0 &&
|
||||||
|
html.value.trim().length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const canPreview = computed(
|
||||||
|
() => Boolean(selectedEvent.value && selectedLocale.value) && html.value.trim().length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
function formatLocale(locale: string): string {
|
||||||
|
const lower = locale.toLowerCase();
|
||||||
|
if (lower === "zh" || lower.startsWith("zh-")) {
|
||||||
|
return t("admin.settings.emailTemplates.localeZh");
|
||||||
|
}
|
||||||
|
if (lower === "en" || lower.startsWith("en-")) {
|
||||||
|
return t("admin.settings.emailTemplates.localeEn");
|
||||||
|
}
|
||||||
|
return locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTemplate(template: {
|
||||||
|
subject: string;
|
||||||
|
html: string;
|
||||||
|
is_custom?: boolean;
|
||||||
|
placeholders?: string[];
|
||||||
|
}) {
|
||||||
|
subject.value = template.subject;
|
||||||
|
html.value = template.html;
|
||||||
|
isCustomTemplate.value = template.is_custom === true;
|
||||||
|
if (template.placeholders?.length) {
|
||||||
|
placeholders.value = template.placeholders;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTemplate() {
|
||||||
|
if (!selectedEvent.value || !selectedLocale.value) return;
|
||||||
|
loadingTemplate.value = true;
|
||||||
|
try {
|
||||||
|
const template = await adminAPI.settings.getEmailTemplate(
|
||||||
|
selectedEvent.value,
|
||||||
|
selectedLocale.value,
|
||||||
|
);
|
||||||
|
applyTemplate(template);
|
||||||
|
await refreshPreview();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
appStore.showError(extractApiErrorMessage(err, t("common.error")));
|
||||||
|
} finally {
|
||||||
|
loadingTemplate.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTemplateList() {
|
||||||
|
loadingList.value = true;
|
||||||
|
try {
|
||||||
|
const response = await adminAPI.settings.getEmailTemplates();
|
||||||
|
eventOptions.value = response.events.map(normalizeEventOption);
|
||||||
|
localeOptions.value = response.locales;
|
||||||
|
placeholders.value = response.placeholders || [];
|
||||||
|
initializingSelection.value = true;
|
||||||
|
selectedEvent.value = eventOptions.value[0]?.value || "";
|
||||||
|
selectedLocale.value = response.locales[0] || "";
|
||||||
|
await loadTemplate();
|
||||||
|
initializingSelection.value = false;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
initializingSelection.value = false;
|
||||||
|
appStore.showError(extractApiErrorMessage(err, t("common.error")));
|
||||||
|
} finally {
|
||||||
|
loadingList.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveTemplate() {
|
||||||
|
if (!canSave.value) {
|
||||||
|
appStore.showError(t("admin.settings.emailTemplates.validationRequired"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
const template = await adminAPI.settings.updateEmailTemplate(
|
||||||
|
selectedEvent.value,
|
||||||
|
selectedLocale.value,
|
||||||
|
{
|
||||||
|
subject: subject.value,
|
||||||
|
html: html.value,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
applyTemplate(template);
|
||||||
|
await refreshPreview();
|
||||||
|
appStore.showSuccess(t("admin.settings.emailTemplates.saveSuccess"));
|
||||||
|
} catch (err: unknown) {
|
||||||
|
appStore.showError(extractApiErrorMessage(err, t("common.error")));
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshPreview() {
|
||||||
|
if (!canPreview.value) {
|
||||||
|
previewSubject.value = "";
|
||||||
|
previewHtml.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
previewing.value = true;
|
||||||
|
try {
|
||||||
|
const preview = await adminAPI.settings.previewEmailTemplate({
|
||||||
|
event: selectedEvent.value,
|
||||||
|
locale: selectedLocale.value,
|
||||||
|
subject: subject.value,
|
||||||
|
html: html.value,
|
||||||
|
});
|
||||||
|
previewSubject.value = preview.subject;
|
||||||
|
previewHtml.value = preview.html;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
appStore.showError(extractApiErrorMessage(err, t("common.error")));
|
||||||
|
} finally {
|
||||||
|
previewing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreOfficial() {
|
||||||
|
if (!selectedEvent.value || !selectedLocale.value) return;
|
||||||
|
if (!window.confirm(t("admin.settings.emailTemplates.restoreConfirm"))) return;
|
||||||
|
|
||||||
|
restoring.value = true;
|
||||||
|
try {
|
||||||
|
const template = await adminAPI.settings.restoreOfficialEmailTemplate(
|
||||||
|
selectedEvent.value,
|
||||||
|
selectedLocale.value,
|
||||||
|
);
|
||||||
|
applyTemplate(template);
|
||||||
|
await refreshPreview();
|
||||||
|
appStore.showSuccess(t("admin.settings.emailTemplates.restoreSuccess"));
|
||||||
|
} catch (err: unknown) {
|
||||||
|
appStore.showError(extractApiErrorMessage(err, t("common.error")));
|
||||||
|
} finally {
|
||||||
|
restoring.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyPlaceholder(placeholder: string) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(placeholder);
|
||||||
|
appStore.showSuccess(t("admin.settings.emailTemplates.placeholderCopied"));
|
||||||
|
} catch {
|
||||||
|
appStore.showError(t("common.error"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([selectedEvent, selectedLocale], ([eventValue, localeValue], [oldEvent, oldLocale]) => {
|
||||||
|
if (initializingSelection.value) return;
|
||||||
|
if (!eventValue || !localeValue) return;
|
||||||
|
if (eventValue === oldEvent && localeValue === oldLocale) return;
|
||||||
|
void loadTemplate();
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void loadTemplateList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
Loading…
x
Reference in New Issue
Block a user