mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-06 20:32:55 +08:00
* feat(i18n): enhance locale handling and routing - Implemented dynamic locale aliases in router configuration. - Added support for preferred locale storage in global state. - Improved locale resolution logic in router beforeEach guard. - Created utility functions for locale management and path manipulation. - Added tests for locale matching and message extraction. - Updated Header component to allow language selection. - Refactored getRouterPathWithLang to utilize new locale utilities. - Updated Vite configuration to support aliasing for vue-i18n. - Bumped version numbers across various packages to 1.9.0. * feat(i18n): update version to 1.8.0 and enhance locale handling - Updated version numbers across all package.json files to 1.8.0. - Enhanced locale handling in App.vue by centralizing locale configurations. - Improved Turnstile component to support dynamic language rendering. - Refactored i18n utilities to include initial locale setup and empty locale messages. - Updated i18n.ts to utilize the new locale management structure. - Added naive-locale.ts for better integration with Naive UI's locale handling. - Adjusted Header.vue to streamline language selection and locale changes. - Fixed translations in multiple locale files for consistency and accuracy. * fix(i18n): address review feedback * feat(i18n): update default locale to English and enhance language handling in components * fix(i18n): switch locale selector to dropdown * docs: add topbar language and github order design spec * fix(i18n): 修复 Header 语言切换器相关问题,恢复为独立控件并调整样式 * Refactor locale handling in router and add locale-guard utility functions - Improved locale resolution logic in router by introducing utility functions for better readability and maintainability. - Added `locale-guard.js` to encapsulate locale-related functions such as getting route locale, resolving locale for navigation, and applying locale navigation state. - Updated JWT synchronization logic to streamline the handling of JWT from query parameters. - Modified i18n messages test to check for coverage of registered locale message keys instead of extracting English source messages. * 删除顶部栏语言和GitHub顺序设计文档 * fix: 修复前端设置初始化时未返回 domains 数组导致的 undefined 错误 * refactor(i18n): consolidate locale infrastructure * fix(i18n): stabilize locale route switching * fix(i18n): persist default locale selection * fix(i18n): 修复前端设置初始化时未返回 domains 数组导致的 undefined 错误,统一按空数组兜底处理 feat(i18n): 添加 locale 别名处理,支持默认语言的重定向 test(i18n): 增加对默认语言别名重定向的测试用例 * refactor: replace useAppI18n with useScopedI18n in multiple components for improved localization management * fix(tests): 移除不必要的 URL 断言以简化 Passkey 测试 * fix(i18n): 更新语言切换逻辑,确保使用当前语言设置进行路由导航 * fix(i18n): 强制路由切换以确保语言切换后正确导航 * refactor(i18n): 优化消息注册和路由本地化逻辑,移除冗余代码 * refactor(i18n): 拆分 API 文件以优化路由管理,更新语言处理逻辑 * fix: align i18n release notes and frontend test script
252 lines
8.1 KiB
Vue
252 lines
8.1 KiB
Vue
<script setup>
|
|
import '@wangeditor/editor/dist/css/style.css'
|
|
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
|
import { useScopedI18n } from '@/i18n/app'
|
|
import { onMounted, onBeforeUnmount, ref, shallowRef } from 'vue'
|
|
import AdminContact from '../common/AdminContact.vue'
|
|
|
|
import { useGlobalState } from '../../store'
|
|
import { api } from '../../api'
|
|
|
|
const message = useMessage()
|
|
const isPreview = ref(false)
|
|
const editorRef = shallowRef()
|
|
const sending = ref(false)
|
|
|
|
|
|
const { settings, sendMailModel, indexTab, userSettings } = useGlobalState()
|
|
|
|
const { t } = useScopedI18n('views.index.SendMail')
|
|
|
|
const contentTypes = [
|
|
{ label: t('text'), value: 'text' },
|
|
{ label: t('html'), value: 'html' },
|
|
{ label: t('rich text'), value: 'rich' },
|
|
]
|
|
|
|
const normalizeSendMailText = (content) => {
|
|
return content
|
|
.replace(/[\u00AD\u200B-\u200D\u2060\uFEFF]/g, '')
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
}
|
|
|
|
const hasSendMailContent = (content, contentType) => {
|
|
if (typeof content !== 'string' || !content) {
|
|
return false
|
|
}
|
|
|
|
if (contentType === 'text') {
|
|
return normalizeSendMailText(content).length > 0
|
|
}
|
|
|
|
const container = document.createElement('div')
|
|
container.innerHTML = content
|
|
container.querySelectorAll('script, style, noscript, template').forEach((node) => node.remove())
|
|
|
|
const plainContent = normalizeSendMailText(container.textContent ?? '')
|
|
if (plainContent.length > 0) {
|
|
return true
|
|
}
|
|
|
|
return Boolean(container.querySelector('img, audio, video, iframe, svg, canvas, table'))
|
|
}
|
|
|
|
const send = async () => {
|
|
if (sending.value) {
|
|
return
|
|
}
|
|
|
|
const subject = `${sendMailModel.value.subject ?? ''}`.trim()
|
|
const toMail = `${sendMailModel.value.toMail ?? ''}`.trim()
|
|
const content = `${sendMailModel.value.content ?? ''}`
|
|
|
|
if (!subject) {
|
|
message.error(t('subjectEmpty'))
|
|
return
|
|
}
|
|
if (!toMail) {
|
|
message.error(t('toMailEmpty'))
|
|
return
|
|
}
|
|
if (!hasSendMailContent(content, sendMailModel.value.contentType)) {
|
|
message.error(t('contentEmpty'))
|
|
return
|
|
}
|
|
|
|
const payload = {
|
|
from_name: sendMailModel.value.fromName,
|
|
to_name: sendMailModel.value.toName,
|
|
to_mail: toMail,
|
|
subject,
|
|
is_html: sendMailModel.value.contentType != 'text',
|
|
content,
|
|
}
|
|
|
|
sending.value = true
|
|
try {
|
|
await api.fetch(`/api/send_mail`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(payload)
|
|
})
|
|
sendMailModel.value = {
|
|
fromName: "",
|
|
toName: "",
|
|
toMail: "",
|
|
subject: "",
|
|
contentType: 'text',
|
|
content: "",
|
|
}
|
|
isPreview.value = false
|
|
message.success(t("successSend"));
|
|
indexTab.value = 'sendbox'
|
|
} catch (error) {
|
|
message.error(error.message || "error");
|
|
} finally {
|
|
sending.value = false
|
|
}
|
|
}
|
|
|
|
const requestAccess = async () => {
|
|
try {
|
|
await api.fetch(`/api/request_send_mail_access`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify({})
|
|
}
|
|
)
|
|
message.success(t("success"))
|
|
await api.getSettings();
|
|
} catch (error) {
|
|
message.error(error.message || "error");
|
|
}
|
|
}
|
|
|
|
const toolbarConfig = {
|
|
excludeKeys: ["uploadVideo"]
|
|
}
|
|
|
|
const editorConfig = {
|
|
MENU_CONF: {
|
|
'uploadImage': {
|
|
async customUpload() {
|
|
message.error(t('tooLarge'))
|
|
},
|
|
maxFileSize: 1 * 1024 * 1024,
|
|
base64LimitSize: 1 * 1024 * 1024,
|
|
}
|
|
}
|
|
}
|
|
|
|
onBeforeUnmount(() => {
|
|
const editor = editorRef.value
|
|
if (editor == null) return
|
|
editor.destroy()
|
|
})
|
|
|
|
const handleCreated = (editor) => {
|
|
editorRef.value = editor;
|
|
}
|
|
|
|
onMounted(async () => {
|
|
// make sure user_id is fetched
|
|
if (!userSettings.value.user_id) await api.getUserSettings(message);
|
|
await api.getSettings();
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="center" v-if="settings.address">
|
|
<n-card :bordered="false" embedded>
|
|
<div v-if="!settings.send_balance || settings.send_balance <= 0">
|
|
<n-alert type="warning" :show-icon="false" :bordered="false">
|
|
{{ t('requestAccessTip') }}
|
|
<n-button type="primary" tertiary @click="requestAccess" size="small">{{ t('requestAccess')
|
|
}}</n-button>
|
|
</n-alert>
|
|
<AdminContact />
|
|
</div>
|
|
<div v-else>
|
|
<n-alert type="info" :show-icon="false" :bordered="false" closable>
|
|
{{ t('send_balance') }}: {{ settings.send_balance }}
|
|
</n-alert>
|
|
<n-flex justify="end">
|
|
<n-button type="primary" :loading="sending" :disabled="sending" @click="send">{{ t('send') }}</n-button>
|
|
</n-flex>
|
|
<div class="left">
|
|
<n-form :model="sendMailModel">
|
|
<n-form-item :label="t('fromName')" label-placement="top">
|
|
<n-input-group>
|
|
<n-input v-model:value="sendMailModel.fromName" />
|
|
<n-input :value="settings.address" disabled />
|
|
</n-input-group>
|
|
</n-form-item>
|
|
<n-form-item :label="t('toName')" label-placement="top">
|
|
<n-input-group>
|
|
<n-input v-model:value="sendMailModel.toName" />
|
|
<n-input v-model:value="sendMailModel.toMail" />
|
|
</n-input-group>
|
|
</n-form-item>
|
|
<n-form-item :label="t('subject')" label-placement="top">
|
|
<n-input v-model:value="sendMailModel.subject" />
|
|
</n-form-item>
|
|
<n-form-item :label="t('options')" label-placement="top">
|
|
<n-radio-group v-model:value="sendMailModel.contentType">
|
|
<n-radio-button v-for="option in contentTypes" :key="option.value" :value="option.value"
|
|
:label="option.label" />
|
|
</n-radio-group>
|
|
<n-button v-if="sendMailModel.contentType != 'text'" @click="isPreview = !isPreview"
|
|
style="margin-left: 10px;">
|
|
{{ isPreview ? t('edit') : t('preview') }}
|
|
</n-button>
|
|
</n-form-item>
|
|
<n-form-item :label="t('content')" label-placement="top">
|
|
<n-card :bordered="false" embedded v-if="isPreview">
|
|
<div v-html="sendMailModel.content" />
|
|
</n-card>
|
|
<div v-else-if="sendMailModel.contentType == 'rich'" style="border: 1px solid #ccc">
|
|
<Toolbar style="border-bottom: 1px solid #ccc" :defaultConfig="toolbarConfig"
|
|
:editor="editorRef" mode="default" />
|
|
<Editor style="height: 500px; overflow-y: hidden;" v-model="sendMailModel.content"
|
|
:defaultConfig="editorConfig" mode="default" @onCreated="handleCreated" />
|
|
</div>
|
|
<n-input v-else type="textarea" v-model:value="sendMailModel.content" :autosize="{
|
|
minRows: 3
|
|
}" />
|
|
</n-form-item>
|
|
</n-form>
|
|
</div>
|
|
</div>
|
|
</n-card>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.n-card {
|
|
max-width: 800px;
|
|
}
|
|
|
|
.n-button {
|
|
text-align: left;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.center {
|
|
display: flex;
|
|
text-align: center;
|
|
place-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.left {
|
|
text-align: left;
|
|
place-items: left;
|
|
justify-content: left;
|
|
}
|
|
|
|
.n-alert {
|
|
margin-bottom: 10px;
|
|
}
|
|
</style>
|