conflict-nuxt-4/app/components/lazy-load/global/MyContent.vue
Baghi330 7892a7cefb 1
2026-02-14 10:41:53 +03:30

875 lines
29 KiB
Vue
Executable File

<template>
<div class="my-content">
<template v-if="localViewMode == 'list'">
<div class="flex flex-wrap -mx-2">
<div
class="w-full px-2 main-content firefox-scrollbar"
:style="{ height: props.mainSchema.height }"
>
<template v-if="props.mainSchema.items.length">
<div
class="mb-3 border-b main-content-item p-3"
v-for="(itemData, i) in props.mainSchema.items"
:key="i"
@click="changeCurrent(itemData)"
@contextmenu.prevent="onRightClick(itemData, $event)"
>
<template v-if="itemData.inner_hits">
<div
class="flex flex-wrap -mx-2"
v-for="(collapseItem, indexCollapse) in props.mainSchema
.schemaItems.collapse_items?.items || []"
:key="indexCollapse"
>
<template v-if="collapseItem.array_key">
<div
class="w-full px-2"
v-for="(subItemData, s) in getArrayData(
itemData,
collapseItem,
)"
:key="s"
>
<lineContent
:lineSchema="collapseItem"
:itemData="subItemData"
:arrayItemData="props.mainSchema.items"
:textSearch="props.mainSchema.textSearch"
@openModalHandler="openModalHandler"
@click-item="onMyContentAction('click-item', $event)"
>
</lineContent>
</div>
</template>
<template v-else>
<lineContent
:lineSchema="collapseItem"
:itemData="itemData"
:arrayItemData="props.mainSchema.items"
:textSearch="props.mainSchema.textSearch"
@openModalHandler="openModalHandler"
@click-item="onMyContentAction('click-item', $event)"
>
</lineContent>
</template>
</div>
</template>
<template
v-else-if="
props.mainSchema.schemaItems &&
props.mainSchema.schemaItems.items
"
>
<div
class="flex flex-wrap -mx-2"
v-for="(lineSchema, index) in props.mainSchema.schemaItems
?.items"
:key="index"
>
<lineContent
:lineSchema="lineSchema"
:itemData="itemData"
:arrayItemData="props.mainSchema.items"
:textSearch="props.mainSchema.textSearch"
@openModalHandler="openModalHandler"
@click-item="onMyContentAction('click-item', $event)"
>
</lineContent>
</div>
</template>
<template v-if="props.mainSchema.isSearchingState">
<div class="flex items-center">
<label
class="whitespace-nowrap ml-1"
style="color: gray; font-family: sahel-semi-bold"
>
پاسخ را همراه امتیاز ذخیره کنید :
</label>
<NuxtRating
class="star-rating"
:read-only="false"
:rating-value="itemData.rate_value ?? 0"
border-color="#db8403"
active-color="#ffa41c"
inactive-color="#fff"
:rating-step="0.5"
:rounded-corners="true"
:border-width="5"
:rating-size="20"
@rating-selected="
saveRating(
$event,
i,
itemData,
props.mainSchema.schemaItems,
)
"
@rating-hovered="(event) => (rating.value = event)"
/>
</div>
</template>
<div
class="search-item__actions"
v-if="props.mainSchema.schemaItems.actions?.length"
>
<span class="tavasi tavasi-more-vert"></span>
<template
v-for="(schema, indexIcon) in props.mainSchema.schemaItems
.actions"
:key="'action' + indexIcon"
>
<div class="search-item__actions flex gap-1 mt-2">
<UButton
v-for="(schema, index) in props.mainSchema.schemaItems
.actions"
:key="'action-' + index"
:title="schema.title"
variant="ghost"
size="xs"
:ui="{ icon: { base: 'w-4 h-4' }, padding: 'p-1.5' }"
@click.stop="handleActionClick(itemData, schema)"
>
<UIcon
v-if="schema.key === 'tbookmark'"
:name="
itemData._source.tbookmark == 1
? schema.toggle_icons?.icon1
: schema.toggle_icons?.icon2
"
class="w-4 h-4 text-dark-primary-800"
/>
<UIcon
v-else-if="schema.icon"
:name="schema.icon"
class="w-4 h-4 text-dark-primary-800"
/>
<span v-else class="text-xs">{{ schema.title }}</span>
</UButton>
</div>
</template>
</div>
</div>
</template>
<template v-else>
<table-no-data></table-no-data>
</template>
</div>
</div>
<div class="">
<myPagination
v-model:paginationInfo="localPagination"
:show-total-records="true"
:show-page-selection="true"
@pageChanged="fetchData"
@limitChanged="fetchData"
/>
</div>
</template>
<!-- viewMode = 'table' -->
<template v-if="localViewMode == 'table'">
<div class="flex flex-wrap -mx-2">
<div class="w-full px-0">
<MyTable
:table-columns="props.mainSchema.tableColumns"
:action-buttons="props.mainSchema.tableActions"
:raw-data="props.mainSchema.items"
:cell-menu-items="props.mainSchema.menuItems"
:show-search="false"
:tableBodyMaxHeight="props.mainSchema.height"
:pagination="localPagination"
@my-table-action="myTableAction"
/>
<myPagination
v-model:paginationInfo="localPagination"
:show-total-records="true"
:show-page-selection="true"
@pageChanged="fetchData"
@limitChanged="fetchData"
/>
</div>
</div>
</template>
<!-- viewMode = 'card' -->
<template v-if="localViewMode == 'card'">
<div class="flex flex-wrap -mx-2">
<div class="w-full px-2 main-content firefox-scrollbar">
<div
class="flex flex-wrap -mx-2"
v-if="props.mainSchema.items.length"
>
<div
v-for="(itemData, i) in props.mainSchema.items"
:key="i"
class="mb-4 w-full sm:w-1/2 md:w-1/3 lg:w-1/4 xl:w-1/5 px-2"
>
<div
class="main-content-item-card p-4 rounded-lg shadow-md border border-gray-200 bg-white flex flex-col h-full"
>
<div v-if="!itemData?._source" class="text-red-500 italic">
داده نامعتبر
</div>
<template v-else>
<div
v-if="props.mainSchema.schemaItems?.items?.length"
class="flex flex-col gap-2"
>
<template
v-for="(lineSchema, index) in props.mainSchema.schemaItems
.items"
:key="index"
>
<lineContent
:lineSchema="lineSchema"
:itemData="itemData"
:arrayItemData="props.mainSchema.items"
:textSearch="props.mainSchema.textSearch"
@openModalHandler="openModalHandler"
@click-item="onMyContentAction('click-item', $event)"
/>
</template>
</div>
<div
v-else-if="itemData._source.title"
class="font-semibold text-gray-800"
>
{{ itemData._source.title }}
</div>
<div v-else class="text-gray-500 text-sm italic">
بدون عنوان
</div>
</template>
<div
v-if="props.mainSchema.schemaItems.actions?.length"
class="mt-auto pt-3 border-t border-gray-100 flex gap-2"
>
<UButton
v-for="(schema, idx) in props.mainSchema.schemaItems
.actions"
:key="'action-' + idx"
:title="schema.title"
variant="ghost"
size="xs"
:ui="{ icon: { base: 'w-4 h-4' }, padding: 'p-1.5' }"
@click.stop="handleActionClick(itemData, schema)"
>
<UIcon
v-if="schema.key === 'tbookmark'"
:name="
itemData._source.tbookmark == 1
? schema.toggle_icons?.icon1
: schema.toggle_icons?.icon2
"
class="w-4 h-4 text-gray-800"
/>
<UIcon
v-else-if="schema.icon"
:name="schema.icon"
class="w-4 h-4 text-gray-800"
/>
<span v-else class="text-xs">{{ schema.title }}</span>
</UButton>
</div>
</div>
</div>
</div>
<template v-else>
<table-no-data />
</template>
</div>
</div>
</template>
<!-- viewMode = 'three-column-card' -->
<template v-if="localViewMode == 'three-column-card'">
<div class="flex flex-wrap -mx-2">
<div class="w-full px-2 main-content firefox-scrollbar">
<div
class="flex flex-wrap -mx-2"
v-if="props.mainSchema.items.length"
>
<div
class="mb-3 main-content-item p-3 w-full sm:w-1/3 px-2"
v-for="(itemData, i) in props.mainSchema.items"
:key="i"
>
<div class="flex justify-center"></div>
<template v-if="itemData.inner_hits">
<div
class="flex flex-wrap -mx-2"
v-for="(collapseItem, indexCollapse) in props.mainSchema
.schemaItems.collapse_items?.items || []"
:key="indexCollapse"
>
<template v-if="collapseItem.array_key">
<div
class="w-1/3 px-2"
v-for="(subItemData, s) in getArrayData(
itemData,
collapseItem,
)"
:key="s"
>
<lineContent
:lineSchema="collapseItem"
:itemData="subItemData"
:arrayItemData="props.mainSchema.items"
:textSearch="props.mainSchema.textSearch"
>
</lineContent>
</div>
</template>
<template v-else>
<lineContent
:lineSchema="collapseItem"
:itemData="itemData"
:arrayItemData="props.mainSchema.items"
:textSearch="props.mainSchema.textSearch"
>
</lineContent>
</template>
</div>
</template>
<template v-else>
<div
v-if="
props.mainSchema.schemaItems &&
props.mainSchema.schemaItems.items
"
class="flex flex-wrap -mx-2"
v-for="(lineSchema, index) in props.mainSchema.schemaItems
?.items || []"
:key="index"
>
<lineContent
:lineSchema="lineSchema"
:itemData="itemData"
:arrayItemData="props.mainSchema.items"
:textSearch="props.mainSchema.textSearch"
@openModalHandler="openModalHandler"
@click-item="onMyContentAction('click-item', $event)"
>
</lineContent>
</div>
</template>
<div
class="search-item__actions"
v-if="props.mainSchema.schemaItems.actions"
>
<span class="tavasi tavasi-more-vert"></span>
</div>
</div>
</div>
<template v-else>
<table-no-data></table-no-data>
</template>
</div>
</div>
</template>
<!-- viewMode = 'two-column-card' -->
<template v-if="localViewMode == 'two-column-card'">
<div class="flex flex-wrap -mx-2">
<div class="w-full sm:w-1/2 px-2 main-content firefox-scrollbar">
<template v-if="props.mainSchema.items.length">
<div
class="mb-3 border-b main-content-item p-3"
v-for="(itemData, i) in props.mainSchema.items"
:key="i"
>
<template v-if="itemData.inner_hits">
<div
class="flex flex-wrap -mx-2"
v-for="(collapseItem, indexCollapse) in props.mainSchema
.schemaItems.collapse_items?.items || []"
:key="indexCollapse"
>
<template v-if="collapseItem.array_key">
<div
class="w-full px-2"
v-for="(subItemData, s) in getArrayData(
itemData,
collapseItem,
)"
:key="s"
>
<lineContent
:lineSchema="collapseItem"
:itemData="subItemData"
:arrayItemData="props.mainSchema.items"
:textSearch="props.mainSchema.textSearch"
>
</lineContent>
</div>
</template>
<template v-else>
<lineContent
:lineSchema="collapseItem"
:itemData="itemData"
:arrayItemData="props.mainSchema.items"
:textSearch="props.mainSchema.textSearch"
>
</lineContent>
</template>
</div>
</template>
<template v-else>
<div
v-if="
props.mainSchema.schemaItems &&
props.mainSchema.schemaItems.items
"
class="flex flex-wrap -mx-2"
v-for="(lineSchema, index) in props.mainSchema.schemaItems
?.items || []"
:key="index"
>
<lineContent
:lineSchema="lineSchema"
:itemData="itemData"
:arrayItemData="props.mainSchema.items"
:textSearch="props.mainSchema.textSearch"
@openModalHandler="openModalHandler"
@click-item="onMyContentAction('click-item', $event)"
>
</lineContent>
</div>
</template>
<div
class="search-item__actions"
v-if="props.mainSchema.schemaItems.actions"
>
<span class="tavasi tavasi-more-vert"></span>
</div>
</div>
</template>
<template v-else>
<table-no-data></table-no-data>
</template>
</div>
</div>
</template>
</div>
</template>
<script setup>
import { ref, computed, onBeforeMount, onMounted, onUnmounted } from "vue";
import { useNuxtApp, useRoute, useRouter } from "#app";
import lineContent from "@/components/lazy-load/global/lineContent.vue";
import MyTable from "@/components/lazy-load/global/MyTable.vue";
import { watch } from "vue";
// --- متغیرهای نمونه (برای تست) ---
const columns = []; // حذف شد چون استفاده نمی‌شود
const buttons = []; // حذف شد
const data = []; // حذف شد
const menuItems = []; // حذف شد
const props = defineProps({
mainSchema: {
type: Object,
required: true,
default: () => ({}),
},
pagination: {
// ✅ اضافه شد
type: Object,
required: true,
},
});
const localPagination = ref({
// pages: 1000,
total: 0,
page: 1,
// offset: 0,
limit: 10,
});
watch(
() => props.mainSchema.pagination,
(newPagination) => {
if (newPagination) {
localPagination.value.total = newPagination.total ?? 0;
// page و limit معمولاً توسط fetchData ست شدن، ولی اگر Parent override کنه:
if (newPagination.page) localPagination.value.page = newPagination.page;
if (newPagination.limit)
localPagination.value.limit = newPagination.limit;
}
},
{ immediate: true, deep: true },
);
const fetchData = (newPagination) => {
// newPagination می‌تونه از myPagination بیاد
// مثلاً: { pageNumber: 3, limit: 20 }
const page = newPagination.pageNumber ?? localPagination.value.page;
const limit = newPagination.limit ?? localPagination.value.limit;
// state داخلی رو به‌روز کن (صفحه و limit)
localPagination.value.page = page;
localPagination.value.limit = limit;
// ✅ فقط این دو مورد رو بفرست به Parent
onMyContentAction("pagination", { page, limit });
};
const myTableAction = ({ action, payload }) => {
// console.log("myTableAction :", action, payload);
onMyContentAction(action, payload);
};
const onMyContentAction = (action, payload) => {
emit("my-content-action", {
action: action,
payload: payload,
});
};
// --- Props و Emits ---
const localViewMode = ref(props.mainSchema.viewMode);
// ✅ فقط یک ایمیت واحد
const emit = defineEmits(["my-content-action"]);
// --- Composables ---
const route = useRoute();
const router = useRouter();
// --- Refs ---
const contextMenu = ref({ visible: false, x: 0, y: 0, node: null });
const rating = ref("");
const page = ref({ pages: 0, total: 0, page: 1, offset: 0, limit: 10 });
const reRender = ref(1);
const sorting = ref({ sortby: "created", sortorder: undefined });
// --- Methods ---
const contextMenuAction = (payload) =>
onMyContentAction("context-menu-action", payload);
const onRightClick = (node, event) => {
if (props.mainSchema.contextMenuItems?.length) {
contextMenu.value = {
x: event.clientX,
y: event.clientY,
visible: true,
node: node,
};
document.addEventListener("click", hideContextMenu);
}
};
const hideContextMenu = () => {
contextMenu.value.visible = false;
document.removeEventListener("click", hideContextMenu);
};
const saveRating = (rate_value, index, itemData, schemaItems) => {
let schema = undefined;
if (itemData?.inner_hits && schemaItems?.collapse_items?.items?.length)
schema = schemaItems.collapse_items.items[0];
else if (schemaItems?.items?.length) schema = schemaItems.items[0];
if (!schema) return;
const key_id = schema.link_id ?? "_id";
itemData.rate_value = rate_value;
props.mainSchema.items[index] = itemData;
const id_route = getSourceData(itemData, key_id);
onMyContentAction("rate-item", { id: id_route, rate: rate_value });
};
const openModalHandler = (item, isReadonly = true) =>
onMyContentAction("ModalHandler", { item, isReadonly });
const handlerActions = (event) => {
const { key } = event.rowAction;
if (key === "summary") {
onMyContentAction("show-summary", event.item);
} else if (key === "copy") {
copyToClipboard(
"",
urlResolver(event.item._id, event.rowAction.link_route, event.item),
);
} else if (key === "tbookmark") {
AddToFavorites(event.item, event.rowAction, event.index);
} else {
onMyContentAction("actionsHandler", { key, event });
}
};
const AddToFavorites = (item, icon, index) => {
onMyContentAction("myContent_addToFavorites", { item, icon, index });
};
const handlerActionsList = (item, key, icon) => {
if (key === "summary") {
onMyContentAction("show-summary", item);
} else if (key === "copy") {
copyToClipboard(
"",
urlResolver(
findvalueForKey(item, icon.link_route.id),
icon.link_route,
item,
),
);
} else if (key === "SubjectForm") {
onMyContentAction("SubjectForm", item);
} else if (key === "edit") {
onMyContentAction(icon.emit ? "edit" : "ModalHandler", {
item,
key,
icon,
isReadonly: !icon.emit,
});
} else if (key === "delete") {
onMyContentAction(icon.emit ? "delete" : "deleteResearch", item);
}
};
const urlResolver = (_id, route, item) => {
let query = {};
if (buildName?.() !== "majles")
query.searchtext = props.mainSchema.textSearch ?? undefined;
if (route?.query) {
if (typeof route.query === "string") {
const [query_key, query_value] = route.query.split("=");
if (query_key && query_value) query[query_key] = query_value;
} else {
for (const [key, value] of Object.entries(route.query)) {
query[key] = item[value] ?? item?._source[value];
}
}
}
const id_route = getSourceData(item, route?.id);
const id_route2 = route?.id2 ? getSourceData(item, route.id2) : undefined;
const key = getSourceData(item, route?.key_item) ?? route.key;
if (route.name === "navigation" || route.name === "navigationView")
query.ls = "list";
return router.resolve({
name: "navigationView",
params: { id: id_route, id2: id_route2, key },
query,
}).href;
};
const getSourceData = (itemData, key) => {
let sourceData = itemData;
key.split(".").forEach((k) => {
sourceData = sourceData?.[k] ?? sourceData?._source?.[k];
});
return sourceData;
};
const getArrayData = (itemData, collapseItem) => {
if (!collapseItem.array_key) return [itemData];
const sourceData = getSourceData(itemData, collapseItem.array_key);
return Array.isArray(sourceData) ? sourceData : [itemData];
};
const findvalueForKey = (item, keyName) =>
item[keyName] ?? item?._source[keyName];
const onLinkedTitleClick = ({ rowItem, tableColumn }) => {
let key = props.mainSchema.mainSchemaKey;
if (key === "qsection") key = "qasection";
else if (key === "rsection") key = "rgsection";
const title_value =
key === "qasection" || key === "rgsection"
? rowItem._source.qanon_title
: rowItem._source.title;
const valueId = findvalueForKey(rowItem, tableColumn.link_route?.id);
if (valueId) {
onMyContentAction("click-item", { id: valueId, title: title_value });
}
const href = urlResolver(valueId, tableColumn.link_route, rowItem);
const cloneList = props.mainSchema.items.map((item) => ({
_id: item._id,
_source: {
id: item._id,
title: item?._source?.title,
qanon_title: item?._source?.qanon_title,
qanon_id: item?._source?.qanon_id,
ref_key: key,
},
}));
localStorage.setItem("myList", JSON.stringify(cloneList));
localStorage.setItem("myItem", JSON.stringify(rowItem));
window.open(href, "_blank");
};
const pageLimitChanged = (paging) => {
resetPagination();
page.value.limit = paging.limit;
onMyContentAction("changePage", { ...page.value });
};
const pageChanged = (paging) => {
const p = paging.pageNumber - 1;
page.value.offset = p * paging.limit;
page.value.limit = paging.limit;
page.value.page = paging.pageNumber;
onMyContentAction("changePage", { ...page.value });
};
const sortChanged = (sortingInfo) => {
page.value.page = 0;
page.value.offset = 0;
sorting.value = sortingInfo;
onMyContentAction("changePage", { ...sorting.value });
};
const resetPagination = () => {
page.value = { pages: 0, total: 0, page: 1, offset: 0, limit: 10 };
};
const changeCurrent = (item) => {
onMyContentAction("changeCurrent", item);
};
const handleActionClick = (itemData, schema) => {
handlerActionsList(itemData, schema.key, schema);
};
// --- Lifecycle ---
onBeforeMount(() => {
if (props.mainSchema.items && route.params?.key === "qasection") {
props.mainSchema.items.forEach((item) => {
if (!item._source.qanon_etebar?.trim())
item._source.qanon_etebar = "معتبر";
});
}
});
onMounted(() => {
page.value = props.mainSchema.pagination || {
pages: 0,
total: 0,
page: 1,
offset: 0,
limit: 10,
};
fetchData({ pageNumber: 1, limit: 10 });
});
onUnmounted(() => {
document.removeEventListener("click", hideContextMenu);
});
</script>
<style lang="scss" scoped>
/* سبک‌ها بدون تغییر */
.main-content-item {
position: relative;
overflow: hidden;
&:hover,
&.active {
background-color: #e8fcff;
.search-item__actions {
width: auto;
transition: width 0.5s;
background: #fff;
border-radius: 0 0.5em 0.5em 0;
.tavasi-more-vert {
transition: all 0.2s;
display: none;
}
}
}
}
.main-content-item-card {
box-shadow: rgba(191, 191, 191, 0.24) 0px 0px 3px 1px;
border-radius: 10px;
position: relative;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
&:hover,
&.active {
background-color: var(--list-background-color);
.search-item__actions {
width: auto;
transition: width 0.5s;
background: #fff;
border-radius: 0 0.5em 0.5em 0;
.tavasi-more-vert {
transition: all 0.2s;
display: none;
}
}
}
}
.search-item__actions {
position: absolute;
left: 0;
width: 1.6em;
top: 1em;
transition: all 0.5s;
display: flex;
align-items: center;
.btn {
display: flex;
align-items: center;
justify-content: center;
padding: 0.175rem 0.35rem;
&:hover {
filter: brightness(0.7);
}
.icon-copy2 {
font-size: 0.8rem;
}
&.favorites {
color: var(--color-primary-color);
.icon-bookmark-1,
.icon-bookmark-2 {
height: 1.3em;
}
}
}
}
.main-content {
height: calc(100dvh - 15em);
overflow: auto;
}
@media (max-width: 575.98px) {
.main-content {
height: calc(100dvh - 17em);
}
}
.star-rating-text {
float: left;
margin-left: 10px;
}
</style>
<style lang="scss">
.my-content {
&.refine-main-filter-enabled {
.my-content-table {
.table-responsive {
height: calc(-22.5em + 100vh) !important;
}
}
}
}
</style>