<template> <div> <v-chart :key="treeCounter" :option="option" style="height: 90dvh; width: 90%" ref="chart" @mouseover="handleChartClick" lazy ></v-chart> <template v-if="showDiv"> <div v-if="showButtons" class="rounded shadow container-fluid p-2" @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" :style="{ position: 'absolute', top: position.top, left: position.left, backgroundColor: 'white', border: '1px solid #dee2e6', padding: '5px', width: '15em', }" > <div class="row me-auto tooltip-button"> <div class="col-9 d-flex border-bottom mb-1"> <button v-tooltip="`افزودن فرزند`" class="btn" style="color: #dee2e6" @click="onButtonClick('add')" > <svg class="icon icon-Component-133--1"> <use xlink:href="#icon-Component-133--1"></use> </svg> </button> <button v-tooltip="`ویرایش کردن`" class="btn" @click="onButtonClick('edit')" > <svg class="icon icon-Component-242--1"> <use xlink:href="#icon-Component-242--1"></use> </svg> </button> <button v-tooltip="`پاک کردن`" class="btn" @click="onButtonClick('delete')" > <svg class="icon icon-Component-295--1"> <use xlink:href="#icon-Component-295--1"></use> </svg> </button> </div> <div class="col-3 mb-1"> <button v-tooltip="`خارج شدن`" class="btn" @click="closeButtons"> <svg class="icon icon-Component-21--1"> <use xlink:href="#icon-Component-21--1"></use> </svg> </button> </div> </div> <div class="row my-3"> <div v-if="buttonAction == 'add'" class="col w-90 px-3"> <form> <div class="row align-items-center"> <div class="col-8 m-0"> <input type="text" class="form-control" id="inlineFormInputName" placeholder="کلمه جدید" v-model="addText" /> </div> <div class="col-2"> <button class="btn accept-button" @click.prevent="addNewChildren(addText)" > <svg class="icon icon-Component-233--1"> <use xlink:href="#icon-Component-233--1"></use> </svg> </button> </div> <div class="col-2"> <button class="btn reject-button" @click="buttonAction = ''"> <svg class="icon icon-Component-21--1"> <use xlink:href="#icon-Component-21--1"></use> </svg> </button> </div> </div> </form> </div> <div v-else-if="buttonAction == 'edit'" class="col w-90 px-3"> <form> <div class="row align-items-center"> <div class="col-8"> <input type="text" class="form-control" id="inlineFormInputName" :placeholder="tooltipText" v-model="editText" /> </div> <div class="col-2"> <button class="btn accept-button" @click.prevent="editNode(editText)" > <svg class="icon icon-Component-233--1"> <use xlink:href="#icon-Component-233--1"></use> </svg> </button> </div> <div class="col-2"> <button class="btn reject-button" @click="buttonAction = ''"> <svg class="icon icon-Component-21--1"> <use xlink:href="#icon-Component-21--1"></use> </svg> </button> </div> </div> </form> </div> <div v-else class="col"> <p>{{ tooltipText }}</p> </div> </div> </div> </template> </div> </template> <script> import researchApi from "@apis/researchApi"; import HttpService from "@services/httpService"; import { mapActions, mapState } from "pinia"; import { useCommonStore } from "~/stores/commonStore"; // import { use } from "echarts/core"; import { TreeChart } from "echarts/charts"; import { TooltipComponent, ToolboxComponent } from "echarts/components"; import { CanvasRenderer } from "echarts/renderers"; use([TooltipComponent, TreeChart, CanvasRenderer, ToolboxComponent]); /** * @vue-prop {Array} [dataTreeMap=] - آرایهای از دادههای نقشه درخت * * @vue-data {Number} treeCounter - شمارنده درخت * @vue-data {Boolean} showButtons - وضعیت نمایش دکمهها * @vue-data {Boolean} showDiv - وضعیت نمایش divدکمه ها * @vue-data {String} buttonAction - اقدام دکمه * @vue-data {String} addText - متن افزودن * @vue-data {String} editText - متن ویرایش * @vue-data {undefined|Object} httpService - سرویس HTTP * @vue-data {Object} itemData - داده آیتم * @vue-data {String} tooltipText - متن ابزارک راهنما * @vue-data {Object} position - موقعیت ابزارک راهنما * @vue-data {String} position.top - موقعیت عمودی ابزارک راهنما * @vue-data {String} position.left - موقعیت افقی ابزارک راهنما * @vue-data {Object} option - گزینههای پیکربندی نمودار * @vue-data {Object} option.tooltip - تنظیمات ابزارک راهنما * @vue-data {String} option.tooltip.trigger - نوع راهاندازی ابزارک راهنما * @vue-data {String} option.tooltip.triggerOn - رویدادی که ابزارک راهنما بر اساس آن راهاندازی میشود * @vue-data {Array} option.series - آرایهای از سریهای نمودار * @vue-data {Object} option.series[0] - سری اول نمودار * @vue-data {String} option.series[0].type - نوع سری (درخت) * @vue-data {Number} option.series[0].id - شناسه سری * @vue-data {String} option.series[0].name - نام سری * @vue-data {Array} option.series[0].data - دادههای سری * @vue-data {String} option.series[0].top - موقعیت بالای نمودار * @vue-data {String} option.series[0].left - موقعیت چپ نمودار * @vue-data {String} option.series[0].bottom - موقعیت پایین نمودار * @vue-data {String} option.series[0].right - موقعیت راست نمودار * @vue-data {String} option.series[0].while - عرض نمودار * @vue-data {String} option.series[0].height - ارتفاع نمودار * @vue-data {Number} option.series[0].zoom - میزان بزرگنمایی نمودار * @vue-data {Number} option.series[0].symbolSize - اندازه نماد * @vue-data {String} option.series[0].edgeForkPosition - موقعیت شاخههای لبه * @vue-data {String} option.series[0].edgeShape - شکل لبه * @vue-data {Number} option.series[0].initialTreeDepth - عمق اولیه درخت * @vue-data {String} option.series[0].orient - جهت نمودار * @vue-data {Object} option.series[0].lineStyle - سبک خط * @vue-data {Number} option.series[0].lineStyle.width - عرض خط * @vue-data {Object} option.series[0].label - برچسبها * @vue-data {String} option.series[0].label.backgroundColor - رنگ پسزمینه برچسب * @vue-data {Array} option.series[0].label.position - موقعیت برچسب * @vue-data {Number} option.series[0].label.distance - فاصله برچسب * @vue-data {String} option.series[0].label.verticalAlign - تراز عمودی برچسب * @vue-data {String} option.series[0].label.align - تراز برچسب * @vue-data {Number} option.series[0].label.borderWidth - عرض مرز برچسب * @vue-data {Number} option.series[0].label.borderDashOffset - فاصله نقطهچین مرز برچسب * @vue-data {Array} option.series[0].label.borderRadius - شعاع گوشههای مرز برچسب * @vue-data {Array} option.series[0].label.padding - پدینگ برچسب * @vue-data {Number} option.series[0].label.lineHeight - ارتفاع خط برچسب * @vue-data {Object} option.series[0].leaves - برگها * @vue-data {Object} option.series[0].leaves.label - برچسبهای برگ * @vue-data {String} option.series[0].leaves.label.position - موقعیت برچسب برگ * @vue-data {String} option.series[0].leaves.label.verticalAlign - تراز عمودی برچسب برگ * @vue-data {String} option.series[0].leaves.label.align - تراز برچسب برگ * @vue-data {Object} option.series[0].emphasis - تاکید * @vue-data {String} option.series[0].emphasis.focus - تمرکز تاکید * @vue-data {String} option.series[0].emphasis.blurScope - محدوده مات شدن تاکید * @vue-data {Boolean} option.series[0].expandAndCollapse - وضعیت باز و بسته شدن * @vue-data {Number} option.series[0].animationDuration - مدت زمان انیمیشن * @vue-data {Number} option.series[0].animationDurationUpdate - مدت زمان بهروزرسانی انیمیشن */ export default { props: { dataTreeMap: { default() { return []; }, type: Array, }, }, data() { return { treeCounter: 1, showButtons: false, showDiv: false, buttonAction: "", addText: "", editText: "", httpService: undefined, itemData: {}, tooltipText: "", position: { top: "0px", left: "0px" }, option: { tooltip: { trigger: "item", triggerOn: "mousemove", }, toolbox: { show: true, itemSize: 15, feature: { myTool1: { show: true, title: " نمایش ", icon: '<path d="M19.017 16.243c-1.431-0.009-2.588-1.166-2.597-2.596v-0.001c0.004-0.466 0.133-0.902 0.354-1.276l-0.006 0.012c-0.112-0.009-0.229-0.035-0.349-0.035-2.152 0-3.896 1.744-3.896 3.896s1.744 3.896 3.896 3.896c2.152 0 3.896-1.744 3.896-3.896v0c0-0.12-0.025-0.231-0.035-0.348-0.362 0.215-0.797 0.344-1.261 0.348h-0.001z"></path><path d="M16.42 7.152c-0.067-0.001-0.146-0.002-0.225-0.002-5.746 0-10.661 3.558-12.662 8.591l-0.032 0.092-0.133 0.411 0.133 0.411c2.035 5.125 6.951 8.683 12.697 8.683 0.079 0 0.157-0.001 0.236-0.002h-0.012c0.067 0.001 0.146 0.002 0.225 0.002 5.746 0 10.661-3.558 12.662-8.591l0.032-0.092 0.133-0.411-0.133-0.411c-2.035-5.125-6.951-8.683-12.697-8.683-0.079 0-0.157 0.001-0.236 0.002h0.012zM16.42 22.737c-0.107 0.004-0.234 0.006-0.36 0.006-4.417 0-8.218-2.642-9.907-6.432l-0.027-0.069c1.718-3.858 5.518-6.499 9.935-6.499 0.127 0 0.253 0.002 0.378 0.006l-0.018-0.001c0.107-0.004 0.234-0.006 0.36-0.006 4.417 0 8.218 2.642 9.907 6.432l0.027 0.069c-1.718 3.858-5.518 6.499-9.935 6.499-0.127 0-0.253-0.002-0.378-0.006h0.018z"></path>', onclick: function () { this.myToolClick("نمایش"); }.bind(this), iconStyle: { color: "", }, }, myTool2: { show: true, title: " ویرایش ", icon: '<path d="M9.333 25.856c-0.001 0-0.001 0-0.002 0-0.127 0-0.246-0.031-0.352-0.085l0.004 0.002-0.027 0.019-0.123-0.117c-0.024-0.021-0.046-0.041-0.066-0.064l-0-0-3.249-3.105 2.035-3.256 4.533 4.38-1.007 0.695h12.192c0.423 0 0.765 0.343 0.765 0.765s-0.343 0.765-0.765 0.765v0zM8.332 17.375l11.501-10.416c0.521-0.631 1.304-1.031 2.18-1.031 0.735 0 1.404 0.281 1.906 0.741l-0.002-0.002 1.333 1.5c0.385 0.41 0.621 0.963 0.621 1.571 0 0.714-0.325 1.351-0.836 1.773l-0.004 0.003-10.74 11.569z"></path>', onclick: function () { this.myToolClick("ویرایش"); }.bind(this), iconStyle: { color: "black", }, }, }, }, series: [ { type: "tree", id: 0, name: "tree1", data: [], top: "0%", left: "40%", bottom: "30%", right: "5%", while: "100%", height: "100%", zoom: 1, symbolSize: 10, // roam: true, edgeForkPosition: "30%", edgeShape: "curve", // expandAndCollapse: true, initialTreeDepth: 5, orient: "RL", // symbol: "none", lineStyle: { width: 2, }, label: { color: "#fff", fontFamily: "sahel", fontSize: 15, align: "center", position: "left", verticalAlign: "middle", borderRadius: 3, padding: 6, borderWidth: 2, // shadowColor: "rgba(51, 41, 41, 1)", // shadowBlur: 2.5, // shadowOffsetX: 2, // shadowOffsetY: 2, width: 140, height: 12, backgroundColor: "#ee6666", distance: 80, formatter: function (params) { const maxLength = 15; const text = params.name.length > maxLength ? params.name.substring(0, maxLength) + "..." : params.name; return text; }, }, leaves: { label: { distance: 80, color: "#fff", fontFamily: "sahel", fontSize: 15, align: "center", position: "left", verticalAlign: "middle", borderRadius: 3, padding: 6, borderWidth: 2, // shadowColor: "rgba(51, 41, 41, 1)", // shadowBlur: 2.5, // shadowOffsetX: 2, // shadowOffsetY: 2, width: 140, height: 12, backgroundColor: "#fff", formatter: function (params) { const maxLength = 15; const text = params.name.length > maxLength ? params.name.substring(0, maxLength) + "..." : params.name; return text; }, }, }, lineStyle: { curveness: 0.6, }, emphasis: { focus: "relative", blurScope: "coordinateSystem", }, expandAndCollapse: true, animationDuration: 550, animationDurationUpdate: 750, symbolOffset: [30, 0], }, ], }, }; }, watch: { dataTreeMap: { handler(newData) { this.option.series[0].data = newData; this.treeCounter++; }, deep: true, immediate: true, }, dataForTreeMapGetter(newValue) { this.option.series[0].data = newValue; this.treeCounter++; }, }, beforeMount() { this.httpService = new HttpService(); }, mounted() { this.option.series[0].data = this.dataForTreeMapGetter; this.option.toolbox.feature.myTool1.iconStyle.color = "black"; this.option.toolbox.feature.myTool2.iconStyle.color = "#fff"; }, computed: { ...mapState(useCommonStore, [ // "dataForTreeMapGetter", "researchTermsGetter", ]), }, methods: { // ...mapActions(useResearchStore, ["dataForTreeMapSetter"]), /** *تغیرات مربوط به حالت نمایش و ویرایش در چارت */ myToolClick(item) { if (item == "نمایش") { this.showDiv = false; this.option.toolbox.feature.myTool1.iconStyle.color = "black"; this.option.toolbox.feature.myTool2.iconStyle.color = "#fff"; } else { this.showDiv = true; this.option.toolbox.feature.myTool1.iconStyle.color = "#fff"; this.option.toolbox.feature.myTool2.iconStyle.color = "black"; } }, /** * نشان دادن دکمهها در هنگام وارد شدن ماوس */ onMouseEnter() { this.showButtons = true; }, /** * پنهان کردن دکمهها در هنگام خارج شدن ماوس */ onMouseLeave() { this.showButtons = false; }, /** * وقتی کاربر روی نقاط چارت کلیک میکند، این تابع وظیفه نمایش اطلاعات مربوط به آن نقطه را دارد. * @param {Object} params - اطلاعات مربوط به کلیک شده روی چارت */ handleChartClick(params) { const { offsetX, offsetY } = params.event; // تنظیم موقعیت نمایش اطلاعات this.position = { top: `${offsetY - 150}px`, left: `${offsetX}px` }; // نمایش متن توضیحات this.tooltipText = params.name || "No data"; // ذخیره اطلاعات آیتم کلیک شده this.itemData = params; this.buttonAction = ""; this.addText = ""; this.editText = ""; // نمایش دکمههای مربوط به این آیتم this.showButtons = true; }, /** * وقتی کاربر روی دکمههای اضافه کردن یا حذف کلیک میکند، این تابع عملکرد متناسب با دکمه انجام میدهد. * @param {string} buttonName - نام دکمهای که کلیک شده است ('add' یا 'delete') */ onButtonClick(buttonName) { this.buttonAction = buttonName; // تعیین عملیات اضافه کردن یا حذف بر اساس دکمه انتخاب شده if (buttonName == "add") this.fetchingExistingChildren(); else if (buttonName == "delete") this.removeNode(); // else if (buttonName == "edit") this.editNode(); }, /** * این تابع دکمههای نمایش داده شده بر روی چارت را پنهان میکند. */ closeButtons() { this.showButtons = false; }, /** * این تابع مسئول اضافه کردن آیتمهای جدید به چارت است. * @param {string} item - عنوان آیتم جدید */ addNewChildren(item) { let data = this.itemData.data; // تهیه پارامترهای مورد نیاز برای ارسال درخواست const payload = { projectid: this.researchTermsGetter?.id, parent: "", listtype: 0, title: item, listid: "", id: "", }; // تنظیم مقادیر پارامترها بر اساس شرایط if (data.pid === "Root") { payload.id = undefined; payload.parent = 0; delete payload.listid; } else { payload.listid = data?.id; payload.parent = data?.id; delete payload.id; } // ارسال درخواست به سرور let url = researchApi.subject.add; this.httpService .postRequest(url, payload) .then((res) => { // نمایش پیام موفقیت this.mySwalToast({ title: "موفق", html: " با موفقیت انجام شد ", icon: "success", }); // اضافه کردن آیتمهای جدید به دادههای مربوطه res.data.forEach((element, index) => { var node = { item: element, text: element.title, name: element.title, id: element.id, pid: 0, opened: false, selected: false, disabled: false, loading: false, children: [], }; data.children.push(node); // اضافه کردن آیتم به چارت if (payload.parent === 0) { this.option.series[0].data[0].children.push(node); } else { this.addChildToParent( this.option.series[0].data[0], payload.parent, node ); } this.treeCounter++; }); }) .catch((error) => { // نمایش پیام خطا console.error("Error adding new children:", error); this.mySwalToast({ title: "خطا", html: "مشکلی پیش آمد", icon: "error", }); }) .finally(() => { this.buttonAction = ""; }); }, /** * این تابع مسئول اضافه کردن یک آیتم فرزند به آیتم پدر میباشد. * @param {Object} node - آیتم پدر * @param {string} parentId - شناسه آیتم پدر * @param {Object} newChild - آیتم فرزند جدید * @returns {boolean} - مقدار منطقی True در صورت موفقیت آمیز بودن اضافه کردن، در غیر این صورت False */ addChildToParent(node, parentId, newChild) { if (node.id === parentId) { node.children.push(newChild); return true; } if (node.children) { for (let child of node.children) { if (this.addChildToParent(child, parentId, newChild)) { return true; } } } return false; }, /** * این تابع مسئول دریافت و نمایش آیتمهای موجود زیر یک آیتم پدر است. */ fetchingExistingChildren() { let dataParent = this.itemData.data; if (dataParent.children.length > 1) { return; } try { // دریافت لیست آیتمهای زیر یک آیتم پدر از سرور this.getListTree(dataParent.id).then((list) => { // اضافه کردن هر آیتم به لیست فرزندان آیتم پدر list.forEach((element, index) => { const dataColor = dataParent.label.backgroundColor; var node = { item: element, text: element.title, name: element.title, id: element.id, pid: dataParent.id, opened: false, selected: false, disabled: false, loading: false, children: [], label: { backgroundColor: dataColor, }, itemStyle: { color: dataColor, }, }; dataParent.children.push(node); // اضافه کردن آیتم به چارت if (dataParent.id === 0) { this.option.series[0].data[0].children.push(node); } else { this.addChildToParent( this.option.series[0].data[0], dataParent.id, node ); } this.treeCounter++; }); }); } catch (error) { console.error("Error fetching children nodes:", error); } }, /** * این تابع مسئول حذف یک آیتم از چارت میباشد. */ removeNode() { let removeData = this.itemData.data; if (removeData.pid === "Root") return; this.mySwalConfirm({ title: "هشدار!!!", html: `از حذف <b>${removeData.text}</b> اطمینان دارید؟ `, icon: "warning", }).then((result) => { if (result.isConfirmed) { this.getRemov(removeData); this.closeButtons(); } }); }, /** * این تابع مسئول حذف یک آیتم از چارت میباشد. * @param {Object} nodeToRemove - آیتمی که قرار است حذف شود */ removeNodeFromChart(nodeToRemove) { // حذف آیتم از چارت if (nodeToRemove.pid === 0) { let index = this.option.series[0].data[0].children.findIndex( (child) => child.item.id === nodeToRemove.item.id ); if (index !== -1) { this.option.series[0].data[0].children.splice(index, 1); } } else { let parentData = this.getParentData(nodeToRemove); if (parentData) { let index = parentData.children.findIndex( (child) => child.item.id === nodeToRemove.item.id ); if (index !== -1) { parentData.children.splice(index, 1); } } } this.treeCounter++; // افزایش شمارنده درخت }, /** * این تابع مسئول یافتن و بازگشت آیتم پدر مربوط به یک آیتم است. * @param {Object} node - آیتم فرزند * @returns {Object} - آیتم پدر */ getParentData(node) { let parentData = null; // جستجوی آیتم پدر با استفاده از شناسه آن function findParent(nodeId, data) { for (let i = 0; i < data.length; i++) { if (data[i].id === nodeId) { parentData = data[i]; break; } else if (data[i].children && data[i].children.length) { findParent(nodeId, data[i].children); } } } // فراخوانی تابع جستجوی آیتم پدر findParent(node.pid, this.option.series[0].data[0].children); return parentData; }, /** * این تابع مسئول دریافت لیست آیتمهای زیر یک آیتم پدر از سرور است. * @param {string} parentId - شناسه آیتم پدر * @returns {Promise} - لیست آیتمهای زیر آیتم پدر */ getListTree(parentId = 0) { const payload = { projectid: this.researchTermsGetter?.id, parent: parentId, sortby: "id", offset: 0, limit: 100, listtype: 0, }; let url = researchApi.subject.list; // ارسال درخواست به سرور و بازگشت پاسخ به صورت Promise return this.httpService.formDataRequest(url, payload).then((res) => { return res.data; }); }, /** * این تابع مسئول حذف یک آیتم از چارت میباشد. * @param {Object} item - آیتمی که قرار است حذف شود */ getRemov(item) { const payload = { subjectid: item.id, projectid: this.researchTermsGetter?.id, listid: item.id, }; let url = researchApi.subject.delete; this.httpService.postRequest(url, payload).then((res) => { this.mySwalToast({ title: "موفق", html: "با موفقیت حذف شد", icon: "success", }); // حذف آیتم از چارت this.removeNodeFromChart(item); }); }, /** * این تابع مسئول ویرایش یک آیتم از چارت میباشد. * @param {string} item - عنوان جدید آیتم */ editNode(item) { let data = this.itemData.data; const payload = { subjectid: data.id, projectid: this.researchTermsGetter?.id, title: item, }; let url = researchApi.subject.edit; this.httpService .formDataRequest(url, payload) .then((res) => { this.mySwalToast({ title: "موفق", html: "تغییر نام با موفقیت انجام شد ", icon: "success", }); data.name = item; data.text = item; // پیدا کردن و بهروز رسانی آیتم در چارت const updateNodeText = (node, id, newText) => { if (node.id === id) { node.name = newText; node.text = newText; return true; } if (node.children) { for (let child of node.children) { if (updateNodeText(child, id, newText)) { return true; } } } return false; }; updateNodeText(this.option.series[0].data[0], data.id, item); this.treeCounter++; this.closeButtons(); }) .finally(() => { this.buttonAction = ""; }); }, }, }; </script> <style lang="scss"> .tooltip-button { button { color: #dee2e6 !important; &:hover { color: black !important; } } } .reject-button { color: #dee2e6 !important; &:hover { color: rgb(231, 10, 10) !important; } } .accept-button { color: #dee2e6 !important; &:hover { color: rgb(37, 223, 12) !important; } } </style>