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",
|
"axios": "^1.2.1",
|
||||||
"browser-image-compression": "^2.0.2",
|
"browser-image-compression": "^2.0.2",
|
||||||
"pinia": "^2.0.11",
|
"pinia": "^2.0.11",
|
||||||
"quasar": "^2.18.2",
|
"quasar": "^2.18.6",
|
||||||
"vue": "^3.4.18",
|
"sass-embedded": "^1.97.2",
|
||||||
|
"vue": "^3.5.27",
|
||||||
"vue-i18n": "^9.2.2",
|
"vue-i18n": "^9.2.2",
|
||||||
"vue-router": "^4.0.12"
|
"vue-router": "^4.0.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.14.0",
|
"@eslint/js": "^9.14.0",
|
||||||
"@intlify/unplugin-vue-i18n": "^2.0.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",
|
"@quasar/quasar-app-extension-qmarkdown": "^2.0.5",
|
||||||
"@twa-dev/types": "^8.0.2",
|
"@twa-dev/types": "^8.0.2",
|
||||||
"@types/node": "^20.17.30",
|
"@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 { boot } from 'quasar/wrappers'
|
||||||
|
import pnPageWrapper from 'components/pnPageWrapper.vue'
|
||||||
import pnPageCard from 'components/pnPageCard.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 pnAutoAvatar from 'components/pnAutoAvatar.vue'
|
||||||
import pnOverlay from 'components/pnOverlay.vue'
|
import pnOverlay from 'components/pnOverlay.vue'
|
||||||
import pnSmallDialog from 'components/pnSmallDialog.vue'
|
import pnSmallDialog from 'components/pnSmallDialog.vue'
|
||||||
@@ -9,10 +10,12 @@ import pnShadowScroll from 'components/pnShadowScroll.vue'
|
|||||||
import pnMagicOverlay from 'components/pnMagicOverlay.vue'
|
import pnMagicOverlay from 'components/pnMagicOverlay.vue'
|
||||||
import pnAccountBlockName from 'components/pnAccountBlockName.vue'
|
import pnAccountBlockName from 'components/pnAccountBlockName.vue'
|
||||||
import pnOnboardBtn from 'components/pnOnboardBtn.vue'
|
import pnOnboardBtn from 'components/pnOnboardBtn.vue'
|
||||||
|
import pnBottomSheetDialog from 'components/pnBottomSheetDialog.vue'
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
|
pnPageWrapper,
|
||||||
pnPageCard,
|
pnPageCard,
|
||||||
pnScrollList,
|
pnPageTemplate,
|
||||||
pnAutoAvatar,
|
pnAutoAvatar,
|
||||||
pnOverlay,
|
pnOverlay,
|
||||||
pnImageSelector,
|
pnImageSelector,
|
||||||
@@ -20,7 +23,8 @@ const components = {
|
|||||||
pnShadowScroll,
|
pnShadowScroll,
|
||||||
pnMagicOverlay,
|
pnMagicOverlay,
|
||||||
pnAccountBlockName,
|
pnAccountBlockName,
|
||||||
pnOnboardBtn
|
pnOnboardBtn,
|
||||||
|
pnBottomSheetDialog
|
||||||
}
|
}
|
||||||
|
|
||||||
export default boot(({ app }) => {
|
export default boot(({ app }) => {
|
||||||
|
|||||||
@@ -10,12 +10,43 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default defineBoot(({ app }) => {
|
export default defineBoot(({ app }) => {
|
||||||
if (window.Telegram?.WebApp) {
|
const tg = window.Telegram?.WebApp
|
||||||
const webApp = window.Telegram.WebApp
|
|
||||||
webApp.ready()
|
tg.ready()
|
||||||
sessionStorage.setItem('isTelegram', webApp.initData ? 'true' : 'false')
|
sessionStorage.setItem('isTelegram', tg.initData ? 'true' : 'false')
|
||||||
webApp.SettingsButton.isVisible = true
|
|
||||||
app.config.globalProperties.$tg = webApp
|
const platform = tg.platform.toLowerCase()
|
||||||
app.provide('tg', webApp)
|
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>
|
<template>
|
||||||
<div
|
<div class="flex row items-center no-wrap logo-component">
|
||||||
class="flex row items-center no-wrap logo-component"
|
<LogoIcon
|
||||||
>
|
v-bind="$attrs"
|
||||||
<img
|
class="logo-svg"
|
||||||
src="src/assets/logo.svg"
|
:class="{
|
||||||
style="height: 24px; max-width: 24px; padding-right: 2px;"
|
'is-animated': animated,
|
||||||
|
'hide-bg': !withBackground
|
||||||
|
}"
|
||||||
|
:style="{ color: iconColor }"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span
|
<div :class="'text-' + textColor" class="logo-text q-ml-xs">
|
||||||
class="text-brand"
|
<span>tg</span>
|
||||||
style="margin-right: 0.05em;"
|
<span class="text-bold">Crew</span>
|
||||||
>
|
</div>
|
||||||
tg
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="text-brand text-bold">
|
|
||||||
Crew
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.logo-component {
|
.logo-svg {
|
||||||
svg {
|
height: 24px;
|
||||||
width: 1em;
|
width: 24px;
|
||||||
height: 1em;
|
|
||||||
margin-right: 0.125em;
|
:deep(.logo-bg) {
|
||||||
|
transition: opacity 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
&.hide-bg :deep(.logo-bg) {
|
||||||
line-height: 1;
|
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 {
|
@keyframes expand-r { 50% { r: 8px; } }
|
||||||
fill: $brand;
|
@keyframes o { 50% { transform: var(--t); } }
|
||||||
}
|
@keyframes draw {
|
||||||
|
0%, 100% { stroke-dashoffset: 0; }
|
||||||
@keyframes blink {
|
50% { stroke-dashoffset: 1; }
|
||||||
100%,
|
|
||||||
0% {
|
|
||||||
fill: $brand2;
|
|
||||||
}
|
|
||||||
60% {
|
|
||||||
fill: $brand2;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#path5 {
|
|
||||||
animation: blink 3s infinite;
|
|
||||||
}
|
}
|
||||||
</style>
|
</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 modelValue = defineModel<('terms'| 'consent')[]>({ default: () => [] })
|
||||||
|
|
||||||
const emit = defineEmits(['agree'])
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-template>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{ $t(title) }}
|
{{ $t(title) }}
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #card-actions>
|
||||||
<q-btn
|
<q-btn
|
||||||
rounded color="primary"
|
rounded color="primary"
|
||||||
class="w100 fix-disabled-btn"
|
class="w100 fix-disabled-btn"
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
{{ $t(btnText) }}
|
{{ $t(btnText) }}
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</template>
|
</template>
|
||||||
<pn-scroll-list>
|
|
||||||
<div class="flex column items-center q-pa-md q-pb-sm w100">
|
<div class="flex column items-center q-pa-md q-pb-sm w100">
|
||||||
<slot name="myCompany"/>
|
<slot name="myCompany"/>
|
||||||
|
|
||||||
@@ -57,8 +57,7 @@
|
|||||||
</q-input>
|
</q-input>
|
||||||
</q-form>
|
</q-form>
|
||||||
</div>
|
</div>
|
||||||
</pn-scroll-list>
|
</pn-page-template>
|
||||||
</pn-page-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-template>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{ $t(title) }}
|
{{ $t(title) }}
|
||||||
</template>
|
</template>
|
||||||
<pn-scroll-list>
|
|
||||||
<markdown-viewver
|
<markdown-viewver
|
||||||
class="q-pa-md"
|
class="q-px-md"
|
||||||
:locale
|
:locale
|
||||||
:documentName
|
:documentName
|
||||||
/>
|
/>
|
||||||
</pn-scroll-list>
|
</pn-page-template>
|
||||||
</pn-page-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -9,12 +9,7 @@
|
|||||||
{{ customer.email }}
|
{{ customer.email }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="ellipsis">
|
<span v-else class="ellipsis">
|
||||||
{{
|
{{ nameTgUser(tgUser) }}
|
||||||
tgUser?.first_name +
|
|
||||||
(tgUser?.first_name && tgUser?.last_name ? ' ' : '') +
|
|
||||||
tgUser?.last_name +
|
|
||||||
(!(tgUser?.first_name || tgUser?.last_name) ? tgUser?.username : '')
|
|
||||||
}}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -22,13 +17,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { inject } from 'vue'
|
import { inject } from 'vue'
|
||||||
import { useAuthStore } from 'stores/auth'
|
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 authStore = useAuthStore()
|
||||||
const customer = authStore.customer
|
const customer = authStore.customer
|
||||||
const tg = inject('tg') as WebApp
|
const tg = inject('tg') as WebApp
|
||||||
const tgUser = tg.initDataUnsafe.user
|
const tgUser = tg.initDataUnsafe.user as WebAppUser | undefined
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<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
|
<q-btn
|
||||||
v-if="showCalendarBtn"
|
v-if="showCalendarBtn"
|
||||||
flat round
|
round
|
||||||
:color="calendarActive ? 'primary' : 'grey'"
|
:color="calendarActive ? 'primary' : 'grey-2'"
|
||||||
|
:class="calendarActive ? 'text-white' : 'text-grey'"
|
||||||
|
class="q-mr-sm"
|
||||||
@click="emit('toggle-calendar')"
|
@click="emit('toggle-calendar')"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@@ -21,6 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
|
<div class="w100">
|
||||||
<q-input
|
<q-input
|
||||||
v-model="search"
|
v-model="search"
|
||||||
clearable
|
clearable
|
||||||
@@ -29,18 +32,21 @@
|
|||||||
filled
|
filled
|
||||||
:placeholder="$t(placeholder)"
|
:placeholder="$t(placeholder)"
|
||||||
dense
|
dense
|
||||||
class="col-grow q-mx-sm"
|
class="rounded-fix bg-white shadow-1"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<q-icon name="mdi-magnify" color="grey"/>
|
<q-icon name="mdi-magnify" color="grey"/>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="showFilterBtn"
|
v-if="showFilterBtn"
|
||||||
@click="emit('open-filters')"
|
@click="emit('open-filters')"
|
||||||
flat round
|
round
|
||||||
:color="filterActive ? 'primary' : 'grey'"
|
:color="filterActive ? 'primary' : 'grey-2'"
|
||||||
|
:class="filterActive ? 'text-white' : 'text-grey'"
|
||||||
|
class="q-ml-sm"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<q-icon name="mdi-filter-outline" size="sm"/>
|
<q-icon name="mdi-filter-outline" size="sm"/>
|
||||||
|
|||||||
@@ -1,98 +1,125 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model="modelValue"
|
v-model="modelValue"
|
||||||
maximized
|
|
||||||
transition-show="slide-up"
|
transition-show="slide-up"
|
||||||
transition-hide="slide-down"
|
transition-hide="slide-down"
|
||||||
position="bottom"
|
position="bottom"
|
||||||
>
|
>
|
||||||
<q-card
|
<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="
|
style="
|
||||||
border-top-left-radius: var(--top-radius) !important;
|
border-top-left-radius: var(--top-radius) !important;
|
||||||
border-top-right-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
|
<div ref="cardHeaderRef" class="flex items-center no-wrap justify-between w100 q-my-none q-pa-md">
|
||||||
ref="cardHeaderRef"
|
<q-resize-observer @resize="onHeaderResize" />
|
||||||
class="flex items-center no-wrap justify-between w100 q-my-none q-pa-md"
|
<div class="flex column q-mx-xs">
|
||||||
>
|
<span class="text-h6" style="line-height: 1.5rem;">
|
||||||
<div class="text-h6 q-mx-xs ellipsis">{{ $t(title) }}</div>
|
{{ $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">
|
<div class="flex items-center justify-between no-wrap">
|
||||||
<slot name="btnSlot">
|
<slot name="btnSlot">
|
||||||
<q-btn
|
<q-btn icon="mdi-close" @click="modelValue = false" flat round />
|
||||||
icon="mdi-close"
|
|
||||||
@click="modelValue = false"
|
|
||||||
flat round
|
|
||||||
/>
|
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
ref="cardBodyRef"
|
<div class="q-px-none q-ma-none">
|
||||||
class="q-px-none q-ma-none"
|
|
||||||
>
|
|
||||||
<pn-shadow-scroll
|
<pn-shadow-scroll
|
||||||
:hideShadows="false"
|
|
||||||
:height="bodyHeight"
|
:height="bodyHeight"
|
||||||
|
:bottomOffset
|
||||||
|
:topOffset
|
||||||
>
|
>
|
||||||
<div ref="cardBodyInnerRef" class="q-pa-none q-ma-none">
|
<div class="q-pa-none q-ma-none">
|
||||||
<q-resize-observer @resize="updateDimensions" />
|
<q-resize-observer @resize="onInnerBodyResize" />
|
||||||
<slot/>
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</pn-shadow-scroll>
|
</pn-shadow-scroll>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
style="overflow: hidden"
|
<div v-if="hasFooter" class="absolute-bottom">
|
||||||
:style="{ height: footerHeight + 'px'}"
|
<q-resize-observer @resize="onFooterResize" />
|
||||||
>
|
<div class="q-pa-md">
|
||||||
<div ref="cardFooterInnerRef" class="q-pa-md">
|
<slot name="footer" />
|
||||||
<q-resize-observer @resize="updateDimensions" />
|
|
||||||
<slot name="footer"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div :style="{ height: tgInsetBottom + 'px' }" />
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, useSlots } from 'vue'
|
||||||
import { useSlots } from 'vue'
|
|
||||||
|
|
||||||
const modelValue = defineModel<boolean>({
|
const modelValue = defineModel<boolean>({ required: true })
|
||||||
required: true
|
const props = withDefaults(defineProps<{
|
||||||
})
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
title: string
|
title: string
|
||||||
}>()
|
caption?: string
|
||||||
|
maximized?: boolean
|
||||||
|
}>(), { maximized: true })
|
||||||
|
|
||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
const cardHeaderRef = ref<HTMLElement | null>(null)
|
const cardRef = ref()
|
||||||
const cardFooterInnerRef = ref<HTMLElement | null>(null)
|
|
||||||
const cardBodyInnerRef = ref<HTMLElement | null>(null)
|
|
||||||
|
|
||||||
const headerHeight = ref(0)
|
const headerHeight = ref(0)
|
||||||
const footerHeight = ref(0)
|
const footerHeight = ref(0)
|
||||||
const bodyInnerHeight = ref(0)
|
const bodyInnerHeight = ref(0)
|
||||||
const bodyHeight = ref(0)
|
const tgInsetBottom = ref(0)
|
||||||
|
const topOffset = 16
|
||||||
|
|
||||||
const updateDimensions = async () => {
|
const windowHeight = ref(window.innerHeight)
|
||||||
await nextTick(() => {
|
|
||||||
headerHeight.value = cardHeaderRef.value?.offsetHeight || 0
|
const hasFooter = computed(() => !!slots['footer'])
|
||||||
footerHeight.value = cardFooterInnerRef.value?.offsetHeight || 0
|
const bottomOffset = computed(() => hasFooter.value ? footerHeight.value : tgInsetBottom.value)
|
||||||
bodyInnerHeight.value = cardBodyInnerRef.value?.offsetHeight || 0
|
|
||||||
const needScroll = headerHeight.value + footerHeight.value + bodyInnerHeight.value > window.innerHeight - 48
|
const bodyHeight = computed(() => {
|
||||||
bodyHeight.value = needScroll
|
// hack for update computed params on window resize
|
||||||
? window.innerHeight - 48 - headerHeight.value - footerHeight.value
|
const _trigger = windowHeight.value //eslint-disable-line
|
||||||
: bodyInnerHeight.value + 16
|
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' })
|
const getTgInsetBottom = () => {
|
||||||
watch(modelValue, updateDimensions)
|
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>
|
</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>
|
<template>
|
||||||
<q-item
|
<q-item
|
||||||
@click="showDialog=true"
|
@click="showDialog = true"
|
||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
>
|
>
|
||||||
@@ -26,71 +26,19 @@
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
<q-dialog
|
<pn-bottom-sheet-dialog
|
||||||
|
:title
|
||||||
|
:caption
|
||||||
v-model="showDialog"
|
v-model="showDialog"
|
||||||
maximized
|
|
||||||
transition-show="slide-up"
|
|
||||||
transition-hide="slide-down"
|
|
||||||
position="bottom"
|
|
||||||
>
|
>
|
||||||
<q-card
|
<div class="q-px-md">
|
||||||
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" />
|
|
||||||
<slot/>
|
<slot/>
|
||||||
</div>
|
</div>
|
||||||
</pn-shadow-scroll>
|
</pn-bottom-sheet-dialog>
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
ref="cardFooterRef"
|
|
||||||
class="q-pa-md"
|
|
||||||
>
|
|
||||||
<slot name="footer"/>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
|
|
||||||
</q-dialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useSlots } from 'vue'
|
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
title: string
|
title: string
|
||||||
@@ -100,31 +48,7 @@
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const showDialog=ref<boolean>(false)
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.fix-card-width {
|
|
||||||
width: var(--body-width) !important;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="flex column justify-center col-grow items-center">
|
<div class="flex column justify-center col-grow items-center">
|
||||||
<q-icon :name="icon" size="160px" class="q-pb-md"/>
|
<q-icon :name="icon" size="160px" class="q-pb-md"/>
|
||||||
<div class="text-h6 text-brand text-center">
|
<div class="text-h6 text-brand text-center">
|
||||||
{{ message1 }}
|
{{ $t(message1) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
<div class="flex column justify-center col-grow items-center">
|
<div class="flex column justify-center col-grow items-center">
|
||||||
<q-icon :name="icon" size="160px" class="q-pb-md"/>
|
<q-icon :name="icon" size="160px" class="q-pb-md"/>
|
||||||
<div class="text-h6 text-brand text-center">
|
<div class="text-h6 text-brand text-center">
|
||||||
{{ message1 }}
|
{{ $t(message1) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="message2" class="text-caption text-center">
|
<div v-if="message2" class="text-caption text-center">
|
||||||
{{ message2 }}
|
{{ $t(message2) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,5 +44,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style>
|
|
||||||
|
<style scoped>
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
id="overlay"
|
id="overlay"
|
||||||
class="fixed-full q-dialog__backdrop"
|
class="q-dialog__backdrop absolute-top"
|
||||||
style="z-index: 5000; --q-transition-duration: 300ms;"
|
style="z-index: 5000; --q-transition-duration: 300ms; height: 100vh; width: 10vw;"
|
||||||
@click.prevent.stop
|
@click.prevent.stop
|
||||||
@touchstart.prevent.stop
|
@touchstart.prevent.stop
|
||||||
@touchmove.prevent.stop
|
@touchmove.prevent.stop
|
||||||
@@ -13,6 +13,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,24 +1,117 @@
|
|||||||
<template>
|
<template>
|
||||||
<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"
|
class="w100 col-grow relative-position top-rounded-card bg-white overflow-hidden"
|
||||||
style="min-height: 48px"
|
|
||||||
>
|
>
|
||||||
<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>
|
||||||
|
|
||||||
|
<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/>
|
<slot/>
|
||||||
<div v-if="hasFooter" class="flex w100 q-pa-md bg-white">
|
</pn-shadow-scroll>
|
||||||
<slot name="footer"/>
|
|
||||||
|
<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>
|
</div>
|
||||||
<slot v-if="hasTabs" name="tabs"/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 slots = useSlots()
|
||||||
const hasFooter = computed(() => !!slots.footer)
|
const hasHeader = computed(() => !!slots['card-header'])
|
||||||
const hasTabs = computed(() => !!slots.tabs)
|
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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
</style>
|
</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>
|
<template>
|
||||||
<div :class="{'fix-scroll-area-content': hideShadows }" class="fix-scroll-area-content-width column col col-grow">
|
|
||||||
<q-scroll-area
|
<q-scroll-area
|
||||||
ref="scrollAreaRef"
|
ref="scrollAreaRef"
|
||||||
:style="{ height: height + 'px' }"
|
:style="scrollStyle"
|
||||||
class="w100 q-pa-none q-ma-none column col col-grow"
|
:vertical-offset="[top, bottom]"
|
||||||
|
:thumb-style="{ borderRadius: '3px', background: '#999', width: '6px', opacity: '0.4' }"
|
||||||
@scroll="onScroll"
|
@scroll="onScroll"
|
||||||
:class=" {
|
class="shadow-scroll w100"
|
||||||
'shadow-top': hasScrolled,
|
|
||||||
'shadow-bottom': hasScrolledBottom
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
|
<q-resize-observer @resize="onInnerResize"/>
|
||||||
|
<div :style="{ height: top + 'px'}"/>
|
||||||
<slot/>
|
<slot/>
|
||||||
|
<div :style="{ height: bottom + 'px'}"/>
|
||||||
</q-scroll-area>
|
</q-scroll-area>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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
|
height: number
|
||||||
hideShadows: boolean
|
topOffset?: number
|
||||||
|
bottomOffset?: number
|
||||||
scrollPosition?: number
|
scrollPosition?: number
|
||||||
}>()
|
offset?: number
|
||||||
|
}>(),
|
||||||
const emit = defineEmits(['updateScrollPosition'])
|
{
|
||||||
|
scrollPosition: 0,
|
||||||
const hasScrolled = ref(false)
|
offset: 16
|
||||||
const hasScrolledBottom = ref(false)
|
|
||||||
|
|
||||||
interface ScrollInfo {
|
|
||||||
verticalPosition: number;
|
|
||||||
verticalPercentage: number;
|
|
||||||
verticalSize: number;
|
|
||||||
verticalContainerSize: number;
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
function onScroll (info: ScrollInfo) {
|
const emit = defineEmits([
|
||||||
hasScrolled.value = info.verticalPosition > 0
|
'updateScrollPosition',
|
||||||
const scrollEnd = info.verticalPosition + info.verticalContainerSize >= info.verticalSize - 1
|
'resizeScrollArea'
|
||||||
hasScrolledBottom.value = !scrollEnd
|
])
|
||||||
|
|
||||||
|
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)
|
emit('updateScrollPosition', scrollAreaRef.value?.getScrollPosition().top || 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollAreaRef = ref()
|
const scrollAreaRef = ref()
|
||||||
|
|
||||||
onActivated(() => {
|
watch(() => props.scrollPosition, async (newPosition) => {
|
||||||
scrollAreaRef.value?.setScrollPosition('vertical', props?.scrollPosition || 0)
|
await nextTick()
|
||||||
})
|
|
||||||
|
|
||||||
watch(() => props.scrollPosition, (newPosition) => {
|
|
||||||
scrollAreaRef.value?.setScrollPosition('vertical', newPosition)
|
scrollAreaRef.value?.setScrollPosition('vertical', newPosition)
|
||||||
}, { immediate: true })
|
}, { 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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.q-scrollarea {
|
.shadow-scroll:before {
|
||||||
position: relative;
|
content: "";
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.q-scrollarea::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
width: 100%;
|
||||||
height: 4px;
|
height: var(--top-height);
|
||||||
background: linear-gradient(to bottom,
|
background: linear-gradient(white, transparent);
|
||||||
rgba(0,0,0,0.12) 0%,
|
|
||||||
rgba(0,0,0,0.08) 50%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
z-index: 95;
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-8px);
|
|
||||||
will-change: opacity, transform;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.q-scrollarea::after {
|
.shadow-scroll:after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 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%;
|
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) {
|
.shadow-scroll :deep(.q-scrollarea__content) {
|
||||||
content: none;
|
max-width: min(100vw, var(--body-width));
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-dialog v-model="modelValue" class="q-ma-none">
|
<q-dialog
|
||||||
<q-card
|
v-model="modelValue"
|
||||||
class="q-pa-none q-ma-none w100 no-scroll rounded-card"
|
class="q-ma-none"
|
||||||
style="max-height: calc(100vh - 48px);"
|
|
||||||
>
|
>
|
||||||
<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">
|
<div class="flex no-wrap items-center w100">
|
||||||
<q-resize-observer @resize="onHeaderResize"/>
|
<q-resize-observer @resize="onHeaderResize"/>
|
||||||
<span class="text-bold">
|
<span class="text-bold">
|
||||||
@@ -15,10 +23,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<div>
|
<div
|
||||||
<pn-shadow-scroll :height="scrollAreaHeight" :hideShadows="false">
|
ref="cardBodyRef"
|
||||||
<div>
|
class="q-px-none q-ma-none"
|
||||||
<q-resize-observer @resize="onBodyResize"/>
|
>
|
||||||
|
<pn-shadow-scroll
|
||||||
|
:height="bodyHeight"
|
||||||
|
>
|
||||||
|
<div ref="cardBodyInnerRef" class="q-pa-none q-ma-none">
|
||||||
|
<q-resize-observer @resize="updateDimensions" />
|
||||||
<slot/>
|
<slot/>
|
||||||
</div>
|
</div>
|
||||||
</pn-shadow-scroll>
|
</pn-shadow-scroll>
|
||||||
@@ -29,44 +42,60 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, nextTick } from 'vue'
|
import { ref, watch, nextTick } from 'vue'
|
||||||
import pnShadowScroll from './pnShadowScroll.vue'
|
import { useSlots } from 'vue'
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
title: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const modelValue = defineModel<boolean>({
|
const modelValue = defineModel<boolean>({
|
||||||
required: true
|
required: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
title: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const slots = useSlots()
|
||||||
|
const cardBodyInnerRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
const headerHeight = ref(0)
|
const headerHeight = ref(0)
|
||||||
|
const bodyInnerHeight = ref(0)
|
||||||
const bodyHeight = ref(0)
|
const bodyHeight = ref(0)
|
||||||
const scrollAreaHeight = ref(0)
|
const cardHeight = ref(0)
|
||||||
|
|
||||||
interface sizeParams {
|
interface sizeParams {
|
||||||
height: number,
|
height: number,
|
||||||
width: number
|
width: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onResizeCard (size :sizeParams) {
|
||||||
|
cardHeight.value = size.width
|
||||||
|
}
|
||||||
|
|
||||||
function onHeaderResize (size: sizeParams) {
|
function onHeaderResize (size: sizeParams) {
|
||||||
headerHeight.value = size.height
|
headerHeight.value = size.height
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBodyResize (size: sizeParams) {
|
const updateDimensions = async () => {
|
||||||
bodyHeight.value = size.height
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateScrollAreaHeight () {
|
|
||||||
await nextTick(() => {
|
await nextTick(() => {
|
||||||
scrollAreaHeight.value = Math.min(
|
bodyInnerHeight.value = cardBodyInnerRef.value?.offsetHeight || 0
|
||||||
bodyHeight.value + 16,
|
|
||||||
window.innerHeight - headerHeight.value - 64
|
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(() => slots.body?.(), updateDimensions, { flush: 'post' })
|
||||||
watch(headerHeight, updateScrollAreaHeight)
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<q-icon name="close"/>
|
<q-icon name="close"/>
|
||||||
{{$t('close')}}
|
{{ $t('close') }}
|
||||||
</div>
|
</div>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-template>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{ $t(title) }}
|
{{ $t(title) }}
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #card-actions>
|
||||||
<q-btn
|
<q-btn
|
||||||
rounded color="primary"
|
rounded color="primary"
|
||||||
class="w100 fix-disabled-btn"
|
class="w100 fix-disabled-btn"
|
||||||
@@ -14,7 +14,6 @@
|
|||||||
</q-btn>
|
</q-btn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<pn-scroll-list>
|
|
||||||
<div class="flex column items-center q-pa-md q-pb-sm w100">
|
<div class="flex column items-center q-pa-md q-pb-sm w100">
|
||||||
<pn-image-selector
|
<pn-image-selector
|
||||||
v-model="projectMod.logo"
|
v-model="projectMod.logo"
|
||||||
@@ -52,16 +51,28 @@
|
|||||||
filled
|
filled
|
||||||
autogrow
|
autogrow
|
||||||
no-error-icon
|
no-error-icon
|
||||||
class="w100 q-pt-sm"
|
class="w100 q-pt-sm fix-bottom-padding"
|
||||||
:label="$t('project_block__project_description')"
|
:label="$t('project_block__project_description')"
|
||||||
:rules="[inputRules.longText]"
|
:rules="[inputRules.longText]"
|
||||||
@update:model-value="formRef.validate()"
|
@update:model-value="formRef.validate()"
|
||||||
debounce="300"
|
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>
|
</q-form>
|
||||||
</div>
|
</div>
|
||||||
</pn-scroll-list>
|
</pn-page-template>
|
||||||
</pn-page-card>
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -84,6 +95,7 @@
|
|||||||
const projectMod = reactive({
|
const projectMod = reactive({
|
||||||
name: props.project?.name ?? '',
|
name: props.project?.name ?? '',
|
||||||
description: props.project?.description ?? '',
|
description: props.project?.description ?? '',
|
||||||
|
chat_info: props.project?.chat_info ?? '',
|
||||||
logo: props.project?.logo ?? null
|
logo: props.project?.logo ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -94,6 +106,7 @@
|
|||||||
const outData = {
|
const outData = {
|
||||||
name: projectMod.name,
|
name: projectMod.name,
|
||||||
description: getFieldValue(projectMod.description, props.project?.description),
|
description: getFieldValue(projectMod.description, props.project?.description),
|
||||||
|
chat_info: getFieldValue(projectMod.chat_info, props.project?.chat_info),
|
||||||
logo: projectMod.logo !== '' ? projectMod.logo : null
|
logo: projectMod.logo !== '' ? projectMod.logo : null
|
||||||
}
|
}
|
||||||
emit('update', removeNullKeys(outData, ['logo']))
|
emit('update', removeNullKeys(outData, ['logo']))
|
||||||
|
|||||||
@@ -69,6 +69,7 @@
|
|||||||
title="subscribe__dialog_select_tariff_title"
|
title="subscribe__dialog_select_tariff_title"
|
||||||
v-model="showTarfiffSelectDialog"
|
v-model="showTarfiffSelectDialog"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div class="w100 column">
|
<div class="w100 column">
|
||||||
<q-banner
|
<q-banner
|
||||||
v-if="!currentTariff.can_change"
|
v-if="!currentTariff.can_change"
|
||||||
@@ -103,12 +104,13 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="w100 text-info cursor-pointer"
|
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') }}
|
{{ $t('subscribe__dialog_select_tariff_notes_more') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<q-btn
|
<q-btn
|
||||||
@@ -149,7 +151,6 @@
|
|||||||
import telegramStar from 'components/TelegramStar.vue'
|
import telegramStar from 'components/TelegramStar.vue'
|
||||||
import pnSubscribeTariffs from 'components/subscribe/pnSubscribeTariffs.vue'
|
import pnSubscribeTariffs from 'components/subscribe/pnSubscribeTariffs.vue'
|
||||||
import pnSubscribePeriods from 'components/subscribe/pnSubscribePeriods.vue'
|
import pnSubscribePeriods from 'components/subscribe/pnSubscribePeriods.vue'
|
||||||
import pnBottomSheetDialog from 'components/pnBottomSheetDialog.vue'
|
|
||||||
import {
|
import {
|
||||||
calcChangeTariff,
|
calcChangeTariff,
|
||||||
changeTariff,
|
changeTariff,
|
||||||
@@ -176,6 +177,7 @@
|
|||||||
const selectedTariffPrice = computed(() => props.tariffs.find((el: Tariff) => el.id === props.currentTariff?.id)?.price ?? 0)
|
const selectedTariffPrice = computed(() => props.tariffs.find((el: Tariff) => el.id === props.currentTariff?.id)?.price ?? 0)
|
||||||
|
|
||||||
const showTarfiffSelectDialog = ref(false)
|
const showTarfiffSelectDialog = ref(false)
|
||||||
|
|
||||||
async function clickChangeTariff () {
|
async function clickChangeTariff () {
|
||||||
if (props.currentTariff &&
|
if (props.currentTariff &&
|
||||||
selectedTariff.value &&
|
selectedTariff.value &&
|
||||||
@@ -196,6 +198,7 @@
|
|||||||
description: 'subscribe__dialog_select_tariff_warn_once_day_msg_2'
|
description: 'subscribe__dialog_select_tariff_warn_once_day_msg_2'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return {}
|
||||||
})
|
})
|
||||||
|
|
||||||
const activePeriodMessage = ref('')
|
const activePeriodMessage = ref('')
|
||||||
@@ -218,7 +221,7 @@
|
|||||||
const renew = await calcRenewTariff(props.currentTariff.id, selectedPeriod.value)
|
const renew = await calcRenewTariff(props.currentTariff.id, selectedPeriod.value)
|
||||||
const textBtn = t('subscribe__pay_renew') + ' ⭐' + formatNumber(renew.amount) +
|
const textBtn = t('subscribe__pay_renew') + ' ⭐' + formatNumber(renew.amount) +
|
||||||
' (' + t('subscribe__tariff_exp') + ' ' + date.formatDate(renew.end_date * 1000, 'DD.MM.YYYY') + ')'
|
' (' + 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 })
|
emit('update', { status: 'show', textBtn, url })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-template>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{ $t(title) }}
|
{{ $t(title) }}
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #card-actions>
|
||||||
<q-btn
|
<q-btn
|
||||||
rounded color="primary"
|
rounded color="primary"
|
||||||
class="w100 fix-disabled-btn"
|
class="w100 fix-disabled-btn"
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
{{ $t(btnText) }}
|
{{ $t(btnText) }}
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</template>
|
</template>
|
||||||
<pn-scroll-list>
|
|
||||||
<div class="column items-center q-pa-md q-pb-sm">
|
<div class="column items-center q-pa-md q-pb-sm">
|
||||||
<div class="relative-position flex justify-center w100 text-center">
|
<div class="relative-position flex justify-center w100 text-center">
|
||||||
<pn-auto-avatar
|
<pn-auto-avatar
|
||||||
@@ -148,8 +148,7 @@
|
|||||||
</q-input>
|
</q-input>
|
||||||
</q-form>
|
</q-form>
|
||||||
</div>
|
</div>
|
||||||
</pn-scroll-list>
|
</pn-page-template>
|
||||||
</pn-page-card>
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -68,10 +68,6 @@ body {
|
|||||||
margin-right: calc(max((100vw - var(--body-width))/2, 0px) + 18px) !important;
|
margin-right: calc(max((100vw - var(--body-width))/2, 0px) + 18px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.projects-header {
|
|
||||||
background-color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-rounded-card {
|
.top-rounded-card {
|
||||||
border-top-left-radius: var(--top-radius) !important;
|
border-top-left-radius: var(--top-radius) !important;
|
||||||
border-top-right-radius: var(--top-radius) !important;
|
border-top-right-radius: var(--top-radius) !important;
|
||||||
@@ -81,8 +77,9 @@ body {
|
|||||||
border-radius: var(--top-radius) !important;
|
border-radius: var(--top-radius) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-card {
|
.rounded-fix {
|
||||||
background-color: rgb(255, 255, 255, 0.91) !important;
|
border-radius: 1000px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.orline {
|
.orline {
|
||||||
@@ -105,6 +102,7 @@ body {
|
|||||||
|
|
||||||
.fix-disabled-btn.q-btn[disabled]:not(.q-btn--flat) {
|
.fix-disabled-btn.q-btn[disabled]:not(.q-btn--flat) {
|
||||||
background-color: $grey-5 !important;
|
background-color: $grey-5 !important;
|
||||||
|
opacity: 1 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fix-disabled-btn.q-btn.q-btn--flat[disabled] {
|
.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
|
<q-page-container
|
||||||
class="main-content q-pa-none q-ma-none no-scroll bg-transparent"
|
class="main-content q-pa-none q-ma-none no-scroll bg-transparent"
|
||||||
>
|
>
|
||||||
<q-page class="no-scroll column">
|
<q-page>
|
||||||
<router-view />
|
<router-view />
|
||||||
</q-page>
|
</q-page>
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-template>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{$t('account__change_auth_method')}}
|
{{$t('account__change_auth_method')}}
|
||||||
</template>
|
</template>
|
||||||
<pn-scroll-list>
|
|
||||||
<account-helper :type/>
|
<account-helper :type/>
|
||||||
</pn-scroll-list>
|
</pn-page-template>
|
||||||
</pn-page-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-template>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{$t('account_change_email__title')}}
|
{{$t('account_change_email__title')}}
|
||||||
</template>
|
</template>
|
||||||
<pn-scroll-list>
|
|
||||||
<q-stepper
|
<q-stepper
|
||||||
v-model="step"
|
v-model="step"
|
||||||
vertical
|
vertical
|
||||||
@@ -185,8 +185,7 @@
|
|||||||
message2="account_change_email__ok_message2"
|
message2="account_change_email__ok_message2"
|
||||||
route-name="account"
|
route-name="account"
|
||||||
/>
|
/>
|
||||||
</pn-scroll-list>
|
</pn-page-template>
|
||||||
</pn-page-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-template>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{$t('account__change_password')}}
|
{{$t('account__change_password')}}
|
||||||
</template>
|
</template>
|
||||||
<pn-scroll-list>
|
|
||||||
<account-helper :type :email/>
|
<account-helper :type :email/>
|
||||||
</pn-scroll-list>
|
</pn-page-template>
|
||||||
</pn-page-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-template>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{$t('login__register_title')}}
|
{{$t('login__register_title')}}
|
||||||
</template>
|
</template>
|
||||||
<pn-scroll-list>
|
|
||||||
<account-helper :type :email/>
|
<account-helper :type :email/>
|
||||||
</pn-scroll-list>
|
</pn-page-template>
|
||||||
</pn-page-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-template>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{$t('forgot_password__password_recovery')}}
|
{{$t('forgot_password__password_recovery')}}
|
||||||
</template>
|
</template>
|
||||||
<pn-scroll-list>
|
|
||||||
<account-helper :type :email/>
|
<account-helper :type :email/>
|
||||||
</pn-scroll-list>
|
</pn-page-template>
|
||||||
</pn-page-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-template>
|
||||||
<template #title>
|
<template #title>
|
||||||
<pn-account-block-name/>
|
<pn-account-block-name/>
|
||||||
<q-btn
|
<q-btn
|
||||||
@@ -10,8 +10,7 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<pn-scroll-list>
|
<q-list>
|
||||||
<q-list separator>
|
|
||||||
<q-item
|
<q-item
|
||||||
v-for="item in displayItems"
|
v-for="item in displayItems"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@@ -38,8 +37,6 @@
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</pn-scroll-list>
|
|
||||||
</pn-page-card>
|
|
||||||
|
|
||||||
<pn-small-dialog
|
<pn-small-dialog
|
||||||
v-model="showStopUsingDialog"
|
v-model="showStopUsingDialog"
|
||||||
@@ -51,6 +48,7 @@
|
|||||||
mainBtnLabel="account__stop_using_dialog_btn_ok"
|
mainBtnLabel="account__stop_using_dialog_btn_ok"
|
||||||
@clickMainBtn="onConfirmStopUsing"
|
@clickMainBtn="onConfirmStopUsing"
|
||||||
/>
|
/>
|
||||||
|
</pn-page-template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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: 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: 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: 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(() => (
|
const displayItems = computed(() => (
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-template>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{$t('agreements__title')}}
|
{{$t('agreements__title')}}
|
||||||
</template>
|
</template>
|
||||||
<pn-scroll-list>
|
|
||||||
<div class="flex column w100 q-py-md q-px-lg q-gutter-y-sm text-caption">
|
<div class="flex column w100 q-py-md q-px-lg q-gutter-y-sm text-caption">
|
||||||
<div class="text-body1">
|
<div class="text-body1">
|
||||||
<div v-if="route.query.type === 'update'">{{ $t('agreements__description_update') }}</div>
|
<div v-if="route.query.type === 'update'">{{ $t('agreements__description_update') }}</div>
|
||||||
<div>{{ $t('agreements__description') }}</div>
|
<div>{{ $t('agreements__description') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<agreement-block
|
<agreement-block v-model="agreement"/>
|
||||||
v-model="agreement"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</pn-scroll-list>
|
|
||||||
<template #footer>
|
<template #card-actions>
|
||||||
<div class="column no-wrap items-center w100">
|
<div class="column no-wrap items-center w100">
|
||||||
<q-btn
|
<q-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -42,7 +40,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</pn-page-card>
|
</pn-page-template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-template>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{ $t('chat_card__title') }}
|
{{ $t('chat_card__title') }}
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
|
||||||
|
<template #card-actions>
|
||||||
<q-btn
|
<q-btn
|
||||||
rounded color="primary"
|
rounded color="primary"
|
||||||
class="w100"
|
class="w100"
|
||||||
@@ -14,7 +15,6 @@
|
|||||||
</q-btn>
|
</q-btn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<pn-scroll-list>
|
|
||||||
<div
|
<div
|
||||||
v-if="chat"
|
v-if="chat"
|
||||||
class="flex column items-center q-px-md q-pt-md q-pb-sm"
|
class="flex column items-center q-px-md q-pt-md q-pb-sm"
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
<q-item-label class="text-caption text-bold q-py-none" header>
|
||||||
{{ $t('chat_card__members') + ' (' + chatUsers.length +')' }}
|
{{ $t('chat_card__members') + ' (' + chatUsers.length +')' }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
@@ -71,8 +71,7 @@
|
|||||||
</pn-user-list-item-body>
|
</pn-user-list-item-body>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</pn-scroll-list>
|
</pn-page-template>
|
||||||
</pn-page-card>
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-template>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{$t('mask__title')}}
|
{{$t('mask__title')}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #footer>
|
<template #card-actions>
|
||||||
<q-btn
|
<q-btn
|
||||||
rounded color="primary"
|
rounded color="primary"
|
||||||
class="w100 fix-disabled-btn"
|
class="w100 fix-disabled-btn"
|
||||||
@@ -14,15 +14,15 @@
|
|||||||
</q-btn>
|
</q-btn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<pn-scroll-list>
|
<template #card-header>
|
||||||
<template #card-body-header>
|
<div class="q-pa-md flex no-wrap items-center bg-grey-2">
|
||||||
<div class="q-ma-md flex no-wrap items-center">
|
|
||||||
<q-icon name="mdi-information q-pr-sm" size="sm" color="primary"/>
|
<q-icon name="mdi-information q-pr-sm" size="sm" color="primary"/>
|
||||||
<span class="text-caption">
|
<span class="text-caption">
|
||||||
{{ $t('mask__info_block')}}
|
{{ $t('mask__info_block')}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<q-table
|
<q-table
|
||||||
flat
|
flat
|
||||||
:rows="displayCompanies"
|
:rows="displayCompanies"
|
||||||
@@ -138,7 +138,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</q-table>
|
</q-table>
|
||||||
<div class="q-py-none flex column w100" style="min-height: 18px"/>
|
<div class="q-py-none flex column w100" style="min-height: 18px"/>
|
||||||
</pn-scroll-list>
|
|
||||||
|
|
||||||
<q-dialog v-model="showDialogHelp">
|
<q-dialog v-model="showDialogHelp">
|
||||||
<q-card class="q-ma-sm w100 rounded-card">
|
<q-card class="q-ma-sm w100 rounded-card">
|
||||||
@@ -156,7 +155,7 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
</pn-page-card>
|
</pn-page-template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -5,16 +5,23 @@
|
|||||||
<q-scroll-area
|
<q-scroll-area
|
||||||
style="height: 100vh"
|
style="height: 100vh"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-center" style="height: 100vh">
|
<div
|
||||||
<q-card
|
class="w100 flex justify-center items-center"
|
||||||
id="login_block"
|
style="min-height: 100vh;"
|
||||||
flat
|
|
||||||
class="flex column no-wrap items-center justify-between q-px-xl q-py-md login-card"
|
|
||||||
style="max-width: 400px;"
|
|
||||||
>
|
>
|
||||||
|
<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
|
<base-logo
|
||||||
class="col-grow q-pa-lg text-h5"
|
withBackground
|
||||||
|
animated
|
||||||
|
class="text-h5"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex column w100">
|
<div class="flex column w100">
|
||||||
<q-input
|
<q-input
|
||||||
@@ -106,27 +113,23 @@
|
|||||||
flat rounded
|
flat rounded
|
||||||
sm no-caps
|
sm no-caps
|
||||||
color="primary"
|
color="primary"
|
||||||
class="q-my-md w100"
|
class="q-mt-md w100"
|
||||||
@click="handleTelegramLogin"
|
@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-avatar size="md" class="q-mr-sm">
|
||||||
<q-img v-if="tgUser?.photo_url" :src="tgUser.photo_url"/>
|
<q-img v-if="tgUser?.photo_url" :src="tgUser.photo_url"/>
|
||||||
<q-icon v-else size="md" class="q-mr-none" name="telegram"/>
|
<q-icon v-else size="md" class="q-mr-none" name="telegram"/>
|
||||||
</q-avatar>
|
</q-avatar>
|
||||||
|
|
||||||
<span>
|
<span class="ellipsis">
|
||||||
{{
|
{{ nameTgUser(tgUser) }}
|
||||||
tgUser?.first_name +
|
|
||||||
(tgUser?.first_name && tgUser?.last_name ? ' ' : '') +
|
|
||||||
tgUser?.last_name +
|
|
||||||
(!(tgUser?.first_name || tgUser?.last_name) ? tgUser?.username : '')
|
|
||||||
}}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</div>
|
||||||
|
</pn-glass>
|
||||||
</div>
|
</div>
|
||||||
</q-scroll-area>
|
</q-scroll-area>
|
||||||
</q-page>
|
</q-page>
|
||||||
@@ -138,11 +141,13 @@
|
|||||||
import baseLogo from 'components/BaseLogo.vue'
|
import baseLogo from 'components/BaseLogo.vue'
|
||||||
import { useInputRules } from 'composables/useInputRules'
|
import { useInputRules } from 'composables/useInputRules'
|
||||||
import { useAuthStore } from 'stores/auth'
|
import { useAuthStore } from 'stores/auth'
|
||||||
import type { WebApp } from '@twa-dev/types'
|
|
||||||
import { QInput } from 'quasar'
|
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 tg = inject('tg') as WebApp
|
||||||
const tgUser = tg.initDataUnsafe.user
|
const tgUser = tg.initDataUnsafe.user as WebAppUser | undefined
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -1,124 +1,203 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-wrapper>
|
||||||
<template #title>
|
<template #title>
|
||||||
<project-page-header/>
|
<project-page-header/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<q-tab-panels
|
<q-tab-panels
|
||||||
|
id="tab-panels"
|
||||||
v-model="tabSelect"
|
v-model="tabSelect"
|
||||||
keep-alive
|
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
|
<q-tab-panel
|
||||||
v-for="tab in tabs"
|
v-for="tab in tabs"
|
||||||
:key="tab.name"
|
:key="tab.name"
|
||||||
:name="tab.name"
|
:name="tab.name"
|
||||||
class="q-pa-none flex column col-grow no-scroll"
|
class="q-pa-none column w100"
|
||||||
style="flex-grow: 2"
|
|
||||||
>
|
>
|
||||||
<router-view/>
|
<router-view :tabsSlotHeight/>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
</q-tab-panels>
|
</q-tab-panels>
|
||||||
|
|
||||||
<template #tabs>
|
<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
|
<q-tabs
|
||||||
style = "z-index: 1000"
|
|
||||||
v-model="tabSelect"
|
v-model="tabSelect"
|
||||||
dense
|
dense
|
||||||
align="justify"
|
align="center"
|
||||||
id="tabs"
|
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
|
<q-route-tab
|
||||||
v-for="tab in tabs"
|
v-for="tab in tabs"
|
||||||
:to="tab.to"
|
:to="{ name: tab.name }"
|
||||||
:name="tab.name"
|
:name="tab.name"
|
||||||
:key="tab.name"
|
:key="tab.name"
|
||||||
|
:ripple="false"
|
||||||
no-caps
|
no-caps
|
||||||
dense
|
dense
|
||||||
:class="tabSelect === tab.name ? 'active' : ''"
|
:class="(tabSelect === tab.name ? 'active' : '')"
|
||||||
class="flex column w100"
|
|
||||||
>
|
>
|
||||||
<template #default>
|
<div class="column items-center">
|
||||||
<div class="flex column items-center">
|
<q-icon :name="tab.icon" :size="config?.icon"/>
|
||||||
<q-icon :name="tab.icon" :size="maxTabWidth < baseTabWidth ? 'sm' : 'md'"/>
|
<span
|
||||||
<div
|
v-if="config?.showLabel"
|
||||||
class="text-caption flex justify-center"
|
class="text-caption"
|
||||||
:style="{ width: (baseTabWidth - 32) + 'px'}"
|
|
||||||
v-if="maxTabWidth < baseTabWidth"
|
|
||||||
>
|
>
|
||||||
<span>
|
{{ $t(tab.label) }}
|
||||||
<q-resize-observer @resize="size => tab.width = size.width"/>
|
|
||||||
{{$t(tab.label)}}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</q-route-tab>
|
</q-route-tab>
|
||||||
<q-resize-observer @resize="size => tabsWidth = size.width" />
|
<q-resize-observer @resize="size => tabsWidth = size.width" />
|
||||||
</q-tabs>
|
</q-tabs>
|
||||||
</q-footer>
|
<div class="bg-transparent"
|
||||||
|
style="height: var(--tg-safe-area-inset-bottom)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
</pn-page-card>
|
</pn-page-wrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onBeforeMount, computed } from 'vue'
|
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 projectPageHeader from 'pages/project-page/ProjectPageHeader.vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const chatsStore = useChatsStore()
|
const tabs = [
|
||||||
const usersStore = useUsersStore()
|
{ name: 'chats', label: 'project__chats', icon: 'mdi-chat-outline' },
|
||||||
const companiesStore = useCompaniesStore()
|
{ name: 'users', label: 'project__users', icon: 'mdi-account-outline' },
|
||||||
|
{ name: 'companies', label: 'project__companies', icon: 'mdi-account-group-outline' }
|
||||||
const chatsQty = computed(() => chatsStore.chats.length)
|
] as const
|
||||||
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 tabSelect = ref<string>()
|
const tabSelect = ref<string>()
|
||||||
|
|
||||||
onBeforeMount(() => {
|
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
|
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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped lang="scss">
|
||||||
.active {
|
#tabs :deep(.q-tab) {
|
||||||
|
padding: 0 var(--current-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabs :deep(.active) {
|
||||||
color: var(--q-primary)
|
color: var(--q-primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-panel-color {
|
#tabs :deep(.q-router-link--active .q-tab__indicator) {
|
||||||
background: transparent
|
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 {
|
#tabs :deep(.q-focus-helper) {
|
||||||
display: flex;
|
border-radius: calc(var(--tabs-height) / 2);
|
||||||
flex-direction: column;
|
border: 0px;
|
||||||
flex-grow: 2;
|
|
||||||
width: 100%
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
#tabs :deep(.q-tabs__arrow) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
<style scoped>
|
#tabs :deep(.q-tab__content) {
|
||||||
#tabs :deep(.q-tab__indicator) {
|
min-width: var(--min-width-tab);
|
||||||
height: 3px !important;
|
}
|
||||||
}
|
|
||||||
|
#tabs :deep(.q-tab--active .q-tab__indicator) {
|
||||||
|
width: var(--active-tab-width);
|
||||||
|
right: inherit;
|
||||||
|
left: inherit;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-template>
|
||||||
<template #title>
|
<template #title>
|
||||||
|
<div class="q-pr-md ellipsis">
|
||||||
{{ $t('projects__title') }}
|
{{ $t('projects__title') }}
|
||||||
|
</div>
|
||||||
<q-btn
|
<q-btn
|
||||||
@click="goAccount"
|
@click="goAccount"
|
||||||
flat rounded
|
flat rounded
|
||||||
@@ -14,27 +16,18 @@
|
|||||||
<pn-account-block-name/>
|
<pn-account-block-name/>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</template>
|
</template>
|
||||||
<pn-scroll-list
|
<template #card-header>
|
||||||
:hideHeader="projects.length === 0 && archiveProjects.length === 0"
|
<pn-action-bar
|
||||||
>
|
|
||||||
<template #card-body-header>
|
|
||||||
<q-input
|
|
||||||
v-model="searchProject"
|
v-model="searchProject"
|
||||||
clearable
|
placeholder="projects__search"
|
||||||
clear-icon="close"
|
:show-calendar-btn="false"
|
||||||
:placeholder="$t('projects__search')"
|
:show-filter-btn="false"
|
||||||
dense filled
|
/>
|
||||||
class="col-grow q-px-md q-py-md"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="mdi-magnify" />
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<q-list separator v-if="projects.length !== 0">
|
<q-list v-if="projects.length !== 0">
|
||||||
<template
|
<template
|
||||||
v-for = "item in activeProjects"
|
v-for="item in activeProjects"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
>
|
>
|
||||||
<q-slide-item
|
<q-slide-item
|
||||||
@@ -125,7 +118,7 @@
|
|||||||
enter-active-class="animated slideInDown"
|
enter-active-class="animated slideInDown"
|
||||||
leave-active-class="animated slideOutUp"
|
leave-active-class="animated slideOutUp"
|
||||||
>
|
>
|
||||||
<q-list separator v-if="showArchive" class="w100">
|
<q-list v-if="showArchive" class="w100">
|
||||||
<q-item
|
<q-item
|
||||||
v-for = "item in archiveProjects"
|
v-for = "item in archiveProjects"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@@ -160,37 +153,34 @@
|
|||||||
<pn-onboard-btn
|
<pn-onboard-btn
|
||||||
v-if="projects.length===0 && projectsInit"
|
v-if="projects.length===0 && projectsInit"
|
||||||
icon="mdi-briefcase-plus-outline"
|
icon="mdi-briefcase-plus-outline"
|
||||||
:message1="$t('projects__lets_start')"
|
message1="projects__lets_start"
|
||||||
:message2="$t('projects__lets_start_description')"
|
message2="projects__lets_start_description"
|
||||||
@btn-click="createNewProject"
|
@btn-click="createNewProject"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="flex column justify-center items-center w100"
|
class="column justify-center items-center w100"
|
||||||
style="position: absolute; bottom: 0;"
|
style="position: absolute; bottom: 0;"
|
||||||
v-if="!projectsInit"
|
v-if="!projectsInit"
|
||||||
>
|
>
|
||||||
<q-linear-progress indeterminate />
|
<q-linear-progress indeterminate />
|
||||||
</div>
|
</div>
|
||||||
</pn-scroll-list>
|
|
||||||
|
|
||||||
<q-page-sticky
|
<template #card-fab>
|
||||||
position="bottom-right"
|
|
||||||
:offset="[0, 18]"
|
|
||||||
class="fix-fab-offset"
|
|
||||||
>
|
|
||||||
<transition
|
<transition
|
||||||
appear
|
appear
|
||||||
enter-active-class="animated zoomIn"
|
enter-active-class="animated zoomIn"
|
||||||
>
|
>
|
||||||
|
<pn-glass-btn type="fab" color="primary">
|
||||||
<q-btn
|
<q-btn
|
||||||
fab
|
fab
|
||||||
icon="add"
|
icon="add"
|
||||||
color="brand"
|
color="transparent"
|
||||||
@click="createNewProject"
|
@click="createNewProject"
|
||||||
/>
|
/>
|
||||||
|
</pn-glass-btn>
|
||||||
</transition>
|
</transition>
|
||||||
</q-page-sticky>
|
</template>
|
||||||
</pn-page-card>
|
</pn-page-template>
|
||||||
|
|
||||||
<pn-small-dialog
|
<pn-small-dialog
|
||||||
v-model="showDialogArchiveProject"
|
v-model="showDialogArchiveProject"
|
||||||
@@ -221,6 +211,8 @@
|
|||||||
import { ref, computed, watch, onMounted } from 'vue'
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useProjectsStore } from 'stores/projects'
|
import { useProjectsStore } from 'stores/projects'
|
||||||
|
import pnGlassBtn from 'components/pnGlassBtn.vue'
|
||||||
|
import pnActionBar from 'components/pnActionBar.vue'
|
||||||
import type { Project } from 'types/Project'
|
import type { Project } from 'types/Project'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<doc-block title="privacy__title" document-name="Privacy-Policy"/>
|
<doc-block
|
||||||
|
title="account__privacy"
|
||||||
|
document-name="Privacy-Policy"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-template>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{ $t('settings__title') }}
|
{{ $t('settings__title') }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<pn-scroll-list>
|
<q-list>
|
||||||
<q-list separator>
|
|
||||||
<q-item-label header>{{ $t('settings__software_title') }}</q-item-label>
|
<q-item-label header>{{ $t('settings__software_title') }}</q-item-label>
|
||||||
|
|
||||||
<pn-item-btm-dialog
|
<pn-item-btm-dialog
|
||||||
@@ -80,8 +79,7 @@
|
|||||||
</pn-item-btm-dialog>
|
</pn-item-btm-dialog>
|
||||||
</template>
|
</template>
|
||||||
</q-list>
|
</q-list>
|
||||||
</pn-scroll-list>
|
</pn-page-template>
|
||||||
</pn-page-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-template>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{$t('subscribe__title')}}
|
{{$t('subscribe__title')}}
|
||||||
</template>
|
</template>
|
||||||
<pn-scroll-list>
|
|
||||||
<template #card-body-header>
|
<template #card-header>
|
||||||
<div
|
<div
|
||||||
v-if="tariffs && currentTariff"
|
v-if="tariffs && currentTariff"
|
||||||
id="subscribe-current-balance"
|
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">
|
<div class="flex w100 justify-between items-center no-wrap">
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="tariffs && currentTariff">
|
<div v-if="tariffs && currentTariff">
|
||||||
<subscribe-upgrade
|
<subscribe-upgrade
|
||||||
v-if="isCurrentTariffTest"
|
v-if="isCurrentTariffTest"
|
||||||
@@ -68,8 +69,7 @@
|
|||||||
@update="prepareTgBtn"
|
@update="prepareTgBtn"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</pn-scroll-list>
|
</pn-page-template>
|
||||||
</pn-page-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card>
|
<pn-page-template>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{ $t('support__title') }}
|
{{ $t('support__title') }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<pn-scroll-list>
|
|
||||||
<div class="q-pa-md flex column w100 items-center">
|
<div class="q-pa-md flex column w100 items-center">
|
||||||
<div align="center" >
|
<div align="center" >
|
||||||
<div>{{ $t('support__work_time_text') }}</div>
|
<div>{{ $t('support__work_time_text') }}</div>
|
||||||
@@ -33,8 +32,7 @@
|
|||||||
{{ supportEmailAddress }}
|
{{ supportEmailAddress }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</pn-scroll-list>
|
</pn-page-template>
|
||||||
</pn-page-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="q-pa-none flex column col-grow no-scroll">
|
<pn-page-card v-bind="$attrs">
|
||||||
<pn-scroll-list
|
<template #card-header>
|
||||||
:hideHeader="chats.length === 0"
|
|
||||||
>
|
|
||||||
<template #card-body-header>
|
|
||||||
<pn-action-bar
|
<pn-action-bar
|
||||||
|
v-if="chats.length !== 0"
|
||||||
v-model="search"
|
v-model="search"
|
||||||
placeholder="chats__search"
|
placeholder="chats__search"
|
||||||
:show-calendar-btn="false"
|
:show-calendar-btn="false"
|
||||||
@@ -12,27 +10,7 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #card-body-btn>
|
<q-list v-if="chats.length !== 0">
|
||||||
<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-slide-item
|
<q-slide-item
|
||||||
v-for="item in displayChats"
|
v-for="item in displayChats"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@@ -97,7 +75,7 @@
|
|||||||
</q-list>
|
</q-list>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="obsoleteChats.length !== 0"
|
v-if="displayObsoleteChats.length !== 0"
|
||||||
class="flex column items-center w100"
|
class="flex column items-center w100"
|
||||||
:class="showObsoleteChats ? 'bg-grey-12' : ''"
|
:class="showObsoleteChats ? 'bg-grey-12' : ''"
|
||||||
>
|
>
|
||||||
@@ -106,13 +84,13 @@
|
|||||||
color="grey"
|
color="grey"
|
||||||
flat
|
flat
|
||||||
no-caps
|
no-caps
|
||||||
@click="showObsoleteChats=!showObsoleteChats"
|
@click="showObsoleteChats = !showObsoleteChats"
|
||||||
icon-right="arrow_drop_down"
|
icon-right="arrow_drop_down"
|
||||||
:class="{ 'rotate-icon': showObsoleteChats }"
|
:class="{ 'rotate-icon': showObsoleteChats }"
|
||||||
>
|
>
|
||||||
<span class="text-caption">
|
<span class="text-caption">
|
||||||
{{ !showObsoleteChats
|
{{ !showObsoleteChats
|
||||||
? $t('chats__show_obsolete_chats') + ' (' + obsoleteChats.length +')'
|
? $t('chats__show_obsolete_chats') + ' (' + displayObsoleteChats.length +')'
|
||||||
: $t('chats__hide_obsolete_chats')
|
: $t('chats__hide_obsolete_chats')
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
@@ -124,9 +102,9 @@
|
|||||||
enter-active-class="animated slideInDown"
|
enter-active-class="animated slideInDown"
|
||||||
leave-active-class="animated slideOutUp"
|
leave-active-class="animated slideOutUp"
|
||||||
>
|
>
|
||||||
<q-list separator v-if="showObsoleteChats" class="w100">
|
<q-list v-if="showObsoleteChats" class="w100">
|
||||||
<q-item
|
<q-item
|
||||||
v-for = "item in obsoleteChats"
|
v-for = "item in displayObsoleteChats"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="w100 text-grey"
|
class="w100 text-grey"
|
||||||
>
|
>
|
||||||
@@ -168,58 +146,71 @@
|
|||||||
<pn-onboard-btn
|
<pn-onboard-btn
|
||||||
v-if="chats.length === 0 && obsoleteChats.length === 0 && chatsInit"
|
v-if="chats.length === 0 && obsoleteChats.length === 0 && chatsInit"
|
||||||
icon="mdi-chat-plus-outline"
|
icon="mdi-chat-plus-outline"
|
||||||
:message1="$t('chats__onboard_msg1')"
|
message1="chats__onboard_msg1"
|
||||||
:message2="$t('chats__onboard_msg2')"
|
message2="chats__onboard_msg2"
|
||||||
@click="showOverlay=true; fabState=true"
|
@click="showAttachChatDialog=true; fabState=true"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="flex column justify-center items-center w100"
|
class="column justify-center items-center w100"
|
||||||
style="position: absolute; bottom: 0;"
|
style="position: absolute; bottom: 0;"
|
||||||
v-if="!chatsInit"
|
v-if="!chatsInit"
|
||||||
>
|
>
|
||||||
<q-linear-progress indeterminate />
|
<q-linear-progress indeterminate />
|
||||||
</div>
|
</div>
|
||||||
</pn-scroll-list>
|
<template #card-fab>
|
||||||
|
<div v-if="chats.length !== 0 || obsoleteChats.length !== 0">
|
||||||
<q-page-sticky
|
|
||||||
:style="{ zIndex: !showOverlay ? 'inherit' : '5100 !important' }"
|
|
||||||
position="bottom-right"
|
|
||||||
:offset="[0, 18]"
|
|
||||||
class="fix-fab-offset"
|
|
||||||
>
|
|
||||||
<transition
|
<transition
|
||||||
appear
|
appear
|
||||||
enter-active-class="animated zoomIn"
|
enter-active-class="animated zoomIn"
|
||||||
>
|
>
|
||||||
<q-fab
|
<pn-glass-btn
|
||||||
v-model="fabState"
|
type="fab"
|
||||||
v-if="showFab"
|
color="grey-2"
|
||||||
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=""
|
|
||||||
>
|
>
|
||||||
|
<q-btn
|
||||||
|
@click="showDialogHelp = !showDialogHelp"
|
||||||
|
fab
|
||||||
|
icon="mdi-help"
|
||||||
|
color="transparent"
|
||||||
|
class="text-grey"
|
||||||
|
/>
|
||||||
|
</pn-glass-btn>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template #icon>
|
<transition
|
||||||
<q-list class="q-py-sm" v-if="isEnableChatAdd">
|
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
|
<q-item
|
||||||
v-for="item in fabMenu"
|
v-for="item in fabMenu"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="w100"
|
class="w100"
|
||||||
:clickable="item.type !== 'header'"
|
:clickable="item.type !== 'header'"
|
||||||
v-ripple
|
:v-ripple="item.type !== 'header'"
|
||||||
@click="item.type !== 'header' && item.func ? item?.func() : () => {}"
|
@click="item.type !== 'header' && item.func
|
||||||
|
? (showAttachChatDialog = false, item.func())
|
||||||
|
: null"
|
||||||
>
|
>
|
||||||
<template v-if="item.type === 'header'">
|
<template v-if="item.type === 'header'">
|
||||||
<div class="w100 column" :class="item.id === 4 ? 'q-pt-md' : ''">
|
<div class="w100 column" :class="item.id === 4 ? 'q-pt-md' : ''">
|
||||||
@@ -250,6 +241,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|
||||||
<div v-else class="w100 bg-white column q-py-md q-px-md rounded-card fab-action-item">
|
<div v-else class="w100 bg-white column q-py-md q-px-md rounded-card fab-action-item">
|
||||||
<div class="text-negative">
|
<div class="text-negative">
|
||||||
{{ $t('chats__disable_add_chat') }}
|
{{ $t('chats__disable_add_chat') }}
|
||||||
@@ -258,14 +250,7 @@
|
|||||||
{{ $t('chats__disable_add_chat_description') }}
|
{{ $t('chats__disable_add_chat_description') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</pn-simple-scroll-dialog>
|
||||||
</q-fab-action>
|
|
||||||
</q-fab>
|
|
||||||
</transition>
|
|
||||||
</q-page-sticky>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<pn-overlay v-if="showOverlay"/>
|
|
||||||
|
|
||||||
<pn-small-dialog
|
<pn-small-dialog
|
||||||
v-model="showDialogDeleteChat"
|
v-model="showDialogDeleteChat"
|
||||||
@@ -285,7 +270,7 @@
|
|||||||
title="chat__help_title"
|
title="chat__help_title"
|
||||||
>
|
>
|
||||||
<div class="q-py-none q-px-md q-ma-none">
|
<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="2"/>
|
||||||
<pn-chat-type-item :access="1"/>
|
<pn-chat-type-item :access="1"/>
|
||||||
<pn-chat-type-item :state="2"/>
|
<pn-chat-type-item :state="2"/>
|
||||||
@@ -306,21 +291,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</pn-simple-scroll-dialog>
|
</pn-simple-scroll-dialog>
|
||||||
|
<template #overlay>
|
||||||
|
<pn-overlay v-if="showAttachChatDialog" style="z-index:1005;"/>
|
||||||
|
</template>
|
||||||
|
</pn-page-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { useAuthStore } from 'stores/auth'
|
||||||
import { useChatsStore } from 'stores/chats'
|
import { useChatsStore } from 'stores/chats'
|
||||||
import type { WebApp } from '@twa-dev/types'
|
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 { useI18n } from "vue-i18n"
|
||||||
import { date } from 'quasar'
|
import { date } from 'quasar'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import pnChatTypeItem from 'components/pnChatTypeItem.vue'
|
import pnChatTypeItem from 'components/pnChatTypeItem.vue'
|
||||||
import pnSimpleScrollDialog from 'components/pnSimpleScrollDialog.vue'
|
import pnSimpleScrollDialog from 'components/pnSimpleScrollDialog.vue'
|
||||||
import pnActionBar from 'components/pnActionBar.vue'
|
import pnActionBar from 'components/pnActionBar.vue'
|
||||||
|
import pnGlassBtn from 'components/pnGlassBtn.vue'
|
||||||
import { chatAccess, chatState } from 'utils/chats'
|
import { chatAccess, chatState } from 'utils/chats'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -329,7 +318,7 @@
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const showOverlay = ref<boolean>(false)
|
const showAttachChatDialog = ref<boolean>(false)
|
||||||
const chatsStore = useChatsStore()
|
const chatsStore = useChatsStore()
|
||||||
const showDialogDeleteChat = ref<boolean>(false)
|
const showDialogDeleteChat = ref<boolean>(false)
|
||||||
const deleteChatId = ref<number | undefined>(undefined)
|
const deleteChatId = ref<number | undefined>(undefined)
|
||||||
@@ -444,28 +433,6 @@
|
|||||||
await router.push({ name: 'chat_info', params: { id: route.params.id, chatId }})
|
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) {
|
function getDate (d: number) {
|
||||||
return new Date(d * 1000).getFullYear() === new Date(Date.now()).getFullYear()
|
return new Date(d * 1000).getFullYear() === new Date(Date.now()).getFullYear()
|
||||||
? date.formatDate(d * 1000, 'DD MMM')
|
? date.formatDate(d * 1000, 'DD MMM')
|
||||||
@@ -475,31 +442,19 @@
|
|||||||
const obsoleteChats = computed(() => chatsStore.obsoleteChats)
|
const obsoleteChats = computed(() => chatsStore.obsoleteChats)
|
||||||
const showObsoleteChats = ref(false)
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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 */
|
/* fix mini border after slide */
|
||||||
:deep(.q-slide-item__right)
|
:deep(.q-slide-item__right)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,33 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="q-pa-none flex column col-grow no-scroll">
|
<pn-page-card v-bind="$attrs">
|
||||||
<pn-scroll-list
|
<q-list v-if="companies.length !== 0">
|
||||||
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">
|
|
||||||
<q-slide-item
|
<q-slide-item
|
||||||
v-for="item in displayCompanies"
|
v-for="item in displayCompanies"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@@ -97,38 +70,56 @@
|
|||||||
<pn-onboard-btn
|
<pn-onboard-btn
|
||||||
v-if="companies.length <= 1 && companiesInit"
|
v-if="companies.length <= 1 && companiesInit"
|
||||||
icon="mdi-account-multiple-plus-outline"
|
icon="mdi-account-multiple-plus-outline"
|
||||||
:message1="$t('companies__onboard_msg1')"
|
message1="companies__onboard_msg1"
|
||||||
:message2="$t('companies__onboard_msg2')"
|
message2="companies__onboard_msg2"
|
||||||
@btn-click="createCompany"
|
@btn-click="createCompany"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="flex column justify-center items-center w100"
|
class="column justify-center items-center w100"
|
||||||
style="position: absolute; bottom: 0;"
|
style="position: absolute; bottom: 0;"
|
||||||
v-if="!companiesInit"
|
v-if="!companiesInit"
|
||||||
>
|
>
|
||||||
<q-linear-progress indeterminate />
|
<q-linear-progress indeterminate />
|
||||||
</div>
|
</div>
|
||||||
</pn-scroll-list>
|
<template #card-fab>
|
||||||
|
|
||||||
<q-page-sticky
|
<div v-if="companies.length >= 3">
|
||||||
position="bottom-right"
|
|
||||||
:offset="[0, 18]"
|
|
||||||
class="fix-fab-offset"
|
|
||||||
>
|
|
||||||
<transition
|
<transition
|
||||||
appear
|
appear
|
||||||
enter-active-class="animated zoomIn"
|
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
|
<q-btn
|
||||||
v-if="showFab"
|
|
||||||
fab
|
fab
|
||||||
icon="add"
|
icon="add"
|
||||||
color="brand"
|
color="transparent"
|
||||||
@click="createCompany()"
|
@click="createCompany()"
|
||||||
/>
|
/>
|
||||||
|
</pn-glass-btn>
|
||||||
</transition>
|
</transition>
|
||||||
</q-page-sticky>
|
</template>
|
||||||
</div>
|
|
||||||
|
|
||||||
<pn-small-dialog
|
<pn-small-dialog
|
||||||
v-model="showDialogDeleteCompany"
|
v-model="showDialogDeleteCompany"
|
||||||
@@ -142,13 +133,15 @@
|
|||||||
@close="onCancel()"
|
@close="onCancel()"
|
||||||
@before-hide="onDialogBeforeHide()"
|
@before-hide="onDialogBeforeHide()"
|
||||||
/>
|
/>
|
||||||
|
</pn-page-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onActivated, onDeactivated, onBeforeUnmount } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { useCompaniesStore } from 'stores/companies'
|
import { useCompaniesStore } from 'stores/companies'
|
||||||
import { useUsersStore } from 'stores/users'
|
import { useUsersStore } from 'stores/users'
|
||||||
|
import PnGlassBtn from 'components/pnGlassBtn.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -222,28 +215,6 @@
|
|||||||
return arr.length
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -55,10 +55,8 @@
|
|||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
@click="toProjects"
|
@click="toProjects"
|
||||||
flat
|
flat no-caps
|
||||||
no-caps
|
dense rounded
|
||||||
dense
|
|
||||||
rounded
|
|
||||||
class="q-ml-lg"
|
class="q-ml-lg"
|
||||||
>
|
>
|
||||||
<span class="flex items-center no-wrap q-pl-sm">
|
<span class="flex items-center no-wrap q-pl-sm">
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="q-pa-none flex column col-grow no-scroll">
|
<pn-page-card v-bind="$attrs">
|
||||||
<pn-scroll-list
|
<template #card-header>
|
||||||
:hideHeader="users.length === 0"
|
|
||||||
>
|
|
||||||
<template #card-body-header>
|
|
||||||
<pn-action-bar
|
<pn-action-bar
|
||||||
|
v-if="users.length !== 0"
|
||||||
v-model="search"
|
v-model="search"
|
||||||
placeholder="users__search"
|
placeholder="users__search"
|
||||||
:show-calendar-btn="false"
|
:show-calendar-btn="false"
|
||||||
@@ -12,7 +10,7 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<q-list separator v-if="users.length !== 0">
|
<q-list v-if="users.length !== 0">
|
||||||
<q-slide-item
|
<q-slide-item
|
||||||
v-for="item in displayUsers"
|
v-for="item in displayUsers"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@@ -63,7 +61,7 @@
|
|||||||
enter-active-class="animated slideInDown"
|
enter-active-class="animated slideInDown"
|
||||||
leave-active-class="animated slideOutUp"
|
leave-active-class="animated slideOutUp"
|
||||||
>
|
>
|
||||||
<q-list separator v-if="showLeaveUsers" class="w100">
|
<q-list v-if="showLeaveUsers" class="w100">
|
||||||
<q-item
|
<q-item
|
||||||
v-for = "item in leaveUsers"
|
v-for = "item in leaveUsers"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@@ -106,7 +104,7 @@
|
|||||||
enter-active-class="animated slideInDown"
|
enter-active-class="animated slideInDown"
|
||||||
leave-active-class="animated slideOutUp"
|
leave-active-class="animated slideOutUp"
|
||||||
>
|
>
|
||||||
<q-list separator v-if="showBlockedUsers" class="w100">
|
<q-list v-if="showBlockedUsers" class="w100">
|
||||||
<q-item
|
<q-item
|
||||||
v-for = "item in blockedUsers"
|
v-for = "item in blockedUsers"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@@ -126,19 +124,17 @@
|
|||||||
<pn-onboard-btn
|
<pn-onboard-btn
|
||||||
v-if="users.length === 0 && usersInit"
|
v-if="users.length === 0 && usersInit"
|
||||||
icon="mdi-account-outline"
|
icon="mdi-account-outline"
|
||||||
:message1="$t('users__onboard_msg1')"
|
message1="users__onboard_msg1"
|
||||||
:message2="$t('users__onboard_msg2')"
|
message2="users__onboard_msg2"
|
||||||
noBtn
|
noBtn
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="flex column justify-center items-center w100"
|
class="column justify-center items-center w100"
|
||||||
style="position: absolute; bottom: 0;"
|
style="position: absolute; bottom: 0;"
|
||||||
v-if="!usersInit"
|
v-if="!usersInit"
|
||||||
>
|
>
|
||||||
<q-linear-progress indeterminate />
|
<q-linear-progress indeterminate />
|
||||||
</div>
|
</div>
|
||||||
</pn-scroll-list>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<pn-small-dialog
|
<pn-small-dialog
|
||||||
v-model="showDialogBlockUser"
|
v-model="showDialogBlockUser"
|
||||||
@@ -164,6 +160,7 @@
|
|||||||
@clickMainBtn="onConfirmRestoreUser()"
|
@clickMainBtn="onConfirmRestoreUser()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
</pn-page-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'chats',
|
name: 'chats',
|
||||||
path: 'chats',
|
path: 'chats',
|
||||||
component: () => import('pages/project-page/ProjectPageChats.vue'),
|
component: () => import('pages/project-page/ProjectPageChats.vue'),
|
||||||
|
props: true,
|
||||||
meta: {
|
meta: {
|
||||||
backRoute: 'projects'
|
backRoute: 'projects'
|
||||||
}
|
}
|
||||||
@@ -52,6 +53,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
{
|
{
|
||||||
name: 'users',
|
name: 'users',
|
||||||
path: 'users',
|
path: 'users',
|
||||||
|
props: true,
|
||||||
component: () => import('pages/project-page/ProjectPageUsers.vue'),
|
component: () => import('pages/project-page/ProjectPageUsers.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
backRoute: 'projects'
|
backRoute: 'projects'
|
||||||
@@ -60,6 +62,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
{
|
{
|
||||||
name: 'companies',
|
name: 'companies',
|
||||||
path: 'companies',
|
path: 'companies',
|
||||||
|
props: true,
|
||||||
component: () => import('pages/project-page/ProjectPageCompanies.vue'),
|
component: () => import('pages/project-page/ProjectPageCompanies.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
backRoute: 'projects'
|
backRoute: 'projects'
|
||||||
@@ -138,9 +141,9 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('pages/account/ConsentPage.vue')
|
component: () => import('pages/account/ConsentPage.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'change_tariff_rules',
|
name: 'tariffs_info',
|
||||||
path: '/change-tariff-rules',
|
path: '/tariffs-info',
|
||||||
component: () => import('src/pages/account/ChangeTariffRules.vue')
|
component: () => import('pages/account/TariffsInfoPage.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'support',
|
name: 'support',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useProjectsStore } from 'stores/projects'
|
|||||||
import { useAuthStore } from 'stores/auth'
|
import { useAuthStore } from 'stores/auth'
|
||||||
import { useChatsStore } from 'stores/chats'
|
import { useChatsStore } from 'stores/chats'
|
||||||
import { useUsersStore } from 'stores/users'
|
import { useUsersStore } from 'stores/users'
|
||||||
|
import { useCompaniesStore } from 'stores/companies'
|
||||||
|
|
||||||
export const useSSEStore = defineStore('sse', () => {
|
export const useSSEStore = defineStore('sse', () => {
|
||||||
const eventSource = ref<EventSource | null>(null)
|
const eventSource = ref<EventSource | null>(null)
|
||||||
@@ -16,6 +17,7 @@ export const useSSEStore = defineStore('sse', () => {
|
|||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const chatsStore = useChatsStore()
|
const chatsStore = useChatsStore()
|
||||||
const usersStore = useUsersStore()
|
const usersStore = useUsersStore()
|
||||||
|
const companiesStore = useCompaniesStore()
|
||||||
|
|
||||||
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
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)
|
const data = JSON.parse(event.data)
|
||||||
if (data.project_id === currentProjectId.value) {
|
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)
|
const eventSourceComputed = computed(() => eventSource.value)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ interface ProjectParams {
|
|||||||
description: string | null
|
description: string | null
|
||||||
logo: string | null
|
logo: string | null
|
||||||
is_logo_bg: boolean | null
|
is_logo_bg: boolean | null
|
||||||
|
chat_info: string | null
|
||||||
[key: string]: unknown
|
[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, " ")
|
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 {
|
export {
|
||||||
parseIntString,
|
parseIntString,
|
||||||
removeNullKeys,
|
removeNullKeys,
|
||||||
getFieldValue,
|
getFieldValue,
|
||||||
formatNumber
|
formatNumber,
|
||||||
|
nameTgUser
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user