454 lines
24 KiB
Vue
454 lines
24 KiB
Vue
<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([]);
|
||
|
||
// Seeder State
|
||
const seedStatus = ref({ isSeeded: false, seededAt: null });
|
||
const isSeeding = ref(false);
|
||
const seedLoading = ref(false);
|
||
|
||
// Auto-Save Settings State
|
||
const autoSaveSettings = ref({ enabled: true, intervalSeconds: 60 });
|
||
const autoSaveLoading = ref(false);
|
||
const autoSaveSaving = ref(false);
|
||
|
||
const autoSaveIntervalOptions = [
|
||
{ label: '۳۰ ثانیه (سریع)', value: 30 },
|
||
{ label: '۱ دقیقه (پیشفرض)', value: 60 },
|
||
{ label: '۲ دقیقه', value: 120 },
|
||
{ label: '۳ دقیقه', value: 180 },
|
||
{ label: '۵ دقیقه', value: 300 },
|
||
{ label: '۱۰ دقیقه', value: 600 }
|
||
];
|
||
|
||
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 Promise.all([loadSettings(), loadSeedStatus(), loadAutoSaveSettings()]);
|
||
});
|
||
|
||
const loadAutoSaveSettings = async () => {
|
||
autoSaveLoading.value = true;
|
||
try {
|
||
const data = await SettingService.getAutoSaveSetting();
|
||
if (data) {
|
||
autoSaveSettings.value = {
|
||
enabled: data.enabled !== undefined ? data.enabled : true,
|
||
intervalSeconds: Number(data.intervalSeconds) || 60
|
||
};
|
||
}
|
||
} catch (e) {
|
||
console.warn('Failed to load autosave settings:', e);
|
||
} finally {
|
||
autoSaveLoading.value = false;
|
||
}
|
||
};
|
||
|
||
const saveAutoSaveSettings = async () => {
|
||
autoSaveSaving.value = true;
|
||
try {
|
||
await SettingService.updateAutoSaveSetting(autoSaveSettings.value);
|
||
toast.add({
|
||
severity: 'success',
|
||
summary: 'موفقیتآمیز',
|
||
detail: 'تنظیمات ذخیره خودکار با موفقیت ذخیره شد.',
|
||
life: 3000
|
||
});
|
||
} catch (e) {
|
||
toast.add({
|
||
severity: 'error',
|
||
summary: 'خطا',
|
||
detail: 'خطا در ذخیره تنظیمات ذخیره خودکار',
|
||
life: 3000
|
||
});
|
||
} finally {
|
||
autoSaveSaving.value = false;
|
||
}
|
||
};
|
||
|
||
const loadSeedStatus = async () => {
|
||
seedLoading.value = true;
|
||
try {
|
||
const data = await SettingService.getSeedStatus();
|
||
seedStatus.value = data || { isSeeded: false, seededAt: null };
|
||
} catch (e) {
|
||
console.warn('Failed to load seed status:', e);
|
||
} finally {
|
||
seedLoading.value = false;
|
||
}
|
||
};
|
||
|
||
const handleSeedDatabase = () => {
|
||
if (seedStatus.value.isSeeded) return;
|
||
|
||
confirm.require({
|
||
message:
|
||
'توجه: این عملیات دادههای اولیه سیستم (کاربران پیشفرض، نقشها، دستهبندیها، ویژگیهای تکمیلی و وضعیتهای املاک) را در پایگاه داده ایجاد میکند. این عملیات فقط یکبار قابل اجراست و پس از انجام دکمه برای همیشه قفل خواهد شد. آیا ادامه میدهید؟',
|
||
header: 'تایید مقداردهی اولیه پایگاه داده (Seed Database)',
|
||
icon: 'pi pi-exclamation-triangle',
|
||
rejectProps: { label: 'انصراف', severity: 'secondary', outlined: true },
|
||
acceptProps: { label: 'تایید و شروع مقداردهی', severity: 'primary' },
|
||
accept: async () => {
|
||
isSeeding.value = true;
|
||
try {
|
||
const res = await SettingService.seedDatabase();
|
||
seedStatus.value = {
|
||
isSeeded: true,
|
||
seededAt: res.seededAt || new Date()
|
||
};
|
||
toast.add({
|
||
severity: 'success',
|
||
summary: 'موفقیتآمیز',
|
||
detail: res.message || 'پایگاه داده با موفقیت مقداردهی شد و قفل گردید.',
|
||
life: 5000
|
||
});
|
||
await loadSettings();
|
||
} catch (err) {
|
||
const errMsg = err?.response?.data?.message || err.message || 'خطا در مقداردهی پایگاه داده';
|
||
toast.add({
|
||
severity: 'error',
|
||
summary: 'خطا در مقداردهی',
|
||
detail: errMsg,
|
||
life: 4000
|
||
});
|
||
await loadSeedStatus();
|
||
} finally {
|
||
isSeeding.value = false;
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
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>
|
||
|
||
<!-- ── Database Seeder Section (One-Time Execution) ── -->
|
||
<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 lg:flex-row lg:items-center justify-between gap-6">
|
||
<div class="flex items-start gap-4">
|
||
<div
|
||
class="w-14 h-14 rounded-2xl flex items-center justify-center text-2xl flex-shrink-0 transition-colors"
|
||
:class="seedStatus.isSeeded ? 'bg-surface-100 text-surface-500 dark:bg-surface-800 dark:text-surface-400' : 'bg-indigo-100 text-indigo-600 dark:bg-indigo-950 dark:text-indigo-400 shadow-sm'"
|
||
>
|
||
<i :class="seedStatus.isSeeded ? 'pi pi-lock' : 'pi pi-database'"></i>
|
||
</div>
|
||
<div>
|
||
<div class="flex items-center gap-2.5 flex-wrap">
|
||
<h3 class="text-xl font-bold m-0 text-surface-900 dark:text-surface-0">مقداردهی اولیه پایگاه داده (Seed Database)</h3>
|
||
<Tag v-if="seedStatus.isSeeded" value="مقداردهی شده و قفل است" severity="success" icon="pi pi-lock" rounded class="px-2.5 text-xs font-bold" />
|
||
<Tag v-else value="آماده مقداردهی (یکبار مصرف)" severity="warn" icon="pi pi-exclamation-circle" rounded class="px-2.5 text-xs font-bold" />
|
||
</div>
|
||
<p class="text-surface-500 dark:text-surface-400 m-0 mt-1.5 text-sm leading-relaxed max-w-2xl">
|
||
ایجاد خودکار کاربران اولیه، نقشها و سطوح دسترسی، دستهبندیهای املاک، ویژگیهای تکمیلی و وضعیتهای املاک در دیتابیس. این عملیات صرفاً یکبار قابل اجراست و پس از انجام دکمه برای همیشه قفل میشود.
|
||
</p>
|
||
<div v-if="seedStatus.isSeeded && seedStatus.seededAt" class="mt-2 text-xs text-surface-400 flex items-center gap-1.5 font-mono">
|
||
<i class="pi pi-calendar text-xs"></i>
|
||
<span>تاریخ اجرا: {{ new Date(seedStatus.seededAt).toLocaleString('fa-IR') }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-3 flex-shrink-0">
|
||
<Button v-if="!seedStatus.isSeeded" label="اجرای مقداردهی اولیه دادهها" icon="pi pi-database" severity="primary" rounded :loading="isSeeding" @click="handleSeedDatabase" class="px-5 font-semibold" />
|
||
<Button v-else label="پایگاه داده مقداردهی شده است" icon="pi pi-lock" severity="secondary" rounded disabled class="px-5 font-semibold opacity-70 cursor-not-allowed" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Auto-Save Settings Card ── -->
|
||
<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 lg:flex-row lg:items-center justify-between gap-6">
|
||
<div class="flex items-start gap-4">
|
||
<div class="w-14 h-14 rounded-2xl flex items-center justify-center text-2xl flex-shrink-0 bg-emerald-100 text-emerald-600 dark:bg-emerald-950 dark:text-emerald-400 shadow-sm">
|
||
<i class="pi pi-save"></i>
|
||
</div>
|
||
<div>
|
||
<div class="flex items-center gap-2.5 flex-wrap">
|
||
<h3 class="text-xl font-bold m-0 text-surface-900 dark:text-surface-0">تنظیمات ذخیره خودکار (Auto-Save)</h3>
|
||
<Tag :value="autoSaveSettings.enabled ? 'فعال' : 'غیرفعال'" :severity="autoSaveSettings.enabled ? 'success' : 'secondary'" rounded class="px-2.5 text-xs font-bold" />
|
||
</div>
|
||
<p class="text-surface-500 dark:text-surface-400 m-0 mt-1.5 text-sm leading-relaxed max-w-2xl">
|
||
پیکربندی ذخیره پسزمینه خودکار فرمها و املاک. در صورت فعال بودن، تغییرات شما بدون نیاز به فشردن دستی دکمه ذخیره، در بازههای مشخص ذخیره شده و اعلان آن در گوشه صفحه نمایش داده میشود.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex flex-wrap items-center gap-4 flex-shrink-0">
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-xs font-medium text-surface-600 dark:text-surface-300">فعال بودن:</span>
|
||
<ToggleSwitch v-model="autoSaveSettings.enabled" />
|
||
</div>
|
||
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-xs font-medium text-surface-600 dark:text-surface-300">بازه زمانی:</span>
|
||
<Dropdown v-model="autoSaveSettings.intervalSeconds" :options="autoSaveIntervalOptions" optionLabel="label" optionValue="value" :disabled="!autoSaveSettings.enabled" class="!rounded-xl text-xs w-44" />
|
||
</div>
|
||
|
||
<Button label="ذخیره تنظیمات" icon="pi pi-check" severity="primary" rounded size="small" :loading="autoSaveSaving" @click="saveAutoSaveSettings" class="px-4 font-semibold" />
|
||
</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>
|