logo set
This commit is contained in:
@@ -61,3 +61,23 @@ export function deleteBusinessImage(businessId: string, imageId: string) {
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function uploadBusinessLogoLight(businessId: string, file: File) {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('file', file);
|
||||||
|
return apiFormData<Business>(`/businesses/${businessId}/logo-light`, form);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadBusinessLogoDark(businessId: string, file: File) {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('file', file);
|
||||||
|
return apiFormData<Business>(`/businesses/${businessId}/logo-dark`, form);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeBusinessLogoLight(businessId: string) {
|
||||||
|
return api<Business>(`/businesses/${businessId}/logo-light`, { method: 'DELETE' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeBusinessLogoDark(businessId: string) {
|
||||||
|
return api<Business>(`/businesses/${businessId}/logo-dark`, { method: 'DELETE' });
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,21 +1,43 @@
|
|||||||
import { api, apiFormData } from './client';
|
import { api, apiFormData } from './client';␍
|
||||||
|
␍
|
||||||
export type PlatformSettings = {
|
export type PlatformSettings = {␍
|
||||||
loginHeroImageUrl: string | null;
|
loginHeroImageUrl: string | null;␍
|
||||||
};
|
logoLightUrl: string | null;␍
|
||||||
|
logoDarkUrl: string | null;␍
|
||||||
export function fetchPlatformSettings() {
|
};␍
|
||||||
return api<PlatformSettings>('/platform-settings');
|
␍
|
||||||
}
|
export function fetchPlatformSettings() {␍
|
||||||
|
return api<PlatformSettings>('/platform-settings');␍
|
||||||
export function uploadLoginHeroImage(file: File) {
|
}␍
|
||||||
const form = new FormData();
|
␍
|
||||||
form.append('file', file);
|
export function uploadLoginHeroImage(file: File) {␍
|
||||||
return apiFormData<PlatformSettings>('/platform-settings/login-hero-image', form);
|
const form = new FormData();␍
|
||||||
}
|
form.append('file', file);␍
|
||||||
|
return apiFormData<PlatformSettings>('/platform-settings/login-hero-image', form);␍
|
||||||
export function removeLoginHeroImage() {
|
}␍
|
||||||
return api<PlatformSettings>('/platform-settings/login-hero-image', {
|
␍
|
||||||
method: 'DELETE',
|
export function removeLoginHeroImage() {␍
|
||||||
});
|
return api<PlatformSettings>('/platform-settings/login-hero-image', {␍
|
||||||
}
|
method: 'DELETE',␍
|
||||||
|
});␍
|
||||||
|
}␍
|
||||||
|
␍
|
||||||
|
export function uploadPlatformLogoLight(file: File) {␍
|
||||||
|
const form = new FormData();␍
|
||||||
|
form.append('file', file);␍
|
||||||
|
return apiFormData<PlatformSettings>('/platform-settings/logo-light', form);␍
|
||||||
|
}␍
|
||||||
|
␍
|
||||||
|
export function uploadPlatformLogoDark(file: File) {␍
|
||||||
|
const form = new FormData();␍
|
||||||
|
form.append('file', file);␍
|
||||||
|
return apiFormData<PlatformSettings>('/platform-settings/logo-dark', form);␍
|
||||||
|
}␍
|
||||||
|
␍
|
||||||
|
export function removePlatformLogoLight() {␍
|
||||||
|
return api<PlatformSettings>('/platform-settings/logo-light', { method: 'DELETE' });␍
|
||||||
|
}␍
|
||||||
|
␍
|
||||||
|
export function removePlatformLogoDark() {␍
|
||||||
|
return api<PlatformSettings>('/platform-settings/logo-dark', { method: 'DELETE' });␍
|
||||||
|
}␍
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ export type Business = {
|
|||||||
ownerId?: string | null;
|
ownerId?: string | null;
|
||||||
category?: BusinessCategory;
|
category?: BusinessCategory;
|
||||||
images?: BusinessImage[];
|
images?: BusinessImage[];
|
||||||
|
logoLightUrl?: string | null;
|
||||||
|
logoDarkUrl?: string | null;
|
||||||
concurrentCapacity?: number;
|
concurrentCapacity?: number;
|
||||||
slotStepMinutes?: number;
|
slotStepMinutes?: number;
|
||||||
serviceClassesEnabled?: boolean;
|
serviceClassesEnabled?: boolean;
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<img
|
<img
|
||||||
v-if="variant === 'mark'"
|
v-if="variant === 'mark'"
|
||||||
src="/icons/icon-192.png"
|
:src="markSrc"
|
||||||
alt="منتایم"
|
:alt="alt"
|
||||||
:class="imgClass"
|
:class="imgClass"
|
||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<img
|
<img
|
||||||
src="/brand/logo-lite.png"
|
:src="lightSrc"
|
||||||
alt="منتایم"
|
:alt="alt"
|
||||||
:class="['dark:hidden', imgClass]"
|
:class="['dark:hidden', imgClass]"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
src="/brand/logo-dark.png"
|
:src="darkSrc"
|
||||||
alt="منتایم"
|
:alt="alt"
|
||||||
:class="['hidden dark:block', imgClass]"
|
:class="['hidden dark:block', imgClass]"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -21,16 +21,22 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { useAppLogoProps, type LogoVariant } from '@/components/brand/useAppLogo';
|
||||||
export type LogoVariant = 'auto' | 'mark' | 'horizontal' | 'vertical';
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
variant?: LogoVariant;
|
variant?: LogoVariant;
|
||||||
class?: string;
|
class?: string;
|
||||||
|
alt?: string;
|
||||||
|
lightSrc?: string;
|
||||||
|
darkSrc?: string;
|
||||||
}>(),
|
}>(),
|
||||||
{ variant: 'auto' },
|
{
|
||||||
|
variant: 'auto',
|
||||||
|
alt: 'منتایم',
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const imgClass = computed(() => props.class ?? 'h-8 w-auto object-contain');
|
const imgClass = computed(() => props.class ?? 'h-8 w-auto object-contain');
|
||||||
|
const { lightSrc, darkSrc, markSrc } = useAppLogoProps(props);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { computed, onMounted } from 'vue';
|
||||||
|
import { resolveLogoPair } from '@/utils/media';
|
||||||
|
import { usePlatformSettingsStore } from '@/stores/platform-settings';
|
||||||
|
|
||||||
|
export type LogoVariant = 'auto' | 'mark' | 'horizontal' | 'vertical';
|
||||||
|
|
||||||
|
export function useAppLogoProps(
|
||||||
|
props: {
|
||||||
|
variant?: LogoVariant;
|
||||||
|
lightSrc?: string;
|
||||||
|
darkSrc?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const platform = usePlatformSettingsStore();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void platform.load();
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolved = computed(() => {
|
||||||
|
if (props.lightSrc || props.darkSrc) {
|
||||||
|
return resolveLogoPair(props.lightSrc, props.darkSrc);
|
||||||
|
}
|
||||||
|
return platform.logoSources;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
lightSrc: computed(() => resolved.value.light),
|
||||||
|
darkSrc: computed(() => resolved.value.dark),
|
||||||
|
markSrc: computed(() => '/icons/icon-192.png'),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-xl border border-border bg-surface-2 p-3">
|
||||||
|
<p class="mb-2 text-xs font-medium text-muted">{{ label }}</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex aspect-[3/1] items-center justify-center overflow-hidden rounded-lg border border-border px-4 py-3"
|
||||||
|
:class="darkPreview ? 'bg-slate-900' : 'bg-white'"
|
||||||
|
>
|
||||||
|
<img :src="previewSrc" alt="" class="max-h-12 w-auto max-w-full object-contain" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
|
<label
|
||||||
|
class="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-brand-600 px-3 py-2 text-xs font-semibold text-white transition hover:bg-brand-700"
|
||||||
|
>
|
||||||
|
<span>{{ uploading ? 'در حال آپلود...' : 'آپلود' }}</span>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/jpeg,image/png,image/webp,image/gif"
|
||||||
|
class="hidden"
|
||||||
|
:disabled="uploading || removing"
|
||||||
|
@change="onPick"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="hasCustom"
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg border border-border px-3 py-2 text-xs text-muted transition hover:border-red-500/40 hover:text-red-600 disabled:opacity-50"
|
||||||
|
:disabled="uploading || removing"
|
||||||
|
@click="emit('remove')"
|
||||||
|
>
|
||||||
|
{{ removing ? 'در حال حذف...' : 'حذف' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
label: string;
|
||||||
|
previewSrc: string;
|
||||||
|
uploading?: boolean;
|
||||||
|
removing?: boolean;
|
||||||
|
hasCustom?: boolean;
|
||||||
|
darkPreview?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
pick: [file: File];
|
||||||
|
remove: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function onPick(event: Event) {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
input.value = '';
|
||||||
|
if (file) {
|
||||||
|
emit('pick', file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-text">لوگوی کسبوکار</label>
|
||||||
|
<p class="mt-1 text-xs text-muted">
|
||||||
|
اختیاری — JPG، PNG یا WebP تا ۵ مگابایت. اگر فقط یکی را بارگذاری کنید، در هر دو حالت روشن و تاریک نمایش داده میشود.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
|
<LogoUploadField
|
||||||
|
label="حالت روشن"
|
||||||
|
:preview-src="lightPreview"
|
||||||
|
:uploading="uploadingLight"
|
||||||
|
:removing="removingLight"
|
||||||
|
:has-custom="!!lightUrl"
|
||||||
|
@pick="(file) => emit('upload-light', file)"
|
||||||
|
@remove="emit('remove-light')"
|
||||||
|
/>
|
||||||
|
<LogoUploadField
|
||||||
|
label="حالت تاریک"
|
||||||
|
:preview-src="darkPreview"
|
||||||
|
:uploading="uploadingDark"
|
||||||
|
:removing="removingDark"
|
||||||
|
:has-custom="!!darkUrl"
|
||||||
|
dark-preview
|
||||||
|
@pick="(file) => emit('upload-dark', file)"
|
||||||
|
@remove="emit('remove-dark')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import LogoUploadField from '@/components/shared/LogoUploadField.vue';
|
||||||
|
import { resolveLogoPair } from '@/utils/media';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
lightUrl?: string | null;
|
||||||
|
darkUrl?: string | null;
|
||||||
|
fallbackLight?: string;
|
||||||
|
fallbackDark?: string;
|
||||||
|
uploadingLight?: boolean;
|
||||||
|
uploadingDark?: boolean;
|
||||||
|
removingLight?: boolean;
|
||||||
|
removingDark?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'upload-light': [file: File];
|
||||||
|
'upload-dark': [file: File];
|
||||||
|
'remove-light': [];
|
||||||
|
'remove-dark': [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const resolved = computed(() =>
|
||||||
|
resolveLogoPair(
|
||||||
|
props.lightUrl,
|
||||||
|
props.darkUrl,
|
||||||
|
props.fallbackLight,
|
||||||
|
props.fallbackDark,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const lightPreview = computed(() => resolved.value.light);
|
||||||
|
const darkPreview = computed(() => resolved.value.dark);
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { fetchPlatformSettings } from '@/api/platform-settings';
|
||||||
|
import {
|
||||||
|
DEFAULT_LOGO_DARK,
|
||||||
|
DEFAULT_LOGO_LIGHT,
|
||||||
|
resolveLogoPair,
|
||||||
|
} from '@/utils/media';
|
||||||
|
|
||||||
|
export const usePlatformSettingsStore = defineStore('platformSettings', () => {
|
||||||
|
const loginHeroImageUrl = ref<string | null>(null);
|
||||||
|
const logoLightUrl = ref<string | null>(null);
|
||||||
|
const logoDarkUrl = ref<string | null>(null);
|
||||||
|
const loaded = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const logoSources = computed(() =>
|
||||||
|
resolveLogoPair(logoLightUrl.value, logoDarkUrl.value),
|
||||||
|
);
|
||||||
|
|
||||||
|
async function load(force = false) {
|
||||||
|
if (loaded.value && !force) return;
|
||||||
|
if (loading.value) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await fetchPlatformSettings();
|
||||||
|
loginHeroImageUrl.value = data.loginHeroImageUrl;
|
||||||
|
logoLightUrl.value = data.logoLightUrl;
|
||||||
|
logoDarkUrl.value = data.logoDarkUrl;
|
||||||
|
loaded.value = true;
|
||||||
|
} catch {
|
||||||
|
loginHeroImageUrl.value = null;
|
||||||
|
logoLightUrl.value = null;
|
||||||
|
logoDarkUrl.value = null;
|
||||||
|
loaded.value = true;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loginHeroImageUrl,
|
||||||
|
logoLightUrl,
|
||||||
|
logoDarkUrl,
|
||||||
|
defaultLogoLight: DEFAULT_LOGO_LIGHT,
|
||||||
|
defaultLogoDark: DEFAULT_LOGO_DARK,
|
||||||
|
logoSources,
|
||||||
|
loaded,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -19,3 +19,32 @@ 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 const DEFAULT_LOGO_LIGHT = '/brand/logo-lite.png';
|
||||||
|
export const DEFAULT_LOGO_DARK = '/brand/logo-dark.png';
|
||||||
|
|
||||||
|
export type LogoSourcePair = {
|
||||||
|
light: string;
|
||||||
|
dark: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolveLogoPair(
|
||||||
|
lightUrl: string | null | undefined,
|
||||||
|
darkUrl: string | null | undefined,
|
||||||
|
fallbackLight = DEFAULT_LOGO_LIGHT,
|
||||||
|
fallbackDark = DEFAULT_LOGO_DARK,
|
||||||
|
): LogoSourcePair {
|
||||||
|
const light = uploadUrl(lightUrl);
|
||||||
|
const dark = uploadUrl(darkUrl);
|
||||||
|
|
||||||
|
if (light && dark) {
|
||||||
|
return { light, dark };
|
||||||
|
}
|
||||||
|
if (light && !dark) {
|
||||||
|
return { light, dark: light };
|
||||||
|
}
|
||||||
|
if (!light && dark) {
|
||||||
|
return { light: dark, dark };
|
||||||
|
}
|
||||||
|
return { light: fallbackLight, dark: fallbackDark };
|
||||||
|
}
|
||||||
|
|||||||
@@ -131,6 +131,20 @@
|
|||||||
@remove-existing="onRemoveImage"
|
@remove-existing="onRemoveImage"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<LogoUploadPair
|
||||||
|
v-if="editingId && detailBusiness"
|
||||||
|
:light-url="detailBusiness.logoLightUrl"
|
||||||
|
:dark-url="detailBusiness.logoDarkUrl"
|
||||||
|
:uploading-light="uploadingLogoLight"
|
||||||
|
:uploading-dark="uploadingLogoDark"
|
||||||
|
:removing-light="removingLogoLight"
|
||||||
|
:removing-dark="removingLogoDark"
|
||||||
|
@upload-light="onUploadLogoLight"
|
||||||
|
@upload-dark="onUploadLogoDark"
|
||||||
|
@remove-light="onRemoveLogoLight"
|
||||||
|
@remove-dark="onRemoveLogoDark"
|
||||||
|
/>
|
||||||
|
|
||||||
<BusinessServicesSection
|
<BusinessServicesSection
|
||||||
v-if="editingId && form.categoryId"
|
v-if="editingId && form.categoryId"
|
||||||
:business-id="editingId"
|
:business-id="editingId"
|
||||||
@@ -221,13 +235,18 @@ import {
|
|||||||
deleteBusinessImage,
|
deleteBusinessImage,
|
||||||
fetchBusiness,
|
fetchBusiness,
|
||||||
fetchBusinesses,
|
fetchBusinesses,
|
||||||
|
removeBusinessLogoDark,
|
||||||
|
removeBusinessLogoLight,
|
||||||
updateBusiness,
|
updateBusiness,
|
||||||
uploadBusinessImages,
|
uploadBusinessImages,
|
||||||
|
uploadBusinessLogoDark,
|
||||||
|
uploadBusinessLogoLight,
|
||||||
} from '@/api/businesses';
|
} from '@/api/businesses';
|
||||||
import { fetchUser, fetchUsers } from '@/api/users';
|
import { fetchUser, fetchUsers } from '@/api/users';
|
||||||
import type { Business, BusinessCategory, User } from '@/api/types';
|
import type { Business, BusinessCategory, User } from '@/api/types';
|
||||||
import { ApiError } from '@/api/client';
|
import { ApiError } from '@/api/client';
|
||||||
import BusinessImageUpload from '@/components/admin/BusinessImageUpload.vue';
|
import BusinessImageUpload from '@/components/admin/BusinessImageUpload.vue';
|
||||||
|
import LogoUploadPair from '@/components/shared/LogoUploadPair.vue';
|
||||||
import BusinessListCard from '@/components/admin/BusinessListCard.vue';
|
import BusinessListCard from '@/components/admin/BusinessListCard.vue';
|
||||||
import BusinessServicesSection from '@/components/admin/BusinessServicesSection.vue';
|
import BusinessServicesSection from '@/components/admin/BusinessServicesSection.vue';
|
||||||
import AppBadge from '@/components/ui/AppBadge.vue';
|
import AppBadge from '@/components/ui/AppBadge.vue';
|
||||||
@@ -265,6 +284,10 @@ const isEditMode = ref(false);
|
|||||||
const detailBusiness = ref<Business | null>(null);
|
const detailBusiness = ref<Business | null>(null);
|
||||||
const imageUploadRef = ref<InstanceType<typeof BusinessImageUpload> | null>(null);
|
const imageUploadRef = ref<InstanceType<typeof BusinessImageUpload> | null>(null);
|
||||||
const deletingImageId = ref<string | null>(null);
|
const deletingImageId = ref<string | null>(null);
|
||||||
|
const uploadingLogoLight = ref(false);
|
||||||
|
const uploadingLogoDark = ref(false);
|
||||||
|
const removingLogoLight = ref(false);
|
||||||
|
const removingLogoDark = ref(false);
|
||||||
|
|
||||||
const assignOpen = ref(false);
|
const assignOpen = ref(false);
|
||||||
const assignTarget = ref<Business | null>(null);
|
const assignTarget = ref<Business | null>(null);
|
||||||
@@ -460,6 +483,68 @@ async function onRemoveImage(imageId: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshDetailBusiness() {
|
||||||
|
if (!editingId.value) return;
|
||||||
|
detailBusiness.value = await fetchBusiness(editingId.value);
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onUploadLogoLight(file: File) {
|
||||||
|
if (!editingId.value) return;
|
||||||
|
uploadingLogoLight.value = true;
|
||||||
|
error.value = '';
|
||||||
|
try {
|
||||||
|
await uploadBusinessLogoLight(editingId.value, file);
|
||||||
|
await refreshDetailBusiness();
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e instanceof ApiError ? e.message : 'آپلود لوگو ناموفق بود';
|
||||||
|
} finally {
|
||||||
|
uploadingLogoLight.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onUploadLogoDark(file: File) {
|
||||||
|
if (!editingId.value) return;
|
||||||
|
uploadingLogoDark.value = true;
|
||||||
|
error.value = '';
|
||||||
|
try {
|
||||||
|
await uploadBusinessLogoDark(editingId.value, file);
|
||||||
|
await refreshDetailBusiness();
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e instanceof ApiError ? e.message : 'آپلود لوگو ناموفق بود';
|
||||||
|
} finally {
|
||||||
|
uploadingLogoDark.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRemoveLogoLight() {
|
||||||
|
if (!editingId.value) return;
|
||||||
|
removingLogoLight.value = true;
|
||||||
|
error.value = '';
|
||||||
|
try {
|
||||||
|
await removeBusinessLogoLight(editingId.value);
|
||||||
|
await refreshDetailBusiness();
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e instanceof ApiError ? e.message : 'حذف لوگو ناموفق بود';
|
||||||
|
} finally {
|
||||||
|
removingLogoLight.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRemoveLogoDark() {
|
||||||
|
if (!editingId.value) return;
|
||||||
|
removingLogoDark.value = true;
|
||||||
|
error.value = '';
|
||||||
|
try {
|
||||||
|
await removeBusinessLogoDark(editingId.value);
|
||||||
|
await refreshDetailBusiness();
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e instanceof ApiError ? e.message : 'حذف لوگو ناموفق بود';
|
||||||
|
} finally {
|
||||||
|
removingLogoDark.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onTitleBlur() {
|
function onTitleBlur() {
|
||||||
if (!isEditMode.value && !editingId.value) {
|
if (!isEditMode.value && !editingId.value) {
|
||||||
form.value.slug = slugify(form.value.title);
|
form.value.slug = slugify(form.value.title);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<section class="space-y-2">
|
<section class="space-y-2">
|
||||||
<h1 class="text-xl font-bold text-text">تنظیمات پلتفرم</h1>
|
<h1 class="text-xl font-bold text-text">تنظیمات پلتفرم</h1>
|
||||||
<p class="text-sm text-muted">
|
<p class="text-sm text-muted">
|
||||||
تصویر پسزمینه صفحه ورود دسکتاپ را از اینجا مدیریت کنید.
|
لوگوی اصلی پلتفرم و تصویر صفحه ورود دسکتاپ را از اینجا مدیریت کنید.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -21,6 +21,29 @@
|
|||||||
{{ success }}
|
{{ success }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<section class="mt-6 rounded-2xl border border-border bg-surface-2 p-4 lg:p-6">
|
||||||
|
<h2 class="text-base font-semibold text-text">لوگوی اصلی منتایم</h2>
|
||||||
|
<p class="mt-1 text-xs text-muted">
|
||||||
|
در صفحه اصلی و جاهایی که لوگوی کسبوکار تنظیم نشده نمایش داده میشود.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div v-if="loading" class="mt-4 text-sm text-muted">در حال بارگذاری...</div>
|
||||||
|
<LogoUploadPair
|
||||||
|
v-else
|
||||||
|
class="mt-4"
|
||||||
|
:light-url="settings?.logoLightUrl"
|
||||||
|
:dark-url="settings?.logoDarkUrl"
|
||||||
|
:uploading-light="uploadingLogoLight"
|
||||||
|
:uploading-dark="uploadingLogoDark"
|
||||||
|
:removing-light="removingLogoLight"
|
||||||
|
:removing-dark="removingLogoDark"
|
||||||
|
@upload-light="onUploadLogoLight"
|
||||||
|
@upload-dark="onUploadLogoDark"
|
||||||
|
@remove-light="onRemoveLogoLight"
|
||||||
|
@remove-dark="onRemoveLogoDark"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="mt-6 rounded-2xl border border-border bg-surface-2 p-4 lg:p-6">
|
<section class="mt-6 rounded-2xl border border-border bg-surface-2 p-4 lg:p-6">
|
||||||
<h2 class="text-base font-semibold text-text">تصویر صفحه ورود (دسکتاپ)</h2>
|
<h2 class="text-base font-semibold text-text">تصویر صفحه ورود (دسکتاپ)</h2>
|
||||||
<p class="mt-1 text-xs text-muted">
|
<p class="mt-1 text-xs text-muted">
|
||||||
@@ -85,21 +108,33 @@ import { computed, onMounted, ref } from 'vue';
|
|||||||
import {
|
import {
|
||||||
fetchPlatformSettings,
|
fetchPlatformSettings,
|
||||||
removeLoginHeroImage,
|
removeLoginHeroImage,
|
||||||
|
removePlatformLogoDark,
|
||||||
|
removePlatformLogoLight,
|
||||||
uploadLoginHeroImage,
|
uploadLoginHeroImage,
|
||||||
|
uploadPlatformLogoDark,
|
||||||
|
uploadPlatformLogoLight,
|
||||||
type PlatformSettings,
|
type PlatformSettings,
|
||||||
} from '@/api/platform-settings';
|
} from '@/api/platform-settings';
|
||||||
import { ApiError } from '@/api/client';
|
import { ApiError } from '@/api/client';
|
||||||
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 LogoUploadPair from '@/components/shared/LogoUploadPair.vue';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import { usePlatformSettingsStore } from '@/stores/platform-settings';
|
||||||
import { uploadUrl } from '@/utils/media';
|
import { uploadUrl } from '@/utils/media';
|
||||||
|
|
||||||
|
const platformStore = usePlatformSettingsStore();
|
||||||
|
|
||||||
const DEFAULT_PREVIEW = '/landing/hero-car-wash.jpg';
|
const DEFAULT_PREVIEW = '/landing/hero-car-wash.jpg';
|
||||||
|
|
||||||
const settings = ref<PlatformSettings | null>(null);
|
const settings = ref<PlatformSettings | null>(null);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const uploading = ref(false);
|
const uploading = ref(false);
|
||||||
const removing = ref(false);
|
const removing = ref(false);
|
||||||
|
const uploadingLogoLight = ref(false);
|
||||||
|
const uploadingLogoDark = ref(false);
|
||||||
|
const removingLogoLight = ref(false);
|
||||||
|
const removingLogoDark = ref(false);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const success = ref('');
|
const success = ref('');
|
||||||
|
|
||||||
@@ -157,6 +192,66 @@ async function onRemove() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onUploadLogoLight(file: File) {
|
||||||
|
uploadingLogoLight.value = true;
|
||||||
|
error.value = '';
|
||||||
|
success.value = '';
|
||||||
|
try {
|
||||||
|
settings.value = await uploadPlatformLogoLight(file);
|
||||||
|
await platformStore.load(true);
|
||||||
|
success.value = 'لوگوی حالت روشن بهروزرسانی شد.';
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e instanceof ApiError ? e.message : 'آپلود لوگو ناموفق بود.';
|
||||||
|
} finally {
|
||||||
|
uploadingLogoLight.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onUploadLogoDark(file: File) {
|
||||||
|
uploadingLogoDark.value = true;
|
||||||
|
error.value = '';
|
||||||
|
success.value = '';
|
||||||
|
try {
|
||||||
|
settings.value = await uploadPlatformLogoDark(file);
|
||||||
|
await platformStore.load(true);
|
||||||
|
success.value = 'لوگوی حالت تاریک بهروزرسانی شد.';
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e instanceof ApiError ? e.message : 'آپلود لوگو ناموفق بود.';
|
||||||
|
} finally {
|
||||||
|
uploadingLogoDark.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRemoveLogoLight() {
|
||||||
|
removingLogoLight.value = true;
|
||||||
|
error.value = '';
|
||||||
|
success.value = '';
|
||||||
|
try {
|
||||||
|
settings.value = await removePlatformLogoLight();
|
||||||
|
await platformStore.load(true);
|
||||||
|
success.value = 'لوگوی حالت روشن حذف شد.';
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e instanceof ApiError ? e.message : 'حذف لوگو ناموفق بود.';
|
||||||
|
} finally {
|
||||||
|
removingLogoLight.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRemoveLogoDark() {
|
||||||
|
removingLogoDark.value = true;
|
||||||
|
error.value = '';
|
||||||
|
success.value = '';
|
||||||
|
try {
|
||||||
|
settings.value = await removePlatformLogoDark();
|
||||||
|
await platformStore.load(true);
|
||||||
|
success.value = 'لوگوی حالت تاریک حذف شد.';
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e instanceof ApiError ? e.message : 'حذف لوگو ناموفق بود.';
|
||||||
|
} finally {
|
||||||
|
removingLogoDark.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void load();
|
void load();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user