755 lines
20 KiB
Vue
755 lines
20 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>收件人</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>发送时间</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>邮件内容</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>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'
|
||
|
|
import { mailActions, aiActions } from '../store'
|
||
|
|
|
||
|
|
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,
|
||
|
|
})
|
||
|
|
|
||
|
|
const response = await aiActions.writingAssistant({
|
||
|
|
prompt: '请为未来邮件生成一个开头',
|
||
|
|
type: 'OUTLINE',
|
||
|
|
tone: 'EMOTIONAL',
|
||
|
|
length: 'SHORT',
|
||
|
|
context: '写给未来的自己'
|
||
|
|
})
|
||
|
|
|
||
|
|
closeToast()
|
||
|
|
content.value = response.data.content
|
||
|
|
showSuccessToast('已生成开头')
|
||
|
|
} catch (error) {
|
||
|
|
closeToast()
|
||
|
|
showFailToast('生成失败,请重试')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// AI生成内容建议
|
||
|
|
const generateSuggestions = async () => {
|
||
|
|
try {
|
||
|
|
showLoadingToast({
|
||
|
|
message: '生成中...',
|
||
|
|
forbidClick: true,
|
||
|
|
})
|
||
|
|
|
||
|
|
const response = await aiActions.writingAssistant({
|
||
|
|
prompt: '为未来邮件提供内容建议',
|
||
|
|
type: 'DRAFT',
|
||
|
|
tone: 'INSPIRATIONAL',
|
||
|
|
length: 'MEDIUM',
|
||
|
|
context: content.value || '写给未来的自己'
|
||
|
|
})
|
||
|
|
|
||
|
|
closeToast()
|
||
|
|
Dialog.alert({
|
||
|
|
title: '内容建议',
|
||
|
|
message: response.data.content,
|
||
|
|
})
|
||
|
|
} catch (error) {
|
||
|
|
closeToast()
|
||
|
|
showFailToast('生成失败,请重试')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// AI情感分析
|
||
|
|
const analyzeEmotion = async () => {
|
||
|
|
if (!content.value) {
|
||
|
|
showFailToast('请先填写邮件内容')
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
showLoadingToast({
|
||
|
|
message: '分析中...',
|
||
|
|
forbidClick: true,
|
||
|
|
})
|
||
|
|
|
||
|
|
const response = await aiActions.sentimentAnalysis({
|
||
|
|
content: content.value
|
||
|
|
})
|
||
|
|
|
||
|
|
closeToast()
|
||
|
|
const sentiment = response.data.sentiment
|
||
|
|
const emotions = response.data.emotions.map(e => e.type).join('、')
|
||
|
|
const summary = response.data.summary
|
||
|
|
|
||
|
|
Dialog.alert({
|
||
|
|
title: '情感分析',
|
||
|
|
message: `情感倾向: ${sentiment}\n主要情感: ${emotions}\n分析: ${summary}`,
|
||
|
|
})
|
||
|
|
} 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,
|
||
|
|
})
|
||
|
|
|
||
|
|
const mailData = buildMailData()
|
||
|
|
await mailActions.createMail(mailData)
|
||
|
|
|
||
|
|
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,
|
||
|
|
})
|
||
|
|
|
||
|
|
const mailData = buildMailData()
|
||
|
|
const response = await mailActions.createMail(mailData)
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
.preset-options {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 10px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.preset-button {
|
||
|
|
margin-bottom: 10px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.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 {
|
||
|
|
background: linear-gradient(135deg, #4a5f7a, #2c3e50);
|
||
|
|
border: none;
|
||
|
|
height: 50px;
|
||
|
|
font-size: 16px;
|
||
|
|
font-weight: bold;
|
||
|
|
margin-bottom: 10px;
|
||
|
|
box-shadow: 0 8px 20px rgba(74, 95, 122, 0.3);
|
||
|
|
transition: all 0.3s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.save-button:hover {
|
||
|
|
transform: translateY(-2px);
|
||
|
|
box-shadow: 0 12px 25px rgba(74, 95, 122, 0.4);
|
||
|
|
}
|
||
|
|
|
||
|
|
.save-button:active {
|
||
|
|
transform: translateY(0);
|
||
|
|
box-shadow: 0 5px 15px rgba(74, 95, 122, 0.3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.send-button {
|
||
|
|
background: linear-gradient(135deg, #00D4FF, #1D3B5A);
|
||
|
|
border: none;
|
||
|
|
height: 50px;
|
||
|
|
font-size: 16px;
|
||
|
|
font-weight: bold;
|
||
|
|
box-shadow: 0 8px 20px rgba(0, 212, 255, 0.3);
|
||
|
|
transition: all 0.3s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.send-button:hover {
|
||
|
|
transform: translateY(-2px);
|
||
|
|
box-shadow: 0 12px 25px rgba(0, 212, 255, 0.4);
|
||
|
|
}
|
||
|
|
|
||
|
|
.send-button:active {
|
||
|
|
transform: translateY(0);
|
||
|
|
box-shadow: 0 5px 15px rgba(0, 212, 255, 0.3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.preset-button {
|
||
|
|
margin: 5px;
|
||
|
|
box-shadow: 0 4px 10px rgba(0, 212, 255, 0.2);
|
||
|
|
transition: all 0.3s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.preset-button:hover {
|
||
|
|
transform: translateY(-2px);
|
||
|
|
box-shadow: 0 6px 15px rgba(0, 212, 255, 0.3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.media-uploader .van-button {
|
||
|
|
box-shadow: 0 4px 10px rgba(0, 212, 255, 0.2);
|
||
|
|
transition: all 0.3s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.media-uploader .van-button:hover {
|
||
|
|
transform: translateY(-2px);
|
||
|
|
box-shadow: 0 6px 15px rgba(0, 212, 255, 0.3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.event-picker {
|
||
|
|
padding: 20px;
|
||
|
|
height: 100%;
|
||
|
|
overflow-y: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.event-picker h3 {
|
||
|
|
margin: 0 0 15px;
|
||
|
|
font-size: 18px;
|
||
|
|
}
|
||
|
|
</style>
|