1
This commit is contained in:
parent
cc647ffaba
commit
7892a7cefb
2
.env
2
.env
|
|
@ -3,5 +3,5 @@ NUXT_PUBLIC_API_NAME=api/
|
||||||
IS_DEVLOP_MODE=1
|
IS_DEVLOP_MODE=1
|
||||||
|
|
||||||
NUXT_PUBLIC_SYSTEM=monir
|
NUXT_PUBLIC_SYSTEM=monir
|
||||||
NUXT_PUBLIC_BASE_URL=http://192.168.23.60/
|
NUXT_PUBLIC_BASE_URL=http://192.168.23.160/
|
||||||
NUXT_PUBLIC_BASE_URL2=https://hamfahmi.ir/
|
NUXT_PUBLIC_BASE_URL2=https://hamfahmi.ir/
|
||||||
|
|
@ -3,3 +3,4 @@ NUXT_PUBLIC_SYSTEM=majles
|
||||||
|
|
||||||
# (اختیاری)
|
# (اختیاری)
|
||||||
NUXT_PUBLIC_APP_NAME=Majles System
|
NUXT_PUBLIC_APP_NAME=Majles System
|
||||||
|
NUXT_PUBLIC_BASE_URL=http://192.168.23.160/
|
||||||
27
app/apis/elpApi.js
Executable file
27
app/apis/elpApi.js
Executable file
|
|
@ -0,0 +1,27 @@
|
||||||
|
export default {
|
||||||
|
base: {
|
||||||
|
update_field: "elp/v1/indices/{{type_name}}/update/field/{{doc_id}}",
|
||||||
|
compute_field: "elp/v1/indices/{{type_name}}/compute/field",
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
elp_base_search: "elp/v1/indices/{{index_key}}/search",
|
||||||
|
elp_voice_search: "elp/voice/{{index_key}}/search",
|
||||||
|
elp_db_search: "elp/db/indices/{{index_key}}/search",
|
||||||
|
},
|
||||||
|
Info: {
|
||||||
|
elp_completion: "elp/v1/indices/{{type_name}}/completion/{{property_key}}",
|
||||||
|
elp_main_label: "elp/v1/base/option/get/{{main_label}}",
|
||||||
|
elp_update_field: "elp/v1/indices/{{type_name}}/update/field/{{doc_id}}",
|
||||||
|
elp_list_Tree: "/elp/tbase/{{type_name}}/list/tree",
|
||||||
|
elp_get_Tree: "/elp/tbase/{{type_name}}/get/item/all",
|
||||||
|
elp_update_Tree: "/elp/tbase/{{type_name}}/update/{{id}}",
|
||||||
|
elp_delete_Tree: "/elp/tbase/{{type_name}}/delete/{{id}}",
|
||||||
|
elp_move_in_Tree:
|
||||||
|
"/elp/tbase/{{type_name}}/move_in/{{drop_id}}/{{drag_id}}",
|
||||||
|
elp_insert_Tree: "/elp/tbase/{{type_name}}/insert",
|
||||||
|
elp_relation_Tree: "/elp/tbase/{{type_name}}/relation/add",
|
||||||
|
},
|
||||||
|
data_entry:{
|
||||||
|
getSchema:"elp/schema/get"
|
||||||
|
}
|
||||||
|
};
|
||||||
20
app/apis/permitApi.js
Executable file
20
app/apis/permitApi.js
Executable file
|
|
@ -0,0 +1,20 @@
|
||||||
|
export default {
|
||||||
|
roles: {
|
||||||
|
list: "role/list",
|
||||||
|
add: "role/add",
|
||||||
|
edit: "role/edit",
|
||||||
|
delete: "role/del",
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
addOrEditUserPermission: "uperm/add",
|
||||||
|
addOrEditRolePermission: "rperm/add",
|
||||||
|
addOrEditLoginRolePermission: "rperm/login/add",
|
||||||
|
deleteUserPermission: "uperm/del",
|
||||||
|
deleteRolePermission: "rperm/del",
|
||||||
|
listUserPermission: "uperm/list",
|
||||||
|
listRolePermission: "rperm/list",
|
||||||
|
listUserRole: "rperm/listuser",
|
||||||
|
deleteUserRole: "urole/del",
|
||||||
|
userPermissionTags: "permit/uperm/list/tags",
|
||||||
|
},
|
||||||
|
};
|
||||||
31
app/apis/repoApi.js
Executable file
31
app/apis/repoApi.js
Executable file
|
|
@ -0,0 +1,31 @@
|
||||||
|
export default {
|
||||||
|
search: {
|
||||||
|
mirror_search:
|
||||||
|
"repo/monir/mirror/{{mirror_type}}/{{index_key}}/{{search_type}}/{{sortKey}}/{{field_collapse}}/{{offset}}/{{limit}}/{{filter}}",
|
||||||
|
autoComplate: "monir/complation/{{index_key}}/{{filter}}",
|
||||||
|
queryNormal:
|
||||||
|
"repo/monir/search/{{index_key}}/{{search_type}}/{{sortKey}}/{{field_collapse}}/{{offset}}/{{limit}}/{{filter}}",
|
||||||
|
textSearch:
|
||||||
|
"repo/monir/search/text/{{index_key}}/{{field}}/{{offset}}/{{limit}}/{{filter}}",
|
||||||
|
// queryMirror:
|
||||||
|
// "monir/mirror/{{mirror_type}}/{{index_key}}/{{search_type}}/{{sortKey}}/{{field_collapse}}/{{offset}}/{{limit}}/{{filter}}",
|
||||||
|
},
|
||||||
|
|
||||||
|
public: {
|
||||||
|
updateProperty_full:
|
||||||
|
"public/{{index_key}}/edit/{{entity_id}}/{{property}}/index/full",
|
||||||
|
|
||||||
|
updateEntity: "repo/public/{{index_key}}/update/{{id}}",
|
||||||
|
get_doc_byid: "repo/public/get/byid/{{index_key}}/{{entity_id}}",
|
||||||
|
},
|
||||||
|
calendar: {
|
||||||
|
schema: {
|
||||||
|
list: "repo/schema",
|
||||||
|
},
|
||||||
|
list: "repo/calendar/@offset/@limit/@filter",
|
||||||
|
list_collpase: "repo/calendar/@field_collapsed/@offset/@limit/@filter",
|
||||||
|
addTask: "repo/calendar/add",
|
||||||
|
update: "repo/calendar/edit/@id",
|
||||||
|
delete: "repo/calendar/delete/@id",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useConfirmState, useConfirmActions } from "~/composables/useConfirm";
|
import { useConfirmState, useConfirmActions } from "@/composables/useConfirm";
|
||||||
|
|
||||||
const state = useConfirmState();
|
const state = useConfirmState();
|
||||||
const actions = useConfirmActions();
|
const actions = useConfirmActions();
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
:items="normalizedItems"
|
:items="normalizedItems"
|
||||||
:multiple="selectSchema.multiple"
|
:multiple="selectSchema.multiple"
|
||||||
:searchable="selectSchema.searchable"
|
:searchable="selectSchema.searchable"
|
||||||
|
:search-input="selectSchema.searchInput"
|
||||||
:creatable="selectSchema.creatable"
|
:creatable="selectSchema.creatable"
|
||||||
:placeholder="selectSchema.placeholder"
|
:placeholder="selectSchema.placeholder"
|
||||||
:option-attribute="selectSchema.optionAttribute"
|
:option-attribute="selectSchema.optionAttribute"
|
||||||
|
|
@ -49,6 +50,7 @@
|
||||||
:items="normalizedItems"
|
:items="normalizedItems"
|
||||||
:multiple="selectSchema.multiple"
|
:multiple="selectSchema.multiple"
|
||||||
:searchable="selectSchema.searchable"
|
:searchable="selectSchema.searchable"
|
||||||
|
:search-input="selectSchema.searchInput"
|
||||||
:creatable="selectSchema.creatable"
|
:creatable="selectSchema.creatable"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
:placeholder="selectSchema.placeholder"
|
:placeholder="selectSchema.placeholder"
|
||||||
|
|
@ -119,6 +121,7 @@ const selectSchema = computed(() => {
|
||||||
multiple:
|
multiple:
|
||||||
i.multiple === true || i.multi_select === 1 || i.multi_select === true,
|
i.multiple === true || i.multi_select === 1 || i.multi_select === true,
|
||||||
searchable: i.searchable ?? true,
|
searchable: i.searchable ?? true,
|
||||||
|
searchInput: i.searchInput ?? true,
|
||||||
creatable: i.creatable ?? i.allowCreate ?? false,
|
creatable: i.creatable ?? i.allowCreate ?? false,
|
||||||
loading: i.loading ?? false,
|
loading: i.loading ?? false,
|
||||||
placeholder: i.placeholder || "لطفاً انتخاب کنید...",
|
placeholder: i.placeholder || "لطفاً انتخاب کنید...",
|
||||||
|
|
|
||||||
501
app/components/auto-import/HeaderTools.vue
Executable file
501
app/components/auto-import/HeaderTools.vue
Executable file
|
|
@ -0,0 +1,501 @@
|
||||||
|
<template>
|
||||||
|
<div :ref="selectedTabDetails" class="w-full">
|
||||||
|
<div class="container-fluid max-w-full px-3">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 px-3">
|
||||||
|
<div
|
||||||
|
class="mt-3 mb-1 flex h-auto items-center justify-between bg-gray-100 dark:bg-dark-primary-800 rounded-md p-1"
|
||||||
|
>
|
||||||
|
<div class="w-full">
|
||||||
|
<div
|
||||||
|
v-for="(headerItems, index) in headerTools"
|
||||||
|
:key="index"
|
||||||
|
class="flex justify-between items-center my-2"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between w-full flex-wrap px-2">
|
||||||
|
<div
|
||||||
|
v-for="(my_item, idx) in getArrayItems(headerItems)"
|
||||||
|
:key="idx"
|
||||||
|
class="flex items-center flex-wrap"
|
||||||
|
>
|
||||||
|
<template v-if="isArrayItems(my_item)">
|
||||||
|
<div
|
||||||
|
v-for="(headItem, myItemIndex) in getArrayItems(
|
||||||
|
my_item,
|
||||||
|
)"
|
||||||
|
:key="myItemIndex"
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
<DropdownSetting
|
||||||
|
v-if="headItem.type === 'dropdownSetting'"
|
||||||
|
:schema="headItem"
|
||||||
|
@dropdown-setting-btn-clicked="
|
||||||
|
(val) =>
|
||||||
|
emitHandler('dropdown-setting', { data: val })
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else-if="headItem.key === 'label'"
|
||||||
|
class="mr-3"
|
||||||
|
v-tooltip="headItem.tooltip || ''"
|
||||||
|
>
|
||||||
|
<span class="text-primary font-medium px-3">{{
|
||||||
|
headItem.label
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="headItem.key === 'text'"
|
||||||
|
class="mr-3"
|
||||||
|
v-tooltip="headItem.tooltip || ''"
|
||||||
|
>
|
||||||
|
<span class="text-primary font-medium px-3"
|
||||||
|
>{{ headItem.label }}:</span
|
||||||
|
>
|
||||||
|
<span>{{ getDataValue(headItem.source_key) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="headItem.key === 'switch'" class="mr-2">
|
||||||
|
<SwitchButtons
|
||||||
|
:switchSchema="headItem.switchSchema"
|
||||||
|
@update:model-value="
|
||||||
|
(val) =>
|
||||||
|
emitHandler('switch-component-change', {
|
||||||
|
data: {
|
||||||
|
name: headItem.name,
|
||||||
|
value: val,
|
||||||
|
item: headItem,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="headItem.key === 'range'"
|
||||||
|
class="flex items-center mx-1"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="headItem.label"
|
||||||
|
class="text-sm text-gray-600 me-2"
|
||||||
|
>
|
||||||
|
{{ headItem.label }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
class="form-range appearance-none h-1.5 w-24 bg-gray-200 rounded-lg outline-none"
|
||||||
|
:min="headItem.min || 0"
|
||||||
|
:max="headItem.max || 5"
|
||||||
|
:step="headItem.step || 1"
|
||||||
|
:value="headItem.value"
|
||||||
|
@input="(e) => handleRange(e, headItem)"
|
||||||
|
/>
|
||||||
|
<span class="ms-2 text-sm min-w-[18px] text-center">{{
|
||||||
|
headItem.value
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-else-if="headItem.key === 'icon'"
|
||||||
|
class="btn p-2 rounded hover:text-primary transition-colors"
|
||||||
|
:title="headItem.label"
|
||||||
|
v-tooltip="headItem.tooltip || ''"
|
||||||
|
@click="emitHandler('icon-click', { data: headItem })"
|
||||||
|
>
|
||||||
|
<!-- <svg :class="'icon icon-' + headItem.icon">
|
||||||
|
<use :xlink:href="'#icon-' + headItem.icon" />
|
||||||
|
</svg> -->
|
||||||
|
<UIcon
|
||||||
|
:name="headItem.icon"
|
||||||
|
:class="headItem.size"
|
||||||
|
class="cursor-pointer"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-else-if="
|
||||||
|
headItem.key === 'multiSelect' ||
|
||||||
|
headItem.key === 'dropdown'
|
||||||
|
"
|
||||||
|
class="mx-1"
|
||||||
|
>
|
||||||
|
<MySelect
|
||||||
|
v-model:dropdownSchema="headItem.dropdownSchema"
|
||||||
|
:selectSchema="headItem.dropdownSchema"
|
||||||
|
@dropdownSelectEvents="
|
||||||
|
(event) => {
|
||||||
|
if (!event) return;
|
||||||
|
if (event.action !== 'change') return;
|
||||||
|
|
||||||
|
emitHandler('multiselect-click', {
|
||||||
|
data: {
|
||||||
|
name: headItem.name,
|
||||||
|
value: event.payload,
|
||||||
|
item: headItem,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="headItem.key === 'button'"
|
||||||
|
v-tooltip="headItem.tooltip || ''"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn bg-primary text-white px-3 py-1 rounded hover:bg-primary-dark"
|
||||||
|
@click="
|
||||||
|
emitHandler('button-click', { data: headItem })
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ headItem.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="headItem.key === 'iconButton'"
|
||||||
|
v-tooltip="headItem.tooltip || ''"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn flex items-center gap-1 px-2 py-1 rounded hover:text-primary"
|
||||||
|
@click="
|
||||||
|
emitHandler('iconButton-click', {
|
||||||
|
data: headItem,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
:style="{ color: headItem.color }"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
v-if="headItem.side === 'right'"
|
||||||
|
:class="'icon icon-' + headItem.icon"
|
||||||
|
>
|
||||||
|
<use :xlink:href="'#icon-' + headItem.icon" />
|
||||||
|
</svg>
|
||||||
|
{{ headItem.label }}
|
||||||
|
<svg
|
||||||
|
v-if="headItem.side === 'left'"
|
||||||
|
:class="'icon icon-' + headItem.icon"
|
||||||
|
>
|
||||||
|
<use :xlink:href="'#icon-' + headItem.icon" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="
|
||||||
|
headItem.key === 'rangeDate' && !headItem.isShow
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<RangeDateToolsHeader
|
||||||
|
@date-picker-handler="
|
||||||
|
(val) => emitHandler('date-picker', { data: val })
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="headItem.key === 'autoComplation'">
|
||||||
|
<AutoComplation
|
||||||
|
@auto-complation-handler="
|
||||||
|
(val) =>
|
||||||
|
emitHandler('auto-complation', { data: val })
|
||||||
|
"
|
||||||
|
:autoComplationSchema="{
|
||||||
|
placeholder: headItem.placeholder,
|
||||||
|
autocompleteUrl: headItem.autocompleteUrl,
|
||||||
|
debounceTime: headItem.debounceTime,
|
||||||
|
minCharsForAutocomplete:
|
||||||
|
headItem.minCharsForAutocomplete,
|
||||||
|
maxHistoryItems: headItem.maxHistoryItems,
|
||||||
|
showSearchButton: headItem.showSearchButton,
|
||||||
|
filters: headItem.filters,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="headItem.key === 'prevNext'"
|
||||||
|
class="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="flex items-center btn bg-primary text-white pl-2 py-1 rounded hover:bg-primary-700 transition-colors duration-300 ease-in-out disabled:cursor-not-allowed"
|
||||||
|
:disabled="headItem.prevDisabled"
|
||||||
|
@click="
|
||||||
|
emitHandler('prev-click', { data: headItem })
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<UIcon
|
||||||
|
name="i-lucide-chevron-right"
|
||||||
|
class="w-6 h-6"
|
||||||
|
/>
|
||||||
|
<span>قبلی</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="flex items-center btn bg-primary text-white pr-2 py-1 rounded hover:bg-primary-700 transition-colors duration-300 ease-in-out disabled:cursor-not-allowed"
|
||||||
|
:disabled="headItem.nextDisabled"
|
||||||
|
@click="
|
||||||
|
emitHandler('next-click', { data: headItem })
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span>بعدی</span>
|
||||||
|
<UIcon
|
||||||
|
name="i-lucide-chevron-left"
|
||||||
|
class="w-6 h-6"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Non-array items -->
|
||||||
|
<template v-else>
|
||||||
|
<DropdownSetting
|
||||||
|
v-if="my_item.type === 'dropdownSetting'"
|
||||||
|
:schema="my_item"
|
||||||
|
@dropdown-setting-btn-clicked="
|
||||||
|
(val) =>
|
||||||
|
emitHandler('dropdown-setting', { data: val })
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else-if="my_item.key === 'label'"
|
||||||
|
class="mr-3"
|
||||||
|
v-tooltip="my_item.tooltip || ''"
|
||||||
|
>
|
||||||
|
<span class="text-primary font-medium px-3">{{
|
||||||
|
my_item.label
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="my_item.key === 'text'"
|
||||||
|
class="mr-3"
|
||||||
|
v-tooltip="my_item.tooltip || ''"
|
||||||
|
>
|
||||||
|
<span class="text-primary font-medium px-3"
|
||||||
|
>{{ my_item.label }}:</span
|
||||||
|
>
|
||||||
|
<span>{{ getDataValue(my_item.source_key) }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- جایگزینی سوئیچ قدیمی -->
|
||||||
|
<div v-else-if="my_item.key === 'switch'" class="mr-2">
|
||||||
|
<SwitchButtons
|
||||||
|
:switchSchema="my_item.switchSchema"
|
||||||
|
@update:model-value="
|
||||||
|
(val) =>
|
||||||
|
emitHandler('switch-component-change', {
|
||||||
|
data: {
|
||||||
|
name: my_item.name,
|
||||||
|
value: val,
|
||||||
|
item: my_item,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="my_item.key === 'range'"
|
||||||
|
class="flex items-center mx-1"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="my_item.label"
|
||||||
|
class="text-sm text-gray-600 me-2"
|
||||||
|
>
|
||||||
|
{{ my_item.label }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
class="form-range appearance-none h-1.5 w-24 bg-gray-200 rounded-lg outline-none"
|
||||||
|
:min="my_item.min || 0"
|
||||||
|
:max="my_item.max || 5"
|
||||||
|
:step="my_item.step || 1"
|
||||||
|
:value="my_item.value"
|
||||||
|
@input="(e) => handleRange(e, my_item)"
|
||||||
|
/>
|
||||||
|
<span class="ms-2 text-sm min-w-[18px] text-center">{{
|
||||||
|
my_item.value
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-else-if="my_item.key === 'icon'"
|
||||||
|
class="btn p-2 rounded hover:text-primary transition-colors"
|
||||||
|
:title="my_item.label"
|
||||||
|
v-tooltip="my_item.tooltip || ''"
|
||||||
|
@click="emitHandler('icon-click', { data: my_item })"
|
||||||
|
>
|
||||||
|
<!-- <svg :class="'icon icon-' + my_item.icon">
|
||||||
|
<use :xlink:href="'#icon-' + my_item.icon" />
|
||||||
|
</svg> -->
|
||||||
|
<UIcon :name="my_item.icon" :class="my_item.size" />
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-else-if="
|
||||||
|
my_item.key === 'multiSelect' ||
|
||||||
|
my_item.key === 'dropdown'
|
||||||
|
"
|
||||||
|
class="mx-1"
|
||||||
|
>
|
||||||
|
<MySelect
|
||||||
|
v-model:dropdownSelectConfig="my_item.dropdownSchema"
|
||||||
|
:selectSchema="my_item.dropdownSchema"
|
||||||
|
@dropdownSelectEvents="
|
||||||
|
(event) => {
|
||||||
|
if (event.action !== 'change') return;
|
||||||
|
|
||||||
|
emitHandler('multiselect-click', {
|
||||||
|
data: {
|
||||||
|
name: my_item.name,
|
||||||
|
value: event.payload,
|
||||||
|
item: my_item,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="my_item.key === 'button'"
|
||||||
|
v-tooltip="my_item.tooltip || ''"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn bg-primary text-white px-3 py-1 rounded hover:bg-primary-dark"
|
||||||
|
@click="
|
||||||
|
emitHandler('button-click', { data: my_item })
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ my_item.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="my_item.key === 'iconButton'"
|
||||||
|
v-tooltip="my_item.tooltip || ''"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn flex items-center gap-1 px-2 py-1 rounded hover:text-primary"
|
||||||
|
@click="
|
||||||
|
emitHandler('iconButton-click', { data: my_item })
|
||||||
|
"
|
||||||
|
:style="{ color: my_item.color }"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
v-if="my_item.side === 'right'"
|
||||||
|
:class="'icon icon-' + my_item.icon"
|
||||||
|
>
|
||||||
|
<use :xlink:href="'#icon-' + my_item.icon" />
|
||||||
|
</svg>
|
||||||
|
{{ my_item.label }}
|
||||||
|
<svg
|
||||||
|
v-if="my_item.side === 'left'"
|
||||||
|
:class="'icon icon-' + my_item.icon"
|
||||||
|
>
|
||||||
|
<use :xlink:href="'#icon-' + my_item.icon" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="
|
||||||
|
my_item.key === 'rangeDate' && !my_item.isShow
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<RangeDateToolsHeader
|
||||||
|
@date-picker-handler="
|
||||||
|
(val) => emitHandler('date-picker', { data: val })
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="my_item.key === 'autoComplation'">
|
||||||
|
<AutoComplation
|
||||||
|
@auto-complation-handler="
|
||||||
|
(val) =>
|
||||||
|
emitHandler('auto-complation', { data: val })
|
||||||
|
"
|
||||||
|
:autoComplationSchema="{
|
||||||
|
autocompleteUrl: my_item.autocompleteUrl,
|
||||||
|
placeholder: my_item.placeholder,
|
||||||
|
debounceTime: my_item.debounceTime,
|
||||||
|
minCharsForAutocomplete:
|
||||||
|
my_item.minCharsForAutocomplete,
|
||||||
|
maxHistoryItems: my_item.maxHistoryItems,
|
||||||
|
showSearchButton: my_item.showSearchButton,
|
||||||
|
filters: my_item.filters,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="my_item.key === 'prevNext'"
|
||||||
|
class="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="flex items-center btn bg-primary text-white pl-2 py-1 rounded hover:bg-primary-700 transition-colors duration-300 ease-in-out disabled:cursor-not-allowed"
|
||||||
|
:disabled="my_item.prevDisabled"
|
||||||
|
@click="emitHandler('prev-click', { data: my_item })"
|
||||||
|
>
|
||||||
|
<UIcon
|
||||||
|
name="i-lucide-chevron-right"
|
||||||
|
class="w-6 h-6"
|
||||||
|
/>
|
||||||
|
<span>قبلی</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="flex items-center btn bg-primary text-white pr-2 py-1 rounded hover:bg-primary-700 transition-colors duration-300 ease-in-out disabled:cursor-not-allowed"
|
||||||
|
:disabled="my_item.nextDisabled"
|
||||||
|
@click="emitHandler('next-click', { data: my_item })"
|
||||||
|
>
|
||||||
|
<span>بعدی</span>
|
||||||
|
<UIcon name="i-lucide-chevron-left" class="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
headerTools: { type: Array, default: () => [] },
|
||||||
|
entity: { type: Object, default: () => ({}) },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["header-tools-action"]);
|
||||||
|
|
||||||
|
function emitHandler(action, payload) {
|
||||||
|
emit("header-tools-action", {
|
||||||
|
action,
|
||||||
|
data: payload.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedTabDetails = ref(null);
|
||||||
|
const localEntity = computed(() => props.entity || {});
|
||||||
|
|
||||||
|
function isArrayItems(item) {
|
||||||
|
return Array.isArray(item) || (item && item.items && !item.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArrayItems(item) {
|
||||||
|
return item.items ? item.items : Array.isArray(item) ? item : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDataValue(key) {
|
||||||
|
if (!localEntity.value) return "--";
|
||||||
|
const _source = localEntity.value._source || localEntity.value;
|
||||||
|
let res = "";
|
||||||
|
|
||||||
|
const keys = key.split("/");
|
||||||
|
for (const k of keys) {
|
||||||
|
const kv = k.split(".");
|
||||||
|
const value = kv.length === 2 ? _source[kv[0]]?.[kv[1]] : _source[k];
|
||||||
|
if (value) {
|
||||||
|
res = res ? `${res} / ${value}` : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res || _source[key] || _source.qanon_title || _source.title || "--";
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRange(event, item) {
|
||||||
|
const value = parseInt(event.target.value, 10);
|
||||||
|
item.value = value;
|
||||||
|
emitHandler("range-change", { data: { name: item.name, value, item } });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
191
app/components/auto-import/MySelect.vue
Executable file
191
app/components/auto-import/MySelect.vue
Executable file
|
|
@ -0,0 +1,191 @@
|
||||||
|
<template>
|
||||||
|
<div class="my-select">
|
||||||
|
<USelect
|
||||||
|
v-model="selectedValue"
|
||||||
|
:items="formattedItems"
|
||||||
|
:placeholder="selectSchema.placeholder || 'انتخاب کنید'"
|
||||||
|
:multiple="selectSchema.multiple || false"
|
||||||
|
:disabled="selectSchema.disabled || loading"
|
||||||
|
:loading="loading"
|
||||||
|
@update:modelValue="onModelUpdate"
|
||||||
|
:class="[gridColumnClass, 'cursor-pointer']"
|
||||||
|
:style="selectWidthStyle"
|
||||||
|
:ui="{ wrapper: 'w-full', base: 'w-full' }"
|
||||||
|
dir="rtl"
|
||||||
|
color="primary"
|
||||||
|
size="xl"
|
||||||
|
highlight
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, watch } from "vue";
|
||||||
|
import { useCachedRequest } from "@/composables/useCachedRequest";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
selectSchema: { type: Object, default: () => ({}) },
|
||||||
|
gridColumnClass: { type: String, default: "" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["my-select-action"]);
|
||||||
|
const emitAction = (action, payload) => {
|
||||||
|
emit("my-select-action", { action, payload });
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedValue = ref(null);
|
||||||
|
const rawItems = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const lastEmittedValue = ref(null);
|
||||||
|
|
||||||
|
const { fetchRequest } = useCachedRequest();
|
||||||
|
const mode = computed(() => props.selectSchema.mode ?? "local");
|
||||||
|
|
||||||
|
const formattedItems = computed(() => {
|
||||||
|
const labelKey = props.selectSchema.labelKey || "name";
|
||||||
|
const valueKey = props.selectSchema.valueKey || "id";
|
||||||
|
|
||||||
|
return rawItems.value.map((item) => {
|
||||||
|
if (typeof item === "string") return { label: item, value: item };
|
||||||
|
return {
|
||||||
|
label:
|
||||||
|
item[labelKey] ?? item.name ?? item.title ?? item.label ?? "بدون عنوان",
|
||||||
|
value: item[valueKey] ?? item.id ?? item._id ?? item.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const selectWidthStyle = computed(() => {
|
||||||
|
return props.selectSchema.width ? { width: props.selectSchema.width } : {};
|
||||||
|
});
|
||||||
|
|
||||||
|
// مقایسه امن برای جلوگیری از emit تکراری
|
||||||
|
const isSameValue = (a, b) => {
|
||||||
|
if (Array.isArray(a) && Array.isArray(b)) {
|
||||||
|
if (a.length !== b.length) return false;
|
||||||
|
return a.every((v, i) => v === b[i]);
|
||||||
|
}
|
||||||
|
return a === b;
|
||||||
|
};
|
||||||
|
|
||||||
|
const emitSelected = (val) => {
|
||||||
|
if (isSameValue(val, lastEmittedValue.value)) return;
|
||||||
|
|
||||||
|
lastEmittedValue.value = Array.isArray(val) ? [...val] : val;
|
||||||
|
|
||||||
|
const selectedItem = formattedItems.value.find((i) =>
|
||||||
|
props.selectSchema.multiple ? val?.includes(i.value) : i.value === val,
|
||||||
|
);
|
||||||
|
|
||||||
|
emitAction("selected", selectedItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncDefaultSelection = () => {
|
||||||
|
if (props.selectSchema.value != null) return;
|
||||||
|
if (!formattedItems.value.length) return;
|
||||||
|
|
||||||
|
const isEmpty =
|
||||||
|
selectedValue.value == null ||
|
||||||
|
(Array.isArray(selectedValue.value) && !selectedValue.value.length);
|
||||||
|
|
||||||
|
if (isEmpty) {
|
||||||
|
const firstItem = formattedItems.value[0];
|
||||||
|
const newVal = props.selectSchema.multiple
|
||||||
|
? [firstItem.value]
|
||||||
|
: firstItem.value;
|
||||||
|
|
||||||
|
selectedValue.value = newVal;
|
||||||
|
emitSelected(newVal);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onModelUpdate = (val) => {
|
||||||
|
emitSelected(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchItems = async () => {
|
||||||
|
if (mode.value !== "api") {
|
||||||
|
rawItems.value =
|
||||||
|
props.selectSchema.options ?? props.selectSchema.items ?? [];
|
||||||
|
syncDefaultSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiConfig = props.selectSchema.apiConfig || {};
|
||||||
|
if (!apiConfig.url) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await fetchRequest(apiConfig);
|
||||||
|
rawItems.value = Array.isArray(data) ? data : (data?.data ?? []);
|
||||||
|
syncDefaultSelection();
|
||||||
|
emitAction("fetch:success", { items: rawItems.value });
|
||||||
|
} catch (error) {
|
||||||
|
emitAction("fetch:error", { error: error?.message ?? error });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.selectSchema.items, props.selectSchema.options],
|
||||||
|
() => {
|
||||||
|
if (mode.value !== "api") {
|
||||||
|
rawItems.value =
|
||||||
|
props.selectSchema.options ?? props.selectSchema.items ?? [];
|
||||||
|
syncDefaultSelection();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.selectSchema.value,
|
||||||
|
(val) => {
|
||||||
|
selectedValue.value = val ?? null;
|
||||||
|
emitSelected(val);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(fetchItems);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.my-select {
|
||||||
|
button[role="combobox"] {
|
||||||
|
direction: rtl; /* راستچین کردن متن و placeholder */
|
||||||
|
text-align: right; /* ترازبندی متن داخل button */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. placeholder */
|
||||||
|
button[role="combobox"] [data-slot="placeholder"] {
|
||||||
|
text-align: right;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. آیکون پایین (chevron) */
|
||||||
|
button[role="combobox"] [data-slot="trailing"] {
|
||||||
|
right: auto; /* اگر نیاز باشد موقعیت icon را اصلاح کنید */
|
||||||
|
left: 0; /* icon به سمت چپ میرود */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4. منوی بازشونده (اگر teleport شده باشد) */
|
||||||
|
/* کلاس واقعی منو را از inspector پیدا کنید و به جای .u-select__menu بنویسید */
|
||||||
|
.u-select__menu {
|
||||||
|
direction: rtl !important;
|
||||||
|
text-align: right !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 5. آیتمهای منو */
|
||||||
|
.u-select__menu [data-slot="item"] {
|
||||||
|
text-align: right;
|
||||||
|
direction: rtl;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse; /* اگر icon ها در آیتم هستند آنها را برعکس میکند */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.i-lucide\:check {
|
||||||
|
color: #22c55e;
|
||||||
|
stroke: #22c55e;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,9 +1,80 @@
|
||||||
<template>
|
<template>
|
||||||
<h1>main list</h1>
|
<div>
|
||||||
|
<HeaderTools
|
||||||
|
:header-tools="headerTools"
|
||||||
|
@header-tools-action="headerToolsAction"
|
||||||
|
></HeaderTools>
|
||||||
|
<div class="p-4 pt-0 w-full lg:flex-1">
|
||||||
|
<MyContent
|
||||||
|
:mainSchema="myContentSchema"
|
||||||
|
:pagination="pagination"
|
||||||
|
@my-content-action="myContentAction"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {};
|
import sampelDataDb from "@/json/data-entry/sampelDataDb.json";
|
||||||
|
import refineCodes from "@/json/refineCodes.json";
|
||||||
|
import MyContent from "@/components/lazy-load/global/MyContent.vue";
|
||||||
|
const { $http: httpService } = useNuxtApp();
|
||||||
|
const props = defineProps({
|
||||||
|
listConflicts: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const pagination = ref({
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
let myContentSchema = {};
|
||||||
|
myContentSchema = ref(JSON.parse(JSON.stringify(sampelDataDb)));
|
||||||
|
myContentSchema.value.items = props.listConflicts.hits.hits;
|
||||||
|
myContentSchema.value.pagination = {
|
||||||
|
total: props.listConflicts.hits.total.value,
|
||||||
|
page: pagination.value.page,
|
||||||
|
limit: pagination.value.limit,
|
||||||
|
};
|
||||||
|
const headerTools = computed(() => [
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: "dropdown",
|
||||||
|
key: "dropdown",
|
||||||
|
name: "refine_codes",
|
||||||
|
dropdownSchema: {
|
||||||
|
width: "18em",
|
||||||
|
modelValue: null, // ✅ اینو بذار
|
||||||
|
optionAttribute: "title",
|
||||||
|
valueAttribute: "value",
|
||||||
|
searchable: false,
|
||||||
|
placeholder: "انتخاب کنید",
|
||||||
|
items: refineCodes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "prevNext",
|
||||||
|
name: "entityNavigator",
|
||||||
|
prevDisabled: false,
|
||||||
|
nextDisabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
function headerToolsAction({ action, data }) {
|
||||||
|
if (action === "prev-click") {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "next-click") {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function myContentAction({ action, data }) {
|
||||||
|
console.log(action, data);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style lang="scss"></style>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,26 @@
|
||||||
<template>
|
<template>
|
||||||
<h1>RelationEdit</h1>
|
<div>
|
||||||
|
<HeaderTools></HeaderTools>
|
||||||
<TiptapEditor></TiptapEditor>
|
<TiptapEditor :accordionData="accordionItems"></TiptapEditor>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {};
|
const props = defineProps({
|
||||||
|
listConflicts: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const accordionItems = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "عنوان",
|
||||||
|
content: "متن",
|
||||||
|
isOpen: true,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,26 @@
|
||||||
<template>
|
<template>
|
||||||
<h1>RuleEdit</h1>
|
<div>
|
||||||
|
<HeaderTools></HeaderTools>
|
||||||
<TiptapEditor></TiptapEditor>
|
<TiptapEditor :accordionData="accordionItems"></TiptapEditor>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {};
|
const props = defineProps({
|
||||||
|
listConflicts: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const accordionItems = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "عنوان",
|
||||||
|
content: "متن",
|
||||||
|
isOpen: true,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|
|
||||||
62
app/components/lazy-load/global/EChart.vue
Executable file
62
app/components/lazy-load/global/EChart.vue
Executable file
|
|
@ -0,0 +1,62 @@
|
||||||
|
<template>
|
||||||
|
<div ref="chartRef" class="chart"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
option: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
autoresize: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const chartRef = ref(null)
|
||||||
|
let chartInstance = null
|
||||||
|
|
||||||
|
const initChart = () => {
|
||||||
|
if (!chartRef.value) return
|
||||||
|
chartInstance = echarts.init(chartRef.value)
|
||||||
|
chartInstance.setOption(props.option)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeChart = () => {
|
||||||
|
chartInstance?.resize()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initChart()
|
||||||
|
if (props.autoresize) {
|
||||||
|
window.addEventListener('resize', resizeChart)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', resizeChart)
|
||||||
|
chartInstance?.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
/* وقتی option از بیرون عوض شد */
|
||||||
|
watch(
|
||||||
|
() => props.option,
|
||||||
|
(newOption) => {
|
||||||
|
if (chartInstance && newOption) {
|
||||||
|
chartInstance.setOption(newOption, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
906
app/components/lazy-load/global/FormBuilder.vue
Executable file
906
app/components/lazy-load/global/FormBuilder.vue
Executable file
|
|
@ -0,0 +1,906 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="mt-3 text__15 text__green" v-if="description">
|
||||||
|
<span>{{ description }} </span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- tab/grouped mode -->
|
||||||
|
<template :key="renderMain" v-if="isArrayItems()">
|
||||||
|
<!-- start:horizontal mode -->
|
||||||
|
<template v-if="displayMode == 'horizontal'">
|
||||||
|
<div class="horizontal">
|
||||||
|
<!-- Tabs -->
|
||||||
|
<ul class="flex bg-gray-100 rounded-t-md overflow-hidden h-12">
|
||||||
|
<li
|
||||||
|
v-for="(groupItem, index) in localFormElements"
|
||||||
|
:key="'groupItem' + index"
|
||||||
|
@click="setTab(index)"
|
||||||
|
class="cursor-pointer mt-2"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="px-4 py-2 block relative"
|
||||||
|
:class="
|
||||||
|
currentTab === index
|
||||||
|
? 'bg-white text-gray-900 rounded-t-md'
|
||||||
|
: 'text-gray-600 hover:bg-gray-200'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ groupItem.title }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Tab content -->
|
||||||
|
<div
|
||||||
|
class="tab-content bg-white p-4 border border-t-0 border-gray-300 rounded-b-md"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(groupItem, index) in localFormElements"
|
||||||
|
:key="'tabcontent' + index"
|
||||||
|
>
|
||||||
|
<div v-show="currentTab === index" class="px-3">
|
||||||
|
<form>
|
||||||
|
<div class="grid grid-cols-12 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="(formElement, index1) in getGroupListElements(
|
||||||
|
groupItem.items,
|
||||||
|
)"
|
||||||
|
:key="formElement.key + index1"
|
||||||
|
:class="giveColClass('', formElement)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="
|
||||||
|
componentMap[
|
||||||
|
returnComponentNameDefault(formElement, groupItem)
|
||||||
|
]
|
||||||
|
"
|
||||||
|
:ref="`${refKeyBase}_${formElement.key}`"
|
||||||
|
:key="'element' + formElement.key + '_' + index1"
|
||||||
|
:formElement="
|
||||||
|
addValueToFormElement(formElement, groupItem)
|
||||||
|
"
|
||||||
|
:otherData="otherData"
|
||||||
|
:isReadOnly="readOnly"
|
||||||
|
:multipleMode="multipleMode"
|
||||||
|
:acceptType="acceptType"
|
||||||
|
:class="giveColClass('', formElement)"
|
||||||
|
:inputClass="giveColClass('input', formElement)"
|
||||||
|
:labelClass="'col-12'"
|
||||||
|
class="inside-entity align-items-center"
|
||||||
|
:classComponentName="classComponentName"
|
||||||
|
@updateComponentOption="
|
||||||
|
(ev) => updateComponentOption(ev, formElement)
|
||||||
|
"
|
||||||
|
@link-route-clicked="linkRouteClicked"
|
||||||
|
@take-value="
|
||||||
|
(ev) => takeValueChanged(ev, formElement, groupItem)
|
||||||
|
"
|
||||||
|
@on-upload-file="onUploadFile"
|
||||||
|
@action-affected-item="
|
||||||
|
(ev) => actionAffectedItem(ev, formElement)
|
||||||
|
"
|
||||||
|
@keydown="
|
||||||
|
formBuilderAction('formBuilderKeydown', $event)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<div
|
||||||
|
v-if="isHorizontalButtonSubmit"
|
||||||
|
class="mt-4 flex justify-end"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="
|
||||||
|
isShowButtonAccordion(groupItem) &&
|
||||||
|
!isReadOnlyGroup(groupItem)
|
||||||
|
"
|
||||||
|
type="submit"
|
||||||
|
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||||
|
@click.prevent="saveGroupProperty(groupItem)"
|
||||||
|
>
|
||||||
|
{{ groupItem.submit_label ?? "ثبت" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- start:vertical mode -->
|
||||||
|
<div
|
||||||
|
class="flex flex-col md:flex-row"
|
||||||
|
v-else-if="displayMode == 'vertical'"
|
||||||
|
>
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div :class="listClass ?? 'md:w-1/4 w-1/3'">
|
||||||
|
<div
|
||||||
|
class="flex flex-col space-y-2"
|
||||||
|
role="tablist"
|
||||||
|
aria-orientation="vertical"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="(groupItem, index) in localFormElements"
|
||||||
|
:key="'groupItem' + index"
|
||||||
|
@click="setTab(index)"
|
||||||
|
class="px-4 py-2 text-left rounded-lg transition-colors duration-200"
|
||||||
|
:class="
|
||||||
|
currentTab == index
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
|
||||||
|
"
|
||||||
|
role="tab"
|
||||||
|
:aria-selected="currentTab == index"
|
||||||
|
>
|
||||||
|
{{ groupItem.title }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div :class="mainClass ?? 'md:w-3/4 w-2/3'">
|
||||||
|
<div class="tab-content">
|
||||||
|
<div
|
||||||
|
class="tab-pane transition-all duration-300"
|
||||||
|
:class="currentTab == index ? 'block' : 'hidden'"
|
||||||
|
v-for="(groupItem, index) in localFormElements"
|
||||||
|
:key="'tab-pane' + index"
|
||||||
|
role="tabpanel"
|
||||||
|
>
|
||||||
|
<div class="ml-5 relative">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="grid grid-cols-12 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="(formElement, index1) in getGroupListElements(
|
||||||
|
groupItem?.items,
|
||||||
|
)"
|
||||||
|
:key="index1"
|
||||||
|
:class="giveColClass('', formElement)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="
|
||||||
|
componentMap[
|
||||||
|
returnComponentNameDefault(formElement, groupItem)
|
||||||
|
]
|
||||||
|
"
|
||||||
|
:ref="`${refKeyBase}_${formElement.key}`"
|
||||||
|
:key="'element' + formElement.key + '_' + index1"
|
||||||
|
:formElement="
|
||||||
|
addValueToFormElement(formElement, groupItem)
|
||||||
|
"
|
||||||
|
:otherData="otherData"
|
||||||
|
:chartComponentName="formElement.chartComponentName"
|
||||||
|
:isReadOnly="readOnly"
|
||||||
|
:multipleMode="multipleMode"
|
||||||
|
:acceptType="acceptType"
|
||||||
|
:class="
|
||||||
|
giveColClass('', formElement) +
|
||||||
|
' inside-entity align-items-center'
|
||||||
|
"
|
||||||
|
:inputClass="giveColClass('input', formElement)"
|
||||||
|
:labelClass="'col-12'"
|
||||||
|
:classComponentName="classComponentName"
|
||||||
|
@treemap-node-click="
|
||||||
|
formBuilderAction('treemap-node-click', $event)
|
||||||
|
"
|
||||||
|
@updateComponentOption="
|
||||||
|
(ev) => updateComponentOption(ev, formElement)
|
||||||
|
"
|
||||||
|
@link-route-clicked="linkRouteClicked"
|
||||||
|
@take-value="
|
||||||
|
(ev) => takeValueChanged(ev, formElement, groupItem)
|
||||||
|
"
|
||||||
|
@on-upload-file="onUploadFile"
|
||||||
|
@action-affected-item="
|
||||||
|
(ev) => actionAffectedItem(ev, formElement)
|
||||||
|
"
|
||||||
|
@keydown="
|
||||||
|
formBuilderAction('formBuilderKeydown', $event)
|
||||||
|
"
|
||||||
|
:widthChart="'100%'"
|
||||||
|
:heightChart="'64dvh'"
|
||||||
|
:isOpenModal="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- accordion & accordion-show -->
|
||||||
|
<template
|
||||||
|
v-else-if="
|
||||||
|
displayMode == 'accordion' || displayMode == 'accordion-show'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<UAccordion
|
||||||
|
:items="accordionItems"
|
||||||
|
:type="displayMode === 'accordion-show' ? 'multiple' : 'single'"
|
||||||
|
:collapsible="displayMode !== 'accordion-show'"
|
||||||
|
:default-value="
|
||||||
|
displayMode === 'accordion-show'
|
||||||
|
? accordionItems.map((_, i) => String(i))
|
||||||
|
: []
|
||||||
|
"
|
||||||
|
:unmount-on-hide="false"
|
||||||
|
:ui="{
|
||||||
|
root: 'w-full space-y-2',
|
||||||
|
item: 'rounded-md border border-gray-200 dark:border-gray-700',
|
||||||
|
header: 'flex bg-gray-100 dark:bg-dark-primary-700 rounded-md',
|
||||||
|
trigger:
|
||||||
|
'group flex-1 items-center justify-between gap-2 py-3 px-4 font-medium text-start focus-visible:outline-primary cursor-pointer',
|
||||||
|
content:
|
||||||
|
'overflow-hidden data-[state=open]:animate-[accordion-down_200ms_ease-out] data-[state=closed]:animate-[accordion-up_200ms_ease-out]',
|
||||||
|
body: 'p-0',
|
||||||
|
label: 'text-base font-medium text-gray-900 dark:text-white',
|
||||||
|
trailingIcon:
|
||||||
|
'ms-auto size-5 text-gray-500 group-data-[state=open]:rotate-180 transition-transform',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #content="{ item, index }">
|
||||||
|
<form @submit.prevent="saveGroupProperty(item.data)">
|
||||||
|
<div class="p-4">
|
||||||
|
<div v-if="item.data.is_array" class="mb-4">
|
||||||
|
<TableComponent
|
||||||
|
:key="`table-${index}`"
|
||||||
|
height="14em"
|
||||||
|
:tableColumns="item.data.table_columns"
|
||||||
|
:tagActions="item.data.tag_actions"
|
||||||
|
:items="localFormData[item.data.key]"
|
||||||
|
:itemKey="item.data.key"
|
||||||
|
:formElements="item.data.items"
|
||||||
|
:isReadOnly="isReadOnlyGroup(item.data)"
|
||||||
|
:showActions="!isReadOnlyGroup(item.data)"
|
||||||
|
@updateTableComponent="updateTableComponent"
|
||||||
|
@add-table-item="addTableItem(item.data)"
|
||||||
|
@edit-table-item="editTableItem($event, item.data)"
|
||||||
|
@action-table-item="actionTableItem($event, item.data)"
|
||||||
|
@delete-table-item="deleteTableItem($event, item.data)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="grid grid-cols-12 gap-4 mb-4">
|
||||||
|
<div
|
||||||
|
v-for="(formElement, idx) in getGroupListElements(
|
||||||
|
item.data.items,
|
||||||
|
)"
|
||||||
|
:key="`${formElement.key}-${idx}`"
|
||||||
|
:class="giveColClass('', formElement)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="
|
||||||
|
componentMap[
|
||||||
|
returnComponentNameDefault(formElement, item.data)
|
||||||
|
]
|
||||||
|
"
|
||||||
|
:ref="`${refKeyBase}_${formElement.key}`"
|
||||||
|
:formElement="
|
||||||
|
addValueToFormElement(formElement, item.data)
|
||||||
|
"
|
||||||
|
:otherData="otherData"
|
||||||
|
:isReadOnly="isReadOnlyGroup(item.data, formElement)"
|
||||||
|
:multipleMode="multipleMode"
|
||||||
|
:acceptType="acceptType"
|
||||||
|
:class="giveColClass('', formElement)"
|
||||||
|
:inputClass="giveColClass('input', formElement)"
|
||||||
|
classComponentName="inside-entity"
|
||||||
|
@updateComponentOption="
|
||||||
|
(ev) => updateComponentOption(ev, formElement)
|
||||||
|
"
|
||||||
|
@link-route-clicked="linkRouteClicked"
|
||||||
|
@take-value="
|
||||||
|
(ev) => takeValueChanged(ev, formElement, item.data)
|
||||||
|
"
|
||||||
|
@on-upload-file="onUploadFile"
|
||||||
|
@action-affected-item="
|
||||||
|
(ev) => actionAffectedItem(ev, formElement)
|
||||||
|
"
|
||||||
|
@openModalTags="openModalTags"
|
||||||
|
@keydown="formBuilderAction('formBuilderKeydown', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
isShowButtonAccordion(item.data) &&
|
||||||
|
!isReadOnlyGroup(item.data)
|
||||||
|
"
|
||||||
|
class="flex justify-end"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
type="submit"
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
size="md"
|
||||||
|
class="px-4 cursor-pointer"
|
||||||
|
@click.prevent="saveGroupProperty(item.data)"
|
||||||
|
>
|
||||||
|
{{ item.data.submit_label || "ثبت" }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
</UAccordion>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- default mode -->
|
||||||
|
<template v-else-if="displayMode == 'default'">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row grid grid-cols-12 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="(formElement, index) in getGroupListElements(
|
||||||
|
localFormElements,
|
||||||
|
)"
|
||||||
|
:key="formElement.key + index"
|
||||||
|
:class="giveColClass('', formElement)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="
|
||||||
|
componentMap[
|
||||||
|
returnComponentNameDefault(formElement, groupItem)
|
||||||
|
]
|
||||||
|
"
|
||||||
|
:ref="`${refKeyBase}_${formElement.key}`"
|
||||||
|
:key="index + formElement.key + render"
|
||||||
|
:formElement="
|
||||||
|
addValueToFormElement(formElement, localFormElements)
|
||||||
|
"
|
||||||
|
:otherData="otherData"
|
||||||
|
:isReadOnly="readOnly"
|
||||||
|
:multipleMode="multipleMode"
|
||||||
|
:acceptType="acceptType"
|
||||||
|
:class="giveColClass(formElement.type, formElement)"
|
||||||
|
:inputClass="giveColClass('input', formElement)"
|
||||||
|
:labelClass="giveColClass('label', formElement)"
|
||||||
|
class="inside-entity align-items-center"
|
||||||
|
:classComponentName="classComponentName"
|
||||||
|
@updateComponentOption="
|
||||||
|
(ev) => updateComponentOption(ev, formElement)
|
||||||
|
"
|
||||||
|
@on-action-handler="onActionHandler"
|
||||||
|
@link-route-clicked="linkRouteClicked"
|
||||||
|
@take-value="
|
||||||
|
(ev) => takeValueChanged(ev, formElement, localFormElements)
|
||||||
|
"
|
||||||
|
@on-upload-file="onUploadFile"
|
||||||
|
@action-affected-item="
|
||||||
|
(ev) => actionAffectedItem(ev, formElement)
|
||||||
|
"
|
||||||
|
@take-value-validate="
|
||||||
|
formBuilderAction('take-value-validate', $event)
|
||||||
|
"
|
||||||
|
@keydown="formBuilderAction('formBuilderKeydown', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
reactive,
|
||||||
|
computed,
|
||||||
|
watch,
|
||||||
|
onMounted,
|
||||||
|
onBeforeUnmount,
|
||||||
|
nextTick,
|
||||||
|
} from "vue";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
|
||||||
|
// Async Components (same as before, but using defineAsyncComponent explicitly)
|
||||||
|
|
||||||
|
// --- جایگزینی بخش import و تعریف componentMap ---
|
||||||
|
import FormInputComponent from "@/components/lazy-load/form-builder/FormInputComponent.vue";
|
||||||
|
import FormTextareaComponent from "@/components/lazy-load/form-builder/FormTextareaComponent.vue";
|
||||||
|
import FormSelectComponent from "@/components/lazy-load/form-builder/FormSelectComponent.vue";
|
||||||
|
import FormDateComponent from "@/components/lazy-load/form-builder/FormDateComponent.vue";
|
||||||
|
import FormUploadFilesComponent from "@/components/lazy-load/form-builder/FormUploadFilesComponent.vue";
|
||||||
|
|
||||||
|
// Transform localFormElements into AccordionItem[]
|
||||||
|
const accordionItems = computed(() => {
|
||||||
|
return localFormElements.value.map((groupItem, index) => ({
|
||||||
|
label: groupItem.title,
|
||||||
|
// Optional: you can add `value` if needed for controlled mode
|
||||||
|
// value: String(index),
|
||||||
|
// Pass original data for slots
|
||||||
|
data: groupItem,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
// سایر کامپوننتها را هم به همین ترتیب اضافه کنید:
|
||||||
|
// import FormSelectComponent from '...';
|
||||||
|
// import FormLabelComponentReadOnly from '...';
|
||||||
|
|
||||||
|
const componentMap = {
|
||||||
|
FormInputComponent,
|
||||||
|
FormTextareaComponent,
|
||||||
|
FormSelectComponent,
|
||||||
|
FormDateComponent,
|
||||||
|
FormUploadFilesComponent,
|
||||||
|
// ... سایر کامپوننتهایی که استفاده میکنید
|
||||||
|
};
|
||||||
|
const props = defineProps({
|
||||||
|
refKeyBase: {
|
||||||
|
type: String,
|
||||||
|
default: "formbuilder",
|
||||||
|
},
|
||||||
|
displayMode: {
|
||||||
|
type: String,
|
||||||
|
default: "horizontal",
|
||||||
|
},
|
||||||
|
readOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
dataChart: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
chartComponentName: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
formElements: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
formData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
otherData: {
|
||||||
|
type: [Object, Array],
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
previewMode: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
classComponentName: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
multipleMode: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
isHorizontalButtonSubmit: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
acceptType: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
listClass: {
|
||||||
|
type: String,
|
||||||
|
default: "col-md-12",
|
||||||
|
},
|
||||||
|
mainClass: {
|
||||||
|
type: String,
|
||||||
|
default: "col-md-9",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emits (explicitly defined)
|
||||||
|
const emit = defineEmits(["form-builder-action"]);
|
||||||
|
function formBuilderAction(action, payload) {
|
||||||
|
emit("form-builder-action", {
|
||||||
|
action: action,
|
||||||
|
payload: payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// === Reactive State ===
|
||||||
|
const renderMain = ref(100);
|
||||||
|
const tableRender = ref(0);
|
||||||
|
const currentTab = ref(0);
|
||||||
|
const prevActiveTabIndex = ref(undefined);
|
||||||
|
const localFormElements = ref([]);
|
||||||
|
const localFormData = ref({});
|
||||||
|
const localFormDataChanged = ref({});
|
||||||
|
const activeRequest = ref([]);
|
||||||
|
|
||||||
|
let this_vm = getCurrentInstance();
|
||||||
|
|
||||||
|
// === Computed ===
|
||||||
|
const buttonText = computed(() => {
|
||||||
|
return localFormData.value?.id || localFormData.value?.guid
|
||||||
|
? "بروزرسانی"
|
||||||
|
: "افزودن";
|
||||||
|
});
|
||||||
|
|
||||||
|
// === Methods ===
|
||||||
|
const foundItemFormData = (key, index) => {
|
||||||
|
let item;
|
||||||
|
Object.keys(localFormData.value).forEach((nameItem) => {
|
||||||
|
if (nameItem === key) {
|
||||||
|
const arrayItemFound = localFormData.value[nameItem];
|
||||||
|
item = arrayItemFound?.[index];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return item;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTableComponent = (event) => {
|
||||||
|
const { key, items } = event;
|
||||||
|
localFormData.value[key] = items;
|
||||||
|
localFormDataChanged.value[key] = items;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isShowButtonAccordion = (item) => {
|
||||||
|
if (item.submit_label === "") return false;
|
||||||
|
return !props.readOnly;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isReadOnlyGroup = (groupItem, item = undefined) => {
|
||||||
|
if (props.readOnly) return true;
|
||||||
|
let res = false;
|
||||||
|
if ("has_permission" in groupItem) res = !groupItem.has_permission;
|
||||||
|
if (item && "readOnly" in item && !res) res = item.readOnly;
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveGroupProperty = (groupItem) => {
|
||||||
|
let changed_data = localFormDataChanged.value;
|
||||||
|
if (localFormDataChanged.value[groupItem.key]) {
|
||||||
|
changed_data = localFormDataChanged.value[groupItem.key];
|
||||||
|
}
|
||||||
|
let data = {};
|
||||||
|
let data_form = {};
|
||||||
|
|
||||||
|
if (!groupItem.type_data || groupItem.type_data === "normal") {
|
||||||
|
groupItem.items?.forEach((item) => {
|
||||||
|
if (changed_data[item.key]) data[item.key] = changed_data[item.key];
|
||||||
|
});
|
||||||
|
data_form = data;
|
||||||
|
} else {
|
||||||
|
data_form[groupItem.key] = changed_data;
|
||||||
|
}
|
||||||
|
formBuilderAction("saveFormGroup", data_form);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onActionHandler = ({ action, key, value }) => {
|
||||||
|
const target = action.target;
|
||||||
|
updateValueFormData(target, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isArrayItems = () => {
|
||||||
|
return Array.isArray(localFormElements.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const listenEventBus = ({ value, affectedTo }) => {
|
||||||
|
const refkey = `${props.refKeyBase}_${affectedTo.key}`;
|
||||||
|
const refs = this_vm.refs || {};
|
||||||
|
if (refs[refkey]) {
|
||||||
|
try {
|
||||||
|
const method =
|
||||||
|
affectedTo.action === "ufInitTextValue"
|
||||||
|
? (v) => refs[refkey][0]?.ufInitTextValue(v, true)
|
||||||
|
: (v) => refs[refkey][0]?.[affectedTo.action]?.(v);
|
||||||
|
method?.(value);
|
||||||
|
} catch (err) {
|
||||||
|
// silent
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
localFormData.value[affectedTo.key] = value;
|
||||||
|
localFormDataChanged.value[affectedTo.key] = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setTab = (index) => {
|
||||||
|
if (prevActiveTabIndex.value !== undefined) {
|
||||||
|
localFormElements.value[prevActiveTabIndex.value].active = false;
|
||||||
|
}
|
||||||
|
currentTab.value = index;
|
||||||
|
prevActiveTabIndex.value = index;
|
||||||
|
|
||||||
|
formBuilderAction("setTab", index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGroupListElements = (items) => {
|
||||||
|
if (!items) return [];
|
||||||
|
return items.filter((el) => !("ishide" in el) || el.ishide !== 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findItemSchema = (key, items = undefined) => {
|
||||||
|
items = items || localFormElements.value;
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
if (items[i]?.key === key) return items[i];
|
||||||
|
if (items[i].items) {
|
||||||
|
const found = findItemSchema(key, items[i].items);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSourceData = (itemData, key) => {
|
||||||
|
let elMom = itemData;
|
||||||
|
const elKeys = key.split("__");
|
||||||
|
for (let i = 0; i < elKeys.length - 1; i++) {
|
||||||
|
if (elMom?.[elKeys[i]]) {
|
||||||
|
elMom = elMom[elKeys[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalKey = elKeys[elKeys.length - 1];
|
||||||
|
|
||||||
|
// console.log('getSourceData ', finalKey, elMom?.[finalKey], elMom);
|
||||||
|
return elMom?.[finalKey] ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addValueToFormElement = (formElement, innerGroupItem) => {
|
||||||
|
const cloned = { ...formElement };
|
||||||
|
cloned["componentName"] = returnComponentNameDefault(cloned, innerGroupItem);
|
||||||
|
|
||||||
|
if (formElement.key) {
|
||||||
|
let formData = localFormData.value;
|
||||||
|
if (innerGroupItem && innerGroupItem?.type_data === "object") {
|
||||||
|
formData = localFormData.value[innerGroupItem.key] ?? {};
|
||||||
|
} else if (formElement.key == "file") {
|
||||||
|
formElement.key = "subtitle";
|
||||||
|
}
|
||||||
|
const value = getSourceData(formData, formElement.key);
|
||||||
|
cloned["value"] = value;
|
||||||
|
}
|
||||||
|
// console.log('addValueToFormElement ', formElement.key, cloned.value, cloned);
|
||||||
|
|
||||||
|
return cloned;
|
||||||
|
};
|
||||||
|
|
||||||
|
const returnComponentNameDefault = (item, groupItem) => {
|
||||||
|
const type = item.type;
|
||||||
|
// console.log("type ==> ", type);
|
||||||
|
const _readOnly = props.readOnly
|
||||||
|
? true
|
||||||
|
: groupItem
|
||||||
|
? isReadOnlyGroup(groupItem, item)
|
||||||
|
: false;
|
||||||
|
// console.log("_readOnly ==> ", _readOnly);
|
||||||
|
if (_readOnly) {
|
||||||
|
if (type === "textarea") return "FormTextareaComponent";
|
||||||
|
else if (type === "htmleditor") return "HtmlEditor";
|
||||||
|
else if (type === "tinyeditor") return "MyTinyMce";
|
||||||
|
else return "FormLabelComponentReadOnly";
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "textarea":
|
||||||
|
return "FormTextareaComponent";
|
||||||
|
case "select":
|
||||||
|
return "FormSelectComponent";
|
||||||
|
case "string":
|
||||||
|
case "number":
|
||||||
|
case "float":
|
||||||
|
return "FormInputComponent";
|
||||||
|
case "label":
|
||||||
|
return "FormLabelComponentReadOnly";
|
||||||
|
case "tags":
|
||||||
|
return "tagsComponent";
|
||||||
|
case "selectTags":
|
||||||
|
return "SelectTagsComponent";
|
||||||
|
case "htmleditor":
|
||||||
|
return "HtmlEditor";
|
||||||
|
case "tinyeditor":
|
||||||
|
return "MyTinyMce";
|
||||||
|
case "date":
|
||||||
|
return "FormDateComponent";
|
||||||
|
case "range_date":
|
||||||
|
return "RangeDateComponent";
|
||||||
|
case "checkbox":
|
||||||
|
case "radio":
|
||||||
|
return "CheckboxComponent";
|
||||||
|
case "label_button":
|
||||||
|
return "LabelButtonComponent";
|
||||||
|
case "upload":
|
||||||
|
return "FormUploadFilesComponent";
|
||||||
|
case "multiFormSelectComponent":
|
||||||
|
return "MultiFormSelectComponent";
|
||||||
|
case "chart_content":
|
||||||
|
return "ChartContent";
|
||||||
|
default:
|
||||||
|
return "FormInputComponent";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionAffectedItem = ({ action, key, value }, schemaItem) => {
|
||||||
|
// console.log("actionAffectedItem ", action, key, value);
|
||||||
|
|
||||||
|
formBuilderAction("form-action-affected", { action, key, value, schemaItem });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUploadFile = (fileUploadData) => {
|
||||||
|
formBuilderAction("uploadFile", fileUploadData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const takeValueObjectChanged = (value, formElement, innerGroupItem) => {
|
||||||
|
if (!innerGroupItem || innerGroupItem?.type_data !== "object") return false;
|
||||||
|
|
||||||
|
let root_object =
|
||||||
|
getSourceData(localFormData.value, innerGroupItem.key) ?? {};
|
||||||
|
root_object[formElement.key] = value;
|
||||||
|
localFormData.value[innerGroupItem.key] = root_object;
|
||||||
|
localFormDataChanged.value[innerGroupItem.key] = root_object;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const takeValueChanged = (value, schemaItem, innerGroupItem = null) => {
|
||||||
|
const key = schemaItem.key;
|
||||||
|
if (!takeValueObjectChanged(value, schemaItem, innerGroupItem)) {
|
||||||
|
if (typeof value === "string") value = value.trim() ?? null;
|
||||||
|
localFormData.value[key] = value;
|
||||||
|
localFormDataChanged.value[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
formBuilderAction("changeFormValues", { ...localFormDataChanged.value });
|
||||||
|
formBuilderAction("changeFormValueOne", { key, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const callItemMethod = (key, method, val) => {
|
||||||
|
const refkey = `${props.refKeyBase}_${key}`;
|
||||||
|
let refs = this_vm.refs || {};
|
||||||
|
if (refs[refkey]?.[0]?.[method]) {
|
||||||
|
refs[refkey][0][method](val);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateValueFormData = (key, value) => {
|
||||||
|
if (!value) return;
|
||||||
|
if (typeof value === "string") value = value.trim();
|
||||||
|
|
||||||
|
const refkey = `${props.refKeyBase}_${key}`;
|
||||||
|
let refs = this_vm.refs || {};
|
||||||
|
|
||||||
|
if (refs[refkey]?.initTextValue) {
|
||||||
|
refs[refkey].initTextValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
localFormData.value[key] = value;
|
||||||
|
localFormDataChanged.value[key] = value;
|
||||||
|
formBuilderAction("changeFormValues", { ...localFormDataChanged.value });
|
||||||
|
formBuilderAction("changeFormValueOne", { key, value: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const giveColClass = (type, item) => {
|
||||||
|
// اگر کلاس دستی داده شده باشد استفاده میکنیم
|
||||||
|
// if (item.classes) {
|
||||||
|
// if (type === "") return item.classes;
|
||||||
|
// if (type === "label") return "col-auto";
|
||||||
|
// if (["input", "string", "textarea"].includes(type)) {
|
||||||
|
// return item.inputClass || item.classes;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// بررسی colSpan
|
||||||
|
const span = item.colSpan ?? 12; // پیشفرض کل عرض
|
||||||
|
const validSpan = Math.min(Math.max(span, 1), 12); // بین 1 تا 12
|
||||||
|
return `col-span-${validSpan}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkRouteClicked = (item) => {
|
||||||
|
if (!item.link_route) return;
|
||||||
|
const id_route = localFormData.value[item.link_route.id] ?? "";
|
||||||
|
const key = item.link_route.key;
|
||||||
|
const routeData = router.resolve({
|
||||||
|
name: item.link_route.name,
|
||||||
|
params: { id: id_route, key },
|
||||||
|
query: {},
|
||||||
|
});
|
||||||
|
window.open(routeData.href, "_blank");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Table-specific handlers
|
||||||
|
const deleteTableItem = (event, schema) => {
|
||||||
|
const item = foundItemFormData(schema.key, event);
|
||||||
|
formBuilderAction("delete-table-item", {
|
||||||
|
schema,
|
||||||
|
item,
|
||||||
|
index: event,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const editTableItem = (event, schema) => {
|
||||||
|
const item = foundItemFormData(schema.key, event);
|
||||||
|
formBuilderAction("edit-table-item", {
|
||||||
|
schema,
|
||||||
|
item,
|
||||||
|
index: event,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const duplicateTableItem = (event, schema) => {
|
||||||
|
const item = foundItemFormData(schema.key, event);
|
||||||
|
formBuilderAction("duplicate-table-item", {
|
||||||
|
schema,
|
||||||
|
item,
|
||||||
|
index: event,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionTableItem = (event, schema) => {
|
||||||
|
emit(event.action, {
|
||||||
|
item: event.data.item,
|
||||||
|
index: event.data.index,
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTableItem = (schema) => {
|
||||||
|
formBuilderAction("add-table-item", schema);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open modal for tags
|
||||||
|
const openModalTags = (item) => {
|
||||||
|
formBuilderAction("edit-property-tags", item);
|
||||||
|
};
|
||||||
|
|
||||||
|
// === Setup: Lifecycle & Watchers ===
|
||||||
|
import { getCurrentInstance } from "vue";
|
||||||
|
import { useNuxtApp } from "#app";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const { $eventBus } = useNuxtApp();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
localFormElements.value = props.formElements
|
||||||
|
? cloneDeep(props.formElements)
|
||||||
|
: [];
|
||||||
|
localFormData.value = props.formData ? cloneDeep(props.formData) : {};
|
||||||
|
renderMain.value++;
|
||||||
|
setTab(0);
|
||||||
|
|
||||||
|
// Event bus listener
|
||||||
|
if ($eventBus) {
|
||||||
|
$eventBus.on("catch-form-builder-event", listenEventBus);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if ($eventBus) {
|
||||||
|
$eventBus.off("catch-form-builder-event", listenEventBus);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watchers (deep + immediate)
|
||||||
|
watch(
|
||||||
|
() => props.formElements,
|
||||||
|
(newValue) => {
|
||||||
|
localFormElements.value = newValue ? cloneDeep(newValue) : [];
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.formData,
|
||||||
|
(newValue) => {
|
||||||
|
localFormData.value = newValue ? cloneDeep(newValue) : {};
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expose methods if needed for parent refs (optional, if used externally)
|
||||||
|
defineExpose({
|
||||||
|
callItemMethod,
|
||||||
|
updateValueFormData,
|
||||||
|
setTab,
|
||||||
|
takeValueChanged,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
874
app/components/lazy-load/global/MyContent.vue
Executable file
874
app/components/lazy-load/global/MyContent.vue
Executable file
|
|
@ -0,0 +1,874 @@
|
||||||
|
<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>
|
||||||
763
app/components/lazy-load/global/MyTable.vue
Executable file
763
app/components/lazy-load/global/MyTable.vue
Executable file
|
|
@ -0,0 +1,763 @@
|
||||||
|
<template>
|
||||||
|
<div class="">
|
||||||
|
<!-- Header: جستجو -->
|
||||||
|
<div class="mb-6 flex items-center gap-2" v-if="props.showSearch">
|
||||||
|
<UInput
|
||||||
|
v-model="searchQuery"
|
||||||
|
placeholder="...جستجو"
|
||||||
|
class="w-full max-w-xs bg-gray-100 dark:bg-dark-primary-800 border border-gray-300 dark:border-dark-primary-700 rounded px-3 py-2 text-dark-primary-800 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
v-if="searchQuery"
|
||||||
|
icon="heroicons:x-mark"
|
||||||
|
size="sm"
|
||||||
|
color="gray"
|
||||||
|
variant="ghost"
|
||||||
|
@click="searchQuery = ''"
|
||||||
|
class="px-2 py-1 rounded bg-gray-200 dark:bg-dark-primary-700 hover:bg-gray-300 dark:hover:bg-dark-primary-600 text-dark-primary-800 dark:text-gray-100 transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- جدول / کارتها -->
|
||||||
|
<div
|
||||||
|
class="bg-white dark:bg-dark-primary-800 rounded-lg overflow-hidden shadow-md border border-gray-200 dark:border-dark-primary-700"
|
||||||
|
>
|
||||||
|
<!-- حالت دسکتاپ: جدول -->
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<div
|
||||||
|
class="bg-white dark:bg-dark-primary-800 rounded-lg overflow-hidden shadow-md border border-gray-200 dark:border-dark-primary-700"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
class="w-full text-sm min-w-full"
|
||||||
|
style="table-layout: fixed; border-collapse: collapse"
|
||||||
|
>
|
||||||
|
<thead
|
||||||
|
class="bg-gray-50 dark:bg-dark-primary text-dark-primary-700 dark:text-gray-300"
|
||||||
|
style="display: table; width: 100%; table-layout: fixed"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider whitespace-nowrap"
|
||||||
|
style="width: 3rem"
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
v-for="col in tableColumns"
|
||||||
|
:key="col.key"
|
||||||
|
:style="{ width: col.width ? col.width : 'auto' }"
|
||||||
|
class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
{{ col.title }}
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider whitespace-nowrap"
|
||||||
|
style="width: 6rem"
|
||||||
|
v-if="props.actionButtons?.length"
|
||||||
|
>
|
||||||
|
عملیات
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- بدنه جدول با اسکرول عمودی -->
|
||||||
|
<div
|
||||||
|
class="overflow-y-auto"
|
||||||
|
:style="
|
||||||
|
props.tableBodyMaxHeight
|
||||||
|
? { maxHeight: props.tableBodyMaxHeight }
|
||||||
|
: {}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
class="w-full text-sm min-w-full"
|
||||||
|
style="table-layout: fixed; border-collapse: collapse"
|
||||||
|
>
|
||||||
|
<tbody style="display: block; width: 100%">
|
||||||
|
<tr
|
||||||
|
v-for="(item, index) in searchedItems"
|
||||||
|
:key="item.id"
|
||||||
|
class="border-t border-gray-200 dark:border-dark-primary-700 cursor-move"
|
||||||
|
:class="index % 2 === 0 ? 'even-row' : 'odd-row'"
|
||||||
|
style="display: table; width: 100%; table-layout: fixed"
|
||||||
|
:draggable="props.draggableRows"
|
||||||
|
@dragstart="props.draggableRows && onRowDragStart(item)"
|
||||||
|
>
|
||||||
|
<!-- شماره ردیف -->
|
||||||
|
<td class="p-0" style="width: 3rem">
|
||||||
|
<div
|
||||||
|
class="w-full h-full flex items-center justify-center px-4 text-sm font-medium"
|
||||||
|
:style="{
|
||||||
|
paddingTop: pyClassValue,
|
||||||
|
paddingBottom: pyClassValue,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ item.rowNumber }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- سلولهای داده -->
|
||||||
|
<td
|
||||||
|
v-for="col in tableColumns"
|
||||||
|
:key="col.key"
|
||||||
|
:style="{ width: col.width ? col.width : 'auto' }"
|
||||||
|
class="p-0"
|
||||||
|
>
|
||||||
|
<UContextMenu
|
||||||
|
v-if="col.contextmenu"
|
||||||
|
:items="getMenuItemsWithHandler(col, item, index)"
|
||||||
|
:ui="{ content: 'w-48' }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-full h-full flex items-center px-4 text-right break-words text-sm"
|
||||||
|
:style="{
|
||||||
|
paddingTop: pyClassValue,
|
||||||
|
paddingBottom: pyClassValue,
|
||||||
|
}"
|
||||||
|
@click="
|
||||||
|
!col.isLink &&
|
||||||
|
handleInteraction({
|
||||||
|
type: 'click',
|
||||||
|
column: col,
|
||||||
|
item,
|
||||||
|
value: getCellValue(item, col),
|
||||||
|
index,
|
||||||
|
event: $event,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@dblclick="
|
||||||
|
handleInteraction({
|
||||||
|
type: 'dblclick',
|
||||||
|
column: col,
|
||||||
|
item,
|
||||||
|
value: getCellValue(item, col),
|
||||||
|
index,
|
||||||
|
event: $event,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="col.isLink"
|
||||||
|
@click.stop="
|
||||||
|
handleInteraction({
|
||||||
|
type: 'link',
|
||||||
|
column: col,
|
||||||
|
item,
|
||||||
|
value: getCellValue(item, col),
|
||||||
|
index,
|
||||||
|
event: $event,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
class="text-blue-600 dark:text-blue-400 hover:underline cursor-pointer"
|
||||||
|
v-html="getCellValue(item, col)"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span v-else v-html="getCellValue(item, col)"></span>
|
||||||
|
</div>
|
||||||
|
</UContextMenu>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="w-full cursor-pointer h-full flex items-center px-4 text-right break-words text-sm whitespace-normal"
|
||||||
|
:style="{
|
||||||
|
paddingTop: pyClassValue,
|
||||||
|
paddingBottom: pyClassValue,
|
||||||
|
}"
|
||||||
|
@click="
|
||||||
|
!col.isLink &&
|
||||||
|
handleInteraction({
|
||||||
|
type: 'click',
|
||||||
|
column: col,
|
||||||
|
item,
|
||||||
|
value: getCellValue(item, col),
|
||||||
|
index,
|
||||||
|
event: $event,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@dblclick="
|
||||||
|
handleInteraction({
|
||||||
|
type: 'dblclick',
|
||||||
|
column: col,
|
||||||
|
item,
|
||||||
|
value: getCellValue(item, col),
|
||||||
|
index,
|
||||||
|
event: $event,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="col.isLink"
|
||||||
|
@click.stop="
|
||||||
|
handleInteraction({
|
||||||
|
type: 'link',
|
||||||
|
column: col,
|
||||||
|
item,
|
||||||
|
value: getCellValue(item, col),
|
||||||
|
index,
|
||||||
|
event: $event,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
class="text-blue-600 dark:text-blue-400 hover:underline cursor-pointer"
|
||||||
|
v-html="getCellValue(item, col)"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span v-else v-html="getCellValue(item, col)"></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- عملیات -->
|
||||||
|
<td
|
||||||
|
v-if="props.actionButtons?.length"
|
||||||
|
class="p-0 pl-2"
|
||||||
|
style="width: 6rem"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-full h-full flex items-center justify-center px-4"
|
||||||
|
:style="{
|
||||||
|
paddingTop: pyClassValue,
|
||||||
|
paddingBottom: pyClassValue,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UButton
|
||||||
|
v-if="props.actionButtons.length <= 3"
|
||||||
|
v-for="btn in props.actionButtons"
|
||||||
|
:key="btn.key"
|
||||||
|
:icon="btn.icon"
|
||||||
|
size="md"
|
||||||
|
:color="getButtonColor(btn.key)"
|
||||||
|
variant="ghost"
|
||||||
|
square
|
||||||
|
@click.stop="
|
||||||
|
handleActionFromButton(btn.key, item, index, $event)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<ClientOnly>
|
||||||
|
<UDropdownMenu
|
||||||
|
v-if="props.actionButtons.length > 3"
|
||||||
|
:items="
|
||||||
|
allActionMenuItems(
|
||||||
|
props.actionButtons,
|
||||||
|
item,
|
||||||
|
index
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
icon="heroicons:ellipsis-vertical"
|
||||||
|
size="xs"
|
||||||
|
color="gray"
|
||||||
|
variant="ghost"
|
||||||
|
square
|
||||||
|
/>
|
||||||
|
</UDropdownMenu>
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- بدون داده -->
|
||||||
|
<tr v-if="searchedItems.length === 0">
|
||||||
|
<td
|
||||||
|
:colspan="tableColumns.length + 2"
|
||||||
|
class="py-8 text-center text-gray-400 dark:text-gray-500 text-sm"
|
||||||
|
>
|
||||||
|
❌ دادهای یافت نشد
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- حالت موبایل: کارتها -->
|
||||||
|
<div class="md:hidden">
|
||||||
|
<div
|
||||||
|
class="overflow-y-auto px-4 py-2"
|
||||||
|
:style="
|
||||||
|
props.tableBodyMaxHeight
|
||||||
|
? { maxHeight: props.tableBodyMaxHeight }
|
||||||
|
: {}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in searchedItems"
|
||||||
|
:key="item.id"
|
||||||
|
class="border border-gray-200 dark:border-dark-primary-700 rounded-lg p-4 bg-white dark:bg-dark-primary-800 shadow-sm"
|
||||||
|
:class="index % 2 === 0 ? 'even-row' : 'odd-row'"
|
||||||
|
:draggable="props.draggableRows"
|
||||||
|
@dragstart="props.draggableRows && onRowDragStart(item)"
|
||||||
|
>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<!-- شماره ردیف -->
|
||||||
|
<div class="flex justify-between items-start gap-2">
|
||||||
|
<span
|
||||||
|
class="text-xs font-medium text-gray-500 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
ردیف
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="text-right text-sm font-medium text-dark-primary dark:text-gray-100"
|
||||||
|
>
|
||||||
|
{{ item.rowNumber }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- سایر ستونها -->
|
||||||
|
<div
|
||||||
|
v-for="col in tableColumns"
|
||||||
|
:key="col.key"
|
||||||
|
class="flex justify-between items-start gap-2"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-xs font-medium text-gray-500 dark:text-gray-400 min-w-0"
|
||||||
|
>
|
||||||
|
{{ col.title }}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="text-right flex-1 text-sm text-dark-primary dark:text-gray-100"
|
||||||
|
>
|
||||||
|
<UContextMenu
|
||||||
|
v-if="col.contextmenu"
|
||||||
|
:items="getMenuItemsWithHandler(col, item, index)"
|
||||||
|
:ui="{ content: 'w-48' }"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="col.isLink"
|
||||||
|
@click="
|
||||||
|
handleInteraction({
|
||||||
|
type: 'link',
|
||||||
|
column: col,
|
||||||
|
item,
|
||||||
|
value: getCellValue(item, col),
|
||||||
|
index,
|
||||||
|
event: $event,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
class="text-blue-600 dark:text-blue-400 hover:underline cursor-pointer"
|
||||||
|
v-html="getCellValue(item, col)"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
@click="
|
||||||
|
!col.isLink &&
|
||||||
|
handleInteraction({
|
||||||
|
type: 'click',
|
||||||
|
column: col,
|
||||||
|
item,
|
||||||
|
value: getCellValue(item, col),
|
||||||
|
index,
|
||||||
|
event: $event,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@dblclick="
|
||||||
|
handleInteraction({
|
||||||
|
type: 'dblclick',
|
||||||
|
column: col,
|
||||||
|
item,
|
||||||
|
value: getCellValue(item, col),
|
||||||
|
index,
|
||||||
|
event: $event,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
class="cursor-pointer"
|
||||||
|
v-html="getCellValue(item, col)"
|
||||||
|
></span>
|
||||||
|
</UContextMenu>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<span
|
||||||
|
v-if="col.isLink"
|
||||||
|
@click="
|
||||||
|
handleInteraction({
|
||||||
|
type: 'link',
|
||||||
|
column: col,
|
||||||
|
item,
|
||||||
|
value: getCellValue(item, col),
|
||||||
|
index,
|
||||||
|
event: $event,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
class="text-blue-600 dark:text-blue-400 hover:underline cursor-pointer"
|
||||||
|
v-html="getCellValue(item, col)"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
@click="
|
||||||
|
!col.isLink &&
|
||||||
|
handleInteraction({
|
||||||
|
type: 'click',
|
||||||
|
column: col,
|
||||||
|
item,
|
||||||
|
value: getCellValue(item, col),
|
||||||
|
index,
|
||||||
|
event: $event,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
@dblclick="
|
||||||
|
handleInteraction({
|
||||||
|
type: 'dblclick',
|
||||||
|
column: col,
|
||||||
|
item,
|
||||||
|
value: getCellValue(item, col),
|
||||||
|
index,
|
||||||
|
event: $event,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
class="cursor-pointer"
|
||||||
|
v-html="getCellValue(item, col)"
|
||||||
|
></span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- دکمههای عملیات -->
|
||||||
|
<div
|
||||||
|
class="flex justify-end gap-2 mt-4"
|
||||||
|
v-if="props.actionButtons?.length"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
v-if="props.actionButtons.length <= 3"
|
||||||
|
v-for="btn in props.actionButtons"
|
||||||
|
:key="btn.key"
|
||||||
|
:icon="btn.icon"
|
||||||
|
size="xs"
|
||||||
|
:color="getButtonColor(btn.key)"
|
||||||
|
variant="ghost"
|
||||||
|
square
|
||||||
|
@click.stop="
|
||||||
|
handleActionFromButton(btn.key, item, index, $event)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<ClientOnly>
|
||||||
|
<UDropdownMenu
|
||||||
|
v-if="props.actionButtons.length > 3"
|
||||||
|
:items="
|
||||||
|
allActionMenuItems(props.actionButtons, item, index)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
icon="heroicons:ellipsis-vertical"
|
||||||
|
size="xs"
|
||||||
|
color="gray"
|
||||||
|
variant="ghost"
|
||||||
|
square
|
||||||
|
/>
|
||||||
|
</UDropdownMenu>
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="searchedItems.length === 0"
|
||||||
|
class="text-center py-8 text-gray-400 dark:text-gray-500 text-sm"
|
||||||
|
>
|
||||||
|
❌ دادهای یافت نشد
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
tableColumns: { type: Array, required: true },
|
||||||
|
actionButtons: { type: Array, default: () => [] },
|
||||||
|
rawData: { type: Array, required: true },
|
||||||
|
cellMenuItems: { type: Array, default: () => [] },
|
||||||
|
showSearch: { type: Boolean, default: true },
|
||||||
|
tableBodyMaxHeight: { type: String, default: "80dvh" },
|
||||||
|
pagination: { type: Object, required: true },
|
||||||
|
draggableRows: { type: Boolean, default: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["my-table-action", "drag-start"]);
|
||||||
|
|
||||||
|
const searchQuery = ref("");
|
||||||
|
const clickTimer = ref(null);
|
||||||
|
const pyClassValue = "0.75rem"; // میتوانید از props هم بگیرید
|
||||||
|
|
||||||
|
// شماره ردیف پایدار
|
||||||
|
const rowNumberMap = ref(new Map());
|
||||||
|
const nextRowNumber = ref(1);
|
||||||
|
|
||||||
|
const getItemKey = (item, idx) => {
|
||||||
|
if (!item) return `__idx_${idx}`;
|
||||||
|
if (item.id) return String(item.id);
|
||||||
|
if (item._id) return String(item._id);
|
||||||
|
if (item._source && (item._source.id || item._source._id))
|
||||||
|
return String(item._source.id || item._source._id);
|
||||||
|
try {
|
||||||
|
return JSON.stringify(item);
|
||||||
|
} catch {
|
||||||
|
return `__idx_${idx}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.rawData,
|
||||||
|
(newVal) => {
|
||||||
|
if (!Array.isArray(newVal)) return;
|
||||||
|
newVal.forEach((rawItem, idx) => {
|
||||||
|
const key = getItemKey(rawItem, idx);
|
||||||
|
if (!rowNumberMap.value.has(key)) {
|
||||||
|
rowNumberMap.value.set(key, nextRowNumber.value++);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const processedData = computed(() => {
|
||||||
|
if (!Array.isArray(props.rawData)) return [];
|
||||||
|
|
||||||
|
const offset = props.pagination?.offset ?? 0;
|
||||||
|
|
||||||
|
return props.rawData.map((item, idx) => {
|
||||||
|
const key = getItemKey(item, idx);
|
||||||
|
const computedId = item._id || item.id || key;
|
||||||
|
|
||||||
|
const rowNumber = offset + idx + 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
id: computedId,
|
||||||
|
rowNumber,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchedItems = computed(() => {
|
||||||
|
if (!searchQuery.value) return processedData.value;
|
||||||
|
|
||||||
|
const q = searchQuery.value.toLowerCase();
|
||||||
|
return processedData.value.filter((item) =>
|
||||||
|
Object.values(item).some((val) =>
|
||||||
|
String(val ?? "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(q)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getButtonColor = (key) => {
|
||||||
|
return { edit: "primary", delete: "red", clone: "green" }[key] || "gray";
|
||||||
|
};
|
||||||
|
|
||||||
|
// ====================== getCellValue اصلی ======================
|
||||||
|
|
||||||
|
const getObjectText = (schema, data) => {
|
||||||
|
let text = "";
|
||||||
|
if (schema.is_object == 1 && schema.object_text) {
|
||||||
|
Object.keys(schema.object_text).forEach((k) => {
|
||||||
|
if (data[k] !== undefined) {
|
||||||
|
if (schema.object_text[k]) text += " - " + schema.object_text[k] + ":";
|
||||||
|
text += data[k];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
text = String(data);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateToPersian = (item) => {
|
||||||
|
if (!item) return "";
|
||||||
|
|
||||||
|
let date;
|
||||||
|
const num = Number(item);
|
||||||
|
|
||||||
|
if (!isNaN(num) && item.toString().length === 10) {
|
||||||
|
date = new Date(item * 1000);
|
||||||
|
} else if (typeof item === "string" && !item.includes("T00:00:00")) {
|
||||||
|
date = new Date(item + "T00:00:00");
|
||||||
|
} else {
|
||||||
|
date = new Date(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(date.getTime())) return item;
|
||||||
|
|
||||||
|
return date.toLocaleDateString("fa-IR");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCellValue = (rowItem, column) => {
|
||||||
|
let key = column.key;
|
||||||
|
let trancate_word = column?.trancate_word;
|
||||||
|
let value = "";
|
||||||
|
|
||||||
|
if (rowItem.highlight && key in rowItem.highlight) {
|
||||||
|
if (Array.isArray(rowItem.highlight[key])) {
|
||||||
|
value = rowItem.highlight[key][0];
|
||||||
|
} else {
|
||||||
|
value = rowItem.highlight[key];
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rowItem_data = rowItem._source ?? rowItem;
|
||||||
|
if (!rowItem_data) return "";
|
||||||
|
|
||||||
|
key = key.replace("__", ".");
|
||||||
|
if (key.includes(".")) {
|
||||||
|
const keys = key.split(".");
|
||||||
|
for (let i = 0; i < keys.length - 1; i++) {
|
||||||
|
if (rowItem_data[keys[i]] !== undefined) {
|
||||||
|
rowItem_data = rowItem_data[keys[i]];
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key = keys[keys.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (column?.object_text && rowItem_data[column.key]) {
|
||||||
|
const data = rowItem_data[column.key];
|
||||||
|
if (column.is_array == 1 && Array.isArray(data)) {
|
||||||
|
data.forEach((el) => {
|
||||||
|
const text = getObjectText(column, el);
|
||||||
|
if (text) value += text + "-";
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
value = getObjectText(column, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === "" && rowItem_data[key] !== undefined) {
|
||||||
|
value = rowItem_data[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && trancate_word && trancate_word > 0) {
|
||||||
|
const words = String(value).split(" ");
|
||||||
|
if (words.length > trancate_word) {
|
||||||
|
value = words.slice(0, trancate_word).join(" ") + "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value !== "" && column?.process) {
|
||||||
|
if (column.process === "len") {
|
||||||
|
return String(value).length;
|
||||||
|
} else if (
|
||||||
|
column.process === "convert_date" ||
|
||||||
|
column.process === "convert_gdate"
|
||||||
|
) {
|
||||||
|
return formatDateToPersian(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
value !== "" &&
|
||||||
|
(key.toLowerCase().includes("date") || key.toLowerCase().includes("time"))
|
||||||
|
) {
|
||||||
|
return formatDateToPersian(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (column?.colors && value in column.colors) {
|
||||||
|
value = `<span style="color:${column.colors[value]}">${value}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value ?? "";
|
||||||
|
};
|
||||||
|
|
||||||
|
// ====================== Action & Interaction Handlers ======================
|
||||||
|
const emitAction = (actionKey, item, index) => {
|
||||||
|
const payload = {
|
||||||
|
column: null,
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
menuItem: props.actionButtons.find((b) => b.key === actionKey),
|
||||||
|
};
|
||||||
|
emit("my-table-action", { action: actionKey, payload: payload });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleActionFromButton = (actionKey, item, index, event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
emitAction(actionKey, item, index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const allActionMenuItems = (buttons, item, index) => {
|
||||||
|
if (buttons.length <= 3) return [];
|
||||||
|
return buttons.map((btn) => ({
|
||||||
|
label: btn.title || btn.key,
|
||||||
|
icon: btn.icon,
|
||||||
|
onSelect: () => emitAction(btn.key, item, index),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMenuItemsWithHandler = (column, item, index) => {
|
||||||
|
return props.cellMenuItems.map((section) =>
|
||||||
|
section.map((menuItem) => ({
|
||||||
|
...menuItem,
|
||||||
|
onSelect: () => {
|
||||||
|
emit("my-table-action", {
|
||||||
|
type: "contextmenu",
|
||||||
|
column,
|
||||||
|
item,
|
||||||
|
value: getCellValue(item, column),
|
||||||
|
index,
|
||||||
|
menuItem,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInteraction = ({ type, column, item, value, index, event }) => {
|
||||||
|
if (type === "click" && event?.detail > 1) return;
|
||||||
|
if (type === "link") {
|
||||||
|
emit("my-table-action", { type, column, item, value, index, event });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type === "dblclick") {
|
||||||
|
if (clickTimer.value) clearTimeout(clickTimer.value);
|
||||||
|
emit("my-table-action", { type, column, item, value, index, event });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type === "click") {
|
||||||
|
if (clickTimer.value) clearTimeout(clickTimer.value);
|
||||||
|
clickTimer.value = setTimeout(() => {
|
||||||
|
emit("my-table-action", { type, column, item, value, index, event });
|
||||||
|
clickTimer.value = null;
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onRowDragStart = (item) => {
|
||||||
|
if (!props.draggableRows) return;
|
||||||
|
emit("drag-start", item);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
table {
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.even-row {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
.odd-row {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
.dark .even-row {
|
||||||
|
background-color: #1f2937;
|
||||||
|
}
|
||||||
|
.dark .odd-row {
|
||||||
|
background-color: #111827;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
.text__orange {
|
||||||
|
color: var(--color-text__orange);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
542
app/components/lazy-load/global/lineContent.vue
Executable file
542
app/components/lazy-load/global/lineContent.vue
Executable file
|
|
@ -0,0 +1,542 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col sm:flex-row items-center mb-3 line-content">
|
||||||
|
<template v-for="(itemSchema, j) in props.lineSchema?.items || []" :key="j">
|
||||||
|
<div
|
||||||
|
v-if="isExistDataItems(props.itemData, itemSchema)"
|
||||||
|
class="flex items-center ms-2"
|
||||||
|
:class="itemSchema.style"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="itemSchema.label"
|
||||||
|
class="whitespace-nowrap ms-1 text-black"
|
||||||
|
:style="{ fontFamily: 'sahel-semi-bold' }"
|
||||||
|
for=""
|
||||||
|
>
|
||||||
|
{{ itemSchema.label }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<template v-if="item_schema_link_route">
|
||||||
|
<!-- ⚠️ توجه: itemSchema.link_route نیاز به دسترسی دارد -->
|
||||||
|
<a
|
||||||
|
@click.prevent="openNewPage(props.itemData, itemSchema)"
|
||||||
|
@mousedown.middle.prevent="openNewPage(props.itemData, itemSchema)"
|
||||||
|
class="text-[15px] text-content"
|
||||||
|
v-html="getDataItems(props.itemData, itemSchema)"
|
||||||
|
></a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="itemSchema.isArray">
|
||||||
|
<div class="text-wrap">
|
||||||
|
<span
|
||||||
|
v-for="(item, index) in getDataItems(props.itemData, itemSchema)"
|
||||||
|
:key="index"
|
||||||
|
class="me-2"
|
||||||
|
:class="itemSchema.style"
|
||||||
|
>
|
||||||
|
<span v-if="itemSchema.isArray === 2">{{ item }} ,</span>
|
||||||
|
<span v-else>{{ item.title }} ,</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="itemSchema.style === 'search-voice'">
|
||||||
|
<div class="w-11/12">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in getHtmlSound(props.itemData, itemSchema)"
|
||||||
|
:key="index"
|
||||||
|
class="text-[14px] text-content"
|
||||||
|
>
|
||||||
|
<div class="flex items-center mb-1">
|
||||||
|
<span
|
||||||
|
@click="playVoice(props.itemData, index, $event)"
|
||||||
|
class="search-tag me-2"
|
||||||
|
>
|
||||||
|
پخش صوت
|
||||||
|
</span>
|
||||||
|
<span v-html="item"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<audio
|
||||||
|
:key="
|
||||||
|
audioSource ?? getApiLink(props.itemData._source.sound_link)
|
||||||
|
"
|
||||||
|
ref="audioPlayer"
|
||||||
|
controls
|
||||||
|
crossorigin
|
||||||
|
playsinline
|
||||||
|
>
|
||||||
|
<source :src="audioSource" type="audio/mp3" />
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="text-[14px] text-content"
|
||||||
|
v-html="getDataItems(props.itemData, itemSchema)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useNuxtApp, useRouter } from "#app";
|
||||||
|
import { persianDateAndTime } from "@/manuals/utilities.js";
|
||||||
|
|
||||||
|
// Define props
|
||||||
|
const props = defineProps({
|
||||||
|
lineSchema: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
arrayItemData: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
itemData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
textSearch: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define emits
|
||||||
|
const emit = defineEmits(["openModalHandler", "click-item"]);
|
||||||
|
|
||||||
|
// Use composables and stores
|
||||||
|
const { $http: httpService } = useNuxtApp();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Template ref for audio element
|
||||||
|
const audioPlayer = ref(null);
|
||||||
|
|
||||||
|
// Reactive state (replaces `data()`)
|
||||||
|
const test = ref([
|
||||||
|
{ id: 3262, title: "جدول تجزیه اصطلاحات موضوع" },
|
||||||
|
{ id: 3260, title: "جدول تطبیق جدول جامعه" },
|
||||||
|
{ id: 3263, title: "جدول تجزیه اصطلاحات مقیاس" },
|
||||||
|
]);
|
||||||
|
const vuePlyrOptions = ref({
|
||||||
|
controls: ["play", "progress", "current-time", "mute", "volume", "settings"],
|
||||||
|
speed: { selected: 1, options: [0.75, 1, 1.5, 2] },
|
||||||
|
iconUrl: "/img/plyr.svg",
|
||||||
|
});
|
||||||
|
const audioSource = ref("");
|
||||||
|
|
||||||
|
// Methods (replaces `methods` object)
|
||||||
|
const openModalHandler = (item, schema) => {
|
||||||
|
emit("openModalHandler", { item, schema });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHtmlSound = (itemData, itemSchema) => {
|
||||||
|
let value = highlightKey2(itemData, itemSchema.source_key, "\n");
|
||||||
|
let items = value.split("\n");
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
const playVoice = (itemData, index, event) => {
|
||||||
|
let voice_times = itemData.voice_times;
|
||||||
|
if (!voice_times || !voice_times[index]) return;
|
||||||
|
|
||||||
|
audioSource.value = getApiLink(itemData._source.sound_link);
|
||||||
|
const selectedVoice = voice_times[index];
|
||||||
|
const start = selectedVoice.start || 0;
|
||||||
|
|
||||||
|
// استفاده از template ref به جای جستجو در DOM
|
||||||
|
const player = audioPlayer.value;
|
||||||
|
|
||||||
|
if (!player) {
|
||||||
|
console.warn("Audio element not found!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initPlay = () => {
|
||||||
|
player.muted = true;
|
||||||
|
return player
|
||||||
|
.play()
|
||||||
|
.then(() => {
|
||||||
|
player.pause();
|
||||||
|
player.muted = false;
|
||||||
|
})
|
||||||
|
.catch((err) => console.warn("Init play failed:", err));
|
||||||
|
};
|
||||||
|
|
||||||
|
const playFromStart = () => {
|
||||||
|
player.currentTime = start;
|
||||||
|
return player.play().catch((err) => {
|
||||||
|
console.warn("Playback failed:", err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (player.readyState === 0) {
|
||||||
|
player.addEventListener(
|
||||||
|
"loadedmetadata",
|
||||||
|
() => {
|
||||||
|
initPlay().then(() => playFromStart());
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
initPlay().then(() => playFromStart());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getApiLink = (link) => {
|
||||||
|
if (!link || typeof link !== "string") return "";
|
||||||
|
// نکته: در Nuxt 3 متغیرهای محیطی معمولا با NUXT_PUBLIC_ شروع میشوند
|
||||||
|
let url = process.env.VUE_APP_BASE_URL + process.env.VUE_APP_API_NAME;
|
||||||
|
|
||||||
|
if (link.startsWith("/Quick/")) url += "media2" + link;
|
||||||
|
else if (link.startsWith("meidaex/")) url += link;
|
||||||
|
else url += link;
|
||||||
|
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isExistDataItems = (item, schema) => {
|
||||||
|
if (schema.style == "search-voice") return true;
|
||||||
|
let source_key = schema.source_key;
|
||||||
|
let hilight_key = schema.hilight_key;
|
||||||
|
if (hilight_key && item?.highlight && item?.highlight[hilight_key]?.length)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
const keys = source_key?.split(",");
|
||||||
|
if (keys.length >= 2) {
|
||||||
|
for (let key of keys) {
|
||||||
|
if (item?._source[key]?.trim()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
let value;
|
||||||
|
if (keys[0] == "voice_times") {
|
||||||
|
value = getSourceData(item?._source, "sound_link");
|
||||||
|
} else value = getSourceData(item?._source, keys[0]);
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
if (Array.isArray(value) && value.length == 0) return false;
|
||||||
|
else return true;
|
||||||
|
} else return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDataItems = (item, schema) => {
|
||||||
|
let source_key = schema.source_key;
|
||||||
|
let trancate_word = schema?.trancate_word;
|
||||||
|
let hilight_delimiter = schema?.hilight_delimiter ?? "... ";
|
||||||
|
const keys = source_key?.split(",");
|
||||||
|
let value = "";
|
||||||
|
|
||||||
|
let key = keys[0];
|
||||||
|
if (keys.length > 1) {
|
||||||
|
for (let key1 of keys) {
|
||||||
|
if (item?._source[key1]?.trim()) {
|
||||||
|
key = key1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value = highlightKey2(item, key, hilight_delimiter);
|
||||||
|
if (value != "") return value;
|
||||||
|
|
||||||
|
if (schema.key == "tags") {
|
||||||
|
value = textTags(item, schema.key);
|
||||||
|
} else if (schema.key == "convert_time") {
|
||||||
|
value = getSourceData(item?._source, key);
|
||||||
|
if (typeof value === "string" && value.includes(":")) {
|
||||||
|
let parts = value.split(":");
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
return `${parts[0]}:${parts[1]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
value = getSourceData(item?._source, key);
|
||||||
|
|
||||||
|
if (trancate_word && trancate_word > 0) {
|
||||||
|
let words = value.split(" ");
|
||||||
|
if (words.length > trancate_word) {
|
||||||
|
value = words.slice(0, trancate_word).join(" ");
|
||||||
|
value += "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
value = value.replaceAll("\n", " ");
|
||||||
|
value = value.replaceAll("\t", "");
|
||||||
|
}
|
||||||
|
if (schema?.process) {
|
||||||
|
if (schema.process == "convert_date") {
|
||||||
|
return formatDateToPersian(value);
|
||||||
|
} else if (schema.process == "convert_gdate") {
|
||||||
|
return convertTo3DateLang(value);
|
||||||
|
}
|
||||||
|
} else if (schema.key == "date_create") {
|
||||||
|
return formatDateToPersian(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == "") {
|
||||||
|
value = "--";
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertTo3DateLang = (date) => {
|
||||||
|
const resFa = persianDateAndTime(date, "fa-IR");
|
||||||
|
const resAr = persianDateAndTime(date, "fa-u-ca-islamic-umalqura");
|
||||||
|
const parsedDate =
|
||||||
|
`${resFa.weekday.short} ${resFa.year.numeric}/${resFa.month.numeric}/${resFa.day.numeric}` +
|
||||||
|
` - ${resAr.day.numeric} ${resAr.month.short} ${resAr.year.numeric}`;
|
||||||
|
return parsedDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateToPersian = (item) => {
|
||||||
|
let date;
|
||||||
|
let num = Number(item);
|
||||||
|
if (!isNaN(num)) {
|
||||||
|
if (item.toString().length == 10) {
|
||||||
|
date = new Date(item * 1000);
|
||||||
|
}
|
||||||
|
} else if (!item.includes("T00:00:00")) item = item + "T00:00:00";
|
||||||
|
|
||||||
|
date = new Date(item);
|
||||||
|
let date_f = date.toLocaleDateString("fa-IR");
|
||||||
|
return date_f;
|
||||||
|
};
|
||||||
|
|
||||||
|
const textTags = (item, key) => {
|
||||||
|
let text = "";
|
||||||
|
if (Array.isArray(item._source[key])) text = item._source[key].join("، ");
|
||||||
|
else text = item._source[key];
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
const highlightKey2 = (
|
||||||
|
item,
|
||||||
|
key,
|
||||||
|
hilight_delimiter = "...",
|
||||||
|
posfix = ["fa", "ph"],
|
||||||
|
) => {
|
||||||
|
var text = "";
|
||||||
|
if (item.highlight) {
|
||||||
|
let key_highlight = key;
|
||||||
|
let i = 0;
|
||||||
|
while (i < posfix.length) {
|
||||||
|
if (key_highlight in item.highlight) break;
|
||||||
|
key_highlight = key + "." + posfix[i];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key_highlight in item.highlight) {
|
||||||
|
let value1 = "";
|
||||||
|
value1 = item.highlight[key_highlight];
|
||||||
|
|
||||||
|
if (Array.isArray(value1)) text = value1.join(hilight_delimiter);
|
||||||
|
else text = value1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
const highlightKey = (item, key1, key2 = "", key3 = "") => {
|
||||||
|
var text = "";
|
||||||
|
if (item.highlight) {
|
||||||
|
if (item.highlight[key1]) text = item.highlight[key1].join("... ");
|
||||||
|
else if (key2 && item.highlight[key2])
|
||||||
|
text = item.highlight[key2].join("... ");
|
||||||
|
else if (key3 && item.highlight[key3])
|
||||||
|
text = item.highlight[key3].join("... ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text == "") {
|
||||||
|
if (item._source[key1]) text = item._source[key1];
|
||||||
|
else if (key2 && item._source[key2]) text = item._source[key2];
|
||||||
|
else if (key3 && item._source[key3]) text = item._source[key3];
|
||||||
|
|
||||||
|
if (text.length > 500) text = text.substring(0, 500);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openNewPage = (item, schema) => {
|
||||||
|
let cloneItem = { ...item };
|
||||||
|
if (schema.link_route.length == 0) return;
|
||||||
|
|
||||||
|
if (schema.link_route.url_key) {
|
||||||
|
window.open(item._source.url, "_blank");
|
||||||
|
} else {
|
||||||
|
let cloneList = [];
|
||||||
|
props.arrayItemData.forEach((item) => {
|
||||||
|
cloneList.push({
|
||||||
|
_id: item._id,
|
||||||
|
_source: {
|
||||||
|
id: item._id,
|
||||||
|
title: item?._source?.title ?? item?._source?.book_title,
|
||||||
|
qanon_title: item?._source?.qanon_title,
|
||||||
|
qanon_id: item?._source?.qanon_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.setItem("myList", JSON.stringify(cloneList));
|
||||||
|
localStorage.setItem("myItem", JSON.stringify(cloneItem));
|
||||||
|
let key = "";
|
||||||
|
let query_key = "";
|
||||||
|
let query_value = "";
|
||||||
|
let keys = "";
|
||||||
|
let id_route = "";
|
||||||
|
let id_route2 = "";
|
||||||
|
let keyName = schema?.link_route.id;
|
||||||
|
let query = {};
|
||||||
|
if (process.env.VUE_APP_BUILD_NAME != "majles") {
|
||||||
|
query = { searchtext: props.textSearch ?? undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema.link_route.id2) {
|
||||||
|
id_route2 = getSourceData(item, schema.link_route.id2);
|
||||||
|
}
|
||||||
|
id_route = getSourceData(item, schema.link_route.id);
|
||||||
|
|
||||||
|
if (schema.link_route?.query) {
|
||||||
|
let querys = schema.link_route.query;
|
||||||
|
if (typeof querys === "string") {
|
||||||
|
keys = schema.link_route?.query.split("=");
|
||||||
|
if (keys.length >= 2) {
|
||||||
|
query_key = keys[0];
|
||||||
|
query_value = keys[1];
|
||||||
|
}
|
||||||
|
if (query_key && query_value) {
|
||||||
|
query[query_key] = query_value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let newObject = {};
|
||||||
|
for (const [key, value] of Object.entries(querys)) {
|
||||||
|
if (value in item) {
|
||||||
|
newObject[key] = item[value];
|
||||||
|
} else {
|
||||||
|
newObject[key] = item?._source[value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object.assign(query, newObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema.link_route?.key_item || schema.link_route.key) {
|
||||||
|
if (schema.link_route?.key_item)
|
||||||
|
key = getSourceData(item, schema.link_route.key_item);
|
||||||
|
else key = schema.link_route.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeData = router.resolve({
|
||||||
|
name: schema.link_route.name,
|
||||||
|
params: {
|
||||||
|
id: id_route,
|
||||||
|
...(id_route2 && { id2: id_route2 }),
|
||||||
|
key: key,
|
||||||
|
},
|
||||||
|
query: query,
|
||||||
|
});
|
||||||
|
|
||||||
|
emit("click-item", { id: id_route });
|
||||||
|
|
||||||
|
window.open(routeData.href, "_blank");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getArrayData = (itemData, collapseItem) => {
|
||||||
|
if (!collapseItem.array_key) return [itemData];
|
||||||
|
let sourceData = getSourceData(itemData, collapseItem.array_key);
|
||||||
|
if (!Array.isArray(sourceData)) return [itemData];
|
||||||
|
return sourceData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSourceData = (itemData, key) => {
|
||||||
|
let sourceData = itemData;
|
||||||
|
key?.split(".").forEach((k) => {
|
||||||
|
let keyName = k;
|
||||||
|
if (keyName in sourceData) {
|
||||||
|
sourceData = sourceData[keyName];
|
||||||
|
} else if (sourceData?._source && keyName in sourceData?._source) {
|
||||||
|
sourceData = sourceData?._source[keyName];
|
||||||
|
} else {
|
||||||
|
sourceData = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return sourceData;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.search-tag {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #96a5b5;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0 5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e4dfd8;
|
||||||
|
height: 20px;
|
||||||
|
margin-left: 8px;
|
||||||
|
&:hover {
|
||||||
|
color: black;
|
||||||
|
border-color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.search-tag {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #96a5b5;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0 5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e4dfd8;
|
||||||
|
height: 20px;
|
||||||
|
margin-left: 8px;
|
||||||
|
&:hover {
|
||||||
|
color: black;
|
||||||
|
border-color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.search-title {
|
||||||
|
cursor: pointer;
|
||||||
|
color: rgb(59, 130, 246) !important;
|
||||||
|
&:hover {
|
||||||
|
color: rgb(59, 130, 246) !important;
|
||||||
|
text-decoration: underline !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.search-label {
|
||||||
|
a {
|
||||||
|
color: #00b6e3;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
color: #00b6e3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.search-body {
|
||||||
|
display: -webkit-box !important;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 25px;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.text-content {
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
59
app/composables/useApiCache.js
Executable file
59
app/composables/useApiCache.js
Executable file
|
|
@ -0,0 +1,59 @@
|
||||||
|
// composables/useApiCache.js
|
||||||
|
export const useApiCache = () => {
|
||||||
|
// دادههای ذخیره شده (key -> response data)
|
||||||
|
const cacheData = useState("cacheData", () => ({}));
|
||||||
|
|
||||||
|
// درخواستهای در حال اجرا (key -> Promise)
|
||||||
|
const pendingRequests = useState("pendingRequests", () => ({}));
|
||||||
|
|
||||||
|
// زمان انقضا برای هر key (ms)
|
||||||
|
const ttlMap = useState("ttlMap", () => ({}));
|
||||||
|
|
||||||
|
// ---- متدهای ساده و قابل فهم ----
|
||||||
|
const hasData = (key) => key in cacheData.value;
|
||||||
|
const getData = (key) => cacheData.value[key];
|
||||||
|
const saveData = (key, data) => (cacheData.value[key] = data);
|
||||||
|
const removeData = (key) => {
|
||||||
|
delete cacheData.value[key];
|
||||||
|
delete ttlMap.value[key];
|
||||||
|
};
|
||||||
|
const clearAllData = () => {
|
||||||
|
Object.keys(cacheData.value).forEach((k) => delete cacheData.value[k]);
|
||||||
|
Object.keys(pendingRequests.value).forEach(
|
||||||
|
(k) => delete pendingRequests.value[k],
|
||||||
|
);
|
||||||
|
Object.keys(ttlMap.value).forEach((k) => delete ttlMap.value[k]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPending = (key) => pendingRequests.value[key];
|
||||||
|
const setPending = (key, promise) => (pendingRequests.value[key] = promise);
|
||||||
|
const clearPending = (key) => delete pendingRequests.value[key];
|
||||||
|
|
||||||
|
const saveDataWithTTL = (key, data, ttlSeconds = 0) => {
|
||||||
|
saveData(key, data);
|
||||||
|
if (ttlSeconds > 0) {
|
||||||
|
ttlMap.value[key] = Date.now() + ttlSeconds * 1000;
|
||||||
|
} else {
|
||||||
|
delete ttlMap.value[key];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isExpired = (key) => {
|
||||||
|
const expireTime = ttlMap.value[key];
|
||||||
|
if (!expireTime) return false;
|
||||||
|
return Date.now() > expireTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasData,
|
||||||
|
getData,
|
||||||
|
saveData,
|
||||||
|
removeData,
|
||||||
|
clearAllData,
|
||||||
|
getPending,
|
||||||
|
setPending,
|
||||||
|
clearPending,
|
||||||
|
saveDataWithTTL,
|
||||||
|
isExpired,
|
||||||
|
};
|
||||||
|
};
|
||||||
87
app/composables/useCachedRequest.js
Executable file
87
app/composables/useCachedRequest.js
Executable file
|
|
@ -0,0 +1,87 @@
|
||||||
|
// composables/useCachedRequest.js
|
||||||
|
import { useApiCache } from "~/composables/useApiCache";
|
||||||
|
|
||||||
|
const tryParseJSON = (str) => {
|
||||||
|
if (typeof str !== "string") return str;
|
||||||
|
if (!str.trim()) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(str);
|
||||||
|
} catch (e) {
|
||||||
|
return str; // اگر parse نشد، رشته را همونطور برگردون
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCachedRequest = () => {
|
||||||
|
const cache = useApiCache();
|
||||||
|
const { $http: httpService } = useNuxtApp();
|
||||||
|
|
||||||
|
// ساختن کلید ساده و یکتا برای هر request
|
||||||
|
const getRequestKey = (config = {}) => {
|
||||||
|
const method = (config.crud || "GET").toUpperCase();
|
||||||
|
const payloadNorm = tryParseJSON(config.payload) ?? null;
|
||||||
|
return `${method}:${config.url}:${JSON.stringify(payloadNorm)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// درخواست با cache و dedupe
|
||||||
|
const fetchRequest = async (config = {}) => {
|
||||||
|
if (!config?.url) throw new Error("apiConfig.url is required");
|
||||||
|
|
||||||
|
const key = getRequestKey(config);
|
||||||
|
|
||||||
|
// پاک کردن cache منقضی شده
|
||||||
|
if (cache.hasData(key) && cache.isExpired(key)) cache.removeData(key);
|
||||||
|
|
||||||
|
// 1. اگر قبلاً داده موجود است → برگردان
|
||||||
|
if (cache.hasData(key)) return cache.getData(key);
|
||||||
|
|
||||||
|
// 2. اگر درخواست در حال اجراست → promise را await کن (dedupe)
|
||||||
|
const pending = cache.getPending(key);
|
||||||
|
if (pending) return await pending;
|
||||||
|
|
||||||
|
// 3. ایجاد request جدید
|
||||||
|
const promise = (async () => {
|
||||||
|
let response;
|
||||||
|
const method = (config.crud || "GET").toUpperCase();
|
||||||
|
|
||||||
|
if (method === "POST") {
|
||||||
|
const payload = tryParseJSON(config.payload) ?? {};
|
||||||
|
response = await httpService.postRequest(config.url, payload);
|
||||||
|
} else {
|
||||||
|
response = await httpService.getRequest(config.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = response?.data ?? response;
|
||||||
|
|
||||||
|
// ذخیره با TTL در صورت وجود
|
||||||
|
const ttl = config.ttlSeconds || 0;
|
||||||
|
if (ttl > 0) {
|
||||||
|
cache.saveDataWithTTL(key, data, ttl);
|
||||||
|
} else {
|
||||||
|
cache.saveData(key, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
})();
|
||||||
|
|
||||||
|
cache.setPending(key, promise);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await promise;
|
||||||
|
} finally {
|
||||||
|
cache.clearPending(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// پاکسازی دستی cache برای logout یا تغییر دادهها
|
||||||
|
const invalidate = (configOrKey) => {
|
||||||
|
const key =
|
||||||
|
typeof configOrKey === "string"
|
||||||
|
? configOrKey
|
||||||
|
: getRequestKey(configOrKey || {});
|
||||||
|
cache.removeData(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearAll = () => cache.clearAllData();
|
||||||
|
|
||||||
|
return { fetchRequest, invalidate, clearAll };
|
||||||
|
};
|
||||||
78
app/composables/useElpService.js
Executable file
78
app/composables/useElpService.js
Executable file
|
|
@ -0,0 +1,78 @@
|
||||||
|
// @/composables/useFormBuilder.js
|
||||||
|
import { useNuxtApp } from "#app";
|
||||||
|
|
||||||
|
export function useElpService(props, emit) {
|
||||||
|
const { $moment } = useNuxtApp(); // فرض میکنیم $moment در Nuxt plugin ثبت شده
|
||||||
|
const { $http: httpService } = useNuxtApp();
|
||||||
|
const toast = useToast();
|
||||||
|
///-----------------------------------------------------
|
||||||
|
///--- یک فیلد را بروزرسانی می کند
|
||||||
|
///-----------------------------------------------------
|
||||||
|
async function uepUpdateField(doc_id, payload, key = "sanad") {
|
||||||
|
if (!doc_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = elpApi.base.update_field;
|
||||||
|
url = url.replace("{{type_name}}", key);
|
||||||
|
url = url.replace("{{doc_id}}", doc_id);
|
||||||
|
return await httpService
|
||||||
|
.postRequest(url, payload)
|
||||||
|
.then(() => {
|
||||||
|
toast.add({
|
||||||
|
title: "موفق",
|
||||||
|
description: "عملیات با موفقیت انجام شد.",
|
||||||
|
icon: "i-lucide-calendar-days",
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.add({
|
||||||
|
title: "خطا",
|
||||||
|
description: "خطا در انجام دوباره تلاش کنید",
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
///-----------------------------------------------------
|
||||||
|
///--- با توجه به یک فیلد، مقدار محاسبه شده ای بر میگردند مثلا : بیشترین مقدار یا مجموع یا ...
|
||||||
|
///-----------------------------------------------------
|
||||||
|
async function uepGetComputeField(
|
||||||
|
key = "mnsanad",
|
||||||
|
property_key = "meet_no",
|
||||||
|
compute_key = "max",
|
||||||
|
filters = {}
|
||||||
|
) {
|
||||||
|
let payload = {
|
||||||
|
compute_key,
|
||||||
|
property_key,
|
||||||
|
filters,
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = elpApi.base.compute_field;
|
||||||
|
url = url.replace("{{type_name}}", key);
|
||||||
|
|
||||||
|
return await httpService
|
||||||
|
.postRequest(url, payload)
|
||||||
|
.then((res) => {
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.add({
|
||||||
|
title: "خطا",
|
||||||
|
description: "خطا در انجام عملیات، دوباره تلاش کنید",
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
///-----------------------------------------------------
|
||||||
|
///-----------------------------------------------------
|
||||||
|
return {
|
||||||
|
uepUpdateField,
|
||||||
|
uepGetComputeField,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -436,6 +436,5 @@
|
||||||
},
|
},
|
||||||
"defaultAlignment": "right"
|
"defaultAlignment": "right"
|
||||||
},
|
},
|
||||||
|
"welcomeContent": "<div style=\"text-align: right;\">\n <p>فلسفه اعم از همه علوم و معارف است، زيرا موضوع آن (موجود) عام ترين موضوعات و در برگيرنده همه چيزهاست. علوم كلًاّ از حيث ثبوت موضوع متوقف بر فلسفه اند، اما فلسفه در ثبوت موضوع خود بر هيچ يك از علوم مبتنى نيست</p>\n <details open>\n <summary>اکاردیون خوشآمدگویی</summary>\n <p>این متن داخل اکاردیون دیفالت باز است و میتوانی محتوای خودت را اینجا اضافه کنی.</p>\n </details>\n</div>"
|
||||||
"welcomeContent": "<div style=\"text-align: right;\">\n <p>فلسفه اعم از همه علوم و معارف است، زيرا موضوع آن (موجود) عام ترين موضوعات و در برگيرنده همه چيزهاست. علوم كلًاّ از حيث ثبوت موضوع متوقف بر فلسفه اند، اما فلسفه در ثبوت موضوع خود بر هيچ يك از علوم مبتنى نيست</p> </div>"
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,26 +20,29 @@
|
||||||
],
|
],
|
||||||
"tableColumns": [
|
"tableColumns": [
|
||||||
{
|
{
|
||||||
"key": "branch",
|
"key": "previous_info.qanon_title",
|
||||||
"title": "دوره",
|
"title": "قانون مقدم",
|
||||||
"isLink": true,
|
"width": "3",
|
||||||
"width": "18%",
|
"trancate_word": 10
|
||||||
"contextmenu": true
|
|
||||||
},
|
},
|
||||||
{ "key": "title", "title": "عنوان", "width": "15%", "contextmenu": true },
|
{ "key": "previous_info.full_path", "title": "ماده مقدم", "width": "1" },
|
||||||
{ "key": "subtitle", "title": "عنوان محتوایی", "width": "25%" },
|
|
||||||
{ "key": "meet_code", "title": "کد جلسه", "width": "12%" },
|
|
||||||
{
|
{
|
||||||
"key": "begin_date",
|
"key": "next_info.qanon_title",
|
||||||
"title": "تاریخ",
|
"title": "قانون موخر",
|
||||||
"isLink": true,
|
"width": "3",
|
||||||
"width": "12%",
|
"trancate_word": 10
|
||||||
"contextmenu": true
|
|
||||||
},
|
},
|
||||||
{ "key": "video_count", "title": "ت فیلم", "width": "5%" },
|
{ "key": "next_info.full_path", "title": "ماده موخر", "width": "1" },
|
||||||
{ "key": "photo_count", "title": "ت تصویر", "width": "5%" },
|
{
|
||||||
{ "key": "sound_count", "title": "ت صوت", "width": "5%" },
|
"key": "subject_unity.main_type",
|
||||||
{ "key": "file_count", "title": "ت فایل", "width": "5%" }
|
"title": "وضعیت وحدت موضوع",
|
||||||
|
"width": "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "conflict_relation_identification.main_type",
|
||||||
|
"title": "تعارض",
|
||||||
|
"width": "2"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"menuItems": [
|
"menuItems": [
|
||||||
[
|
[
|
||||||
247
app/json/refineCodes.json
Normal file
247
app/json/refineCodes.json
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"other_id": 1,
|
||||||
|
"title": "آب",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 2,
|
||||||
|
"title": "آمار، برنامه و بودجه",
|
||||||
|
"value": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 3,
|
||||||
|
"title": "آموزش عالی، پژوهش و فناوری",
|
||||||
|
"value": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 4,
|
||||||
|
"title": "آموزش و پرورش",
|
||||||
|
"value": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 5,
|
||||||
|
"title": "آیین دادرسی اداری",
|
||||||
|
"value": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 6,
|
||||||
|
"title": "آیین دادرسی مدنی",
|
||||||
|
"value": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 7,
|
||||||
|
"title": "آیین دادرسی کیفری",
|
||||||
|
"value": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 8,
|
||||||
|
"title": "اداری و استخدامی",
|
||||||
|
"value": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 9,
|
||||||
|
"title": "اطلاعات، امنیت و نظامی، انتظامی",
|
||||||
|
"value": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 10,
|
||||||
|
"title": "اموال و معاملات دولتی",
|
||||||
|
"value": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 11,
|
||||||
|
"title": "انتخابات",
|
||||||
|
"value": 11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 12,
|
||||||
|
"title": "اوقاف، اماکن دینی و امور مذهبی",
|
||||||
|
"value": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 13,
|
||||||
|
"title": "ایثارگران",
|
||||||
|
"value": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 14,
|
||||||
|
"title": "بانکداری",
|
||||||
|
"value": 14
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 15,
|
||||||
|
"title": "برق و انرژیهای نو",
|
||||||
|
"value": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 16,
|
||||||
|
"title": "بیمه",
|
||||||
|
"value": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 17,
|
||||||
|
"title": "تأمین اجتماعی",
|
||||||
|
"value": 17
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 18,
|
||||||
|
"title": "تجارت",
|
||||||
|
"value": 18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 19,
|
||||||
|
"title": "تشکلهای مدنی و احزاب",
|
||||||
|
"value": 19
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 20,
|
||||||
|
"title": "تشکیلات و امور اداری قوه قضائیه",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 21,
|
||||||
|
"title": "تعاون",
|
||||||
|
"value": 21
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 22,
|
||||||
|
"title": "تعزیرات حکومتی",
|
||||||
|
"value": 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 23,
|
||||||
|
"title": "تقسیمات کشوری و مدیریت محلی",
|
||||||
|
"value": 23
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 24,
|
||||||
|
"title": "ثبت اسناد و املاک",
|
||||||
|
"value": 24
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 25,
|
||||||
|
"title": "پولی و مالی",
|
||||||
|
"value": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 26,
|
||||||
|
"title": "حمل و نقل",
|
||||||
|
"value": 26
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 27,
|
||||||
|
"title": "خانواده",
|
||||||
|
"value": 27
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 28,
|
||||||
|
"title": "رسانه",
|
||||||
|
"value": 28
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 29,
|
||||||
|
"title": "سلامت",
|
||||||
|
"value": 29
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 30,
|
||||||
|
"title": "صنعت",
|
||||||
|
"value": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 31,
|
||||||
|
"title": "فرهنگ و هنر",
|
||||||
|
"value": 31
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 32,
|
||||||
|
"title": "فناوری اطلاعات و ارتباطات",
|
||||||
|
"value": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 33,
|
||||||
|
"title": "کار",
|
||||||
|
"value": 33
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 34,
|
||||||
|
"title": "کشاورزی و منابع طبیعی",
|
||||||
|
"value": 34
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 35,
|
||||||
|
"title": "کیفری",
|
||||||
|
"value": 35
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 36,
|
||||||
|
"title": "مالکیت فکری",
|
||||||
|
"value": 36
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 37,
|
||||||
|
"title": "مالیات",
|
||||||
|
"value": 37
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 38,
|
||||||
|
"title": "محاسبات عمومی",
|
||||||
|
"value": 38
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 39,
|
||||||
|
"title": "محیط زیست",
|
||||||
|
"value": 39
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 40,
|
||||||
|
"title": "مدنی و امور حسبی",
|
||||||
|
"value": 40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 41,
|
||||||
|
"title": "مسکن و شهرسازی",
|
||||||
|
"value": 41
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 42,
|
||||||
|
"title": "مصرف",
|
||||||
|
"value": 42
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 43,
|
||||||
|
"title": "معدن",
|
||||||
|
"value": 43
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 44,
|
||||||
|
"title": "مناطق آزاد و ویژه اقتصادی",
|
||||||
|
"value": 44
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 45,
|
||||||
|
"title": "میراث فرهنگی و گردشگری",
|
||||||
|
"value": 45
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 46,
|
||||||
|
"title": "نظام قانونگذاری",
|
||||||
|
"value": 46
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 47,
|
||||||
|
"title": "نظامهای صنفی و حرفهای",
|
||||||
|
"value": 47
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 48,
|
||||||
|
"title": "نفت و گاز",
|
||||||
|
"value": 48
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_id": 49,
|
||||||
|
"title": "ورزش",
|
||||||
|
"value": 49
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -4,23 +4,20 @@
|
||||||
"id": "MainList",
|
"id": "MainList",
|
||||||
"key": "MainList",
|
"key": "MainList",
|
||||||
"label": "فهرست",
|
"label": "فهرست",
|
||||||
"icon": "emojione-closed-book",
|
"icon": "i-lucide-list",
|
||||||
"develop": 0,
|
|
||||||
"active": true
|
"active": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "RuleEdit",
|
"id": "RuleEdit",
|
||||||
"label": "احکام",
|
"label": "احکام",
|
||||||
"key": "RuleEdit",
|
"key": "RuleEdit",
|
||||||
"develop": 0,
|
"icon": "i-lucide-scroll-text"
|
||||||
"icon": "i-mdi-library-outline"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "RelationEdit",
|
"id": "RelationEdit",
|
||||||
"key": "RelationEdit",
|
"key": "RelationEdit",
|
||||||
"icon": "emojione-orange-book",
|
"label": "روابط",
|
||||||
"develop": 0,
|
"icon": "i-lucide-network"
|
||||||
"label": "روابط"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
773
app/manuals/utilities.js
Executable file
773
app/manuals/utilities.js
Executable file
|
|
@ -0,0 +1,773 @@
|
||||||
|
// import Vue from "vue";
|
||||||
|
// const crypto = require("crypto");
|
||||||
|
import exportFromJSON from "export-from-json";
|
||||||
|
// import store from "@store/store";
|
||||||
|
export const isLoginRemoved = () => process.env.VUE_APP_LOGIN_REMOVE == 1;
|
||||||
|
|
||||||
|
// util to export to excel file using plugin
|
||||||
|
// https://github.com/zheeeng/export-from-json
|
||||||
|
export const convertJsonToExcelUsingPlugin = async (
|
||||||
|
data,
|
||||||
|
fileName = "download"
|
||||||
|
) => {
|
||||||
|
// 'txt'(default), 'css', 'html', 'json', 'csv', 'xls', 'xml'
|
||||||
|
const exportType = exportFromJSON.types.xls;
|
||||||
|
const withBOM = true;
|
||||||
|
|
||||||
|
return await exportFromJSON({ data, fileName, exportType, withBOM });
|
||||||
|
};
|
||||||
|
|
||||||
|
// util to export to excel file manually
|
||||||
|
export const exportJsonToExcelManually = (JSONData, FileTitle, ShowLabel) => {
|
||||||
|
//If JSONData is not an object then JSON.parse will parse the JSON string in an Object
|
||||||
|
var arrData = typeof JSONData != "object" ? JSON.parse(JSONData) : JSONData;
|
||||||
|
var CSV = "";
|
||||||
|
//This condition will generate the Label/Header
|
||||||
|
if (ShowLabel) {
|
||||||
|
var row = "";
|
||||||
|
//This loop will extract the label from 1st index of on array
|
||||||
|
for (var index in arrData[0]) {
|
||||||
|
//Now convert each value to string and comma-seprated
|
||||||
|
row += encodeURI(index) + ",";
|
||||||
|
}
|
||||||
|
row = row.slice(0, -1);
|
||||||
|
//append Label row with line break
|
||||||
|
CSV += row + "\r\n";
|
||||||
|
}
|
||||||
|
//1st loop is to extract each row
|
||||||
|
for (var i = 0; i < arrData.length; i++) {
|
||||||
|
var row = "";
|
||||||
|
//2nd loop will extract each column and convert it in string comma-seprated
|
||||||
|
for (var index in arrData[i]) {
|
||||||
|
row += '"' + arrData[i][index] + '",';
|
||||||
|
}
|
||||||
|
row.slice(0, row.length - 1);
|
||||||
|
//add a line break after each row
|
||||||
|
CSV += row + "\r\n";
|
||||||
|
}
|
||||||
|
if (CSV == "") {
|
||||||
|
alert("Invalid data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//Generate a file name
|
||||||
|
// application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8;
|
||||||
|
// text/csv;charset=utf-8;
|
||||||
|
// var csvContent = "data:text/csv;charset=utf-8,%EF%BB%BF" + encodeURI(csvContent);
|
||||||
|
|
||||||
|
var filename = FileTitle;
|
||||||
|
var blob = new Blob([CSV], {
|
||||||
|
type: "text/csv;charset=utf-8,BOM",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (navigator.msSaveBlob) {
|
||||||
|
// IE 10+
|
||||||
|
navigator.msSaveBlob(blob, filename);
|
||||||
|
} else {
|
||||||
|
var link = document.createElement("a");
|
||||||
|
if (link.download !== undefined) {
|
||||||
|
// feature detection
|
||||||
|
// Browsers that support HTML5 download attribute
|
||||||
|
var url = URL.createObjectURL(blob);
|
||||||
|
link.setAttribute("href", url);
|
||||||
|
link.style = "visibility:hidden";
|
||||||
|
link.download = filename + ".csv";
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// util to delay promise
|
||||||
|
export const wait = (ms) => {
|
||||||
|
return (x) => {
|
||||||
|
return new Promise((resolve) => setTimeout(() => resolve(x), ms));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// util to createObjectByNewProperties
|
||||||
|
export const createObjectByNewProperties = (list, allowedProperties) => {
|
||||||
|
const properties = [];
|
||||||
|
|
||||||
|
list.forEach((item, index) => {
|
||||||
|
const filtered = Object.keys(item)
|
||||||
|
.filter((key) => allowedProperties.includes(key))
|
||||||
|
.reduce((obj, key) => {
|
||||||
|
obj[key] = item[key];
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
properties[index] = filtered;
|
||||||
|
});
|
||||||
|
return properties;
|
||||||
|
};
|
||||||
|
// util to convert digits to character
|
||||||
|
export const toNumbersInCharacters = (number) => {
|
||||||
|
const persianNumbers = ["۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹", "۰"];
|
||||||
|
const arabicNumbers = ["١", "٢", "٣", "٤", "٥", "٦", "٧", "٨", "٩", "٠"];
|
||||||
|
const englishNumbers = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"];
|
||||||
|
const persianNumbersInCharacters = [
|
||||||
|
"یک",
|
||||||
|
"دو",
|
||||||
|
"سه",
|
||||||
|
"چهار",
|
||||||
|
"پنج",
|
||||||
|
"شش",
|
||||||
|
"هفت",
|
||||||
|
"هشت",
|
||||||
|
"نه",
|
||||||
|
"صفر",
|
||||||
|
];
|
||||||
|
|
||||||
|
// only replace english number with persian number character
|
||||||
|
return persianNumbersInCharacters[englishNumbers.indexOf(number)];
|
||||||
|
|
||||||
|
// convert all numbers to persian digits
|
||||||
|
// return str.split("").map(c =>
|
||||||
|
// persianNumbersInCharacters[englishNumbers.indexOf(c)] ||
|
||||||
|
// persianNumbersInCharacters[englishNumbers.indexOf(c)] || c).join("")
|
||||||
|
};
|
||||||
|
// util to handle if img src is not valid
|
||||||
|
export const handleImageSrcOnError = ({ target }, isUserAvatar = true) => {
|
||||||
|
if (isUserAvatar) target.classList.add("human-avatar");
|
||||||
|
target.classList.add("error");
|
||||||
|
};
|
||||||
|
// util to convert date to local date and time strng;
|
||||||
|
export const persianDateAndTime = (stringDate, local = "fa-IR") => {
|
||||||
|
let date = new Date(stringDate);
|
||||||
|
let jsonDate = {
|
||||||
|
weekday: {
|
||||||
|
long: date.toLocaleDateString(local, { weekday: "long" }),
|
||||||
|
short: date.toLocaleDateString(local, { weekday: "short" }),
|
||||||
|
narrow: date.toLocaleDateString(local, { weekday: "narrow" }),
|
||||||
|
},
|
||||||
|
era: {
|
||||||
|
long: date.toLocaleDateString(local, { era: "long" }),
|
||||||
|
short: date.toLocaleDateString(local, { era: "short" }),
|
||||||
|
narrow: date.toLocaleDateString(local, { era: "narrow" }),
|
||||||
|
},
|
||||||
|
timeZoneName: {
|
||||||
|
long: date.toLocaleDateString(local, { timeZoneName: "long" }),
|
||||||
|
short: date.toLocaleDateString(local, { timeZoneName: "short" }),
|
||||||
|
},
|
||||||
|
year: {
|
||||||
|
numeric: date.toLocaleDateString(local, { year: "numeric" }),
|
||||||
|
"2-digit": date.toLocaleDateString(local, { year: "2-digit" }),
|
||||||
|
},
|
||||||
|
month: {
|
||||||
|
numeric: date.toLocaleDateString(local, { month: "numeric" }),
|
||||||
|
"2-digit": date.toLocaleDateString(local, { month: "2-digit" }),
|
||||||
|
long: date.toLocaleDateString(local, { month: "long" }),
|
||||||
|
short: date.toLocaleDateString(local, { month: "short" }),
|
||||||
|
narrow: date.toLocaleDateString(local, { month: "narrow" }),
|
||||||
|
},
|
||||||
|
day: {
|
||||||
|
numeric: date.toLocaleDateString(local, { day: "numeric" }),
|
||||||
|
"2-digit": date.toLocaleDateString(local, { day: "2-digit" }),
|
||||||
|
},
|
||||||
|
hour: {
|
||||||
|
numeric: date.toLocaleTimeString(local, { hour: "numeric" }),
|
||||||
|
"2-digit": date.toLocaleTimeString(local, { hour: "2-digit" }),
|
||||||
|
},
|
||||||
|
minute: {
|
||||||
|
numeric: date.toLocaleTimeString(local, { minute: "numeric" }),
|
||||||
|
"2-digit": date.toLocaleTimeString(local, { minute: "2-digit" }),
|
||||||
|
},
|
||||||
|
second: {
|
||||||
|
numeric: date.toLocaleTimeString(local, { second: "numeric" }),
|
||||||
|
"2-digit": date.toLocaleTimeString(local, { second: "2-digit" }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return jsonDate;
|
||||||
|
};
|
||||||
|
// util to format numbers to local curency
|
||||||
|
export const formatNumber = (price = "") => {
|
||||||
|
return new Intl.NumberFormat("fa-IR").format(price);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isValidHttpUrl = (string) => {
|
||||||
|
let url;
|
||||||
|
|
||||||
|
try {
|
||||||
|
url = new URL(string);
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.protocol === "http:" || url.protocol === "https:";
|
||||||
|
};
|
||||||
|
export const addJsCssFileToDom = (fileUrl, fileType, uuid) => {
|
||||||
|
let targetElement =
|
||||||
|
fileType == "js" ? "script" : fileType == "css" ? "link" : "none";
|
||||||
|
let targetAttr =
|
||||||
|
fileType == "js" ? "src" : fileType == "css" ? "href" : "none";
|
||||||
|
|
||||||
|
let node = document.createElement(targetElement);
|
||||||
|
node.setAttribute(targetAttr, fileUrl);
|
||||||
|
node.setAttribute("id", targetElement + "-" + uuid);
|
||||||
|
|
||||||
|
document.head.appendChild(node);
|
||||||
|
};
|
||||||
|
export const removeJsCssFileFromDom = (fileType, uuid) => {
|
||||||
|
let targetElement =
|
||||||
|
fileType == "js" ? "script" : fileType == "css" ? "link" : "none";
|
||||||
|
document.getElementById(targetElement + "-" + uuid)?.remove();
|
||||||
|
};
|
||||||
|
export const clearBodyClass = (className = undefined) => {
|
||||||
|
if (className) document.querySelector("html").replace(className, "");
|
||||||
|
else document.querySelector("html").removeAttribute("class");
|
||||||
|
};
|
||||||
|
export const redirectToExternalLink = (href, openInNewTab) => {
|
||||||
|
if (href) {
|
||||||
|
if (isValidHttpUrl(href) && openInNewTab) window.open(href, "_blank");
|
||||||
|
else if (isValidHttpUrl(href) && !openInNewTab) window.location.href = href;
|
||||||
|
else if (!isValidHttpUrl(href) && openInNewTab) {
|
||||||
|
if (href.includes("www.")) window.open("http://" + href, "_blank");
|
||||||
|
} else if (!isValidHttpUrl(href) && !openInNewTab) {
|
||||||
|
if (href.includes("www.")) window.location.href = "http://" + href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const extractAllQueryParams = (href) => {
|
||||||
|
return new URLSearchParams(href);
|
||||||
|
|
||||||
|
// return new Proxy(new URLSearchParams(href), {
|
||||||
|
// get: (searchParams, prop) => searchParams.get(prop),
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
export const makeQueryParams = (url, searchParams) => {
|
||||||
|
var query = new URLSearchParams();
|
||||||
|
|
||||||
|
// (A) URL SEARCH PARAMS OBJECT TO QUICKLY BUILD QUERY STRING
|
||||||
|
Object.keys(searchParams).forEach((key) =>
|
||||||
|
query.append(key, searchParams[key])
|
||||||
|
);
|
||||||
|
|
||||||
|
// (B) CONVERT TO STRING, APPEND TO URL
|
||||||
|
url += "?" + query.toString();
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
// export const decryptData = (
|
||||||
|
// data,
|
||||||
|
// key = "fTjWnZr4u7x!A%D*G-KaNdRgUkXp3s6v",
|
||||||
|
// method = "AES-256-CBC",
|
||||||
|
// iv = "poaskq2234??@35."
|
||||||
|
// ) => {
|
||||||
|
// let decipher = crypto.createDecipheriv(method, key, iv);
|
||||||
|
// let decrypted = decipher.update(data, "base64", "utf8");
|
||||||
|
// let dd = decrypted + decipher.final("utf8");
|
||||||
|
// return JSON.parse(dd);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// util to handle response errors
|
||||||
|
|
||||||
|
export const handleErrors = (error) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
/*
|
||||||
|
401: The HTTP 401 Unauthorized response status code indicates that
|
||||||
|
the client request has not been completed because it lacks
|
||||||
|
valid authentication credentials for the requested resource.
|
||||||
|
|
||||||
|
403: The HTTP 403 Forbidden response status code indicates that
|
||||||
|
the server understands the request but refuses to authorize it.
|
||||||
|
|
||||||
|
404: The HTTP 404 Not Found response status code indicates that
|
||||||
|
the server cannot find the requested resource
|
||||||
|
|
||||||
|
405: The HyperText Transfer Protocol (HTTP) 405 Method Not Allowed
|
||||||
|
response status code indicates that the server knows the
|
||||||
|
request method, but the target resource doesn't support this method
|
||||||
|
|
||||||
|
406: Not Acceptable
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
let res = error.response;
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
if (res.status === 401) {
|
||||||
|
toast.add({
|
||||||
|
title: "خطا!",
|
||||||
|
description: res.data.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
// router.push({ name: 'login' })
|
||||||
|
} else if (res.status === 403) {
|
||||||
|
toast.add({
|
||||||
|
title: "خطا!",
|
||||||
|
description: res.data.message,
|
||||||
|
});
|
||||||
|
} else if (res.status === 404) {
|
||||||
|
toast.add({
|
||||||
|
title: "خطا!",
|
||||||
|
description: res.data.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
// router.push('/404')
|
||||||
|
} else if (res.status === 405) {
|
||||||
|
toast.add({
|
||||||
|
title: "خطا!",
|
||||||
|
description: res.data.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
// router.push('/404')
|
||||||
|
} else if (res.status === 406) {
|
||||||
|
toast.add({
|
||||||
|
title: "خطا!",
|
||||||
|
description: res.data.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
// router.push('/404')
|
||||||
|
} else if (res.status === 500) {
|
||||||
|
toast.add({
|
||||||
|
title: "خطا!",
|
||||||
|
description: res.data.message ?? res.data,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.add({
|
||||||
|
title: "خطا!",
|
||||||
|
description: res.data.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(error.response);
|
||||||
|
} else {
|
||||||
|
let message = "خطا!!!";
|
||||||
|
|
||||||
|
// if (error) {
|
||||||
|
// message = error.message ?? error;
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
// message = "خطا!!!"
|
||||||
|
// }
|
||||||
|
|
||||||
|
reject(new Error(message));
|
||||||
|
|
||||||
|
// text: error.stack,
|
||||||
|
toast.add({
|
||||||
|
title: message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
let message = "خطا!!!";
|
||||||
|
message = error?.message ?? message;
|
||||||
|
|
||||||
|
return reject(new Error(message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function countDownTimer(timeInSecond = 120) {
|
||||||
|
var timeleft = timeInSecond;
|
||||||
|
var downloadTimer = setInterval(function () {
|
||||||
|
if (timeleft <= 0) {
|
||||||
|
clearInterval(downloadTimer);
|
||||||
|
// document.getElementById("countdown").innerHTML = "Finished";
|
||||||
|
} else {
|
||||||
|
// document.getElementById("countdown").innerHTML =
|
||||||
|
// timeleft + " seconds remaining";
|
||||||
|
}
|
||||||
|
timeleft -= 1;
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return timeleft;
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////
|
||||||
|
///// html functions
|
||||||
|
///////////////////////////////////////
|
||||||
|
export function getSelectionHtmlRange() {
|
||||||
|
var sel;
|
||||||
|
if (window.getSelection) {
|
||||||
|
sel = window.getSelection();
|
||||||
|
if (sel.rangeCount) {
|
||||||
|
return sel.getRangeAt(0);
|
||||||
|
}
|
||||||
|
} else if (document.selection) {
|
||||||
|
return document.selection.createRange();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSelectedHtmlInfo() {
|
||||||
|
const selectRange = getSelectionHtmlRange();
|
||||||
|
if (!selectRange) return {};
|
||||||
|
|
||||||
|
// if (selectRange.startContainer !== selectRange.endContainer) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
let begin = 0;
|
||||||
|
let strBegin = "";
|
||||||
|
let prevFullText = "";
|
||||||
|
if (selectRange.startOffset > 0)
|
||||||
|
strBegin = selectRange.startContainer.textContent.substring(
|
||||||
|
0,
|
||||||
|
selectRange.startOffset - 1
|
||||||
|
);
|
||||||
|
|
||||||
|
// برای وقتی که در تگ قبلی ، شماره کلمه انتها ذخیره شده است و نیازی به بررسی قبل از آن نیست
|
||||||
|
if (
|
||||||
|
selectRange.commonAncestorContainer.previousSibling &&
|
||||||
|
selectRange.commonAncestorContainer.previousSibling.dataset?.end
|
||||||
|
) {
|
||||||
|
begin =
|
||||||
|
parseInt(
|
||||||
|
selectRange.commonAncestorContainer.previousSibling.dataset.end
|
||||||
|
) + 1;
|
||||||
|
begin += splitWord(strBegin).length;
|
||||||
|
} else {
|
||||||
|
prevFullText = getPreviousHtmlText(selectRange.startContainer, selectRange);
|
||||||
|
begin += splitWord(prevFullText).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedText = selectRange.startContainer.textContent.substring(
|
||||||
|
selectRange.startOffset,
|
||||||
|
selectRange.endOffset
|
||||||
|
);
|
||||||
|
let end = begin + splitWord(selectedText).length - 1;
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
///// استخراج کلمه کلیک شده و دو کلمه بعدش
|
||||||
|
///// برای حالتی که متنی انتخاب نشده و فقط کلیک شده نیاز هست
|
||||||
|
|
||||||
|
let node = selectRange.startContainer;
|
||||||
|
const fullText = node.textContent;
|
||||||
|
const offset = selectRange.startOffset;
|
||||||
|
|
||||||
|
const words = fullText.split(/\s+/);
|
||||||
|
let totalChars = 0;
|
||||||
|
let wordIndex = -1;
|
||||||
|
let wordsClicked = "";
|
||||||
|
|
||||||
|
/// چون احتمال داره وسط کلمه کلیک شده باشه، قبل کلمه کلیک شده می ایستیم
|
||||||
|
for (let i = 0; i < words.length; i++) {
|
||||||
|
totalChars += words[i].length + 1;
|
||||||
|
if (offset < totalChars) {
|
||||||
|
wordIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wordIndex !== -1) {
|
||||||
|
wordsClicked =
|
||||||
|
words[wordIndex] +
|
||||||
|
" " +
|
||||||
|
(words[wordIndex + 1] || "") +
|
||||||
|
" " +
|
||||||
|
(words[wordIndex + 2] || "");
|
||||||
|
}
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
let info = {
|
||||||
|
begin: begin,
|
||||||
|
end: end,
|
||||||
|
selectedText: selectedText,
|
||||||
|
prevTextBegin: strBegin,
|
||||||
|
texts: wordsClicked,
|
||||||
|
// prevTextFull: prevFullText,
|
||||||
|
};
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPreviousHtmlText(node, selectRange = null) {
|
||||||
|
let text = "";
|
||||||
|
if (!node) return text;
|
||||||
|
else if (selectRange && node == selectRange.startContainer) {
|
||||||
|
if (selectRange.startOffset > 0)
|
||||||
|
text = selectRange.startContainer.textContent.substring(
|
||||||
|
0,
|
||||||
|
selectRange.startOffset - 1
|
||||||
|
);
|
||||||
|
// else
|
||||||
|
// text = selectRange.startContainer.textContent;
|
||||||
|
} else text = node.textContent;
|
||||||
|
|
||||||
|
if (node.previousSibling)
|
||||||
|
text = getPreviousHtmlText(node.previousSibling, null) + text;
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
export function splitWord(text) {
|
||||||
|
let items = text.split(
|
||||||
|
/[\t\n\r\.\'\"«»\)\(\]\[\{\}\%\#\$\*\<\>\/,;،؛ \-!?:٭؟]+/
|
||||||
|
);
|
||||||
|
// let items = text.split("\t\n\r.'\"«»)(][{}%#$*<>/,;،؛ -!?:٭؟")
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeText(
|
||||||
|
text,
|
||||||
|
options = { charArNormal: true, errorLine: true }
|
||||||
|
) {
|
||||||
|
// //defualt
|
||||||
|
// options = {
|
||||||
|
// charArNormal: true,
|
||||||
|
// errorLine: true,
|
||||||
|
// alphabetsNormal: false,
|
||||||
|
// emptylinesNormal: false,
|
||||||
|
// hamzehNormal: false,
|
||||||
|
// spaceCorrection: false,
|
||||||
|
// trimLine: false
|
||||||
|
// };
|
||||||
|
|
||||||
|
text = text.trim();
|
||||||
|
|
||||||
|
//اصلاح خطاهای متن در کد خط
|
||||||
|
if (options?.errorLine) {
|
||||||
|
text = text.replaceAll("\\n", "\n");
|
||||||
|
text = text.replaceAll("\\r", "\r");
|
||||||
|
text = replaceRegText("(\r\n)+", "\n", text);
|
||||||
|
text = text.replaceAll("\r", "\n");
|
||||||
|
|
||||||
|
//شنبهها ==> شنبهها
|
||||||
|
//رسیدگیهای ==> رسیدگیهای
|
||||||
|
// text = replaceRegText("(ه)(ها)", /\1\2/gm, text);
|
||||||
|
}
|
||||||
|
// یکسان سازی اختلاف حروف کیبوردهای مختلف - کدهای مختلف ولی نمایش یکسان
|
||||||
|
if (options?.alphabetsNormal) {
|
||||||
|
text = replaceAlphabets(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
//حذف خطوط و فاصلههای خالی
|
||||||
|
if (options?.emptylinesNormal) {
|
||||||
|
text = replaceRegText("( )+", " ", text);
|
||||||
|
text = replaceRegText("(\n)+", "\n", text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// نرمال کردن حروف اختلافی عربی و فارسی
|
||||||
|
if (options?.charArNormal) {
|
||||||
|
text = text.replaceAll("ي", "ی");
|
||||||
|
text = text.replaceAll("ك", "ک");
|
||||||
|
}
|
||||||
|
|
||||||
|
// پرش از اختلافات همزهای
|
||||||
|
if (options?.hamzehNormal) {
|
||||||
|
text = replaceRegText("ئ|ﺋ", "ی", text);
|
||||||
|
text = replaceRegText("ؤ|ﺅ", "و", text);
|
||||||
|
text = replaceRegText("ﺔ|ۀ|ة", "ه", text);
|
||||||
|
text = replaceRegText("إ|أ", "ا", text);
|
||||||
|
}
|
||||||
|
if (options?.dateNormal) {
|
||||||
|
text = dateNormalize(text);
|
||||||
|
}
|
||||||
|
if (options?.spaceCorrection) {
|
||||||
|
text = spaceCorrection(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// حذف خطوط اضافی اول و انتهای متن
|
||||||
|
if (options?.trimLine) text = text.replace(/^\s+|\s+$/g, "");
|
||||||
|
|
||||||
|
text = text.trim();
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
//برعکس بودن تاریخ ها را درست میکند
|
||||||
|
export function dateNormalize(text, forWord = false) {
|
||||||
|
if (forWord) {
|
||||||
|
// فرمت YYYY/MM/DD یا YYYY-MM-DD
|
||||||
|
text = replaceRegText(
|
||||||
|
/(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})/,
|
||||||
|
(_, year, month, day) =>
|
||||||
|
`${day.padStart(2, "0")}/${month.padStart(2, "0")}/${year}`,
|
||||||
|
text
|
||||||
|
);
|
||||||
|
|
||||||
|
// // فرمت DD/MM/YYYY یا DD-MM-YYYY
|
||||||
|
// text = replaceRegText(
|
||||||
|
// /(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,
|
||||||
|
// (_, day, month, year) =>
|
||||||
|
// `${year}/${month.padStart(2, "0")}/${day.padStart(2, "0")}`,
|
||||||
|
// text
|
||||||
|
// );
|
||||||
|
} else {
|
||||||
|
// فرمت YYYY/MM/DD یا YYYY-MM-DD
|
||||||
|
text = replaceRegText(
|
||||||
|
/(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})/,
|
||||||
|
(_, year, month, day) =>
|
||||||
|
`${year}/${month.padStart(2, "0")}/${day.padStart(2, "0")}`,
|
||||||
|
text
|
||||||
|
);
|
||||||
|
|
||||||
|
// فرمت DD/MM/YYYY یا DD-MM-YYYY
|
||||||
|
text = replaceRegText(
|
||||||
|
/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,
|
||||||
|
(_, day, month, year) =>
|
||||||
|
`${year}/${month.padStart(2, "0")}/${day.padStart(2, "0")}`,
|
||||||
|
text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// // فرمت MM/DD/YYYY
|
||||||
|
// text = replaceRegText(
|
||||||
|
// /(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,
|
||||||
|
// (_, month, day, year) =>
|
||||||
|
// `${year}/${month.padStart(2, "0")}/${day.padStart(2, "0")}`,
|
||||||
|
// text
|
||||||
|
// );
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceAlphabets(text) {
|
||||||
|
let res = text;
|
||||||
|
res = res.replaceAll("\u00AD", "\u200C");
|
||||||
|
res = res.replaceAll("\u00AC", "\u200C"); // نیم اسپیس خاص
|
||||||
|
|
||||||
|
res = replaceRegText("ﺁ|آ", "آ", res);
|
||||||
|
res = replaceRegText("ﺎ|ٱ|ﺍ", "ا", res); //
|
||||||
|
res = replaceRegText("ٲ|أ|ﺄ|ﺃ", "أ", res);
|
||||||
|
res = replaceRegText("ﺈ|ﺇ", "إ", res);
|
||||||
|
res = replaceRegText("ﺐ|ﺏ|ﺑ|ﺒ", "ب", res);
|
||||||
|
res = replaceRegText("ﭖ|ﭗ|ﭙ|ﭘ", "پ", res);
|
||||||
|
res = replaceRegText("ﭡ|ٺ|ٹ|ﭞ|ٿ|ټ|ﺕ|ﺗ|ﺖ|ﺘ", "ت", res);
|
||||||
|
res = replaceRegText("ﺙ|ﺛ|ﺚ|ﺜ", "ث", res);
|
||||||
|
res = replaceRegText("ﺝ|ڃ|ﺠ|ﺟ", "ج", res);
|
||||||
|
res = replaceRegText("ڃ|ﭽ|ﭼ", "چ", res);
|
||||||
|
res = replaceRegText("ﺢ|ﺤ|څ|ځ|ﺣ", "ح", res);
|
||||||
|
res = replaceRegText("ﺥ|ﺦ|ﺨ|ﺧ", "خ", res);
|
||||||
|
res = replaceRegText("ڏ|ډ|ﺪ|ﺩ", "د", res);
|
||||||
|
res = replaceRegText("ﺫ|ﺬ|ذ", "ذ", res);
|
||||||
|
res = replaceRegText("ڙ|ڗ|ڒ|ڑ|ڕ|ﺭ|ﺮ", "ر", res);
|
||||||
|
res = replaceRegText("ﺰ|ﺯ", "ز", res);
|
||||||
|
res = replaceRegText("ﮊ", "ژ", res);
|
||||||
|
res = replaceRegText("ݭ|ݜ|ﺱ|ﺲ|ښ|ﺴ|ﺳ", "س", res);
|
||||||
|
res = replaceRegText("ﺵ|ﺶ|ﺸ|ﺷ", "ش", res);
|
||||||
|
res = replaceRegText("ﺺ|ﺼ|ﺻ|ﺹ", "ص", res); //
|
||||||
|
res = replaceRegText("ﺽ|ﺾ|ﺿ|ﻀ", "ض", res);
|
||||||
|
res = replaceRegText("ﻁ|ﻂ|ﻃ|ﻄ", "ط", res);
|
||||||
|
res = replaceRegText("ﻆ|ﻇ|ﻈ", "ظ", res);
|
||||||
|
res = replaceRegText("ڠ|ﻉ|ﻊ|ﻋ|ﻌ", "ع", res);
|
||||||
|
res = replaceRegText("ﻎ|ۼ|ﻍ|ﻐ|ﻏ", "غ", res);
|
||||||
|
res = replaceRegText("ﻒ|ﻑ|ﻔ|ﻓ", "ف", res);
|
||||||
|
res = replaceRegText("ﻕ|ڤ|ﻖ|ﻗ|ﻘ", "ق", res);
|
||||||
|
res = replaceRegText("ڭ|ﻚ|ﮎ|ﻜ|ﮏ|ګ|ﻛ|ﮑ|ﮐ|ڪ|ك", "ک", res);
|
||||||
|
res = replaceRegText("ﮚ|ﮒ|ﮓ|ﮕ|ﮔ", "گ", res);
|
||||||
|
res = replaceRegText("ﻝ|ﻞ|ﻠ|ڵ|ﻟ", "ل", res); //
|
||||||
|
res = replaceRegText("ﻵ|ﻶ|ﻷ|ﻸ|ﻹ|ﻺ|ﻻ|ﻼ", "لا", res); //
|
||||||
|
res = replaceRegText("ﻡ|ﻤ|ﻢ|ﻣ", "م", res);
|
||||||
|
res = replaceRegText("ڼ|ﻦ|ﻥ|ﻨ|ﻧ", "ن", res);
|
||||||
|
res = replaceRegText("ވ|ﯙ|ۈ|ۋ|ﺆ|ۊ|ۇ|ۏ|ۅ|ۉ|ﻭ|ﻮ|ؤ", "و", res);
|
||||||
|
res = replaceRegText("ﻬ|ھ|ﻩ|ﻫ|ﻪ|ە|ہ", "ه", res);
|
||||||
|
res = replaceRegText("ﭛ|ﻯ|ۍ|ﻰ|ﻱ|ﻲ|ں|ﻳ|ﻴ|ﯼ|ې|ﯽ|ﯾ|ﯿ|ێ|ے|ى|ي", "ی", res);
|
||||||
|
res = replaceRegText("¬", "", res);
|
||||||
|
res = replaceRegText("•|·|●|·|・|∙|。|ⴰ", ".", res);
|
||||||
|
res = replaceRegText(",|٬|٫|‚|،", "،", res);
|
||||||
|
res = replaceRegText("ʕ", "؟", res);
|
||||||
|
res = replaceRegText("۰|٠", "0", res);
|
||||||
|
res = replaceRegText("۱|١", "1", res);
|
||||||
|
res = replaceRegText("۲|٢", "2", res);
|
||||||
|
res = replaceRegText("۳|٣", "3", res);
|
||||||
|
res = replaceRegText("۴|٤", "4", res);
|
||||||
|
res = replaceRegText("۵", "5", res);
|
||||||
|
res = replaceRegText("۶|٦", "6", res);
|
||||||
|
res = replaceRegText("۷|٧", "7", res);
|
||||||
|
res = replaceRegText("۸|٨", "8", res);
|
||||||
|
res = replaceRegText("۹|٩", "9", res);
|
||||||
|
res = replaceRegText("²", "2", res);
|
||||||
|
res = replaceRegText("|ِ|ُ|َ|ٍ|ٌ|ً|", "", res);
|
||||||
|
// res = replaceRegText("ـ", "_", res);
|
||||||
|
res = replaceRegText("ـ", "-", res); //
|
||||||
|
|
||||||
|
// res = replaceRegText("([\u0600-\u06FF])ـ([\u0600-\u06FF])", "\1\2", res) // حذف حروف کشیده
|
||||||
|
// res = replaceRegText("([\u0600-\u06FF])ـ", "\1", res) // حذف حروف کشیده
|
||||||
|
|
||||||
|
res = res.replace(/\u200C+$/, "");
|
||||||
|
res = res.trim();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function spaceCorrection(text) {
|
||||||
|
let res = text;
|
||||||
|
res = replaceRegText("^(بی|می|نمی)( )", "$1", res);
|
||||||
|
res = replaceRegText("( )(می|نمی|بی)( )", "$1$2", res);
|
||||||
|
res = replaceRegText(
|
||||||
|
"( )(هایی|ها|های|ایی|هایم|هایت|هایش|هایمان|هایتان|هایشان|ات|ان|ین|انی|بان|ام|ای|یم|ید|اید|اند|بودم|بودی|بود|بودیم|بودید|بودند|ست)( )",
|
||||||
|
"$2$3",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
res = replaceRegText("( )(شده|نشده)( )", "$2", res);
|
||||||
|
res = replaceRegText(
|
||||||
|
"( )(طلبان|طلب|گرایی|گرایان|شناس|شناسی|گذاری|گذار|گذاران|شناسان|گیری|پذیری|بندی|آوری|سازی|بندی|کننده|کنندگان|گیری|پرداز|پردازی|پردازان|آمیز|سنجی|ریزی|داری|دهنده|آمیز|پذیری|پذیر|پذیران|گر|ریز|ریزی|رسانی|یاب|یابی|گانه|گانهای|انگاری|گا|بند|رسانی|دهندگان|دار)( )",
|
||||||
|
"$2$3",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceRegText(pattern, replacement, text) {
|
||||||
|
const regex = new RegExp(pattern, "g");
|
||||||
|
return text.replace(regex, replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateUID() {
|
||||||
|
// I generate the UID from two parts here
|
||||||
|
// to ensure the random number provide enough bits.
|
||||||
|
var firstPart = (Math.random() * 46656) | 0;
|
||||||
|
var secondPart = (Math.random() * 46656) | 0;
|
||||||
|
firstPart = ("000" + firstPart.toString(36)).slice(-3);
|
||||||
|
secondPart = ("000" + secondPart.toString(36)).slice(-3);
|
||||||
|
return firstPart + secondPart; // eg : 9c8yxr
|
||||||
|
}
|
||||||
|
|
||||||
|
function myEncodeQuery(text) {
|
||||||
|
if (!text || text == "") return "";
|
||||||
|
//text = JSON.stringify(text);
|
||||||
|
let ch1 = encodeURIComponent("#");
|
||||||
|
let ch3 = encodeURIComponent("\\");
|
||||||
|
let ch4 = encodeURIComponent("&");
|
||||||
|
text = text.replaceAll("#", ch1);
|
||||||
|
text = text.replaceAll("&", ch4);
|
||||||
|
text = text.replaceAll("/", "\\");
|
||||||
|
text = text.replaceAll("\\", ch3);
|
||||||
|
|
||||||
|
// با تبدیل نقطه مشکل نشانی درخواست در بک حل نشد
|
||||||
|
//text = text.replaceAll(".", '%2E');
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cleanTextUnpermittedChars(text) {
|
||||||
|
if (!text) return "";
|
||||||
|
|
||||||
|
text = text.replaceAll("([0x0000-0x001F]|(0x007F))", "");
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// utils/tree.js
|
||||||
|
export function findNodeWithPath(nodes, targetId, path = []) {
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const node = nodes[i];
|
||||||
|
const currentPath = [...path, i];
|
||||||
|
|
||||||
|
// ✅ Adjust this condition based on how you identify a node
|
||||||
|
if (node.id === targetId) {
|
||||||
|
return { node, path: currentPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse into children if they exist
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
const result = findNodeWithPath(node.children, targetId, currentPath);
|
||||||
|
if (result) return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null; // Not found
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateNodeByPath(nodes, path, newValue) {
|
||||||
|
let current = nodes;
|
||||||
|
// Navigate to the parent of the target node
|
||||||
|
for (let i = 0; i < path.length - 1; i++) {
|
||||||
|
current = current[path[i]].children;
|
||||||
|
}
|
||||||
|
// Update the target node
|
||||||
|
const targetIndex = path[path.length - 1];
|
||||||
|
current[targetIndex] = { ...current[targetIndex], ...newValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////
|
||||||
|
|
||||||
|
// export { wait, handleErrors }
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
:is="currentComponent"
|
:is="currentComponent"
|
||||||
v-if="currentComponent"
|
v-if="currentComponent"
|
||||||
:activeTabKey="activeTabKey"
|
:activeTabKey="activeTabKey"
|
||||||
|
:listConflicts="listConflicts"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -26,11 +27,13 @@ import { ref, onMounted } from "vue";
|
||||||
import tabBarData from "@/json/tab-bar/data-entry/dataEntry.json";
|
import tabBarData from "@/json/tab-bar/data-entry/dataEntry.json";
|
||||||
import { defineAsyncComponent } from "vue";
|
import { defineAsyncComponent } from "vue";
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const { $http: httpService } = useNuxtApp();
|
||||||
|
|
||||||
// Stateهای Header
|
// Stateهای Header
|
||||||
const isSidebarCollapsed = ref(false);
|
const isSidebarCollapsed = ref(false);
|
||||||
const unreadNotifications = ref(5);
|
const unreadNotifications = ref(5);
|
||||||
const headerTabs = ref([]);
|
const headerTabs = ref([]);
|
||||||
|
const listConflicts = ref([]);
|
||||||
headerTabs.value = tabBarData.tabs;
|
headerTabs.value = tabBarData.tabs;
|
||||||
const activeTabKey = ref(tabBarData.tabs[0]?.key || "");
|
const activeTabKey = ref(tabBarData.tabs[0]?.key || "");
|
||||||
// const contentSchema = computed(() => ({
|
// const contentSchema = computed(() => ({
|
||||||
|
|
@ -79,7 +82,58 @@ headerSchema.value = {
|
||||||
breadcrumb: true,
|
breadcrumb: true,
|
||||||
logo: false,
|
logo: false,
|
||||||
};
|
};
|
||||||
|
const pagination = ref({
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
const getListConflict = async (textSearch = "", filterExtended = "") => {
|
||||||
|
const offset = (pagination.value.page - 1) * pagination.value.limit;
|
||||||
|
const limit = pagination.value.limit;
|
||||||
|
|
||||||
|
let tabFilter = "";
|
||||||
|
let mode_url = "elp";
|
||||||
|
let index_key = "qaconflict";
|
||||||
|
|
||||||
|
// if (props.activeTabKey === "mirbagheri") {
|
||||||
|
// tabFilter = "&f_au=استاد سید محمد مهدی میرباقری";
|
||||||
|
// mode_url = "elp_db";
|
||||||
|
// } else if (props.activeTabKey === "monir") {
|
||||||
|
// tabFilter = "&f_au=سید منیر الدین حسینی الهاشمی(ره)";
|
||||||
|
// mode_url = "elp_db";
|
||||||
|
// }
|
||||||
|
// let allFilters = tabFilter + filterPanel.value;
|
||||||
|
const request = utilGetSearchRequest(
|
||||||
|
index_key,
|
||||||
|
textSearch,
|
||||||
|
"normal",
|
||||||
|
mode_url,
|
||||||
|
tabFilter,
|
||||||
|
filterExtended,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
);
|
||||||
|
|
||||||
|
request.payload_full["search_fields"] = [
|
||||||
|
"branch",
|
||||||
|
"title",
|
||||||
|
"subtitle",
|
||||||
|
"author",
|
||||||
|
];
|
||||||
|
try {
|
||||||
|
const res = await httpService.postRequest(
|
||||||
|
request.url,
|
||||||
|
request.payload_full,
|
||||||
|
);
|
||||||
|
console.log("res", res);
|
||||||
|
|
||||||
|
listConflicts.value = res;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("خطا در دریافت داده:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
getListConflict();
|
||||||
activeTabKey.value = headerTabs.value[0]?.id || "";
|
activeTabKey.value = headerTabs.value[0]?.id || "";
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ export default defineNuxtConfig({
|
||||||
compatibilityDate: "2025-07-15",
|
compatibilityDate: "2025-07-15",
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
modules: ["@nuxt/ui", "@pinia/nuxt", "@nuxt/icon"],
|
modules: ["@nuxt/ui", "@pinia/nuxt", "@nuxt/icon"],
|
||||||
css: ["~/assets/css/main.css"],
|
css: ["@/assets/css/main.css"],
|
||||||
imports: {
|
imports: {
|
||||||
dirs: ["stores", "composables", "apis"],
|
dirs: ["stores", "composables", "apis"],
|
||||||
},
|
},
|
||||||
|
|
@ -42,7 +42,7 @@ export default defineNuxtConfig({
|
||||||
// پوشههای داخل app/
|
// پوشههای داخل app/
|
||||||
"@/components": fileURLToPath(new URL("./app/components", import.meta.url)),
|
"@/components": fileURLToPath(new URL("./app/components", import.meta.url)),
|
||||||
"@/composables": fileURLToPath(
|
"@/composables": fileURLToPath(
|
||||||
new URL("./app/composables", import.meta.url)
|
new URL("./app/composables", import.meta.url),
|
||||||
),
|
),
|
||||||
"@/layouts": fileURLToPath(new URL("./app/layouts", import.meta.url)),
|
"@/layouts": fileURLToPath(new URL("./app/layouts", import.meta.url)),
|
||||||
"@/pages": fileURLToPath(new URL("./app/pages", import.meta.url)),
|
"@/pages": fileURLToPath(new URL("./app/pages", import.meta.url)),
|
||||||
|
|
@ -66,7 +66,7 @@ export default defineNuxtConfig({
|
||||||
// .nuxt (داخل rootDir)
|
// .nuxt (داخل rootDir)
|
||||||
"@/build": fileURLToPath(new URL("./.nuxt", import.meta.url)),
|
"@/build": fileURLToPath(new URL("./.nuxt", import.meta.url)),
|
||||||
"@/internal/nuxt/paths": fileURLToPath(
|
"@/internal/nuxt/paths": fileURLToPath(
|
||||||
new URL("./.nuxt/paths.mjs", import.meta.url)
|
new URL("./.nuxt/paths.mjs", import.meta.url),
|
||||||
),
|
),
|
||||||
|
|
||||||
// shared (اگر وجود داشته باشد — در rootDir)
|
// shared (اگر وجود داشته باشد — در rootDir)
|
||||||
|
|
|
||||||
23
package-lock.json
generated
23
package-lock.json
generated
|
|
@ -12,6 +12,7 @@
|
||||||
"@nuxtjs/i18n": "^10.2.1",
|
"@nuxtjs/i18n": "^10.2.1",
|
||||||
"@pinia/nuxt": "^0.11.3",
|
"@pinia/nuxt": "^0.11.3",
|
||||||
"@tiptap/extension-color": "^3.19.0",
|
"@tiptap/extension-color": "^3.19.0",
|
||||||
|
"@tiptap/extension-details": "^3.19.0",
|
||||||
"@tiptap/extension-drag-handle": "^3.19.0",
|
"@tiptap/extension-drag-handle": "^3.19.0",
|
||||||
"@tiptap/extension-horizontal-rule": "^3.19.0",
|
"@tiptap/extension-horizontal-rule": "^3.19.0",
|
||||||
"@tiptap/extension-link": "^3.19.0",
|
"@tiptap/extension-link": "^3.19.0",
|
||||||
|
|
@ -23,6 +24,7 @@
|
||||||
"@tiptap/starter-kit": "^3.18.0",
|
"@tiptap/starter-kit": "^3.18.0",
|
||||||
"@tiptap/vue-3": "^3.18.0",
|
"@tiptap/vue-3": "^3.18.0",
|
||||||
"@vueuse/integrations": "^14.1.0",
|
"@vueuse/integrations": "^14.1.0",
|
||||||
|
"export-from-json": "^1.7.4",
|
||||||
"lodash-es": "^4.17.22",
|
"lodash-es": "^4.17.22",
|
||||||
"nuxt": "^4.2.1",
|
"nuxt": "^4.2.1",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
|
|
@ -5476,6 +5478,21 @@
|
||||||
"@tiptap/extension-text-style": "^3.19.0"
|
"@tiptap/extension-text-style": "^3.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/extension-details": {
|
||||||
|
"version": "3.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-details/-/extension-details-3.19.0.tgz",
|
||||||
|
"integrity": "sha512-BUc9N8UVl/orzXoDSh9YCB+G1csNDAKm4EeKAQpGotbgv32nnTPKv50BvPx+a5pZfVQHaiJlb98Xmi7dMZs1Ug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.19.0",
|
||||||
|
"@tiptap/extension-text-style": "^3.19.0",
|
||||||
|
"@tiptap/pm": "^3.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tiptap/extension-document": {
|
"node_modules/@tiptap/extension-document": {
|
||||||
"version": "3.18.0",
|
"version": "3.18.0",
|
||||||
"resolved": "https://mirror-npm.runflare.com/@tiptap/extension-document/-/extension-document-3.18.0.tgz",
|
"resolved": "https://mirror-npm.runflare.com/@tiptap/extension-document/-/extension-document-3.18.0.tgz",
|
||||||
|
|
@ -8616,6 +8633,12 @@
|
||||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/export-from-json": {
|
||||||
|
"version": "1.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/export-from-json/-/export-from-json-1.7.4.tgz",
|
||||||
|
"integrity": "sha512-FjmpluvZS2PTYyhkoMfQoyEJMfe2bfAyNpa5Apa6C9n7SWUWyJkG/VFnzERuj3q9Jjo3iwBjwVsDQ7Z7sczthA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/exsolve": {
|
"node_modules/exsolve": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://mirror-npm.runflare.com/exsolve/-/exsolve-1.0.8.tgz",
|
"resolved": "https://mirror-npm.runflare.com/exsolve/-/exsolve-1.0.8.tgz",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"@nuxtjs/i18n": "^10.2.1",
|
"@nuxtjs/i18n": "^10.2.1",
|
||||||
"@pinia/nuxt": "^0.11.3",
|
"@pinia/nuxt": "^0.11.3",
|
||||||
"@tiptap/extension-color": "^3.19.0",
|
"@tiptap/extension-color": "^3.19.0",
|
||||||
|
"@tiptap/extension-details": "^3.19.0",
|
||||||
"@tiptap/extension-drag-handle": "^3.19.0",
|
"@tiptap/extension-drag-handle": "^3.19.0",
|
||||||
"@tiptap/extension-horizontal-rule": "^3.19.0",
|
"@tiptap/extension-horizontal-rule": "^3.19.0",
|
||||||
"@tiptap/extension-link": "^3.19.0",
|
"@tiptap/extension-link": "^3.19.0",
|
||||||
|
|
@ -25,6 +26,7 @@
|
||||||
"@tiptap/starter-kit": "^3.18.0",
|
"@tiptap/starter-kit": "^3.18.0",
|
||||||
"@tiptap/vue-3": "^3.18.0",
|
"@tiptap/vue-3": "^3.18.0",
|
||||||
"@vueuse/integrations": "^14.1.0",
|
"@vueuse/integrations": "^14.1.0",
|
||||||
|
"export-from-json": "^1.7.4",
|
||||||
"lodash-es": "^4.17.22",
|
"lodash-es": "^4.17.22",
|
||||||
"nuxt": "^4.2.1",
|
"nuxt": "^4.2.1",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user