v11
This commit is contained in:
BIN
i18n-2.xlsm
BIN
i18n-2.xlsm
Binary file not shown.
2213
package-lock.json
generated
2213
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -20,15 +20,16 @@
|
||||
"axios": "^1.2.1",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"pinia": "^2.0.11",
|
||||
"quasar": "^2.18.2",
|
||||
"vue": "^3.4.18",
|
||||
"quasar": "^2.18.6",
|
||||
"sass-embedded": "^1.97.2",
|
||||
"vue": "^3.5.27",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@intlify/unplugin-vue-i18n": "^2.0.0",
|
||||
"@quasar/app-vite": "^2.3.0",
|
||||
"@quasar/app-vite": "^2.4.0",
|
||||
"@quasar/quasar-app-extension-qmarkdown": "^2.0.5",
|
||||
"@twa-dev/types": "^8.0.2",
|
||||
"@types/node": "^20.17.30",
|
||||
|
||||
11
src/Global-properties.d.ts
vendored
Normal file
11
src/Global-properties.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { WebApp } from "@twa-dev/types"
|
||||
|
||||
declare module 'vue' {
|
||||
export interface ComponentCustomProperties {
|
||||
$tg: WebApp;
|
||||
$isDesktop: boolean;
|
||||
$isMobile: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
@@ -1,41 +0,0 @@
|
||||
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.x { fill: white; transform-box: fill-box; animation: expand-r 6s ease-in-out infinite;}
|
||||
@keyframes expand-r { 50% { r: 8px; } }
|
||||
|
||||
.c { fill: white; transform-box: fill-box; animation: o 6s ease-in-out infinite; }
|
||||
@keyframes o { 50% { transform: var(--t); } }
|
||||
|
||||
.l {
|
||||
stroke: white;
|
||||
stroke-dasharray: 1;
|
||||
stroke-dashoffset: 1;
|
||||
stroke-width: 1.5;
|
||||
animation: draw 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes draw {
|
||||
0%, 100% { stroke-dashoffset: 0; }
|
||||
50% { stroke-dashoffset: 1; }
|
||||
}
|
||||
</style>
|
||||
<defs>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
|
||||
<stop stop-color="#2AABEE" offset="0%"></stop>
|
||||
<stop stop-color="#229ED9" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
|
||||
<rect width="32" height="32" rx="4" ry="4" fill="url(#linearGradient-1)" />
|
||||
<circle class="x" cx="16" cy="16" r="6" style="--t:scale(1.2)" />
|
||||
<circle class="c" cx="7" cy="7" r="3" style="--t:translate(9px,9px)"/>
|
||||
<circle class="c" cx="28" cy="4" r="2" style="--t:translate(-12px,12px)"/>
|
||||
<circle class="c" cx="6" cy="26" r="4" style="--t:translate(10px,-10px)"/>
|
||||
<circle class="c" cx="26" cy="26" r="5" style="--t:translate(-10px,-10px)"/>
|
||||
<line class="l" x1="16" y1="16" x2="7" y2="7" pathLength="1"/>
|
||||
<line class="l" x1="16" y1="16" x2="28" y2="4" pathLength="1"/>
|
||||
<line class="l" x1="16" y1="16" x2="6" y2="26" pathLength="1"/>
|
||||
<line class="l" x1="16" y1="16" x2="26" y2="26" pathLength="1"/>
|
||||
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,6 +1,7 @@
|
||||
import { boot } from 'quasar/wrappers'
|
||||
import pnPageWrapper from 'components/pnPageWrapper.vue'
|
||||
import pnPageCard from 'components/pnPageCard.vue'
|
||||
import pnScrollList from 'components/pnScrollList.vue'
|
||||
import pnPageTemplate from 'components/pnPageTemplate.vue'
|
||||
import pnAutoAvatar from 'components/pnAutoAvatar.vue'
|
||||
import pnOverlay from 'components/pnOverlay.vue'
|
||||
import pnSmallDialog from 'components/pnSmallDialog.vue'
|
||||
@@ -9,10 +10,12 @@ import pnShadowScroll from 'components/pnShadowScroll.vue'
|
||||
import pnMagicOverlay from 'components/pnMagicOverlay.vue'
|
||||
import pnAccountBlockName from 'components/pnAccountBlockName.vue'
|
||||
import pnOnboardBtn from 'components/pnOnboardBtn.vue'
|
||||
import pnBottomSheetDialog from 'components/pnBottomSheetDialog.vue'
|
||||
|
||||
const components = {
|
||||
pnPageWrapper,
|
||||
pnPageCard,
|
||||
pnScrollList,
|
||||
pnPageTemplate,
|
||||
pnAutoAvatar,
|
||||
pnOverlay,
|
||||
pnImageSelector,
|
||||
@@ -20,7 +23,8 @@ const components = {
|
||||
pnShadowScroll,
|
||||
pnMagicOverlay,
|
||||
pnAccountBlockName,
|
||||
pnOnboardBtn
|
||||
pnOnboardBtn,
|
||||
pnBottomSheetDialog
|
||||
}
|
||||
|
||||
export default boot(({ app }) => {
|
||||
|
||||
@@ -10,12 +10,43 @@ declare global {
|
||||
}
|
||||
|
||||
export default defineBoot(({ app }) => {
|
||||
if (window.Telegram?.WebApp) {
|
||||
const webApp = window.Telegram.WebApp
|
||||
webApp.ready()
|
||||
sessionStorage.setItem('isTelegram', webApp.initData ? 'true' : 'false')
|
||||
webApp.SettingsButton.isVisible = true
|
||||
app.config.globalProperties.$tg = webApp
|
||||
app.provide('tg', webApp)
|
||||
const tg = window.Telegram?.WebApp
|
||||
|
||||
tg.ready()
|
||||
sessionStorage.setItem('isTelegram', tg.initData ? 'true' : 'false')
|
||||
|
||||
const platform = tg.platform.toLowerCase()
|
||||
const isDesktopOrWeb = [
|
||||
'tdesktop',
|
||||
'tdlib',
|
||||
'macos',
|
||||
'web',
|
||||
'weba',
|
||||
'webk'
|
||||
].includes(platform)
|
||||
const isTablet = window.innerWidth > 1024
|
||||
if (!isDesktopOrWeb && !isTablet) {
|
||||
if (tg.isVersionAtLeast('8.0')) tg.requestFullscreen()
|
||||
else tg.expand()
|
||||
}
|
||||
|
||||
tg.SettingsButton.show()
|
||||
|
||||
app.config.globalProperties.$tg = tg
|
||||
app.config.globalProperties.$isDesktop = isDesktopOrWeb || isTablet
|
||||
app.config.globalProperties.$isMobile = !isDesktopOrWeb && !isTablet
|
||||
|
||||
app.provide('tg', tg)
|
||||
})
|
||||
|
||||
/* flag use
|
||||
<div v-if="$isDesktop || isToolbarExpanded">
|
||||
<q-tabs ...> </q-tabs>
|
||||
</div>
|
||||
|
||||
script setup
|
||||
import { getCurrentInstance } from 'vue'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
and use proxy.$isMobile
|
||||
*/
|
||||
@@ -1,57 +1,72 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex row items-center no-wrap logo-component"
|
||||
>
|
||||
<img
|
||||
src="src/assets/logo.svg"
|
||||
style="height: 24px; max-width: 24px; padding-right: 2px;"
|
||||
<div class="flex row items-center no-wrap logo-component">
|
||||
<LogoIcon
|
||||
v-bind="$attrs"
|
||||
class="logo-svg"
|
||||
:class="{
|
||||
'is-animated': animated,
|
||||
'hide-bg': !withBackground
|
||||
}"
|
||||
:style="{ color: iconColor }"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="text-brand"
|
||||
style="margin-right: 0.05em;"
|
||||
>
|
||||
tg
|
||||
</span>
|
||||
|
||||
<span class="text-brand text-bold">
|
||||
Crew
|
||||
</span>
|
||||
<div :class="'text-' + textColor" class="logo-text q-ml-xs">
|
||||
<span>tg</span>
|
||||
<span class="text-bold">Crew</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LogoIcon from 'components/BaseLogoSvg.vue'
|
||||
interface Props {
|
||||
textColor?: string
|
||||
iconColor?: string
|
||||
withBackground?: boolean
|
||||
animated?: boolean
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
textColor: 'brand',
|
||||
iconColor: 'white',
|
||||
withBackground: true,
|
||||
animated: true
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.logo-component {
|
||||
svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-right: 0.125em;
|
||||
.logo-svg {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
|
||||
:deep(.logo-bg) {
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
span {
|
||||
line-height: 1;
|
||||
&.hide-bg :deep(.logo-bg) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.x), :deep(.c) {
|
||||
transform-box: fill-box;
|
||||
}
|
||||
|
||||
&.is-animated {
|
||||
:deep(.x) { animation: expand-r 6s ease-in-out infinite; }
|
||||
:deep(.c) { animation: o 6s ease-in-out infinite; }
|
||||
:deep(.l) {
|
||||
stroke-dasharray: 1;
|
||||
stroke-dashoffset: 1;
|
||||
stroke-width: 1.5;
|
||||
animation: draw 6s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fill-brand {
|
||||
fill: $brand;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
100%,
|
||||
0% {
|
||||
fill: $brand2;
|
||||
}
|
||||
60% {
|
||||
fill: $brand2;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
#path5 {
|
||||
animation: blink 3s infinite;
|
||||
@keyframes expand-r { 50% { r: 8px; } }
|
||||
@keyframes o { 50% { transform: var(--t); } }
|
||||
@keyframes draw {
|
||||
0%, 100% { stroke-dashoffset: 0; }
|
||||
50% { stroke-dashoffset: 1; }
|
||||
}
|
||||
</style>
|
||||
|
||||
33
src/components/BaseLogoSvg.vue
Normal file
33
src/components/BaseLogoSvg.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="logo-gradient">
|
||||
<stop stop-color="#2AABEE" offset="0%"></stop>
|
||||
<stop stop-color="#229ED9" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<rect class="logo-bg" width="32" height="32" rx="4" ry="4" fill="url(#logo-gradient)" />
|
||||
|
||||
<circle class="x" cx="16" cy="16" r="6" style="--t:scale(1.2)" fill="currentColor" />
|
||||
<circle class="c" cx="7" cy="7" r="3" style="--t:translate(9px,9px)" fill="currentColor" />
|
||||
<circle class="c" cx="28" cy="4" r="2" style="--t:translate(-12px,12px)" fill="currentColor" />
|
||||
<circle class="c" cx="6" cy="26" r="4" style="--t:translate(10px,-10px)" fill="currentColor" />
|
||||
<circle class="c" cx="26" cy="26" r="5" style="--t:translate(-10px,-10px)" fill="currentColor" />
|
||||
|
||||
<line class="l" x1="16" y1="16" x2="7" y2="7" pathLength="1" stroke="currentColor" />
|
||||
<line class="l" x1="16" y1="16" x2="28" y2="4" pathLength="1" stroke="currentColor" />
|
||||
<line class="l" x1="16" y1="16" x2="6" y2="26" pathLength="1" stroke="currentColor" />
|
||||
<line class="l" x1="16" y1="16" x2="26" y2="26" pathLength="1" stroke="currentColor" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
</script>
|
||||
@@ -39,7 +39,6 @@
|
||||
|
||||
const modelValue = defineModel<('terms'| 'consent')[]>({ default: () => [] })
|
||||
|
||||
const emit = defineEmits(['agree'])
|
||||
const router = useRouter()
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<pn-page-template>
|
||||
<template #title>
|
||||
{{ $t(title) }}
|
||||
</template>
|
||||
<template #footer>
|
||||
<template #card-actions>
|
||||
<q-btn
|
||||
rounded color="primary"
|
||||
class="w100 fix-disabled-btn"
|
||||
@@ -13,7 +13,7 @@
|
||||
{{ $t(btnText) }}
|
||||
</q-btn>
|
||||
</template>
|
||||
<pn-scroll-list>
|
||||
|
||||
<div class="flex column items-center q-pa-md q-pb-sm w100">
|
||||
<slot name="myCompany"/>
|
||||
|
||||
@@ -57,8 +57,7 @@
|
||||
</q-input>
|
||||
</q-form>
|
||||
</div>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
</pn-page-template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<pn-page-template>
|
||||
<template #title>
|
||||
{{ $t(title) }}
|
||||
</template>
|
||||
<pn-scroll-list>
|
||||
|
||||
<markdown-viewver
|
||||
class="q-pa-md"
|
||||
class="q-px-md"
|
||||
:locale
|
||||
:documentName
|
||||
/>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
</pn-page-template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -9,12 +9,7 @@
|
||||
{{ customer.email }}
|
||||
</span>
|
||||
<span v-else class="ellipsis">
|
||||
{{
|
||||
tgUser?.first_name +
|
||||
(tgUser?.first_name && tgUser?.last_name ? ' ' : '') +
|
||||
tgUser?.last_name +
|
||||
(!(tgUser?.first_name || tgUser?.last_name) ? tgUser?.username : '')
|
||||
}}
|
||||
{{ nameTgUser(tgUser) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -22,13 +17,13 @@
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { useAuthStore } from 'stores/auth'
|
||||
import type { WebApp } from '@twa-dev/types'
|
||||
import { nameTgUser } from 'utils/helpers'
|
||||
import type { WebApp, WebAppUser } from '@twa-dev/types'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const customer = authStore.customer
|
||||
const tg = inject('tg') as WebApp
|
||||
const tgUser = tg.initDataUnsafe.user
|
||||
|
||||
const tgUser = tg.initDataUnsafe.user as WebAppUser | undefined
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex row q-mx-sm q-my-md justify-between items-center no-wrap"
|
||||
class="row no-wrap q-px-md q-py-md justify-between items-center bg-trasparent w100"
|
||||
>
|
||||
<q-btn
|
||||
v-if="showCalendarBtn"
|
||||
flat round
|
||||
:color="calendarActive ? 'primary' : 'grey'"
|
||||
round
|
||||
:color="calendarActive ? 'primary' : 'grey-2'"
|
||||
:class="calendarActive ? 'text-white' : 'text-grey'"
|
||||
class="q-mr-sm"
|
||||
@click="emit('toggle-calendar')"
|
||||
>
|
||||
<div>
|
||||
@@ -21,6 +23,7 @@
|
||||
</div>
|
||||
</q-btn>
|
||||
|
||||
<div class="w100">
|
||||
<q-input
|
||||
v-model="search"
|
||||
clearable
|
||||
@@ -29,18 +32,21 @@
|
||||
filled
|
||||
:placeholder="$t(placeholder)"
|
||||
dense
|
||||
class="col-grow q-mx-sm"
|
||||
class="rounded-fix bg-white shadow-1"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" color="grey"/>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
v-if="showFilterBtn"
|
||||
@click="emit('open-filters')"
|
||||
flat round
|
||||
:color="filterActive ? 'primary' : 'grey'"
|
||||
round
|
||||
:color="filterActive ? 'primary' : 'grey-2'"
|
||||
:class="filterActive ? 'text-white' : 'text-grey'"
|
||||
class="q-ml-sm"
|
||||
>
|
||||
<div>
|
||||
<q-icon name="mdi-filter-outline" size="sm"/>
|
||||
|
||||
@@ -1,98 +1,125 @@
|
||||
<template>
|
||||
<q-dialog
|
||||
v-model="modelValue"
|
||||
maximized
|
||||
transition-show="slide-up"
|
||||
transition-hide="slide-down"
|
||||
position="bottom"
|
||||
>
|
||||
<q-card
|
||||
class="fix-card-width flex column no-scroll no-wrap q-px-none"
|
||||
ref="cardRef"
|
||||
class="fix-card-width column no-scroll no-wrap q-px-none overflow-hidden relative-position"
|
||||
style="
|
||||
border-top-left-radius: var(--top-radius) !important;
|
||||
border-top-right-radius: var(--top-radius) !important;
|
||||
max-height: calc(100vh - 48px);
|
||||
max-height: calc(100vh - 48px - var(--tg-content-safe-area-inset-top) - var(--tg-safe-area-inset-top));
|
||||
"
|
||||
>
|
||||
<div
|
||||
ref="cardHeaderRef"
|
||||
class="flex items-center no-wrap justify-between w100 q-my-none q-pa-md"
|
||||
>
|
||||
<div class="text-h6 q-mx-xs ellipsis">{{ $t(title) }}</div>
|
||||
<div ref="cardHeaderRef" class="flex items-center no-wrap justify-between w100 q-my-none q-pa-md">
|
||||
<q-resize-observer @resize="onHeaderResize" />
|
||||
<div class="flex column q-mx-xs">
|
||||
<span class="text-h6" style="line-height: 1.5rem;">
|
||||
{{ $t(title) }}
|
||||
</span>
|
||||
<span v-if="caption" class="text-grey text-caption">
|
||||
{{ $t(caption) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between no-wrap">
|
||||
<slot name="btnSlot">
|
||||
<q-btn
|
||||
icon="mdi-close"
|
||||
@click="modelValue = false"
|
||||
flat round
|
||||
/>
|
||||
<q-btn icon="mdi-close" @click="modelValue = false" flat round />
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="cardBodyRef"
|
||||
class="q-px-none q-ma-none"
|
||||
>
|
||||
|
||||
<div class="q-px-none q-ma-none">
|
||||
<pn-shadow-scroll
|
||||
:hideShadows="false"
|
||||
:height="bodyHeight"
|
||||
:bottomOffset
|
||||
:topOffset
|
||||
>
|
||||
<div ref="cardBodyInnerRef" class="q-pa-none q-ma-none">
|
||||
<q-resize-observer @resize="updateDimensions" />
|
||||
<slot/>
|
||||
<div class="q-pa-none q-ma-none">
|
||||
<q-resize-observer @resize="onInnerBodyResize" />
|
||||
<slot />
|
||||
</div>
|
||||
</pn-shadow-scroll>
|
||||
</div>
|
||||
<div
|
||||
style="overflow: hidden"
|
||||
:style="{ height: footerHeight + 'px'}"
|
||||
>
|
||||
<div ref="cardFooterInnerRef" class="q-pa-md">
|
||||
<q-resize-observer @resize="updateDimensions" />
|
||||
<slot name="footer"/>
|
||||
|
||||
<div v-if="hasFooter" class="absolute-bottom">
|
||||
<q-resize-observer @resize="onFooterResize" />
|
||||
<div class="q-pa-md">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
<div :style="{ height: tgInsetBottom + 'px' }" />
|
||||
</div>
|
||||
</q-card>
|
||||
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { useSlots } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, useSlots } from 'vue'
|
||||
|
||||
const modelValue = defineModel<boolean>({
|
||||
required: true
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
const modelValue = defineModel<boolean>({ required: true })
|
||||
const props = withDefaults(defineProps<{
|
||||
title: string
|
||||
}>()
|
||||
caption?: string
|
||||
maximized?: boolean
|
||||
}>(), { maximized: true })
|
||||
|
||||
const slots = useSlots()
|
||||
const cardHeaderRef = ref<HTMLElement | null>(null)
|
||||
const cardFooterInnerRef = ref<HTMLElement | null>(null)
|
||||
const cardBodyInnerRef = ref<HTMLElement | null>(null)
|
||||
const cardRef = ref()
|
||||
|
||||
const headerHeight = ref(0)
|
||||
const footerHeight = ref(0)
|
||||
const bodyInnerHeight = ref(0)
|
||||
const bodyHeight = ref(0)
|
||||
const tgInsetBottom = ref(0)
|
||||
const topOffset = 16
|
||||
|
||||
const updateDimensions = async () => {
|
||||
await nextTick(() => {
|
||||
headerHeight.value = cardHeaderRef.value?.offsetHeight || 0
|
||||
footerHeight.value = cardFooterInnerRef.value?.offsetHeight || 0
|
||||
bodyInnerHeight.value = cardBodyInnerRef.value?.offsetHeight || 0
|
||||
const needScroll = headerHeight.value + footerHeight.value + bodyInnerHeight.value > window.innerHeight - 48
|
||||
bodyHeight.value = needScroll
|
||||
? window.innerHeight - 48 - headerHeight.value - footerHeight.value
|
||||
: bodyInnerHeight.value + 16
|
||||
const windowHeight = ref(window.innerHeight)
|
||||
|
||||
const hasFooter = computed(() => !!slots['footer'])
|
||||
const bottomOffset = computed(() => hasFooter.value ? footerHeight.value : tgInsetBottom.value)
|
||||
|
||||
const bodyHeight = computed(() => {
|
||||
// hack for update computed params on window resize
|
||||
const _trigger = windowHeight.value //eslint-disable-line
|
||||
const el = cardRef.value?.$el
|
||||
if (!el) return 0
|
||||
|
||||
const maxH = parseFloat(window.getComputedStyle(el).maxHeight) || 0
|
||||
|
||||
return props.maximized
|
||||
? maxH - headerHeight.value
|
||||
: Math.min(
|
||||
bodyInnerHeight.value + topOffset + (hasFooter.value ? footerHeight.value : 0),
|
||||
maxH - headerHeight.value
|
||||
)
|
||||
})
|
||||
|
||||
interface SizeParams { height: number; width: number }
|
||||
const onHeaderResize = (size: SizeParams) => headerHeight.value = size.height
|
||||
const onFooterResize = (size: SizeParams) => footerHeight.value = hasFooter.value ? size.height : 0
|
||||
const onInnerBodyResize = (size: SizeParams) => bodyInnerHeight.value = size.height
|
||||
|
||||
const handleWindowResize = () => {
|
||||
windowHeight.value = window.innerHeight
|
||||
}
|
||||
|
||||
watch(() => slots.body?.(), updateDimensions, { flush: 'post' })
|
||||
watch(modelValue, updateDimensions)
|
||||
const getTgInsetBottom = () => {
|
||||
const value = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--tg-safe-area-inset-bottom')
|
||||
.trim()
|
||||
|
||||
tgInsetBottom.value = parseFloat(value) || 0
|
||||
}
|
||||
|
||||
onMounted(() => getTgInsetBottom())
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleWindowResize)
|
||||
getTgInsetBottom()
|
||||
})
|
||||
|
||||
onUnmounted(() => window.removeEventListener('resize', handleWindowResize))
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
64
src/components/pnFabDialog.vue
Normal file
64
src/components/pnFabDialog.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<!-- <div :style="{ visibility: !modelValue ? 'inherit' : 'hidden' }">
|
||||
<slot name="btn"/>
|
||||
</div> -->
|
||||
<!-- <div
|
||||
:class="modelValue ? 'absolute' : ''"
|
||||
:style="{ left: fabPosLeft + 'px', top: fabPosTop + 'px' }"
|
||||
>
|
||||
<slot name="btn"/>
|
||||
</div> -->
|
||||
<q-dialog v-model="modelValue" :transition-duration="0">
|
||||
<div class="position-relative q-px-md">
|
||||
|
||||
<q-card
|
||||
class="q-pa-none q-mx-md no-scroll rounded-card absolute"
|
||||
:style="{ left: 0, top: fabPosTop - cardHeight - 16 + 'px' }"
|
||||
align="center"
|
||||
>
|
||||
|
||||
<q-resize-observer @resize="onCardResize"/>
|
||||
<slot name="card"/>
|
||||
</q-card>
|
||||
<div
|
||||
class="absolute bg-red"
|
||||
:style="{ left: fabPosLeft + 100+ 'px', top: fabPosTop +100+ 'px', height: '100px', width: '100px' }"
|
||||
id="fabDialog"
|
||||
>
|
||||
{{ fabPosLeft }} {{ fabPosTop }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const modelValue = defineModel<boolean>({
|
||||
required: true
|
||||
})
|
||||
|
||||
withDefaults(defineProps<{
|
||||
fabPosLeft?: number
|
||||
fabPosTop?: number
|
||||
}>(),
|
||||
{
|
||||
fabPosLeft: 0,
|
||||
fabPosTop: 0
|
||||
}
|
||||
)
|
||||
|
||||
interface sizeParams {
|
||||
height: number,
|
||||
width: number
|
||||
}
|
||||
|
||||
const cardHeight = ref(0)
|
||||
function onCardResize (size: sizeParams) {
|
||||
cardHeight.value = size.height
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
121
src/components/pnGlass.vue
Normal file
121
src/components/pnGlass.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="glass-wrapper">
|
||||
<div class="glass-effect" :style="glassStyle"></div>
|
||||
<div class="glass-tint"></div>
|
||||
<div class="glass-shine"></div>
|
||||
<div class="glass-slot">
|
||||
<slot/>
|
||||
</div>
|
||||
|
||||
<svg style="display: none">
|
||||
<filter
|
||||
id="glass-distortion"
|
||||
x="0%"
|
||||
y="0%"
|
||||
width="100%"
|
||||
height="100%"
|
||||
filterUnits="objectBoundingBox"
|
||||
>
|
||||
<feTurbulence
|
||||
type="fractalNoise"
|
||||
baseFrequency="0.01 0.01"
|
||||
numOctaves="1"
|
||||
seed="3"
|
||||
result="turbulence"
|
||||
/>
|
||||
<!-- Seeds: 14, 17, -->
|
||||
|
||||
<feComponentTransfer in="turbulence" result="mapped">
|
||||
<feFuncR type="gamma" amplitude="1" exponent="10" offset="0.5" />
|
||||
<feFuncG type="gamma" amplitude="0" exponent="1" offset="0" />
|
||||
<feFuncB type="gamma" amplitude="0" exponent="1" offset="0.5" />
|
||||
</feComponentTransfer>
|
||||
|
||||
<feGaussianBlur in="turbulence" stdDeviation="3" result="softMap" />
|
||||
|
||||
<feSpecularLighting
|
||||
in="softMap"
|
||||
surfaceScale="5"
|
||||
specularConstant="1"
|
||||
specularExponent="100"
|
||||
lighting-color="white"
|
||||
result="specLight"
|
||||
>
|
||||
<fePointLight x="-200" y="-200" z="300" />
|
||||
</feSpecularLighting>
|
||||
|
||||
<feComposite
|
||||
in="specLight"
|
||||
operator="arithmetic"
|
||||
k1="0"
|
||||
k2="1"
|
||||
k3="1"
|
||||
k4="0"
|
||||
result="litImage"
|
||||
/>
|
||||
|
||||
<feDisplacementMap
|
||||
in="SourceGraphic"
|
||||
in2="softMap"
|
||||
scale="150"
|
||||
xChannelSelector="R"
|
||||
yChannelSelector="G"
|
||||
/>
|
||||
</filter>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
color?: string
|
||||
}>()
|
||||
|
||||
const glassStyle = computed(() => ({
|
||||
'--glass-color': props.color ?? 'white'
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.glass-wrapper {
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
inset: 0;
|
||||
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 2.2);
|
||||
}
|
||||
|
||||
.glass-effect {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
inset: 0;
|
||||
backdrop-filter: blur(3px);
|
||||
/* filter: url(#glass-distortion); */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.glass-tint {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.glass-shine {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 2px 2px 1px 0 rgba(255, 255, 255, 0.5),
|
||||
inset 0px 0px 1px 1px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.glass-slot {
|
||||
z-index: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
</style>
|
||||
33
src/components/pnGlassBtn.vue
Normal file
33
src/components/pnGlassBtn.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="q-pa-none"
|
||||
:class="typeClass"
|
||||
:style="{ backgroundColor: bgColor ? getColorWithOpacity(color, bgColor) : getColorWithOpacity(color) }"
|
||||
>
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { getColorWithOpacity } from 'utils/colors'
|
||||
|
||||
const props = defineProps<{
|
||||
type: string
|
||||
color: string
|
||||
bgColor?: string
|
||||
}>()
|
||||
|
||||
const typeClass = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'fab': return 'q-btn--fab q-btn--rounded'
|
||||
case 'rounded': return 'q-btn--rounded'
|
||||
default: return ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<q-item
|
||||
@click="showDialog=true"
|
||||
@click="showDialog = true"
|
||||
clickable
|
||||
v-ripple
|
||||
>
|
||||
@@ -26,71 +26,19 @@
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-dialog
|
||||
<pn-bottom-sheet-dialog
|
||||
:title
|
||||
:caption
|
||||
v-model="showDialog"
|
||||
maximized
|
||||
transition-show="slide-up"
|
||||
transition-hide="slide-down"
|
||||
position="bottom"
|
||||
>
|
||||
<q-card
|
||||
class="fix-card-width flex column no-scroll no-wrap q-px-none"
|
||||
style="
|
||||
border-top-left-radius: var(--top-radius) !important;
|
||||
border-top-right-radius: var(--top-radius) !important;
|
||||
"
|
||||
>
|
||||
<div
|
||||
ref="cardHeaderRef"
|
||||
class="flex items-center no-wrap justify-between w100 q-my-none q-pa-md"
|
||||
>
|
||||
<div>
|
||||
<div class="flex column q-mx-xs">
|
||||
<span
|
||||
class="text-h6"
|
||||
style="line-height: 1.5rem;"
|
||||
>
|
||||
{{ $t(title) }}
|
||||
</span>
|
||||
<span v-if="caption" class="text-grey text-caption">{{ $t(caption) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between no-wrap">
|
||||
<q-btn
|
||||
icon="mdi-close"
|
||||
@click="showDialog=false"
|
||||
flat round
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="cardBodyRef"
|
||||
class="q-px-none q-ma-none"
|
||||
>
|
||||
<pn-shadow-scroll
|
||||
:hideShadows="false"
|
||||
:height="bodyHeight"
|
||||
>
|
||||
<div ref="cardBodyInnerRef" class="q-px-md q-ma-none">
|
||||
<q-resize-observer @resize="updateDimensions" />
|
||||
<div class="q-px-md">
|
||||
<slot/>
|
||||
</div>
|
||||
</pn-shadow-scroll>
|
||||
</div>
|
||||
<div
|
||||
ref="cardFooterRef"
|
||||
class="q-pa-md"
|
||||
>
|
||||
<slot name="footer"/>
|
||||
</div>
|
||||
</q-card>
|
||||
|
||||
</q-dialog>
|
||||
</pn-bottom-sheet-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useSlots } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
@@ -100,31 +48,7 @@
|
||||
}>()
|
||||
|
||||
const showDialog=ref<boolean>(false)
|
||||
|
||||
const slots = useSlots()
|
||||
const cardHeaderRef = ref<HTMLElement | null>(null)
|
||||
const cardFooterRef = ref<HTMLElement | null>(null)
|
||||
const cardBodyRef = ref<HTMLElement | null>(null)
|
||||
const cardBodyInnerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const headerHeight = ref(0)
|
||||
const footerHeight = ref(0)
|
||||
const bodyInnerHeight = ref(0)
|
||||
const bodyHeight = ref(0)
|
||||
|
||||
const updateDimensions = () => {
|
||||
headerHeight.value = cardHeaderRef.value?.offsetHeight || 0
|
||||
footerHeight.value = cardFooterRef.value?.offsetHeight || 0
|
||||
bodyInnerHeight.value = cardBodyInnerRef.value?.offsetHeight || 0
|
||||
bodyHeight.value = window.innerHeight - headerHeight.value - footerHeight.value - 48
|
||||
}
|
||||
|
||||
watch(() => slots.body?.(), updateDimensions, { flush: 'post' })
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fix-card-width {
|
||||
width: var(--body-width) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="flex column justify-center col-grow items-center">
|
||||
<q-icon :name="icon" size="160px" class="q-pb-md"/>
|
||||
<div class="text-h6 text-brand text-center">
|
||||
{{ message1 }}
|
||||
{{ $t(message1) }}
|
||||
</div>
|
||||
</div>
|
||||
</q-btn>
|
||||
@@ -17,13 +17,13 @@
|
||||
<div class="flex column justify-center col-grow items-center">
|
||||
<q-icon :name="icon" size="160px" class="q-pb-md"/>
|
||||
<div class="text-h6 text-brand text-center">
|
||||
{{ message1 }}
|
||||
{{ $t(message1) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="message2" class="text-caption text-center">
|
||||
{{ message2 }}
|
||||
{{ $t(message2) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,5 +44,6 @@
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div
|
||||
id="overlay"
|
||||
class="fixed-full q-dialog__backdrop"
|
||||
style="z-index: 5000; --q-transition-duration: 300ms;"
|
||||
class="q-dialog__backdrop absolute-top"
|
||||
style="z-index: 5000; --q-transition-duration: 300ms; height: 100vh; width: 10vw;"
|
||||
@click.prevent.stop
|
||||
@touchstart.prevent.stop
|
||||
@touchmove.prevent.stop
|
||||
@@ -13,6 +13,5 @@
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
@@ -1,24 +1,117 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between q-ma-none q-py-none q-px-md text-white text-h6 no-scroll no-wrap w100"
|
||||
style="min-height: 48px"
|
||||
class="w100 col-grow relative-position top-rounded-card bg-white overflow-hidden"
|
||||
>
|
||||
<slot name="title"/>
|
||||
<q-resize-observer @resize="onCardResize"/>
|
||||
<div
|
||||
id="card-header"
|
||||
class="absolute-top"
|
||||
v-if="hasHeader"
|
||||
style="z-index: 100;"
|
||||
>
|
||||
<q-resize-observer @resize="onHeaderResize"/>
|
||||
<slot name="card-header"/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="card-under-header"
|
||||
class="absolute-top"
|
||||
v-if="hasUnderHeader"
|
||||
style="z-index: 100;"
|
||||
:style="{ top: headerHeight + 'px' }"
|
||||
>
|
||||
<slot name="card-under-header"/>
|
||||
</div>
|
||||
|
||||
<pn-shadow-scroll
|
||||
:height="cardHeight"
|
||||
:topOffset="headerHeight"
|
||||
:bottomOffset
|
||||
v-bind="$attrs"
|
||||
class="overflow-hidden"
|
||||
>
|
||||
<slot/>
|
||||
<div v-if="hasFooter" class="flex w100 q-pa-md bg-white">
|
||||
<slot name="footer"/>
|
||||
</pn-shadow-scroll>
|
||||
|
||||
<div
|
||||
class="absolute-bottom q-px-md q-py-md"
|
||||
id="card-actions"
|
||||
v-if="hasActions"
|
||||
style="z-index: 100;"
|
||||
>
|
||||
<q-resize-observer @resize="onActionsResize"/>
|
||||
<slot name="card-actions"/>
|
||||
<div style="height: var(--tg-safe-area-inset-bottom)"/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute column q-px-md q-gutter-y-md"
|
||||
:class="hasActions || tabsSlotHeight ? 'q-pb-none' : 'q-pb-md'"
|
||||
:style="{ bottom: bottomOffset + 'px', right: '0' }"
|
||||
v-if="hasFAB"
|
||||
style="z-index: 100;"
|
||||
>
|
||||
<slot name="card-fab"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<slot v-if="hasTabs" name="tabs"/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSlots, computed } from 'vue'
|
||||
import { useSlots, computed, ref, onMounted } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
tabsSlotHeight?: number
|
||||
}>(),
|
||||
{
|
||||
tabsSlotHeight: 0,
|
||||
}
|
||||
)
|
||||
|
||||
const slots = useSlots()
|
||||
const hasFooter = computed(() => !!slots.footer)
|
||||
const hasTabs = computed(() => !!slots.tabs)
|
||||
const hasHeader = computed(() => !!slots['card-header'])
|
||||
const hasActions = computed(() => !!slots['card-actions'])
|
||||
const hasFAB = computed(() => !!slots['card-fab'])
|
||||
const hasUnderHeader = computed(() => !!slots['card-under-header'])
|
||||
|
||||
const cardHeight = ref(0)
|
||||
const headerHeight = ref(16)
|
||||
const actionsHeight = ref(0)
|
||||
|
||||
interface sizeParams {
|
||||
height: number,
|
||||
width: number
|
||||
}
|
||||
|
||||
function onCardResize (size: sizeParams) {
|
||||
cardHeight.value = size.height
|
||||
}
|
||||
|
||||
function onHeaderResize (size: sizeParams) {
|
||||
headerHeight.value = size.height
|
||||
}
|
||||
|
||||
function onActionsResize (size: sizeParams) {
|
||||
actionsHeight.value = size.height
|
||||
}
|
||||
|
||||
const tgInsetBottom = ref(0)
|
||||
|
||||
const getTgInsetBottom = () => {
|
||||
const value = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--tg-safe-area-inset-bottom')
|
||||
.trim()
|
||||
|
||||
tgInsetBottom.value = parseFloat(value) || 0
|
||||
}
|
||||
|
||||
onMounted(() => getTgInsetBottom())
|
||||
|
||||
const bottomOffset = computed(() => {
|
||||
return Math.max(props.tabsSlotHeight, actionsHeight.value, tgInsetBottom.value)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
19
src/components/pnPageTemplate.vue
Normal file
19
src/components/pnPageTemplate.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<pn-page-wrapper v-bind="$attrs">
|
||||
<template #title>
|
||||
<slot name="title"/>
|
||||
</template>
|
||||
|
||||
<pn-page-card v-bind="$attrs">
|
||||
<template v-for="(_, name) in $slots" v-slot:[name]>
|
||||
<slot :name="name"/>
|
||||
</template>
|
||||
</pn-page-card>
|
||||
</pn-page-wrapper>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
52
src/components/pnPageWrapper.vue
Normal file
52
src/components/pnPageWrapper.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div
|
||||
id="page-wrapper"
|
||||
class="w100 relative-position column no-wrap"
|
||||
style="height: 100vh;"
|
||||
>
|
||||
<div
|
||||
id="tg-area-inset-top"
|
||||
style="height: var(--tg-safe-area-inset-top)"
|
||||
/>
|
||||
<div
|
||||
id="tg-safe-area"
|
||||
class="flex justify-center items-center overflow-hidden"
|
||||
style="height: var(--tg-content-safe-area-inset-top)"
|
||||
>
|
||||
<baseLogo text-color="white" :withBackground="false" :animated="false"/>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-between q-ma-none q-py-none q-px-md text-white text-h6 no-scroll no-wrap w100"
|
||||
style="min-height: 48px;"
|
||||
id="page-title"
|
||||
>
|
||||
<slot name="title"/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="column w100"
|
||||
style="flex: 1 1 auto; min-height: 0;"
|
||||
>
|
||||
<slot/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="tabs"
|
||||
v-if="hasTabs"
|
||||
class="absolute-bottom w100"
|
||||
>
|
||||
<slot name="tabs"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSlots, computed } from 'vue'
|
||||
import BaseLogo from 'components/BaseLogo.vue'
|
||||
|
||||
const slots = useSlots()
|
||||
const hasTabs = computed(() => !!slots.tabs)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -1,131 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
id="page-card"
|
||||
class="w100 flex column glass-card top-rounded-card no-scroll no-wrap"
|
||||
>
|
||||
<div
|
||||
id="card-body-header"
|
||||
v-if="!hideHeader && hasHeader"
|
||||
style="flex-shrink: 0; min-height: var(--top-radius);"
|
||||
>
|
||||
<q-resize-observer @resize="onHeaderResize"/>
|
||||
<slot name="card-body-header"/>
|
||||
</div>
|
||||
<div
|
||||
id="card-body"
|
||||
class="top-rounded-card bg-white column no-wrap"
|
||||
>
|
||||
<div style="min-height: var(--top-radius);">
|
||||
<slot name="card-body-btn"/>
|
||||
<q-resize-observer @resize="onBodyBtnResize"/>
|
||||
</div>
|
||||
<div class="column col col-grow">
|
||||
<q-resize-observer @resize="onBodyResize"/>
|
||||
<pn-shadow-scroll
|
||||
:hideShadows="isResizing"
|
||||
:height="scrollAreaHeight"
|
||||
@updateScrollPosition="onScrollUpdate"
|
||||
:scrollPosition
|
||||
>
|
||||
<div
|
||||
class="w100 column"
|
||||
:style="{ minHeight: scrollAreaHeight + 'px' }"
|
||||
>
|
||||
<slot/>
|
||||
</div>
|
||||
</pn-shadow-scroll>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick, useSlots, computed } from 'vue'
|
||||
|
||||
const emit = defineEmits(['updateScrollPosition', 'updateHeight'])
|
||||
const onScrollUpdate = (pos: number) => emit('updateScrollPosition', pos)
|
||||
|
||||
const slots = useSlots()
|
||||
// work only slot no use in component
|
||||
const hasHeader = computed(() => !!slots['card-body-header'])
|
||||
|
||||
// need if slot draw by expression with v-if
|
||||
withDefaults(defineProps<{
|
||||
hideHeader?: boolean
|
||||
scrollPosition?: number
|
||||
}>(), {
|
||||
hideHeader: false,
|
||||
scrollPosition: 0
|
||||
})
|
||||
|
||||
const heightCard = ref(100)
|
||||
const scrollAreaHeight = ref(100)
|
||||
const headerHeight = ref(0)
|
||||
const headerBtnHeight = ref(0)
|
||||
|
||||
interface sizeParams {
|
||||
height: number,
|
||||
width: number
|
||||
}
|
||||
|
||||
async function onHeaderResize(size: sizeParams) {
|
||||
headerHeight.value = size.height
|
||||
await updateScrollAreaHeight()
|
||||
}
|
||||
|
||||
function onBodyBtnResize (size: sizeParams) {
|
||||
headerBtnHeight.value = size.height
|
||||
}
|
||||
|
||||
async function onBodyResize (size: sizeParams) {
|
||||
heightCard.value = size.height - headerBtnHeight.value
|
||||
await updateScrollAreaHeight()
|
||||
}
|
||||
|
||||
async function updateScrollAreaHeight() {
|
||||
await nextTick(() => {
|
||||
scrollAreaHeight.value = Math.max(0, heightCard.value)
|
||||
emit('updateHeight', scrollAreaHeight.value)
|
||||
})
|
||||
}
|
||||
|
||||
watch(heightCard, updateScrollAreaHeight)
|
||||
watch(headerHeight, updateScrollAreaHeight)
|
||||
|
||||
const isResizing = ref(false)
|
||||
let resizeTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
watch(heightCard, () => {
|
||||
isResizing.value = true
|
||||
|
||||
if (resizeTimer) {
|
||||
clearTimeout(resizeTimer)
|
||||
resizeTimer = null
|
||||
}
|
||||
|
||||
resizeTimer = setTimeout(() => {
|
||||
isResizing.value = false
|
||||
resizeTimer = null
|
||||
}, 150)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#page-card {
|
||||
flex: 1 0 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#card-body {
|
||||
overflow: hidden;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#card-body :deep(.q-scrollarea__content) {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,125 +1,107 @@
|
||||
<template>
|
||||
<div :class="{'fix-scroll-area-content': hideShadows }" class="fix-scroll-area-content-width column col col-grow">
|
||||
<q-scroll-area
|
||||
ref="scrollAreaRef"
|
||||
:style="{ height: height + 'px' }"
|
||||
class="w100 q-pa-none q-ma-none column col col-grow"
|
||||
:style="scrollStyle"
|
||||
:vertical-offset="[top, bottom]"
|
||||
:thumb-style="{ borderRadius: '3px', background: '#999', width: '6px', opacity: '0.4' }"
|
||||
@scroll="onScroll"
|
||||
:class=" {
|
||||
'shadow-top': hasScrolled,
|
||||
'shadow-bottom': hasScrolledBottom
|
||||
}"
|
||||
class="shadow-scroll w100"
|
||||
>
|
||||
<q-resize-observer @resize="onInnerResize"/>
|
||||
<div :style="{ height: top + 'px'}"/>
|
||||
<slot/>
|
||||
<div :style="{ height: bottom + 'px'}"/>
|
||||
</q-scroll-area>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onActivated, watch } from 'vue'
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
height: number
|
||||
hideShadows: boolean
|
||||
topOffset?: number
|
||||
bottomOffset?: number
|
||||
scrollPosition?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['updateScrollPosition'])
|
||||
|
||||
const hasScrolled = ref(false)
|
||||
const hasScrolledBottom = ref(false)
|
||||
|
||||
interface ScrollInfo {
|
||||
verticalPosition: number;
|
||||
verticalPercentage: number;
|
||||
verticalSize: number;
|
||||
verticalContainerSize: number;
|
||||
offset?: number
|
||||
}>(),
|
||||
{
|
||||
scrollPosition: 0,
|
||||
offset: 16
|
||||
}
|
||||
)
|
||||
|
||||
function onScroll (info: ScrollInfo) {
|
||||
hasScrolled.value = info.verticalPosition > 0
|
||||
const scrollEnd = info.verticalPosition + info.verticalContainerSize >= info.verticalSize - 1
|
||||
hasScrolledBottom.value = !scrollEnd
|
||||
const emit = defineEmits([
|
||||
'updateScrollPosition',
|
||||
'resizeScrollArea'
|
||||
])
|
||||
|
||||
const top = computed(() => props.topOffset || props.offset)
|
||||
const bottom = computed(() => props.bottomOffset || props.offset)
|
||||
const innerHeight = ref(0)
|
||||
|
||||
const scrollStyle = computed(() => ({
|
||||
height: props.height + 'px',
|
||||
'--top-height': top.value + 'px',
|
||||
'--bottom-height': bottom.value + 'px'
|
||||
}))
|
||||
|
||||
function onScroll () {
|
||||
emit('updateScrollPosition', scrollAreaRef.value?.getScrollPosition().top || 0)
|
||||
}
|
||||
|
||||
const scrollAreaRef = ref()
|
||||
|
||||
onActivated(() => {
|
||||
scrollAreaRef.value?.setScrollPosition('vertical', props?.scrollPosition || 0)
|
||||
})
|
||||
|
||||
watch(() => props.scrollPosition, (newPosition) => {
|
||||
watch(() => props.scrollPosition, async (newPosition) => {
|
||||
await nextTick()
|
||||
scrollAreaRef.value?.setScrollPosition('vertical', newPosition)
|
||||
}, { immediate: true })
|
||||
|
||||
watch([top, bottom, () => props.height], async () => {
|
||||
await nextTick()
|
||||
emit('resizeScrollArea', {
|
||||
height: props.height,
|
||||
innerHeight: innerHeight.value,
|
||||
topOffset: top.value,
|
||||
bottomOffset: bottom.value
|
||||
})
|
||||
}, { immediate: true })
|
||||
|
||||
interface sizeParams {
|
||||
height: number,
|
||||
width: number
|
||||
}
|
||||
|
||||
function onInnerResize (size: sizeParams) {
|
||||
innerHeight.value = size.height
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.q-scrollarea {
|
||||
position: relative;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.q-scrollarea::before {
|
||||
content: '';
|
||||
.shadow-scroll:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(to bottom,
|
||||
rgba(0,0,0,0.12) 0%,
|
||||
rgba(0,0,0,0.08) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
width: 100%;
|
||||
height: var(--top-height);
|
||||
background: linear-gradient(white, transparent);
|
||||
pointer-events: none;
|
||||
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
will-change: opacity, transform;
|
||||
z-index: 1;
|
||||
z-index: 95;
|
||||
}
|
||||
|
||||
.q-scrollarea::after {
|
||||
content: '';
|
||||
.shadow-scroll:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(to top,
|
||||
rgba(0,0,0,0.12) 0%,
|
||||
rgba(0,0,0,0.08) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
will-change: opacity, transform;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.fix-scroll-area-content-width:deep(.q-scrollarea__content) {
|
||||
width: 100%;
|
||||
height: var(--bottom-height);
|
||||
background: linear-gradient(transparent, white);
|
||||
pointer-events: none;
|
||||
z-index: 95;
|
||||
}
|
||||
|
||||
.fix-scroll-area-content:deep(.q-scrollarea::before) {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.fix-scroll-area-content:deep(.q-scrollarea::after) {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.q-scrollarea.shadow-top::before {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.q-scrollarea.shadow-bottom::after {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
.shadow-scroll :deep(.q-scrollarea__content) {
|
||||
max-width: min(100vw, var(--body-width));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
<template>
|
||||
<q-dialog v-model="modelValue" class="q-ma-none">
|
||||
<q-card
|
||||
class="q-pa-none q-ma-none w100 no-scroll rounded-card"
|
||||
style="max-height: calc(100vh - 48px);"
|
||||
<q-dialog
|
||||
v-model="modelValue"
|
||||
class="q-ma-none"
|
||||
>
|
||||
<q-card-section class="row items-center q-pb-none">
|
||||
<q-card
|
||||
class="q-pa-none q-ma-none w100 q-pb-md no-scroll rounded-card no-wrap overflow-hidden"
|
||||
ref="cardRef"
|
||||
style="
|
||||
max-height: calc(100vh - 48px - 2 * max(var(--tg-content-safe-area-inset-top) + var(--tg-safe-area-inset-top), var(--tg-safe-area-inset-bottom)));
|
||||
width: calc(100vw - 32px);
|
||||
"
|
||||
>
|
||||
<q-resize-observer @resize="onResizeCard" />
|
||||
<q-card-section class="row items-center q-pb-none" ref="cardHeaderRef">
|
||||
<div class="flex no-wrap items-center w100">
|
||||
<q-resize-observer @resize="onHeaderResize"/>
|
||||
<span class="text-bold">
|
||||
@@ -15,10 +23,15 @@
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<div>
|
||||
<pn-shadow-scroll :height="scrollAreaHeight" :hideShadows="false">
|
||||
<div>
|
||||
<q-resize-observer @resize="onBodyResize"/>
|
||||
<div
|
||||
ref="cardBodyRef"
|
||||
class="q-px-none q-ma-none"
|
||||
>
|
||||
<pn-shadow-scroll
|
||||
:height="bodyHeight"
|
||||
>
|
||||
<div ref="cardBodyInnerRef" class="q-pa-none q-ma-none">
|
||||
<q-resize-observer @resize="updateDimensions" />
|
||||
<slot/>
|
||||
</div>
|
||||
</pn-shadow-scroll>
|
||||
@@ -29,44 +42,60 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import pnShadowScroll from './pnShadowScroll.vue'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
}>()
|
||||
import { useSlots } from 'vue'
|
||||
|
||||
const modelValue = defineModel<boolean>({
|
||||
required: true
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
}>()
|
||||
|
||||
const slots = useSlots()
|
||||
const cardBodyInnerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const headerHeight = ref(0)
|
||||
const bodyInnerHeight = ref(0)
|
||||
const bodyHeight = ref(0)
|
||||
const scrollAreaHeight = ref(0)
|
||||
const cardHeight = ref(0)
|
||||
|
||||
interface sizeParams {
|
||||
height: number,
|
||||
width: number
|
||||
}
|
||||
|
||||
function onResizeCard (size :sizeParams) {
|
||||
cardHeight.value = size.width
|
||||
}
|
||||
|
||||
function onHeaderResize (size: sizeParams) {
|
||||
headerHeight.value = size.height
|
||||
}
|
||||
|
||||
function onBodyResize (size: sizeParams) {
|
||||
bodyHeight.value = size.height
|
||||
}
|
||||
|
||||
async function updateScrollAreaHeight () {
|
||||
const updateDimensions = async () => {
|
||||
await nextTick(() => {
|
||||
scrollAreaHeight.value = Math.min(
|
||||
bodyHeight.value + 16,
|
||||
window.innerHeight - headerHeight.value - 64
|
||||
)
|
||||
bodyInnerHeight.value = cardBodyInnerRef.value?.offsetHeight || 0
|
||||
|
||||
const maxH = cardMaxHeight()
|
||||
|
||||
const availableForBody = maxH - headerHeight.value - 32 // 16 - md-padding + 16px - shadow-block
|
||||
bodyHeight.value = Math.min(bodyInnerHeight.value + 32, availableForBody)
|
||||
})
|
||||
}
|
||||
|
||||
watch(bodyHeight, updateScrollAreaHeight)
|
||||
watch(headerHeight, updateScrollAreaHeight)
|
||||
watch(() => slots.body?.(), updateDimensions, { flush: 'post' })
|
||||
watch(modelValue, updateDimensions)
|
||||
|
||||
const cardRef = ref()
|
||||
function cardMaxHeight () {
|
||||
const el = cardRef.value?.$el
|
||||
if (el) {
|
||||
const computedStyle = window.getComputedStyle(el)
|
||||
return parseFloat(computedStyle.maxHeight)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<q-icon name="close"/>
|
||||
{{$t('close')}}
|
||||
{{ $t('close') }}
|
||||
</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<pn-page-template>
|
||||
<template #title>
|
||||
{{ $t(title) }}
|
||||
</template>
|
||||
<template #footer>
|
||||
<template #card-actions>
|
||||
<q-btn
|
||||
rounded color="primary"
|
||||
class="w100 fix-disabled-btn"
|
||||
@@ -14,7 +14,6 @@
|
||||
</q-btn>
|
||||
</template>
|
||||
|
||||
<pn-scroll-list>
|
||||
<div class="flex column items-center q-pa-md q-pb-sm w100">
|
||||
<pn-image-selector
|
||||
v-model="projectMod.logo"
|
||||
@@ -52,16 +51,28 @@
|
||||
filled
|
||||
autogrow
|
||||
no-error-icon
|
||||
class="w100 q-pt-sm"
|
||||
class="w100 q-pt-sm fix-bottom-padding"
|
||||
:label="$t('project_block__project_description')"
|
||||
:rules="[inputRules.longText]"
|
||||
@update:model-value="formRef.validate()"
|
||||
debounce="300"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-model="projectMod.chat_info"
|
||||
dense
|
||||
filled
|
||||
autogrow
|
||||
no-error-icon
|
||||
class="w100 q-pt-sm"
|
||||
:label="$t('project_block__project_chat_info')"
|
||||
:rules="[inputRules.longText]"
|
||||
@update:model-value="formRef.validate()"
|
||||
debounce="300"
|
||||
/>
|
||||
</q-form>
|
||||
</div>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
</pn-page-template>
|
||||
|
||||
</template>
|
||||
|
||||
@@ -84,6 +95,7 @@
|
||||
const projectMod = reactive({
|
||||
name: props.project?.name ?? '',
|
||||
description: props.project?.description ?? '',
|
||||
chat_info: props.project?.chat_info ?? '',
|
||||
logo: props.project?.logo ?? null
|
||||
})
|
||||
|
||||
@@ -94,6 +106,7 @@
|
||||
const outData = {
|
||||
name: projectMod.name,
|
||||
description: getFieldValue(projectMod.description, props.project?.description),
|
||||
chat_info: getFieldValue(projectMod.chat_info, props.project?.chat_info),
|
||||
logo: projectMod.logo !== '' ? projectMod.logo : null
|
||||
}
|
||||
emit('update', removeNullKeys(outData, ['logo']))
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
title="subscribe__dialog_select_tariff_title"
|
||||
v-model="showTarfiffSelectDialog"
|
||||
>
|
||||
|
||||
<div class="w100 column">
|
||||
<q-banner
|
||||
v-if="!currentTariff.can_change"
|
||||
@@ -103,12 +104,13 @@
|
||||
|
||||
<div
|
||||
class="w100 text-info cursor-pointer"
|
||||
@click="router.push({ name: 'change_tariff_rules'})"
|
||||
@click="router.push({ name: 'tariffs_info'})"
|
||||
>
|
||||
{{ $t('subscribe__dialog_select_tariff_notes_more') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="column">
|
||||
<q-btn
|
||||
@@ -149,7 +151,6 @@
|
||||
import telegramStar from 'components/TelegramStar.vue'
|
||||
import pnSubscribeTariffs from 'components/subscribe/pnSubscribeTariffs.vue'
|
||||
import pnSubscribePeriods from 'components/subscribe/pnSubscribePeriods.vue'
|
||||
import pnBottomSheetDialog from 'components/pnBottomSheetDialog.vue'
|
||||
import {
|
||||
calcChangeTariff,
|
||||
changeTariff,
|
||||
@@ -176,6 +177,7 @@
|
||||
const selectedTariffPrice = computed(() => props.tariffs.find((el: Tariff) => el.id === props.currentTariff?.id)?.price ?? 0)
|
||||
|
||||
const showTarfiffSelectDialog = ref(false)
|
||||
|
||||
async function clickChangeTariff () {
|
||||
if (props.currentTariff &&
|
||||
selectedTariff.value &&
|
||||
@@ -196,6 +198,7 @@
|
||||
description: 'subscribe__dialog_select_tariff_warn_once_day_msg_2'
|
||||
}
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
const activePeriodMessage = ref('')
|
||||
@@ -218,7 +221,7 @@
|
||||
const renew = await calcRenewTariff(props.currentTariff.id, selectedPeriod.value)
|
||||
const textBtn = t('subscribe__pay_renew') + ' ⭐' + formatNumber(renew.amount) +
|
||||
' (' + t('subscribe__tariff_exp') + ' ' + date.formatDate(renew.end_date * 1000, 'DD.MM.YYYY') + ')'
|
||||
const url = await urlRenewTariff(props.currentTariff.id, selectedPeriod.value)
|
||||
const url = urlRenewTariff(props.currentTariff.id, selectedPeriod.value)
|
||||
|
||||
emit('update', { status: 'show', textBtn, url })
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<pn-page-template>
|
||||
<template #title>
|
||||
{{ $t(title) }}
|
||||
</template>
|
||||
<template #footer>
|
||||
<template #card-actions>
|
||||
<q-btn
|
||||
rounded color="primary"
|
||||
class="w100 fix-disabled-btn"
|
||||
@@ -13,7 +13,7 @@
|
||||
{{ $t(btnText) }}
|
||||
</q-btn>
|
||||
</template>
|
||||
<pn-scroll-list>
|
||||
|
||||
<div class="column items-center q-pa-md q-pb-sm">
|
||||
<div class="relative-position flex justify-center w100 text-center">
|
||||
<pn-auto-avatar
|
||||
@@ -148,8 +148,7 @@
|
||||
</q-input>
|
||||
</q-form>
|
||||
</div>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
</pn-page-template>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
@@ -68,10 +68,6 @@ body {
|
||||
margin-right: calc(max((100vw - var(--body-width))/2, 0px) + 18px) !important;
|
||||
}
|
||||
|
||||
.projects-header {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.top-rounded-card {
|
||||
border-top-left-radius: var(--top-radius) !important;
|
||||
border-top-right-radius: var(--top-radius) !important;
|
||||
@@ -81,8 +77,9 @@ body {
|
||||
border-radius: var(--top-radius) !important;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background-color: rgb(255, 255, 255, 0.91) !important;
|
||||
.rounded-fix {
|
||||
border-radius: 1000px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.orline {
|
||||
@@ -105,6 +102,7 @@ body {
|
||||
|
||||
.fix-disabled-btn.q-btn[disabled]:not(.q-btn--flat) {
|
||||
background-color: $grey-5 !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.fix-disabled-btn.q-btn.q-btn--flat[disabled] {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -7,7 +7,7 @@
|
||||
<q-page-container
|
||||
class="main-content q-pa-none q-ma-none no-scroll bg-transparent"
|
||||
>
|
||||
<q-page class="no-scroll column">
|
||||
<q-page>
|
||||
<router-view />
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<pn-page-template>
|
||||
<template #title>
|
||||
{{$t('account__change_auth_method')}}
|
||||
</template>
|
||||
<pn-scroll-list>
|
||||
<account-helper :type/>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
</pn-page-template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<pn-page-template>
|
||||
<template #title>
|
||||
{{$t('account_change_email__title')}}
|
||||
</template>
|
||||
<pn-scroll-list>
|
||||
|
||||
<q-stepper
|
||||
v-model="step"
|
||||
vertical
|
||||
@@ -185,8 +185,7 @@
|
||||
message2="account_change_email__ok_message2"
|
||||
route-name="account"
|
||||
/>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
</pn-page-template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<pn-page-template>
|
||||
<template #title>
|
||||
{{$t('account__change_password')}}
|
||||
</template>
|
||||
<pn-scroll-list>
|
||||
<account-helper :type :email/>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
</pn-page-template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<pn-page-template>
|
||||
<template #title>
|
||||
{{$t('login__register_title')}}
|
||||
</template>
|
||||
<pn-scroll-list>
|
||||
<account-helper :type :email/>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
</pn-page-template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<pn-page-template>
|
||||
<template #title>
|
||||
{{$t('forgot_password__password_recovery')}}
|
||||
</template>
|
||||
<pn-scroll-list>
|
||||
<account-helper :type :email/>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
</pn-page-template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<pn-page-template>
|
||||
<template #title>
|
||||
<pn-account-block-name/>
|
||||
<q-btn
|
||||
@@ -10,8 +10,7 @@
|
||||
/>
|
||||
</template>
|
||||
|
||||
<pn-scroll-list>
|
||||
<q-list separator>
|
||||
<q-list>
|
||||
<q-item
|
||||
v-for="item in displayItems"
|
||||
:key="item.id"
|
||||
@@ -38,8 +37,6 @@
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
|
||||
<pn-small-dialog
|
||||
v-model="showStopUsingDialog"
|
||||
@@ -51,6 +48,7 @@
|
||||
mainBtnLabel="account__stop_using_dialog_btn_ok"
|
||||
@clickMainBtn="onConfirmStopUsing"
|
||||
/>
|
||||
</pn-page-template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -84,7 +82,9 @@
|
||||
{ id: 7, name: 'account__support', icon: 'mdi-lifebuoy', description: 'account__support_description', iconColor: 'info', pathName: 'support' },
|
||||
{ id: 8, name: 'account__terms_of_use', icon: 'mdi-book-open-variant-outline', description: '', iconColor: 'grey', pathName: 'terms' },
|
||||
{ id: 9, name: 'account__privacy', icon: 'mdi-lock-outline', description: '', iconColor: 'grey', pathName: 'privacy' },
|
||||
{ id: 10, name: 'account__stop_using', icon: 'mdi-close-box-outline', description: 'account__stop_using_description', iconColor: 'negative', pathName: 'stop_using' }
|
||||
{ id: 10, name: 'account__consent_pd', icon: 'mdi-account-lock-outline', description: '', iconColor: 'grey', pathName: 'consent' },
|
||||
{ id: 11, name: 'account__tarrifs_info', icon: 'mdi-star-circle-outline', description: '', iconColor: 'grey', pathName: 'tarrifs_info' },
|
||||
{ id: 12, name: 'account__stop_using', icon: 'mdi-close-box-outline', description: 'account__stop_using_description', iconColor: 'negative', pathName: 'stop_using' }
|
||||
]))
|
||||
|
||||
const displayItems = computed(() => (
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<pn-page-template>
|
||||
<template #title>
|
||||
{{$t('agreements__title')}}
|
||||
</template>
|
||||
<pn-scroll-list>
|
||||
|
||||
<div class="flex column w100 q-py-md q-px-lg q-gutter-y-sm text-caption">
|
||||
<div class="text-body1">
|
||||
<div v-if="route.query.type === 'update'">{{ $t('agreements__description_update') }}</div>
|
||||
<div>{{ $t('agreements__description') }}</div>
|
||||
</div>
|
||||
<agreement-block
|
||||
v-model="agreement"
|
||||
/>
|
||||
<agreement-block v-model="agreement"/>
|
||||
</div>
|
||||
</pn-scroll-list>
|
||||
<template #footer>
|
||||
|
||||
<template #card-actions>
|
||||
<div class="column no-wrap items-center w100">
|
||||
<q-btn
|
||||
color="primary"
|
||||
@@ -42,7 +40,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</pn-page-card>
|
||||
</pn-page-template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<pn-page-template>
|
||||
<template #title>
|
||||
{{ $t('chat_card__title') }}
|
||||
</template>
|
||||
<template #footer>
|
||||
|
||||
<template #card-actions>
|
||||
<q-btn
|
||||
rounded color="primary"
|
||||
class="w100"
|
||||
@@ -14,7 +15,6 @@
|
||||
</q-btn>
|
||||
</template>
|
||||
|
||||
<pn-scroll-list>
|
||||
<div
|
||||
v-if="chat"
|
||||
class="flex column items-center q-px-md q-pt-md q-pb-sm"
|
||||
@@ -49,7 +49,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-list separator v-if="chatUsers.length !== 0">
|
||||
<q-list v-if="chatUsers.length !== 0">
|
||||
<q-item-label class="text-caption text-bold q-py-none" header>
|
||||
{{ $t('chat_card__members') + ' (' + chatUsers.length +')' }}
|
||||
</q-item-label>
|
||||
@@ -71,8 +71,7 @@
|
||||
</pn-user-list-item-body>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
</pn-page-template>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<pn-page-template>
|
||||
<template #title>
|
||||
{{$t('mask__title')}}
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<template #card-actions>
|
||||
<q-btn
|
||||
rounded color="primary"
|
||||
class="w100 fix-disabled-btn"
|
||||
@@ -14,15 +14,15 @@
|
||||
</q-btn>
|
||||
</template>
|
||||
|
||||
<pn-scroll-list>
|
||||
<template #card-body-header>
|
||||
<div class="q-ma-md flex no-wrap items-center">
|
||||
<template #card-header>
|
||||
<div class="q-pa-md flex no-wrap items-center bg-grey-2">
|
||||
<q-icon name="mdi-information q-pr-sm" size="sm" color="primary"/>
|
||||
<span class="text-caption">
|
||||
{{ $t('mask__info_block')}}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<q-table
|
||||
flat
|
||||
:rows="displayCompanies"
|
||||
@@ -138,7 +138,6 @@
|
||||
</template>
|
||||
</q-table>
|
||||
<div class="q-py-none flex column w100" style="min-height: 18px"/>
|
||||
</pn-scroll-list>
|
||||
|
||||
<q-dialog v-model="showDialogHelp">
|
||||
<q-card class="q-ma-sm w100 rounded-card">
|
||||
@@ -156,7 +155,7 @@
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</pn-page-card>
|
||||
</pn-page-template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -5,16 +5,23 @@
|
||||
<q-scroll-area
|
||||
style="height: 100vh"
|
||||
>
|
||||
<div class="flex items-center justify-center" style="height: 100vh">
|
||||
<q-card
|
||||
id="login_block"
|
||||
flat
|
||||
class="flex column no-wrap items-center justify-between q-px-xl q-py-md login-card"
|
||||
style="max-width: 400px;"
|
||||
<div
|
||||
class="w100 flex justify-center items-center"
|
||||
style="min-height: 100vh;"
|
||||
>
|
||||
<pn-glass
|
||||
id="login_block"
|
||||
class="rounded-card flex items-center q-pa-lg q-my-md shadow-1"
|
||||
style="max-width: 300px;"
|
||||
>
|
||||
<div class="flex column no-wrap items-center">
|
||||
<div class="q-pb-lg">
|
||||
<base-logo
|
||||
class="col-grow q-pa-lg text-h5"
|
||||
withBackground
|
||||
animated
|
||||
class="text-h5"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex column w100">
|
||||
<q-input
|
||||
@@ -106,27 +113,23 @@
|
||||
flat rounded
|
||||
sm no-caps
|
||||
color="primary"
|
||||
class="q-my-md w100"
|
||||
class="q-mt-md w100"
|
||||
@click="handleTelegramLogin"
|
||||
>
|
||||
<div class="flex items-center text-primary">
|
||||
<div class="flex items-center text-primary no-wrap w100 justify-center">
|
||||
<q-avatar size="md" class="q-mr-sm">
|
||||
<q-img v-if="tgUser?.photo_url" :src="tgUser.photo_url"/>
|
||||
<q-icon v-else size="md" class="q-mr-none" name="telegram"/>
|
||||
</q-avatar>
|
||||
|
||||
<span>
|
||||
{{
|
||||
tgUser?.first_name +
|
||||
(tgUser?.first_name && tgUser?.last_name ? ' ' : '') +
|
||||
tgUser?.last_name +
|
||||
(!(tgUser?.first_name || tgUser?.last_name) ? tgUser?.username : '')
|
||||
}}
|
||||
<span class="ellipsis">
|
||||
{{ nameTgUser(tgUser) }}
|
||||
</span>
|
||||
</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
</pn-glass>
|
||||
</div>
|
||||
</q-scroll-area>
|
||||
</q-page>
|
||||
@@ -138,11 +141,13 @@
|
||||
import baseLogo from 'components/BaseLogo.vue'
|
||||
import { useInputRules } from 'composables/useInputRules'
|
||||
import { useAuthStore } from 'stores/auth'
|
||||
import type { WebApp } from '@twa-dev/types'
|
||||
import { QInput } from 'quasar'
|
||||
import pnGlass from 'components/pnGlass.vue'
|
||||
import { nameTgUser } from 'utils/helpers'
|
||||
import type { WebApp, WebAppUser } from '@twa-dev/types'
|
||||
|
||||
const tg = inject('tg') as WebApp
|
||||
const tgUser = tg.initDataUnsafe.user
|
||||
const tgUser = tg.initDataUnsafe.user as WebAppUser | undefined
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -1,124 +1,203 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<pn-page-wrapper>
|
||||
<template #title>
|
||||
<project-page-header/>
|
||||
</template>
|
||||
|
||||
<q-tab-panels
|
||||
id="tab-panels"
|
||||
v-model="tabSelect"
|
||||
keep-alive
|
||||
class="tab-panel-color full-height-panel w100 flex column col-grow no-scroll"
|
||||
class="bg-transparent w100"
|
||||
style="flex-grow: 1;"
|
||||
>
|
||||
<q-tab-panel
|
||||
v-for="tab in tabs"
|
||||
:key="tab.name"
|
||||
:name="tab.name"
|
||||
class="q-pa-none flex column col-grow no-scroll"
|
||||
style="flex-grow: 2"
|
||||
class="q-pa-none column w100"
|
||||
>
|
||||
<router-view/>
|
||||
<router-view :tabsSlotHeight/>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
|
||||
<template #tabs>
|
||||
<q-footer class="bg-grey-1 text-grey main-content">
|
||||
<div class="q-pa-none q-ma-none">
|
||||
<q-resize-observer @resize="onTabsSlotResize"/>
|
||||
<div class="q-py-md q-px-md overflow-hidden rounded-fix">
|
||||
<div style="height: 0px;" class="row no-wrap no-pointer-events overflow-hidden">
|
||||
<div
|
||||
v-for="(tab, idx) in tabs"
|
||||
:key="idx"
|
||||
class="text-caption"
|
||||
>
|
||||
<q-resize-observer @resize="val => onTabResize(val, idx)"/>
|
||||
{{ $t(tab.label) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-tabs
|
||||
style = "z-index: 1000"
|
||||
v-model="tabSelect"
|
||||
dense
|
||||
align="justify"
|
||||
align="center"
|
||||
id="tabs"
|
||||
:breakpoint="0"
|
||||
class="w100 text-grey-7 bg-grey-2 shadow-1 rounded-fix"
|
||||
style="z-index: 1000;"
|
||||
:style="minWidthTab"
|
||||
>
|
||||
<q-route-tab
|
||||
v-for="tab in tabs"
|
||||
:to="tab.to"
|
||||
:to="{ name: tab.name }"
|
||||
:name="tab.name"
|
||||
:key="tab.name"
|
||||
:ripple="false"
|
||||
no-caps
|
||||
dense
|
||||
:class="tabSelect === tab.name ? 'active' : ''"
|
||||
class="flex column w100"
|
||||
:class="(tabSelect === tab.name ? 'active' : '')"
|
||||
>
|
||||
<template #default>
|
||||
<div class="flex column items-center">
|
||||
<q-icon :name="tab.icon" :size="maxTabWidth < baseTabWidth ? 'sm' : 'md'"/>
|
||||
<div
|
||||
class="text-caption flex justify-center"
|
||||
:style="{ width: (baseTabWidth - 32) + 'px'}"
|
||||
v-if="maxTabWidth < baseTabWidth"
|
||||
<div class="column items-center">
|
||||
<q-icon :name="tab.icon" :size="config?.icon"/>
|
||||
<span
|
||||
v-if="config?.showLabel"
|
||||
class="text-caption"
|
||||
>
|
||||
<span>
|
||||
<q-resize-observer @resize="size => tab.width = size.width"/>
|
||||
{{$t(tab.label)}}
|
||||
{{ $t(tab.label) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</q-route-tab>
|
||||
<q-resize-observer @resize="size => tabsWidth = size.width" />
|
||||
</q-tabs>
|
||||
</q-footer>
|
||||
<div class="bg-transparent"
|
||||
style="height: var(--tg-safe-area-inset-bottom)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</pn-page-card>
|
||||
</pn-page-wrapper>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onBeforeMount, computed } from 'vue'
|
||||
import { useChatsStore } from 'stores/chats'
|
||||
import { useCompaniesStore } from 'stores/companies'
|
||||
import { useUsersStore } from 'stores/users'
|
||||
import projectPageHeader from 'pages/project-page/ProjectPageHeader.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const chatsStore = useChatsStore()
|
||||
const usersStore = useUsersStore()
|
||||
const companiesStore = useCompaniesStore()
|
||||
|
||||
const chatsQty = computed(() => chatsStore.chats.length)
|
||||
const usersQty = computed(() => usersStore.users.length)
|
||||
const companiesQty = computed(() => companiesStore.getCompanies.length)
|
||||
|
||||
const tabs = ref([
|
||||
{name: 'chats', label: 'project__chats', icon: 'mdi-chat-outline', to: { name: 'chats'}, qty: chatsQty.value, width: 0 },
|
||||
{name: 'users', label: 'project__users', icon: 'mdi-account-outline', to: { name: 'users'}, qty: usersQty.value, width: 0 },
|
||||
{name: 'companies', label: 'project__companies', icon: 'mdi-account-group-outline', to: { name: 'companies'}, qty: companiesQty.value, width: 0 }
|
||||
])
|
||||
|
||||
// hidden icon name if overflow - with resize-observer
|
||||
const tabsWidth = ref(0)
|
||||
const baseTabWidth = computed(() => Math.floor(tabsWidth.value / 3))
|
||||
const maxTabWidth = computed(() => Math.max(...tabs.value.map(el => el.width)))
|
||||
const tabs = [
|
||||
{ name: 'chats', label: 'project__chats', icon: 'mdi-chat-outline' },
|
||||
{ name: 'users', label: 'project__users', icon: 'mdi-account-outline' },
|
||||
{ name: 'companies', label: 'project__companies', icon: 'mdi-account-group-outline' }
|
||||
] as const
|
||||
|
||||
const tabSelect = ref<string>()
|
||||
|
||||
onBeforeMount(() => {
|
||||
const initialTab = tabs.value.find(t => t.to.name === route.name)?.name || tabs.value[0]?.name || ''
|
||||
const initialTab = tabs.find(t => t.name === route.name)?.name || tabs[0]?.name || ''
|
||||
tabSelect.value = initialTab
|
||||
})
|
||||
|
||||
const tabsSlotHeight = ref(0)
|
||||
|
||||
interface sizeParams {
|
||||
height: number,
|
||||
width: number
|
||||
}
|
||||
|
||||
function onTabsSlotResize(size: sizeParams) {
|
||||
tabsSlotHeight.value = size.height
|
||||
}
|
||||
|
||||
// hidden icon name if overflow - with resize-observer
|
||||
const tabsWidth = ref(0)
|
||||
const widthTabLabel = ref(new Array(tabs.length).fill(0))
|
||||
|
||||
function onTabResize (size: sizeParams, idx: number) {
|
||||
if (size.width) widthTabLabel.value[idx] = size.width
|
||||
}
|
||||
|
||||
const config = computed(() => {
|
||||
const ICON_SM = 24
|
||||
const ICON_MD = 32
|
||||
|
||||
const availableWidth = tabsWidth.value
|
||||
|
||||
const variants = [
|
||||
{ icon: 'sm', gap: 16, showLabel: true },
|
||||
{ icon: 'sm', gap: 8, showLabel: true },
|
||||
{ icon: 'md', gap: 16, showLabel: false },
|
||||
{ icon: 'md', gap: 8, showLabel: false },
|
||||
{ icon: 'sm', gap: 16, showLabel: false },
|
||||
{ icon: 'sm', gap: 8, showLabel: false }
|
||||
] as const
|
||||
|
||||
for (const v of variants) {
|
||||
const iconW = v.icon === 'sm' ? ICON_SM : ICON_MD
|
||||
let totalNeeded = 0
|
||||
|
||||
tabs.forEach((_, idx) => {
|
||||
const labelW = v.showLabel ? widthTabLabel.value[idx] : 0
|
||||
const contentW = Math.max(iconW, labelW)
|
||||
totalNeeded += contentW + (v.gap * 2)
|
||||
})
|
||||
|
||||
if (totalNeeded <= availableWidth || v === variants[variants.length - 1]) {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return variants[variants.length - 1]
|
||||
})
|
||||
|
||||
const minWidthTab = computed(() => {
|
||||
const activeIdx = tabs.findIndex(el => el.name === tabSelect.value)
|
||||
const currentGap = config.value?.gap ?? 8
|
||||
|
||||
const labelW = config.value?.showLabel ? widthTabLabel.value[activeIdx] : 0
|
||||
const iconW = config.value?.icon === 'sm' ? 24 : 32
|
||||
const activeContentWidth = Math.max(labelW, iconW)
|
||||
|
||||
return {
|
||||
'--current-gap': `${currentGap}px`,
|
||||
'--min-width-tab': '1px',
|
||||
'--active-tab-width': (activeContentWidth + 16 * 2) + 'px',
|
||||
'--tabs-height': tabsSlotHeight.value - 32 + 'px'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.active {
|
||||
<style scoped lang="scss">
|
||||
#tabs :deep(.q-tab) {
|
||||
padding: 0 var(--current-gap);
|
||||
}
|
||||
|
||||
#tabs :deep(.active) {
|
||||
color: var(--q-primary)
|
||||
}
|
||||
|
||||
.tab-panel-color {
|
||||
background: transparent
|
||||
#tabs :deep(.q-router-link--active .q-tab__indicator) {
|
||||
height: calc(var(--tabs-height) - var(--tg-safe-area-inset-bottom, 0px));
|
||||
border-radius: calc(var(--tabs-height) / 2);
|
||||
overflow: hidden;
|
||||
opacity: 0.2;
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
.full-height-panel > .q-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 2;
|
||||
width: 100%
|
||||
#tabs :deep(.q-focus-helper) {
|
||||
border-radius: calc(var(--tabs-height) / 2);
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
</style>
|
||||
#tabs :deep(.q-tabs__arrow) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
<style scoped>
|
||||
#tabs :deep(.q-tab__indicator) {
|
||||
height: 3px !important;
|
||||
}
|
||||
#tabs :deep(.q-tab__content) {
|
||||
min-width: var(--min-width-tab);
|
||||
}
|
||||
|
||||
#tabs :deep(.q-tab--active .q-tab__indicator) {
|
||||
width: var(--active-tab-width);
|
||||
right: inherit;
|
||||
left: inherit;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<pn-page-template>
|
||||
<template #title>
|
||||
<div class="q-pr-md ellipsis">
|
||||
{{ $t('projects__title') }}
|
||||
</div>
|
||||
<q-btn
|
||||
@click="goAccount"
|
||||
flat rounded
|
||||
@@ -14,27 +16,18 @@
|
||||
<pn-account-block-name/>
|
||||
</q-btn>
|
||||
</template>
|
||||
<pn-scroll-list
|
||||
:hideHeader="projects.length === 0 && archiveProjects.length === 0"
|
||||
>
|
||||
<template #card-body-header>
|
||||
<q-input
|
||||
<template #card-header>
|
||||
<pn-action-bar
|
||||
v-model="searchProject"
|
||||
clearable
|
||||
clear-icon="close"
|
||||
:placeholder="$t('projects__search')"
|
||||
dense filled
|
||||
class="col-grow q-px-md q-py-md"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
</q-input>
|
||||
placeholder="projects__search"
|
||||
:show-calendar-btn="false"
|
||||
:show-filter-btn="false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<q-list separator v-if="projects.length !== 0">
|
||||
<q-list v-if="projects.length !== 0">
|
||||
<template
|
||||
v-for = "item in activeProjects"
|
||||
v-for="item in activeProjects"
|
||||
:key="item.id"
|
||||
>
|
||||
<q-slide-item
|
||||
@@ -125,7 +118,7 @@
|
||||
enter-active-class="animated slideInDown"
|
||||
leave-active-class="animated slideOutUp"
|
||||
>
|
||||
<q-list separator v-if="showArchive" class="w100">
|
||||
<q-list v-if="showArchive" class="w100">
|
||||
<q-item
|
||||
v-for = "item in archiveProjects"
|
||||
:key="item.id"
|
||||
@@ -160,37 +153,34 @@
|
||||
<pn-onboard-btn
|
||||
v-if="projects.length===0 && projectsInit"
|
||||
icon="mdi-briefcase-plus-outline"
|
||||
:message1="$t('projects__lets_start')"
|
||||
:message2="$t('projects__lets_start_description')"
|
||||
message1="projects__lets_start"
|
||||
message2="projects__lets_start_description"
|
||||
@btn-click="createNewProject"
|
||||
/>
|
||||
<div
|
||||
class="flex column justify-center items-center w100"
|
||||
class="column justify-center items-center w100"
|
||||
style="position: absolute; bottom: 0;"
|
||||
v-if="!projectsInit"
|
||||
>
|
||||
<q-linear-progress indeterminate />
|
||||
</div>
|
||||
</pn-scroll-list>
|
||||
|
||||
<q-page-sticky
|
||||
position="bottom-right"
|
||||
:offset="[0, 18]"
|
||||
class="fix-fab-offset"
|
||||
>
|
||||
<template #card-fab>
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated zoomIn"
|
||||
>
|
||||
<pn-glass-btn type="fab" color="primary">
|
||||
<q-btn
|
||||
fab
|
||||
icon="add"
|
||||
color="brand"
|
||||
color="transparent"
|
||||
@click="createNewProject"
|
||||
/>
|
||||
</pn-glass-btn>
|
||||
</transition>
|
||||
</q-page-sticky>
|
||||
</pn-page-card>
|
||||
</template>
|
||||
</pn-page-template>
|
||||
|
||||
<pn-small-dialog
|
||||
v-model="showDialogArchiveProject"
|
||||
@@ -221,6 +211,8 @@
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useProjectsStore } from 'stores/projects'
|
||||
import pnGlassBtn from 'components/pnGlassBtn.vue'
|
||||
import pnActionBar from 'components/pnActionBar.vue'
|
||||
import type { Project } from 'types/Project'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<doc-block title="consent__title" document-name="Consent_to_Personal_Data_Processing"/>
|
||||
<doc-block
|
||||
title="account__consent_pd"
|
||||
document-name="Consent_to_Personal_Data_Processing"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<doc-block title="privacy__title" document-name="Privacy-Policy"/>
|
||||
<doc-block
|
||||
title="account__privacy"
|
||||
document-name="Privacy-Policy"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<pn-page-template>
|
||||
<template #title>
|
||||
{{ $t('settings__title') }}
|
||||
</template>
|
||||
|
||||
<pn-scroll-list>
|
||||
<q-list separator>
|
||||
<q-list>
|
||||
<q-item-label header>{{ $t('settings__software_title') }}</q-item-label>
|
||||
|
||||
<pn-item-btm-dialog
|
||||
@@ -80,8 +79,7 @@
|
||||
</pn-item-btm-dialog>
|
||||
</template>
|
||||
</q-list>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
</pn-page-template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<pn-page-template>
|
||||
<template #title>
|
||||
{{$t('subscribe__title')}}
|
||||
</template>
|
||||
<pn-scroll-list>
|
||||
<template #card-body-header>
|
||||
|
||||
<template #card-header>
|
||||
<div
|
||||
v-if="tariffs && currentTariff"
|
||||
id="subscribe-current-balance"
|
||||
class="flex w100 q-px-md q-py-md row"
|
||||
class="flex w100 q-px-md q-py-md row bg-grey-11"
|
||||
>
|
||||
|
||||
<div class="flex w100 justify-between items-center no-wrap">
|
||||
@@ -52,6 +52,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="tariffs && currentTariff">
|
||||
<subscribe-upgrade
|
||||
v-if="isCurrentTariffTest"
|
||||
@@ -68,8 +69,7 @@
|
||||
@update="prepareTgBtn"
|
||||
/>
|
||||
</div>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
</pn-page-template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<template>
|
||||
<pn-page-card>
|
||||
<pn-page-template>
|
||||
<template #title>
|
||||
{{ $t('support__title') }}
|
||||
</template>
|
||||
|
||||
<pn-scroll-list>
|
||||
<div class="q-pa-md flex column w100 items-center">
|
||||
<div align="center" >
|
||||
<div>{{ $t('support__work_time_text') }}</div>
|
||||
@@ -33,8 +32,7 @@
|
||||
{{ supportEmailAddress }}
|
||||
</div>
|
||||
</div>
|
||||
</pn-scroll-list>
|
||||
</pn-page-card>
|
||||
</pn-page-template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<doc-block title="terms_of_use__title" document-name="Terms_of_use"/>
|
||||
<doc-block
|
||||
title="account__terms_of_use"
|
||||
document-name="Terms_of_use"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<template>
|
||||
<div class="q-pa-none flex column col-grow no-scroll">
|
||||
<pn-scroll-list
|
||||
:hideHeader="chats.length === 0"
|
||||
>
|
||||
<template #card-body-header>
|
||||
<pn-page-card v-bind="$attrs">
|
||||
<template #card-header>
|
||||
<pn-action-bar
|
||||
v-if="chats.length !== 0"
|
||||
v-model="search"
|
||||
placeholder="chats__search"
|
||||
:show-calendar-btn="false"
|
||||
@@ -12,27 +10,7 @@
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #card-body-btn>
|
||||
<div
|
||||
v-if="chats.length !== 0 || obsoleteChats.length !== 0"
|
||||
class="w100 flex justify-end q-px-md"
|
||||
>
|
||||
<q-btn
|
||||
@click="showDialogHelp = !showDialogHelp"
|
||||
flat dense no-caps
|
||||
class="q-my-xs"
|
||||
>
|
||||
<div class="text-caption flex no-wrap items-center q-px-xs text-grey">
|
||||
<span class="q-pr-xs">
|
||||
{{ $t("chats__help_chat_type") }}
|
||||
</span>
|
||||
<q-icon name="mdi-help-circle-outline" size="xs"/>
|
||||
</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<q-list separator v-if="chats.length !== 0">
|
||||
<q-list v-if="chats.length !== 0">
|
||||
<q-slide-item
|
||||
v-for="item in displayChats"
|
||||
:key="item.id"
|
||||
@@ -97,7 +75,7 @@
|
||||
</q-list>
|
||||
|
||||
<div
|
||||
v-if="obsoleteChats.length !== 0"
|
||||
v-if="displayObsoleteChats.length !== 0"
|
||||
class="flex column items-center w100"
|
||||
:class="showObsoleteChats ? 'bg-grey-12' : ''"
|
||||
>
|
||||
@@ -106,13 +84,13 @@
|
||||
color="grey"
|
||||
flat
|
||||
no-caps
|
||||
@click="showObsoleteChats=!showObsoleteChats"
|
||||
@click="showObsoleteChats = !showObsoleteChats"
|
||||
icon-right="arrow_drop_down"
|
||||
:class="{ 'rotate-icon': showObsoleteChats }"
|
||||
>
|
||||
<span class="text-caption">
|
||||
{{ !showObsoleteChats
|
||||
? $t('chats__show_obsolete_chats') + ' (' + obsoleteChats.length +')'
|
||||
? $t('chats__show_obsolete_chats') + ' (' + displayObsoleteChats.length +')'
|
||||
: $t('chats__hide_obsolete_chats')
|
||||
}}
|
||||
</span>
|
||||
@@ -124,9 +102,9 @@
|
||||
enter-active-class="animated slideInDown"
|
||||
leave-active-class="animated slideOutUp"
|
||||
>
|
||||
<q-list separator v-if="showObsoleteChats" class="w100">
|
||||
<q-list v-if="showObsoleteChats" class="w100">
|
||||
<q-item
|
||||
v-for = "item in obsoleteChats"
|
||||
v-for = "item in displayObsoleteChats"
|
||||
:key="item.id"
|
||||
class="w100 text-grey"
|
||||
>
|
||||
@@ -168,58 +146,71 @@
|
||||
<pn-onboard-btn
|
||||
v-if="chats.length === 0 && obsoleteChats.length === 0 && chatsInit"
|
||||
icon="mdi-chat-plus-outline"
|
||||
:message1="$t('chats__onboard_msg1')"
|
||||
:message2="$t('chats__onboard_msg2')"
|
||||
@click="showOverlay=true; fabState=true"
|
||||
message1="chats__onboard_msg1"
|
||||
message2="chats__onboard_msg2"
|
||||
@click="showAttachChatDialog=true; fabState=true"
|
||||
/>
|
||||
<div
|
||||
class="flex column justify-center items-center w100"
|
||||
class="column justify-center items-center w100"
|
||||
style="position: absolute; bottom: 0;"
|
||||
v-if="!chatsInit"
|
||||
>
|
||||
<q-linear-progress indeterminate />
|
||||
</div>
|
||||
</pn-scroll-list>
|
||||
|
||||
<q-page-sticky
|
||||
:style="{ zIndex: !showOverlay ? 'inherit' : '5100 !important' }"
|
||||
position="bottom-right"
|
||||
:offset="[0, 18]"
|
||||
class="fix-fab-offset"
|
||||
>
|
||||
<template #card-fab>
|
||||
<div v-if="chats.length !== 0 || obsoleteChats.length !== 0">
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated zoomIn"
|
||||
>
|
||||
<q-fab
|
||||
v-model="fabState"
|
||||
v-if="showFab"
|
||||
icon="add"
|
||||
color="brand"
|
||||
:class="isEnableChatAdd ? '' : 'fab-disabled'"
|
||||
direction="up"
|
||||
vertical-actions-align="right"
|
||||
@click="showOverlay = !showOverlay"
|
||||
:disable="!tg.initData"
|
||||
>
|
||||
<q-fab-action
|
||||
square
|
||||
clickable
|
||||
v-ripple
|
||||
class="bg-white change-fab-action q-pa-none"
|
||||
style="cursor: default"
|
||||
label-style=""
|
||||
<pn-glass-btn
|
||||
type="fab"
|
||||
color="grey-2"
|
||||
>
|
||||
<q-btn
|
||||
@click="showDialogHelp = !showDialogHelp"
|
||||
fab
|
||||
icon="mdi-help"
|
||||
color="transparent"
|
||||
class="text-grey"
|
||||
/>
|
||||
</pn-glass-btn>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<template #icon>
|
||||
<q-list class="q-py-sm" v-if="isEnableChatAdd">
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated zoomIn"
|
||||
>
|
||||
<pn-glass-btn
|
||||
type="fab"
|
||||
color="primary"
|
||||
>
|
||||
<q-btn
|
||||
@click="showAttachChatDialog = !showAttachChatDialog"
|
||||
fab
|
||||
icon="add"
|
||||
color="transparent"
|
||||
:disable="!tg.initData"
|
||||
/>
|
||||
</pn-glass-btn>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<pn-simple-scroll-dialog
|
||||
v-model="showAttachChatDialog"
|
||||
title="chats__attach_dialog_title"
|
||||
>
|
||||
<q-list v-if="isEnableChatAdd">
|
||||
<q-item
|
||||
v-for="item in fabMenu"
|
||||
:key="item.id"
|
||||
class="w100"
|
||||
:clickable="item.type !== 'header'"
|
||||
v-ripple
|
||||
@click="item.type !== 'header' && item.func ? item?.func() : () => {}"
|
||||
:v-ripple="item.type !== 'header'"
|
||||
@click="item.type !== 'header' && item.func
|
||||
? (showAttachChatDialog = false, item.func())
|
||||
: null"
|
||||
>
|
||||
<template v-if="item.type === 'header'">
|
||||
<div class="w100 column" :class="item.id === 4 ? 'q-pt-md' : ''">
|
||||
@@ -250,6 +241,7 @@
|
||||
</template>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
||||
<div v-else class="w100 bg-white column q-py-md q-px-md rounded-card fab-action-item">
|
||||
<div class="text-negative">
|
||||
{{ $t('chats__disable_add_chat') }}
|
||||
@@ -258,14 +250,7 @@
|
||||
{{ $t('chats__disable_add_chat_description') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</q-fab-action>
|
||||
</q-fab>
|
||||
</transition>
|
||||
</q-page-sticky>
|
||||
</div>
|
||||
|
||||
<pn-overlay v-if="showOverlay"/>
|
||||
</pn-simple-scroll-dialog>
|
||||
|
||||
<pn-small-dialog
|
||||
v-model="showDialogDeleteChat"
|
||||
@@ -285,7 +270,7 @@
|
||||
title="chat__help_title"
|
||||
>
|
||||
<div class="q-py-none q-px-md q-ma-none">
|
||||
<q-list class="q-pt-sm">
|
||||
<q-list>
|
||||
<pn-chat-type-item :access="2"/>
|
||||
<pn-chat-type-item :access="1"/>
|
||||
<pn-chat-type-item :state="2"/>
|
||||
@@ -306,21 +291,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</pn-simple-scroll-dialog>
|
||||
|
||||
<template #overlay>
|
||||
<pn-overlay v-if="showAttachChatDialog" style="z-index:1005;"/>
|
||||
</template>
|
||||
</pn-page-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onActivated, onDeactivated, onBeforeUnmount, inject } from 'vue'
|
||||
import { ref, computed, inject } from 'vue'
|
||||
import { useAuthStore } from 'stores/auth'
|
||||
import { useChatsStore } from 'stores/chats'
|
||||
import type { WebApp } from '@twa-dev/types'
|
||||
import type { Chat } from 'types/Chat'
|
||||
import type { Chat, ObsoleteChat } from 'types/Chat'
|
||||
import { useI18n } from "vue-i18n"
|
||||
import { date } from 'quasar'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import pnChatTypeItem from 'components/pnChatTypeItem.vue'
|
||||
import pnSimpleScrollDialog from 'components/pnSimpleScrollDialog.vue'
|
||||
import pnActionBar from 'components/pnActionBar.vue'
|
||||
import pnGlassBtn from 'components/pnGlassBtn.vue'
|
||||
import { chatAccess, chatState } from 'utils/chats'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -329,7 +318,7 @@
|
||||
const { t } = useI18n()
|
||||
|
||||
const search = ref('')
|
||||
const showOverlay = ref<boolean>(false)
|
||||
const showAttachChatDialog = ref<boolean>(false)
|
||||
const chatsStore = useChatsStore()
|
||||
const showDialogDeleteChat = ref<boolean>(false)
|
||||
const deleteChatId = ref<number | undefined>(undefined)
|
||||
@@ -444,28 +433,6 @@
|
||||
await router.push({ name: 'chat_info', params: { id: route.params.id, chatId }})
|
||||
}
|
||||
|
||||
// fix fab jumping
|
||||
const showFab = ref(false)
|
||||
const timerId = ref<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
onActivated(() => {
|
||||
timerId.value = setTimeout(() => {
|
||||
showFab.value = true
|
||||
}, 300)
|
||||
})
|
||||
|
||||
onDeactivated(() => {
|
||||
showFab.value = false
|
||||
if (timerId.value) {
|
||||
clearTimeout(timerId.value)
|
||||
timerId.value = null
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timerId.value) clearTimeout(timerId.value)
|
||||
})
|
||||
|
||||
function getDate (d: number) {
|
||||
return new Date(d * 1000).getFullYear() === new Date(Date.now()).getFullYear()
|
||||
? date.formatDate(d * 1000, 'DD MMM')
|
||||
@@ -475,31 +442,19 @@
|
||||
const obsoleteChats = computed(() => chatsStore.obsoleteChats)
|
||||
const showObsoleteChats = ref(false)
|
||||
|
||||
const displayObsoleteChats = computed(() => {
|
||||
return obsoleteChats.value
|
||||
.filter(searchChats)
|
||||
|
||||
function searchChats (el: ObsoleteChat) {
|
||||
if (!search.value || !(search.value && search.value.trim())) return true
|
||||
const searchValue = search.value.trim().toLowerCase()
|
||||
return el.name.toLowerCase().includes(searchValue)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* width of choose element */
|
||||
.change-fab-action .q-fab__label--internal {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.change-fab-action :deep(.q-focus-helper) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.q-item :deep(.q-focus-helper) {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.change-fab-action {
|
||||
width: calc(min(100vw, var(--body-width)) - 48px) !important;
|
||||
border-radius: var(--top-radius)
|
||||
}
|
||||
|
||||
.fab-action-item {
|
||||
text-wrap: auto !important;
|
||||
}
|
||||
|
||||
/* fix mini border after slide */
|
||||
:deep(.q-slide-item__right)
|
||||
{
|
||||
|
||||
@@ -1,33 +1,6 @@
|
||||
<template>
|
||||
<div class="q-pa-none flex column col-grow no-scroll">
|
||||
<pn-scroll-list
|
||||
hideHeader
|
||||
>
|
||||
<template #card-body-btn>
|
||||
<div class="flex items-center justify-end q-pa-sm w100" v-if="companies.length !== 0">
|
||||
<q-btn
|
||||
:color="companies.length <= 2 ? 'grey-6' : 'primary'"
|
||||
flat
|
||||
no-caps
|
||||
@click="maskCompany"
|
||||
:disable="companies.length <= 2"
|
||||
class="q-pr-md"
|
||||
rounded
|
||||
>
|
||||
<q-icon
|
||||
left
|
||||
size="sm"
|
||||
name="mdi-domino-mask"
|
||||
class="q-mr-xs"
|
||||
/>
|
||||
<div>
|
||||
{{ $t('companies__mask')}}
|
||||
</div>
|
||||
</q-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<q-list separator v-if="companies.length !== 0">
|
||||
<pn-page-card v-bind="$attrs">
|
||||
<q-list v-if="companies.length !== 0">
|
||||
<q-slide-item
|
||||
v-for="item in displayCompanies"
|
||||
:key="item.id"
|
||||
@@ -97,38 +70,56 @@
|
||||
<pn-onboard-btn
|
||||
v-if="companies.length <= 1 && companiesInit"
|
||||
icon="mdi-account-multiple-plus-outline"
|
||||
:message1="$t('companies__onboard_msg1')"
|
||||
:message2="$t('companies__onboard_msg2')"
|
||||
message1="companies__onboard_msg1"
|
||||
message2="companies__onboard_msg2"
|
||||
@btn-click="createCompany"
|
||||
/>
|
||||
<div
|
||||
class="flex column justify-center items-center w100"
|
||||
class="column justify-center items-center w100"
|
||||
style="position: absolute; bottom: 0;"
|
||||
v-if="!companiesInit"
|
||||
>
|
||||
<q-linear-progress indeterminate />
|
||||
</div>
|
||||
</pn-scroll-list>
|
||||
<template #card-fab>
|
||||
|
||||
<q-page-sticky
|
||||
position="bottom-right"
|
||||
:offset="[0, 18]"
|
||||
class="fix-fab-offset"
|
||||
>
|
||||
<div v-if="companies.length >= 3">
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated zoomIn"
|
||||
>
|
||||
<pn-glass-btn
|
||||
type="fab"
|
||||
color="grey-2"
|
||||
>
|
||||
<q-btn
|
||||
@click="maskCompany"
|
||||
fab
|
||||
icon="mdi-domino-mask"
|
||||
color="transparent"
|
||||
class="text-grey"
|
||||
/>
|
||||
</pn-glass-btn>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated zoomIn"
|
||||
>
|
||||
<pn-glass-btn
|
||||
type="fab"
|
||||
color="brand"
|
||||
>
|
||||
<q-btn
|
||||
v-if="showFab"
|
||||
fab
|
||||
icon="add"
|
||||
color="brand"
|
||||
color="transparent"
|
||||
@click="createCompany()"
|
||||
/>
|
||||
</pn-glass-btn>
|
||||
</transition>
|
||||
</q-page-sticky>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<pn-small-dialog
|
||||
v-model="showDialogDeleteCompany"
|
||||
@@ -142,13 +133,15 @@
|
||||
@close="onCancel()"
|
||||
@before-hide="onDialogBeforeHide()"
|
||||
/>
|
||||
</pn-page-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onActivated, onDeactivated, onBeforeUnmount } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useCompaniesStore } from 'stores/companies'
|
||||
import { useUsersStore } from 'stores/users'
|
||||
import PnGlassBtn from 'components/pnGlassBtn.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -222,28 +215,6 @@
|
||||
return arr.length
|
||||
}
|
||||
|
||||
// fix fab jumping
|
||||
const showFab = ref(false)
|
||||
const timerId = ref<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
onActivated(() => {
|
||||
timerId.value = setTimeout(() => {
|
||||
showFab.value = true
|
||||
}, 300)
|
||||
})
|
||||
|
||||
|
||||
onDeactivated(() => {
|
||||
showFab.value = false
|
||||
if (timerId.value) {
|
||||
clearTimeout(timerId.value)
|
||||
timerId.value = null
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timerId.value) clearTimeout(timerId.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -55,10 +55,8 @@
|
||||
|
||||
<q-btn
|
||||
@click="toProjects"
|
||||
flat
|
||||
no-caps
|
||||
dense
|
||||
rounded
|
||||
flat no-caps
|
||||
dense rounded
|
||||
class="q-ml-lg"
|
||||
>
|
||||
<span class="flex items-center no-wrap q-pl-sm">
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<template>
|
||||
<div class="q-pa-none flex column col-grow no-scroll">
|
||||
<pn-scroll-list
|
||||
:hideHeader="users.length === 0"
|
||||
>
|
||||
<template #card-body-header>
|
||||
<pn-page-card v-bind="$attrs">
|
||||
<template #card-header>
|
||||
<pn-action-bar
|
||||
v-if="users.length !== 0"
|
||||
v-model="search"
|
||||
placeholder="users__search"
|
||||
:show-calendar-btn="false"
|
||||
@@ -12,7 +10,7 @@
|
||||
/>
|
||||
</template>
|
||||
|
||||
<q-list separator v-if="users.length !== 0">
|
||||
<q-list v-if="users.length !== 0">
|
||||
<q-slide-item
|
||||
v-for="item in displayUsers"
|
||||
:key="item.id"
|
||||
@@ -63,7 +61,7 @@
|
||||
enter-active-class="animated slideInDown"
|
||||
leave-active-class="animated slideOutUp"
|
||||
>
|
||||
<q-list separator v-if="showLeaveUsers" class="w100">
|
||||
<q-list v-if="showLeaveUsers" class="w100">
|
||||
<q-item
|
||||
v-for = "item in leaveUsers"
|
||||
:key="item.id"
|
||||
@@ -106,7 +104,7 @@
|
||||
enter-active-class="animated slideInDown"
|
||||
leave-active-class="animated slideOutUp"
|
||||
>
|
||||
<q-list separator v-if="showBlockedUsers" class="w100">
|
||||
<q-list v-if="showBlockedUsers" class="w100">
|
||||
<q-item
|
||||
v-for = "item in blockedUsers"
|
||||
:key="item.id"
|
||||
@@ -126,19 +124,17 @@
|
||||
<pn-onboard-btn
|
||||
v-if="users.length === 0 && usersInit"
|
||||
icon="mdi-account-outline"
|
||||
:message1="$t('users__onboard_msg1')"
|
||||
:message2="$t('users__onboard_msg2')"
|
||||
message1="users__onboard_msg1"
|
||||
message2="users__onboard_msg2"
|
||||
noBtn
|
||||
/>
|
||||
<div
|
||||
class="flex column justify-center items-center w100"
|
||||
class="column justify-center items-center w100"
|
||||
style="position: absolute; bottom: 0;"
|
||||
v-if="!usersInit"
|
||||
>
|
||||
<q-linear-progress indeterminate />
|
||||
</div>
|
||||
</pn-scroll-list>
|
||||
</div>
|
||||
|
||||
<pn-small-dialog
|
||||
v-model="showDialogBlockUser"
|
||||
@@ -164,6 +160,7 @@
|
||||
@clickMainBtn="onConfirmRestoreUser()"
|
||||
/>
|
||||
|
||||
</pn-page-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -45,6 +45,7 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'chats',
|
||||
path: 'chats',
|
||||
component: () => import('pages/project-page/ProjectPageChats.vue'),
|
||||
props: true,
|
||||
meta: {
|
||||
backRoute: 'projects'
|
||||
}
|
||||
@@ -52,6 +53,7 @@ const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
name: 'users',
|
||||
path: 'users',
|
||||
props: true,
|
||||
component: () => import('pages/project-page/ProjectPageUsers.vue'),
|
||||
meta: {
|
||||
backRoute: 'projects'
|
||||
@@ -60,6 +62,7 @@ const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
name: 'companies',
|
||||
path: 'companies',
|
||||
props: true,
|
||||
component: () => import('pages/project-page/ProjectPageCompanies.vue'),
|
||||
meta: {
|
||||
backRoute: 'projects'
|
||||
@@ -138,9 +141,9 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('pages/account/ConsentPage.vue')
|
||||
},
|
||||
{
|
||||
name: 'change_tariff_rules',
|
||||
path: '/change-tariff-rules',
|
||||
component: () => import('src/pages/account/ChangeTariffRules.vue')
|
||||
name: 'tariffs_info',
|
||||
path: '/tariffs-info',
|
||||
component: () => import('pages/account/TariffsInfoPage.vue')
|
||||
},
|
||||
{
|
||||
name: 'support',
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useProjectsStore } from 'stores/projects'
|
||||
import { useAuthStore } from 'stores/auth'
|
||||
import { useChatsStore } from 'stores/chats'
|
||||
import { useUsersStore } from 'stores/users'
|
||||
import { useCompaniesStore } from 'stores/companies'
|
||||
|
||||
export const useSSEStore = defineStore('sse', () => {
|
||||
const eventSource = ref<EventSource | null>(null)
|
||||
@@ -16,6 +17,7 @@ export const useSSEStore = defineStore('sse', () => {
|
||||
const authStore = useAuthStore()
|
||||
const chatsStore = useChatsStore()
|
||||
const usersStore = useUsersStore()
|
||||
const companiesStore = useCompaniesStore()
|
||||
|
||||
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
||||
|
||||
@@ -83,12 +85,24 @@ export const useSSEStore = defineStore('sse', () => {
|
||||
}
|
||||
})
|
||||
|
||||
eventSource.value.addEventListener('customer-update', (event) => {
|
||||
eventSource.value.addEventListener('user-update', (event) => {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data.project_id === currentProjectId.value) {
|
||||
authStore.updateProfile().catch(() => {})
|
||||
usersStore.init().catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
eventSource.value.addEventListener('company-update', (event) => {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data.project_id === currentProjectId.value) {
|
||||
usersStore.init().catch(() => {})
|
||||
companiesStore.init().catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
eventSource.value.addEventListener('customer-update', () => {
|
||||
authStore.updateProfile().catch(() => {})
|
||||
})
|
||||
}
|
||||
|
||||
const eventSourceComputed = computed(() => eventSource.value)
|
||||
|
||||
@@ -3,6 +3,7 @@ interface ProjectParams {
|
||||
description: string | null
|
||||
logo: string | null
|
||||
is_logo_bg: boolean | null
|
||||
chat_info: string | null
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
|
||||
40
src/utils/colors.ts
Normal file
40
src/utils/colors.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { colors } from 'quasar'
|
||||
|
||||
function getColorWithOpacity(
|
||||
color: string,
|
||||
background: string = '#ffffff',
|
||||
alpha: number = 0.8
|
||||
): string {
|
||||
const clampedAlpha = Math.max(0.01, Math.min(1, alpha))
|
||||
|
||||
const hexColor = color.startsWith('#') ? color : colors.getPaletteColor(color)
|
||||
const hexBackground = background.startsWith('#') ? background : colors.getPaletteColor(background)
|
||||
|
||||
if (!hexColor || !hexColor.startsWith('#') || !hexBackground || !hexBackground.startsWith('#')) {
|
||||
return color
|
||||
}
|
||||
|
||||
const { r: rColor, g: gColor, b: bColor } = colors.hexToRgb(hexColor)
|
||||
const { r: rBg, g: gBg, b: bBg } = colors.hexToRgb(hexBackground)
|
||||
|
||||
const calculateTargetChannel = (colorChannel: number, bgChannel: number): number => {
|
||||
const target = (colorChannel - bgChannel * (1 - clampedAlpha)) / clampedAlpha
|
||||
return Math.max(0, Math.min(255, Math.round(target)))
|
||||
}
|
||||
|
||||
const targetChannels = [
|
||||
calculateTargetChannel(rColor, rBg),
|
||||
calculateTargetChannel(gColor, gBg),
|
||||
calculateTargetChannel(bColor, bBg)
|
||||
]
|
||||
|
||||
if (targetChannels.every(channel => channel === 0)) {
|
||||
return `rgba(${rColor}, ${gColor}, ${bColor}, ${clampedAlpha})`
|
||||
}
|
||||
|
||||
return `rgba(${targetChannels.join(', ')}, ${clampedAlpha})`
|
||||
}
|
||||
|
||||
export {
|
||||
getColorWithOpacity
|
||||
}
|
||||
@@ -30,9 +30,20 @@ const formatNumber = (number: string | number) => {
|
||||
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ")
|
||||
}
|
||||
|
||||
import type { WebAppUser } from '@twa-dev/types'
|
||||
|
||||
const nameTgUser = (name: WebAppUser | undefined) => {
|
||||
if (!name || !name.first_name) return 'Error'
|
||||
return name.first_name +
|
||||
(name.first_name && name.last_name ? ' ' : '') +
|
||||
name.last_name +
|
||||
(!(name.first_name || name.last_name) ? name.username : '')
|
||||
}
|
||||
|
||||
export {
|
||||
parseIntString,
|
||||
removeNullKeys,
|
||||
getFieldValue,
|
||||
formatNumber
|
||||
formatNumber,
|
||||
nameTgUser
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user