feat: Enhance backend with request/SQL logging via morgan/winston and fix frontend by correctly rendering email Markdown content using the 'marked' library.
This commit is contained in:
@@ -13,11 +13,8 @@
|
||||
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>
|
||||
<el-icon v-if="copyStatus === 'copied'"><Check /></el-icon>
|
||||
<el-icon v-else><CopyDocument /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
<button @click="generateRandomEmail" type="button" class="btn btn-secondary">{{ $t('home.random') }}</button>
|
||||
@@ -30,9 +27,17 @@
|
||||
<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 class="header-actions">
|
||||
<button @click="clearInbox" :title="$t('home.clearInbox')" :disabled="messages.length === 0">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</button>
|
||||
<button @click="fetchMessages" :title="$t('home.refresh')">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="newMailNotification" class="new-mail-notification">
|
||||
{{ $t('home.newMailNotification') }}
|
||||
</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>
|
||||
@@ -55,12 +60,27 @@
|
||||
</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 class="header-text">
|
||||
<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="header-actions">
|
||||
<button @click="deleteMessage(selectedMessage.id)" :title="$t('home.delete')">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message-body">
|
||||
{{ selectedMessage.body }}
|
||||
<div class="message-body" v-html="renderedBody">
|
||||
</div>
|
||||
<div v-if="attachmentsLoading" class="loading-state">{{ $t('home.loading') }}</div>
|
||||
<div v-if="attachments.length > 0" class="attachments-section">
|
||||
<h4>{{ $t('home.attachments') }}</h4>
|
||||
<ul>
|
||||
<li v-for="attachment in attachments" :key="attachment.id">
|
||||
<a :href="`/api/attachments/${attachment.id}`" :download="attachment.filename">{{ attachment.filename }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,153 +89,285 @@
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
<script setup>
|
||||
import { ref, onUnmounted, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Delete, Refresh, CopyDocument, Check } from '@element-plus/icons-vue';
|
||||
import { marked } from 'marked';
|
||||
|
||||
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 { t } = useI18n();
|
||||
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 attachments = ref([]);
|
||||
const attachmentsLoading = ref(false);
|
||||
const newMailNotification = ref(false);
|
||||
|
||||
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 renderedBody = computed(() => {
|
||||
if (selectedMessage.value && selectedMessage.value.body) {
|
||||
return marked(selectedMessage.value.body);
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const selectMessage = (message) => {
|
||||
selectedMessage.value = message;
|
||||
};
|
||||
let ws = null;
|
||||
|
||||
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 setupWebSocket = () => {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
|
||||
const allLists = [names, places, concepts];
|
||||
const getRandomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
||||
if (!recipient.value) return;
|
||||
|
||||
const listA = getRandomItem(allLists);
|
||||
const listB = getRandomItem(allLists);
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${wsProtocol}//${window.location.host}/ws?recipient=${encodeURIComponent(recipient.value)}`;
|
||||
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
const word1 = getRandomItem(listA);
|
||||
const word2 = getRandomItem(listB);
|
||||
|
||||
const number = Math.floor(Math.random() * 9000) + 1000;
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connection established');
|
||||
};
|
||||
|
||||
const separators = ['.', '-', '_', ''];
|
||||
const separator = getRandomItem(separators);
|
||||
ws.onmessage = (event) => {
|
||||
const newEmail = JSON.parse(event.data);
|
||||
messages.value.unshift(newEmail);
|
||||
ElMessage.success(t('home.newMailNotification'));
|
||||
};
|
||||
|
||||
let prefix;
|
||||
if (word1 === word2) {
|
||||
prefix = `${word1}${number}`;
|
||||
} else {
|
||||
prefix = `${word1}${separator}${word2}${number}`;
|
||||
}
|
||||
|
||||
recipient.value = `${prefix}@${domain}`;
|
||||
};
|
||||
|
||||
const copyEmail = () => {
|
||||
if (!recipient.value) return;
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
},
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket connection closed');
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
onUnmounted(() => {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
|
||||
const fetchMessages = async () => {
|
||||
if (!recipient.value) {
|
||||
ElMessage.warning(t('home.alerts.enterEmail'));
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
selectedMessage.value = null;
|
||||
attachments.value = [];
|
||||
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) {
|
||||
selectMessage(messages.value[0]);
|
||||
}
|
||||
setupWebSocket(); // Setup WebSocket after fetching messages
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch messages:', error);
|
||||
ElMessage.error(t('home.alerts.fetchFailed'));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAttachments = async (messageId) => {
|
||||
attachmentsLoading.value = true;
|
||||
attachments.value = [];
|
||||
try {
|
||||
const response = await fetch(`/api/messages/${messageId}/attachments`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const data = await response.json();
|
||||
attachments.value = data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch attachments:', error);
|
||||
} finally {
|
||||
attachmentsLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const selectMessage = (message) => {
|
||||
selectedMessage.value = message;
|
||||
if (message) {
|
||||
fetchAttachments(message.id);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteMessage = async (messageId) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
t('home.alerts.confirmDelete'),
|
||||
t('home.delete'),
|
||||
{
|
||||
confirmButtonText: 'OK',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'warning',
|
||||
}
|
||||
);
|
||||
|
||||
const response = await fetch(`/api/messages/${messageId}`, { method: 'DELETE' });
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
const index = messages.value.findIndex(m => m.id === messageId);
|
||||
if (index !== -1) {
|
||||
messages.value.splice(index, 1);
|
||||
}
|
||||
|
||||
if (selectedMessage.value && selectedMessage.value.id === messageId) {
|
||||
const newIndex = Math.max(0, index - 1);
|
||||
selectedMessage.value = messages.value.length > 0 ? messages.value[newIndex] : null;
|
||||
if (selectedMessage.value) {
|
||||
selectMessage(selectedMessage.value);
|
||||
} else {
|
||||
attachments.value = [];
|
||||
}
|
||||
}
|
||||
ElMessage.success(t('home.alerts.deleteSuccess'));
|
||||
} catch (action) {
|
||||
if (action === 'cancel') {
|
||||
ElMessage.info(t('home.alerts.actionCancelled'));
|
||||
} else {
|
||||
console.error('Failed to delete message:', action);
|
||||
ElMessage.error(t('home.alerts.deleteFailed'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const clearInbox = async () => {
|
||||
if (!recipient.value || messages.value.length === 0) return;
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
t('home.alerts.confirmClearAll'),
|
||||
t('home.clearInbox'),
|
||||
{
|
||||
confirmButtonText: 'OK',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'warning',
|
||||
}
|
||||
);
|
||||
|
||||
const response = await fetch(`/api/messages?recipient=${recipient.value}`, { method: 'DELETE' });
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
messages.value = [];
|
||||
selectedMessage.value = null;
|
||||
attachments.value = [];
|
||||
ElMessage.success(t('home.alerts.clearSuccess'));
|
||||
} catch (action) {
|
||||
if (action === 'cancel') {
|
||||
ElMessage.info(t('home.alerts.actionCancelled'));
|
||||
} else {
|
||||
console.error('Failed to clear inbox:', action);
|
||||
ElMessage.error(t('home.alerts.clearFailed'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
ElMessage.error(t('home.alerts.copyFailed'));
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
};
|
||||
|
||||
const showCopySuccess = () => {
|
||||
copyStatus.value = 'copied';
|
||||
setTimeout(() => {
|
||||
copyStatus.value = 'idle';
|
||||
}, 2000);
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user