feat: add loading screens, skeleton states, and table loaders across all list pages
deploy-chabokan / deploy (push) Canceled after 0s
deploy-chabokan / deploy (push) Canceled after 0s
This commit is contained in:
@@ -5,8 +5,22 @@ import { CategoryService } from '@/service/CategoryService';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
const toast = useToast();
|
||||
import axios from 'axios';
|
||||
|
||||
const dataLoading = ref(true);
|
||||
|
||||
const loadData = async () => {
|
||||
dataLoading.value = true;
|
||||
try {
|
||||
categories.value = await CategoryService.getAllCategories();
|
||||
} catch (err) {
|
||||
toast.add({ severity: 'error', summary: 'خطا', detail: 'خطا در دریافت اطلاعات', life: 3000 });
|
||||
} finally {
|
||||
dataLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
CategoryService.getAllCategories().then((data) => (categories.value = data));
|
||||
loadData();
|
||||
});
|
||||
|
||||
const dt = ref();
|
||||
@@ -21,6 +35,7 @@ const filters = ref({
|
||||
});
|
||||
const submitted = ref(false);
|
||||
|
||||
|
||||
function openNew() {
|
||||
category.value = {};
|
||||
submitted.value = false;
|
||||
@@ -115,6 +130,7 @@ function deleteSelectedCategories() {
|
||||
ref="dt"
|
||||
v-model:selection="selectedCategories"
|
||||
:value="categories"
|
||||
:loading="dataLoading"
|
||||
dataKey="id"
|
||||
:paginator="true"
|
||||
:rows="20"
|
||||
@@ -123,9 +139,22 @@ function deleteSelectedCategories() {
|
||||
:rowsPerPageOptions="[20, 40, 80]"
|
||||
currentPageReportTemplate="نمایش {first} تا {last} از {totalRecords} دستهبندی"
|
||||
>
|
||||
<template #loading>
|
||||
<div class="flex flex-col items-center justify-center p-8 gap-3">
|
||||
<ProgressSpinner style="width: 42px; height: 42px" strokeWidth="4" />
|
||||
<span class="text-sm font-medium text-surface-500">در حال بارگذاری دستهبندیها...</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div v-if="!dataLoading" class="p-8 text-center text-surface-500">
|
||||
<i class="pi pi-inbox text-4xl mb-2 text-surface-400"></i>
|
||||
<p>هیچ دستهبندی یافت نشد.</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #header>
|
||||
<div class="flex flex-wrap gap-2 items-center justify-between">
|
||||
<h4 class="m-0">مدیریت دسته بندی ها</h4>
|
||||
|
||||
<IconField>
|
||||
<InputIcon>
|
||||
<i class="pi pi-search" />
|
||||
|
||||
@@ -12,11 +12,23 @@ const selectedFeature = ref();
|
||||
const submitted = ref(false);
|
||||
const feature = ref({});
|
||||
const categories = ref([]);
|
||||
const featureDialog = ref(false);
|
||||
const dataLoading = ref(true);
|
||||
|
||||
onMounted(async () => {
|
||||
categories.value = await CategoryService.getAllCategories();
|
||||
dataLoading.value = true;
|
||||
try {
|
||||
categories.value = await CategoryService.getAllCategories();
|
||||
if (categories.value.length > 0 && !selectedTab.value) {
|
||||
selectedTab.value = categories.value[0]._id;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
dataLoading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const featureType = [
|
||||
{
|
||||
value: 'string',
|
||||
@@ -77,12 +89,24 @@ const saveFeature = async () => {
|
||||
<template>
|
||||
<div className="card">
|
||||
<div class="font-semibold text-xl mb-4">ویژگی دسته بندی ها</div>
|
||||
<Tabs :value="selectedTab" :pt="{}">
|
||||
|
||||
<div v-if="dataLoading" class="space-y-4">
|
||||
<div class="flex gap-2">
|
||||
<Skeleton v-for="i in 4" :key="i" width="8rem" height="2.5rem" class="rounded-xl" />
|
||||
</div>
|
||||
<div class="w-1/3 my-4 space-y-2">
|
||||
<Skeleton width="100%" height="15rem" class="rounded-xl" />
|
||||
<Skeleton width="6rem" height="2.5rem" class="rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs v-else :value="selectedTab" :pt="{}">
|
||||
<TabList>
|
||||
<Tab v-for="category in categories" :key="category._id" :value="category._id">
|
||||
{{ category.nativeName }}
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel v-for="category in categories" :key="category._id + '_content'" :value="category._id">
|
||||
<p>لیست ویژگی های {{ category.nativeName }}</p>
|
||||
|
||||
@@ -124,7 +124,10 @@ const onPageChange = (event) => {
|
||||
}
|
||||
};
|
||||
|
||||
const dataLoading = ref(true);
|
||||
|
||||
const loadData = async () => {
|
||||
dataLoading.value = true;
|
||||
try {
|
||||
if (searchMethod.value === null) {
|
||||
await loadSearchSetting();
|
||||
@@ -141,10 +144,13 @@ const loadData = async () => {
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: 'خطا', detail: 'خطا در بازیابی اطلاعات املاک', life: 3000 });
|
||||
console.error('Error loading data:', error);
|
||||
} finally {
|
||||
dataLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const loading = ref(false);
|
||||
const indexingFinished = ref(false);
|
||||
const indexingMessage = ref('در حال ایندکس...');
|
||||
@@ -322,6 +328,7 @@ const onCardHover = (propertyId) => {
|
||||
ref="dt"
|
||||
v-model:selection="selectedProperties"
|
||||
:value="properties"
|
||||
:loading="dataLoading"
|
||||
dataKey="_id"
|
||||
:paginator="true"
|
||||
:rows="per_page"
|
||||
@@ -335,12 +342,25 @@ const onCardHover = (propertyId) => {
|
||||
stripedRows
|
||||
responsiveLayout="scroll"
|
||||
>
|
||||
<template #loading>
|
||||
<div class="flex flex-col items-center justify-center p-8 gap-3">
|
||||
<ProgressSpinner style="width: 42px; height: 42px" strokeWidth="4" />
|
||||
<span class="text-sm font-medium text-surface-500">در حال بارگذاری اطلاعات املاک...</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div v-if="!dataLoading" class="p-8 text-center text-surface-500">
|
||||
<i class="pi pi-inbox text-4xl mb-2 text-surface-400"></i>
|
||||
<p>هیچ ملکی یافت نشد.</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #header>
|
||||
<div class="flex flex-wrap gap-2 items-center justify-between">
|
||||
<h4 class="m-0 text-lg font-bold">لیست املاک ({{ totalCount }} مورد)</h4>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<Column selectionMode="multiple" style="width: 3rem" :exportable="false"></Column>
|
||||
<Column header="تصویر" style="width: 5rem">
|
||||
<template #body="slotProps">
|
||||
@@ -423,8 +443,25 @@ const onCardHover = (propertyId) => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Loading Skeleton in Split View -->
|
||||
<div v-if="dataLoading" class="cards-grid">
|
||||
<div
|
||||
v-for="i in 6"
|
||||
:key="i"
|
||||
class="p-4 rounded-2xl border border-surface-200 dark:border-surface-700 bg-surface-0 dark:bg-surface-900 shadow-sm flex flex-col gap-3 animate-pulse"
|
||||
>
|
||||
<Skeleton width="100%" height="11rem" class="rounded-xl" />
|
||||
<Skeleton width="70%" height="1.25rem" class="rounded-md" />
|
||||
<Skeleton width="45%" height="1rem" class="rounded-md" />
|
||||
<div class="flex justify-between items-center pt-2 border-t border-surface-100 dark:border-surface-800">
|
||||
<Skeleton width="35%" height="1rem" class="rounded-md" />
|
||||
<Skeleton width="2rem" height="2rem" class="rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cards Grid -->
|
||||
<div v-if="properties.length > 0" class="cards-grid">
|
||||
<div v-else-if="properties.length > 0" class="cards-grid">
|
||||
<div
|
||||
v-for="prop in properties"
|
||||
:key="prop._id || prop.id"
|
||||
@@ -505,9 +542,11 @@ const onCardHover = (propertyId) => {
|
||||
</div>
|
||||
|
||||
<div v-else class="p-8 text-center text-slate-500">
|
||||
ملکی یافت نشد.
|
||||
<i class="pi pi-inbox text-4xl mb-2 text-surface-400"></i>
|
||||
<p>ملکی یافت نشد.</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Pagination -->
|
||||
<Paginator
|
||||
:rows="per_page"
|
||||
|
||||
@@ -11,8 +11,9 @@ const confirm = useConfirm();
|
||||
|
||||
const roles = ref([]);
|
||||
const allPermissions = ref([]);
|
||||
const loading = ref(false);
|
||||
const loading = ref(true);
|
||||
const showDialog = ref(false);
|
||||
|
||||
const editMode = ref(false);
|
||||
const submitting = ref(false);
|
||||
const selectedRole = ref(null);
|
||||
@@ -253,14 +254,22 @@ const confirmDelete = (r, event) => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #loading>
|
||||
<div class="flex flex-col items-center justify-center p-8 gap-3">
|
||||
<ProgressSpinner style="width: 42px; height: 42px" strokeWidth="4" />
|
||||
<span class="text-sm font-medium text-surface-500">در حال بارگذاری نقشها و دسترسیها...</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<div class="empty-state">
|
||||
<div v-if="!loading" class="empty-state">
|
||||
<i class="pi pi-shield empty-icon"></i>
|
||||
<p>هیچ نقشی تعریف نشده است</p>
|
||||
<Button label="افزودن اولین نقش" icon="pi pi-plus" @click="openCreate" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<!-- Native Name -->
|
||||
<Column field="nativeName" header="نام فارسی نقش" style="min-width: 180px">
|
||||
<template #body="{ data }">
|
||||
|
||||
@@ -121,7 +121,10 @@ const onPageChange = (event) => {
|
||||
}
|
||||
};
|
||||
|
||||
const dataLoading = ref(true);
|
||||
|
||||
const loadData = async () => {
|
||||
dataLoading.value = true;
|
||||
try {
|
||||
if (searchMethod.value === null) {
|
||||
await loadSearchSetting();
|
||||
@@ -138,10 +141,13 @@ const loadData = async () => {
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'خطا در دریافت اطلاعات', life: 3000 });
|
||||
console.error('Error loading data:', error);
|
||||
} finally {
|
||||
dataLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const loading = ref(false);
|
||||
const indexingFinished = ref(false);
|
||||
const indexingMessage = ref('در حال ایندکس...');
|
||||
@@ -277,6 +283,7 @@ const toggle = (event, data) => {
|
||||
ref="dt"
|
||||
v-model:selection="selectedTransactions"
|
||||
:value="transactions"
|
||||
:loading="dataLoading"
|
||||
dataKey="id"
|
||||
:paginator="true"
|
||||
:rows="per_page"
|
||||
@@ -292,6 +299,19 @@ const toggle = (event, data) => {
|
||||
filterDisplay="menu"
|
||||
:globalFilterFields="['type', 'status', 'belongingName', 'createdAt', 'description']"
|
||||
>
|
||||
<template #loading>
|
||||
<div class="flex flex-col items-center justify-center p-8 gap-3">
|
||||
<ProgressSpinner style="width: 42px; height: 42px" strokeWidth="4" />
|
||||
<span class="text-sm font-medium text-surface-500">در حال بارگذاری تراکنشها...</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div v-if="!dataLoading" class="p-8 text-center text-surface-500">
|
||||
<i class="pi pi-inbox text-4xl mb-2 text-surface-400"></i>
|
||||
<p>هیچ تراکنشی یافت نشد.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
|
||||
@@ -11,8 +11,9 @@ const confirm = useConfirm();
|
||||
|
||||
const users = ref([]);
|
||||
const roles = ref([]);
|
||||
const loading = ref(false);
|
||||
const loading = ref(true);
|
||||
const showDialog = ref(false);
|
||||
|
||||
const editMode = ref(false);
|
||||
const submitting = ref(false);
|
||||
const selectedUser = ref(null);
|
||||
@@ -266,14 +267,22 @@ const getInitials = (name, email) => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #loading>
|
||||
<div class="flex flex-col items-center justify-center p-8 gap-3">
|
||||
<ProgressSpinner style="width: 42px; height: 42px" strokeWidth="4" />
|
||||
<span class="text-sm font-medium text-surface-500">در حال بارگذاری کاربران...</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<div class="empty-state">
|
||||
<div v-if="!loading" class="empty-state">
|
||||
<i class="pi pi-user-minus empty-icon"></i>
|
||||
<p>هیچ کاربری یافت نشد</p>
|
||||
<Button label="افزودن اولین کاربر" icon="pi pi-plus" @click="openCreate" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<!-- User Column -->
|
||||
<Column header="کاربر" style="min-width: 200px">
|
||||
<template #body="{ data }">
|
||||
|
||||
@@ -11,8 +11,9 @@ const toast = useToast();
|
||||
|
||||
const transactions = ref([]);
|
||||
const totalCount = ref(0);
|
||||
const loading = ref(false);
|
||||
const loading = ref(true);
|
||||
const page = ref(1);
|
||||
|
||||
const perPage = ref(30);
|
||||
|
||||
// Filters
|
||||
@@ -196,13 +197,21 @@ const formatDate = (d) => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #loading>
|
||||
<div class="flex flex-col items-center justify-center p-8 gap-3">
|
||||
<ProgressSpinner style="width: 42px; height: 42px" strokeWidth="4" />
|
||||
<span class="text-sm font-medium text-surface-500">در حال بارگذاری تراکنشهای انبار...</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<div class="empty-state">
|
||||
<div v-if="!loading" class="empty-state">
|
||||
<i class="pi pi-inbox"></i>
|
||||
<p>هیچ تراکنشی یافت نشد</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<Column header="نوع" style="width:7%">
|
||||
<template #body="{ data }">
|
||||
<Tag
|
||||
|
||||
@@ -16,7 +16,8 @@ const totalCount = ref(0);
|
||||
const page = ref(Number(route.query.page) || 1);
|
||||
const perPage = ref(Number(route.query.per_page) || 20);
|
||||
const searchPhrase = ref(route.query.search || '');
|
||||
const loading = ref(false);
|
||||
const loading = ref(true);
|
||||
|
||||
|
||||
// Create/Edit dialog
|
||||
const showDialog = ref(false);
|
||||
@@ -217,14 +218,22 @@ const viewStock = (itemId) => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #loading>
|
||||
<div class="flex flex-col items-center justify-center p-8 gap-3">
|
||||
<ProgressSpinner style="width: 42px; height: 42px" strokeWidth="4" />
|
||||
<span class="text-sm font-medium text-surface-500">در حال بارگذاری اقلام انبار...</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<div class="empty-state">
|
||||
<div v-if="!loading" class="empty-state">
|
||||
<i class="pi pi-inbox empty-icon"></i>
|
||||
<p>هیچ قلمی یافت نشد</p>
|
||||
<Button label="افزودن اولین قلم" icon="pi pi-plus" @click="openCreateDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<Column field="code" header="کد" style="width:8%">
|
||||
<template #body="{ data }">
|
||||
<span class="code-badge" v-if="data.code">{{ data.code }}</span>
|
||||
|
||||
@@ -9,8 +9,9 @@ const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
|
||||
const warehouses = ref([]);
|
||||
const loading = ref(false);
|
||||
const loading = ref(true);
|
||||
const showDialog = ref(false);
|
||||
|
||||
const editMode = ref(false);
|
||||
const submitting = ref(false);
|
||||
const selectedWarehouse = ref(null);
|
||||
@@ -143,14 +144,22 @@ const onSearch = () => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #loading>
|
||||
<div class="flex flex-col items-center justify-center p-8 gap-3">
|
||||
<ProgressSpinner style="width: 42px; height: 42px" strokeWidth="4" />
|
||||
<span class="text-sm font-medium text-surface-500">در حال بارگذاری انبارها...</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<div class="empty-state">
|
||||
<div v-if="!loading" class="empty-state">
|
||||
<i class="pi pi-map empty-icon"></i>
|
||||
<p>هیچ انباری تعریف نشده است</p>
|
||||
<Button label="افزودن اولین انبار" icon="pi pi-plus" @click="openCreate" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<Column field="code" header="کد" style="width:8%">
|
||||
<template #body="{ data }">
|
||||
<span v-if="data.code" class="code-badge">{{ data.code }}</span>
|
||||
|
||||
Reference in New Issue
Block a user