Initial commit

This commit is contained in:
Viet An
2026-03-02 09:45:33 +07:00
commit d17a9e2588
415 changed files with 92113 additions and 0 deletions

View 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>