547 lines
16 KiB
Vue
547 lines
16 KiB
Vue
<script setup lang="ts">
|
|
import { ref } from 'vue';
|
|
import ToggleButton from './button/ToggleButton.vue';
|
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
|
|
|
defineProps<{
|
|
img?: string | null;
|
|
icon?: string;
|
|
title?: string;
|
|
caption?: string;
|
|
color?: string;
|
|
bgColor?: string;
|
|
toggleTitle?: string;
|
|
fallbackImg?: string;
|
|
fallbackCover?: string;
|
|
prefix: string;
|
|
|
|
hideFade?: boolean;
|
|
hideActive?: boolean;
|
|
noImageAction?: boolean;
|
|
active?: boolean;
|
|
readonly?: boolean;
|
|
useToggle?: boolean;
|
|
|
|
menu?: { icon: string; color: string; bgColor: string }[];
|
|
tabsList?: { name: string | number; label: string }[] | boolean;
|
|
}>();
|
|
|
|
defineEmits<{
|
|
(e: 'view'): void;
|
|
(e: 'edit'): void;
|
|
(e: 'update:toggleStatus', toggleStatus: string): void;
|
|
}>();
|
|
|
|
const coverUrl = defineModel<string>('coverUrl', {
|
|
required: false,
|
|
default: '',
|
|
});
|
|
|
|
const toggleStatus = defineModel<string>('toggleStatus', {
|
|
required: false,
|
|
default: 'CREATED',
|
|
});
|
|
|
|
const currentTab = defineModel<string | number>('currentTab');
|
|
|
|
const showOverlay = ref(false);
|
|
const smallBanner = ref(false);
|
|
</script>
|
|
<template>
|
|
<q-img
|
|
v-if="!smallBanner && $q.screen.gt.xs"
|
|
fit="cover"
|
|
class="cover rounded bordered relative-position"
|
|
:style="`height: ${tabsList ? '180px' : '10vw'}`"
|
|
:src="coverUrl || fallbackCover || '/blank-cover.png'"
|
|
@error="coverUrl = ''"
|
|
>
|
|
<nav
|
|
class="full-width full-height"
|
|
:style="`background-image: linear-gradient(
|
|
90deg, ${
|
|
$q.dark.isActive
|
|
? `hsla(var(--gray-10-hsl) / ${hideFade ? '0' : '1'}) 10%,hsla(var(--gray-10-hsl) / 0) 100%`
|
|
: `rgba(255, 255, 255, ${hideFade ? '0' : '1'}) 10%,rgba(255, 255, 255, 0) 100%`
|
|
}
|
|
)`"
|
|
>
|
|
<!-- profile -->
|
|
<div class="flex items-center full-height q-pl-lg" style="z-index: 1">
|
|
<div
|
|
class="surface-1"
|
|
style="border-radius: 50%; border: 4px solid var(--surface-1)"
|
|
>
|
|
<q-btn
|
|
class="absolute-top-right q-ma-xs"
|
|
color="primary"
|
|
flat
|
|
rounded
|
|
padding="4px"
|
|
size="sm"
|
|
icon="mdi-pin-outline"
|
|
style="transition: 0.1s ease-in-out"
|
|
:style="smallBanner ? 'rotate: 90deg' : ''"
|
|
@click="smallBanner = !smallBanner"
|
|
/>
|
|
|
|
<q-avatar
|
|
size="5vw"
|
|
font-size="3vw"
|
|
class="relative-position"
|
|
style="z-index: 1; box-shadow: var(--shadow-2)"
|
|
:style="{
|
|
color: `${color || 'white'}`,
|
|
cursor: `${noImageAction ? 'default' : 'pointer'}`,
|
|
}"
|
|
@mouseover="showOverlay = true"
|
|
@mouseleave="showOverlay = false"
|
|
@click.stop="$emit('view')"
|
|
>
|
|
<div
|
|
v-if="img"
|
|
class="full-width full-height"
|
|
:style="{
|
|
background: `${bgColor || 'var(--brand-1)'}`,
|
|
color: `${color || 'white'}`,
|
|
}"
|
|
>
|
|
<q-img id="profile-view" :src="img" :ratio="1">
|
|
<template #error>
|
|
<q-img
|
|
v-if="fallbackImg"
|
|
:src="fallbackImg"
|
|
:ratio="1"
|
|
style="background-color: transparent"
|
|
>
|
|
<template #error>
|
|
<div
|
|
class="full-width full-height flex items-center justify-center"
|
|
:style="{
|
|
background: `${bgColor || 'var(--brand-1)'}`,
|
|
color: `${color || 'white'}`,
|
|
}"
|
|
>
|
|
<Icon :icon="icon || 'mdi-account'" />
|
|
</div>
|
|
</template>
|
|
</q-img>
|
|
<div
|
|
v-else
|
|
class="full-width full-height flex items-center justify-center"
|
|
:style="{
|
|
background: `${bgColor || 'var(--brand-1)'}`,
|
|
color: `${color || 'white'}`,
|
|
}"
|
|
>
|
|
<Icon :icon="icon || 'mdi-account'" />
|
|
</div>
|
|
</template>
|
|
</q-img>
|
|
</div>
|
|
<div
|
|
v-else
|
|
class="full-width full-height flex items-center justify-center"
|
|
:style="{
|
|
background: `${bgColor || 'var(--brand-1)'}`,
|
|
color: `${color || 'white'}`,
|
|
}"
|
|
>
|
|
<Icon :icon="icon || 'mdi-account'" />
|
|
</div>
|
|
<q-badge
|
|
v-if="!hideActive"
|
|
class="absolute-bottom-right"
|
|
style="
|
|
border-radius: 50%;
|
|
width: 1.25vw;
|
|
height: 1.25vw;
|
|
z-index: 2;
|
|
background: var(--surface-1);
|
|
"
|
|
>
|
|
<q-badge
|
|
class="absolute-center"
|
|
style="border-radius: 50%; width: 0.8vw; height: 0.8vw"
|
|
:style="`background: hsl(var(${active ? '--positive-bg' : '--text-mute'}))`"
|
|
></q-badge>
|
|
</q-badge>
|
|
|
|
<Transition name="slide-fade">
|
|
<div
|
|
v-if="showOverlay && !readonly && !noImageAction"
|
|
class="absolute text-caption full-width full-height"
|
|
style="border-radius: 50% 50%; overflow: hidden"
|
|
:class="{ dark: $q.dark.isActive }"
|
|
>
|
|
<div
|
|
class="upload-overlay absolute-bottom flex items-center justify-center"
|
|
@click.stop="$emit('edit')"
|
|
>
|
|
{{ $t('general.editImage') }}
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</q-avatar>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- text -->
|
|
<div
|
|
class="absolute-bottom relative-position column text-cover justify-between no-wrap"
|
|
:class="{ dark: $q.dark.isActive }"
|
|
>
|
|
<div
|
|
class="row justify-between full-height"
|
|
style="padding-left: calc(6vw + 50px)"
|
|
>
|
|
<div class="col column">
|
|
<span
|
|
class="text-bold ellipsis q-pt-xs text-body1"
|
|
style="width: 90%"
|
|
>
|
|
{{ title }}
|
|
<q-tooltip anchor="bottom left" self="center left" :delay="300">
|
|
{{ title }}
|
|
</q-tooltip>
|
|
</span>
|
|
<span
|
|
v-if="title"
|
|
:class="$q.dark.isActive ? 'foreground' : 'app-text-muted'"
|
|
>
|
|
{{ caption }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- icon -->
|
|
<div
|
|
v-if="$q.screen.gt.xs"
|
|
class="row items-center q-pr-lg"
|
|
style="z-index: 1"
|
|
>
|
|
<span
|
|
v-if="useToggle && toggleTitle"
|
|
class="q-mr-md"
|
|
:class="$q.dark.isActive ? 'foreground' : 'app-text-muted-2'"
|
|
>
|
|
{{ toggleTitle }}
|
|
</span>
|
|
|
|
<ToggleButton
|
|
v-if="useToggle"
|
|
two-way
|
|
:model-value="toggleStatus !== 'INACTIVE'"
|
|
@click="$emit('update:toggleStatus', toggleStatus)"
|
|
/>
|
|
<q-separator
|
|
v-if="useToggle && menu"
|
|
vertical
|
|
class="q-my-lg"
|
|
spaced="lg"
|
|
style="background: hsl(var(--text-mute))"
|
|
/>
|
|
<div v-if="menu" class="q-gutter-x-sm">
|
|
<q-btn
|
|
v-for="(item, n) in menu"
|
|
:key="n"
|
|
flat
|
|
size="sm"
|
|
class="q-pa-xs rounded"
|
|
:icon="item.icon"
|
|
:style="`background-color: ${item.bgColor}; color: ${item.color}`"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row" v-if="tabsList && currentTab">
|
|
<div class="row q-px-sm full-width">
|
|
<q-tabs
|
|
dense
|
|
inline-label
|
|
mobile-arrows
|
|
v-model="currentTab"
|
|
:active-class="`active-tab text-weight-bold ${$q.dark.isActive && 'dark'}`"
|
|
class="app-text-muted full-width"
|
|
align="left"
|
|
v-if="typeof tabsList === 'object'"
|
|
>
|
|
<q-tab
|
|
v-for="tab in tabsList"
|
|
:id="`${prefix}-tab-${tab.label}`"
|
|
v-bind:key="tab.name"
|
|
class="content-tab text-capitalize"
|
|
:class="{ 'tab-label': currentTab !== tab.name }"
|
|
:name="tab.name"
|
|
:label="tab.label"
|
|
/>
|
|
</q-tabs>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
</q-img>
|
|
|
|
<!-- small -->
|
|
<q-img
|
|
v-if="(!$q.screen.gt.sm && smallBanner) || smallBanner || $q.screen.lt.sm"
|
|
fit="cover"
|
|
class="cover rounded bordered relative-position"
|
|
:style="`height: ${tabsList ? '100' : '45'}px`"
|
|
:src="coverUrl || fallbackCover || '/blank-cover.png'"
|
|
@error="coverUrl = ''"
|
|
>
|
|
<nav
|
|
class="full-width full-height column"
|
|
:style="`background-image: linear-gradient(
|
|
90deg, ${
|
|
$q.dark.isActive
|
|
? `hsla(var(--gray-10-hsl) / 1) 0%,hsla(var(--gray-10-hsl) / 0) 100%`
|
|
: `rgba(255, 255, 255, 1) 10%,rgba(255, 255, 255, 0) 100%`
|
|
}
|
|
)`"
|
|
>
|
|
<!-- profile -->
|
|
<span class="row col items-center">
|
|
<div
|
|
class="flex items-center full-height"
|
|
:class="{ 'q-pl-lg': $q.screen.gt.sm, 'q-pl-sm': $q.screen.lt.md }"
|
|
style="z-index: 1"
|
|
>
|
|
<div
|
|
class="surface-1"
|
|
style="border-radius: 50%; border: 2px solid var(--surface-1)"
|
|
>
|
|
<q-avatar
|
|
size="35px"
|
|
font-size="20px"
|
|
class="relative-position"
|
|
style="z-index: 1; box-shadow: var(--shadow-2)"
|
|
:style="{
|
|
color: `${color || 'white'}`,
|
|
cursor: `${noImageAction ? 'default' : 'pointer'}`,
|
|
}"
|
|
@mouseover="showOverlay = true"
|
|
@mouseleave="showOverlay = false"
|
|
@click.stop="$emit('view')"
|
|
>
|
|
<div
|
|
v-if="img"
|
|
class="full-width full-height"
|
|
:style="{
|
|
background: `${bgColor || 'var(--brand-1)'}`,
|
|
color: `${color || 'white'}`,
|
|
}"
|
|
>
|
|
<q-img id="profile-view" :src="img" :ratio="1">
|
|
<template #error>
|
|
<q-img
|
|
v-if="fallbackImg"
|
|
:src="fallbackImg"
|
|
:ratio="1"
|
|
style="background-color: transparent"
|
|
>
|
|
<template #error>
|
|
<div
|
|
class="full-width full-height flex items-center justify-center no-padding"
|
|
:style="{
|
|
background: `${bgColor || 'var(--brand-1)'}`,
|
|
color: `${color || 'white'}`,
|
|
}"
|
|
>
|
|
<Icon
|
|
class="full-width full-height flex items-center justify-center"
|
|
:icon="icon || 'mdi-account'"
|
|
style="width: 25px !important"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</q-img>
|
|
<div
|
|
v-else
|
|
class="full-width full-height flex items-center justify-center no-padding"
|
|
:style="{
|
|
background: `${bgColor || 'var(--brand-1)'}`,
|
|
color: `${color || 'white'}`,
|
|
}"
|
|
>
|
|
<Icon
|
|
class="full-width full-height flex items-center justify-center"
|
|
:icon="icon || 'mdi-account'"
|
|
style="width: 25px !important"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</q-img>
|
|
</div>
|
|
<div
|
|
v-else
|
|
class="full-width full-height flex items-center justify-center"
|
|
:style="{
|
|
background: `${bgColor || 'var(--brand-1)'}`,
|
|
color: `${color || 'white'}`,
|
|
}"
|
|
>
|
|
<Icon
|
|
class="full-width full-height flex items-center justify-center"
|
|
:icon="icon || 'mdi-account'"
|
|
style="width: 25px !important"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-if="!hideActive"
|
|
class="absolute-bottom-right"
|
|
style="
|
|
border-radius: 50%;
|
|
width: 10px;
|
|
height: 10px;
|
|
z-index: 2;
|
|
background: var(--surface-1);
|
|
"
|
|
>
|
|
<div
|
|
class="absolute-center"
|
|
style="border-radius: 50%; width: 8px; height: 8px"
|
|
:style="`background: hsl(var(${active ? '--positive-bg' : '--text-mute'}))`"
|
|
/>
|
|
</div>
|
|
</q-avatar>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col column full-height justify-center text-caption q-ml-md">
|
|
<span class="text-bold ellipsis" style="bottom: 16px">
|
|
{{ title }}
|
|
<q-tooltip anchor="bottom left" self="center left" :delay="300">
|
|
{{ title }}
|
|
</q-tooltip>
|
|
</span>
|
|
<span
|
|
v-if="title"
|
|
:class="$q.dark.isActive ? 'foreground' : 'app-text-muted'"
|
|
style="font-size: 10px; bottom: 4px"
|
|
>
|
|
{{ caption }}
|
|
</span>
|
|
</div>
|
|
</span>
|
|
|
|
<span class="row full-width" v-if="tabsList && currentTab">
|
|
<q-tabs
|
|
dense
|
|
inline-label
|
|
mobile-arrows
|
|
v-model="currentTab"
|
|
:active-class="`active-tab text-weight-bold ${$q.dark.isActive && 'dark'}`"
|
|
class="app-text-muted full-width"
|
|
align="left"
|
|
v-if="typeof tabsList === 'object'"
|
|
>
|
|
<q-tab
|
|
v-for="tab in tabsList"
|
|
:id="`${prefix}-tab-${tab.label}`"
|
|
v-bind:key="tab.name"
|
|
class="content-tab text-capitalize"
|
|
:class="{ 'tab-label': currentTab !== tab.name }"
|
|
:name="tab.name"
|
|
:label="tab.label"
|
|
/>
|
|
</q-tabs>
|
|
</span>
|
|
|
|
<q-btn
|
|
v-if="$q.screen.gt.xs"
|
|
class="absolute-top-right q-ma-xs"
|
|
color="primary"
|
|
flat
|
|
rounded
|
|
padding="4px"
|
|
size="sm"
|
|
icon="mdi-pin-outline"
|
|
style="transition: 0.1s ease-in-out"
|
|
:style="smallBanner ? 'rotate: 90deg' : ''"
|
|
@click="smallBanner = !smallBanner"
|
|
/>
|
|
</nav>
|
|
</q-img>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.cover {
|
|
overflow: hidden;
|
|
background-size: cover;
|
|
background-repeat: no-repeat;
|
|
background-position: top center;
|
|
}
|
|
|
|
.text-cover {
|
|
height: 50%;
|
|
background: linear-gradient(
|
|
90deg,
|
|
rgba(255, 255, 255, 1) 35%,
|
|
rgba(255, 255, 255, 0.5) 100%
|
|
);
|
|
backdrop-filter: blur(11.2px);
|
|
-webkit-backdrop-filter: blur(11.2px);
|
|
|
|
&.dark {
|
|
background: linear-gradient(
|
|
90deg,
|
|
hsla(var(--gray-10-hsl) / 1) 35%,
|
|
hsla(var(--gray-10-hsl) / 0.3) 100%
|
|
);
|
|
}
|
|
}
|
|
|
|
.avatar__status {
|
|
content: ' ';
|
|
display: block;
|
|
block-size: 1rem;
|
|
aspect-ratio: 1;
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
left: 6.5rem;
|
|
border: 1px solid var(--surface-1);
|
|
top: calc(80% - 1.2rem);
|
|
}
|
|
|
|
.upload-overlay {
|
|
top: 60%;
|
|
background-color: hsla(var(--gray-10-hsl) / 0.5);
|
|
color: white;
|
|
|
|
&.dark {
|
|
background-color: rgba(255, 255, 255, 0.2);
|
|
}
|
|
}
|
|
|
|
.slide-fade-enter-active {
|
|
transition: all 0.15s ease-out;
|
|
}
|
|
|
|
.slide-fade-leave-active {
|
|
transition: all 0.15s cubic-bezier(1, 0.5, 0.8, 1);
|
|
}
|
|
|
|
.slide-fade-enter-from,
|
|
.slide-fade-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
.q-img__content > nav {
|
|
pointer-events: all;
|
|
}
|
|
|
|
.active-tab {
|
|
color: var(--brand-1);
|
|
&.dark {
|
|
filter: brightness(1.3);
|
|
}
|
|
}
|
|
|
|
.tab-label {
|
|
color: var(--foreground);
|
|
opacity: 75%;
|
|
}
|
|
</style>
|