update password

This commit is contained in:
2026-05-31 20:44:23 +03:30
parent 4eae73159a
commit f36a4af379
12 changed files with 142 additions and 31 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

-1
View File
@@ -51,7 +51,6 @@ export function loginWithPassword(mobile: string, password: string) {
export function setPassword(payload: { export function setPassword(payload: {
password: string; password: string;
confirmPassword: string; confirmPassword: string;
currentPassword?: string;
}) { }) {
return api<User>('/auth/set-password', { return api<User>('/auth/set-password', {
method: 'POST', method: 'POST',
@@ -0,0 +1,30 @@
<template>
<div class="flex flex-col items-center gap-2 text-center">
<img
src="/brand/welcome-dark.png"
alt="خوش آمدید"
:class="['dark:hidden', imgClass]"
/>
<img
src="/brand/welcome-lite.png"
alt="خوش آمدید"
:class="['hidden dark:block', imgClass]"
/>
<p class="max-w-xs text-sm leading-7 text-muted">
نوبت گیری سریع ، مدیرت آسان زمان
</p>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = withDefaults(
defineProps<{
class?: string;
}>(),
{},
);
const imgClass = computed(() => props.class ?? 'h-9 w-auto object-contain');
</script>
@@ -0,0 +1,85 @@
<template>
<div class="relative -mx-4 overflow-hidden">
<div
v-if="images.length"
ref="trackRef"
class="flex snap-x snap-mandatory overflow-x-auto scroll-smooth"
dir="ltr"
@scroll.passive="updateActiveIndex"
>
<img
v-for="(src, index) in images"
:key="`${src}-${index}`"
:src="src"
:alt="`${title} — تصویر ${index + 1}`"
class="aspect-[4/3] w-full shrink-0 snap-center object-cover"
loading="lazy"
/>
</div>
<div
v-else
class="flex aspect-[4/3] items-center justify-center bg-surface-3 text-muted/40"
>
<AppIcon name="store" size="lg" />
</div>
<template v-if="images.length > 1">
<div
class="pointer-events-none absolute inset-x-0 bottom-3 flex justify-center gap-1.5"
>
<span
v-for="(_, index) in images"
:key="index"
class="h-1.5 rounded-full transition-all"
:class="
index === activeIndex
? 'w-4 bg-white shadow-sm'
: 'w-1.5 bg-white/60'
"
/>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import AppIcon from '@/components/ui/AppIcon.vue';
import { businessGalleryUrls } from '@/utils/media';
import type { BusinessImage } from '@/api/types';
const props = defineProps<{
images?: BusinessImage[] | null;
title?: string;
}>();
const trackRef = ref<HTMLElement | null>(null);
const activeIndex = ref(0);
const images = computed(() => businessGalleryUrls(props.images));
function updateActiveIndex() {
const track = trackRef.value;
if (!track || !images.value.length) {
return;
}
const width = track.clientWidth;
if (!width) {
return;
}
activeIndex.value = Math.min(
images.value.length - 1,
Math.max(0, Math.round(track.scrollLeft / width)),
);
}
watch(images, () => {
activeIndex.value = 0;
if (trackRef.value) {
trackRef.value.scrollLeft = 0;
}
});
</script>
-7
View File
@@ -76,13 +76,6 @@ const navItems = computed((): NavItem[] => {
label: 'کسب‌وکار', label: 'کسب‌وکار',
icon: 'store', icon: 'store',
}); });
} else if (auth.isCustomer) {
items.splice(2, 0, {
to: '/app/register-business',
name: 'app-register-business',
label: 'ثبت کسب‌وکار',
icon: 'plus',
});
} }
return items; return items;
}); });
+15
View File
@@ -19,3 +19,18 @@ export function businessCoverUrl(
const cover = images.find((i) => i.isCover) ?? images[0]; const cover = images.find((i) => i.isCover) ?? images[0];
return uploadUrl(cover?.url) || null; return uploadUrl(cover?.url) || null;
} }
export function businessGalleryUrls(
images?: { url: string; isCover?: boolean; sortOrder?: number }[] | null,
): string[] {
if (!images?.length) return [];
return [...images]
.sort((a, b) => {
if (a.isCover !== b.isCover) {
return a.isCover ? -1 : 1;
}
return (a.sortOrder ?? 0) - (b.sortOrder ?? 0);
})
.map((image) => uploadUrl(image.url))
.filter(Boolean);
}
+10 -8
View File
@@ -18,6 +18,8 @@
بازگشت بازگشت
</button> </button>
<BusinessImageGallery :images="business.images" :title="business.title" />
<div <div
v-if="!business.isActive" v-if="!business.isActive"
class="rounded-xl border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-amber-800 dark:text-amber-300" class="rounded-xl border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-amber-800 dark:text-amber-300"
@@ -67,30 +69,29 @@
از بخش «کسب‌وکار» در منو، خدمات و قیمت را اضافه کنید. از بخش «کسب‌وکار» در منو، خدمات و قیمت را اضافه کنید.
</span> </span>
</div> </div>
<ul v-else class="space-y-3"> <ul v-else class="grid grid-cols-2 gap-3">
<li <li
v-for="svc in services" v-for="svc in services"
:key="svc.id" :key="svc.id"
class="rounded-2xl border border-border bg-surface-2 p-4" class="flex flex-col items-center rounded-2xl border border-border bg-surface-2 p-3 text-center"
> >
<p class="font-medium text-text"> <p class="font-medium text-text">
{{ svc.serviceTemplate?.title ?? 'خدمت' }} {{ svc.serviceTemplate?.title ?? 'خدمت' }}
</p> </p>
<p <p
v-if="svc.serviceTemplate?.description" v-if="svc.serviceTemplate?.description"
class="mt-1 text-sm text-muted" class="mt-1 line-clamp-2 text-xs text-muted"
> >
{{ svc.serviceTemplate.description }} {{ svc.serviceTemplate.description }}
</p> </p>
<div class="mt-2 flex flex-wrap gap-3 text-sm text-muted"> <div class="mt-auto space-y-0.5 pt-2 text-xs text-muted">
<span>{{ formatPriceToman(svc.price) }} تومان</span> <p>{{ formatPriceToman(svc.price) }} تومان</p>
<span>·</span> <p>{{
<span>{{
formatDuration( formatDuration(
svc.durationMinutes, svc.durationMinutes,
svc.serviceTemplate?.defaultDurationMinutes, svc.serviceTemplate?.defaultDurationMinutes,
) )
}}</span> }}</p>
</div> </div>
<AppButton <AppButton
v-if="canBook" v-if="canBook"
@@ -165,6 +166,7 @@ import { ApiError } from '@/api/client';
import type { Business, BusinessService } from '@/api/types'; import type { Business, BusinessService } from '@/api/types';
import AppButton from '@/components/ui/AppButton.vue'; import AppButton from '@/components/ui/AppButton.vue';
import AppIcon from '@/components/ui/AppIcon.vue'; import AppIcon from '@/components/ui/AppIcon.vue';
import BusinessImageGallery from '@/components/business/BusinessImageGallery.vue';
import BookingSlotPicker from '@/components/booking/BookingSlotPicker.vue'; import BookingSlotPicker from '@/components/booking/BookingSlotPicker.vue';
import AppModal from '@/components/ui/AppModal.vue'; import AppModal from '@/components/ui/AppModal.vue';
import WebAppLayout from '@/layouts/WebAppLayout.vue'; import WebAppLayout from '@/layouts/WebAppLayout.vue';
-11
View File
@@ -64,14 +64,6 @@
پس از تنظیم رمز، ورود با رمز عبور یا کد یکبارمصرف هر دو ممکن است. پس از تنظیم رمز، ورود با رمز عبور یا کد یکبارمصرف هر دو ممکن است.
</p> </p>
<AppInput
v-if="auth.user.hasPassword"
id="currentPassword"
v-model="currentPassword"
label="رمز فعلی"
type="password"
ltr
/>
<AppInput id="newPassword" v-model="newPassword" label="رمز جدید" type="password" ltr /> <AppInput id="newPassword" v-model="newPassword" label="رمز جدید" type="password" ltr />
<AppInput <AppInput
id="confirmPassword" id="confirmPassword"
@@ -98,7 +90,6 @@ import WebAppLayout from '@/layouts/WebAppLayout.vue';
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
const auth = useAuthStore(); const auth = useAuthStore();
const currentPassword = ref('');
const newPassword = ref(''); const newPassword = ref('');
const confirmPassword = ref(''); const confirmPassword = ref('');
const saving = ref(false); const saving = ref(false);
@@ -135,10 +126,8 @@ async function onSavePassword() {
const updated = await setPassword({ const updated = await setPassword({
password: newPassword.value, password: newPassword.value,
confirmPassword: confirmPassword.value, confirmPassword: confirmPassword.value,
...(auth.user.hasPassword ? { currentPassword: currentPassword.value } : {}),
}); });
auth.setUser(updated); auth.setUser(updated);
currentPassword.value = '';
newPassword.value = ''; newPassword.value = '';
confirmPassword.value = ''; confirmPassword.value = '';
message.value = 'رمز عبور ذخیره شد.'; message.value = 'رمز عبور ذخیره شد.';
+2 -4
View File
@@ -3,10 +3,7 @@
<div class="flex flex-1 flex-col justify-start gap-8 pt-10"> <div class="flex flex-1 flex-col justify-start gap-8 pt-10">
<section class="flex flex-col items-center text-center"> <section class="flex flex-col items-center text-center">
<AppLogo variant="vertical" class="mb-3 h-28 w-auto object-contain" /> <AppLogo variant="vertical" class="mb-3 h-28 w-auto object-contain" />
<p class="max-w-xs text-sm leading-7 text-muted"> <AppWelcomeMessage />
نوبت‌دهی آنلاین برای مشتریان<br />
مدیریت ساده برای کسب‌وکار شما
</p>
</section> </section>
<section class="flex flex-col gap-4"> <section class="flex flex-col gap-4">
@@ -69,6 +66,7 @@ import type { AuthLoginMode } from '@/components/auth/AuthModeSwitch.vue';
import AppButton from '@/components/ui/AppButton.vue'; import AppButton from '@/components/ui/AppButton.vue';
import AppInput from '@/components/ui/AppInput.vue'; import AppInput from '@/components/ui/AppInput.vue';
import AppLogo from '@/components/brand/AppLogo.vue'; import AppLogo from '@/components/brand/AppLogo.vue';
import AppWelcomeMessage from '@/components/brand/AppWelcomeMessage.vue';
import AuthLayout from '@/layouts/AuthLayout.vue'; import AuthLayout from '@/layouts/AuthLayout.vue';
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';