diff --git a/src/api/businesses.ts b/src/api/businesses.ts
index 7b434b7..7824fd8 100644
--- a/src/api/businesses.ts
+++ b/src/api/businesses.ts
@@ -61,3 +61,23 @@ export function deleteBusinessImage(businessId: string, imageId: string) {
method: 'DELETE',
});
}
+
+export function uploadBusinessLogoLight(businessId: string, file: File) {
+ const form = new FormData();
+ form.append('file', file);
+ return apiFormData(`/businesses/${businessId}/logo-light`, form);
+}
+
+export function uploadBusinessLogoDark(businessId: string, file: File) {
+ const form = new FormData();
+ form.append('file', file);
+ return apiFormData(`/businesses/${businessId}/logo-dark`, form);
+}
+
+export function removeBusinessLogoLight(businessId: string) {
+ return api(`/businesses/${businessId}/logo-light`, { method: 'DELETE' });
+}
+
+export function removeBusinessLogoDark(businessId: string) {
+ return api(`/businesses/${businessId}/logo-dark`, { method: 'DELETE' });
+}
diff --git a/src/api/platform-settings.ts b/src/api/platform-settings.ts
index 3b6c905..c57bdb1 100644
--- a/src/api/platform-settings.ts
+++ b/src/api/platform-settings.ts
@@ -1,21 +1,43 @@
-import { api, apiFormData } from './client';
-
-export type PlatformSettings = {
- loginHeroImageUrl: string | null;
-};
-
-export function fetchPlatformSettings() {
- return api('/platform-settings');
-}
-
-export function uploadLoginHeroImage(file: File) {
- const form = new FormData();
- form.append('file', file);
- return apiFormData('/platform-settings/login-hero-image', form);
-}
-
-export function removeLoginHeroImage() {
- return api('/platform-settings/login-hero-image', {
- method: 'DELETE',
- });
-}
+import { api, apiFormData } from './client';
+
+export type PlatformSettings = {
+ loginHeroImageUrl: string | null;
+ logoLightUrl: string | null;
+ logoDarkUrl: string | null;
+};
+
+export function fetchPlatformSettings() {
+ return api('/platform-settings');
+}
+
+export function uploadLoginHeroImage(file: File) {
+ const form = new FormData();
+ form.append('file', file);
+ return apiFormData('/platform-settings/login-hero-image', form);
+}
+
+export function removeLoginHeroImage() {
+ return api('/platform-settings/login-hero-image', {
+ method: 'DELETE',
+ });
+}
+
+export function uploadPlatformLogoLight(file: File) {
+ const form = new FormData();
+ form.append('file', file);
+ return apiFormData('/platform-settings/logo-light', form);
+}
+
+export function uploadPlatformLogoDark(file: File) {
+ const form = new FormData();
+ form.append('file', file);
+ return apiFormData('/platform-settings/logo-dark', form);
+}
+
+export function removePlatformLogoLight() {
+ return api('/platform-settings/logo-light', { method: 'DELETE' });
+}
+
+export function removePlatformLogoDark() {
+ return api('/platform-settings/logo-dark', { method: 'DELETE' });
+}
diff --git a/src/api/types.ts b/src/api/types.ts
index 01ed1ea..88d84b0 100644
--- a/src/api/types.ts
+++ b/src/api/types.ts
@@ -73,6 +73,8 @@ export type Business = {
ownerId?: string | null;
category?: BusinessCategory;
images?: BusinessImage[];
+ logoLightUrl?: string | null;
+ logoDarkUrl?: string | null;
concurrentCapacity?: number;
slotStepMinutes?: number;
serviceClassesEnabled?: boolean;
diff --git a/src/components/brand/AppLogo.vue b/src/components/brand/AppLogo.vue
index e577274..e0005d0 100644
--- a/src/components/brand/AppLogo.vue
+++ b/src/components/brand/AppLogo.vue
@@ -1,19 +1,19 @@
@@ -21,16 +21,22 @@
diff --git a/src/components/brand/useAppLogo.ts b/src/components/brand/useAppLogo.ts
new file mode 100644
index 0000000..c9ea201
--- /dev/null
+++ b/src/components/brand/useAppLogo.ts
@@ -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'),
+ };
+}
diff --git a/src/components/shared/LogoUploadField.vue b/src/components/shared/LogoUploadField.vue
new file mode 100644
index 0000000..1e8622d
--- /dev/null
+++ b/src/components/shared/LogoUploadField.vue
@@ -0,0 +1,62 @@
+
+
+
{{ label }}
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/shared/LogoUploadPair.vue b/src/components/shared/LogoUploadPair.vue
new file mode 100644
index 0000000..a1abce1
--- /dev/null
+++ b/src/components/shared/LogoUploadPair.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+ اختیاری — JPG، PNG یا WebP تا ۵ مگابایت. اگر فقط یکی را بارگذاری کنید، در هر دو حالت روشن و تاریک نمایش داده میشود.
+
+
+
+
+ emit('upload-light', file)"
+ @remove="emit('remove-light')"
+ />
+ emit('upload-dark', file)"
+ @remove="emit('remove-dark')"
+ />
+
+
+
+
+
diff --git a/src/stores/platform-settings.ts b/src/stores/platform-settings.ts
new file mode 100644
index 0000000..cfecce7
--- /dev/null
+++ b/src/stores/platform-settings.ts
@@ -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(null);
+ const logoLightUrl = ref(null);
+ const logoDarkUrl = ref(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,
+ };
+});
diff --git a/src/utils/media.ts b/src/utils/media.ts
index 5f2a7e2..da724bf 100644
--- a/src/utils/media.ts
+++ b/src/utils/media.ts
@@ -19,3 +19,32 @@ export function businessCoverUrl(
const cover = images.find((i) => i.isCover) ?? images[0];
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 };
+}
diff --git a/src/views/BusinessesView.vue b/src/views/BusinessesView.vue
index 5c61981..3d6ff7d 100644
--- a/src/views/BusinessesView.vue
+++ b/src/views/BusinessesView.vue
@@ -131,6 +131,20 @@
@remove-existing="onRemoveImage"
/>
+
+
(null);
const imageUploadRef = ref | null>(null);
const deletingImageId = ref(null);
+const uploadingLogoLight = ref(false);
+const uploadingLogoDark = ref(false);
+const removingLogoLight = ref(false);
+const removingLogoDark = ref(false);
const assignOpen = ref(false);
const assignTarget = ref(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() {
if (!isEditMode.value && !editingId.value) {
form.value.slug = slugify(form.value.title);
diff --git a/src/views/PlatformSettingsView.vue b/src/views/PlatformSettingsView.vue
index bec6452..ce52639 100644
--- a/src/views/PlatformSettingsView.vue
+++ b/src/views/PlatformSettingsView.vue
@@ -3,7 +3,7 @@
تنظیمات پلتفرم
- تصویر پسزمینه صفحه ورود دسکتاپ را از اینجا مدیریت کنید.
+ لوگوی اصلی پلتفرم و تصویر صفحه ورود دسکتاپ را از اینجا مدیریت کنید.
@@ -21,6 +21,29 @@
{{ success }}
+
+ لوگوی اصلی منتایم
+
+ در صفحه اصلی و جاهایی که لوگوی کسبوکار تنظیم نشده نمایش داده میشود.
+
+
+ در حال بارگذاری...
+
+
+
تصویر صفحه ورود (دسکتاپ)
@@ -85,21 +108,33 @@ import { computed, onMounted, ref } from 'vue';
import {
fetchPlatformSettings,
removeLoginHeroImage,
+ removePlatformLogoDark,
+ removePlatformLogoLight,
uploadLoginHeroImage,
+ uploadPlatformLogoDark,
+ uploadPlatformLogoLight,
type PlatformSettings,
} from '@/api/platform-settings';
import { ApiError } from '@/api/client';
import AppButton from '@/components/ui/AppButton.vue';
import AppIcon from '@/components/ui/AppIcon.vue';
+import LogoUploadPair from '@/components/shared/LogoUploadPair.vue';
import AdminLayout from '@/layouts/AdminLayout.vue';
+import { usePlatformSettingsStore } from '@/stores/platform-settings';
import { uploadUrl } from '@/utils/media';
+const platformStore = usePlatformSettingsStore();
+
const DEFAULT_PREVIEW = '/landing/hero-car-wash.jpg';
const settings = ref(null);
const loading = ref(true);
const uploading = 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 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(() => {
void load();
});