543 lines
15 KiB
Vue
Executable File
543 lines
15 KiB
Vue
Executable File
<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>
|