email-unlimit/frontend/src/App.vue

264 lines
9.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<header class="app-header">
<div class="logo">Email Unlimit</div>
<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 type="submit" class="btn btn-primary">查看收件箱</button>
<button @click="generateRandomEmail" type="button" class="btn btn-secondary">随机生成</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="刷新">
&#x21bb;
</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><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">&times;</button>
<h3>工作原理</h3>
<ol>
<li>在上面的输入框中随意编造一个以<code>@{{ domain }}</code>结尾的邮箱地址</li>
<li>使用这个地址去注册任何网站或接收邮件</li>
<li>在这里输入您刚刚使用的地址点击查看收件箱即可看到邮件</li>
</ol>
</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
name: 'App',
setup() {
const recipient = ref('');
const messages = ref([]);
const selectedMessage = ref(null);
const loading = ref(false);
const showModal = ref(false);
const copyStatus = ref('idle'); // 'idle' | 'copied'
// !!! 生产环境<E78EAF><E5A283>要提示 !!!
// 请务必将下面的 '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>