|
|
@@ -0,0 +1,375 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import '@wangeditor/editor/dist/css/style.css'
|
|
|
+import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
|
|
+import { i18nChangeLanguage, DomEditor } from '@wangeditor/editor'
|
|
|
+import { formatTimezone } from '@/utils/tools'
|
|
|
+
|
|
|
+i18nChangeLanguage('en')
|
|
|
+
|
|
|
+const visible = ref(true)
|
|
|
+const openDialog = (row) => {
|
|
|
+ visible.value = true
|
|
|
+}
|
|
|
+const props = defineProps({
|
|
|
+ data: Object
|
|
|
+})
|
|
|
+
|
|
|
+const emailData = ref({
|
|
|
+ email: '',
|
|
|
+ ccEmail: '',
|
|
|
+ serial_no: ''
|
|
|
+})
|
|
|
+
|
|
|
+const emailRecords: any = ref([
|
|
|
+ {
|
|
|
+ name: 'John Doe',
|
|
|
+ content: 'This is a test email content.',
|
|
|
+ creatTime: '2024-06-01T16:25:31Z'
|
|
|
+ }
|
|
|
+])
|
|
|
+watch(
|
|
|
+ () => props.data,
|
|
|
+ (newVal) => {
|
|
|
+ if (newVal) {
|
|
|
+ const email = newVal?.email
|
|
|
+ emailData.value.email = email?.email
|
|
|
+ emailData.value.ccEmail = email?.cc_email
|
|
|
+ emailData.value.serial_no = newVal?.serial_no
|
|
|
+ emailRecords.value = email?.emailRecords
|
|
|
+ }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ immediate: true,
|
|
|
+ deep: true
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
+const editorRef = shallowRef()
|
|
|
+const mode = ref('default')
|
|
|
+// 内容 HTML
|
|
|
+const valueHtml = ref('')
|
|
|
+
|
|
|
+const toolbarConfig = {
|
|
|
+ excludeKeys: [
|
|
|
+ 'headerSelect',
|
|
|
+ 'italic',
|
|
|
+ 'fontFamily',
|
|
|
+ 'lineHeight',
|
|
|
+ 'group-more-style', // 排除菜单组,写菜单组 key 的值即可
|
|
|
+ 'group-video', // 插入视频
|
|
|
+ 'insertTable', // 插入表格
|
|
|
+ 'codeBlock', // 代码块
|
|
|
+ 'fullScreen' // 全屏
|
|
|
+ ]
|
|
|
+}
|
|
|
+const editorConfig = {
|
|
|
+ MENU_CONF: {
|
|
|
+ uploadImage: {
|
|
|
+ server: '/api/upload',
|
|
|
+ base64LimitSize: 5 * 1024 * 1024
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 组件销毁时,也及时销毁编辑器
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ const editor = editorRef.value
|
|
|
+ if (editor == null) return
|
|
|
+ editor.destroy()
|
|
|
+})
|
|
|
+
|
|
|
+const handleCreated = (editor: any) => {
|
|
|
+ editorRef.value = editor // 记录 editor 实例,重要!
|
|
|
+ // 在 nextTick 中获取 toolbar 实例
|
|
|
+ nextTick(() => {
|
|
|
+ const toolbar = DomEditor.getToolbar(editor)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const editorIconList = [
|
|
|
+ {
|
|
|
+ dataMenuKey: 'blockquote',
|
|
|
+ svgUrl: () => import('@/icons/icon_quotes_b.svg')
|
|
|
+ },
|
|
|
+ {
|
|
|
+ dataMenuKey: 'bold',
|
|
|
+ svgUrl: () => import('@/icons/icon_bold_b.svg')
|
|
|
+ },
|
|
|
+ // {
|
|
|
+ // dataMenuKey: 'underline',
|
|
|
+ // svgUrl: () => import('@/icons/icon_underline_b.svg')
|
|
|
+ // },
|
|
|
+ {
|
|
|
+ dataMenuKey: 'undo',
|
|
|
+ svgUrl: () => import('@/icons/icon_revoke__b.svg')
|
|
|
+ },
|
|
|
+ {
|
|
|
+ dataMenuKey: 'redo',
|
|
|
+ svgUrl: () => import('@/icons/icon_redo_b.svg')
|
|
|
+ }
|
|
|
+]
|
|
|
+// Vue 组件生命周期钩子函数
|
|
|
+onMounted(async () => {
|
|
|
+ for (const item of editorIconList) {
|
|
|
+ const svgModule = await item.svgUrl()
|
|
|
+ const svgUrl = svgModule.default // 获取 SVG 文件的 URL
|
|
|
+ replaceSvgByDataKey(item.dataMenuKey, svgUrl)
|
|
|
+ }
|
|
|
+})
|
|
|
+const replaceSvgByDataKey = (dataMenuKey: any, svgUrl: any) => {
|
|
|
+ const observer = new MutationObserver((mutationsList, observer) => {
|
|
|
+ const element = document.querySelector(`[data-menu-key="${dataMenuKey}"]`)
|
|
|
+ if (element) {
|
|
|
+ // 获取 SVG 内容
|
|
|
+ fetch(svgUrl)
|
|
|
+ .then((res) => res.text())
|
|
|
+ .then((svgContent) => {
|
|
|
+ // 查找旧的 SVG 标签
|
|
|
+ const oldSvg = element.querySelector('svg')
|
|
|
+ if (oldSvg) {
|
|
|
+ oldSvg.outerHTML = svgContent // 替换 SVG
|
|
|
+ }
|
|
|
+ })
|
|
|
+ observer.disconnect() // 找到元素后停止观察
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ observer.observe(document.body, {
|
|
|
+ childList: true,
|
|
|
+ subtree: true
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const handleFocusEditor = () => {
|
|
|
+ editorRef.value.focus()
|
|
|
+}
|
|
|
+
|
|
|
+const sendEmail = () => {
|
|
|
+ const html = editorRef.value.getHtml()
|
|
|
+ const text = editorRef.value.getText()
|
|
|
+ $api
|
|
|
+ .sendEmailApi({
|
|
|
+ action: 'ocean_booking',
|
|
|
+ email: emailData.value.email,
|
|
|
+ communication_cc: emailData.value.ccEmail,
|
|
|
+ serial_no: emailData.value.serial_no,
|
|
|
+ content: html,
|
|
|
+ text
|
|
|
+ })
|
|
|
+ .then((res: any) => {
|
|
|
+ if (res.code === 200) {
|
|
|
+ ElMessage.success('Email sent successfully')
|
|
|
+ emailRecords.value = res.data.emailRecords
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ ElMessage.error('Failed to send email')
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+defineExpose({
|
|
|
+ openDialog
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <el-dialog
|
|
|
+ title="Booking Detail"
|
|
|
+ class="booking-detail-email-dialog"
|
|
|
+ v-model="visible"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ width="1000px"
|
|
|
+ top="10vh"
|
|
|
+ >
|
|
|
+ <div class="email-view">
|
|
|
+ <div class="email-path">
|
|
|
+ <span class="font_family icon-icon_email_b" style="font-size: 18px"></span>
|
|
|
+ <span class="label">Communicate with us: </span>
|
|
|
+ <span class="content">{{ emailData.email }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="separated-by">
|
|
|
+ <el-input v-model="emailData.ccEmail">
|
|
|
+ <template #prefix>
|
|
|
+ <div
|
|
|
+ style="
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ color: var(--color-neutral-1);
|
|
|
+ cursor: default;
|
|
|
+ "
|
|
|
+ >
|
|
|
+ <span style="font-weight: 600">CC:</span>
|
|
|
+ <el-tooltip
|
|
|
+ class="box-item"
|
|
|
+ effect="dark"
|
|
|
+ content="Separated by;"
|
|
|
+ placement="top-start"
|
|
|
+ :offset="-8"
|
|
|
+ >
|
|
|
+ <span class="font_family icon-icon_tipsfilled_b" style="font-size: 19px"></span>
|
|
|
+ </el-tooltip>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-input>
|
|
|
+ </div>
|
|
|
+ <div class="text-editor">
|
|
|
+ <Toolbar
|
|
|
+ style="border-bottom: 1px solid #ccc"
|
|
|
+ :editor="editorRef"
|
|
|
+ :defaultConfig="toolbarConfig"
|
|
|
+ :mode="mode"
|
|
|
+ />
|
|
|
+ <Editor
|
|
|
+ v-model="valueHtml"
|
|
|
+ :defaultConfig="editorConfig"
|
|
|
+ :mode="mode"
|
|
|
+ @onCreated="handleCreated"
|
|
|
+ @click="handleFocusEditor"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <el-button
|
|
|
+ @click="sendEmail"
|
|
|
+ class="el-button--dark"
|
|
|
+ style="float: right; margin: 8px 0 14px 0; height: 40px"
|
|
|
+ ><span class="font_family icon-icon_submit_b" style="margin-right: 4px"></span> Send
|
|
|
+ Email</el-button
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-divider style="margin: 16px 0; border-top-color: var(--color-divider)" />
|
|
|
+ <div class="show-records">
|
|
|
+ <div class="record-item" v-for="(item, index) in emailRecords" :key="index">
|
|
|
+ <div class="header">
|
|
|
+ <div class="avatar">
|
|
|
+ <div>{{ item.name?.slice(0, 1) }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="name">{{ item.name }}</div>
|
|
|
+ <div class="date">{{ formatTimezone(item.creatTime) }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="content">
|
|
|
+ {{ item.content }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.email-view {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+
|
|
|
+ padding: 16px;
|
|
|
+ padding-bottom: 0;
|
|
|
+ border-radius: 12px;
|
|
|
+ background: var(--color-email-bg);
|
|
|
+
|
|
|
+ .show-records {
|
|
|
+ max-height: 370px;
|
|
|
+ overflow: auto;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.w-e-text-container) {
|
|
|
+ min-height: 170px;
|
|
|
+ border-radius: 0 0 6px 6px;
|
|
|
+ p {
|
|
|
+ margin: 0px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.email-path {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+
|
|
|
+ & > .label {
|
|
|
+ margin-left: 8px;
|
|
|
+ padding-top: 2px;
|
|
|
+ color: var(--color-neutral-1);
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ & > .content {
|
|
|
+ display: inline-block;
|
|
|
+ flex: 1;
|
|
|
+ padding-top: 2px;
|
|
|
+ line-height: 18px;
|
|
|
+ color: var(--color-theme);
|
|
|
+ word-break: break-all;
|
|
|
+ }
|
|
|
+}
|
|
|
+.separated-by {
|
|
|
+ :deep(.el-input__wrapper) {
|
|
|
+ box-shadow: 0 0 0 1px var(--color-email-border) inset;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.text-editor {
|
|
|
+ margin-top: 16px;
|
|
|
+ border-radius: 6px;
|
|
|
+ border: 1px solid var(--color-email-border);
|
|
|
+ // overflow: hidden;
|
|
|
+ :deep(div.w-e-toolbar) {
|
|
|
+ border-radius: 6px 6px 0 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.record-item {
|
|
|
+ margin-top: 16px;
|
|
|
+
|
|
|
+ & > .header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 0px 16px 8px;
|
|
|
+ padding-left: 0px;
|
|
|
+
|
|
|
+ .avatar {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ text-align: center;
|
|
|
+ border-radius: 50%;
|
|
|
+ background-color: var(--color-theme);
|
|
|
+ div {
|
|
|
+ height: 14px;
|
|
|
+ line-height: 14px;
|
|
|
+ color: var(--color-avatar);
|
|
|
+ font-weight: 700;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .name {
|
|
|
+ height: 24px;
|
|
|
+ margin-left: 4px;
|
|
|
+ margin-right: 8px;
|
|
|
+ line-height: 24px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: var(--color-neutral-1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .date {
|
|
|
+ height: 24px;
|
|
|
+ line-height: 24px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--color-neutral-2);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ & > .content {
|
|
|
+ padding: 16px 6px;
|
|
|
+ background-color: var(--color-share-link-bg);
|
|
|
+ border-radius: 6px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.text-editor) {
|
|
|
+ & > div:first-of-type {
|
|
|
+ border-bottom: 1px solid var(--color-email-border) !important;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|