Initial commit
This commit is contained in:
686
app/components/marketing/email/forms/EmailForm1.vue
Normal file
686
app/components/marketing/email/forms/EmailForm1.vue
Normal file
@@ -0,0 +1,686 @@
|
||||
<template>
|
||||
<div class="pr-4">
|
||||
<form @submit.prevent="handleSendEmail">
|
||||
<!-- Receiver Section -->
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
<span class="icon-text">
|
||||
<span>Người nhận {{ selectedName ? `(Đang mở: ${selectedName})` : "" }}<b class="ml-1 has-text-danger">*</b></span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="field is-flex">
|
||||
<div class="control is-expanded pr-3 is-flex-grow-1">
|
||||
<input
|
||||
ref="emailTextareaRef"
|
||||
class="input"
|
||||
name="receiver"
|
||||
id="receiver"
|
||||
style="height: 100%"
|
||||
v-model="formData.content.receiver"
|
||||
placeholder="Nhập địa chỉ email người nhận, ngăn cách bằng dấu chấm phẩy (;)"
|
||||
@input="handleInputChange('receiver', $event)"
|
||||
></input>
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="buttons is-flex is-flex-wrap-nowrap">
|
||||
<button
|
||||
v-if="$getEditRights()"
|
||||
type="button"
|
||||
class="button is-light"
|
||||
@click="handleOpenModal('save-list')"
|
||||
style="border-radius: 8px; width: 100%"
|
||||
>
|
||||
<span>Lưu</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-info"
|
||||
@click="handleOpenModal('open-list')"
|
||||
style="border-radius: 8px; width: 100%"
|
||||
>
|
||||
<span>Mở</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subject Section -->
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
<span class="icon-text">
|
||||
<span>Tiêu đề<b class="ml-1 has-text-danger">*</b></span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
type="text"
|
||||
id="subject"
|
||||
name="subject"
|
||||
class="input"
|
||||
v-model="formData.content.subject"
|
||||
placeholder="Nhập tiêu đề email..."
|
||||
required
|
||||
@input="handleInputChange('subject', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Section -->
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
<span class="icon-text">
|
||||
<span>Nội dung</span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="control">
|
||||
<div class="box" style="padding: 0">
|
||||
<Editor
|
||||
:key="quillKey"
|
||||
:text="formData.content.content"
|
||||
pagename="pagedataemail"
|
||||
:height="'300px'"
|
||||
@content="handleQuillChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image URL Section -->
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
<span class="icon-text">
|
||||
<span>Đường dẫn hình ảnh</span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
type="url"
|
||||
id="imageUrl"
|
||||
name="imageUrl"
|
||||
class="input"
|
||||
placeholder="https://hinhanh.com/image.jpg"
|
||||
@input="handleInputChange('imageUrl', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Link URLs Section -->
|
||||
<div class="field">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<label class="label mb-0">
|
||||
<span class="icon-text">
|
||||
<span>Đường dẫn liên kết</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<button type="button" @click="addLinkUrl" class="button is-normal">
|
||||
<span class="icon">
|
||||
<SvgIcon v-bind="{ name: 'add4.svg', type: 'black', size: 16 }"></SvgIcon>
|
||||
</span>
|
||||
<span>Thêm liên kết</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(link, index) in formData.content.linkUrl"
|
||||
:key="index"
|
||||
class="field columns has-addons mb-2"
|
||||
:class="`${formData.content.linkUrl.length > 1 ? 'pr-3' : ''}`"
|
||||
>
|
||||
<div class="control column is-6 pr-0 is-expanded">
|
||||
<input
|
||||
type="url"
|
||||
class="input"
|
||||
style="border-radius: 8px"
|
||||
v-model="formData.content.linkUrl[index]"
|
||||
:placeholder="`Liên kết ${index + 1}: https://example.com`"
|
||||
@input="props.onDataChange(formData)"
|
||||
/>
|
||||
</div>
|
||||
<div class="control column is-6 is-expanded">
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
style="border-radius: 8px"
|
||||
v-model="formData.content.textLinkUrl[index]"
|
||||
:placeholder="`Tên ${index + 1}`"
|
||||
@input="props.onDataChange(formData)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="control column is-1 p-0"
|
||||
v-if="formData.content.linkUrl.length > 1"
|
||||
style="align-self: center; width: 40px"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@click="removeLinkUrl(index)"
|
||||
class="button is-danger has-text-white"
|
||||
style="border-radius: 8px"
|
||||
>
|
||||
<span class="icon">
|
||||
<SvgIcon v-bind="{ name: 'trash.svg', type: 'white', size: 16 }"></SvgIcon>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Keywords Section -->
|
||||
<div v-if="detectedKeywords.length > 0" class="field">
|
||||
<label class="label">
|
||||
<span class="icon-text">
|
||||
<span>Từ khóa</span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="box">
|
||||
<div v-for="(keyword, index) in detectedKeywords" :key="index" class="field has-addons mb-2 columns">
|
||||
<div class="control column is-4 pr-0 pb-0">
|
||||
<span style="width: 100%" class="button is-static is-small">{{ keyword }}</span>
|
||||
</div>
|
||||
<div class="control is-expanded column is-8 pb-0">
|
||||
<input
|
||||
type="text"
|
||||
class="input is-small"
|
||||
:value="getKeywordValue(keyword)"
|
||||
:placeholder="`Nhập giá trị cho ${keyword}`"
|
||||
@input="handleKeywordValueChange(keyword, ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button class="button is-primary has-text-white" :class="{ 'is-loading': loading }" :disabled="loading">
|
||||
<span v-if="loading">Đang gửi</span>
|
||||
<template v-else>
|
||||
<span class="icon">
|
||||
<SvgIcon v-bind="{ name: 'send.svg', type: 'white', size: 18 }" />
|
||||
</span>
|
||||
<span>Gửi email</span>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<Modal @dataevent="handleRowClick" @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal"></Modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useNuxtApp } from "nuxt/app";
|
||||
import { render } from "@vue-email/render";
|
||||
const nuxtApp = useNuxtApp();
|
||||
|
||||
const $snackbar = nuxtApp.$snackbar as (message?: string) => void;
|
||||
const $getpath = nuxtApp.$getpath as (message?: string) => void;
|
||||
const $getEditRights = nuxtApp.$getEditRights as () => boolean;
|
||||
|
||||
// const { $getpath, $snackbar} = useNuxtApp()
|
||||
|
||||
import { ref, watch, nextTick } from "vue";
|
||||
|
||||
import axios from "axios";
|
||||
import Modal from "@/components/Modal.vue";
|
||||
import Template1 from "@/lib/email/templates/Template1.vue";
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import Editor from "@/components/common/Editor.vue";
|
||||
|
||||
const apiUrl = `${$getpath()}data`;
|
||||
const sendEmailUrl = `${$getpath()}send-email`;
|
||||
const showmodal = ref();
|
||||
|
||||
// Types
|
||||
interface KeywordItem {
|
||||
keyword: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface FormContent {
|
||||
receiver: string;
|
||||
subject: string;
|
||||
content: string;
|
||||
imageUrl: string | null;
|
||||
linkUrl: string[];
|
||||
textLinkUrl: string[];
|
||||
keyword: KeywordItem[];
|
||||
html: string;
|
||||
}
|
||||
|
||||
interface FormData {
|
||||
id?: number;
|
||||
name: string;
|
||||
template: string;
|
||||
content: FormContent;
|
||||
}
|
||||
|
||||
type ModalType = "none" | "save-list" | "open-list";
|
||||
|
||||
interface Props {
|
||||
onDataChange: (data: FormData) => void;
|
||||
initialData?: FormData;
|
||||
}
|
||||
|
||||
// Props
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
initialData: () => ({
|
||||
id: undefined,
|
||||
name: "",
|
||||
template: "template1",
|
||||
content: {
|
||||
receiver: "",
|
||||
subject: "",
|
||||
content: "",
|
||||
imageUrl: null,
|
||||
linkUrl: [""],
|
||||
textLinkUrl: [""],
|
||||
keyword: [{ keyword: "", value: "" }],
|
||||
html: "",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const selectedName = ref("");
|
||||
const selectedId = ref("");
|
||||
|
||||
// Refs
|
||||
const emailTextareaRef = ref<HTMLTextAreaElement | null>(null);
|
||||
const loading = ref(false);
|
||||
const modalType = ref<ModalType>("none");
|
||||
const formData = ref<FormData>(props.initialData);
|
||||
const quillKey = ref(0);
|
||||
const dataEmails = ref([]);
|
||||
const detectedKeywords = ref<string[]>([]);
|
||||
|
||||
const handleRowClick = (data: { email: string; name: string; id: string }) => {
|
||||
formData.value = {
|
||||
...formData.value,
|
||||
content: {
|
||||
...formData.value.content,
|
||||
receiver: data.email,
|
||||
},
|
||||
};
|
||||
props.onDataChange(formData.value);
|
||||
selectedName.value = data.name;
|
||||
selectedId.value = data.id;
|
||||
|
||||
nextTick(() => {
|
||||
emailTextareaRef.value?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
// Methods
|
||||
const extractKeywords = (content: string): string[] => {
|
||||
if (!content) return [];
|
||||
|
||||
const regex = /\{\{([^}]+)\}\}/g;
|
||||
const matches: string[] = [];
|
||||
let match: RegExpExecArray | null = null;
|
||||
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
if (match[1]) {
|
||||
matches.push(match[1].trim());
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(matches)];
|
||||
};
|
||||
|
||||
const getKeywordValue = (keyword: string): string => {
|
||||
const found = formData.value.content.keyword.find((kw) => kw.keyword === keyword);
|
||||
return found ? found.value : "";
|
||||
};
|
||||
|
||||
const handleInputChange = (field: keyof FormContent | "name" | "template", eventOrValue: Event | string) => {
|
||||
const value =
|
||||
typeof eventOrValue === "string" ? eventOrValue : (eventOrValue.target as HTMLInputElement)?.value ?? "";
|
||||
if (field === "receiver" && value.trim() === "") {
|
||||
selectedName.value = "";
|
||||
}
|
||||
|
||||
if (field === "name" || field === "template") {
|
||||
formData.value = { ...formData.value, [field]: value };
|
||||
} else {
|
||||
formData.value = {
|
||||
...formData.value,
|
||||
content: { ...formData.value.content, [field]: value },
|
||||
};
|
||||
}
|
||||
props.onDataChange(formData.value);
|
||||
};
|
||||
|
||||
const handleQuillChange = (content: string) => {
|
||||
formData.value = {
|
||||
...formData.value,
|
||||
content: {
|
||||
...formData.value.content,
|
||||
content: content,
|
||||
},
|
||||
};
|
||||
props.onDataChange(formData.value);
|
||||
};
|
||||
|
||||
const handleLinkUrlChange = (index: number, eventOrValue: Event | string) => {
|
||||
const value =
|
||||
typeof eventOrValue === "string" ? eventOrValue : (eventOrValue.target as HTMLInputElement)?.value ?? "";
|
||||
const newLinkUrl = [...formData.value.content.linkUrl];
|
||||
newLinkUrl[index] = value;
|
||||
formData.value = {
|
||||
...formData.value,
|
||||
content: { ...formData.value.content, linkUrl: newLinkUrl },
|
||||
};
|
||||
props.onDataChange(formData.value);
|
||||
};
|
||||
|
||||
const handleTextLinkUrlChange = (index: number, eventOrValue: Event | string) => {
|
||||
const value =
|
||||
typeof eventOrValue === "string" ? eventOrValue : (eventOrValue.target as HTMLInputElement)?.value ?? "";
|
||||
const newTextLinkUrl = [...formData.value.content.textLinkUrl];
|
||||
newTextLinkUrl[index] = value;
|
||||
formData.value = {
|
||||
...formData.value,
|
||||
content: { ...formData.value.content, textLinkUrl: newTextLinkUrl },
|
||||
};
|
||||
props.onDataChange(formData.value);
|
||||
};
|
||||
|
||||
const handleKeywordValueChange = (keyword: string, eventOrValue: Event | string) => {
|
||||
const value =
|
||||
typeof eventOrValue === "string" ? eventOrValue : (eventOrValue.target as HTMLInputElement)?.value ?? "";
|
||||
const keywordIndex = formData.value.content.keyword.findIndex((kw) => kw.keyword === keyword);
|
||||
if (keywordIndex !== -1) {
|
||||
const newKeywords = [...formData.value.content.keyword];
|
||||
const keywordItem = newKeywords[keywordIndex];
|
||||
if (keywordItem) {
|
||||
keywordItem.value = value;
|
||||
formData.value = {
|
||||
...formData.value,
|
||||
content: { ...formData.value.content, keyword: newKeywords },
|
||||
};
|
||||
props.onDataChange(formData.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addLinkUrl = () => {
|
||||
formData.value = {
|
||||
...formData.value,
|
||||
content: {
|
||||
...formData.value.content,
|
||||
linkUrl: [...formData.value.content.linkUrl, ""],
|
||||
textLinkUrl: [...formData.value.content.textLinkUrl, ""],
|
||||
},
|
||||
};
|
||||
props.onDataChange(formData.value);
|
||||
};
|
||||
|
||||
const removeLinkUrl = (index: number) => {
|
||||
if (formData.value.content.linkUrl.length > 1) {
|
||||
const newLinkUrl = formData.value.content.linkUrl.filter((_, i) => i !== index);
|
||||
const newTextLinkUrl = formData.value.content.textLinkUrl.filter((_, i) => i !== index);
|
||||
formData.value = {
|
||||
...formData.value,
|
||||
content: { ...formData.value.content, linkUrl: newLinkUrl, textLinkUrl: newTextLinkUrl },
|
||||
};
|
||||
props.onDataChange(formData.value);
|
||||
}
|
||||
};
|
||||
|
||||
const getSelectedTemplate = (previewMode = true) => {
|
||||
const templateProps = {
|
||||
content: {
|
||||
receiver: formData.value.content.receiver || "",
|
||||
subject: formData.value.content.subject || "Thông báo mới",
|
||||
message: formData.value.content.content || "Bạn có một thông báo mới.",
|
||||
imageUrl: formData.value.content.imageUrl || null,
|
||||
linkUrl: formData.value.content.linkUrl.filter((link) => link.trim() !== ""),
|
||||
textLinkUrl: formData.value.content.textLinkUrl.filter((text) => text.trim() !== ""),
|
||||
keyword: formData.value.content.keyword,
|
||||
},
|
||||
previewMode: previewMode,
|
||||
};
|
||||
|
||||
// Switch between different templates based on formData.template
|
||||
let component;
|
||||
switch (formData.value.template) {
|
||||
case "template1":
|
||||
component = Template1;
|
||||
break;
|
||||
default:
|
||||
component = Template1;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
component,
|
||||
props: templateProps,
|
||||
};
|
||||
};
|
||||
|
||||
const handleSendEmail = async () => {
|
||||
if (!formData.value.content.receiver.trim()) {
|
||||
$snackbar("Validation Error: Please enter a receiver email address");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
// Get template component and props
|
||||
const { component, props: templateProps } = getSelectedTemplate();
|
||||
|
||||
// Render Vue component to HTML string
|
||||
let emailHtml = await render(component, templateProps);
|
||||
|
||||
// If no image URL provided, remove image section from HTML
|
||||
if ((formData.value.content.imageUrl ?? "").trim() === "") {
|
||||
emailHtml = emailHtml.replace(/<tr\s+class=["']header-row["'][^>]*>[\s\S]*?<\/tr>/gi, "");
|
||||
emailHtml = emailHtml.replace(/\n\s*\n\s*\n/g, "\n\n");
|
||||
}
|
||||
|
||||
// Replace keywords in HTML
|
||||
let finalEmailHtml = emailHtml;
|
||||
if (formData.value.content.keyword && formData.value.content.keyword.length > 0) {
|
||||
formData.value.content.keyword.forEach(({ keyword, value }) => {
|
||||
if (keyword && value) {
|
||||
const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, "g");
|
||||
finalEmailHtml = finalEmailHtml.replace(regex, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const response = await nuxtApp.$insertapi("sendemail", {
|
||||
to: formData.value.content.receiver,
|
||||
content: finalEmailHtml,
|
||||
subject: formData.value.content.subject || "Thông báo từ BigDataTech",
|
||||
}, undefined, false
|
||||
);
|
||||
|
||||
if (response !== null) {
|
||||
$snackbar(`Email sent successfully! Sent to ${formData.value.content.receiver.split(";").length} recipient(s)`);
|
||||
|
||||
// Reset form
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
name: "",
|
||||
template: formData.value.template,
|
||||
content: {
|
||||
receiver: "",
|
||||
subject: "",
|
||||
content: "",
|
||||
imageUrl: null,
|
||||
linkUrl: [""],
|
||||
textLinkUrl: [""],
|
||||
keyword: [{ keyword: "", value: "" }],
|
||||
html: "",
|
||||
},
|
||||
};
|
||||
props.onDataChange(formData.value);
|
||||
quillKey.value++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending email:", error);
|
||||
const errorMsg = axios.isAxiosError(error)
|
||||
? error.response?.data?.error || error.message
|
||||
: "Unknown error occurred";
|
||||
|
||||
$snackbar(`Failed to send email: ${errorMsg}`);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenModal = async (type: ModalType) => {
|
||||
modalType.value = type;
|
||||
await getData();
|
||||
console.log(modalType.value);
|
||||
if (type === "save-list") {
|
||||
showmodal.value = {
|
||||
component: "marketing/email/dataGmail/SaveListGmail",
|
||||
title: "Lưu danh sách email",
|
||||
width: "450px",
|
||||
height: "350px",
|
||||
vbind: {
|
||||
emails: formData.value.content.receiver,
|
||||
name: selectedName.value,
|
||||
id: selectedId.value,
|
||||
onClose: handleCloseModal,
|
||||
},
|
||||
};
|
||||
} else if (type === "open-list") {
|
||||
showmodal.value = {
|
||||
component: "marketing/email/dataGmail/OpenListGmail",
|
||||
title: "Danh sách email",
|
||||
width: "95%",
|
||||
height: "auto",
|
||||
vbind: {
|
||||
dataEmail: dataEmails.value,
|
||||
loading: loading.value,
|
||||
onClose: handleCloseModal,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
modalType.value = "none";
|
||||
showmodal.value = undefined;
|
||||
};
|
||||
|
||||
const getData = async () => {
|
||||
loading.value = true;
|
||||
const emailListUrl = `${apiUrl}/Email_List/`;
|
||||
|
||||
try {
|
||||
const response = await axios.get(emailListUrl);
|
||||
if (response.status === 200) {
|
||||
console.log(response);
|
||||
dataEmails.value = response.data.rows;
|
||||
|
||||
console.log(dataEmails.value);
|
||||
} else {
|
||||
throw new Error("Failed to fetch email list");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching email list:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditList = (id: string, name: string, emails: string) => {
|
||||
modalType.value = "save-list";
|
||||
};
|
||||
|
||||
// Watchers
|
||||
// watch(
|
||||
// () => selectedEmails.value,
|
||||
// (newValue) => {
|
||||
// if (newValue) {
|
||||
// formData.value = {
|
||||
// ...formData.value,
|
||||
// content: {
|
||||
// ...formData.value.content,
|
||||
// receiver: newValue,
|
||||
// },
|
||||
// }
|
||||
// props.onDataChange(formData.value)
|
||||
|
||||
// nextTick(() => {
|
||||
// emailTextareaRef.value?.focus()
|
||||
// })
|
||||
|
||||
// setSelectedEmails('')
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
|
||||
watch(
|
||||
() => props.initialData,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
formData.value = { ...formData.value, ...newValue };
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => formData.value.content.content,
|
||||
(newValue) => {
|
||||
const keywords = extractKeywords(newValue);
|
||||
detectedKeywords.value = keywords;
|
||||
|
||||
if (keywords.length > 0) {
|
||||
const newKeywords = keywords.map((keyword) => {
|
||||
const existing = formData.value.content.keyword.find((k) => k.keyword === keyword);
|
||||
return existing || { keyword, value: "" };
|
||||
});
|
||||
|
||||
formData.value = {
|
||||
...formData.value,
|
||||
content: {
|
||||
...formData.value.content,
|
||||
keyword: newKeywords,
|
||||
},
|
||||
};
|
||||
props.onDataChange(formData.value);
|
||||
} else {
|
||||
formData.value = {
|
||||
...formData.value,
|
||||
content: {
|
||||
...formData.value.content,
|
||||
keyword: [{ keyword: "", value: "" }],
|
||||
},
|
||||
};
|
||||
props.onDataChange(formData.value);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.ql-toolbar) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
:deep(.ql-container) {
|
||||
height: calc(100% - 42px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user