This commit is contained in:
2026-07-16 13:34:02 +03:30
parent a3d2764a4e
commit cc998e2cfe
15 changed files with 482 additions and 58 deletions
+15 -9
View File
@@ -1,19 +1,19 @@
<template>
<img
v-if="variant === 'mark'"
src="/icons/icon-192.png"
alt="منتایم"
:src="markSrc"
:alt="alt"
:class="imgClass"
/>
<template v-else>
<img
src="/brand/logo-lite.png"
alt="منتایم"
:src="lightSrc"
:alt="alt"
:class="['dark:hidden', imgClass]"
/>
<img
src="/brand/logo-dark.png"
alt="منتایم"
:src="darkSrc"
:alt="alt"
:class="['hidden dark:block', imgClass]"
/>
</template>
@@ -21,16 +21,22 @@
<script setup lang="ts">
import { computed } from 'vue';
export type LogoVariant = 'auto' | 'mark' | 'horizontal' | 'vertical';
import { useAppLogoProps, type LogoVariant } from '@/components/brand/useAppLogo';
const props = withDefaults(
defineProps<{
variant?: LogoVariant;
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 { lightSrc, darkSrc, markSrc } = useAppLogoProps(props);
</script>
+9 -1
View File
@@ -3,7 +3,13 @@
<div
class="flex h-11 w-11 shrink-0 items-center justify-center overflow-hidden rounded-full bg-white p-1.5 shadow-sm ring-1 ring-slate-200 dark:bg-slate-800 dark:ring-slate-600"
>
<AppLogo variant="horizontal" class="h-full w-full object-contain" />
<AppLogo
variant="horizontal"
class="h-full w-full object-contain"
:light-src="activeLogos.light"
:dark-src="activeLogos.dark"
:alt="brand.name"
/>
</div>
<span class="text-lg font-bold text-slate-800 dark:text-slate-100">
{{ brand.name }}
@@ -14,8 +20,10 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import AppLogo from '@/components/brand/AppLogo.vue';
import { useLogoSources } from '@/composables/useLogoSources';
import { useTenantStore } from '@/stores/tenant';
const tenant = useTenantStore();
const { brand } = storeToRefs(tenant);
const { activeLogos } = useLogoSources();
</script>
+34
View File
@@ -0,0 +1,34 @@
import { computed, onMounted } from 'vue';
import { resolveLogoPair, DEFAULT_LOGO_LIGHT, DEFAULT_LOGO_DARK } 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'),
};
}
export { DEFAULT_LOGO_LIGHT, DEFAULT_LOGO_DARK };
+62
View File
@@ -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>
+68
View File
@@ -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>