Files
emall-web/src/views/Compose.vue
2025-10-16 16:21:56 +08:00

792 lines
22 KiB
Vue

<template>
<div class="compose-container">
<!-- 深空背景 -->
<div class="space-background">
<div class="stars" ref="stars"></div>
</div>
<!-- 顶部导航 -->
<div class="header glass-card">
<van-icon name="arrow-left" size="24" @click="goBack" />
<h2>撰写未来邮件</h2>
<div></div>
</div>
<!-- 撰写表单 -->
<div class="compose-content">
<div class="form-section glass-card p-20">
<!-- 收件人选择 -->
<div class="form-group">
<h3 class="font-semibold">收件人</h3>
<van-radio-group v-model="recipientType" direction="horizontal">
<van-radio name="SELF">自己</van-radio>
<van-radio name="SPECIFIC">他人</van-radio>
<van-radio name="PUBLIC">任意有缘人</van-radio>
</van-radio-group>
<van-field
v-if="recipientType === 'SPECIFIC'"
v-model="recipientEmail"
placeholder="收件人邮箱"
class="custom-field mt-10"
/>
</div>
<!-- 发送时间选择 -->
<div class="form-group mt-20">
<h3 class="font-semibold">发送时间</h3>
<van-radio-group v-model="timeType" direction="horizontal">
<van-radio name="preset">预设时间</van-radio>
<van-radio name="custom">自定义</van-radio>
<van-radio name="condition">条件触发</van-radio>
</van-radio-group>
<div v-if="timeType === 'preset'" class="preset-options mt-10">
<van-button
v-for="option in presetTimeOptions"
:key="option.value"
:type="selectedPresetTime === option.value ? 'primary' : 'default'"
round
size="small"
class="preset-button"
@click="selectPresetTime(option.value)"
>
{{ option.label }}
</van-button>
</div>
<van-datetime-picker
v-if="timeType === 'custom'"
v-model="customDeliveryDate"
type="date"
:min-date="minDate"
class="custom-date-picker mt-10"
/>
<div v-if="timeType === 'condition'" class="condition-options mt-10">
<van-cell-group>
<van-cell title="地点触发" is-link @click="showLocationPicker = true" />
<van-cell title="事件触发" is-link @click="showEventPicker = true" />
</van-cell-group>
</div>
</div>
<!-- 邮件内容 -->
<div class="form-group mt-20">
<h3 class="font-semibold">邮件内容</h3>
<van-field
v-model="subject"
placeholder="标题"
class="custom-field"
/>
<van-field
v-model="content"
type="textarea"
placeholder="写下你想对未来的自己说的话..."
rows="8"
autosize
class="custom-field mt-10"
/>
<!-- 附件和多媒体 -->
<div class="media-options mt-10">
<van-uploader :after-read="afterRead" class="media-uploader">
<van-button icon="photo-o" type="primary" plain round size="small">
添加图片
</van-button>
</van-uploader>
<van-button icon="volume-o" type="primary" plain round size="small" class="ml-10">
添加语音
</van-button>
<van-button icon="video-o" type="primary" plain round size="small" class="ml-10">
添加视频
</van-button>
</div>
</div>
<!-- AI助手 -->
<div class="form-group mt-20">
<h3 class="font-semibold">AI写作助手</h3>
<van-cell-group>
<van-cell title="生成开头" is-link @click="generateOpening" />
<van-cell title="内容建议" is-link @click="generateSuggestions" />
<van-cell title="情感分析" is-link @click="analyzeEmotion" />
</van-cell-group>
</div>
</div>
</div>
<!-- 底部操作按钮 -->
<div class="footer-actions">
<van-button round block class="save-button" @click="saveDraft">
存入胶囊
</van-button>
<van-button round block type="primary" class="send-button" @click="sendMail">
发送至未来
</van-button>
</div>
<!-- 地点选择弹窗 -->
<van-popup v-model:show="showLocationPicker" position="bottom">
<van-area
:area-list="areaList"
@confirm="onLocationConfirm"
@cancel="showLocationPicker = false"
/>
</van-popup>
<!-- 事件选择弹窗 -->
<van-popup v-model:show="showEventPicker" position="bottom" :style="{ height: '50%' }">
<div class="event-picker">
<h3>选择触发事件</h3>
<van-cell-group>
<van-cell
v-for="event in triggerEvents"
:key="event.id"
:title="event.name"
:label="event.description"
@click="selectEvent(event)"
/>
</van-cell-group>
</div>
</van-popup>
</div>
</template>
<script>
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { showLoadingToast, showSuccessToast, showFailToast, closeToast, Dialog } from 'vant'
export default {
name: 'Compose',
setup() {
const router = useRouter()
const stars = ref(null)
// 表单数据
const recipientType = ref('SELF') // 对应API的SELF, SPECIFIC, PUBLIC
const recipientEmail = ref('')
const timeType = ref('preset') // preset, custom, condition
const selectedPresetTime = ref('1year')
const customDeliveryDate = ref(new Date(Date.now() + 365 * 24 * 60 * 60 * 1000))
const subject = ref('')
const content = ref('')
const attachments = ref([]) // 附件列表
const isEncrypted = ref(false) // 是否加密
const capsuleStyle = ref('default') // 胶囊样式
// 弹窗控制
const showLocationPicker = ref(false)
const showEventPicker = ref(false)
const selectedLocation = ref(null) // 选中的地点
const selectedEvent = ref(null) // 选中的触发事件
// 最小日期为明天
const minDate = computed(() => {
const tomorrow = new Date()
tomorrow.setDate(tomorrow.getDate() + 1)
return tomorrow
})
// 预设时间选项
const presetTimeOptions = [
{ label: '1天后', value: '1day' },
{ label: '1周后', value: '1week' },
{ label: '1个月后', value: '1month' },
{ label: '1年后', value: '1year' },
{ label: '5年后', value: '5years' },
{ label: '10年后', value: '10years' }
]
// 触发事件选项
const triggerEvents = [
{
id: 1,
name: '人类登陆火星',
description: '当检测到相关新闻时触发',
keywords: ['火星', '登陆', '太空探索'],
type: 'SPACE_EVENT'
},
{
id: 2,
name: '获得理想工作',
description: '当您更新个人资料为在职状态时触发',
keywords: ['工作', '职业', '就业'],
type: 'CAREER_EVENT'
},
{
id: 3,
name: '结婚纪念日',
description: '在每年的结婚纪念日触发',
keywords: ['结婚', '纪念日', '婚礼'],
type: 'PERSONAL_EVENT'
},
{
id: 4,
name: '孩子出生',
description: '当您添加家庭成员信息时触发',
keywords: ['孩子', '出生', '家庭'],
type: 'FAMILY_EVENT'
}
]
// 模拟地区数据
const areaList = {
province_list: {
110000: '北京市',
120000: '天津市',
310000: '上海市',
440000: '广东省',
330000: '浙江省',
320000: '江苏省'
},
city_list: {
110100: '北京市',
120100: '天津市',
310100: '上海市',
440100: '广州市',
440300: '深圳市',
330100: '杭州市',
320100: '南京市'
},
county_list: {
110101: '东城区',
110102: '西城区',
440103: '荔湾区',
440304: '福田区',
330102: '上城区',
320102: '玄武区'
}
}
// 生成星空背景
const generateStars = () => {
if (!stars.value) return
const starsContainer = stars.value
const starCount = 100
for (let i = 0; i < starCount; i++) {
const star = document.createElement('div')
star.className = 'star'
// 随机位置
const left = Math.random() * 100
const top = Math.random() * 100
// 随机大小
const size = Math.random() * 3 + 1
// 随机动画延迟
const delay = Math.random() * 4
star.style.left = `${left}%`
star.style.top = `${top}%`
star.style.width = `${size}px`
star.style.height = `${size}px`
star.style.animationDelay = `${delay}s`
starsContainer.appendChild(star)
}
}
// 返回上一页
const goBack = () => {
router.back()
}
// 选择预设时间
const selectPresetTime = (value) => {
selectedPresetTime.value = value
}
// 地点选择确认
const onLocationConfirm = (values) => {
showLocationPicker.value = false
const locationName = values.map(item => item.name).join('/')
selectedLocation.value = {
city: values[1]?.name || '',
province: values[0]?.name || '',
district: values[2]?.name || ''
}
showFailToast(`已选择地点: ${locationName}`)
}
// 选择触发事件
const selectEvent = (event) => {
showEventPicker.value = false
selectedEvent.value = event
showFailToast(`已选择触发事件: ${event.name}`)
}
// 文件上传后处理
const afterRead = (file) => {
// 这里应该调用文件上传API
// 模拟上传成功
const attachment = {
type: 'IMAGE',
url: URL.createObjectURL(file.file),
thumbnail: URL.createObjectURL(file.file),
size: file.file.size
}
attachments.value.push(attachment)
showSuccessToast(`已添加图片: ${file.file.name}`)
}
// AI生成开头
const generateOpening = async () => {
try {
showLoadingToast({
message: '生成中...',
forbidClick: true,
})
// 模拟API调用延迟
await new Promise(resolve => setTimeout(resolve, 2000))
// 模拟AI生成的开头
const opennings = [
"亲爱的未来的我,",
"时光荏苒,当你读到这封信时,",
"写给多年后的自己,",
"未来的我,你好!",
"此刻的我,想对未来的你说:"
]
const randomIndex = Math.floor(Math.random() * opennings.length)
content.value = opennings[randomIndex] + "\n\n"
closeToast()
showSuccessToast('已生成开头')
} catch (error) {
closeToast()
showFailToast('生成失败,请重试')
}
}
// AI生成内容建议
const generateSuggestions = async () => {
try {
showLoadingToast({
message: '生成中...',
forbidClick: true,
})
// 模拟API调用延迟
await new Promise(resolve => setTimeout(resolve, 2000))
// 模拟AI生成的内容建议
const suggestions = [
"你可以分享当前的生活状态、工作情况,以及对未来的期望和梦想。也可以记录下此刻的心情和思考,让未来的你能够回忆起这段时光。",
"考虑写下你现在的目标和计划,以及希望未来的自己已经实现了哪些。也可以询问未来的你某些问题的答案,比如'你幸福吗?'、'你成为想成为的人了吗?'。",
"你可以描述当前的世界、科技、文化等,让未来的你能够对比时代的变迁。也可以分享一些珍贵的回忆和瞬间,这些对你来说可能意义非凡。",
"写下你对未来的预测和想象,无论是对个人生活还是对整个世界。这些预测在将来读来会非常有趣,看看你猜对了多少。"
]
const randomIndex = Math.floor(Math.random() * suggestions.length)
closeToast()
Dialog.alert({
title: '内容建议',
message: suggestions[randomIndex],
})
} catch (error) {
closeToast()
showFailToast('生成失败,请重试')
}
}
// AI情感分析
const analyzeEmotion = async () => {
if (!content.value) {
showFailToast('请先填写邮件内容')
return
}
try {
showLoadingToast({
message: '分析中...',
forbidClick: true,
})
// 模拟API调用延迟
await new Promise(resolve => setTimeout(resolve, 1500))
// 模拟情感分析结果
const sentiments = ['POSITIVE', 'NEUTRAL', 'EMOTIONAL', 'HOPEFUL']
const emotions = [
{ type: 'HAPPY', score: 0.7 },
{ type: 'HOPEFUL', score: 0.8 },
{ type: 'NOSTALGIC', score: 0.5 }
]
const randomSentiment = sentiments[Math.floor(Math.random() * sentiments.length)]
const emotionTypes = emotions.map(e => e.type).join('、')
const summaries = [
"这封信充满了对未来的期待和希望,表达了积极向上的情感。",
"文字中透露出对过去时光的怀念和对未来的思考。",
"这封信情感真挚,表达了内心深处的感受和想法。",
"文字中既有对现实的思考,也有对未来的憧憬和规划。"
]
const randomSummary = summaries[Math.floor(Math.random() * summaries.length)]
closeToast()
Dialog.alert({
title: '情感分析',
message: `情感倾向: ${randomSentiment}\n主要情感: ${emotionTypes}\n分析: ${randomSummary}`,
})
} catch (error) {
closeToast()
showFailToast('分析失败,请重试')
}
}
// 构建邮件数据
const buildMailData = () => {
// 计算发送时间
let sendTime
let triggerType = 'TIME'
let triggerCondition = {}
if (timeType.value === 'preset') {
const now = new Date()
sendTime = new Date(now)
switch (selectedPresetTime.value) {
case '1day':
sendTime.setDate(now.getDate() + 1)
break
case '1week':
sendTime.setDate(now.getDate() + 7)
break
case '1month':
sendTime.setMonth(now.getMonth() + 1)
break
case '1year':
sendTime.setFullYear(now.getFullYear() + 1)
break
case '5years':
sendTime.setFullYear(now.getFullYear() + 5)
break
case '10years':
sendTime.setFullYear(now.getFullYear() + 10)
break
}
} else if (timeType.value === 'custom') {
sendTime = customDeliveryDate.value
} else if (timeType.value === 'condition') {
triggerType = selectedLocation.value ? 'LOCATION' : 'EVENT'
if (selectedLocation.value) {
triggerCondition.location = selectedLocation.value
}
if (selectedEvent.value) {
triggerCondition.event = {
keywords: selectedEvent.value.keywords,
type: selectedEvent.value.type
}
}
}
// 格式化收件人类型
let recipientTypeFormatted = recipientType.value
return {
title: subject.value,
content: content.value,
recipientType: recipientTypeFormatted,
recipientEmail: recipientType.value === 'SPECIFIC' ? recipientEmail.value : undefined,
sendTime: sendTime ? sendTime.toISOString() : undefined,
triggerType,
triggerCondition,
attachments: attachments.value,
isEncrypted: isEncrypted.value,
capsuleStyle: capsuleStyle.value
}
}
// 保存草稿
const saveDraft = async () => {
if (!subject.value) {
showFailToast('请填写邮件标题')
return
}
try {
showLoadingToast({
message: '保存中...',
forbidClick: true,
})
// 模拟API调用延迟
await new Promise(resolve => setTimeout(resolve, 1000))
// 保存到本地存储作为草稿
const mailData = buildMailData()
const drafts = JSON.parse(localStorage.getItem('draftMails') || '[]')
drafts.push({
...mailData,
id: Date.now().toString(),
status: 'DRAFT',
createdAt: new Date().toISOString()
})
localStorage.setItem('draftMails', JSON.stringify(drafts))
closeToast()
showSuccessToast('草稿已保存')
router.back()
} catch (error) {
closeToast()
const errorMessage = error.response?.data?.message || '保存失败,请重试'
showFailToast(errorMessage)
}
}
// 发送邮件
const sendMail = async () => {
if (!subject.value) {
showFailToast('请填写邮件标题')
return
}
if (!content.value) {
showFailToast('请填写邮件内容')
return
}
if (recipientType.value === 'SPECIFIC' && !recipientEmail.value) {
showFailToast('请填写收件人邮箱')
return
}
try {
showLoadingToast({
message: '发送中...',
forbidClick: true,
})
// 模拟API调用延迟
await new Promise(resolve => setTimeout(resolve, 1500))
const mailData = buildMailData()
// 保存到本地存储作为已发送邮件
const sentMails = JSON.parse(localStorage.getItem('sentMails') || '[]')
sentMails.push({
...mailData,
id: Date.now().toString(),
status: 'PENDING',
createdAt: new Date().toISOString()
})
localStorage.setItem('sentMails', JSON.stringify(sentMails))
closeToast()
// 计算发送时间用于显示
let deliveryDate
if (timeType.value === 'preset' || timeType.value === 'custom') {
deliveryDate = new Date(mailData.sendTime)
} else {
deliveryDate = new Date()
deliveryDate.setFullYear(deliveryDate.getFullYear() + 1) // 默认显示一年后
}
Dialog.confirm({
title: '邮件已发送',
message: `您的邮件将在${deliveryDate.toLocaleDateString()}送达,是否返回首页?`,
confirmButtonText: '返回首页',
cancelButtonText: '继续撰写',
})
.then(() => {
router.push('/home')
})
.catch(() => {
// 继续撰写
})
} catch (error) {
closeToast()
const errorMessage = error.response?.data?.message || '发送失败,请重试'
showFailToast(errorMessage)
}
}
onMounted(() => {
generateStars()
})
return {
stars,
recipientType,
recipientEmail,
timeType,
selectedPresetTime,
customDeliveryDate,
minDate,
presetTimeOptions,
subject,
content,
showLocationPicker,
showEventPicker,
triggerEvents,
areaList,
goBack,
selectPresetTime,
onLocationConfirm,
selectEvent,
afterRead,
generateOpening,
generateSuggestions,
analyzeEmotion,
saveDraft,
sendMail
}
}
}
</script>
<style scoped>
.compose-container {
height: 100vh;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
margin: 15px;
z-index: 10;
}
.header h2 {
margin: 0;
font-size: 18px;
font-weight: bold;
}
.compose-content {
flex: 1;
overflow-y: auto;
padding: 0 15px;
}
.form-section {
margin-bottom: 20px;
}
.form-group h3 {
margin: 0 0 10px;
font-size: 16px;
font-weight: bold;
}
.custom-field {
background-color: rgba(255, 255, 255, 0.05);
border-radius: 8px;
}
.custom-field :deep(.van-field__control) {
color: var(--text-primary);
}
/* 单选按钮选中样式 */
:deep(.van-radio-group) {
margin-bottom: 10px;
}
:deep(.van-radio) {
margin-right: 15px;
}
:deep(.van-radio__icon) {
font-size: 18px;
}
:deep(.van-radio__icon--checked .van-icon) {
background-color: #4285f4;
border-color: #4285f4;
box-shadow: 0 0 8px rgba(66, 133, 244, 0.5);
}
:deep(.van-radio__label) {
color: var(--text-primary);
font-size: 14px;
}
.preset-options {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.preset-button {
margin-bottom: 10px;
transition: all 0.3s ease;
}
.preset-button.van-button--primary {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(66, 133, 244, 0.4);
}
.custom-date-picker {
background-color: rgba(255, 255, 255, 0.05);
border-radius: 8px;
}
.media-options {
display: flex;
align-items: center;
}
.media-uploader {
display: inline-block;
}
.footer-actions {
padding: 15px;
display: flex;
flex-direction: column;
gap: 10px;
}
.save-button {
height: 50px;
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
}
.send-button {
height: 50px;
font-size: 16px;
font-weight: bold;
}
.preset-button {
margin: 5px;
}
.media-uploader .van-button {
/* 按钮样式已由全局样式处理 */
}
.event-picker {
padding: 20px;
height: 100%;
overflow-y: auto;
}
.event-picker h3 {
margin: 0 0 15px;
font-size: 18px;
}
</style>