feat: add switch with zh|en

This commit is contained in:
2025-07-28 23:36:33 +08:00
parent 7bfb909532
commit a186439b84
14 changed files with 536 additions and 260 deletions

221
frontend/src/views/Home.vue Normal file
View File

@@ -0,0 +1,221 @@
<template>
<main id="app-main">
<section class="hero-section">
<h1>{{ $t('home.title') }}</h1>
<p>{{ $t('home.subtitle') }}<code>@shenjianl.cn</code>{{ $t('home.subtitleAfter') }}</p>
<form @submit.prevent="fetchMessages" class="input-group">
<div class="input-wrapper">
<input
type="email"
v-model="recipient"
class="email-input"
:placeholder="$t('home.placeholder')"
required
/>
<button @click="copyEmail" type="button" class="btn-copy" :title="$t('home.copyTitle')">
<span v-if="copyStatus === 'copied'"></span>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z"/>
<path d="M2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1h-1v1a.5.5 0 0 1-.5.5H2.5a.5.5 0 0 1-.5-.5V6.5a.5.5 0 0 1 .5-.5H3v-1z"/>
</svg>
</button>
</div>
<button @click="generateRandomEmail" type="button" class="btn btn-secondary">{{ $t('home.random') }}</button>
<button type="submit" class="btn btn-primary">{{ $t('home.checkInbox') }}</button>
</form>
</section>
<section class="inbox-section">
<div class="inbox-container">
<div class="message-list">
<div class="message-list-header">
<h2>{{ $t('home.inbox') }}</h2>
<button @click="fetchMessages" class="refresh-btn" :title="$t('home.refresh')">
&#x21bb;
</button>
</div>
<div v-if="loading" class="loading-state">{{ $t('home.loading') }}</div>
<div v-else-if="messages.length === 0" class="empty-state">{{ $t('home.noMail') }}</div>
<div v-else>
<div
v-for="message in messages"
:key="message.id"
class="message-item"
:class="{ selected: selectedMessage && selectedMessage.id === message.id }"
@click="selectMessage(message)"
>
<div class="from">{{ message.sender }}</div>
<div class="subject">{{ message.subject }}</div>
</div>
</div>
</div>
<div class="message-detail">
<div v-if="!selectedMessage" class="empty-state">
<p>{{ $t('home.selectMail') }}</p>
</div>
<div v-else>
<div class="message-content-header">
<h3>{{ selectedMessage.subject }}</h3>
<p class="from-line"><strong>{{ $t('home.from') }}:</strong> {{ selectedMessage.sender }}</p>
<p><strong>{{ $t('home.to') }}:</strong> {{ selectedMessage.recipient }}</p>
</div>
<div class="message-body">
{{ selectedMessage.body }}
</div>
</div>
</div>
</div>
</section>
</main>
</template>
<script>
import { ref } from 'vue';
export default {
name: 'Home',
setup() {
const recipient = ref('');
const messages = ref([]);
const selectedMessage = ref(null);
const loading = ref(false);
const copyStatus = ref('idle'); // 'idle' | 'copied'
const domain = 'shenjianl.cn';
const fetchMessages = async () => {
if (!recipient.value) {
alert('请输入一个邮箱地址');
return;
}
loading.value = true;
selectedMessage.value = null; // Clear selected message on new fetch
try {
const response = await fetch(`/api/messages?recipient=${recipient.value}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
messages.value = data;
if (messages.value.length > 0) {
selectedMessage.value = messages.value[0];
}
} catch (error) {
console.error('Failed to fetch messages:', error);
alert('无法获取邮件,请检查后端服务和 Nginx 配置是否正常。');
} finally {
loading.value = false;
}
};
const selectMessage = (message) => {
selectedMessage.value = message;
};
const generateRandomEmail = () => {
const names = [
'alex', 'casey', 'morgan', 'jordan', 'taylor', 'jamie', 'ryan', 'drew', 'jesse', 'pat',
'chris', 'dylan', 'aaron', 'blake', 'cameron', 'devon', 'elliot', 'finn', 'gray', 'harper',
'kai', 'logan', 'max', 'noah', 'owen', 'quinn', 'riley', 'rowan', 'sage', 'skyler'
];
const places = [
'tokyo', 'paris', 'london', 'cairo', 'sydney', 'rio', 'moscow', 'rome', 'nile', 'everest',
'sahara', 'amazon', 'gobi', 'andes', 'pacific', 'kyoto', 'berlin', 'dubai', 'seoul', 'milan',
'vienna', 'prague', 'athens', 'lisbon', 'oslo', 'helsinki', 'zürich', 'geneva', 'brussels', 'amsterdam'
];
const concepts = [
'apollo', 'artemis', 'athena', 'zeus', 'thor', 'loki', 'odin', 'freya', 'phoenix', 'dragon',
'griffin', 'sphinx', 'pyramid', 'colossus', 'acropolis', 'obelisk', 'pagoda', 'castle', 'cyberspace', 'matrix',
'protocol', 'algorithm', 'pixel', 'vector', 'photon', 'quark', 'nova', 'pulsar', 'saga', 'voyage',
'enigma', 'oracle', 'cipher', 'vortex', 'helix', 'axiom', 'zenith', 'epoch', 'nexus', 'trinity'
];
const allLists = [names, places, concepts];
const getRandomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
const listA = getRandomItem(allLists);
const listB = getRandomItem(allLists);
const word1 = getRandomItem(listA);
const word2 = getRandomItem(listB);
const number = Math.floor(Math.random() * 9000) + 1000;
const separators = ['.', '-', '_', ''];
const separator = getRandomItem(separators);
let prefix;
if (word1 === word2) {
prefix = `${word1}${number}`;
} else {
prefix = `${word1}${separator}${word2}${number}`;
}
recipient.value = `${prefix}@${domain}`;
};
const copyEmail = () => {
if (!recipient.value) return;
const textToCopy = recipient.value;
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(textToCopy).then(() => {
showCopySuccess();
}).catch(err => {
console.error('Modern copy failed: ', err);
fallbackCopy(textToCopy);
});
} else {
fallbackCopy(textToCopy);
}
};
const fallbackCopy = (text) => {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.top = '-9999px';
textArea.style.left = '-9999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
if (successful) {
showCopySuccess();
} else {
throw new Error('Fallback copy was unsuccessful');
}
} catch (err) {
console.error('Fallback copy failed: ', err);
alert('复制失败!');
}
document.body.removeChild(textArea);
};
const showCopySuccess = () => {
copyStatus.value = 'copied';
setTimeout(() => {
copyStatus.value = 'idle';
}, 2000);
};
return {
recipient,
messages,
selectedMessage,
loading,
copyStatus,
domain,
fetchMessages,
selectMessage,
generateRandomEmail,
copyEmail,
};
},
};
</script>

View File

@@ -0,0 +1,110 @@
<template>
<div class="settings-page">
<div class="settings-container">
<h2>{{ $t('howItWorks.title') }}</h2>
<ol>
<i18n-t keypath="howItWorks.step1" tag="li">
<template #domain>
<code>@{{ 'shenjianl.cn' }}</code>
</template>
</i18n-t>
<li>{{ $t('howItWorks.step2') }}</li>
<li>{{ $t('howItWorks.step3') }}</li>
</ol>
<div class="language-switcher">
<h3>{{ $t('language') }}</h3>
<button @click="setLanguage('zh')" :class="{ active: currentLocale === 'zh' }">中文</button>
<button @click="setLanguage('en')" :class="{ active: currentLocale === 'en' }">English</button>
</div>
</div>
</div>
</template>
<script>
import { useLanguageStore } from '@/stores/language';
import { computed } from 'vue';
export default {
name: 'Settings',
setup() {
const languageStore = useLanguageStore();
const setLanguage = (lang) => {
languageStore.setLocale(lang);
};
const currentLocale = computed(() => languageStore.locale);
return {
setLanguage,
currentLocale,
};
},
};
</script>
<style scoped>
.settings-page {
padding: 2rem;
display: flex;
justify-content: center;
}
.settings-container {
width: 100%;
max-width: 800px;
background-color: var(--light-grey);
padding: 2rem;
border-radius: var(--border-radius);
box-shadow: var(--card-shadow);
}
h2 {
color: var(--primary-purple);
margin-top: 0;
}
ol {
padding-left: 1.5rem;
line-height: 1.8;
font-size: 1.05rem;
color: var(--dark-grey);
}
code {
background-color: var(--light-purple);
color: var(--text-light);
padding: 0.3em 0.6em;
border-radius: 6px;
font-family: monospace;
font-weight: 500;
}
.language-switcher {
margin-top: 2rem;
border-top: 1px solid var(--medium-grey);
padding-top: 1.5rem;
}
.language-switcher h3 {
margin-top: 0;
color: var(--dark-grey);
}
.language-switcher button {
margin-right: 1rem;
padding: 0.5rem 1rem;
border: 1px solid var(--medium-grey);
background-color: #ffffff;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s, color 0.3s;
}
.language-switcher button.active {
background-color: var(--primary-purple);
color: var(--text-light);
border-color: var(--primary-purple);
}
</style>