This commit is contained in:
Viet An
2026-07-09 16:45:48 +07:00
parent 42124d4071
commit df4ed4bd4d
20 changed files with 726 additions and 964 deletions

View File

@@ -1,329 +1,322 @@
<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)"
/>
</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">
<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="has-text-danger">*</b></span>
</span>
</label>
<div class="field has-addons">
<div class="control is-expanded">
<input
type="text"
id="subject"
name="subject"
ref="emailTextareaRef"
class="input"
v-model="formData.content.subject"
placeholder="Nhập tiêu đề email..."
required
@input="handleInputChange('subject', $event)"
name="receiver"
id="receiver"
v-model="formData.content.receiver"
@input="handleInputChange('receiver', $event)"
placeholder="Nhập địa chỉ email người nhận, ngăn cách bằng dấu chấm phẩy (;)"
title="Nhập địa chỉ email người nhận, ngăn cách bằng dấu chấm phẩy (;)"
/>
</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"
<button
v-if="$getEditRights()"
@click="handleOpenModal('save-list')"
class="button is-primary is-light"
type="button"
>
<Editor
:key="quillKey"
:text="formData.content.content"
pagename="pagedataemail"
:height="'300px'"
@content="handleQuillChange"
/>
</div>
<span class="icon">
<Icon
name="material-symbols:save-outline-rounded"
:size="20"
/>
</span>
</button>
<span class="tooltiptext left-auto right-0">Lưu</span>
</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)"
/>
<button
@click="handleOpenModal('open-list')"
class="button is-info is-light"
type="button"
>
<span class="icon">
<Icon
name="material-symbols:list-alt-outline-rounded"
:size="20"
/>
</span>
</button>
<span class="tooltiptext left-auto right-0">Xem danh sách </span>
</div>
</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">
<Icon name="material-symbols:add-rounded" />
</span>
<span>Thêm liên kết</span>
</button>
</div>
<!-- Subject Section -->
<div class="field">
<label class="label">
<span class="icon-text">
<span>Tiêu đề <b class="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">
<Editor
:key="quillKey"
:text="formData.content.content"
style="height: 300px"
@content="handleQuillChange"
/>
</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
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"
>
<div class="level-right">
<div class="level-item">
<button
@click="addLinkUrl"
class="button is-primary is-light"
type="button"
@click="removeLinkUrl(index)"
class="button is-danger has-text-white"
style="border-radius: 8px"
>
<span class="icon">
<Icon name="material-symbols:delete-outline-rounded" />
<Icon
name="material-symbols:add-rounded"
:size="18"
/>
</span>
<span>Thêm liên kết</span>
</button>
</div>
</div>
</div>
<!-- Keywords Section -->
<div
v-if="detectedKeywords.length > 0"
class="field"
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' : ''}`"
>
<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 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>
<!-- Submit Button -->
<div class="field">
<div class="control">
<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
class="button is-primary has-text-white"
:class="{ 'is-loading': loading }"
:disabled="loading"
type="button"
@click="removeLinkUrl(index)"
class="button is-danger has-text-white"
style="border-radius: 8px"
>
<span v-if="loading">Đang gửi</span>
<template v-else>
<span class="icon">
<Icon name="material-symbols:send-rounded" />
</span>
<span>Gửi email</span>
</template>
<span class="icon">
<Icon name="material-symbols:delete-outline-rounded" />
</span>
</button>
</div>
</div>
</form>
</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.value)"
/>
</div>
</div>
</div>
</div>
<!-- Submit Button -->
<div class="field">
<div class="control is-flex is-justify-content-flex-end">
<button :class="['button is-primary has-text-white', loading && 'is-loading']">
<span v-if="loading">Đang gửi</span>
<template v-else>
<span class="icon">
<Icon name="material-symbols:send-rounded" />
</span>
<span>Gửi email</span>
</template>
</button>
</div>
</div>
</form>
<Modal
@dataevent="handleRowClick"
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
></Modal>
/>
</template>
<script setup lang="ts">
<script setup>
import { render } from "@vue-email/render";
import Modal from "~/components/Modal.vue";
import Template1 from "~/components/marketing/email/Template1.vue";
import Editor from "~/components/common/Editor.vue";
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 apiUrl = `${$getpath()}data`;
const { $getdata, $getEditRights, $snackbar } = useNuxtApp();
const showmodal = ref();
// Types
interface KeywordItem {
keyword: string;
value: string;
}
// 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 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;
}
// interface FormData {
// id?: number;
// name: string;
// template: string;
// content: FormContent;
// }
type ModalType = "none" | "save-list" | "open-list";
// type ModalType = "none" | "save-list" | "open-list";
interface Props {
onDataChange: (data: FormData) => void;
initialData?: FormData;
}
// 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 props = defineProps({
onDataChange: Function,
initialData: {
type: Object,
default: () => ({
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 emailTextareaRef = ref(null);
const loading = ref(false);
const modalType = ref<ModalType>("none");
const formData = ref<FormData>(props.initialData);
const modalType = ref("none");
const formData = ref(props.initialData);
const quillKey = ref(0);
const dataEmails = ref([]);
const detectedKeywords = ref<string[]>([]);
const detectedKeywords = ref([]);
const handleRowClick = (data: { email: string; name: string; id: string }) => {
const handleRowClick = (data /* : { email: string; name: string; id: string } */) => {
formData.value = {
...formData.value,
content: {
@@ -341,12 +334,12 @@ const handleRowClick = (data: { email: string; name: string; id: string }) => {
};
// Methods
const extractKeywords = (content: string): string[] => {
const extractKeywords = (content) => {
if (!content) return [];
const regex = /\{\{([^}]+)\}\}/g;
const matches: string[] = [];
let match: RegExpExecArray | null = null;
const matches = [];
let match = null;
while ((match = regex.exec(content)) !== null) {
if (match[1]) {
@@ -357,14 +350,13 @@ const extractKeywords = (content: string): string[] => {
return [...new Set(matches)];
};
const getKeywordValue = (keyword: string): string => {
const getKeywordValue = (keyword) => {
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 ?? "");
const handleInputChange = (field /* : keyof FormContent | "name" | "template" */, eventOrValue) => {
const value = typeof eventOrValue === "string" ? eventOrValue : (eventOrValue.target?.value ?? "");
if (field === "receiver" && value.trim() === "") {
selectedName.value = "";
}
@@ -380,7 +372,7 @@ const handleInputChange = (field: keyof FormContent | "name" | "template", event
props.onDataChange(formData.value);
};
const handleQuillChange = (content: string) => {
const handleQuillChange = (content) => {
formData.value = {
...formData.value,
content: {
@@ -391,9 +383,8 @@ const handleQuillChange = (content: string) => {
props.onDataChange(formData.value);
};
const handleLinkUrlChange = (index: number, eventOrValue: Event | string) => {
const value =
typeof eventOrValue === "string" ? eventOrValue : ((eventOrValue.target as HTMLInputElement)?.value ?? "");
const handleLinkUrlChange = (index, eventOrValue) => {
const value = typeof eventOrValue === "string" ? eventOrValue : (eventOrValue.target?.value ?? "");
const newLinkUrl = [...formData.value.content.linkUrl];
newLinkUrl[index] = value;
formData.value = {
@@ -403,9 +394,8 @@ const handleLinkUrlChange = (index: number, eventOrValue: Event | string) => {
props.onDataChange(formData.value);
};
const handleTextLinkUrlChange = (index: number, eventOrValue: Event | string) => {
const value =
typeof eventOrValue === "string" ? eventOrValue : ((eventOrValue.target as HTMLInputElement)?.value ?? "");
const handleTextLinkUrlChange = (index, eventOrValue) => {
const value = typeof eventOrValue === "string" ? eventOrValue : (eventOrValue.target?.value ?? "");
const newTextLinkUrl = [...formData.value.content.textLinkUrl];
newTextLinkUrl[index] = value;
formData.value = {
@@ -415,9 +405,8 @@ const handleTextLinkUrlChange = (index: number, eventOrValue: Event | string) =>
props.onDataChange(formData.value);
};
const handleKeywordValueChange = (keyword: string, eventOrValue: Event | string) => {
const value =
typeof eventOrValue === "string" ? eventOrValue : ((eventOrValue.target as HTMLInputElement)?.value ?? "");
const handleKeywordValueChange = (keyword, eventOrValue) => {
const value = typeof eventOrValue === "string" ? eventOrValue : (eventOrValue.target?.value ?? "");
const keywordIndex = formData.value.content.keyword.findIndex((kw) => kw.keyword === keyword);
if (keywordIndex !== -1) {
const newKeywords = [...formData.value.content.keyword];
@@ -445,7 +434,7 @@ const addLinkUrl = () => {
props.onDataChange(formData.value);
};
const removeLinkUrl = (index: number) => {
const removeLinkUrl = (index) => {
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);
@@ -563,10 +552,10 @@ const handleSendEmail = async () => {
}
};
const handleOpenModal = async (type: ModalType) => {
const handleOpenModal = async (type) => {
modalType.value = type;
await getData();
console.log(modalType.value);
await getEmailList();
if (type === "save-list") {
showmodal.value = {
component: "marketing/email/dataGmail/SaveListGmail",
@@ -584,7 +573,7 @@ const handleOpenModal = async (type: ModalType) => {
showmodal.value = {
component: "marketing/email/dataGmail/OpenListGmail",
title: "Danh sách email",
width: "95%",
width: "min(700px, 75%)",
height: "auto",
vbind: {
dataEmail: dataEmails.value,
@@ -600,24 +589,13 @@ const handleCloseModal = () => {
showmodal.value = undefined;
};
const getData = async () => {
async function getEmailList() {
loading.value = true;
const emailListUrl = `${apiUrl}/Email_List/`;
dataEmails.value = await $getdata("Email_List");
loading.value = false;
}
try {
const response = await $fetch(emailListUrl);
dataEmails.value = response.rows;
console.log(response);
console.log(dataEmails.value);
} catch (error) {
console.error("Error fetching email list:", error);
throw error;
} finally {
loading.value = false;
}
};
const handleEditList = (id: string, name: string, emails: string) => {
const handleEditList = (id, name, emails) => {
modalType.value = "save-list";
};