add skeletin

This commit is contained in:
2026-06-03 16:35:53 +03:30
parent 9e7aa905b7
commit b3dd977f04
16 changed files with 176 additions and 15 deletions
@@ -9,7 +9,7 @@
<p v-if="error" class="text-sm text-red-600">{{ error }}</p>
<div v-if="loading" class="text-sm text-muted">در حال بارگذاری خدمات…</div>
<ListCardSkeleton v-if="loading" :count="2" />
<ul v-else-if="services.length" class="space-y-2">
<li
@@ -69,6 +69,7 @@ import {
import { fetchServiceTemplates } from '@/api/service-templates';
import type { ServiceTemplate } from '@/api/types';
import { ApiError } from '@/api/client';
import ListCardSkeleton from '@/components/ui/ListCardSkeleton.vue';
import AppButton from '@/components/ui/AppButton.vue';
import AppIconButton from '@/components/ui/AppIconButton.vue';
import AppSelect from '@/components/ui/AppSelect.vue';
+13
View File
@@ -0,0 +1,13 @@
<template>
<div
class="animate-pulse rounded-lg bg-surface-3"
:class="klass"
aria-hidden="true"
/>
</template>
<script setup lang="ts">
const { class: klass } = defineProps<{
class?: string;
}>();
</script>
@@ -0,0 +1,41 @@
<template>
<ul :class="listClass" aria-hidden="true">
<li
v-for="i in count"
:key="i"
class="flex items-center gap-3 rounded-xl border border-border bg-surface-2 p-2.5"
>
<div class="min-w-0 flex-1 space-y-2">
<div class="flex items-start justify-between gap-2">
<AppSkeleton class="h-4 w-32" />
<AppSkeleton class="h-4 w-14 shrink-0 rounded-lg" />
</div>
<AppSkeleton class="h-3 w-full max-w-[80%]" />
<AppSkeleton class="h-3 w-24" />
</div>
<AppSkeleton :class="imageClass" class="shrink-0 rounded-lg" />
</li>
</ul>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import AppSkeleton from '@/components/ui/AppSkeleton.vue';
const props = withDefaults(
defineProps<{
count?: number;
listClass?: string;
size?: 'md' | 'lg';
}>(),
{
count: 4,
listClass: 'space-y-2',
size: 'md',
},
);
const imageClass = computed(() =>
props.size === 'lg' ? 'h-20 w-20' : 'h-14 w-14 sm:h-16 sm:w-16',
);
</script>
+15
View File
@@ -0,0 +1,15 @@
<template>
<div class="space-y-4" aria-hidden="true">
<AppSkeleton class="h-6 w-40" />
<AppSkeleton class="h-24 w-full rounded-2xl" />
<AppSkeleton class="h-12 w-full rounded-xl" />
<AppSkeleton class="h-24 w-full rounded-xl" />
<AppSkeleton class="h-12 w-full rounded-xl" />
<AppSkeleton class="h-12 w-full rounded-xl" />
<AppSkeleton class="h-11 w-full rounded-xl" />
</div>
</template>
<script setup lang="ts">
import AppSkeleton from '@/components/ui/AppSkeleton.vue';
</script>
+12
View File
@@ -0,0 +1,12 @@
<template>
<div class="space-y-3 rounded-xl border border-border bg-surface-3 p-3" aria-hidden="true">
<AppSkeleton class="h-4 w-24" />
<AppSkeleton class="h-5 w-40" />
<AppSkeleton class="h-4 w-32" />
<AppSkeleton class="h-6 w-16 rounded-lg" />
</div>
</template>
<script setup lang="ts">
import AppSkeleton from '@/components/ui/AppSkeleton.vue';
</script>
+37
View File
@@ -0,0 +1,37 @@
<template>
<ul :class="listClass" aria-hidden="true">
<li
v-for="i in count"
:key="i"
class="rounded-2xl border border-border bg-surface-2 p-4"
>
<div class="flex items-center justify-between gap-4">
<AppSkeleton class="h-5 w-36 max-w-[45%]" />
<AppSkeleton class="h-4 w-24 shrink-0" />
</div>
<div class="mt-2 flex items-center justify-between gap-2">
<div class="flex flex-wrap gap-2">
<AppSkeleton class="h-6 w-14 rounded-lg" />
<AppSkeleton class="h-6 w-12 rounded-lg" />
<AppSkeleton class="h-6 w-16 rounded-lg" />
</div>
<AppSkeleton class="h-9 w-9 shrink-0 rounded-full" />
</div>
</li>
</ul>
</template>
<script setup lang="ts">
import AppSkeleton from '@/components/ui/AppSkeleton.vue';
withDefaults(
defineProps<{
count?: number;
listClass?: string;
}>(),
{
count: 4,
listClass: 'space-y-2',
},
);
</script>
+19
View File
@@ -0,0 +1,19 @@
<template>
<ul class="grid grid-cols-2 gap-3" aria-hidden="true">
<li
v-for="i in count"
:key="i"
class="flex min-h-[7rem] flex-col rounded-xl border border-border bg-surface-2 p-3"
>
<AppSkeleton class="mx-auto h-4 w-3/4" />
<AppSkeleton class="mx-auto mt-2 h-3 w-1/2" />
<AppSkeleton class="mx-auto mt-auto h-3 w-16 pt-4" />
</li>
</ul>
</template>
<script setup lang="ts">
import AppSkeleton from '@/components/ui/AppSkeleton.vue';
withDefaults(defineProps<{ count?: number }>(), { count: 4 });
</script>
+15
View File
@@ -0,0 +1,15 @@
<template>
<div class="grid grid-cols-2 gap-3 lg:grid-cols-3" aria-hidden="true">
<AppSkeleton
v-for="i in count"
:key="i"
class="h-24 rounded-2xl"
/>
</div>
</template>
<script setup lang="ts">
import AppSkeleton from '@/components/ui/AppSkeleton.vue';
withDefaults(defineProps<{ count?: number }>(), { count: 5 });
</script>
+2 -1
View File
@@ -22,7 +22,7 @@
{{ error }}
</p>
<div v-if="loading" class="mt-4 text-sm text-muted">در حال بارگذاری…</div>
<ListCardSkeleton v-if="loading" list-class="mt-4 space-y-2" />
<EmptyState v-else-if="filteredBookings.length === 0" class="mt-4">
{{
@@ -66,6 +66,7 @@ import { deleteBooking, fetchBookings } from '@/api/bookings';
import { fetchBusinesses } from '@/api/businesses';
import type { Booking, BookingStatus, Business } from '@/api/types';
import { ApiError } from '@/api/client';
import ListCardSkeleton from '@/components/ui/ListCardSkeleton.vue';
import AppBadge from '@/components/ui/AppBadge.vue';
import AppIconButton from '@/components/ui/AppIconButton.vue';
import AppSelect from '@/components/ui/AppSelect.vue';
+2 -1
View File
@@ -28,7 +28,7 @@
{{ error }}
</p>
<div v-if="loading" class="mt-4 text-sm text-muted">در حال بارگذاری…</div>
<ListCardSkeleton v-if="loading" list-class="mt-4 space-y-2" />
<EmptyState v-else-if="requests.length === 0" class="mt-4">
درخواستی یافت نشد.
@@ -95,6 +95,7 @@ import {
import type { BusinessRegistrationRequest, BusinessRegistrationStatus } from '@/api/types';
import { ApiError } from '@/api/client';
import AppBadge from '@/components/ui/AppBadge.vue';
import ListCardSkeleton from '@/components/ui/ListCardSkeleton.vue';
import AppButton from '@/components/ui/AppButton.vue';
import AppModal from '@/components/ui/AppModal.vue';
import AppTextarea from '@/components/ui/AppTextarea.vue';
+4 -7
View File
@@ -30,7 +30,7 @@
{{ error }}
</p>
<div v-if="loading" class="mt-4 text-sm text-muted">در حال بارگذاری…</div>
<BusinessCardSkeleton v-if="loading" size="lg" list-class="mt-4 space-y-2" />
<EmptyState v-else-if="filteredBusinesses.length === 0" class="mt-4">
{{
@@ -116,12 +116,7 @@
<form v-if="assignTarget" class="space-y-4" @submit.prevent="onAssign">
<p class="text-sm font-medium text-text">{{ assignTarget.title }}</p>
<div
v-if="assignOwnerLoading"
class="rounded-xl border border-border bg-surface-3 px-4 py-3 text-sm text-muted"
>
در حال بارگذاری اطلاعات مدیر…
</div>
<InlineCardSkeleton v-if="assignOwnerLoading" />
<div
v-else-if="assignCurrentOwner"
@@ -203,6 +198,8 @@ import { ApiError } from '@/api/client';
import BusinessImageUpload from '@/components/admin/BusinessImageUpload.vue';
import BusinessServicesSection from '@/components/admin/BusinessServicesSection.vue';
import BusinessListCard from '@/components/admin/BusinessListCard.vue';
import BusinessCardSkeleton from '@/components/ui/BusinessCardSkeleton.vue';
import InlineCardSkeleton from '@/components/ui/InlineCardSkeleton.vue';
import AppBadge from '@/components/ui/AppBadge.vue';
import AppButton from '@/components/ui/AppButton.vue';
import AppIconButton from '@/components/ui/AppIconButton.vue';
+2 -1
View File
@@ -24,7 +24,7 @@
{{ error }}
</p>
<div v-if="loading" class="mt-4 text-sm text-muted">در حال بارگذاری…</div>
<ListCardSkeleton v-if="loading" list-class="mt-4 space-y-2" />
<EmptyState v-else-if="filteredCategories.length === 0" class="mt-4">
{{
@@ -92,6 +92,7 @@ import {
import type { BusinessCategory } from '@/api/types';
import { ApiError } from '@/api/client';
import AppBadge from '@/components/ui/AppBadge.vue';
import ListCardSkeleton from '@/components/ui/ListCardSkeleton.vue';
import AppButton from '@/components/ui/AppButton.vue';
import AppIconButton from '@/components/ui/AppIconButton.vue';
import AppInput from '@/components/ui/AppInput.vue';
+6 -1
View File
@@ -16,7 +16,10 @@
{{ error }}
</p>
<div v-if="loading" class="mt-6 text-sm text-muted">در حال بارگذاری…</div>
<div v-if="loading" class="mt-6 space-y-8">
<StatGridSkeleton />
<BusinessCardSkeleton size="lg" :count="3" />
</div>
<template v-else>
<div class="mt-6 grid grid-cols-2 gap-3 lg:grid-cols-3">
@@ -69,6 +72,8 @@ import { fetchBusinesses } from '@/api/businesses';
import { fetchUsers } from '@/api/users';
import type { Business } from '@/api/types';
import { ApiError } from '@/api/client';
import BusinessCardSkeleton from '@/components/ui/BusinessCardSkeleton.vue';
import StatGridSkeleton from '@/components/ui/StatGridSkeleton.vue';
import BusinessListCard from '@/components/admin/BusinessListCard.vue';
import StatCard from '@/components/admin/StatCard.vue';
import EmptyState from '@/components/ui/EmptyState.vue';
+2 -1
View File
@@ -28,7 +28,7 @@
{{ error }}
</p>
<div v-if="loading" class="mt-4 text-sm text-muted">در حال بارگذاری…</div>
<ListCardSkeleton v-if="loading" list-class="mt-4 space-y-2" />
<EmptyState v-else-if="requests.length === 0" class="mt-4">
درخواستی یافت نشد.
@@ -100,6 +100,7 @@ import {
import type { BusinessServiceRequest, BusinessServiceRequestStatus } from '@/api/types';
import { ApiError } from '@/api/client';
import AppBadge from '@/components/ui/AppBadge.vue';
import ListCardSkeleton from '@/components/ui/ListCardSkeleton.vue';
import AppButton from '@/components/ui/AppButton.vue';
import AppModal from '@/components/ui/AppModal.vue';
import AppTextarea from '@/components/ui/AppTextarea.vue';
+2 -1
View File
@@ -30,7 +30,7 @@
{{ error }}
</p>
<div v-if="loading" class="mt-4 text-sm text-muted">در حال بارگذاری…</div>
<ListCardSkeleton v-if="loading" list-class="mt-4 space-y-2" />
<EmptyState v-else-if="!selectedCategoryId" class="mt-4">
ابتدا یک دسته انتخاب کنید.
@@ -104,6 +104,7 @@ import {
import type { BusinessCategory, ServiceTemplate } from '@/api/types';
import { ApiError } from '@/api/client';
import AppBadge from '@/components/ui/AppBadge.vue';
import ListCardSkeleton from '@/components/ui/ListCardSkeleton.vue';
import AppButton from '@/components/ui/AppButton.vue';
import AppIconButton from '@/components/ui/AppIconButton.vue';
import AppInput from '@/components/ui/AppInput.vue';
+2 -1
View File
@@ -12,7 +12,7 @@
{{ error }}
</p>
<div v-if="loading" class="mt-4 text-sm text-muted">در حال بارگذاری…</div>
<ListCardSkeleton v-if="loading" list-class="mt-4 space-y-2" />
<EmptyState v-else-if="filteredUsers.length === 0" class="mt-4">
{{
@@ -104,6 +104,7 @@ import { fetchUsers, setUserPassword, updateUser } from '@/api/users';
import type { User, UserRole } from '@/api/types';
import { ApiError } from '@/api/client';
import AppBadge from '@/components/ui/AppBadge.vue';
import ListCardSkeleton from '@/components/ui/ListCardSkeleton.vue';
import AppButton from '@/components/ui/AppButton.vue';
import AppIconButton from '@/components/ui/AppIconButton.vue';
import AppInput from '@/components/ui/AppInput.vue';