Commit 26f467d0 by Sweet Zhang

初始化项目

parent dd310620
<!DOCTYPE html> <!doctype html>
<html lang=""> <html lang="">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title> <title>银盾邮件系统</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^7.0.1",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
...@@ -34,13 +35,16 @@ ...@@ -34,13 +35,16 @@
"@vue/eslint-config-typescript": "^14.6.0", "@vue/eslint-config-typescript": "^14.6.0",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.7.0",
"autoprefixer": "^10.4.21",
"eslint": "^9.31.0", "eslint": "^9.31.0",
"eslint-plugin-playwright": "^2.2.0", "eslint-plugin-playwright": "^2.2.0",
"eslint-plugin-vue": "~10.3.0", "eslint-plugin-vue": "~10.3.0",
"jiti": "^2.4.2", "jiti": "^2.4.2",
"jsdom": "^26.1.0", "jsdom": "^26.1.0",
"npm-run-all2": "^8.0.4", "npm-run-all2": "^8.0.4",
"postcss": "^8.5.6",
"prettier": "3.6.2", "prettier": "3.6.2",
"tailwindcss": "^3.4.17",
"typescript": "~5.8.0", "typescript": "~5.8.0",
"vite": "^7.0.6", "vite": "^7.0.6",
"vite-plugin-vue-devtools": "^8.0.0", "vite-plugin-vue-devtools": "^8.0.0",
......
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
<script setup lang="ts"></script>
<template> <template>
<h1>You did it!</h1> <div id="app" class="min-h-screen bg-gray-50 text-gray-800 flex flex-col">
<p> <!-- 登录页面 -->
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the <LoginPage v-if="isLoginPage && !isAuthenticated" @login="handleLogin" />
documentation
</p> <!-- 主应用布局 -->
<div v-else class="flex flex-1 overflow-hidden">
<!-- 侧边导航 -->
<Sidebar :current-page="currentPage" @change-page="handlePageChange" @logout="handleLogout" />
<!-- 移动端菜单按钮 -->
<button
class="md:hidden fixed top-4 left-4 z-50 bg-blue-600 text-white p-2 rounded shadow-lg"
@click="showMobileMenu = !showMobileMenu"
>
<i class="fas fa-bars"></i>
</button>
<!-- 移动端侧边栏 -->
<MobileSidebar
v-if="showMobileMenu"
:current-page="currentPage"
@change-page="handleMobilePageChange"
@close-menu="showMobileMenu = false"
@logout="handleLogout"
/>
<!-- 主内容区域 -->
<main class="flex-1 overflow-y-auto bg-gray-50 p-4 md:p-6">
<header class="mb-6">
<h2 class="text-2xl font-bold text-gray-800">
{{ pageTitles[currentPage] }}
</h2>
</header>
<!-- 写邮件页面 -->
<ComposeEmail
v-if="currentPage === 'compose'"
:senders="senders"
:contacts="contacts"
:variables="variables"
:variable-templates="variableTemplates"
:emails="emails"
@save-email="saveEmail"
/>
<!-- 联系人管理页面 -->
<ContactManagement
v-if="currentPage === 'contacts'"
:contacts="contacts"
@update-contacts="updateContacts"
/>
<!-- 发件人管理页面 -->
<SenderManagement
v-if="currentPage === 'senders'"
:senders="senders"
@update-senders="updateSenders"
/>
<!-- 变量管理页面 -->
<VariableManagement
v-if="currentPage === 'variables'"
:variables="variables"
:variable-templates="variableTemplates"
@update-variables="updateVariables"
@update-variable-templates="updateVariableTemplates"
/>
<!-- 邮件管理页面 -->
<EmailManagement
v-if="currentPage === 'emails'"
:emails="emails"
@reuse-email="reuseEmail"
/>
</main>
</div>
</div>
</template> </template>
<style scoped></style> <script setup lang="ts">
import { ref, onMounted } from 'vue'
import LoginPage from './components/LoginPage.vue'
import Sidebar from './components/Sidebar.vue'
import MobileSidebar from './components/MobileSidebar.vue'
import ComposeEmail from './components/ComposeEmail.vue'
import ContactManagement from './components/ContactManagement.vue'
import SenderManagement from './components/SenderManagement.vue'
import VariableManagement from './components/VariableManagement.vue'
import EmailManagement from './components/EmailManagement.vue'
import { Contact, Sender, Variable, VariableTemplate, Email } from './types'
// 状态管理
const isLoginPage = ref(true)
const isAuthenticated = ref(false)
const currentPage = ref('compose')
const showMobileMenu = ref(false)
// 数据存储
const contacts = ref<Contact[]>([])
const senders = ref<Sender[]>([])
const variables = ref<Variable[]>([])
const variableTemplates = ref<VariableTemplate[]>([])
const emails = ref<Email[]>([])
// 页面标题映射
const pageTitles = {
compose: '写邮件',
contacts: '联系人管理',
senders: '发件人管理',
variables: '变量管理',
emails: '邮件记录',
}
// 方法
const handleLogin = () => {
// 模拟登录验证
isAuthenticated.value = true
isLoginPage.value = false
// 登录成功后加载初始数据
loadInitialData()
}
const handleLogout = () => {
isAuthenticated.value = false
isLoginPage.value = true
}
const handlePageChange = (page: string) => {
currentPage.value = page
}
const handleMobilePageChange = (page: string) => {
currentPage.value = page
showMobileMenu.value = false
}
const updateContacts = (newContacts: Contact[]) => {
contacts.value = newContacts
}
const updateSenders = (newSenders: Sender[]) => {
senders.value = newSenders
}
const updateVariables = (newVariables: Variable[]) => {
variables.value = newVariables
}
const updateVariableTemplates = (newTemplates: VariableTemplate[]) => {
variableTemplates.value = newTemplates
}
const saveEmail = (email: Email) => {
emails.value.push(email)
}
const reuseEmail = (emailData: any) => {
currentPage.value = 'compose'
// 这里可以传递需要复用的邮件数据到ComposeEmail组件
// 实际实现中可以使用状态管理或props
}
const loadInitialData = () => {
// 模拟加载初始数据
// 联系人
contacts.value = [
{
id: '1',
name: '张三',
title: '先生',
company: 'ABC公司',
email: 'zhangsan@example.com',
ccEmail: 'zhangsan_cc@example.com',
other: '技术总监',
},
{
id: '2',
name: '李四',
title: '女士',
company: 'XYZ企业',
email: 'lisi@example.com',
ccEmail: '',
other: '市场经理',
},
]
// 发件人
senders.value = [
{
id: '1',
email: 'service@mycompany.com',
password: '******',
smtpServer: 'smtp.mycompany.com',
smtpPort: '587',
},
]
// 变量
variables.value = [
{
id: '1',
name: '用户名',
key: 'username',
description: '接收者的用户名',
},
{
id: '2',
name: '订单号',
key: 'order_no',
description: '订单编号',
},
{
id: '3',
name: '金额',
key: 'amount',
description: '订单金额',
},
]
// 变量模板
variableTemplates.value = [
{
id: '1',
name: '订单通知',
description: '订单相关通知邮件模板',
variableIds: ['1', '2', '3'],
},
]
// 邮件记录
emails.value = [
{
id: '1',
sender: 'service@mycompany.com',
to: 'zhangsan@example.com',
cc: 'zhangsan_cc@example.com',
subject: '关于您的订单',
content: '尊敬的{{username}},您的订单{{order_no}}已发货,金额为{{amount}}元。',
sendTime: new Date().toISOString(),
status: 'sent',
attachments: [{ name: '订单详情.pdf' }],
},
{
id: '2',
sender: 'service@mycompany.com',
to: 'lisi@example.com',
cc: '',
subject: '市场活动邀请',
content: '尊敬的{{username}},诚邀您参加我们的市场活动。',
sendTime: new Date(Date.now() + 86400000).toISOString(),
status: 'scheduled',
},
]
}
// 初始化
onMounted(() => {
// 检查是否已登录(实际项目中应该检查本地存储或令牌)
})
</script>
<template>
<div>
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">添加联系人</h3>
<button
v-if="editingContactId"
@click="resetForm"
class="text-gray-500 hover:text-gray-700"
>
<i class="fas fa-times"></i> 取消编辑
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div>
<label class="block text-gray-700 mb-1 text-sm">姓名 *</label>
<input
v-model="formData.name"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">称谓</label>
<input
v-model="formData.title"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="先生/女士/教授等"
/>
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">公司</label>
<input
v-model="formData.company"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">邮箱 *</label>
<input
v-model="formData.email"
type="email"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">关联抄送邮箱</label>
<input
v-model="formData.ccEmail"
type="email"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="当此联系人作为收件人时自动抄送的邮箱"
/>
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">其他信息</label>
<input
v-model="formData.other"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
<div class="mt-4 flex justify-end">
<button
@click="saveContact"
class="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-700 transition-colors"
:disabled="!formData.name || !formData.email"
>
{{ editingContactId ? '更新联系人' : '添加联系人' }}
</button>
</div>
</div>
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="p-6 border-b border-gray-200">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<h3 class="text-lg font-semibold">联系人列表</h3>
<div class="w-full sm:w-auto">
<div class="relative">
<input
v-model="searchTerm"
type="text"
class="w-full px-3 py-2 pl-10 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="搜索联系人..."
/>
<i
class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
></i>
</div>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
姓名
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
称谓
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
公司
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
邮箱
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
抄送邮箱
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
其他信息
</th>
<th
class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"
>
操作
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="contact in filteredContacts" :key="contact.id">
<td class="px-6 py-4 whitespace-nowrap">{{ contact.name }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ contact.title || '-' }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ contact.company || '-' }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ contact.email }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ contact.ccEmail || '-' }}</td>
<td class="px-6 py-4">{{ contact.other || '-' }}</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
@click="editContact(contact)"
class="text-blue-600 hover:text-blue-900 mr-3"
>
编辑
</button>
<button @click="deleteContact(contact.id)" class="text-red-600 hover:text-red-900">
删除
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="filteredContacts.length === 0" class="p-8 text-center text-gray-500">
<i class="fas fa-address-book text-4xl mb-3 opacity-30"></i>
<p>暂无联系人,请添加联系人</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, defineProps, defineEmits } from 'vue'
import { Contact } from '../types'
const props = defineProps({
contacts: {
type: Array as () => Contact[],
required: true,
},
})
const emits = defineEmits(['update-contacts'])
// 状态
const contacts = ref<Contact[]>([...props.contacts])
const searchTerm = ref('')
const editingContactId = ref('')
const formData = ref<Partial<Contact>>({
name: '',
title: '',
company: '',
email: '',
ccEmail: '',
other: '',
})
// 计算属性
const filteredContacts = computed(() => {
return contacts.value.filter(
(contact) =>
contact.name.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
contact.email.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
contact.company.toLowerCase().includes(searchTerm.value.toLowerCase()),
)
})
// 方法
const resetForm = () => {
editingContactId.value = ''
formData.value = {
name: '',
title: '',
company: '',
email: '',
ccEmail: '',
other: '',
}
}
const saveContact = () => {
if (!formData.value.name || !formData.value.email) return
if (editingContactId.value) {
// 更新现有联系人
const index = contacts.value.findIndex((c) => c.id === editingContactId.value)
if (index > -1) {
contacts.value[index] = {
...contacts.value[index],
...formData.value,
} as Contact
emits('update-contacts', [...contacts.value])
alert('联系人更新成功')
}
} else {
// 添加新联系人
const newContact: Contact = {
id: Date.now().toString(),
name: formData.value.name || '',
title: formData.value.title || '',
company: formData.value.company || '',
email: formData.value.email || '',
ccEmail: formData.value.ccEmail || '',
other: formData.value.other || '',
}
contacts.value.push(newContact)
emits('update-contacts', [...contacts.value])
alert('联系人添加成功')
}
resetForm()
}
const editContact = (contact: Contact) => {
editingContactId.value = contact.id
formData.value = { ...contact }
}
const deleteContact = (id: string) => {
if (confirm('确定要删除这个联系人吗?')) {
contacts.value = contacts.value.filter((contact) => contact.id !== id)
emits('update-contacts', [...contacts.value])
}
}
</script>
<template>
<div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] flex flex-col">
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-semibold">选择联系人</h3>
<button @click="$emit('close')">
<i class="fas fa-times text-gray-500"></i>
</button>
</div>
<div class="p-4 flex-1 overflow-y-auto">
<div class="mb-4">
<input
v-model="searchTerm"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="搜索联系人..."
/>
</div>
<div class="space-y-2">
<div
v-for="contact in filteredContacts"
:key="contact.id"
class="flex items-center p-3 border border-gray-200 rounded-md hover:bg-blue-50 cursor-pointer"
@click="toggleSelection(contact)"
>
<input
type="checkbox"
:id="'contact-' + contact.id"
:checked="selectedContacts.includes(contact.id)"
class="mr-3"
/>
<label for="'contact-' + contact.id" class="flex-1">
<div class="font-medium">{{ contact.name }}</div>
<div class="text-sm text-gray-500">{{ contact.email }}</div>
</label>
<div class="text-sm text-gray-500">{{ contact.company || '' }}</div>
</div>
</div>
<div v-if="filteredContacts.length === 0" class="p-6 text-center text-gray-500">
<p>未找到匹配的联系人</p>
</div>
</div>
<div class="p-4 border-t border-gray-200 flex justify-end gap-3">
<button
@click="$emit('close')"
class="px-6 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
>
取消
</button>
<button
@click="confirmSelection"
class="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-700 transition-colors"
>
确定
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, defineProps, defineEmits } from 'vue'
import { Contact } from '../types'
const props = defineProps({
contacts: {
type: Array as () => Contact[],
required: true,
},
})
const emits = defineEmits(['confirm-selection', 'close'])
// 状态
const searchTerm = ref('')
const selectedContacts = ref<string[]>([])
// 计算属性
const filteredContacts = computed(() => {
return props.contacts.filter(
(contact) =>
contact.name.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
contact.email.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
contact.company.toLowerCase().includes(searchTerm.value.toLowerCase()),
)
})
// 方法
const toggleSelection = (contact: Contact) => {
const index = selectedContacts.value.indexOf(contact.id)
if (index > -1) {
selectedContacts.value.splice(index, 1)
} else {
selectedContacts.value.push(contact.id)
}
}
const confirmSelection = () => {
const selected = props.contacts.filter((contact) => selectedContacts.value.includes(contact.id))
const to = selected.map((contact) => contact.email).join(',')
const cc = selected
.map((contact) => contact.ccEmail)
.filter(Boolean)
.join(',')
emits('confirm-selection', { to, cc })
}
</script>
<template>
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="p-6 border-b border-gray-200">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<h3 class="text-lg font-semibold">邮件发送记录</h3>
<div class="w-full sm:w-auto flex gap-2">
<div class="relative">
<input
v-model="searchTerm"
type="text"
class="w-full px-3 py-2 pl-10 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="搜索主题或收件人..."
/>
<i
class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
></i>
</div>
<select
v-model="filterStatus"
class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="">全部状态</option>
<option value="sent">已发送</option>
<option value="scheduled">已定时</option>
<option value="draft">草稿</option>
<option value="failed">发送失败</option>
</select>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
发件人
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
收件人
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
主题
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
发送时间
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
状态
</th>
<th
class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"
>
操作
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="email in filteredEmails" :key="email.id">
<td class="px-6 py-4 whitespace-nowrap">{{ email.sender }}</td>
<td class="px-6 py-4 whitespace-nowrap max-w-xs truncate">{{ email.to }}</td>
<td class="px-6 py-4 max-w-xs truncate">{{ email.subject }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ formatDate(email.sendTime) }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
:class="
email.status === 'sent'
? 'bg-green-100 text-green-800'
: email.status === 'scheduled'
? 'bg-yellow-100 text-yellow-800'
: email.status === 'draft'
? 'bg-gray-100 text-gray-800'
: 'bg-red-100 text-red-800'
"
>
{{
email.status === 'sent'
? '已发送'
: email.status === 'scheduled'
? '已定时'
: email.status === 'draft'
? '草稿'
: '发送失败'
}}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
@click="viewEmailDetail(email)"
class="text-blue-600 hover:text-blue-900 mr-3"
>
查看
</button>
<button @click="reuseEmailContent(email)" class="text-green-600 hover:text-green-900">
复用
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="filteredEmails.length === 0" class="p-8 text-center text-gray-500">
<i class="fas fa-history text-4xl mb-3 opacity-30"></i>
<p>暂无邮件发送记录</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, defineProps, defineEmits } from 'vue'
import { Email } from '../types'
const props = defineProps({
emails: {
type: Array as () => Email[],
required: true,
},
})
const emits = defineEmits(['reuse-email'])
// 状态
const searchTerm = ref('')
const filterStatus = ref('')
// 计算属性
const filteredEmails = computed(() => {
return props.emails
.filter((email) => {
const matchesSearch =
email.subject.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
email.to.toLowerCase().includes(searchTerm.value.toLowerCase())
const matchesStatus = !filterStatus.value || email.status === filterStatus.value
return matchesSearch && matchesStatus
})
.sort((a, b) => new Date(b.sendTime).getTime() - new Date(a.sendTime).getTime())
})
// 方法
const formatDate = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleString()
}
const viewEmailDetail = (email: Email) => {
// 显示邮件详情
alert(`邮件主题: ${email.subject}\n收件人: ${email.to}\n发送时间: ${formatDate(email.sendTime)}`)
// 实际项目中可以打开详情弹窗
}
const reuseEmailContent = (email: Email) => {
// 触发复用邮件内容事件
emits('reuse-email', {
subject: email.subject,
content: email.content,
})
}
</script>
<template>
<div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] flex flex-col">
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-semibold">邮件预览</h3>
<button @click="$emit('close')">
<i class="fas fa-times text-gray-500"></i>
</button>
</div>
<div class="p-6 flex-1 overflow-y-auto">
<div class="mb-4">
<div class="text-sm text-gray-500">发件人:</div>
<div class="font-medium">{{ sender }}</div>
</div>
<div class="mb-4">
<div class="text-sm text-gray-500">收件人:</div>
<div>{{ emailForm.to }}</div>
</div>
<div v-if="emailForm.cc" class="mb-4">
<div class="text-sm text-gray-500">抄送人:</div>
<div>{{ emailForm.cc }}</div>
</div>
<div class="mb-6 pt-4 border-t border-gray-200">
<div class="text-xl font-semibold">{{ emailForm.subject }}</div>
</div>
<div class="mb-6">
<div v-html="previewContent" class="prose max-w-none"></div>
</div>
<div v-if="attachments.length > 0" class="pt-4 border-t border-gray-200">
<div class="text-sm text-gray-500 mb-2">附件:</div>
<div class="space-y-1">
<div
v-for="(file, index) in attachments"
:key="index"
class="flex items-center text-sm"
>
<i class="fas fa-file mr-2 text-gray-400"></i>
<span>{{ file.name }}</span>
</div>
</div>
</div>
<div
v-if="emailForm.scheduleSend"
class="mt-4 pt-4 border-t border-gray-200 text-sm text-gray-600"
>
<i class="fas fa-clock mr-1"></i> 定时发送: {{ emailForm.sendTime || '未设置时间' }}
</div>
</div>
<div class="p-4 border-t border-gray-200 flex justify-end gap-3">
<button
@click="$emit('close')"
class="px-6 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
>
返回编辑
</button>
<button
@click="$emit('confirm-send')"
class="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-700 transition-colors"
>
确认发送
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, defineProps, defineEmits } from 'vue'
import { EmailForm } from '../types'
const props = defineProps({
emailForm: {
type: Object as () => EmailForm,
required: true,
},
sender: {
type: String,
required: true,
},
attachments: {
type: Array as () => File[],
required: true,
},
})
const emits = defineEmits(['confirm-send', 'close'])
// 计算属性
const previewContent = computed(() => {
// 替换变量为占位符用于预览
return props.emailForm.content.replace(
/{{\s*(\w+)\s*}}/g,
'<span class="bg-blue-100 px-1 rounded">[$1]</span>',
)
})
</script>
<template>
<!-- 全屏背景容器,带有渐变效果 -->
<div
class="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-blue-100 p-4 sm:p-6"
>
<!-- 登录卡片容器,带阴影和动画 -->
<div
class="w-full max-w-md bg-white rounded-xl shadow-lg overflow-hidden transform transition-all duration-300 hover:shadow-xl"
>
<!-- 顶部蓝色装饰条 -->
<div class="h-1.5 bg-gradient-to-r from-blue-500 to-blue-700"></div>
<!-- 登录内容区域 -->
<div class="p-6 sm:p-8">
<!-- 图标和标题区域 -->
<div class="text-center mb-8">
<div
class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-100 mb-4"
>
<i class="fas fa-envelope text-2xl text-blue-600"></i>
</div>
<h2 class="text-[clamp(1.5rem,3vw,1.8rem)] font-bold text-gray-800">邮件系统</h2>
<p class="text-gray-500 mt-2">请登录您的账号以继续</p>
</div>
<!-- 登录表单 -->
<form @submit.prevent="handleLogin" class="space-y-5">
<!-- 用户名输入框 -->
<div class="space-y-2">
<label for="username" class="block text-sm font-medium text-gray-700"> 用户名 </label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-user text-gray-400"></i>
</div>
<input
id="username"
v-model="username"
type="text"
required
class="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 placeholder-gray-400"
placeholder="请输入用户名"
:class="usernameFocused ? 'ring-2 ring-blue-200 border-blue-300' : ''"
@focus="usernameFocused = true"
@blur="usernameFocused = false"
/>
</div>
</div>
<!-- 密码输入框 -->
<div class="space-y-2">
<div class="flex justify-between items-center">
<label for="password" class="block text-sm font-medium text-gray-700"> 密码 </label>
<button
type="button"
class="text-sm text-blue-600 hover:text-blue-800 transition-colors"
@click="handleForgotPassword"
>
忘记密码?
</button>
</div>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-lock text-gray-400"></i>
</div>
<input
id="password"
v-model="password"
:type="showPassword ? 'text' : 'password'"
required
class="block w-full pl-10 pr-10 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 placeholder-gray-400"
placeholder="请输入密码"
:class="passwordFocused ? 'ring-2 ring-blue-200 border-blue-300' : ''"
@focus="passwordFocused = true"
@blur="passwordFocused = false"
/>
<button
type="button"
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"
@click="showPassword = !showPassword"
>
<i :class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
</button>
</div>
</div>
<!-- 记住我选项 -->
<div class="flex items-center">
<input
id="remember-me"
v-model="rememberMe"
type="checkbox"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label for="remember-me" class="ml-2 block text-sm text-gray-700"> 记住我 </label>
</div>
<!-- 登录按钮 -->
<button
type="submit"
class="w-full bg-blue-500 hover:bg-blue-700 text-white font-medium py-3 px-4 rounded-lg transition-all duration-200 transform hover:scale-[1.02] active:scale-[0.98] focus:ring-4 focus:ring-blue-300"
:disabled="isSubmitting"
>
<span v-if="!isSubmitting">登录系统</span>
<span v-if="isSubmitting" class="flex items-center justify-center">
<i class="fas fa-spinner fa-spin mr-2"></i>
登录中...
</span>
</button>
</form>
<!-- 底部分隔线和其他登录方式 -->
<div class="mt-8">
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-white text-gray-500">其他登录方式</span>
</div>
</div>
<div class="mt-6 grid grid-cols-2 gap-3">
<button
class="flex items-center justify-center py-2 px-4 border border-gray-300 rounded-lg shadow-sm bg-white text-gray-700 hover:bg-gray-50 transition-colors"
>
<i class="fab fa-github mr-2 text-gray-800"></i>
<span>GitHub</span>
</button>
<button
class="flex items-center justify-center py-2 px-4 border border-gray-300 rounded-lg shadow-sm bg-white text-gray-700 hover:bg-gray-50 transition-colors"
>
<i class="fab fa-google mr-2 text-red-500"></i>
<span>Google</span>
</button>
</div>
</div>
</div>
<!-- 底部版权信息 -->
<div class="bg-gray-50 px-6 py-4 text-center text-sm text-gray-500 border-t border-gray-100">
<p>© 2024 邮件系统. 保留所有权利.</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, defineEmits } from 'vue'
// 定义事件 - 登录成功和跳转到忘记密码页面
const emits = defineEmits(['login', 'go-to-forgot-password'])
// 状态管理
const username = ref('')
const password = ref('')
const rememberMe = ref(false)
const showPassword = ref(false)
const isSubmitting = ref(false)
const usernameFocused = ref(false)
const passwordFocused = ref(false)
// 登录处理
const handleLogin = async () => {
// 模拟登录加载状态
isSubmitting.value = true
try {
// 模拟API请求延迟
await new Promise((resolve) => setTimeout(resolve, 1200))
// 验证用户名和密码(实际项目中应调用后端接口)
if (username.value && password.value) {
// 如果勾选了记住我,保存用户名到本地存储
if (rememberMe.value) {
localStorage.setItem(
'savedEmailUser',
JSON.stringify({
username: username.value,
}),
)
} else {
// 否则清除本地存储
localStorage.removeItem('savedEmailUser')
}
// 触发登录事件,传递用户信息
emits('login', {
username: username.value,
password: password.value,
})
} else {
alert('请输入完整的用户名和密码')
}
} catch (error) {
console.error('登录失败:', error)
alert('登录失败,请稍后重试')
} finally {
// 重置加载状态
isSubmitting.value = false
}
}
// 处理忘记密码点击事件
const handleForgotPassword = () => {
// 触发跳转到忘记密码页面的事件
emits('go-to-forgot-password')
}
// 页面加载时自动填充(如果有记住的用户信息)
const loadSavedCredentials = () => {
const savedUser = localStorage.getItem('savedEmailUser')
if (savedUser) {
const user = JSON.parse(savedUser)
username.value = user.username
rememberMe.value = true
}
}
// 初始化加载
loadSavedCredentials()
</script>
<style scoped>
/* 自定义动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 登录卡片动画 */
.bg-white {
animation: fadeIn 0.5s ease-out forwards;
}
/* 输入框聚焦状态优化 */
input:focus {
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
/* 按钮交互效果增强 */
button:hover {
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
/* 适配暗色模式 */
@media (prefers-color-scheme: dark) {
.bg-white {
background-color: #1f2937;
}
.text-gray-800 {
color: #f9fafb;
}
.text-gray-700 {
color: #e5e7eb;
}
.text-gray-500 {
color: #9ca3af;
}
.border-gray-300 {
border-color: #4b5563;
}
.bg-gray-50 {
background-color: #111827;
}
.bg-blue-100 {
background-color: #1e3a8a;
}
}
</style>
<template>
<div class="fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden" @click="$emit('close-menu')">
<div class="bg-sky-700 text-white w-64 h-full p-4" @click.stop>
<div class="flex justify-between items-center mb-6">
<h1 class="text-xl font-bold">邮件系统</h1>
<button @click="$emit('close-menu')">
<i class="fas fa-times"></i>
</button>
</div>
<nav>
<ul>
<li class="mb-2">
<button
@click="$emit('change-page', 'compose')"
class="w-full text-left px-4 py-2 rounded hover:bg-blue-500 transition-colors flex items-center"
:class="currentPage === 'compose' ? 'bg-blue-500' : ''"
>
<i class="fas fa-pen mr-2"></i>写邮件
</button>
</li>
<li class="mb-2">
<button
@click="$emit('change-page', 'contacts')"
class="w-full text-left px-4 py-2 rounded hover:bg-blue-500 transition-colors flex items-center"
:class="currentPage === 'contacts' ? 'bg-blue-500' : ''"
>
<i class="fas fa-address-book mr-2"></i>联系人管理
</button>
</li>
<li class="mb-2">
<button
@click="$emit('change-page', 'senders')"
class="w-full text-left px-4 py-2 rounded hover:bg-blue-500 transition-colors flex items-center"
:class="currentPage === 'senders' ? 'bg-blue-500' : ''"
>
<i class="fas fa-user-circle mr-2"></i>发件人管理
</button>
</li>
<li class="mb-2">
<button
@click="$emit('change-page', 'variables')"
class="w-full text-left px-4 py-2 rounded hover:bg-blue-500 transition-colors flex items-center"
:class="currentPage === 'variables' ? 'bg-blue-500' : ''"
>
<i class="fas fa-variable mr-2"></i>变量管理
</button>
</li>
<li class="mb-2">
<button
@click="$emit('change-page', 'emails')"
class="w-full text-left px-4 py-2 rounded hover:bg-blue-500 transition-colors flex items-center"
:class="currentPage === 'emails' ? 'bg-blue-500' : ''"
>
<i class="fas fa-history mr-2"></i>邮件记录
</button>
</li>
</ul>
</nav>
<div class="absolute bottom-4 left-0 right-0 px-4">
<button
@click="$emit('logout')"
class="w-full text-left px-4 py-2 rounded hover:bg-blue-500 transition-colors flex items-center text-sm"
>
<i class="fas fa-sign-out-alt mr-2"></i>退出登录
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
currentPage: {
type: String,
required: true,
},
})
const emits = defineEmits(['change-page', 'close-menu', 'logout'])
</script>
<template>
<div>
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">添加发件人邮箱</h3>
<button v-if="editingSenderId" @click="resetForm" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i> 取消编辑
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-gray-700 mb-1 text-sm">邮箱地址 *</label>
<input
v-model="formData.email"
type="email"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="例如:service@example.com"
/>
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">密码/授权码 *</label>
<input
v-model="formData.password"
type="password"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="邮箱密码或授权码"
/>
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">SMTP服务器 *</label>
<input
v-model="formData.smtpServer"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="例如:smtp.example.com"
/>
</div>
<div>
<label class="block text-gray-700 mb-1 text-sm">SMTP端口 *</label>
<input
v-model="formData.smtpPort"
type="number"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="例如:587"
/>
</div>
</div>
<div class="mt-4 flex justify-end">
<button
@click="saveSender"
class="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-700 transition-colors"
:disabled="
!formData.email || !formData.password || !formData.smtpServer || !formData.smtpPort
"
>
{{ editingSenderId ? '更新发件人' : '添加发件人' }}
</button>
</div>
</div>
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="p-6 border-b border-gray-200">
<h3 class="text-lg font-semibold">发件人列表</h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
邮箱地址
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
SMTP服务器
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
SMTP端口
</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
状态
</th>
<th
class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"
>
操作
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="sender in senders" :key="sender.id">
<td class="px-6 py-4 whitespace-nowrap">{{ sender.email }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ sender.smtpServer }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ sender.smtpPort }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
>
可用
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button @click="editSender(sender)" class="text-blue-600 hover:text-blue-900 mr-3">
编辑
</button>
<button @click="deleteSender(sender.id)" class="text-red-600 hover:text-red-900">
删除
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="senders.length === 0" class="p-8 text-center text-gray-500">
<i class="fas fa-envelope text-4xl mb-3 opacity-30"></i>
<p>暂无发件人邮箱,请添加发件人</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, defineProps, defineEmits } from 'vue'
import { Sender } from '../types'
const props = defineProps({
senders: {
type: Array as () => Sender[],
required: true,
},
})
const emits = defineEmits(['update-senders'])
// 状态
const senders = ref<Sender[]>([...props.senders])
const editingSenderId = ref('')
const formData = ref<Partial<Sender>>({
email: '',
password: '',
smtpServer: '',
smtpPort: '',
})
// 方法
const resetForm = () => {
editingSenderId.value = ''
formData.value = {
email: '',
password: '',
smtpServer: '',
smtpPort: '',
}
}
const saveSender = () => {
if (
!formData.value.email ||
!formData.value.password ||
!formData.value.smtpServer ||
!formData.value.smtpPort
)
return
if (editingSenderId.value) {
// 更新现有发件人
const index = senders.value.findIndex((s) => s.id === editingSenderId.value)
if (index > -1) {
senders.value[index] = {
...senders.value[index],
...formData.value,
} as Sender
emits('update-senders', [...senders.value])
alert('发件人更新成功')
}
} else {
// 添加新发件人
const newSender: Sender = {
id: Date.now().toString(),
email: formData.value.email || '',
password: formData.value.password || '',
smtpServer: formData.value.smtpServer || '',
smtpPort: formData.value.smtpPort || '',
}
senders.value.push(newSender)
emits('update-senders', [...senders.value])
alert('发件人添加成功')
}
resetForm()
}
const editSender = (sender: Sender) => {
editingSenderId.value = sender.id
formData.value = { ...sender }
}
const deleteSender = (id: string) => {
if (confirm('确定要删除这个发件人吗?')) {
senders.value = senders.value.filter((sender) => sender.id !== id)
emits('update-senders', [...senders.value])
}
}
</script>
<template>
<aside
class="bg-sky-700 text-white w-64 flex-shrink-0 hidden md:block transition-all duration-300 ease-in-out"
>
<div class="p-4 border-b border-blue-500">
<h1 class="text-xl font-bold">邮件系统</h1>
</div>
<nav class="p-4">
<ul>
<li class="mb-2">
<button
@click="$emit('change-page', 'compose')"
class="w-full text-left px-4 py-2 rounded hover:bg-blue-500 transition-colors flex items-center"
:class="currentPage === 'compose' ? 'bg-blue-500' : ''"
>
<i class="fas fa-pen mr-2"></i>写邮件
</button>
</li>
<li class="mb-2">
<button
@click="$emit('change-page', 'contacts')"
class="w-full text-left px-4 py-2 rounded hover:bg-blue-500 transition-colors flex items-center"
:class="currentPage === 'contacts' ? 'bg-blue-500' : ''"
>
<i class="fas fa-address-book mr-2"></i>联系人管理
</button>
</li>
<li class="mb-2">
<button
@click="$emit('change-page', 'senders')"
class="w-full text-left px-4 py-2 rounded hover:bg-blue-500 transition-colors flex items-center"
:class="currentPage === 'senders' ? 'bg-blue-500' : ''"
>
<i class="fas fa-user-circle mr-2"></i>发件人管理
</button>
</li>
<li class="mb-2">
<button
@click="$emit('change-page', 'variables')"
class="w-full text-left px-4 py-2 rounded hover:bg-blue-500 transition-colors flex items-center"
:class="currentPage === 'variables' ? 'bg-blue-500' : ''"
>
<i class="fas fa-variable mr-2"></i>变量管理
</button>
</li>
<li class="mb-2">
<button
@click="$emit('change-page', 'emails')"
class="w-full text-left px-4 py-2 rounded hover:bg-blue-500 transition-colors flex items-center"
:class="currentPage === 'emails' ? 'bg-blue-500' : ''"
>
<i class="fas fa-history mr-2"></i>邮件记录
</button>
</li>
</ul>
</nav>
<div class="absolute bottom-4 left-0 right-0 px-4">
<button
@click="$emit('logout')"
class="w-full text-left px-4 py-2 rounded hover:bg-blue-500 transition-colors flex items-center text-sm"
>
<i class="fas fa-sign-out-alt mr-2"></i>退出登录
</button>
</div>
</aside>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
currentPage: {
type: String,
required: true,
},
})
const emits = defineEmits(['change-page', 'logout'])
</script>
<template>
<div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md max-h-[80vh] flex flex-col">
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-semibold">插入变量</h3>
<button @click="$emit('close')">
<i class="fas fa-times text-gray-500"></i>
</button>
</div>
<div class="p-4 flex-1 overflow-y-auto">
<div class="mb-4">
<input
v-model="searchTerm"
type="text"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="搜索变量..."
/>
</div>
<div class="grid grid-cols-1 gap-2">
<button
v-for="variable in filteredVariables"
:key="variable.id"
class="p-3 border border-gray-200 rounded-md hover:bg-blue-50 text-left transition-colors"
@click="selectVariable(variable)"
>
<div class="font-medium font-mono">{{ variablePrefix }}{{ variable.key }}</div>
<div class="text-sm text-gray-500">{{ variable.name }}</div>
</button>
</div>
<div v-if="filteredVariables.length === 0" class="p-6 text-center text-gray-500">
<p>未找到匹配的变量</p>
</div>
</div>
<div class="p-4 border-t border-gray-200 flex justify-end">
<button
@click="$emit('close')"
class="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-700 transition-colors"
>
关闭
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, defineProps, defineEmits } from 'vue'
import { Variable } from '../types'
const props = defineProps({
variables: {
type: Array as () => Variable[],
required: true,
},
})
const emits = defineEmits(['insert-variable', 'close'])
// 状态
const searchTerm = ref('')
const variablePrefix = '{{'
// 计算属性
const filteredVariables = computed(() => {
return props.variables.filter(
(variable) =>
variable.name.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
variable.key.toLowerCase().includes(searchTerm.value.toLowerCase()),
)
})
// 方法
const selectVariable = (variable: Variable) => {
emits('insert-variable', variable)
}
</script>
/* 导入Tailwind CSS */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* 导入Font Awesome */
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css');
/* 自定义样式 */
#app {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.prose {
max-width: 100%;
}
/* 滚动条美化 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
}
...@@ -4,6 +4,9 @@ import { createPinia } from 'pinia' ...@@ -4,6 +4,9 @@ import { createPinia } from 'pinia'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
// 引入全部样式
import './index.css'
const app = createApp(App) const app = createApp(App)
app.use(createPinia()) app.use(createPinia())
......
// 联系人类型
export interface Contact {
id: string
name: string
title: string
company: string
email: string
ccEmail: string
other: string
}
// 发件人类型
export interface Sender {
id: string
email: string
password: string
smtpServer: string
smtpPort: string
}
// 变量类型
export interface Variable {
id: string
name: string
key: string
description: string
}
// 变量模板类型
export interface VariableTemplate {
id: string
name: string
description: string
variableIds: string[]
}
// 邮件类型
export interface Email {
id: string
sender: string
to: string
cc: string
subject: string
content: string
sendTime: string
status: 'sent' | 'scheduled' | 'draft' | 'failed'
attachments?: { name: string }[]
}
// 邮件表单类型
export interface EmailForm {
to: string
cc: string
subject: string
content: string
scheduleSend: boolean
sendTime: string
}
// 忘记密码表单类型
export interface ForgotPasswordForm {
email: string
newPassword: string
confirmPassword: string
}
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
}
...@@ -7,14 +7,13 @@ import vueDevTools from 'vite-plugin-vue-devtools' ...@@ -7,14 +7,13 @@ import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ // 关键配置:设置基础路径为子目录 yd-email
vue(), // 生产环境(部署到服务器)用 '/yd-email/',本地开发用 '/'(避免开发时路径错误)
vueJsx(), base: process.env.NODE_ENV === 'production' ? '/yd-email/' : '/',
vueDevTools(), plugins: [vue(), vueJsx(), vueDevTools()],
],
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url)),
}, },
}, },
}) })
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment