feat: add switch with zh|en
This commit is contained in:
parent
7bfb909532
commit
a186439b84
|
|
@ -1,4 +1,4 @@
|
||||||
1.在终端执行命令时使用适合Windows 的命令
|
1.在终端执行命令时使用适合Windows的命令,不要使用linux的命令
|
||||||
|
|
||||||
2.始终使用中文回答
|
2.始终使用中文回答
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,11 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
|
"pinia": "^3.0.3",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"vue": "^3.5.18"
|
"vue": "^3.5.18",
|
||||||
|
"vue-i18n": "^11.1.11",
|
||||||
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-pwa": "~5.0.0",
|
"@vue/cli-plugin-pwa": "~5.0.0",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
<link rel="icon" href="<%= BASE_URL %>logo.svg" type="image/svg+xml">
|
<link rel="icon" href="<%= BASE_URL %>logo.svg" type="image/svg+xml">
|
||||||
<link rel="manifest" href="<%= BASE_URL %>manifest.json">
|
<link rel="manifest" href="<%= BASE_URL %>manifest.json">
|
||||||
<meta name="theme-color" content="#000000">
|
<meta name="theme-color" content="#000000">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<link rel="apple-touch-icon" href="<%= BASE_URL %>img/icons/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="<%= BASE_URL %>img/icons/apple-touch-icon.png">
|
||||||
<title>临时邮件</title>
|
<title>临时邮件</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -1,264 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<header class="app-header">
|
<AppHeader />
|
||||||
<div class="logo">Email Unlimit</div>
|
<router-view />
|
||||||
<nav class="nav-links">
|
|
||||||
<a href="https://gitea.shenjianl.cn/shenjianZ/email-unlimit" target="_blank">Gitee</a>
|
|
||||||
<a href="#" @click.prevent="showHowItWorks">How it Works</a>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main id="app-main">
|
|
||||||
<section class="hero-section">
|
|
||||||
<h1>您的专属临时邮箱,无限且私密</h1>
|
|
||||||
<p>输入任何<code>@shenjianl.cn</code>地址,立即在此查看收件箱。</p>
|
|
||||||
<form @submit.prevent="fetchMessages" class="input-group">
|
|
||||||
<div class="input-wrapper">
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
v-model="recipient"
|
|
||||||
class="email-input"
|
|
||||||
placeholder="输入您的临时邮箱地址..."
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<button @click="copyEmail" type="button" class="btn-copy" title="复制地址">
|
|
||||||
<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">随机生成</button>
|
|
||||||
<button type="submit" class="btn btn-primary">查看收件箱</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="inbox-section">
|
|
||||||
<div class="inbox-container">
|
|
||||||
<div class="message-list">
|
|
||||||
<div class="message-list-header">
|
|
||||||
<h2>收件箱</h2>
|
|
||||||
<button @click="fetchMessages" class="refresh-btn" title="刷新">
|
|
||||||
↻
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="loading" class="loading-state">正在加载...</div>
|
|
||||||
<div v-else-if="messages.length === 0" class="empty-state">暂无邮件</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>请从选择一封邮件查看</p>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<div class="message-content-header">
|
|
||||||
<h3>{{ selectedMessage.subject }}</h3>
|
|
||||||
<p class="from-line"><strong>发件人:</strong> {{ selectedMessage.sender }}</p>
|
|
||||||
<p><strong>收件人:</strong> {{ selectedMessage.recipient }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="message-body">
|
|
||||||
{{ selectedMessage.body }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- How it Works Modal -->
|
|
||||||
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<button @click="closeModal" class="close-btn">×</button>
|
|
||||||
<h3>工作原理</h3>
|
|
||||||
<ol>
|
|
||||||
<li>在上面的输入框中,随意编造一个以<code>@{{ domain }}</code>结尾的邮箱地址。</li>
|
|
||||||
<li>使用这个地址去注册任何网站或接收邮件。</li>
|
|
||||||
<li>在这里输入您刚刚使用的地址,点击“查看收件箱”,即可看到邮件。</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref, onMounted } from 'vue';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import AppHeader from './components/AppHeader.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
|
components: {
|
||||||
|
AppHeader,
|
||||||
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const recipient = ref('');
|
// 在根组件中建立 i18n 上下文,以供所有子路由继承
|
||||||
const messages = ref([]);
|
useI18n();
|
||||||
const selectedMessage = ref(null);
|
|
||||||
const loading = ref(false);
|
|
||||||
const showModal = ref(false);
|
|
||||||
const copyStatus = ref('idle'); // 'idle' | 'copied'
|
|
||||||
// !!! 生产环境<EFBFBD><EFBFBD>要提示 !!!
|
|
||||||
// 请务必将下面的 'yourdomain.com' 替换为您的真实域名
|
|
||||||
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 {
|
|
||||||
// API URL 已修改为相对路径,以适配 Nginx 反向代理
|
|
||||||
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;
|
|
||||||
// Automatically select the first message if available
|
|
||||||
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)];
|
|
||||||
|
|
||||||
// Randomly pick 2 lists to combine words from
|
|
||||||
const listA = getRandomItem(allLists);
|
|
||||||
const listB = getRandomItem(allLists);
|
|
||||||
|
|
||||||
const word1 = getRandomItem(listA);
|
|
||||||
const word2 = getRandomItem(listB);
|
|
||||||
|
|
||||||
const number = Math.floor(Math.random() * 9000) + 1000; // Random 4-digit number
|
|
||||||
|
|
||||||
// Randomly choose a separator
|
|
||||||
const separators = ['.', '-', '_', ''];
|
|
||||||
const separator = getRandomItem(separators);
|
|
||||||
|
|
||||||
let prefix;
|
|
||||||
if (word1 === word2) {
|
|
||||||
// Avoids "alex.alex1234" if the same list and word are picked
|
|
||||||
prefix = `${word1}${number}`;
|
|
||||||
} else {
|
|
||||||
prefix = `${word1}${separator}${word2}${number}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
recipient.value = `${prefix}@${domain}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyEmail = () => {
|
|
||||||
if (!recipient.value) return;
|
|
||||||
|
|
||||||
const textToCopy = recipient.value;
|
|
||||||
|
|
||||||
// Modern browsers in secure contexts
|
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
|
||||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
|
||||||
showCopySuccess();
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('Modern copy failed: ', err);
|
|
||||||
fallbackCopy(textToCopy); // Try fallback on error
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Fallback for older browsers or insecure contexts
|
|
||||||
fallbackCopy(textToCopy);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fallbackCopy = (text) => {
|
|
||||||
const textArea = document.createElement('textarea');
|
|
||||||
textArea.value = text;
|
|
||||||
|
|
||||||
// Make the textarea out of sight
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
const showHowItWorks = () => {
|
|
||||||
showModal.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
showModal.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
recipient,
|
|
||||||
messages,
|
|
||||||
selectedMessage,
|
|
||||||
loading,
|
|
||||||
showModal,
|
|
||||||
copyStatus,
|
|
||||||
domain,
|
|
||||||
fetchMessages,
|
|
||||||
selectMessage,
|
|
||||||
generateRandomEmail,
|
|
||||||
copyEmail,
|
|
||||||
showHowItWorks,
|
|
||||||
closeModal,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -65,7 +65,6 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-links a {
|
.nav-links a {
|
||||||
margin-left: 2rem;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--dark-grey);
|
color: var(--dark-grey);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg t="1753714773086" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2330" width="64" height="64"><path d="M400.896 261.156571l20.041143-100.205714A18.285714 18.285714 0 0 1 438.857143 146.285714h146.285714a18.285714 18.285714 0 0 1 17.92 14.701715l20.041143 100.169142c17.846857 7.899429 34.779429 17.700571 50.541714 29.220572l96.804572-32.768a18.285714 18.285714 0 0 1 21.686857 8.192l73.142857 126.683428a18.285714 18.285714 0 0 1-3.766857 22.893715l-76.763429 67.474285a277.211429 277.211429 0 0 1 0 58.294858l76.8 67.474285a18.285714 18.285714 0 0 1 3.730286 22.893715l-73.142857 126.683428a18.285714 18.285714 0 0 1-21.686857 8.192l-96.804572-32.768c-15.762286 11.52-32.694857 21.321143-50.541714 29.220572l-20.041143 100.205714A18.285714 18.285714 0 0 1 585.142857 877.714286h-146.285714a18.285714 18.285714 0 0 1-17.92-14.701715l-20.041143-100.169142a273.993143 273.993143 0 0 1-50.541714-29.220572l-96.804572 32.768a18.285714 18.285714 0 0 1-21.686857-8.192l-73.142857-126.683428a18.285714 18.285714 0 0 1 3.766857-22.893715l76.763429-67.474285a277.211429 277.211429 0 0 1 0-58.294858l-76.8-67.474285a18.285714 18.285714 0 0 1-3.730286-22.893715l73.142857-126.683428a18.285714 18.285714 0 0 1 21.686857-8.192l96.804572 32.768c15.762286-11.52 32.694857-21.321143 50.541714-29.220572zM603.428571 512a91.428571 91.428571 0 1 0-182.857142 0 91.428571 91.428571 0 0 0 182.857142 0z m36.571429 0a128 128 0 1 1-256 0 128 128 0 0 1 256 0z m-205.165714-234.166857a18.285714 18.285714 0 0 1-11.117715 13.385143 237.421714 237.421714 0 0 0-58.697142 33.938285 18.285714 18.285714 0 0 1-17.188572 2.962286l-91.794286-31.085714-58.148571 100.754286 72.777143 63.963428a18.285714 18.285714 0 0 1 6.034286 16.310857 239.908571 239.908571 0 0 0 0 67.876572 18.285714 18.285714 0 0 1-6.034286 16.310857l-72.777143 63.963428L256 726.930286l91.794286-31.085715a18.285714 18.285714 0 0 1 17.188571 2.998858 237.421714 237.421714 0 0 0 58.697143 33.938285 18.285714 18.285714 0 0 1 11.154286 13.385143L453.851429 841.142857h116.297142l19.017143-94.976a18.285714 18.285714 0 0 1 11.117715-13.385143 237.421714 237.421714 0 0 0 58.697142-33.938285 18.285714 18.285714 0 0 1 17.188572-2.962286l91.794286 31.085714 58.148571-100.754286-72.777143-63.963428a18.285714 18.285714 0 0 1-6.034286-16.310857 239.908571 239.908571 0 0 0 0-67.876572 18.285714 18.285714 0 0 1 6.034286-16.310857l72.777143-63.963428L768 297.069714l-91.794286 31.085715a18.285714 18.285714 0 0 1-17.188571-2.998858 237.421714 237.421714 0 0 0-58.697143-33.938285 18.285714 18.285714 0 0 1-11.154286-13.385143L570.148571 182.857143h-116.297142l-19.017143 94.976z" fill="#000000" p-id="2331"></path></svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
|
|
@ -0,0 +1,48 @@
|
||||||
|
<template>
|
||||||
|
<header class="app-header">
|
||||||
|
<router-link to="/" class="logo-link">
|
||||||
|
<div class="logo">Email Unlimit</div>
|
||||||
|
</router-link>
|
||||||
|
<nav class="nav-links">
|
||||||
|
<a href="https://gitea.shenjianl.cn/shenjianZ/email-unlimit" target="_blank">Gitee</a>
|
||||||
|
<router-link to="/settings" class="settings-link" title="设置">
|
||||||
|
<img src="@/assets/setting.svg" alt="Settings" class="settings-icon" />
|
||||||
|
</router-link>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'AppHeader',
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.logo-link {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
transition: filter 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-link:hover .settings-icon {
|
||||||
|
/* Generated by https://codepen.io/sosuke/pen/Pjoqqp */
|
||||||
|
/* This filter converts black to the --primary-purple color */
|
||||||
|
filter: brightness(0) saturate(100%) invert(20%) sepia(80%) saturate(3750%) hue-rotate(261deg) brightness(95%) contrast(94%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { createI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
zh: {
|
||||||
|
howItWorks: {
|
||||||
|
title: '工作原理',
|
||||||
|
step1: '在上面的输入框中,随意编造一个以 {domain} 结尾的邮箱地址。',
|
||||||
|
step2: '使用这个地址去注册任何网站或接收邮件。',
|
||||||
|
step3: '在这里输入您刚刚使用的地址,点击“查看收件箱”,即可看到邮件。',
|
||||||
|
},
|
||||||
|
language: '语言',
|
||||||
|
home: {
|
||||||
|
title: '您的专属临时邮箱,无限且私密',
|
||||||
|
subtitle: '输入任何',
|
||||||
|
subtitleAfter: '地址,立即在此查看收件箱。',
|
||||||
|
placeholder: '输入您的临时邮箱地址...',
|
||||||
|
copyTitle: '复制地址',
|
||||||
|
random: '随机生成',
|
||||||
|
checkInbox: '查看收件箱',
|
||||||
|
inbox: '收件箱',
|
||||||
|
refresh: '刷新',
|
||||||
|
loading: '正在加载...',
|
||||||
|
noMail: '暂无邮件',
|
||||||
|
selectMail: '请从选择一封邮件查看',
|
||||||
|
from: '发件人',
|
||||||
|
to: '收件人',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
howItWorks: {
|
||||||
|
title: 'How it Works',
|
||||||
|
step1: 'Invent any email address ending in {domain} in the input box above.',
|
||||||
|
step2: 'Use this address to register on any website or receive emails.',
|
||||||
|
step3: 'Enter the address you just used here and click "Check Inbox" to see the emails.',
|
||||||
|
},
|
||||||
|
language: 'Language',
|
||||||
|
home: {
|
||||||
|
title: 'Your Exclusive, Unlimited, and Private Temporary Email',
|
||||||
|
subtitle: 'Enter any address ending in ',
|
||||||
|
subtitleAfter: ' to check the inbox right here.',
|
||||||
|
placeholder: 'Enter your temporary email address...',
|
||||||
|
copyTitle: 'Copy Address',
|
||||||
|
random: 'Generate Random',
|
||||||
|
checkInbox: 'Check Inbox',
|
||||||
|
inbox: 'Inbox',
|
||||||
|
refresh: 'Refresh',
|
||||||
|
loading: 'Loading...',
|
||||||
|
noMail: 'No mail yet',
|
||||||
|
selectMail: 'Please select an email to view',
|
||||||
|
from: 'From',
|
||||||
|
to: 'To',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
legacy: false, // 使用组合式 API
|
||||||
|
locale: 'zh', // 设置默认语言
|
||||||
|
fallbackLocale: 'en', // 设置回退语言
|
||||||
|
messages, // 设置语言环境信息
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
|
|
@ -1,6 +1,30 @@
|
||||||
import { createApp } from 'vue'
|
import { createApp, watch } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import './assets/main.css' // 引入新的全局样式文件
|
|
||||||
import './registerServiceWorker'
|
import './registerServiceWorker'
|
||||||
|
import router from './router'
|
||||||
|
import i18n from './i18n'
|
||||||
|
import { useLanguageStore } from './stores/language'
|
||||||
|
import './assets/main.css'
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
const app = createApp(App)
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
|
app.use(pinia)
|
||||||
|
app.use(router)
|
||||||
|
app.use(i18n)
|
||||||
|
|
||||||
|
const languageStore = useLanguageStore(pinia)
|
||||||
|
|
||||||
|
// 监听 Pinia store 中的 locale 变化,并更新 i18n
|
||||||
|
watch(
|
||||||
|
() => languageStore.locale,
|
||||||
|
(newLocale) => {
|
||||||
|
i18n.global.locale.value = newLocale
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 初始化时,确保 i18n 的 locale 与 store 同步
|
||||||
|
i18n.global.locale.value = languageStore.locale
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
|
import Home from '../views/Home.vue';
|
||||||
|
import Settings from '../views/Settings.vue';
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
component: Home,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
name: 'Settings',
|
||||||
|
component: Settings,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(process.env.BASE_URL),
|
||||||
|
routes,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'user_language';
|
||||||
|
|
||||||
|
export const useLanguageStore = defineStore('language', {
|
||||||
|
state: () => ({
|
||||||
|
// 优先从 localStorage 获取,否则默认为中文
|
||||||
|
locale: localStorage.getItem(STORAGE_KEY) || 'zh',
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setLocale(newLocale) {
|
||||||
|
this.locale = newLocale;
|
||||||
|
// 将新设置存入 localStorage
|
||||||
|
localStorage.setItem(STORAGE_KEY, newLocale);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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')">
|
||||||
|
↻
|
||||||
|
</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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -17,5 +17,15 @@ module.exports = defineConfig({
|
||||||
// swSrc: 'dev/sw.js',
|
// swSrc: 'dev/sw.js',
|
||||||
// ...other Workbox options...
|
// ...other Workbox options...
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
chainWebpack: config => {
|
||||||
|
config.plugin('define').tap(definitions => {
|
||||||
|
Object.assign(definitions[0], {
|
||||||
|
__VUE_OPTIONS_API__: true,
|
||||||
|
__VUE_PROD_DEVTOOLS__: false,
|
||||||
|
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false
|
||||||
|
})
|
||||||
|
return definitions
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
Loading…
Reference in New Issue