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-users', to: '/users/list-users' },
|
||||||
{ label: 'نقشها و دسترسیها', icon: 'pi pi-fw pi-shield', to: '/roles/list-roles' }
|
{ 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -121,6 +121,17 @@ const router = createRouter({
|
|||||||
name: 'list-roles',
|
name: 'list-roles',
|
||||||
component: () => import('@/views/pages/Roles/ListRoles.vue')
|
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',
|
path: '/uikit/formlayout',
|
||||||
name: 'formlayout',
|
name: 'formlayout',
|
||||||
|
|||||||
@@ -55,9 +55,28 @@ export const PropertyService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(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 () => {
|
indexProperties: async () => {
|
||||||
const config = {
|
const config = {
|
||||||
method: 'post',
|
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;
|
return response.data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(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 () => {
|
indexTransactions: async () => {
|
||||||
const config = {
|
const config = {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { FilterMatchMode } from '@primevue/core/api';
|
|||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { onMounted, ref, watch, computed } from 'vue';
|
import { onMounted, ref, watch, computed } from 'vue';
|
||||||
import { PropertyService } from '@/service/PropertyService';
|
import { PropertyService } from '@/service/PropertyService';
|
||||||
|
import { SettingService } from '@/service/SettingService';
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import Chip from 'primevue/chip';
|
import Chip from 'primevue/chip';
|
||||||
|
import Tag from 'primevue/tag';
|
||||||
import InputIcon from 'primevue/inputicon';
|
import InputIcon from 'primevue/inputicon';
|
||||||
import IconField from 'primevue/iconfield';
|
import IconField from 'primevue/iconfield';
|
||||||
import SelectButton from 'primevue/selectbutton';
|
import SelectButton from 'primevue/selectbutton';
|
||||||
@@ -34,6 +36,7 @@ const selectedPropertyId = ref(null);
|
|||||||
const mapRef = ref(null);
|
const mapRef = ref(null);
|
||||||
const splitMapRef = ref(null);
|
const splitMapRef = ref(null);
|
||||||
const propertyCardRefs = ref({});
|
const propertyCardRefs = ref({});
|
||||||
|
const searchMethod = ref(null); // 'typesense' | 'mongo'
|
||||||
|
|
||||||
const filters = ref({
|
const filters = ref({
|
||||||
global: { value: null, matchMode: FilterMatchMode.CONTAINS }
|
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';
|
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();
|
loadData();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -112,7 +126,15 @@ const onPageChange = (event) => {
|
|||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
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 || [];
|
properties.value = response.properties || response.belongings || response || [];
|
||||||
totalCount.value = response.totalCount || (Array.isArray(response) ? response.length : 0);
|
totalCount.value = response.totalCount || (Array.isArray(response) ? response.length : 0);
|
||||||
pagesCount.value = Math.ceil(totalCount.value / per_page.value);
|
pagesCount.value = Math.ceil(totalCount.value / per_page.value);
|
||||||
@@ -122,6 +144,7 @@ const loadData = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const indexingFinished = ref(false);
|
const indexingFinished = ref(false);
|
||||||
const indexingMessage = ref('در حال ایندکس...');
|
const indexingMessage = ref('در حال ایندکس...');
|
||||||
@@ -262,6 +285,18 @@ const onCardHover = (propertyId) => {
|
|||||||
</IconField>
|
</IconField>
|
||||||
<Chip v-if="searchPhrase" class="mx-1" :label="searchPhrase" removable @remove="handleRemoveSearch" />
|
<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 -->
|
<!-- View Mode Selector -->
|
||||||
<SelectButton
|
<SelectButton
|
||||||
v-model="viewMode"
|
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 { useToast } from 'primevue/usetoast';
|
||||||
import { onMounted, ref, watch } from 'vue';
|
import { onMounted, ref, watch } from 'vue';
|
||||||
import { TransactionService } from '@/service/TransactionService';
|
import { TransactionService } from '@/service/TransactionService';
|
||||||
|
import { SettingService } from '@/service/SettingService';
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import Chip from 'primevue/chip';
|
import Chip from 'primevue/chip';
|
||||||
|
import Tag from 'primevue/tag';
|
||||||
import InputIcon from 'primevue/inputicon';
|
import InputIcon from 'primevue/inputicon';
|
||||||
import IconField from 'primevue/iconfield';
|
import IconField from 'primevue/iconfield';
|
||||||
import LoadingCheck from '@/components/LoadingCheck.vue';
|
import LoadingCheck from '@/components/LoadingCheck.vue';
|
||||||
import Popover from 'primevue/popover';
|
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();
|
loadData();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -74,7 +90,7 @@ watch(
|
|||||||
{ deep: true }
|
{ deep: true }
|
||||||
);
|
);
|
||||||
const handleRemoveSearch = () => {
|
const handleRemoveSearch = () => {
|
||||||
removeFromQuery('searchPhrase'); // Ensure 'searchPhrase' is the correct key
|
removeFromQuery('query');
|
||||||
searchPhrase.value = '';
|
searchPhrase.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -105,20 +121,27 @@ const onPageChange = (event) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadData = () => {
|
const loadData = async () => {
|
||||||
TransactionService.searchTransactions(route.query)
|
try {
|
||||||
.then((response) => {
|
if (searchMethod.value === null) {
|
||||||
console.log('response', response);
|
await loadSearchSetting();
|
||||||
transactions.value = response.transactions || response;
|
}
|
||||||
totalCount.value = response.totalCount || response.length || 0;
|
let response;
|
||||||
pagesCount.value = Math.ceil(totalCount.value / per_page.value);
|
if (searchMethod.value === 'mongo') {
|
||||||
})
|
response = await TransactionService.searchTransactionsMongo(route.query);
|
||||||
.catch((error) => {
|
} else {
|
||||||
toast.add({ severity: 'error', summary: 'Error', detail: 'خطا در دریافت اطلاعات', life: 3000 });
|
response = await TransactionService.searchTransactions(route.query);
|
||||||
console.error('Error loading data:', error);
|
}
|
||||||
});
|
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 loading = ref(false);
|
||||||
const indexingFinished = ref(false);
|
const indexingFinished = ref(false);
|
||||||
const indexingMessage = ref('در حال ایندکس...');
|
const indexingMessage = ref('در حال ایندکس...');
|
||||||
@@ -270,15 +293,27 @@ const toggle = (event, data) => {
|
|||||||
:globalFilterFields="['type', 'status', 'belongingName', 'createdAt', 'description']"
|
:globalFilterFields="['type', 'status', 'belongingName', 'createdAt', 'description']"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<h4 class="m-0">مدیریت تراکنشها</h4>
|
<div class="flex items-center gap-3">
|
||||||
<div class="p-input-icon-right w-1/3 mt-2 lg:mt-0">
|
<h4 class="m-0 font-bold">مدیریت تراکنشها</h4>
|
||||||
<InputText v-model="searchPhrase" placeholder="جستجو..." class="w-full" />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<Chip v-if="searchPhrase" class="mx-2 mt-2" :label="searchPhrase" removable @remove="handleRemoveSearch" />
|
<Chip v-if="searchPhrase" class="mx-2 mt-2" :label="searchPhrase" removable @remove="handleRemoveSearch" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<Column field="_id" header="شناسه" sortable style="width: 10%"></Column>
|
<Column field="_id" header="شناسه" sortable style="width: 10%"></Column>
|
||||||
<Column field="belongingName" header="ملک" sortable style="width: 15%"></Column>
|
<Column field="belongingName" header="ملک" sortable style="width: 15%"></Column>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user