update password
This commit is contained in:
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 |
@@ -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>
|
||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 = 'رمز عبور ذخیره شد.';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user