feat: add search settings page in dashboard, sidebar menu, and MongoDB search integration
deploy-chabokan / deploy (push) Canceled after 0s
deploy-chabokan / deploy (push) Canceled after 0s
This commit is contained in:
@@ -37,8 +37,15 @@ const model = ref([
|
||||
{ label: 'کاربران', icon: 'pi pi-fw pi-users', to: '/users/list-users' },
|
||||
{ label: 'نقشها و دسترسیها', icon: 'pi pi-fw pi-shield', to: '/roles/list-roles' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'تنظیمات سیستم',
|
||||
items: [
|
||||
{ label: 'تنظیمات جستجو', icon: 'pi pi-fw pi-sliders-h', to: '/settings/search-settings' }
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -121,6 +121,17 @@ const router = createRouter({
|
||||
name: 'list-roles',
|
||||
component: () => import('@/views/pages/Roles/ListRoles.vue')
|
||||
},
|
||||
// ─── Settings ────────────────────────────────────────────────
|
||||
{
|
||||
path: '/settings/search-settings',
|
||||
name: 'search-settings',
|
||||
component: () => import('@/views/pages/Settings/SearchSettings.vue')
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
redirect: '/settings/search-settings'
|
||||
},
|
||||
|
||||
{
|
||||
path: '/uikit/formlayout',
|
||||
name: 'formlayout',
|
||||
|
||||
@@ -55,9 +55,28 @@ export const PropertyService = {
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
searchPropertiesMongo: async (query) => {
|
||||
const config = {
|
||||
method: 'get',
|
||||
url: '/api/properties/admin/search-mongo',
|
||||
withCredentials: true,
|
||||
params: query,
|
||||
headers: PropertyService._getAuthHeader()
|
||||
};
|
||||
try {
|
||||
const response = await axios(config);
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
indexProperties: async () => {
|
||||
const config = {
|
||||
method: 'post',
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export const SettingService = {
|
||||
_getAuthHeader: () => {
|
||||
const bmToken = document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('bm-token='))
|
||||
?.split('=')[1];
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${bmToken}`
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get search settings for all modules
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
getSearchSettings: async () => {
|
||||
const config = {
|
||||
method: 'get',
|
||||
url: '/api/settings/search-settings',
|
||||
withCredentials: true,
|
||||
headers: SettingService._getAuthHeader()
|
||||
};
|
||||
try {
|
||||
const response = await axios(config);
|
||||
return response.data.data;
|
||||
} catch (err) {
|
||||
console.error('Error fetching search settings:', err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get search setting for a specific page key
|
||||
* @param {string} pageKey
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
getSearchSettingByPage: async (pageKey) => {
|
||||
const config = {
|
||||
method: 'get',
|
||||
url: `/api/settings/search-settings/${pageKey}`,
|
||||
withCredentials: true,
|
||||
headers: SettingService._getAuthHeader()
|
||||
};
|
||||
try {
|
||||
const response = await axios(config);
|
||||
return response.data.data;
|
||||
} catch (err) {
|
||||
console.error(`Error fetching search setting for ${pageKey}:`, err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update search method for a specific page key
|
||||
* @param {string} pageKey
|
||||
* @param {string} searchMethod - 'typesense' | 'mongo'
|
||||
* @param {Object} extraConfig
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
updateSearchSetting: async (pageKey, searchMethod, extraConfig = {}) => {
|
||||
const config = {
|
||||
method: 'put',
|
||||
url: `/api/settings/search-settings/${pageKey}`,
|
||||
withCredentials: true,
|
||||
data: { searchMethod, extraConfig },
|
||||
headers: SettingService._getAuthHeader()
|
||||
};
|
||||
try {
|
||||
const response = await axios(config);
|
||||
return response.data.data;
|
||||
} catch (err) {
|
||||
console.error(`Error updating search setting for ${pageKey}:`, err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Bulk update search settings
|
||||
* @param {Array<{pageKey: string, searchMethod: string}>} settings
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
bulkUpdateSearchSettings: async (settings) => {
|
||||
const config = {
|
||||
method: 'put',
|
||||
url: '/api/settings/search-settings',
|
||||
withCredentials: true,
|
||||
data: { settings },
|
||||
headers: SettingService._getAuthHeader()
|
||||
};
|
||||
try {
|
||||
const response = await axios(config);
|
||||
return response.data.data;
|
||||
} catch (err) {
|
||||
console.error('Error bulk updating search settings:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -168,9 +168,28 @@ export const TransactionService = {
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
searchTransactionsMongo: async (query) => {
|
||||
const config = {
|
||||
method: 'get',
|
||||
url: '/api/transactions/admin/search-mongo',
|
||||
withCredentials: true,
|
||||
params: query,
|
||||
headers: TransactionService._getAuthHeader()
|
||||
};
|
||||
try {
|
||||
const response = await axios(config);
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
indexTransactions: async () => {
|
||||
const config = {
|
||||
method: 'post',
|
||||
|
||||
@@ -3,8 +3,10 @@ import { FilterMatchMode } from '@primevue/core/api';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { onMounted, ref, watch, computed } from 'vue';
|
||||
import { PropertyService } from '@/service/PropertyService';
|
||||
import { SettingService } from '@/service/SettingService';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import Chip from 'primevue/chip';
|
||||
import Tag from 'primevue/tag';
|
||||
import InputIcon from 'primevue/inputicon';
|
||||
import IconField from 'primevue/iconfield';
|
||||
import SelectButton from 'primevue/selectbutton';
|
||||
@@ -34,6 +36,7 @@ const selectedPropertyId = ref(null);
|
||||
const mapRef = ref(null);
|
||||
const splitMapRef = ref(null);
|
||||
const propertyCardRefs = ref({});
|
||||
const searchMethod = ref(null); // 'typesense' | 'mongo'
|
||||
|
||||
const filters = ref({
|
||||
global: { value: null, matchMode: FilterMatchMode.CONTAINS }
|
||||
@@ -47,7 +50,18 @@ const pagesCount = ref(0);
|
||||
|
||||
const DEFAULT_IMAGE = 'https://cloud.mbmanager.ir/images/item-no-image.png';
|
||||
|
||||
onMounted(() => {
|
||||
const loadSearchSetting = async () => {
|
||||
try {
|
||||
const setting = await SettingService.getSearchSettingByPage('properties');
|
||||
searchMethod.value = setting?.searchMethod || 'typesense';
|
||||
} catch (e) {
|
||||
console.warn('Failed to load search setting, defaulting to typesense', e);
|
||||
searchMethod.value = 'typesense';
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadSearchSetting();
|
||||
loadData();
|
||||
});
|
||||
|
||||
@@ -112,7 +126,15 @@ const onPageChange = (event) => {
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const response = await PropertyService.searchProperties(route.query);
|
||||
if (searchMethod.value === null) {
|
||||
await loadSearchSetting();
|
||||
}
|
||||
let response;
|
||||
if (searchMethod.value === 'mongo') {
|
||||
response = await PropertyService.searchPropertiesMongo(route.query);
|
||||
} else {
|
||||
response = await PropertyService.searchProperties(route.query);
|
||||
}
|
||||
properties.value = response.properties || response.belongings || response || [];
|
||||
totalCount.value = response.totalCount || (Array.isArray(response) ? response.length : 0);
|
||||
pagesCount.value = Math.ceil(totalCount.value / per_page.value);
|
||||
@@ -122,6 +144,7 @@ const loadData = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const loading = ref(false);
|
||||
const indexingFinished = ref(false);
|
||||
const indexingMessage = ref('در حال ایندکس...');
|
||||
@@ -262,6 +285,18 @@ const onCardHover = (propertyId) => {
|
||||
</IconField>
|
||||
<Chip v-if="searchPhrase" class="mx-1" :label="searchPhrase" removable @remove="handleRemoveSearch" />
|
||||
|
||||
<!-- Search Engine Mode Tag -->
|
||||
<Tag
|
||||
v-if="searchMethod"
|
||||
:value="searchMethod === 'mongo' ? 'جستجو: MongoDB' : 'جستجو: Typesense'"
|
||||
:severity="searchMethod === 'mongo' ? 'success' : 'info'"
|
||||
:icon="searchMethod === 'mongo' ? 'pi pi-database' : 'pi pi-bolt'"
|
||||
class="cursor-pointer text-xs"
|
||||
v-tooltip.bottom="'کلیک برای تغییر موتور جستجو در تنظیمات'"
|
||||
@click="router.push('/settings/search-settings')"
|
||||
/>
|
||||
|
||||
|
||||
<!-- View Mode Selector -->
|
||||
<SelectButton
|
||||
v-model="viewMode"
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
<script setup>
|
||||
import { onMounted, ref, computed } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { SettingService } from '@/service/SettingService';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
const router = useRouter();
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const settings = ref([]);
|
||||
|
||||
const methodOptions = [
|
||||
{ label: 'Typesense', value: 'typesense', icon: 'pi pi-bolt', class: 'text-amber-500' },
|
||||
{ label: 'MongoDB', value: 'mongo', icon: 'pi pi-database', class: 'text-emerald-500' }
|
||||
];
|
||||
|
||||
onMounted(async () => {
|
||||
await loadSettings();
|
||||
});
|
||||
|
||||
const loadSettings = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await SettingService.getSearchSettings();
|
||||
settings.value = data;
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'خطا',
|
||||
detail: 'خطا در بارگذاری تنظیمات جستجو',
|
||||
life: 3000
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const stats = computed(() => {
|
||||
const total = settings.value.length;
|
||||
const mongoCount = settings.value.filter((s) => s.searchMethod === 'mongo').length;
|
||||
const typesenseCount = settings.value.filter((s) => s.searchMethod === 'typesense').length;
|
||||
const persistedCount = settings.value.filter((s) => s.isPersisted).length;
|
||||
return { total, mongoCount, typesenseCount, persistedCount };
|
||||
});
|
||||
|
||||
const updateMethod = async (pageKey, newMethod) => {
|
||||
try {
|
||||
const updated = await SettingService.updateSearchSetting(pageKey, newMethod);
|
||||
const index = settings.value.findIndex((s) => s.pageKey === pageKey);
|
||||
if (index !== -1) {
|
||||
settings.value[index] = { ...settings.value[index], ...updated };
|
||||
}
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'بروزرسانی شد',
|
||||
detail: `روش جستجوی "${updated.title}" به ${newMethod === 'mongo' ? 'MongoDB' : 'Typesense'} تغییر یافت.`,
|
||||
life: 3000
|
||||
});
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'خطا',
|
||||
detail: 'خطا در ذخیرهسازی تنظیمات در پایگاه داده',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const setAllToMethod = async (method) => {
|
||||
confirm.require({
|
||||
message: `آیا از تغییر روش جستجوی تمامی بخشها به ${method === 'mongo' ? 'MongoDB' : 'Typesense'} اطمینان دارید؟`,
|
||||
header: 'تایید تغییر همگانی',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
rejectProps: { label: 'انصراف', severity: 'secondary', outlined: true },
|
||||
acceptProps: { label: 'تایید و اعمال', severity: method === 'mongo' ? 'success' : 'warn' },
|
||||
accept: async () => {
|
||||
saving.value = true;
|
||||
try {
|
||||
const payload = settings.value.map((s) => ({
|
||||
pageKey: s.pageKey,
|
||||
searchMethod: method
|
||||
}));
|
||||
const updatedList = await SettingService.bulkUpdateSearchSettings(payload);
|
||||
settings.value = updatedList;
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'موفقیتآمیز',
|
||||
detail: `تمامی بخشها با موفقیت به ${method === 'mongo' ? 'MongoDB' : 'Typesense'} تغییر یافتند.`,
|
||||
life: 4000
|
||||
});
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'خطا',
|
||||
detail: 'خطا در بروزرسانی همگانی تنظیمات',
|
||||
life: 3000
|
||||
});
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const navigateToModule = (routePath) => {
|
||||
if (routePath) {
|
||||
router.push(routePath);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-settings-page">
|
||||
<Toast />
|
||||
<ConfirmDialog />
|
||||
|
||||
<!-- ── Header Banner ── -->
|
||||
<div class="card p-6 mb-6 shadow-sm border border-surface-200 dark:border-surface-700 bg-surface-0 dark:bg-surface-900 rounded-2xl">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-14 h-14 rounded-2xl bg-primary-100 dark:bg-primary-950 flex items-center justify-center text-primary-600 dark:text-primary-400">
|
||||
<i class="pi pi-sliders-h text-2xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold m-0 text-surface-900 dark:text-surface-0">تنظیمات موتور جستجو</h2>
|
||||
<p class="text-surface-500 dark:text-surface-400 m-0 mt-1 text-sm">
|
||||
انتخاب روش جستجو (MongoDB یا Typesense) برای هر بخش از سیستم به صورت مجزا
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Badges -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="flex items-center gap-2 px-3 py-2 rounded-xl bg-surface-100 dark:bg-surface-800 text-surface-700 dark:text-surface-300 text-xs font-semibold">
|
||||
<i class="pi pi-list text-primary-500"></i>
|
||||
<span>کل بخشها: {{ stats.total }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 px-3 py-2 rounded-xl bg-emerald-50 dark:bg-emerald-950 text-emerald-700 dark:text-emerald-300 text-xs font-semibold">
|
||||
<i class="pi pi-database text-emerald-500"></i>
|
||||
<span>MongoDB: {{ stats.mongoCount }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 px-3 py-2 rounded-xl bg-amber-50 dark:bg-amber-950 text-amber-700 dark:text-amber-300 text-xs font-semibold">
|
||||
<i class="pi pi-bolt text-amber-500"></i>
|
||||
<span>Typesense: {{ stats.typesenseCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Quick Actions Toolbar ── -->
|
||||
<div class="card p-4 mb-6 flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<Button
|
||||
label="تنظیم همه روی MongoDB"
|
||||
icon="pi pi-database"
|
||||
severity="success"
|
||||
outlined
|
||||
size="small"
|
||||
:disabled="loading || saving"
|
||||
@click="setAllToMethod('mongo')"
|
||||
/>
|
||||
<Button
|
||||
label="تنظیم همه روی Typesense"
|
||||
icon="pi pi-bolt"
|
||||
severity="warn"
|
||||
outlined
|
||||
size="small"
|
||||
:disabled="loading || saving"
|
||||
@click="setAllToMethod('typesense')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
text
|
||||
rounded
|
||||
:loading="loading"
|
||||
v-tooltip.top="'بارگذاری مجدد'"
|
||||
@click="loadSettings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Settings Cards Grid ── -->
|
||||
<div v-if="loading" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div v-for="i in 6" :key="i" class="card p-6">
|
||||
<Skeleton width="40%" height="1.5rem" class="mb-3" />
|
||||
<Skeleton width="80%" height="1rem" class="mb-4" />
|
||||
<Skeleton width="100%" height="2.5rem" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div
|
||||
v-for="item in settings"
|
||||
:key="item.pageKey"
|
||||
class="card p-6 rounded-2xl border transition-all duration-200 hover:shadow-md flex flex-col justify-between"
|
||||
:class="[
|
||||
item.searchMethod === 'mongo'
|
||||
? 'border-emerald-200 dark:border-emerald-900 bg-emerald-50/10'
|
||||
: 'border-amber-200 dark:border-amber-900 bg-amber-50/10'
|
||||
]"
|
||||
>
|
||||
<div>
|
||||
<!-- Card Top Header -->
|
||||
<div class="flex items-start justify-between gap-3 mb-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-10 h-10 rounded-xl flex items-center justify-center text-lg"
|
||||
:class="[
|
||||
item.searchMethod === 'mongo'
|
||||
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300'
|
||||
: 'bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-300'
|
||||
]"
|
||||
>
|
||||
<i :class="item.icon || 'pi pi-file'"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-bold m-0 text-surface-900 dark:text-surface-0">{{ item.title }}</h3>
|
||||
<span class="text-xs text-surface-500 font-mono">{{ item.pageKey }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Persistence Badge -->
|
||||
<Tag
|
||||
:value="item.isPersisted ? 'ذخیره در دیتابیس' : 'پیشفرض'"
|
||||
:severity="item.isPersisted ? 'success' : 'secondary'"
|
||||
class="text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="text-sm text-surface-600 dark:text-surface-300 mb-4 leading-relaxed">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
|
||||
<!-- Endpoints Information -->
|
||||
<div class="p-3 mb-4 rounded-xl bg-surface-100 dark:bg-surface-800/80 text-xs space-y-1 font-mono">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-surface-500">اندپوینت فعال:</span>
|
||||
<span class="font-bold" :class="item.searchMethod === 'mongo' ? 'text-emerald-600 dark:text-emerald-400' : 'text-amber-600 dark:text-amber-400'">
|
||||
{{ item.searchMethod === 'mongo' ? item.endpointMongo : item.endpointTypesense }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Selector & Actions -->
|
||||
<div class="pt-4 border-t border-surface-200 dark:border-surface-700 flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-semibold text-surface-600 dark:text-surface-400">موتور جستجو:</span>
|
||||
<SelectButton
|
||||
:modelValue="item.searchMethod"
|
||||
:options="methodOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
:allowEmpty="false"
|
||||
size="small"
|
||||
@update:modelValue="(val) => updateMethod(item.pageKey, val)"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div class="flex items-center gap-1.5 px-1 py-0.5 text-xs font-bold">
|
||||
<i :class="slotProps.option.icon"></i>
|
||||
<span>{{ slotProps.option.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</SelectButton>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
v-if="item.routePath"
|
||||
label="مشاهده صفحه"
|
||||
icon="pi pi-arrow-left"
|
||||
iconPos="right"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
text
|
||||
@click="navigateToModule(item.routePath)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Information Guide ── -->
|
||||
<div class="card p-6 mt-8 rounded-2xl bg-surface-50 dark:bg-surface-800/50 border border-surface-200 dark:border-surface-700">
|
||||
<h4 class="text-base font-bold mb-3 flex items-center gap-2 text-surface-800 dark:text-surface-100">
|
||||
<i class="pi pi-info-circle text-primary-500"></i>
|
||||
<span>راهنمای روشهای جستجو</span>
|
||||
</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs leading-relaxed text-surface-600 dark:text-surface-300">
|
||||
<div class="p-4 rounded-xl bg-surface-0 dark:bg-surface-900 border border-amber-200 dark:border-amber-900/50">
|
||||
<div class="font-bold text-amber-700 dark:text-amber-400 flex items-center gap-2 mb-1">
|
||||
<i class="pi pi-bolt"></i>
|
||||
<span>موتور Typesense</span>
|
||||
</div>
|
||||
<p class="m-0">
|
||||
موتور جستجوی ایندکسشده با سرعت فوقالعاده بالا، تحمل خطای تایپی (Typo Tolerance) و سیستم Faceting پیشرفته. نیازمند فرآیند ایندکسسازی دادهها است.
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 rounded-xl bg-surface-0 dark:bg-surface-900 border border-emerald-200 dark:border-emerald-900/50">
|
||||
<div class="font-bold text-emerald-700 dark:text-emerald-400 flex items-center gap-2 mb-1">
|
||||
<i class="pi pi-database"></i>
|
||||
<span>موتور MongoDB</span>
|
||||
</div>
|
||||
<p class="m-0">
|
||||
جستجوی زنده مستقیم روی پایگاه داده MongoDB با الگوهای عبارت منظم (Regex) و فیلترهای دقیق. بدون نیاز به ایندکس جداگانه و همیشه هماهنگ با آخرین تغییرات پایگاه داده.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-settings-page {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,13 +3,29 @@ import { FilterMatchMode } from '@primevue/core/api';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { TransactionService } from '@/service/TransactionService';
|
||||
import { SettingService } from '@/service/SettingService';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import Chip from 'primevue/chip';
|
||||
import Tag from 'primevue/tag';
|
||||
import InputIcon from 'primevue/inputicon';
|
||||
import IconField from 'primevue/iconfield';
|
||||
import LoadingCheck from '@/components/LoadingCheck.vue';
|
||||
import Popover from 'primevue/popover';
|
||||
onMounted(() => {
|
||||
|
||||
const searchMethod = ref(null);
|
||||
|
||||
const loadSearchSetting = async () => {
|
||||
try {
|
||||
const setting = await SettingService.getSearchSettingByPage('transactions');
|
||||
searchMethod.value = setting?.searchMethod || 'typesense';
|
||||
} catch (e) {
|
||||
console.warn('Failed to load search setting for transactions, defaulting to typesense', e);
|
||||
searchMethod.value = 'typesense';
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadSearchSetting();
|
||||
loadData();
|
||||
});
|
||||
|
||||
@@ -74,7 +90,7 @@ watch(
|
||||
{ deep: true }
|
||||
);
|
||||
const handleRemoveSearch = () => {
|
||||
removeFromQuery('searchPhrase'); // Ensure 'searchPhrase' is the correct key
|
||||
removeFromQuery('query');
|
||||
searchPhrase.value = '';
|
||||
};
|
||||
|
||||
@@ -105,20 +121,27 @@ const onPageChange = (event) => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadData = () => {
|
||||
TransactionService.searchTransactions(route.query)
|
||||
.then((response) => {
|
||||
console.log('response', response);
|
||||
transactions.value = response.transactions || response;
|
||||
totalCount.value = response.totalCount || response.length || 0;
|
||||
pagesCount.value = Math.ceil(totalCount.value / per_page.value);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'خطا در دریافت اطلاعات', life: 3000 });
|
||||
console.error('Error loading data:', error);
|
||||
});
|
||||
const loadData = async () => {
|
||||
try {
|
||||
if (searchMethod.value === null) {
|
||||
await loadSearchSetting();
|
||||
}
|
||||
let response;
|
||||
if (searchMethod.value === 'mongo') {
|
||||
response = await TransactionService.searchTransactionsMongo(route.query);
|
||||
} else {
|
||||
response = await TransactionService.searchTransactions(route.query);
|
||||
}
|
||||
transactions.value = response.transactions || response;
|
||||
totalCount.value = response.totalCount || response.length || 0;
|
||||
pagesCount.value = Math.ceil(totalCount.value / per_page.value);
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'خطا در دریافت اطلاعات', life: 3000 });
|
||||
console.error('Error loading data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const loading = ref(false);
|
||||
const indexingFinished = ref(false);
|
||||
const indexingMessage = ref('در حال ایندکس...');
|
||||
@@ -270,15 +293,27 @@ const toggle = (event, data) => {
|
||||
:globalFilterFields="['type', 'status', 'belongingName', 'createdAt', 'description']"
|
||||
>
|
||||
<template #header>
|
||||
<div class="">
|
||||
<h4 class="m-0">مدیریت تراکنشها</h4>
|
||||
<div class="p-input-icon-right w-1/3 mt-2 lg:mt-0">
|
||||
<InputText v-model="searchPhrase" placeholder="جستجو..." class="w-full" />
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<h4 class="m-0 font-bold">مدیریت تراکنشها</h4>
|
||||
<Tag
|
||||
v-if="searchMethod"
|
||||
:value="searchMethod === 'mongo' ? 'جستجو: MongoDB' : 'جستجو: Typesense'"
|
||||
:severity="searchMethod === 'mongo' ? 'success' : 'info'"
|
||||
:icon="searchMethod === 'mongo' ? 'pi pi-database' : 'pi pi-bolt'"
|
||||
class="cursor-pointer text-xs"
|
||||
v-tooltip.bottom="'کلیک برای تغییر موتور جستجو در تنظیمات'"
|
||||
@click="router.push('/settings/search-settings')"
|
||||
/>
|
||||
</div>
|
||||
<div class="p-input-icon-right w-full sm:w-72 mt-2 lg:mt-0">
|
||||
<InputText v-model="searchPhrase" placeholder="جستجو در تراکنشها..." class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<Chip v-if="searchPhrase" class="mx-2 mt-2" :label="searchPhrase" removable @remove="handleRemoveSearch" />
|
||||
</template>
|
||||
|
||||
|
||||
<Column field="_id" header="شناسه" sortable style="width: 10%"></Column>
|
||||
<Column field="belongingName" header="ملک" sortable style="width: 15%"></Column>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user