before_redesign
This commit is contained in:
BIN
i18n-2.xlsm
BIN
i18n-2.xlsm
Binary file not shown.
34
src/App.vue
34
src/App.vue
@@ -3,13 +3,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, inject, onUnmounted, watch } from 'vue'
|
import { computed, watch, inject, onMounted, onUnmounted } from 'vue'
|
||||||
import { useSettingsStore } from 'stores/settings'
|
import { useSettingsStore } from 'stores/settings'
|
||||||
import { useAuthStore } from 'stores/auth'
|
import { useAuthStore } from 'stores/auth'
|
||||||
import { useSSEStore } from 'stores/SSE'
|
import { useSSEStore } from 'stores/SSE'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import type { WebApp } from '@twa-dev/types'
|
import type { WebApp } from '@twa-dev/types'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useQuasar } from 'quasar'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import type { CompanyParams } from 'types/Company'
|
import type { CompanyParams } from 'types/Company'
|
||||||
|
|
||||||
@@ -40,6 +41,25 @@
|
|||||||
if (newVal) await settingsStore.init()
|
if (newVal) await settingsStore.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// displayMode
|
||||||
|
const displayMode = computed(() => settingsStore.settings.displayMode)
|
||||||
|
const bgColor = getComputedStyle(document.documentElement)
|
||||||
|
.getPropertyValue('--bg-base').trim() as `#${string}`
|
||||||
|
const headerColor = bgColor || tg.themeParams.header_bg_color || tg.themeParams.bg_color
|
||||||
|
|
||||||
|
watch(displayMode, (val) => {
|
||||||
|
if (val === 'full' && tg.isVersionAtLeast('8.0')) {
|
||||||
|
tg.requestFullscreen()
|
||||||
|
tg.setHeaderColor('bg_color')
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
tg.exitFullscreen()
|
||||||
|
tg.expand()
|
||||||
|
tg.setHeaderColor(headerColor)
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await settingsStore.init()
|
await settingsStore.init()
|
||||||
if (isAuth.value) sseStore.connect()
|
if (isAuth.value) sseStore.connect()
|
||||||
@@ -49,4 +69,16 @@
|
|||||||
sseStore.disconnect()
|
sseStore.disconnect()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const $q = useQuasar()
|
||||||
|
|
||||||
|
$q.iconMapFn = (iconName) => {
|
||||||
|
if (iconName.startsWith('pn-') === true) {
|
||||||
|
const name = iconName.substring(3)
|
||||||
|
|
||||||
|
return {
|
||||||
|
cls: 'pn-icon ' + name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
2
src/Global-properties.d.ts
vendored
2
src/Global-properties.d.ts
vendored
@@ -5,6 +5,8 @@ declare module 'vue' {
|
|||||||
$tg: WebApp;
|
$tg: WebApp;
|
||||||
$isDesktop: boolean;
|
$isDesktop: boolean;
|
||||||
$isMobile: boolean;
|
$isMobile: boolean;
|
||||||
|
$isIOS: boolean;
|
||||||
|
$isAndroid: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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'
|
||||||
import pnImageSelector from 'components/pnImageSelector.vue'
|
|
||||||
import pnShadowScroll from 'components/pnShadowScroll.vue'
|
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'
|
||||||
@@ -18,7 +17,6 @@ const components = {
|
|||||||
pnPageTemplate,
|
pnPageTemplate,
|
||||||
pnAutoAvatar,
|
pnAutoAvatar,
|
||||||
pnOverlay,
|
pnOverlay,
|
||||||
pnImageSelector,
|
|
||||||
pnSmallDialog,
|
pnSmallDialog,
|
||||||
pnShadowScroll,
|
pnShadowScroll,
|
||||||
pnMagicOverlay,
|
pnMagicOverlay,
|
||||||
|
|||||||
@@ -25,16 +25,15 @@ export default defineBoot(({ app }) => {
|
|||||||
'webk'
|
'webk'
|
||||||
].includes(platform)
|
].includes(platform)
|
||||||
const isTablet = window.innerWidth > 1024
|
const isTablet = window.innerWidth > 1024
|
||||||
if (!isDesktopOrWeb && !isTablet) {
|
if (!isDesktopOrWeb && !isTablet) tg.expand()
|
||||||
if (tg.isVersionAtLeast('8.0')) tg.requestFullscreen()
|
|
||||||
else tg.expand()
|
|
||||||
}
|
|
||||||
|
|
||||||
tg.SettingsButton.show()
|
tg.SettingsButton.show()
|
||||||
|
|
||||||
app.config.globalProperties.$tg = tg
|
app.config.globalProperties.$tg = tg
|
||||||
app.config.globalProperties.$isDesktop = isDesktopOrWeb || isTablet
|
app.config.globalProperties.$isDesktop = isDesktopOrWeb || isTablet
|
||||||
app.config.globalProperties.$isMobile = !isDesktopOrWeb && !isTablet
|
app.config.globalProperties.$isMobile = !isDesktopOrWeb && !isTablet
|
||||||
|
app.config.globalProperties.$isIOS = platform === 'ios'
|
||||||
|
app.config.globalProperties.$isAndroid = platform === 'android'
|
||||||
|
|
||||||
app.provide('tg', tg)
|
app.provide('tg', tg)
|
||||||
})
|
})
|
||||||
@@ -46,7 +45,6 @@ export default defineBoot(({ app }) => {
|
|||||||
|
|
||||||
script setup
|
script setup
|
||||||
import { getCurrentInstance } from 'vue'
|
import { getCurrentInstance } from 'vue'
|
||||||
|
const app = getCurrentInstance()
|
||||||
const { proxy } = getCurrentInstance()
|
const isMobile = app?.appContext.config.globalProperties.$isMobile
|
||||||
and use proxy.$isMobile
|
|
||||||
*/
|
*/
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
<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"/>
|
||||||
|
|
||||||
<pn-image-selector
|
<pn-image-selector
|
||||||
v-model="companyMod.logo"
|
v-model="companyMod.logo"
|
||||||
:size="100"
|
:size="100"
|
||||||
@@ -30,31 +29,33 @@
|
|||||||
@validation-error="isFormValid = false"
|
@validation-error="isFormValid = false"
|
||||||
class="q-gutter-y-lg w100"
|
class="q-gutter-y-lg w100"
|
||||||
>
|
>
|
||||||
|
<template
|
||||||
<q-input
|
|
||||||
v-for="input in textInputs"
|
v-for="input in textInputs"
|
||||||
:key="input.id"
|
:key="input.id"
|
||||||
:model-value="companyMod[input.val]"
|
|
||||||
@update:model-value="handleInput(input.val, $event, input.useTrim)"
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
class="w100 fix-bottom-padding"
|
|
||||||
:class="input.val === 'name' ? '' : 'q-pt-sm'"
|
|
||||||
:autogrow="input.val === 'description' || input.val === 'address'"
|
|
||||||
:label="input.label ? $t(input.label) : void 0"
|
|
||||||
:rules="input.rules"
|
|
||||||
no-error-icon
|
|
||||||
:label-slot="Boolean(input.label)"
|
|
||||||
debounce="300"
|
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<q-input
|
||||||
<q-icon v-if="input.icon" :name="input.icon"/>
|
v-model="companyMod[input.val]"
|
||||||
</template>
|
no-error-icon
|
||||||
<template #label v-if="input.label">
|
dense
|
||||||
{{$t(input.label) }}
|
filled
|
||||||
<span v-if="input.val === 'name'" class="text-red">*</span>
|
:label="input.label ? $t(input.label) : void 0"
|
||||||
</template>
|
:label-slot="Boolean(input.label)"
|
||||||
</q-input>
|
class="w100 fix-bottom-padding"
|
||||||
|
:class="input.val === 'name' ? '' : 'q-pt-sm'"
|
||||||
|
:rules="input.rules"
|
||||||
|
@update:model-value="formRef.validate()"
|
||||||
|
:autogrow="input.val === 'description' || input.val === 'address'"
|
||||||
|
debounce="300"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon v-if="input.icon" :name="input.icon"/>
|
||||||
|
</template>
|
||||||
|
<template #label v-if="input.label">
|
||||||
|
{{$t(input.label) }}
|
||||||
|
<span v-if="input.val === 'name'" class="text-red">*</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</template>
|
||||||
</q-form>
|
</q-form>
|
||||||
</div>
|
</div>
|
||||||
</pn-page-template>
|
</pn-page-template>
|
||||||
@@ -62,8 +63,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted, } from 'vue'
|
import { ref, reactive, onMounted, } from 'vue'
|
||||||
import { removeNullKeys, getFieldValue } from 'utils/helpers'
|
import pnImageSelector from 'components/pnImageSelector.vue'
|
||||||
import { useInputRules } from 'composables/useInputRules'
|
import { useInputRules } from 'composables/useInputRules'
|
||||||
|
import { removeNullKeys, getFieldValue } from 'utils/helpers'
|
||||||
import type { CompanyParams } from 'types/Company'
|
import type { CompanyParams } from 'types/Company'
|
||||||
|
|
||||||
const { inputRules } = useInputRules()
|
const { inputRules } = useInputRules()
|
||||||
@@ -96,37 +98,37 @@
|
|||||||
icon?: string
|
icon?: string
|
||||||
val: keyof CompanyParams
|
val: keyof CompanyParams
|
||||||
rules: ((value: string) => boolean | string)[]
|
rules: ((value: string) => boolean | string)[]
|
||||||
useTrim: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const textInputs: TextInput[] = [
|
const textInputs: TextInput[] = [
|
||||||
{ id: 1, val: 'name', label: 'company_block__name', rules: [inputRules.require, inputRules.middleText], useTrim: true },
|
{ id: 1, val: 'name', label: 'company_block__name', rules: [inputRules.require, inputRules.middleText] },
|
||||||
{ id: 2, val: 'description', label: 'company_block__description', rules: [inputRules.longText], useTrim: false },
|
{ id: 2, val: 'description', label: 'company_block__description', rules: [inputRules.longText] },
|
||||||
{ id: 3, val: 'site', icon: 'mdi-web', rules: [inputRules.middleText], useTrim: true },
|
{ id: 3, val: 'site', icon: 'mdi-web', rules: [inputRules.middleText] },
|
||||||
{ id: 4, val: 'address', icon: 'mdi-map-marker-outline', rules: [inputRules.longText], useTrim: false },
|
{ id: 4, val: 'address', icon: 'mdi-map-marker-outline', rules: [inputRules.longText] },
|
||||||
{ id: 5, val: 'phone', icon: 'mdi-phone-outline', rules: [inputRules.middleText], useTrim: true },
|
{ id: 5, val: 'phone', icon: 'mdi-phone-outline', rules: [inputRules.middleText] },
|
||||||
{ id: 6, val: 'email', icon: 'mdi-email-outline', rules: [inputRules.middleText], useTrim: true }
|
{ id: 6, val: 'email', icon: 'mdi-email-outline', rules: [inputRules.middleText] }
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
const handleInput = (field: keyof CompType, value: string | null | number, useTrim: boolean) => {
|
|
||||||
companyMod[field] = typeof value === 'string' ? (useTrim ? value.trim() : value) : value
|
|
||||||
if (formRef.value) formRef.value.validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
const isFormValid = ref(false)
|
const isFormValid = ref(false)
|
||||||
|
|
||||||
function onSubmit() {
|
function onSubmit() {
|
||||||
// companyMod.description! - ! on end for TS, i promise: params !== null | undefined
|
const { logo, name, description, site, address, phone, email } = companyMod
|
||||||
|
let logoValue: string | null | undefined = undefined
|
||||||
|
|
||||||
|
if (typeof logo === 'string' && logo.startsWith('data:image')) logoValue = logo
|
||||||
|
if (logo === null) logoValue = null
|
||||||
|
|
||||||
const outData = {
|
const outData = {
|
||||||
name: companyMod.name,
|
name: name.trim(),
|
||||||
description: getFieldValue(companyMod.description!, props.company?.description),
|
description: getFieldValue(description!.trim(), props.company?.description),
|
||||||
site: getFieldValue(companyMod.site!, props.company?.site),
|
site: getFieldValue(site!.trim(), props.company?.site),
|
||||||
address: getFieldValue(companyMod.address!, props.company?.address),
|
address: getFieldValue(address!.trim(), props.company?.address),
|
||||||
phone: getFieldValue(companyMod.phone!, props.company?.phone),
|
phone: getFieldValue(phone!.trim(), props.company?.phone),
|
||||||
email: getFieldValue(companyMod.email!, props.company?.email),
|
email: getFieldValue(email!.trim(), props.company?.email),
|
||||||
logo: companyMod.logo !== '' ? companyMod.logo : null
|
...(logoValue !== undefined && { logo: logoValue })
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('update', removeNullKeys(outData, ['logo']))
|
emit('update', removeNullKeys(outData, ['logo']))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
14
src/components/infoBlockEmpty.vue
Normal file
14
src/components/infoBlockEmpty.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<div class="column items-center text-grey w100">
|
||||||
|
<div class="text-h6 w60 text-center">
|
||||||
|
{{$t('empty_description')}}
|
||||||
|
</div>
|
||||||
|
<q-icon size="120px" name="mdi-clippy" class="q-ma-sm"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
@@ -1,138 +1,106 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="background-canvas-wrapper" class="flex fit column bg-base">
|
<div
|
||||||
<canvas id="canvas"/>
|
id="background-canvas-wrapper"
|
||||||
|
class="w100 no-scroll overflow-hidden bg-base position-relative"
|
||||||
|
style="height: 100vh;"
|
||||||
|
>
|
||||||
|
<canvas id="canvas" class="absolute-top"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup> // eslint-disable-line
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
import { onMounted } from 'vue'
|
onMounted(() => {
|
||||||
|
const canvas = document.getElementById("canvas") as HTMLCanvasElement
|
||||||
onMounted(() => {
|
const ctx = canvas.getContext("2d")!
|
||||||
const canvasBody = document.getElementById("canvas")
|
const opts = {
|
||||||
const drawArea = canvasBody.getContext("2d")
|
pColor: "rgb(200,200,200)",
|
||||||
|
lRGB: "200,200,200",
|
||||||
const opts = {
|
amt: 20,
|
||||||
particleColor: "rgb(200,200,200)",
|
defS: 0.1,
|
||||||
lineColor: "rgb(200,200,200)",
|
varS: 1,
|
||||||
particleAmount: 30,
|
defR: 2,
|
||||||
defaultSpeed: 0.1,
|
varR: 2,
|
||||||
variantSpeed: 1,
|
linkR2: 200 ** 2
|
||||||
defaultRadius: 2,
|
|
||||||
variantRadius: 2,
|
|
||||||
linkRadius: 200
|
|
||||||
}
|
|
||||||
|
|
||||||
const delay = 200
|
|
||||||
let tid
|
|
||||||
const rgb = opts.lineColor.match(/\d+/g)
|
|
||||||
let w
|
|
||||||
let h
|
|
||||||
const particles = []
|
|
||||||
|
|
||||||
function resizeReset () {
|
|
||||||
w = canvasBody.width = window.innerWidth
|
|
||||||
h = canvasBody.height = window.innerHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
function deBouncer () {
|
|
||||||
clearTimeout(tid)
|
|
||||||
tid = setTimeout(function () { resizeReset() }, delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkDistance (x1, y1, x2, y2) {
|
|
||||||
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
function setup () {
|
|
||||||
resizeReset()
|
|
||||||
for (let i = 0; i < opts.particleAmount; i++) {
|
|
||||||
particles.push(new Particle())
|
|
||||||
}
|
}
|
||||||
window.requestAnimationFrame(loop)
|
|
||||||
}
|
|
||||||
|
|
||||||
function loop() {
|
let w: number, h: number, aid: number
|
||||||
window.requestAnimationFrame(loop)
|
const particles: Particle[] = []
|
||||||
drawArea.clearRect(0, 0, w, h)
|
|
||||||
for (let i = 0; i < particles.length; i++) {
|
|
||||||
particles[i].update()
|
|
||||||
particles[i].draw()
|
|
||||||
}
|
|
||||||
for (let i = 0; i < particles.length; i++) {
|
|
||||||
linkPoints(particles[i], particles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function linkPoints (point1, hubs) {
|
const resize = () => {
|
||||||
for (let i = 0; i < hubs.length; i++) {
|
w = canvas.width = window.innerWidth
|
||||||
const distance = checkDistance(point1.x, point1.y, hubs[i].x, hubs[i].y)
|
h = canvas.height = window.innerHeight
|
||||||
const opacity = 1 - distance / opts.linkRadius
|
}
|
||||||
if (opacity > 0) {
|
|
||||||
drawArea.lineWidth = 0.5
|
class Particle {
|
||||||
drawArea.strokeStyle = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${opacity})`
|
x = Math.random() * w
|
||||||
drawArea.beginPath()
|
y = Math.random() * h
|
||||||
drawArea.moveTo(point1.x, point1.y)
|
s = opts.defS + Math.random() * opts.varS
|
||||||
drawArea.lineTo(hubs[i].x, hubs[i].y)
|
a = Math.random() * Math.PI * 2
|
||||||
drawArea.closePath()
|
vx = Math.cos(this.a) * this.s
|
||||||
drawArea.stroke()
|
vy = Math.sin(this.a) * this.s
|
||||||
|
r = opts.defR + Math.random() * opts.varR
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (this.x + this.vx > w || this.x + this.vx < 0) this.vx *= -1
|
||||||
|
if (this.y + this.vy > h || this.y + this.vy < 0) this.vy *= -1
|
||||||
|
this.x += this.vx
|
||||||
|
this.y += this.vy
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2)
|
||||||
|
ctx.fillStyle = opts.pColor
|
||||||
|
ctx.fill()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function Particle () {
|
resize()
|
||||||
this.x = Math.random() * w
|
for (let i = 0; i < opts.amt; i++) particles.push(new Particle())
|
||||||
this.y = Math.random() * h
|
|
||||||
this.speed = opts.defaultSpeed + Math.random() * opts.variantSpeed
|
const loop = () => {
|
||||||
this.directionAngle = Math.floor(Math.random() * 360)
|
ctx.clearRect(0, 0, w, h)
|
||||||
this.color = opts.particleColor
|
particles.forEach(p => p.update())
|
||||||
this.radius = opts.defaultRadius + Math.random() * opts. variantRadius
|
|
||||||
this.vector = {
|
for (let i = 0; i < particles.length; i++) {
|
||||||
x: Math.cos(this.directionAngle) * this.speed,
|
const p1 = particles[i]
|
||||||
y: Math.sin(this.directionAngle) * this.speed
|
for (let j = i + 1; j < particles.length; j++) {
|
||||||
}
|
const p2 = particles[j]
|
||||||
this.update = function () {
|
if (!p1 || !p2) continue
|
||||||
this.border()
|
|
||||||
this.x += this.vector.x
|
const dx = p1.x - p2.x
|
||||||
this.y += this.vector.y
|
const dy = p1.y - p2.y
|
||||||
}
|
const d2 = dx * dx + dy * dy
|
||||||
this.border = function () {
|
if (d2 < opts.linkR2) {
|
||||||
if (this.x >= w || this.x <= 0) {
|
ctx.strokeStyle = `rgba(${opts.lRGB}, ${1 - d2 / opts.linkR2})`
|
||||||
this.vector.x *= -1
|
ctx.lineWidth = 0.5
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(p1.x, p1.y)
|
||||||
|
ctx.lineTo(p2.x, p2.y)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (this.y >= h || this.y <= 0) {
|
aid = requestAnimationFrame(loop)
|
||||||
this.vector.y *= -1
|
|
||||||
}
|
|
||||||
if (this.x > w) this.x = w
|
|
||||||
if (this.y > h) this.y = h
|
|
||||||
if (this.x < 0) this.x = 0
|
|
||||||
if (this.y < 0) this.y = 0
|
|
||||||
}
|
}
|
||||||
this.draw = function () {
|
|
||||||
drawArea.beginPath()
|
|
||||||
drawArea.arc(this.x, this.y, this.radius, 0, Math.PI*2)
|
|
||||||
drawArea.closePath()
|
|
||||||
drawArea.fillStyle = this.color
|
|
||||||
drawArea.fill()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("resize", function () { deBouncer() })
|
window.addEventListener("resize", resize)
|
||||||
resizeReset()
|
loop()
|
||||||
setup()
|
|
||||||
})
|
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener("resize", resize)
|
||||||
|
cancelAnimationFrame(aid)
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#background-canvas-wrapper {
|
#background-canvas-wrapper {
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
display: block;
|
display: block;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100%;
|
height: 100vh !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
<div class="w100">
|
<div class="w100" style="padding-top: 1px; padding-bottom: 1px;">
|
||||||
<q-input
|
<q-input
|
||||||
v-model="search"
|
v-model="search"
|
||||||
clearable
|
clearable
|
||||||
@@ -55,11 +55,13 @@
|
|||||||
rounded
|
rounded
|
||||||
floating
|
floating
|
||||||
transparent
|
transparent
|
||||||
style="position: relative; top: -6px; margin-left: -12px"
|
style="position: relative; top: -6px; margin-left: -12px;"
|
||||||
:style="{ opacity: filterBadge ? 0.8 : 0 }"
|
:style="{ opacity: filterBadge ? 0.8 : 0 }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
||||||
|
<slot/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
transition-show="slide-up"
|
transition-show="slide-up"
|
||||||
transition-hide="slide-down"
|
transition-hide="slide-down"
|
||||||
position="bottom"
|
position="bottom"
|
||||||
>
|
:persistent="loading"
|
||||||
|
>
|
||||||
<q-card
|
<q-card
|
||||||
ref="cardRef"
|
ref="cardRef"
|
||||||
class="fix-card-width column no-scroll no-wrap q-px-none overflow-hidden relative-position"
|
class="fix-card-width column no-scroll no-wrap q-px-none overflow-hidden relative-position"
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
{{ $t(caption) }}
|
{{ $t(caption) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between no-wrap">
|
<div class="flex items-center justify-between no-wrap" v-if="!loading">
|
||||||
<slot name="btnSlot">
|
<slot name="btnSlot">
|
||||||
<q-btn icon="mdi-close" @click="modelValue = false" flat round />
|
<q-btn icon="mdi-close" @click="modelValue = false" flat round />
|
||||||
</slot>
|
</slot>
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
:height="bodyHeight"
|
:height="bodyHeight"
|
||||||
:bottomOffset
|
:bottomOffset
|
||||||
:topOffset
|
:topOffset
|
||||||
>
|
>
|
||||||
<div class="q-pa-none q-ma-none">
|
<div class="q-pa-none q-ma-none">
|
||||||
<q-resize-observer @resize="onInnerBodyResize" />
|
<q-resize-observer @resize="onInnerBodyResize" />
|
||||||
<slot />
|
<slot />
|
||||||
@@ -63,7 +64,11 @@
|
|||||||
title: string
|
title: string
|
||||||
caption?: string
|
caption?: string
|
||||||
maximized?: boolean
|
maximized?: boolean
|
||||||
}>(), { maximized: true })
|
loading?: boolean
|
||||||
|
}>(), {
|
||||||
|
maximized: true,
|
||||||
|
loading: false
|
||||||
|
})
|
||||||
|
|
||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
const cardRef = ref()
|
const cardRef = ref()
|
||||||
|
|||||||
35
src/components/pnCutterLongText.vue
Normal file
35
src/components/pnCutterLongText.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="text.length <= tailLength" class="w100">
|
||||||
|
{{ text }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex no-wrap w100">
|
||||||
|
<div class="col ellipsis col-shrink">
|
||||||
|
{{ textStart }}
|
||||||
|
</div>
|
||||||
|
<div class="col col-grow">
|
||||||
|
{{ textEnd }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
text: string
|
||||||
|
tailLength?: number
|
||||||
|
}>(), {
|
||||||
|
tailLength: 15
|
||||||
|
})
|
||||||
|
|
||||||
|
const textStart = computed(() => {
|
||||||
|
return props.text.slice(0, props.text.length - props.tailLength)
|
||||||
|
})
|
||||||
|
|
||||||
|
const textEnd = computed(() => {
|
||||||
|
return props.text.slice(-props.tailLength)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
@@ -1,74 +1,52 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="q-mb-sm">
|
||||||
<div
|
<div
|
||||||
class="relative-position"
|
class="relative-position"
|
||||||
|
:style="{
|
||||||
|
width: sizePx,
|
||||||
|
height: sizePx,
|
||||||
|
display: 'block'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<q-file
|
||||||
|
ref="imgFileSelector"
|
||||||
|
v-model="imageFile"
|
||||||
|
:style="{ display: 'none' }"
|
||||||
|
@update:model-value="handleUpload()"
|
||||||
|
:filter="checkImgType"
|
||||||
|
:accept="allowedMimeTypes.join(', ')"
|
||||||
|
/>
|
||||||
|
<q-icon
|
||||||
|
v-if="modelValue === '' || modelValue === undefined || modelValue === null"
|
||||||
|
name="mdi-camera-plus-outline"
|
||||||
|
class="absolute-full fit text-grey-4"
|
||||||
|
:style="{ fontSize: String(iconsize) + 'px'}"
|
||||||
|
@click = "imgFileSelectorClick"
|
||||||
|
/>
|
||||||
|
<q-img
|
||||||
|
v-else
|
||||||
|
fit="cover"
|
||||||
|
:src="modelValue"
|
||||||
:style="{
|
:style="{
|
||||||
width: sizePx,
|
|
||||||
height: sizePx,
|
height: sizePx,
|
||||||
display: 'block'
|
maxWidth: sizePx,
|
||||||
|
borderRadius: avatar ? String(size/2) + 'px' : 'var(--top-radius)',
|
||||||
}"
|
}"
|
||||||
>
|
@click="imgFileSelectorClick"
|
||||||
<q-file
|
/>
|
||||||
ref="imgFileSelector"
|
|
||||||
v-model="imageFile"
|
|
||||||
:style="{ display: 'none' }"
|
|
||||||
@update:model-value="handleUpload()"
|
|
||||||
:filter="checkImgType"
|
|
||||||
:accept="allowedMimeTypes.join(', ')"
|
|
||||||
/>
|
|
||||||
<q-icon
|
|
||||||
v-if="modelValue === '' || modelValue === undefined || modelValue === null"
|
|
||||||
name="mdi-camera-plus-outline"
|
|
||||||
class="absolute-full fit text-grey-4"
|
|
||||||
:style="{ fontSize: String(iconsize) + 'px'}"
|
|
||||||
@click = "imgFileSelectorClick"
|
|
||||||
/>
|
|
||||||
<q-img
|
|
||||||
v-else
|
|
||||||
fit="cover"
|
|
||||||
:src="modelValue"
|
|
||||||
:style="{
|
|
||||||
height: sizePx,
|
|
||||||
maxWidth: sizePx,
|
|
||||||
borderRadius: avatar ? String(size/2) + 'px' : 'var(--top-radius)',
|
|
||||||
}"
|
|
||||||
@click="showDialog = true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-dialog v-model="showDialog">
|
<q-btn
|
||||||
<q-card
|
v-if="modelValue"
|
||||||
class="w100 relative-position overflow-hidden"
|
icon="mdi-delete-outline"
|
||||||
style="height: auto;"
|
round
|
||||||
>
|
color="primary"
|
||||||
<q-img :src="modelValue"/>
|
size="md"
|
||||||
<q-btn
|
style="border: solid 2px white; bottom: -16px; right: -16px;"
|
||||||
icon="mdi-close"
|
class="absolute-bottom-right"
|
||||||
@click="showDialog = false"
|
@click="deleteImage"
|
||||||
class="absolute-top-right q-ma-sm"
|
/>
|
||||||
color="grey-9"
|
|
||||||
round flat
|
|
||||||
style="background-color: rgba(255, 255, 255, 0.65) !important;"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex no-wrap items-center justify-center q-pb-sm absolute-bottom w100"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
v-for="btn in menuBtns"
|
|
||||||
:key="btn.name"
|
|
||||||
:color="btn.color"
|
|
||||||
@click="btn.f"
|
|
||||||
class="q-mx-sm"
|
|
||||||
rounded flat
|
|
||||||
style="background-color: rgba(255, 255, 255, 0.65) !important;"
|
|
||||||
>
|
|
||||||
{{ $t(btn.label) }}
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -76,7 +54,7 @@
|
|||||||
import { QFile } from 'quasar'
|
import { QFile } from 'quasar'
|
||||||
import imageCompression from 'browser-image-compression'
|
import imageCompression from 'browser-image-compression'
|
||||||
|
|
||||||
const modelValue = defineModel<string>()
|
const modelValue = defineModel<string | null>()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
size?: number
|
size?: number
|
||||||
@@ -92,15 +70,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const imageFile = ref(null) // file-from selector
|
const imageFile = ref(null) // file-from selector
|
||||||
const imgFileSelector= ref() as Ref<QFile> // input file DOM
|
const imgFileSelector = ref() as Ref<QFile> // input file DOM
|
||||||
const size = ref<number>(props.size ? props.size : 100)
|
const size = ref<number>(props.size ? props.size : 100)
|
||||||
const iconsize = ref<number>(props.iconsize ? props.iconsize : 75)
|
const iconsize = ref<number>(props.iconsize ? props.iconsize : 75)
|
||||||
const showDialog = ref<boolean>(false)
|
|
||||||
|
|
||||||
const menuBtns = [
|
|
||||||
{ name: 'delete', color: 'negative', label: 'image_selector__delete', f: deleteImage },
|
|
||||||
{ name: 'change', color: 'primary', label: 'image_selector__change', f: imgFileSelectorClick }
|
|
||||||
]
|
|
||||||
|
|
||||||
const sizePx = computed(() => {
|
const sizePx = computed(() => {
|
||||||
return String(size.value) + 'px'
|
return String(size.value) + 'px'
|
||||||
@@ -129,12 +101,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteImage () {
|
function deleteImage () {
|
||||||
showDialog.value = false
|
|
||||||
imageFile.value = null
|
imageFile.value = null
|
||||||
modelValue.value = ''
|
modelValue.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function imgToBase64(file: File): Promise<string | ArrayBuffer | null> {
|
function imgToBase64 (file: File): Promise<string | ArrayBuffer | null> {
|
||||||
const reader: FileReader = new FileReader()
|
const reader: FileReader = new FileReader()
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
|
|
||||||
|
|||||||
64
src/components/pnInlinePair copy.vue
Normal file
64
src/components/pnInlinePair copy.vue
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<div class="row no-wrap items-center w100">
|
||||||
|
<div
|
||||||
|
v-if="item1"
|
||||||
|
class="second-line-item flex no-wrap items-center q-mr-sm"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
v-if="item1Icon"
|
||||||
|
:name="item1Icon"
|
||||||
|
:color="item1IconColor"
|
||||||
|
class="q-mr-none"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="ellipsis"
|
||||||
|
:class="item1Class ?? '' "
|
||||||
|
>
|
||||||
|
{{ item1 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="item2"
|
||||||
|
class="second-line-item flex no-wrap items-center"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
v-if="item2Icon"
|
||||||
|
:name="item2Icon"
|
||||||
|
:color="item2IconColor"
|
||||||
|
class="q-mr-none"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="ellipsis"
|
||||||
|
:class="item2Class ?? '' "
|
||||||
|
>
|
||||||
|
{{ item2 }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
item1: string | null
|
||||||
|
item1Class?: string
|
||||||
|
item1Icon?: string
|
||||||
|
item1IconColor?: string
|
||||||
|
item2: string | null
|
||||||
|
item2Class?: string
|
||||||
|
item2Icon?: string
|
||||||
|
item2IconColor?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.second-line-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 60%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="message2" class="text-caption text-center">
|
<div v-if="message2" class="text-caption text-center" style="max-width: min(75vw, calc(var(--body-width) * 0.75));">
|
||||||
{{ $t(message2) }}
|
{{ $t(message2) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<q-resize-observer @resize="onCardResize"/>
|
<q-resize-observer @resize="onCardResize"/>
|
||||||
<div
|
<div
|
||||||
id="card-header"
|
id="card-header"
|
||||||
class="absolute-top"
|
class="absolute-top top-rounded-card overflow-hidden"
|
||||||
v-if="hasHeader"
|
v-if="hasHeader"
|
||||||
style="z-index: 100;"
|
style="z-index: 100;"
|
||||||
>
|
>
|
||||||
@@ -23,12 +23,24 @@
|
|||||||
<slot name="card-under-header"/>
|
<slot name="card-under-header"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="card-inner-header"
|
||||||
|
v-if="hasInnerHeader"
|
||||||
|
style="z-index: 100;"
|
||||||
|
>
|
||||||
|
<q-resize-observer @resize="onInnerHeaderResize"/>
|
||||||
|
<slot name="card-inner-header"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<pn-shadow-scroll
|
<pn-shadow-scroll
|
||||||
:height="cardHeight"
|
ref="shadowScrollRef"
|
||||||
|
:height="cardHeight - innerHeaderHeight"
|
||||||
:topOffset="headerHeight"
|
:topOffset="headerHeight"
|
||||||
:bottomOffset
|
:bottomOffset
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
class="overflow-hidden"
|
class="overflow-hidden"
|
||||||
|
@focusin="onFocusIn"
|
||||||
|
@editor-cursor-update="onEditorCursorUpdate"
|
||||||
>
|
>
|
||||||
<slot/>
|
<slot/>
|
||||||
</pn-shadow-scroll>
|
</pn-shadow-scroll>
|
||||||
@@ -58,7 +70,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useSlots, computed, ref, onMounted } from 'vue'
|
import { useSlots, computed, ref, onMounted, getCurrentInstance } from 'vue'
|
||||||
|
const app = getCurrentInstance()
|
||||||
|
const isMobile = app?.appContext.config.globalProperties.$isMobile
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
tabsSlotHeight?: number
|
tabsSlotHeight?: number
|
||||||
@@ -70,12 +84,14 @@
|
|||||||
|
|
||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
const hasHeader = computed(() => !!slots['card-header'])
|
const hasHeader = computed(() => !!slots['card-header'])
|
||||||
|
const hasUnderHeader = computed(() => !!slots['card-under-header'])
|
||||||
|
const hasInnerHeader = computed(() => !!slots['card-inner-header'])
|
||||||
const hasActions = computed(() => !!slots['card-actions'])
|
const hasActions = computed(() => !!slots['card-actions'])
|
||||||
const hasFAB = computed(() => !!slots['card-fab'])
|
const hasFAB = computed(() => !!slots['card-fab'])
|
||||||
const hasUnderHeader = computed(() => !!slots['card-under-header'])
|
|
||||||
|
|
||||||
const cardHeight = ref(0)
|
const cardHeight = ref(0)
|
||||||
const headerHeight = ref(16)
|
const headerHeight = ref(16)
|
||||||
|
const innerHeaderHeight = ref(0)
|
||||||
const actionsHeight = ref(0)
|
const actionsHeight = ref(0)
|
||||||
|
|
||||||
interface sizeParams {
|
interface sizeParams {
|
||||||
@@ -91,6 +107,10 @@
|
|||||||
headerHeight.value = size.height
|
headerHeight.value = size.height
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onInnerHeaderResize (size: sizeParams) {
|
||||||
|
innerHeaderHeight.value = size.height
|
||||||
|
}
|
||||||
|
|
||||||
function onActionsResize (size: sizeParams) {
|
function onActionsResize (size: sizeParams) {
|
||||||
actionsHeight.value = size.height
|
actionsHeight.value = size.height
|
||||||
}
|
}
|
||||||
@@ -111,6 +131,55 @@
|
|||||||
return Math.max(props.tabsSlotHeight, actionsHeight.value, tgInsetBottom.value)
|
return Math.max(props.tabsSlotHeight, actionsHeight.value, tgInsetBottom.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// smart scroll only for page with editable content
|
||||||
|
const shadowScrollRef = ref()
|
||||||
|
|
||||||
|
function onFocusIn(e: FocusEvent) {
|
||||||
|
if (!isMobile) return
|
||||||
|
const target = e.target as HTMLElement
|
||||||
|
|
||||||
|
if (!['INPUT', 'TEXTAREA'].includes(target.tagName)) return
|
||||||
|
const viewport = shadowScrollRef.value?.$el?.querySelector('.q-scrollarea__container') as HTMLElement
|
||||||
|
if (!viewport) return
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const field = target.closest('.q-field') as HTMLElement || target
|
||||||
|
|
||||||
|
const fieldRect = field.getBoundingClientRect()
|
||||||
|
const viewportRect = viewport.getBoundingClientRect()
|
||||||
|
|
||||||
|
const fieldBottomRelative = fieldRect.bottom - viewportRect.top
|
||||||
|
const marginBottom = 0
|
||||||
|
const availableHeight = viewport.offsetHeight - actionsHeight.value - marginBottom
|
||||||
|
|
||||||
|
if (fieldBottomRelative > availableHeight) {
|
||||||
|
const scrollDiff = fieldBottomRelative - availableHeight
|
||||||
|
viewport.scrollTo({
|
||||||
|
top: viewport.scrollTop + scrollDiff,
|
||||||
|
behavior: 'smooth'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 1000) // fix apple slow keyboard
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEditorCursorUpdate(e: Event) {
|
||||||
|
if (!isMobile) return
|
||||||
|
const coords = (e as CustomEvent).detail as { top: number, bottom: number } | null
|
||||||
|
if (!coords) return
|
||||||
|
|
||||||
|
const scrollEl = shadowScrollRef.value?.$el
|
||||||
|
const viewport = scrollEl?.querySelector('.q-scrollarea__container') as HTMLElement
|
||||||
|
if (!viewport) return
|
||||||
|
|
||||||
|
const viewportRect = viewport.getBoundingClientRect()
|
||||||
|
const cursorBottomRelativeToViewport = coords.bottom - viewportRect.top
|
||||||
|
const visibleThreshold = viewport.offsetHeight - actionsHeight.value
|
||||||
|
|
||||||
|
if (cursorBottomRelativeToViewport > visibleThreshold) {
|
||||||
|
const scrollDiff = cursorBottomRelativeToViewport - visibleThreshold
|
||||||
|
viewport.scrollBy({ top: scrollDiff, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
id="page-wrapper"
|
id="page-wrapper"
|
||||||
class="w100 relative-position column no-wrap"
|
class="w100 relative-position column no-wrap no-scroll overflow-hidden"
|
||||||
style="height: 100vh;"
|
style="height: 100vh;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id="tg-area-inset-top"
|
id="tg-area-inset-top"
|
||||||
style="height: var(--tg-safe-area-inset-top)"
|
style="height: var(--tg-safe-area-inset-top) !important; flex-shrink: 0;"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
id="tg-safe-area"
|
id="tg-safe-area"
|
||||||
class="flex justify-center items-center overflow-hidden"
|
class="flex justify-center items-center overflow-hidden"
|
||||||
style="height: var(--tg-content-safe-area-inset-top)"
|
style="height: var(--tg-content-safe-area-inset-top) !important; flex-shrink: 0;"
|
||||||
>
|
>
|
||||||
<baseLogo text-color="white" :withBackground="false" :animated="false"/>
|
<base-logo text-color="white" :withBackground="false" :animated="false"/>
|
||||||
</div>
|
</div>
|
||||||
<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="flex items-center justify-between q-ma-none q-py-none q-px-md text-white text-h6 no-scroll no-wrap w100"
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
|
|
||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
const hasTabs = computed(() => !!slots.tabs)
|
const hasTabs = computed(() => !!slots.tabs)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
236
src/components/pnTabs.vue
Normal file
236
src/components/pnTabs.vue
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
<template>
|
||||||
|
<pn-page-wrapper>
|
||||||
|
<template #title>
|
||||||
|
<slot name="title" />
|
||||||
|
</template>
|
||||||
|
<q-tab-panels
|
||||||
|
id="tab-panels"
|
||||||
|
v-model="tabSelect"
|
||||||
|
keep-alive
|
||||||
|
class="bg-transparent w100"
|
||||||
|
style="flex-grow: 1;"
|
||||||
|
>
|
||||||
|
<q-tab-panel
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.name"
|
||||||
|
:name="tab.name"
|
||||||
|
class="q-pa-none column w100"
|
||||||
|
>
|
||||||
|
<router-view :tabsSlotHeight/>
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
|
|
||||||
|
<template #tabs>
|
||||||
|
<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 text-bold"
|
||||||
|
>
|
||||||
|
<q-resize-observer @resize="val => onTabResize(val, idx)"/>
|
||||||
|
{{ $t(tab.label) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="tabs-wrapper"
|
||||||
|
class="bg-white shadow-tg rounded-fix"
|
||||||
|
:style="minWidthTab"
|
||||||
|
>
|
||||||
|
<q-tabs
|
||||||
|
v-model="tabSelect"
|
||||||
|
dense
|
||||||
|
align="center"
|
||||||
|
id="tabs"
|
||||||
|
:breakpoint="0"
|
||||||
|
class="w100 text-grey-7 "
|
||||||
|
style="z-index: 1000;"
|
||||||
|
>
|
||||||
|
<q-route-tab
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:to="{ name: tab.name }"
|
||||||
|
:name="tab.name"
|
||||||
|
:key="tab.name"
|
||||||
|
:ripple="false"
|
||||||
|
no-caps
|
||||||
|
dense
|
||||||
|
:class="tabSelect === tab.name ? 'active' : ''"
|
||||||
|
>
|
||||||
|
<div class="column items-center">
|
||||||
|
<q-icon :name="tab.icon + (tabSelect === tab.name ? '' : '-outline')" :size="config?.icon"/>
|
||||||
|
<span
|
||||||
|
v-if="config?.showLabel"
|
||||||
|
class="text-caption tab-label text-bold"
|
||||||
|
>
|
||||||
|
{{ $t(tab.label) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</q-route-tab>
|
||||||
|
<q-resize-observer @resize="size => tabsWidth = size.width" />
|
||||||
|
</q-tabs>
|
||||||
|
</div>
|
||||||
|
<div class="bg-transparent"
|
||||||
|
style="height: var(--tg-safe-area-inset-bottom)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</pn-page-wrapper>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onBeforeMount, computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
interface Tab {
|
||||||
|
name: string
|
||||||
|
label: string
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tabs: Tab[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const tabSelect = ref<string>()
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
const initialTab = props.tabs.find(t => t.name === route.name)?.name || props.tabs[0]?.name || ''
|
||||||
|
tabSelect.value = initialTab
|
||||||
|
})
|
||||||
|
|
||||||
|
const tabsSlotHeight = ref(0)
|
||||||
|
|
||||||
|
interface sizeParams {
|
||||||
|
height: number,
|
||||||
|
width: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTabsSlotResize(size: sizeParams) {
|
||||||
|
tabsSlotHeight.value = size.height
|
||||||
|
}
|
||||||
|
|
||||||
|
// hidden icon name if overflow - with resize-observer
|
||||||
|
const tabsWidth = ref(0)
|
||||||
|
const widthTabLabel = ref(new Array(props.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 - 2 * 8
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
props.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 = props.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)
|
||||||
|
|
||||||
|
const PADDING_AROUND = 3
|
||||||
|
|
||||||
|
return {
|
||||||
|
'--current-gap': `${currentGap}px`,
|
||||||
|
'--min-width-tab': '1px',
|
||||||
|
'--active-tab-width': (activeContentWidth + 16 * 2) + 'px',
|
||||||
|
'--tabs-height': tabsSlotHeight.value - 32 - PADDING_AROUND * 2 + 'px',
|
||||||
|
'--padding-around': PADDING_AROUND + 'px'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
#tabs-wrapper {
|
||||||
|
padding: var(--padding-around);
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabs :deep(.q-tabs__content) {
|
||||||
|
justify-content: space-evenly;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabs :deep(.q-tab) {
|
||||||
|
padding: 0 var(--current-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabs :deep(.active) {
|
||||||
|
color: var(--q-primary)
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabs :deep(.q-router-link--active .q-tab__indicator) {
|
||||||
|
height: calc(var(--tabs-height) - var(--tg-safe-area-inset-bottom, 0px));
|
||||||
|
border-radius: calc(var(--tabs-height) / 2);
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0.1;
|
||||||
|
background-color: var(--q-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabs :deep(.q-focus-helper) {
|
||||||
|
border-radius: calc(var(--tabs-height) / 2);
|
||||||
|
top: var(--tabs-y-padding);
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabs :deep(.q-tabs__arrow) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabs :deep(.q-tab__content) {
|
||||||
|
min-width: var(--min-width-tab);
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabs :deep(.q-tab__content .tab-label) {
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabs :deep(.q-tab--active .q-tab__indicator) {
|
||||||
|
width: var(--active-tab-width);
|
||||||
|
right: inherit;
|
||||||
|
left: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabs :deep(.q-focus-helper) {
|
||||||
|
width: calc(100% - var(--current-gap) * 2 + 32px) !important;
|
||||||
|
left: calc(var(--current-gap) - 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -78,9 +78,10 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import pnImageSelector from 'components/pnImageSelector.vue'
|
||||||
import { useInputRules } from 'composables/useInputRules'
|
import { useInputRules } from 'composables/useInputRules'
|
||||||
import type { ProjectParams } from 'types/Project'
|
|
||||||
import { removeNullKeys, getFieldValue } from 'utils/helpers'
|
import { removeNullKeys, getFieldValue } from 'utils/helpers'
|
||||||
|
import type { ProjectParams } from 'types/Project'
|
||||||
|
|
||||||
const { inputRules } = useInputRules()
|
const { inputRules } = useInputRules()
|
||||||
|
|
||||||
@@ -103,12 +104,19 @@
|
|||||||
const isFormValid = ref(false)
|
const isFormValid = ref(false)
|
||||||
|
|
||||||
function onSubmit() {
|
function onSubmit() {
|
||||||
|
const { logo, name, description, chat_info } = projectMod
|
||||||
|
let logoValue: string | null | undefined = undefined
|
||||||
|
|
||||||
|
if (typeof logo === 'string' && logo.startsWith('data:image')) logoValue = logo
|
||||||
|
if (logo === null) logoValue = null
|
||||||
|
|
||||||
const outData = {
|
const outData = {
|
||||||
name: projectMod.name,
|
name,
|
||||||
description: getFieldValue(projectMod.description, props.project?.description),
|
description: getFieldValue(description, props.project?.description),
|
||||||
chat_info: getFieldValue(projectMod.chat_info, props.project?.chat_info),
|
chat_info: getFieldValue(chat_info, props.project?.chat_info),
|
||||||
logo: projectMod.logo !== '' ? projectMod.logo : null
|
...(logoValue !== undefined && { logo: logoValue })
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('update', removeNullKeys(outData, ['logo']))
|
emit('update', removeNullKeys(outData, ['logo']))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
|
||||||
export const useInputRules = () => {
|
export const useInputRules = () => {
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -11,8 +10,10 @@ export const useInputRules = () => {
|
|||||||
require: (val: string) => !!val?.trim() || t('input_rules__require'),
|
require: (val: string) => !!val?.trim() || t('input_rules__require'),
|
||||||
email: (val: string) => /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(val) || t('input_rules__incorrect_email'),
|
email: (val: string) => /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(val) || t('input_rules__incorrect_email'),
|
||||||
password: (val: string) => val.length >= 8 || t('input_rules__password_require'),
|
password: (val: string) => val.length >= 8 || t('input_rules__password_require'),
|
||||||
middleText: (val: string) => val.length <= 60 || t('input_rules__middle_text'),
|
middleText: (val: string) => !val || val.length <= 60 || t('input_rules__middle_text'),
|
||||||
longText: (val: string) => val.length <= 250 || t('input_rules__long_text')
|
longText: (val: string) => !val || val.length <= 250 || t('input_rules__long_text'),
|
||||||
|
editorText: (val: string) => !val || val.length <= 7800 || t('input_rules__editor_text'),
|
||||||
|
editorPlainText: (val: string) => !val || val.length <= 3800 || t('input_rules__editor_text')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
109
src/css/app.scss
109
src/css/app.scss
@@ -7,14 +7,6 @@
|
|||||||
background: $brand !important;
|
background: $brand !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-brand2 {
|
|
||||||
color: $brand2 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-brand2 {
|
|
||||||
background: $brand2 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-base {
|
.bg-base {
|
||||||
background: $base-bg !important;
|
background: $base-bg !important;
|
||||||
}
|
}
|
||||||
@@ -39,6 +31,10 @@ body, html, #q-app {
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#q-app {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
@@ -50,22 +46,17 @@ body, html, #q-app {
|
|||||||
--dynamic-font-size: 16px;
|
--dynamic-font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#q-app {
|
|
||||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
|
||||||
padding-bottom: constant(safe-area-inset-bottom, 0); // Для старых iOS
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
max-width: 600px;
|
max-width: var(--body-width);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fix-fab-offset {
|
.projects-header {
|
||||||
margin-right: calc(max((100vw - var(--body-width))/2, 0px) + 18px) !important;
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-rounded-card {
|
.top-rounded-card {
|
||||||
@@ -91,15 +82,10 @@ body {
|
|||||||
.orline:after {
|
.orline:after {
|
||||||
content: "";
|
content: "";
|
||||||
flex: 1 1;
|
flex: 1 1;
|
||||||
border-bottom: 1px solid grey;
|
border-bottom: 1px solid;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'myFont';
|
|
||||||
src: url(./fonts/Inter-Regular.woff2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
opacity: 1 !important;
|
||||||
@@ -109,3 +95,82 @@ body {
|
|||||||
color: $grey-9 !important;
|
color: $grey-9 !important;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.q-field--filled .q-field__control:after {
|
||||||
|
height: 1px !important
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-field--filled.q-field--focused .q-field__control::before {
|
||||||
|
background: $grey-4 !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q-field--filled.q-field--error .q-field__control::before {
|
||||||
|
background: $grey-2;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-tg {
|
||||||
|
box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.12), 0 0 16px -4px rgba(0, 0, 0, 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'myFont';
|
||||||
|
src: url(./fonts/Inter-Regular.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pn-icon {
|
||||||
|
font-family: 'pn-icon';
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'pn-icon';
|
||||||
|
src: url(./fonts/pn.woff) format("woff")
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-file-default:before {
|
||||||
|
content: "\e900";
|
||||||
|
}
|
||||||
|
.icon-file-doc:before {
|
||||||
|
content: "\e901";
|
||||||
|
}
|
||||||
|
.icon-file-xls:before {
|
||||||
|
content: "\e902";
|
||||||
|
}
|
||||||
|
.icon-file-ppt:before {
|
||||||
|
content: "\e903";
|
||||||
|
}
|
||||||
|
.icon-file-vsd:before {
|
||||||
|
content: "\e904";
|
||||||
|
}
|
||||||
|
.icon-file-pdf:before {
|
||||||
|
content: "\e905";
|
||||||
|
}
|
||||||
|
.icon-file-archive:before {
|
||||||
|
content: "\e906";
|
||||||
|
}
|
||||||
|
.icon-file-img:before {
|
||||||
|
content: "\e907";
|
||||||
|
}
|
||||||
|
.icon-file-dwg:before {
|
||||||
|
content: "\e908";
|
||||||
|
}
|
||||||
|
.icon-file-skp:before {
|
||||||
|
content: "\e909";
|
||||||
|
}
|
||||||
|
.icon-file-ttf:before {
|
||||||
|
content: "\e90a";
|
||||||
|
}
|
||||||
|
.icon-file-txt:before {
|
||||||
|
content: "\e90b";
|
||||||
|
}
|
||||||
|
.icon-file-audio:before {
|
||||||
|
content: "\e90c";
|
||||||
|
}
|
||||||
|
.icon-file-video:before {
|
||||||
|
content: "\e90d";
|
||||||
|
}
|
||||||
|
.icon-file-code:before {
|
||||||
|
content: "\e90e";
|
||||||
|
}
|
||||||
|
|||||||
BIN
src/css/fonts/pn.eot
Normal file
BIN
src/css/fonts/pn.eot
Normal file
Binary file not shown.
25
src/css/fonts/pn.svg
Normal file
25
src/css/fonts/pn.svg
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<metadata>Generated by IcoMoon</metadata>
|
||||||
|
<defs>
|
||||||
|
<font id="pn" horiz-adv-x="1024">
|
||||||
|
<font-face units-per-em="1024" ascent="960" descent="-64" />
|
||||||
|
<missing-glyph horiz-adv-x="1024" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="512" d="" />
|
||||||
|
<glyph unicode="" glyph-name="file-default" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256z" />
|
||||||
|
<glyph unicode="" glyph-name="file-doc" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM188.75 721h112.125l73.875-387.75 90 387.75h95.25l89.625-388.5 73.5 388.5h112.125l-121.125-546h-113.25l-88.875 365.25-88.875-365.25h-113.25l-121.125 546z" />
|
||||||
|
<glyph unicode="" glyph-name="file-xls" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM128 721h129.375l102.375-188.25 102.375 188.25h129.375l-159-270.75 163.125-275.25h-130.875l-105 191.25-105-191.25h-130.875l163.125 275.25-159 270.75zM640 512h128v-64h-128v64zM832 512h64v-64h-64v64zM640 384h128v-64h-128v64zM832 384h64v-64h-64v64zM640 256h128v-64h-128v64zM832 256h64v-64h-64v64z" />
|
||||||
|
<glyph unicode="" glyph-name="file-ppt" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM129.75 721.688h213c41 0 77-7.5 108-22.5 31.25-15 55.25-36.375 72-64.125 16.75-27.5 25.125-58.875 25.125-94.125 0-53.5-18.375-95.75-55.125-126.75-36.5-30.75-87.125-46.125-151.875-46.125h-98.625v-192.375h-112.5v546zM242.25 630.563v-171.375h100.5c29.75 0 52.375 7 67.875 21 15.75 14 23.625 34 23.625 60 0 26.75-7.875 48.375-23.625 64.875s-37.5 25-65.25 25.5h-103.125zM733.813 512c0.946-0.020 1.71-0.043 2.473-0.071l-0.286 0.008v-157h160v-2.938c0-0.001 0-0.001 0-0.002 0-88.102-71.208-159.572-159.21-159.998h-0.040c-0.206-0.001-0.45-0.002-0.694-0.002-87.861 0-159.181 70.818-159.993 158.487l-0.001 0.077c-0.004 0.432-0.007 0.942-0.007 1.453 0 87.577 70.361 158.72 157.638 159.983l0.119 0.001zM804.875 496.25c32.147-15.624 57.664-40.678 73.455-71.534l0.42-0.903h-73.875v72.437z" />
|
||||||
|
<glyph unicode="" glyph-name="file-vsd" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM130.25 721h124.875l123-410.625 123.75 410.625h125.25l-190.125-546h-117.375l-189.375 546zM815.875 500.563l67.875-67.875-51.75-51.75v-156.937h-96v-32h-96v96h96v-32h64v124.687l-35.375 35.313h-124.625v32h123.25l52.625 52.562z" />
|
||||||
|
<glyph unicode="" glyph-name="file-pdf" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM478.938 808.563c5.033-0.186 9.623-1.208 13.375-3.125 15.626-7.972 26.459-25.664 31.312-51.25 6.833-36.023 2.358-66.338-22-149.875-4.758-16.32-8.625-31.461-8.625-33.625 0-8.292 18.944-46.781 35.813-72.687 28.642-43.989 67.322-83.572 111.187-113.813 11.914-8.366 14.397-9.187 22.5-9.187 5.003 0 13.104 1.007 18 1.687 46.416 8.808 134.839 12.372 162.562 6.562 28.48-5.977 46.195-18.296 51.25-35.687 2.925-10.057 2.008-20.229-2.125-23.687-2.005-1.67-3.791-0.673-9.062 5-20.394 21.952-55.131 26.519-148.75 19.687-12.554-0.92-25.929-2.238-29.75-2.75l-6.938-0.875 4.313-3.562c5.88-4.867 39.111-20.504 60.25-28.375 26.098-9.711 42.045-13.415 64.375-14.438 22.362-1.141 34.615 1.192 48.5 9.25 14.524 8.43 17.263 3.106 5.813-11.375-8.362-10.597-21.344-19.616-35.938-25-9.085-3.288-14.163-3.946-36.875-4-23.206-0.085-28.555 0.508-44.938 4.75-37.601 9.754-84.275 31.375-129.812 60.125-6.834 4.361-8.648 4.539-18.25 3.687-25.362-2.588-99.908-18.034-137.75-28.562-55.516-15.408-94.889-27.774-99-31.062-1.414-1.141-4.954-6.452-7.813-11.75-24.864-46.23-62.903-99.388-91.75-128.187-19.778-19.749-34.872-30.812-55.375-40.625v-0.313c-13.538-6.713-16.602-7.194-31.187-7.875-14.061-0.596-17.442-0.129-26 3.5-20.293 8.587-30.624 17.985-35.875 32.875-3.946 11.185-3.157 17.609 3.625 31.187 13.429 26.843 54.267 58.31 117.5 90.563 26.963 13.732 34.312 15.957 34.312 10.25 0-4.341-8.271-11.66-29-25.75-56.875-38.655-88.952-76.293-94.75-111.125-1.45-8.757-0.465-9.134 10.125-3.75 24.522 12.444 59.103 46.529 96.937 95.5 61.876 80.087 157.92 251.5 181.875 324.562l3.562 10.75-9.687 33.313c-5.316 18.345-12.079 43.648-15.062 56.187-2.985 12.546-6.205 26.008-7.125 29.875-0.918 3.862-2.836 15.283-4.25 25.437-7.955 57.012-4.335 81.411 14.75 100 11.658 11.372 30.651 18.122 45.75 17.562zM480.063 781.563c-3.266-1.251-9.423-15.54-11.875-27.5-5.113-24.947-0.997-81.847 9.125-125.813 2.022-8.791 3.971-15.937 4.312-15.937 1.031 0 16.012 48.395 22 71.062 4.625 17.487 5.624 24.503 5.75 41.25 0.138 18.596-0.278 21.102-4.875 31.187-5.501 12.079-20.071 27.573-24.438 25.75zM471.687 516.125c-0.644-0.698-3.187-7.595-5.625-15.312-14.534-46.051-44.809-124.79-65.5-170.313-6.14-13.507-11.187-25.194-11.187-25.875 0-2.487 3.984-1.318 37.562 10.813 59.798 21.605 87.644 29.965 132.687 40 16.458 3.663 29.934 7.238 29.938 7.75 0.003 0.579-7.223 8.118-16 16.688-33.633 32.844-66.928 76.91-91.625 121.187-5.005 8.978-9.603 15.743-10.25 15.062z" />
|
||||||
|
<glyph unicode="" glyph-name="file-archive" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM224 896h64v-64h64v-64h-64v-64h64v-64h-64v-64h64v-64h-64v-64h-64v64h64v64h-64v64h64v64h-64v64h64v64h-64v64zM192 384h192v-320h-192v320zM256 256v-128h64v128h-64z" />
|
||||||
|
<glyph unicode="" glyph-name="file-img" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM288 768c53.019 0 96-42.981 96-96 0 0 0 0 0 0v0c0-53.019-42.981-96-96-96 0 0 0 0 0 0v0c-53.019 0-96 42.981-96 96 0 0 0 0 0 0v0c0 53.019 42.981 96 96 96 0 0 0 0 0 0v0zM652.813 576l256-512h-768l153.562 409.625 153.625-204.812 204.813 307.187z" />
|
||||||
|
<glyph unicode="" glyph-name="file-dwg" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM320 896h64v-192h64v-64h448v-64h-448v-64h-64v-512h-64v512h-64v64h-128v64h128v64h64v192zM320 640v-64h64v64h-64z" />
|
||||||
|
<glyph unicode="" glyph-name="file-skp" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM586.938 816.625c1.815 0 13.221-8.748 36-27.625 6.673-5.532 15.558-12.853 19.75-16.25 8.147-6.597 47.682-38.903 70.625-57.75 7.654-6.283 22.133-18.209 32.187-26.437 10.052-8.222 25.014-20.512 33.313-27.312 8.297-6.796 19.127-15.658 24.062-19.688 19.461-15.884 28.569-23.724 29.062-25.125 0.273-0.761-0.146-29.456-1.062-63.687-1.521-58.979-2.655-112.105-5.125-252.75-0.645-33.687-1.389-62.35-1.813-63.687-0.383-1.331-1.973-2.75-3.438-3.188-1.451-0.383-6.92-2.636-12.187-4.812s-18.451-7.426-29.313-11.688c-10.863-4.258-28.933-11.338-40.125-15.75-11.191-4.42-26.258-10.344-33.5-13.188-7.244-2.842-24.722-9.783-38.875-15.375-14.152-5.589-33.518-13.261-43.062-17-30.058-11.78-92.454-36.4-100.562-39.688-4.278-1.74-15.053-5.935-23.938-9.375-8.889-3.441-20.187-7.941-25.125-9.937-23.729-9.632-43.385-16.938-45.375-16.938-1.365 0-9.235 8.076-20.625 21.187h0.062c-10.126 11.653-22.235 25.516-26.875 30.813-4.638 5.304-16.441 18.747-26.187 29.938-9.748 11.192-22.467 25.792-28.25 32.375-25.475 28.995-45.119 51.532-48.437 55.562-1.977 2.405-11.095 12.89-20.312 23.313-22.424 25.361-37.972 43.403-40.312 46.938-1.347 2.034-2.111 7.073-2.625 16.938-0.282 7.718-1.532 25.363-2.5 39.187-0.956 13.824-2.086 30.245-2.5 36.5-0.381 6.255-0.966 14.021-1.188 17.313-0.191 3.298-0.98 15.121-1.625 26.313-2.49 42.216-5.421 87.646-8.625 132.312-1.295 18.104-2.855 42.882-3.5 55.063-0.646 12.178-1.75 28.835-2.375 37.063-0.433 5.703-0.608 9.541-0.562 12.062 0.051 2.521 0.298 3.735 0.812 4.25 0.756 0.761 18.683 6.543 39.75 12.875 21.064 6.34 47.262 14.238 58.125 17.562 10.863 3.327 24.809 7.556 31.063 9.438 6.255 1.882 33.744 10.269 61.062 18.625s68.817 20.973 92.187 28.062c23.368 7.091 57.713 17.565 76.313 23.25 18.601 5.684 34.627 10.312 35.625 10.312zM579.313 805.438c-2.939 0.435-6.327-0.46-12.438-2.438-2.804-0.95-11.538-3.648-19.438-6.062s-21.084-6.462-29.313-9c-8.232-2.548-40.839-12.533-72.438-22.187s-64.441-19.673-73-22.312c-8.554-2.643-17.211-5.266-19.187-5.875-1.968-0.665-10.57-3.251-19.125-5.875-17.19-5.256-53.95-16.487-88-26.937-12.178-3.736-27.283-8.383-33.625-10.312-12.1-3.659-14.847-5.528-13.688-9.188 1.161-3.631 57.506-58.933 60.062-58.937 1.261-0.003 12.492 3.239 25 7.25 24.333 7.785 38.336 12.217 64 20.25 8.889 2.785 23.426 7.416 32.313 10.25 8.889 2.833 26.43 8.37 38.938 12.313 12.508 3.945 34.874 11.085 49.687 15.812 32.784 10.461 47.167 14.922 57.375 17.875l7.812 2.25 3.625-2.75c1.986-1.512 5.21-4.354 7.187-6.312 1.975-1.949 15.393-13.702 29.875-26.125 14.481-12.425 26.67-22.922 27-23.312 0.645-0.761 40.136-35.363 56.75-49.75 37.902-32.809 37.856-32.75 41.813-32.75 2.129 0 8.376 1.527 13.938 3.438 5.551 1.91 29.036 9.93 52.125 17.812 23.088 7.869 43.128 14.922 44.562 15.687 5.19 2.785 2.806 6.276-12.875 18.688-4.161 3.298-12.737 10.337-19.062 15.625-12.235 10.221-83.14 68.893-91.75 75.937-2.813 2.31-10.158 8.331-16.313 13.375-6.16 5.038-20.075 16.478-30.938 25.5-19.222 15.965-35.298 29.302-51.187 42.375-4.272 3.517-6.749 5.253-9.687 5.688zM527 567.875c-6.242-0.992-23.435-6.689-79.125-25.313-43.988-14.71-99.956-33.425-124.313-41.562-40.53-13.542-47.25-16.305-47.25-19.75 0-0.665 7.978-9.282 17.688-19.312 9.706-10.031 23.278-24.248 30.125-31.625 6.854-7.376 13.26-13.667 14.313-14 1.055-0.286 17.006 4.551 35.438 10.875 18.432 6.331 40.209 13.814 48.438 16.625 8.232 2.814 23.593 8.16 34.125 11.875 10.533 3.717 20.339 6.619 21.812 6.438 1.572-0.189 10.5-8.044 21.563-18.938 49.844-49.087 58.673-57.437 60.938-57.437 1.295 0 6.133 1.34 10.687 2.937 7.871 2.757 12.476 4.38 44.812 16.188 16.581 6.055 22.089 8.055 37.688 13.5 22.143 7.747 24.322 8.988 22.687 13.25-0.272 0.857-5.863 6.32-12.25 12.125-19.527 17.729-37.184 34.12-51.688 48-7.604 7.281-20.817 19.618-29.375 27.437-8.555 7.823-23.245 21.574-32.688 30.5-9.448 8.925-18.463 16.991-20 17.938-0.655 0.403-1.544 0.58-3.625 0.25zM475.187 318.063l-5.75-1.375c-3.173-0.766-13.050-4.033-21.938-7.312-44.493-16.44-76.602-28.132-81.687-29.813-3.117-1.044-6.593-2.846-7.688-4.062-1.842-2.034-1.76-2.401 0.25-5.5 3.051-4.677 50.353-57.225 57.687-64.062l6.125-5.625 4.5 1.625c2.519 0.947 23.164 8.686 45.875 17.25 58.911 22.225 63.765 24.060 65.5 25.5 4.154 3.46 1.78 6.731-24.5 33.625-14.322 14.658-28.783 29.592-32.187 33.187l-6.188 6.562z" />
|
||||||
|
<glyph unicode="" glyph-name="file-ttf" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM192 768h320v-64h-128v-384h-64v384h-128v64zM512 576h320v-64h-128v-384h-64v384h-128v64z" />
|
||||||
|
<glyph unicode="" glyph-name="file-txt" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM292.188 694h92.625l181.687-485.312h-106.313l-33.687 100h-175.312l-33.375-100h-106.312l180.687 485.312zM753.187 576c44.444 0 79.625-11.243 105.625-33.687 26.222-22.222 39.375-53.556 39.375-94v-156.313c0.222-34.222 4.979-60.132 14.313-77.687v-5.625h-97.313c-4.444 8.667-7.687 19.424-9.687 32.313-23.333-26-53.667-39-91-39-35.333 0-64.667 10.243-88 30.687-23.111 20.444-34.687 46.201-34.687 77.313 0 38.222 14.153 67.556 42.375 88 28.444 20.444 69.444 30.778 123 31h44.313v20.687c0 16.667-4.333 30-13 40-8.444 10-21.868 15-40.313 15-16.222 0-29.042-3.91-38.375-11.687-9.111-7.778-13.625-18.444-13.625-32h-96.375c0 20.889 6.486 40.222 19.375 58s31.069 31.687 54.625 41.687c23.556 10.222 50.042 15.313 79.375 15.313zM338.5 571.313l-60.312-181.625h121.312l-61 181.625zM765.5 372.688c-48.222 0-73.889-16.667-77-50l-0.313-5.687c0-12 4.181-21.91 12.625-29.687s20.021-11.625 34.687-11.625c14.222 0 27.313 3.090 39.313 9.313 12 6.444 20.91 15.021 26.687 25.687v62h-36z" />
|
||||||
|
<glyph unicode="" glyph-name="file-audio" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM799.937 736v-377.312c1.071-26.653-25.792-59.237-67.251-80.562l-0.374-0.188c-2.538-1.311-5.523-2.755-8.576-4.137-50.502-22.867-101.501-20.708-118.419 3.935l-0.129 0.202c-0.931 1.348-1.854 2.985-2.635 4.71-12.413 27.415 15.658 67.296 63.483 92.211l0.402 0.204c2.75 1.436 5.985 3.010 9.3 4.51 26.201 11.864 52.535 16.991 74.175 15.738l-0.037 0.002v222.5l-325.562-50.313v-306.687c1.048-26.661-25.837-59.248-67.316-80.564l-0.372-0.186c-2.515-1.298-5.474-2.728-8.498-4.098-50.518-22.874-101.533-20.707-118.435 3.959l-0.128 0.202c-0.937 1.355-1.867 3-2.652 4.734-12.409 27.406 15.639 67.269 63.436 92.186l0.404 0.205c2.72 1.418 5.92 2.973 9.197 4.457 26.266 11.893 52.666 17.017 74.336 15.728l-0.034 0.002v372.812l425.688 65.75z" />
|
||||||
|
<glyph unicode="" glyph-name="file-video" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM507.625 768c1.3 0.019 2.835 0.030 4.371 0.030 174.697 0 316.696-139.992 319.937-313.914l0.004-0.303 0.062-5.813c0-0.003 0-0.006 0-0.010 0-176.224-142.451-319.177-318.485-319.99h-0.078c-0.433-0.002-0.945-0.003-1.458-0.003-175.698 0-318.323 141.602-319.979 316.909l-0.001 0.157c-0.009 0.884-0.014 1.928-0.014 2.974 0 175.196 140.794 317.508 315.408 319.961l0.231 0.003zM324.313 675.563v-455.125l472.125 227.562-472.125 227.562z" />
|
||||||
|
<glyph unicode="" glyph-name="file-code" d="M128 960c-35.456 0-64-28.544-64-64v-896c0-35.456 28.544-64 64-64h768c35.456 0 64 28.544 64 64v704l-256 256h-576zM552.875 576h32.813l-133.75-512h-32.25l133.187 512zM375.25 485.438v-49.562l-169.75-108.688 169.75-108.75v-49.562l-215.25 141.187v34.188l215.25 141.187zM648.75 485.438l215.25-141.187v-34.188l-215.25-141.187v49.562l169.75 108.75-169.75 108.688v49.562z" />
|
||||||
|
</font></defs></svg>
|
||||||
|
After Width: | Height: | Size: 14 KiB |
BIN
src/css/fonts/pn.ttf
Normal file
BIN
src/css/fonts/pn.ttf
Normal file
Binary file not shown.
BIN
src/css/fonts/pn.woff
Normal file
BIN
src/css/fonts/pn.woff
Normal file
Binary file not shown.
@@ -13,7 +13,6 @@ $warning : #F2C037;
|
|||||||
$lightgrey : #DCDCDC;
|
$lightgrey : #DCDCDC;
|
||||||
|
|
||||||
$brand: #419FD9;
|
$brand: #419FD9;
|
||||||
$brand2: #F36D3A;
|
|
||||||
$base-bg: #517DA2;
|
$base-bg: #517DA2;
|
||||||
|
|
||||||
$tgcolor: #419FD9;
|
$tgcolor: #419FD9;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -77,14 +77,15 @@
|
|||||||
{ id: 2, name: 'account__auth_change_method', description: 'account__auth_change_method_description', icon: 'mdi-account-sync-outline', iconColor: 'primary', pathName: 'change_account_auth_method', display: !authStore.customer?.email },
|
{ id: 2, name: 'account__auth_change_method', description: 'account__auth_change_method_description', icon: 'mdi-account-sync-outline', iconColor: 'primary', pathName: 'change_account_auth_method', display: !authStore.customer?.email },
|
||||||
{ id: 3, name: 'account__auth_change_password', description: 'account__auth_change_password_description', icon: 'mdi-account-key-outline', iconColor: 'primary', pathName: 'change_account_password', display: !!authStore.customer?.email },
|
{ id: 3, name: 'account__auth_change_password', description: 'account__auth_change_password_description', icon: 'mdi-account-key-outline', iconColor: 'primary', pathName: 'change_account_password', display: !!authStore.customer?.email },
|
||||||
{ id: 4, name: 'account__auth_change_account', description: 'account__auth_change_account_description', icon: 'mdi-account-switch-outline', iconColor: 'primary', pathName: 'change_account_email', display: !!authStore.customer?.email },
|
{ id: 4, name: 'account__auth_change_account', description: 'account__auth_change_account_description', icon: 'mdi-account-switch-outline', iconColor: 'primary', pathName: 'change_account_email', display: !!authStore.customer?.email },
|
||||||
{ id: 5, name: 'account__company_data', icon: 'mdi-account-group-outline', description: 'account__company_data_description', pathName: 'your_company' },
|
{ id: 5, name: 'account__backup', icon: 'mdi-file-restore-outline', description: 'account__backup_description', pathName: 'backup' },
|
||||||
{ id: 6, name: 'account__settings', icon: 'mdi-cog-outline', description: 'account__settings_description', iconColor: 'info', pathName: 'settings' },
|
{ id: 6, name: 'account__company_data', icon: 'mdi-account-group-outline', description: 'account__company_data_description', pathName: 'your_company' },
|
||||||
{ id: 7, name: 'account__support', icon: 'mdi-lifebuoy', description: 'account__support_description', iconColor: 'info', pathName: 'support' },
|
{ id: 7, name: 'account__settings', icon: 'mdi-cog-outline', description: 'account__settings_description', iconColor: 'info', pathName: 'settings' },
|
||||||
{ id: 8, name: 'account__terms_of_use', icon: 'mdi-book-open-variant-outline', description: '', iconColor: 'grey', pathName: 'terms' },
|
{ id: 8, name: 'account__support', icon: 'mdi-lifebuoy', description: 'account__support_description', iconColor: 'info', pathName: 'support' },
|
||||||
{ id: 9, name: 'account__privacy', icon: 'mdi-lock-outline', description: '', iconColor: 'grey', pathName: 'privacy' },
|
{ id: 9, name: 'account__terms_of_use', icon: 'mdi-book-open-variant-outline', description: '', iconColor: 'grey', pathName: 'terms' },
|
||||||
{ id: 10, name: 'account__consent_pd', icon: 'mdi-account-lock-outline', description: '', iconColor: 'grey', pathName: 'consent' },
|
{ id: 10, name: 'account__privacy', icon: 'mdi-lock-outline', description: '', iconColor: 'grey', pathName: 'privacy' },
|
||||||
{ id: 11, name: 'account__tarrifs_info', icon: 'mdi-star-circle-outline', description: '', iconColor: 'grey', pathName: 'tarrifs_info' },
|
{ id: 11, name: 'account__consent_pd', icon: 'mdi-account-lock-outline', description: '', iconColor: 'grey', pathName: 'consent' },
|
||||||
{ id: 12, name: 'account__stop_using', icon: 'mdi-close-box-outline', description: 'account__stop_using_description', iconColor: 'negative', pathName: 'stop_using' }
|
{ id: 12, name: 'account__tarrifs_info', icon: 'mdi-star-circle-outline', description: '', iconColor: 'grey', pathName: 'tarrifs_info' },
|
||||||
|
{ id: 14, 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(() => (
|
||||||
@@ -110,7 +111,6 @@
|
|||||||
await authStore.termsRevoked()
|
await authStore.termsRevoked()
|
||||||
tg.close()
|
tg.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
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'
|
import { useCompaniesStore } from 'stores/companies'
|
||||||
import { parseIntString } from 'src/utils/helpers'
|
import { parseIntString } from 'utils/helpers'
|
||||||
import { useUserSection } from 'composables/useUserSection'
|
import { useUserSection } from 'composables/useUserSection'
|
||||||
import pnChatTypeItem from 'components/pnChatTypeItem.vue'
|
import pnChatTypeItem from 'components/pnChatTypeItem.vue'
|
||||||
import pnUserListItemBody from 'components/pnUserListItemBody.vue'
|
import pnUserListItemBody from 'components/pnUserListItemBody.vue'
|
||||||
|
|||||||
@@ -1,203 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-wrapper>
|
<pn-tabs :tabs>
|
||||||
<template #title>
|
<template #title>
|
||||||
<project-page-header/>
|
<project-page-header/>
|
||||||
</template>
|
</template>
|
||||||
<q-tab-panels
|
</pn-tabs>
|
||||||
id="tab-panels"
|
|
||||||
v-model="tabSelect"
|
|
||||||
keep-alive
|
|
||||||
class="bg-transparent w100"
|
|
||||||
style="flex-grow: 1;"
|
|
||||||
>
|
|
||||||
<q-tab-panel
|
|
||||||
v-for="tab in tabs"
|
|
||||||
:key="tab.name"
|
|
||||||
:name="tab.name"
|
|
||||||
class="q-pa-none column w100"
|
|
||||||
>
|
|
||||||
<router-view :tabsSlotHeight/>
|
|
||||||
</q-tab-panel>
|
|
||||||
</q-tab-panels>
|
|
||||||
|
|
||||||
<template #tabs>
|
|
||||||
<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
|
|
||||||
v-model="tabSelect"
|
|
||||||
dense
|
|
||||||
align="center"
|
|
||||||
id="tabs"
|
|
||||||
:breakpoint="0"
|
|
||||||
class="w100 text-grey-7 bg-grey-2 shadow-1 rounded-fix"
|
|
||||||
style="z-index: 1000;"
|
|
||||||
:style="minWidthTab"
|
|
||||||
>
|
|
||||||
<q-route-tab
|
|
||||||
v-for="tab in tabs"
|
|
||||||
:to="{ name: tab.name }"
|
|
||||||
:name="tab.name"
|
|
||||||
:key="tab.name"
|
|
||||||
:ripple="false"
|
|
||||||
no-caps
|
|
||||||
dense
|
|
||||||
:class="(tabSelect === tab.name ? 'active' : '')"
|
|
||||||
>
|
|
||||||
<div class="column items-center">
|
|
||||||
<q-icon :name="tab.icon" :size="config?.icon"/>
|
|
||||||
<span
|
|
||||||
v-if="config?.showLabel"
|
|
||||||
class="text-caption"
|
|
||||||
>
|
|
||||||
{{ $t(tab.label) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</q-route-tab>
|
|
||||||
<q-resize-observer @resize="size => tabsWidth = size.width" />
|
|
||||||
</q-tabs>
|
|
||||||
<div class="bg-transparent"
|
|
||||||
style="height: var(--tg-safe-area-inset-bottom)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
</pn-page-wrapper>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onBeforeMount, computed } from 'vue'
|
|
||||||
import projectPageHeader from 'pages/project-page/ProjectPageHeader.vue'
|
import projectPageHeader from 'pages/project-page/ProjectPageHeader.vue'
|
||||||
import { useRoute } from 'vue-router'
|
import pnTabs from 'components/pnTabs.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ name: 'chats', label: 'project__chats', icon: 'mdi-chat-outline' },
|
{ name: 'chats', label: 'project__chats', icon: 'mdi-chat' },
|
||||||
{ name: 'users', label: 'project__users', icon: 'mdi-account-outline' },
|
{ name: 'users', label: 'project__users', icon: 'mdi-account-circle' },
|
||||||
{ name: 'companies', label: 'project__companies', icon: 'mdi-account-group-outline' }
|
{ name: 'companies', label: 'project__companies', icon: 'mdi-account-group' }
|
||||||
] as const
|
]
|
||||||
|
|
||||||
const tabSelect = ref<string>()
|
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
const initialTab = tabs.find(t => t.name === route.name)?.name || tabs[0]?.name || ''
|
|
||||||
tabSelect.value = initialTab
|
|
||||||
})
|
|
||||||
|
|
||||||
const tabsSlotHeight = ref(0)
|
|
||||||
|
|
||||||
interface sizeParams {
|
|
||||||
height: number,
|
|
||||||
width: number
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTabsSlotResize(size: sizeParams) {
|
|
||||||
tabsSlotHeight.value = size.height
|
|
||||||
}
|
|
||||||
|
|
||||||
// hidden icon name if overflow - with resize-observer
|
|
||||||
const tabsWidth = ref(0)
|
|
||||||
const widthTabLabel = ref(new Array(tabs.length).fill(0))
|
|
||||||
|
|
||||||
function onTabResize (size: sizeParams, idx: number) {
|
|
||||||
if (size.width) widthTabLabel.value[idx] = size.width
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = computed(() => {
|
|
||||||
const ICON_SM = 24
|
|
||||||
const ICON_MD = 32
|
|
||||||
|
|
||||||
const availableWidth = tabsWidth.value
|
|
||||||
|
|
||||||
const variants = [
|
|
||||||
{ icon: 'sm', gap: 16, showLabel: true },
|
|
||||||
{ icon: 'sm', gap: 8, showLabel: true },
|
|
||||||
{ icon: 'md', gap: 16, showLabel: false },
|
|
||||||
{ icon: 'md', gap: 8, showLabel: false },
|
|
||||||
{ icon: 'sm', gap: 16, showLabel: false },
|
|
||||||
{ icon: 'sm', gap: 8, showLabel: false }
|
|
||||||
] as const
|
|
||||||
|
|
||||||
for (const v of variants) {
|
|
||||||
const iconW = v.icon === 'sm' ? ICON_SM : ICON_MD
|
|
||||||
let totalNeeded = 0
|
|
||||||
|
|
||||||
tabs.forEach((_, idx) => {
|
|
||||||
const labelW = v.showLabel ? widthTabLabel.value[idx] : 0
|
|
||||||
const contentW = Math.max(iconW, labelW)
|
|
||||||
totalNeeded += contentW + (v.gap * 2)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (totalNeeded <= availableWidth || v === variants[variants.length - 1]) {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return variants[variants.length - 1]
|
|
||||||
})
|
|
||||||
|
|
||||||
const minWidthTab = computed(() => {
|
|
||||||
const activeIdx = tabs.findIndex(el => el.name === tabSelect.value)
|
|
||||||
const currentGap = config.value?.gap ?? 8
|
|
||||||
|
|
||||||
const labelW = config.value?.showLabel ? widthTabLabel.value[activeIdx] : 0
|
|
||||||
const iconW = config.value?.icon === 'sm' ? 24 : 32
|
|
||||||
const activeContentWidth = Math.max(labelW, iconW)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'--current-gap': `${currentGap}px`,
|
|
||||||
'--min-width-tab': '1px',
|
|
||||||
'--active-tab-width': (activeContentWidth + 16 * 2) + 'px',
|
|
||||||
'--tabs-height': tabsSlotHeight.value - 32 + 'px'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
#tabs :deep(.q-tab) {
|
|
||||||
padding: 0 var(--current-gap);
|
|
||||||
}
|
|
||||||
|
|
||||||
#tabs :deep(.active) {
|
|
||||||
color: var(--q-primary)
|
|
||||||
}
|
|
||||||
|
|
||||||
#tabs :deep(.q-router-link--active .q-tab__indicator) {
|
|
||||||
height: calc(var(--tabs-height) - var(--tg-safe-area-inset-bottom, 0px));
|
|
||||||
border-radius: calc(var(--tabs-height) / 2);
|
|
||||||
overflow: hidden;
|
|
||||||
opacity: 0.2;
|
|
||||||
background-color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tabs :deep(.q-focus-helper) {
|
|
||||||
border-radius: calc(var(--tabs-height) / 2);
|
|
||||||
border: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tabs :deep(.q-tabs__arrow) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tabs :deep(.q-tab__content) {
|
|
||||||
min-width: var(--min-width-tab);
|
|
||||||
}
|
|
||||||
|
|
||||||
#tabs :deep(.q-tab--active .q-tab__indicator) {
|
|
||||||
width: var(--active-tab-width);
|
|
||||||
right: inherit;
|
|
||||||
left: inherit;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -93,14 +93,13 @@
|
|||||||
<div
|
<div
|
||||||
v-if="archiveProjects.length !== 0"
|
v-if="archiveProjects.length !== 0"
|
||||||
class="flex column items-center w100"
|
class="flex column items-center w100"
|
||||||
:class="showArchive ? 'bg-grey-12' : ''"
|
|
||||||
>
|
>
|
||||||
<q-btn
|
<q-btn
|
||||||
class="w100 rotate-icon-btn"
|
class="w100 rotate-icon-btn"
|
||||||
color="grey"
|
color="grey"
|
||||||
flat
|
flat
|
||||||
no-caps
|
no-caps
|
||||||
@click="showArchive=!showArchive"
|
@click="showArchive = !showArchive"
|
||||||
icon-right="arrow_drop_down"
|
icon-right="arrow_drop_down"
|
||||||
:class="{ 'rotate-icon': showArchive }"
|
:class="{ 'rotate-icon': showArchive }"
|
||||||
>
|
>
|
||||||
@@ -270,9 +269,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!projectsStore.isInit) {
|
if (!projectsStore.isInit) await projectsStore.init()
|
||||||
await projectsStore.init()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(showDialogRestoreArchive, (newD :boolean) => {
|
watch(showDialogRestoreArchive, (newD :boolean) => {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import baseLogo from 'components/BaseLogo.vue'
|
import baseLogo from 'components/BaseLogo.vue'
|
||||||
import meshBackground from 'components/meshBackground.vue'
|
import meshBackground from 'components/meshBackground.vue'
|
||||||
import { BOT_NAME } from 'src/utils/constants'
|
import { BOT_NAME } from 'utils/constants'
|
||||||
|
|
||||||
function openWebsite () {
|
function openWebsite () {
|
||||||
window.open('https://t.me/'+ BOT_NAME)
|
window.open('https://t.me/'+ BOT_NAME)
|
||||||
|
|||||||
473
src/pages/account/BackupPage.vue
Normal file
473
src/pages/account/BackupPage.vue
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
<template>
|
||||||
|
<pn-page-template v-bind="$attrs">
|
||||||
|
<template #title>
|
||||||
|
{{ $t('backup__title') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #card-header>
|
||||||
|
<q-select
|
||||||
|
v-if="projects.length !== 0"
|
||||||
|
v-model="selectedProject"
|
||||||
|
:options="filteredProjects"
|
||||||
|
option-value="id"
|
||||||
|
option-label="name"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
use-input
|
||||||
|
hide-selected
|
||||||
|
fill-input
|
||||||
|
input-debounce="300"
|
||||||
|
class="w100 q-pa-md"
|
||||||
|
:label="$t('backup__project')"
|
||||||
|
@filter="filterProjectFn"
|
||||||
|
filled dense
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="mdi-briefcase-outline" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #selected-item="scope">
|
||||||
|
<span :class="scope.opt?.is_archived ? 'text-grey' : ''">
|
||||||
|
{{ scope.opt?.name }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #option="scope">
|
||||||
|
<q-item v-bind="scope.itemProps">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label :class="scope.opt.is_archived ? 'text-grey' : ''">
|
||||||
|
{{ scope.opt.name }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
|
<pn-action-bar
|
||||||
|
v-if="files.length !== 0"
|
||||||
|
v-model="search"
|
||||||
|
placeholder="backup__files_search"
|
||||||
|
:calendar-active="showCalendar"
|
||||||
|
:filter-active="showFiltersDialog"
|
||||||
|
:calendar-badge="!!datesRange"
|
||||||
|
:filter-badge="!checkFiltersSelect"
|
||||||
|
@toggle-calendar="showCalendar = !showCalendar"
|
||||||
|
@open-filters="showFiltersDialog = true"
|
||||||
|
style="padding-top: 0 !important;"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #card-under-header>
|
||||||
|
<transition
|
||||||
|
enter-active-class="animated zoomIn"
|
||||||
|
leave-active-class="animated zoomOut"
|
||||||
|
>
|
||||||
|
<div v-if="showCalendar" class="q-mx-md shadow-1 rounded-card">
|
||||||
|
<q-date
|
||||||
|
class="w100 fix-calendar rounded-card bg-grey-11"
|
||||||
|
first-day-of-week="1"
|
||||||
|
v-model="datesRange"
|
||||||
|
range
|
||||||
|
flat
|
||||||
|
:events="filesDates"
|
||||||
|
event-color="brand"
|
||||||
|
today-btn
|
||||||
|
minimal
|
||||||
|
dense
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<q-list class="w100">
|
||||||
|
<q-item
|
||||||
|
v-for="item in displayFiles"
|
||||||
|
:key="item.id"
|
||||||
|
@click="downloadFile(item.id)"
|
||||||
|
v-ripple
|
||||||
|
clickable
|
||||||
|
>
|
||||||
|
<q-item-section avatar class="items-center" >
|
||||||
|
<q-icon
|
||||||
|
:name="fileIcon(item.filename).icon"
|
||||||
|
:style="{ color: fileIcon(item.filename).color}"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section
|
||||||
|
:class="item.parent.type === 'chat' && item.parent.is_deleted ? 'text-strike' : ''"
|
||||||
|
>
|
||||||
|
<q-item-label lines="1" class="text-bold">
|
||||||
|
<pn-cutter-long-text
|
||||||
|
:text = item.filename
|
||||||
|
:tailLength="10"
|
||||||
|
/>
|
||||||
|
</q-item-label>
|
||||||
|
|
||||||
|
<q-item-label caption lines="1">
|
||||||
|
<pn-inline-pair
|
||||||
|
:item1="usersStore.userNameById(item.published_by)"
|
||||||
|
:item1Class="usersStore.userById(item.published_by)?.is_blocked ? 'text-strike' : ''"
|
||||||
|
:item2="item.parent.name"
|
||||||
|
:item2Icon="
|
||||||
|
item.parent.type === 'chat'
|
||||||
|
? 'mdi-chat-outline'
|
||||||
|
: item.parent.type === 'task'
|
||||||
|
? 'mdi-clipboard-outline'
|
||||||
|
: 'mdi-calendar-month-outline'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side class="text-caption">
|
||||||
|
<q-item-label >
|
||||||
|
{{ date.formatDate(item.published * 1000, 'D MMM') }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label >
|
||||||
|
{{ fileSize(item.size, $t) }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
<div
|
||||||
|
v-if="files.length === 0"
|
||||||
|
class="column items-center text-grey w100"
|
||||||
|
>
|
||||||
|
<div class="text-h6 w60 text-center">
|
||||||
|
{{$t('backup__empty_description')}}
|
||||||
|
</div>
|
||||||
|
<q-icon size="120px" name="mdi-clippy" class="q-ma-sm"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pn-bottom-sheet-dialog
|
||||||
|
title="files__filters"
|
||||||
|
v-model="showFiltersDialog"
|
||||||
|
>
|
||||||
|
<template #btnSlot>
|
||||||
|
<div>
|
||||||
|
<q-btn
|
||||||
|
v-if="!checkFiltersSelect"
|
||||||
|
@click="resetFilters"
|
||||||
|
flat
|
||||||
|
no-caps
|
||||||
|
dense
|
||||||
|
color="grey-6"
|
||||||
|
>
|
||||||
|
{{ $t('files__filters_reset')}}
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<q-btn
|
||||||
|
rounded
|
||||||
|
class="w100"
|
||||||
|
color="primary"
|
||||||
|
@click="showFiltersDialog = false"
|
||||||
|
>
|
||||||
|
{{$t('files__filters_continue')}}
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="q-px-md">
|
||||||
|
<div class="q-pl-sm text-bold text-caption">
|
||||||
|
{{ $t('files__filters_extension') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex row">
|
||||||
|
<div
|
||||||
|
v-for="(item,idx) in fileExtExample"
|
||||||
|
:key="idx"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
:name="fileIcon(item).icon"
|
||||||
|
:style="{ color: fileIcon(item).color }"
|
||||||
|
@click = "updateFileExtFilters(fileIcon(item).type)"
|
||||||
|
:class="filters.fileExt.includes(fileIcon(item).type) ? 'active' : 'inactive'"
|
||||||
|
size="md"
|
||||||
|
class="q-pa-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-pl-sm q-mt-md text-bold text-caption">
|
||||||
|
{{ $t('files__filters_source') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex column">
|
||||||
|
<div
|
||||||
|
v-for="(item,idx) in fileSourceOptions"
|
||||||
|
:key="idx"
|
||||||
|
>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="filters.fileSource"
|
||||||
|
:val="item.value"
|
||||||
|
>
|
||||||
|
{{ $t(item.label) }}
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-pl-sm q-mt-md text-bold text-caption">
|
||||||
|
{{ $t('files__filters_by') }}
|
||||||
|
</div>
|
||||||
|
<q-select
|
||||||
|
v-model="filters.fileBy"
|
||||||
|
:options="users"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
class="w100 q-pt-sm"
|
||||||
|
option-value="id"
|
||||||
|
option-label="displayName"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
use-chips
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
<template #option="scope">
|
||||||
|
<q-item v-bind="scope.itemProps">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<pn-auto-avatar
|
||||||
|
:img="scope.opt.photo"
|
||||||
|
:name="scope.opt.displayName"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ scope.opt.displayName }}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
|
<div class="q-pl-sm q-mt-md text-bold text-caption">
|
||||||
|
{{ $t('files__filters_size') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex column">
|
||||||
|
<div
|
||||||
|
v-for="(item,idx) in fileSizeOptions"
|
||||||
|
:key="idx"
|
||||||
|
>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="filters.fileSize"
|
||||||
|
:val="item.value"
|
||||||
|
:label = "$t(item.label)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</pn-bottom-sheet-dialog>
|
||||||
|
</pn-page-template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, inject, watch } from 'vue'
|
||||||
|
import { useProjectsStore } from 'stores/projects'
|
||||||
|
import { useFilesStore } from 'stores/files'
|
||||||
|
import { useUsersStore } from 'stores/users'
|
||||||
|
import pnActionBar from 'components/pnActionBar.vue'
|
||||||
|
import pnCutterLongText from 'components/pnCutterLongText.vue'
|
||||||
|
import pnInlinePair from 'components/pnInlinePair.vue'
|
||||||
|
import { date } from 'quasar'
|
||||||
|
import { fileIcon, fileSize } from 'utils/files-functions'
|
||||||
|
import type { FileLink } from 'types/FileLink'
|
||||||
|
import type { WebApp } from '@twa-dev/types'
|
||||||
|
|
||||||
|
const tg = inject('tg') as WebApp
|
||||||
|
|
||||||
|
const projectsStore = useProjectsStore()
|
||||||
|
const filesStore = useFilesStore()
|
||||||
|
const usersStore = useUsersStore()
|
||||||
|
|
||||||
|
const files = ref<FileLink[]>([])
|
||||||
|
const projects = computed(() => projectsStore.projects)
|
||||||
|
const selectedProject = ref<number | null>(null)
|
||||||
|
const searchProjectQuery = ref('')
|
||||||
|
const search = ref('')
|
||||||
|
const showCalendar = ref<boolean>(false)
|
||||||
|
const datesRange = ref<null | { from: string, to: string }>(null)
|
||||||
|
const showFiltersDialog = ref(false)
|
||||||
|
|
||||||
|
const displayFiles = computed(() => {
|
||||||
|
|
||||||
|
return files.value
|
||||||
|
.filter(bySearch)
|
||||||
|
.filter(byExt)
|
||||||
|
.filter(bySource)
|
||||||
|
.filter(bySize)
|
||||||
|
.filter(byFrom)
|
||||||
|
.filter(checkDateInterval)
|
||||||
|
.sort((f1: FileLink, f2: FileLink) => f2.published - f1.published)
|
||||||
|
|
||||||
|
function bySearch (el: FileLink) {
|
||||||
|
if (!search.value || !(search.value && search.value.trim())) return true
|
||||||
|
const searchValue = search.value.trim().toLowerCase()
|
||||||
|
return el.filename.toLowerCase().includes(searchValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkDateInterval (el: FileLink) {
|
||||||
|
if (!datesRange.value) return true
|
||||||
|
const start = (typeof datesRange.value !== 'string') ? datesRange.value.from : datesRange.value
|
||||||
|
const end = (typeof datesRange.value !== 'string') ? datesRange.value.to : datesRange.value
|
||||||
|
const from = date.extractDate(start, 'YYYY/MM/DD').getTime()
|
||||||
|
const to = date.extractDate(end, 'YYYY/MM/DD').getTime() + (24 * 60 * 60 * 1000 - 1)
|
||||||
|
return (from < el.published * 1000) && ( to >= el.published * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function byExt (el: FileLink) {
|
||||||
|
if (filters.value.fileExt.length === 0) return true
|
||||||
|
const type = fileIcon(el.filename).type
|
||||||
|
return filters.value.fileExt.includes(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
function bySource (el: FileLink) {
|
||||||
|
if (filters.value.fileSource.length === 0) return true
|
||||||
|
return filters.value.fileSource.includes(el.parent.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
function bySize (el: FileLink) {
|
||||||
|
if (filters.value.fileSize.length === 0) return true
|
||||||
|
|
||||||
|
const fs = el.size
|
||||||
|
const sortedFilters = [...filters.value.fileSize].sort((a, b) => a - b)
|
||||||
|
|
||||||
|
const ranges = fileSizeOptions.map((option, index) => ({
|
||||||
|
min: index === 0 ? 0 : fileSizeOptions[index - 1]!.value,
|
||||||
|
max: option.value,
|
||||||
|
value: option.value
|
||||||
|
}))
|
||||||
|
|
||||||
|
return ranges.some(range =>
|
||||||
|
sortedFilters.includes(range.value) &&
|
||||||
|
(fs > range.min &&
|
||||||
|
(range.max === Infinity ? true : fs <= range.max))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function byFrom (el: FileLink) {
|
||||||
|
if (filters.value.fileBy.length === 0) return true
|
||||||
|
return filters.value.fileBy.includes(el.published_by)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
interface Filters {
|
||||||
|
fileSource: Array<'chat' | 'task' | 'meeting'>
|
||||||
|
fileSize: number[]
|
||||||
|
fileBy: number[]
|
||||||
|
fileExt: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultFilters = {
|
||||||
|
fileSource: [],
|
||||||
|
fileSize: [],
|
||||||
|
fileBy: [],
|
||||||
|
fileExt: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters = ref<Filters>({ ...defaultFilters })
|
||||||
|
|
||||||
|
const checkFiltersSelect = computed(() => (
|
||||||
|
Object.values(filters.value).every(el => el.length === 0)
|
||||||
|
))
|
||||||
|
|
||||||
|
function resetFilters() {
|
||||||
|
(Object.keys(filters.value) as (keyof Filters)[]).forEach(key => filters.value[key] = [])
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileExtExample = [
|
||||||
|
'1.common', '2.doc', '3.xls', '4.vsd', '5.ppt', '6.pdf', '7.png', '8.mp3', '9.mp4', '10.js', '11.txt', '12.zip', '13.skp', '14.dwg', '15.ttf'
|
||||||
|
]
|
||||||
|
|
||||||
|
function updateFileExtFilters (select: string) {
|
||||||
|
if (filters.value.fileExt.includes(select)) {
|
||||||
|
const idx = filters.value.fileExt.indexOf(select)
|
||||||
|
filters.value.fileExt.splice(idx, 1)
|
||||||
|
} else filters.value.fileExt.push(select)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileSourceOptions = [
|
||||||
|
{ id: 1, icon: 'mdi-chat-outline', value: 'chat', label: 'files__filters_source_chats' },
|
||||||
|
{ id: 2, icon: 'mdi-clipboard-outline', value: 'task', label: 'files__filters_source_tasks' },
|
||||||
|
{ id: 3, icon: 'mdi-calendar-month-outline', value: 'meeting', label: 'files__filters_source_meetings' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const fileSizeOptions = [
|
||||||
|
{ id: 1, value: 5242880, label: 'files__filters_size_small' }, // 5MB
|
||||||
|
{ id: 2, value: 26214400, label: 'files__filters_size_middle' }, // 25MB
|
||||||
|
{ id: 3, value: 104857600, label: 'files__filters_size_big' }, // 100MB
|
||||||
|
{ id: 4, value: Infinity, label: 'files__filters_size_very_big' } // more 100 MB
|
||||||
|
]
|
||||||
|
|
||||||
|
const users = computed(() => usersStore.users)
|
||||||
|
|
||||||
|
const filesDates = computed(() => displayFiles.value.map(el => date.formatDate(el.published * 1000, 'YYYY/MM/DD')))
|
||||||
|
|
||||||
|
async function downloadFile (fileId: number) {
|
||||||
|
try {
|
||||||
|
const fileParams = await filesStore.downloadParams(fileId)
|
||||||
|
if (!fileParams) throw new Error('Download failed')
|
||||||
|
tg.downloadFile(fileParams)
|
||||||
|
} catch (error) {
|
||||||
|
alert(error)
|
||||||
|
console.error('Download error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredProjects = computed(() => {
|
||||||
|
const needle = searchProjectQuery.value.toLowerCase()
|
||||||
|
if (needle === '') {
|
||||||
|
return projectsStore.projects
|
||||||
|
}
|
||||||
|
return projectsStore.projects.filter(
|
||||||
|
v => v.name.toLowerCase().includes(needle)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function filterProjectFn (val: string, update: (fn: () => void) => void) {
|
||||||
|
update(() => {
|
||||||
|
searchProjectQuery.value = val
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selectedProject, async () => {
|
||||||
|
if (selectedProject.value) {
|
||||||
|
projectsStore.setCurrentProjectId(selectedProject.value)
|
||||||
|
files.value = await filesStore.getFilesList(selectedProject.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const lastProjectId = computed(() => projectsStore.lastProjectId)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!projectsStore.isInit) await projectsStore.init()
|
||||||
|
selectedProject.value = lastProjectId.value ?? projects.value[0]?.id ?? null
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.second-line-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fix-card-width {
|
||||||
|
width: var(--body-width) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
.inactive {
|
||||||
|
opacity: 0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
.fix-calendar :deep(.q-date__view) {
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fix-calendar :deep(.q-date__calendar-days-container) {
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-template>
|
<pn-page-template>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{ $t('settings__title') }}
|
{{ $t('settings__title') }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<q-list>
|
<q-list>
|
||||||
<q-item-label header>{{ $t('settings__software_title') }}</q-item-label>
|
<q-item-label header>{{ $t('settings__software_title') }}</q-item-label>
|
||||||
|
|
||||||
@@ -37,6 +36,29 @@
|
|||||||
/>
|
/>
|
||||||
</pn-item-btm-dialog>
|
</pn-item-btm-dialog>
|
||||||
|
|
||||||
|
<pn-item-btm-dialog
|
||||||
|
title="settings__display_mode"
|
||||||
|
icon="mdi-overscan"
|
||||||
|
iconColor="primary"
|
||||||
|
>
|
||||||
|
<template #value>
|
||||||
|
{{ $t(displayModeLabel) }}
|
||||||
|
</template>
|
||||||
|
<pn-list-selector
|
||||||
|
v-model="displayMode"
|
||||||
|
:options="displayModeOptions"
|
||||||
|
/>
|
||||||
|
<q-banner
|
||||||
|
v-if="$isAndroid"
|
||||||
|
class="bg-negative text-white q-mt-lg"
|
||||||
|
>
|
||||||
|
<template #avatar>
|
||||||
|
<q-icon name="mdi-alert-circle-outline"/>
|
||||||
|
</template>
|
||||||
|
{{ $t('settings__display_mode_android_warning') }}
|
||||||
|
</q-banner>
|
||||||
|
</pn-item-btm-dialog>
|
||||||
|
|
||||||
<template v-if="authStore.isAuth">
|
<template v-if="authStore.isAuth">
|
||||||
<q-item-label
|
<q-item-label
|
||||||
class="q-mt-md"
|
class="q-mt-md"
|
||||||
@@ -115,6 +137,19 @@
|
|||||||
await settingsStore.updateSettings({ fontSize: newValue })
|
await settingsStore.updateSettings({ fontSize: newValue })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const displayMode = ref<'full' | 'window'>('window')
|
||||||
|
const displayModeOptions = [
|
||||||
|
{ value: 'full', label: 'settings__display_mode_full' },
|
||||||
|
{ value: 'window', label: 'settings__display_mode_window' }
|
||||||
|
]
|
||||||
|
const displayModeLabel = computed(() =>
|
||||||
|
displayModeOptions.find(el => el.value === displayMode.value)?.label ?? ''
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(displayMode, async (newValue) => {
|
||||||
|
await settingsStore.updateSettings({ displayMode: newValue })
|
||||||
|
})
|
||||||
|
|
||||||
const timeZoneBot = ref<{ tz: string, offset: number,offsetString: string }>()
|
const timeZoneBot = ref<{ tz: string, offset: number,offsetString: string }>()
|
||||||
|
|
||||||
watch(timeZoneBot, async (newValue) => {
|
watch(timeZoneBot, async (newValue) => {
|
||||||
@@ -123,9 +158,10 @@
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
locale.value = settingsStore.settings.locale
|
locale.value = settingsStore.settings.locale
|
||||||
localeBot.value = settingsStore.settings.localeBot
|
|
||||||
fontSize.value = settingsStore.settings.fontSize
|
fontSize.value = settingsStore.settings.fontSize
|
||||||
|
displayMode.value = settingsStore.settings.displayMode
|
||||||
timeZoneBot.value = settingsStore.settings.timeZoneBot
|
timeZoneBot.value = settingsStore.settings.timeZoneBot
|
||||||
|
localeBot.value = settingsStore.settings.localeBot
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<doc-block
|
<doc-block
|
||||||
title="subscription_guide__title"
|
title="account__tarrifs_info"
|
||||||
document-name="Subscription_guide"
|
document-name="Provision_on_subscription_tariffs"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,20 @@
|
|||||||
<pn-page-card v-bind="$attrs">
|
<pn-page-card v-bind="$attrs">
|
||||||
<template #card-header>
|
<template #card-header>
|
||||||
<pn-action-bar
|
<pn-action-bar
|
||||||
v-if="chats.length !== 0"
|
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"
|
||||||
:show-filter-btn="false"
|
:show-filter-btn="false"
|
||||||
/>
|
>
|
||||||
|
<q-btn
|
||||||
|
@click="showDialogHelp = !showDialogHelp"
|
||||||
|
round
|
||||||
|
color="grey-2"
|
||||||
|
class="text-grey q-ml-sm"
|
||||||
|
icon="mdi-help"
|
||||||
|
/>
|
||||||
|
</pn-action-bar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<q-list v-if="chats.length !== 0">
|
<q-list v-if="chats.length !== 0">
|
||||||
@@ -77,7 +85,6 @@
|
|||||||
<div
|
<div
|
||||||
v-if="displayObsoleteChats.length !== 0"
|
v-if="displayObsoleteChats.length !== 0"
|
||||||
class="flex column items-center w100"
|
class="flex column items-center w100"
|
||||||
:class="showObsoleteChats ? 'bg-grey-12' : ''"
|
|
||||||
>
|
>
|
||||||
<q-btn
|
<q-btn
|
||||||
class="w100 rotate-icon-btn"
|
class="w100 rotate-icon-btn"
|
||||||
@@ -158,26 +165,6 @@
|
|||||||
<q-linear-progress indeterminate />
|
<q-linear-progress indeterminate />
|
||||||
</div>
|
</div>
|
||||||
<template #card-fab>
|
<template #card-fab>
|
||||||
<div v-if="chats.length !== 0 || obsoleteChats.length !== 0">
|
|
||||||
<transition
|
|
||||||
appear
|
|
||||||
enter-active-class="animated zoomIn"
|
|
||||||
>
|
|
||||||
<pn-glass-btn
|
|
||||||
type="fab"
|
|
||||||
color="grey-2"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
@click="showDialogHelp = !showDialogHelp"
|
|
||||||
fab
|
|
||||||
icon="mdi-help"
|
|
||||||
color="transparent"
|
|
||||||
class="text-grey"
|
|
||||||
/>
|
|
||||||
</pn-glass-btn>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<transition
|
<transition
|
||||||
appear
|
appear
|
||||||
enter-active-class="animated zoomIn"
|
enter-active-class="animated zoomIn"
|
||||||
@@ -356,9 +343,9 @@
|
|||||||
|
|
||||||
const displayChats = computed(() => {
|
const displayChats = computed(() => {
|
||||||
return chats.value
|
return chats.value
|
||||||
.filter(searchChats)
|
.filter(BySearch)
|
||||||
|
|
||||||
function searchChats (el: Chat) {
|
function BySearch (el: Chat) {
|
||||||
if (!search.value || !(search.value && search.value.trim())) return true
|
if (!search.value || !(search.value && search.value.trim())) return true
|
||||||
const searchValue = search.value.trim().toLowerCase()
|
const searchValue = search.value.trim().toLowerCase()
|
||||||
return el.name.toLowerCase().includes(searchValue) ||
|
return el.name.toLowerCase().includes(searchValue) ||
|
||||||
|
|||||||
@@ -1,70 +1,88 @@
|
|||||||
<template>
|
<template>
|
||||||
<pn-page-card v-bind="$attrs">
|
<pn-page-card v-bind="$attrs">
|
||||||
|
<template #card-header>
|
||||||
|
<pn-action-bar
|
||||||
|
v-model="search"
|
||||||
|
placeholder="chats__search"
|
||||||
|
:show-calendar-btn="false"
|
||||||
|
:show-filter-btn="false"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
v-if="companies.length > 2"
|
||||||
|
@click="maskCompany"
|
||||||
|
round
|
||||||
|
color="grey-2"
|
||||||
|
class="text-grey q-ml-sm"
|
||||||
|
icon="mdi-domino-mask"
|
||||||
|
/>
|
||||||
|
</pn-action-bar>
|
||||||
|
</template>
|
||||||
|
|
||||||
<q-list v-if="companies.length !== 0">
|
<q-list v-if="companies.length !== 0">
|
||||||
<q-slide-item
|
<template
|
||||||
v-for="item in displayCompanies"
|
v-for="item in displayCompanies"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@right="handleSlide($event, item.id)"
|
|
||||||
right-color="red"
|
|
||||||
>
|
>
|
||||||
<template #right v-if="item.id !== myCompany?.id">
|
<q-slide-item
|
||||||
<q-icon size="lg" name="mdi-account-multiple-minus-outline"/>
|
@right="handleSlide($event, item.id)"
|
||||||
</template>
|
|
||||||
<q-item
|
|
||||||
:key="item.id"
|
|
||||||
clickable
|
clickable
|
||||||
v-ripple
|
v-ripple
|
||||||
class="w100"
|
|
||||||
@click="goCompanyInfo(item.id)"
|
@click="goCompanyInfo(item.id)"
|
||||||
|
right-color="red"
|
||||||
>
|
>
|
||||||
<q-item-section avatar>
|
<template #right v-if="item.id !== myCompany?.id">
|
||||||
<pn-auto-avatar
|
<q-icon size="lg" name="mdi-account-multiple-minus-outline"/>
|
||||||
:img="item.logo"
|
</template>
|
||||||
:name="item.name"
|
<q-item>
|
||||||
type="rounded"
|
<q-item-section avatar>
|
||||||
size="lg"
|
<pn-auto-avatar
|
||||||
/>
|
:img="item.logo"
|
||||||
</q-item-section>
|
:name="item.name"
|
||||||
<q-item-section>
|
type="rounded"
|
||||||
<q-item-label lines="1" class="text-caption text-amber-10" v-if="item.id === myCompany?.id">
|
size="lg"
|
||||||
<div class="flex items-center">
|
|
||||||
<q-icon name="star" class="q-pr-xs"/>
|
|
||||||
{{ $t('companies__my_company') }}
|
|
||||||
</div>
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-label lines="1" class="text-bold">
|
|
||||||
{{ item.name }}
|
|
||||||
<q-icon
|
|
||||||
v-if="companiesStore.checkCompanyMasked(item.id)"
|
|
||||||
name="mdi-domino-mask"
|
|
||||||
color="grey"
|
|
||||||
size="sm"
|
|
||||||
/>
|
/>
|
||||||
</q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption lines="2"
|
|
||||||
style="max-width: -webkit-fill-available; white-space: pre-line"
|
|
||||||
>
|
|
||||||
{{ item.description }}
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section side>
|
|
||||||
<div class="flex items-end column">
|
|
||||||
<span class="text-caption flex items-center">
|
|
||||||
<q-icon name="mdi-account-outline" color="grey" />
|
|
||||||
<span>{{ getQtyUsers(item.id) }}</span>
|
|
||||||
</span>
|
|
||||||
<div class="flex items-center row text-caption">
|
|
||||||
<q-icon v-if="item.site" name="mdi-web"/>
|
|
||||||
<q-icon v-if="item.address" name="mdi-map-marker-outline"/>
|
|
||||||
<q-icon v-if="item.phone" name="mdi-phone-outline"/>
|
|
||||||
<q-icon v-if="item.email" name="mdi-email-outline"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
<q-item-section>
|
||||||
</q-slide-item>
|
<q-item-label lines="1" class="text-caption text-amber-10" v-if="item.id === myCompany?.id">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<q-icon name="star" class="q-pr-xs"/>
|
||||||
|
{{ $t('companies__my_company') }}
|
||||||
|
</div>
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label lines="1" class="text-bold">
|
||||||
|
{{ item.name }}
|
||||||
|
<q-icon
|
||||||
|
v-if="companiesStore.checkCompanyMasked(item.id)"
|
||||||
|
name="mdi-domino-mask"
|
||||||
|
color="grey"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
caption lines="2"
|
||||||
|
style="max-width: -webkit-fill-available; white-space: pre-line"
|
||||||
|
>
|
||||||
|
{{ item.description }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section side>
|
||||||
|
<div class="flex items-end column">
|
||||||
|
<span class="text-caption flex items-center">
|
||||||
|
<q-icon name="mdi-account-outline" color="grey" />
|
||||||
|
<span>{{ getQtyUsers(item.id) }}</span>
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center row text-caption">
|
||||||
|
<q-icon v-if="item.site" name="mdi-web"/>
|
||||||
|
<q-icon v-if="item.address" name="mdi-map-marker-outline"/>
|
||||||
|
<q-icon v-if="item.phone" name="mdi-phone-outline"/>
|
||||||
|
<q-icon v-if="item.email" name="mdi-email-outline"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-slide-item>
|
||||||
|
</template>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|
||||||
<pn-onboard-btn
|
<pn-onboard-btn
|
||||||
@@ -82,27 +100,6 @@
|
|||||||
<q-linear-progress indeterminate />
|
<q-linear-progress indeterminate />
|
||||||
</div>
|
</div>
|
||||||
<template #card-fab>
|
<template #card-fab>
|
||||||
|
|
||||||
<div v-if="companies.length >= 3">
|
|
||||||
<transition
|
|
||||||
appear
|
|
||||||
enter-active-class="animated zoomIn"
|
|
||||||
>
|
|
||||||
<pn-glass-btn
|
|
||||||
type="fab"
|
|
||||||
color="grey-2"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
@click="maskCompany"
|
|
||||||
fab
|
|
||||||
icon="mdi-domino-mask"
|
|
||||||
color="transparent"
|
|
||||||
class="text-grey"
|
|
||||||
/>
|
|
||||||
</pn-glass-btn>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<transition
|
<transition
|
||||||
appear
|
appear
|
||||||
enter-active-class="animated zoomIn"
|
enter-active-class="animated zoomIn"
|
||||||
@@ -141,13 +138,17 @@
|
|||||||
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 pnActionBar from 'components/pnActionBar.vue'
|
||||||
import PnGlassBtn from 'components/pnGlassBtn.vue'
|
import PnGlassBtn from 'components/pnGlassBtn.vue'
|
||||||
|
import type { Company } from 'types/Company'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const companiesStore = useCompaniesStore()
|
const companiesStore = useCompaniesStore()
|
||||||
const usersStore = useUsersStore()
|
const usersStore = useUsersStore()
|
||||||
const showDialogDeleteCompany = ref<boolean>(false)
|
|
||||||
|
const search = ref('')
|
||||||
|
const showDialogDeleteCompany = ref(false)
|
||||||
const deleteCompanyId = ref<number | undefined>(undefined)
|
const deleteCompanyId = ref<number | undefined>(undefined)
|
||||||
const currentSlideEvent = ref<SlideEvent | null>(null)
|
const currentSlideEvent = ref<SlideEvent | null>(null)
|
||||||
const closedByUserAction = ref(false)
|
const closedByUserAction = ref(false)
|
||||||
@@ -157,16 +158,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const users = computed(() => usersStore.users)
|
const users = computed(() => usersStore.users)
|
||||||
const companies = companiesStore.getCompanies
|
const companies = computed(() => companiesStore.companies)
|
||||||
const companiesInit = computed(() => companiesStore.isInit)
|
const companiesInit = computed(() => companiesStore.isInit)
|
||||||
|
|
||||||
const myCompany = computed(() => companies.find(el => el.is_own))
|
const myCompany = computed(() => companies.value.find(el => el.is_own))
|
||||||
|
|
||||||
const displayCompanies = computed(() => {
|
const displayCompanies = computed(() => {
|
||||||
const otherComp = companies.filter(el => !el.is_own)
|
const otherComp = companies.value.filter(el => !el.is_own)
|
||||||
return myCompany.value
|
const c = myCompany.value
|
||||||
? [myCompany.value, ...otherComp]
|
? [myCompany.value, ...otherComp]
|
||||||
: otherComp
|
: otherComp
|
||||||
|
|
||||||
|
return c.filter(searchCompanies)
|
||||||
|
|
||||||
|
function searchCompanies (el: Company) {
|
||||||
|
if (!search.value || !(search.value && search.value.trim())) return true
|
||||||
|
const searchValue = search.value.trim().toLowerCase()
|
||||||
|
return el.name.toLowerCase().includes(searchValue) ||
|
||||||
|
el.description && el.description.toLowerCase().includes(searchValue)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
async function maskCompany () {
|
async function maskCompany () {
|
||||||
@@ -194,14 +204,6 @@
|
|||||||
closedByUserAction.value = false
|
closedByUserAction.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCancel() {
|
|
||||||
closedByUserAction.value = true
|
|
||||||
if (currentSlideEvent.value) {
|
|
||||||
currentSlideEvent.value.reset()
|
|
||||||
currentSlideEvent.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onConfirm() {
|
async function onConfirm() {
|
||||||
closedByUserAction.value = true
|
closedByUserAction.value = true
|
||||||
if (deleteCompanyId.value) {
|
if (deleteCompanyId.value) {
|
||||||
@@ -210,6 +212,14 @@
|
|||||||
currentSlideEvent.value = null
|
currentSlideEvent.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onCancel() {
|
||||||
|
closedByUserAction.value = true
|
||||||
|
if (currentSlideEvent.value) {
|
||||||
|
currentSlideEvent.value.reset()
|
||||||
|
currentSlideEvent.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getQtyUsers (companyId: number) {
|
function getQtyUsers (companyId: number) {
|
||||||
const arr = users.value.filter(el => el.company_id === companyId)
|
const arr = users.value.filter(el => el.company_id === companyId)
|
||||||
return arr.length
|
return arr.length
|
||||||
@@ -218,11 +228,9 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
:deep(.q-slide-item__right) {
|
||||||
/* fix mini border after slide */
|
|
||||||
:deep(.q-slide-item__right)
|
|
||||||
{
|
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
height: 98%;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
import routes from './routes'
|
import routes from './routes'
|
||||||
import { useAuthStore } from 'stores/auth'
|
import { useAuthStore } from 'stores/auth'
|
||||||
import { useProjectsStore } from 'stores/projects'
|
import { useProjectsStore } from 'stores/projects'
|
||||||
import { computed } from 'vue'
|
|
||||||
const tg = window.Telegram?.WebApp
|
const tg = window.Telegram?.WebApp
|
||||||
|
|
||||||
declare module 'vue-router' {
|
declare module 'vue-router' {
|
||||||
@@ -35,6 +34,9 @@ export default defineRouter(function (/* { store, ssrContext } */) {
|
|||||||
|
|
||||||
const publicPath = ['404', 'terms', 'privacy', 'consent', 'settings']
|
const publicPath = ['404', 'terms', 'privacy', 'consent', 'settings']
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const projectsStore = useProjectsStore()
|
||||||
|
|
||||||
Router.beforeEach(async (to) => {
|
Router.beforeEach(async (to) => {
|
||||||
if (to.name !== 'telegram_only' && sessionStorage.getItem('isTelegram') === 'false') {
|
if (to.name !== 'telegram_only' && sessionStorage.getItem('isTelegram') === 'false') {
|
||||||
return { name: 'telegram_only', replace: true }
|
return { name: 'telegram_only', replace: true }
|
||||||
@@ -43,9 +45,6 @@ export default defineRouter(function (/* { store, ssrContext } */) {
|
|||||||
if (to.name === 'telegram_only') return true
|
if (to.name === 'telegram_only') return true
|
||||||
if (typeof to.name === 'string' && publicPath.includes(to.name)) return true
|
if (typeof to.name === 'string' && publicPath.includes(to.name)) return true
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
|
||||||
const projectsStore = useProjectsStore()
|
|
||||||
|
|
||||||
if (to.name === 'login')
|
if (to.name === 'login')
|
||||||
return authStore.isAuth ? { name: 'projects', replace: true } : true
|
return authStore.isAuth ? { name: 'projects', replace: true } : true
|
||||||
|
|
||||||
@@ -58,13 +57,9 @@ export default defineRouter(function (/* { store, ssrContext } */) {
|
|||||||
|
|
||||||
if (to.params.id) {
|
if (to.params.id) {
|
||||||
if (!projectsStore.isInit) await projectsStore.init()
|
if (!projectsStore.isInit) await projectsStore.init()
|
||||||
|
if (to.params.id && projectsStore.projectById(Number(to.params.id))) {
|
||||||
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
if (projectsStore.currentProjectId !== Number(to.params.id)) {
|
||||||
|
projectsStore.setCurrentProjectId(Number(to.params.id))
|
||||||
if (to.params.id) {
|
|
||||||
if (currentProjectId.value !== Number(to.params.id)) {
|
|
||||||
if (!projectsStore.projectById(Number(to.params.id)))
|
|
||||||
projectsStore.setCurrentProjectId(Number(to.params.id))
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
projectsStore.setCurrentProjectId(null)
|
projectsStore.setCurrentProjectId(null)
|
||||||
@@ -93,7 +88,9 @@ export default defineRouter(function (/* { store, ssrContext } */) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useProjectsStore().setCurrentProjectId(to.params.id ? Number(to.params.id) : null)
|
if (to.params.id) projectsStore.setLastProjectId(Number(to.params.id))
|
||||||
|
if (to.name === 'projects') projectsStore.setLastProjectId(null)
|
||||||
|
projectsStore.setCurrentProjectId(to.params.id ? Number(to.params.id) : null)
|
||||||
})
|
})
|
||||||
|
|
||||||
return Router
|
return Router
|
||||||
|
|||||||
@@ -150,10 +150,15 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/support',
|
path: '/support',
|
||||||
component: () => import('pages/account/SupportPage.vue')
|
component: () => import('pages/account/SupportPage.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'backup',
|
||||||
|
path: '/backup',
|
||||||
|
component: () => import('pages/account/BackupPage.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'your_company',
|
name: 'your_company',
|
||||||
path: '/your-company',
|
path: '/your-company',
|
||||||
component: () => import('pages/CompanyYourPage.vue')
|
component: () => import('pages/account/CompanyYourPage.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'login',
|
name: 'login',
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ export const useSSEStore = defineStore('sse', () => {
|
|||||||
|
|
||||||
eventSource.value.addEventListener('chat-update', (event) => {
|
eventSource.value.addEventListener('chat-update', (event) => {
|
||||||
const data = JSON.parse(event.data)
|
const data = JSON.parse(event.data)
|
||||||
projectsStore.init().catch(() => {}) // for update count of chats on projects page
|
|
||||||
authStore.updateProfile().catch(() => {}) // for update count of active chats in subscribe
|
authStore.updateProfile().catch(() => {}) // for update count of active chats in subscribe
|
||||||
|
projectsStore.init().catch(() => {}) // for update count of chats on projects page
|
||||||
if (data.project_id === currentProjectId.value) {
|
if (data.project_id === currentProjectId.value) {
|
||||||
chatsStore.init().catch(() => {})
|
chatsStore.init().catch(() => {})
|
||||||
usersStore.init().catch(() => {}) // for update users from chats
|
usersStore.init().catch(() => {}) // for update users from chats
|
||||||
@@ -78,6 +78,7 @@ export const useSSEStore = defineStore('sse', () => {
|
|||||||
eventSource.value.addEventListener('chat-delete', (event) => {
|
eventSource.value.addEventListener('chat-delete', (event) => {
|
||||||
const data = JSON.parse(event.data)
|
const data = JSON.parse(event.data)
|
||||||
projectsStore.init().catch(() => {}) // for update count of chats on projects page
|
projectsStore.init().catch(() => {}) // for update count of chats on projects page
|
||||||
|
|
||||||
authStore.updateProfile().catch(() => {}) // for update count of active chats in subscribe
|
authStore.updateProfile().catch(() => {}) // for update count of active chats in subscribe
|
||||||
if (data.project_id === currentProjectId.value) {
|
if (data.project_id === currentProjectId.value) {
|
||||||
chatsStore.init().catch(() => {})
|
chatsStore.init().catch(() => {})
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
await api.get('/auth/logout', {})
|
await api.get('/auth/logout', {})
|
||||||
customer.value = null
|
customer.value = null
|
||||||
|
projectsStore.setCurrentProjectId(null)
|
||||||
projectsStore.reset()
|
projectsStore.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +94,6 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
const getCodeCurrentEmail = async () =>
|
const getCodeCurrentEmail = async () =>
|
||||||
await api.post('/auth/email/change-email')
|
await api.post('/auth/email/change-email')
|
||||||
|
|
||||||
|
|
||||||
const confirmCurrentEmailCode = async (code: string) =>
|
const confirmCurrentEmailCode = async (code: string) =>
|
||||||
await api.post('/auth/email/change-email', { code })
|
await api.post('/auth/email/change-email', { code })
|
||||||
|
|
||||||
|
|||||||
@@ -69,8 +69,6 @@ export const useCompaniesStore = defineStore('companies', () => {
|
|||||||
companiesMask.value = data.data
|
companiesMask.value = data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCompanies = computed(() => companies.value)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
companies,
|
companies,
|
||||||
companiesMask,
|
companiesMask,
|
||||||
@@ -82,8 +80,7 @@ export const useCompaniesStore = defineStore('companies', () => {
|
|||||||
remove,
|
remove,
|
||||||
companyById,
|
companyById,
|
||||||
checkCompanyMasked,
|
checkCompanyMasked,
|
||||||
updateMask,
|
updateMask
|
||||||
getCompanies
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
30
src/stores/files.ts
Normal file
30
src/stores/files.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { computed } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { api } from 'boot/axios'
|
||||||
|
import { useProjectsStore } from 'stores/projects'
|
||||||
|
import type { FileLink } from 'types/FileLink'
|
||||||
|
|
||||||
|
export const useFilesStore = defineStore('files', () => {
|
||||||
|
const projectsStore = useProjectsStore()
|
||||||
|
const currentProjectId = computed(() => projectsStore.currentProjectId)
|
||||||
|
|
||||||
|
async function getFilesList (projectId: number) {
|
||||||
|
const { data } = await api.get(`/project/${projectId}/file`)
|
||||||
|
return data.data as FileLink[]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadParams (fileId: number) {
|
||||||
|
const { data } = await api.get(`/project/${currentProjectId.value}/file/${fileId}/token`)
|
||||||
|
return data.data
|
||||||
|
? {
|
||||||
|
url: `/api/download/${data.data.token}`,
|
||||||
|
file_name: data.data.filename as string
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getFilesList,
|
||||||
|
downloadParams
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -11,24 +11,26 @@ import type { Project, ProjectParams } from 'types/Project'
|
|||||||
export const useProjectsStore = defineStore('projects', () => {
|
export const useProjectsStore = defineStore('projects', () => {
|
||||||
const projects = ref<Project[]>([])
|
const projects = ref<Project[]>([])
|
||||||
const currentProjectId = ref<number | null>(null)
|
const currentProjectId = ref<number | null>(null)
|
||||||
|
const lastProjectId = ref<number | null>(null)
|
||||||
const isInit = ref<boolean>(false)
|
const isInit = ref<boolean>(false)
|
||||||
const chatsStore = useChatsStore()
|
const chatsStore = useChatsStore()
|
||||||
const usersStore = useUsersStore()
|
const usersStore = useUsersStore()
|
||||||
const companiesStore = useCompaniesStore()
|
const companiesStore = useCompaniesStore()
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
|
||||||
async function init () {
|
|
||||||
// guard for double request
|
async function init() {
|
||||||
|
// guard for double request
|
||||||
if (isLoading.value) return
|
if (isLoading.value) return
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
reset()
|
|
||||||
const { data } = await api.get('/project')
|
const { data } = await api.get('/project')
|
||||||
const projectsAPI = data.data
|
const projectsAPI = data.data
|
||||||
projects.value.push(...projectsAPI)
|
projects.value = projectsAPI
|
||||||
isInit.value = true
|
isInit.value = true
|
||||||
} catch {
|
} catch (error) {
|
||||||
reset()
|
console.error('Failed to init projects:', error)
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -76,6 +78,10 @@ export const useProjectsStore = defineStore('projects', () => {
|
|||||||
currentProjectId.value = (typeof id ==='number') ? id : null
|
currentProjectId.value = (typeof id ==='number') ? id : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setLastProjectId (id: number | null | undefined) {
|
||||||
|
lastProjectId.value = (typeof id ==='number') ? id : null
|
||||||
|
}
|
||||||
|
|
||||||
async function initStores () {
|
async function initStores () {
|
||||||
resetStores()
|
resetStores()
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -105,12 +111,14 @@ export const useProjectsStore = defineStore('projects', () => {
|
|||||||
isInit,
|
isInit,
|
||||||
projects,
|
projects,
|
||||||
currentProjectId,
|
currentProjectId,
|
||||||
|
lastProjectId,
|
||||||
add,
|
add,
|
||||||
update,
|
update,
|
||||||
archive,
|
archive,
|
||||||
restore,
|
restore,
|
||||||
projectById,
|
projectById,
|
||||||
setCurrentProjectId,
|
setCurrentProjectId,
|
||||||
|
setLastProjectId,
|
||||||
initStores,
|
initStores,
|
||||||
resetStores
|
resetStores
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,32 @@
|
|||||||
|
import { ref, computed, inject, getCurrentInstance } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, computed, inject } from 'vue'
|
|
||||||
import { api } from 'boot/axios'
|
import { api } from 'boot/axios'
|
||||||
import { useAuthStore } from 'stores/auth'
|
import { useAuthStore } from 'stores/auth'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Lang } from 'quasar'
|
import { Lang } from 'quasar'
|
||||||
import type { WebApp } from '@twa-dev/types'
|
import type { WebApp } from '@twa-dev/types'
|
||||||
|
|
||||||
|
const app = getCurrentInstance()
|
||||||
|
const isIOS = app?.appContext.config.globalProperties.$isIOS
|
||||||
|
const isMobile = app?.appContext.config.globalProperties.$isMobile
|
||||||
|
|
||||||
interface AppSettings {
|
interface AppSettings {
|
||||||
fontSize: number
|
|
||||||
locale: string
|
locale: string
|
||||||
timeZoneBot: { tz: string, offset: number, offsetString: string }
|
fontSize: number
|
||||||
|
displayMode: 'full' | 'window'
|
||||||
localeBot: string
|
localeBot: string
|
||||||
|
timeZoneBot: { tz: string, offset: number, offsetString: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultFontSize = 16
|
const defaultFontSize = 16
|
||||||
|
const defaultDisplayMode = isIOS && isMobile ? 'full' : 'window'
|
||||||
|
|
||||||
const defaultSettings: AppSettings = {
|
const defaultSettings: AppSettings = {
|
||||||
fontSize: defaultFontSize,
|
|
||||||
locale: 'en-US',
|
locale: 'en-US',
|
||||||
timeZoneBot: { tz: 'Europe/Moscow', offset: 3, offsetString: '+03:00' },
|
fontSize: defaultFontSize,
|
||||||
localeBot: 'en-US'
|
displayMode: defaultDisplayMode,
|
||||||
|
localeBot: 'en-US',
|
||||||
|
timeZoneBot: { tz: 'Europe/Moscow', offset: 3, offsetString: '+03:00' }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSettingsStore = defineStore('settings', () => {
|
export const useSettingsStore = defineStore('settings', () => {
|
||||||
@@ -98,29 +105,25 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
const init = async () => {
|
const init = async () => {
|
||||||
if (authStore.isAuth) {
|
if (authStore.isAuth) {
|
||||||
try {
|
try {
|
||||||
const response = await api.get<{ data: AppSettings }>('/customer/settings')
|
const { data } = await api.get('/customer/settings')
|
||||||
let serverData = response.data.data
|
settings.value = {
|
||||||
const updateData: Partial<AppSettings> = {}
|
locale: data.data.locale || detectLocale(),
|
||||||
|
fontSize: data.data.fontSize || defaultSettings.fontSize,
|
||||||
if (!serverData.fontSize) updateData.fontSize = defaultSettings.fontSize
|
displayMode: data.data.displayMode || defaultSettings.displayMode,
|
||||||
if (!serverData.locale) updateData.locale = detectLocale()
|
localeBot: data.data.localeBot || detectLocale(),
|
||||||
if (!serverData.timeZoneBot) updateData.timeZoneBot = defaultSettings.timeZoneBot
|
timeZoneBot: data.data.timeZoneBot || defaultSettings.timeZoneBot
|
||||||
if (!serverData.localeBot) updateData.localeBot = detectLocale()
|
|
||||||
|
|
||||||
if (Object.keys(updateData).length > 0) {
|
|
||||||
await api.put('/customer/settings', updateData)
|
|
||||||
const response = await api.get<{ data: AppSettings }>('/customer/settings')
|
|
||||||
serverData = response.data.data
|
|
||||||
}
|
}
|
||||||
|
if (!data.data.locale || !data.data.fontSize || !data.data.displayMode ||
|
||||||
settings.value = serverData
|
!data.data.localeBot || !data.data.timeZoneBot
|
||||||
|
) await saveSettings()
|
||||||
} catch {
|
} catch {
|
||||||
settings.value.locale = detectLocale()
|
settings.value.locale = detectLocale()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
settings.value = {
|
settings.value = {
|
||||||
...defaultSettings,
|
...defaultSettings,
|
||||||
locale: detectLocale()
|
locale: detectLocale(),
|
||||||
|
localeBot: detectLocale()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateCssVariable()
|
updateCssVariable()
|
||||||
@@ -128,6 +131,11 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
isInit.value = true
|
isInit.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saveSettings = async () => {
|
||||||
|
const { data } = await api.put('/settings', settings.value)
|
||||||
|
if (data.data) settings.value = data.data
|
||||||
|
}
|
||||||
|
|
||||||
const updateSettings = async (newSettings: Partial<AppSettings>) => {
|
const updateSettings = async (newSettings: Partial<AppSettings>) => {
|
||||||
settings.value = { ...settings.value, ...newSettings }
|
settings.value = { ...settings.value, ...newSettings }
|
||||||
updateCssVariable()
|
updateCssVariable()
|
||||||
|
|||||||
23
src/types/FileLink.ts
Normal file
23
src/types/FileLink.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
interface FileLink {
|
||||||
|
id: number
|
||||||
|
filename: string
|
||||||
|
mime: string
|
||||||
|
size: number
|
||||||
|
published_by: number
|
||||||
|
published: number
|
||||||
|
parent: {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
} & (
|
||||||
|
| { type: 'task' | 'meeting' }
|
||||||
|
| {
|
||||||
|
type: 'chat'
|
||||||
|
is_deleted: boolean
|
||||||
|
}
|
||||||
|
)
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export type {
|
||||||
|
FileLink
|
||||||
|
}
|
||||||
124
src/utils/files-functions.ts
Normal file
124
src/utils/files-functions.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
type TranslateFunction = (key: string) => string
|
||||||
|
|
||||||
|
const fileSize = (value: number, t: TranslateFunction): string => {
|
||||||
|
if (value === 0) return '-'
|
||||||
|
|
||||||
|
const units = ['B', 'kB', 'MB', 'GB', 'TB'] as const
|
||||||
|
const i = Math.floor(Math.log(value) / Math.log(1024))
|
||||||
|
|
||||||
|
const index = Math.max(0, Math.min(i, units.length - 1))
|
||||||
|
|
||||||
|
const result = (value / Math.pow(1024, index)).toFixed(1)
|
||||||
|
|
||||||
|
const unitKey = units[index]
|
||||||
|
|
||||||
|
return `${result} ${t(unitKey!)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface ParsedFile {
|
||||||
|
name: string
|
||||||
|
ext: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFileName(filename: string): ParsedFile {
|
||||||
|
const lastDotIndex = filename.lastIndexOf('.')
|
||||||
|
|
||||||
|
if (lastDotIndex === -1) {
|
||||||
|
return { name: filename, ext: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: filename.slice(0, lastDotIndex),
|
||||||
|
ext: filename.slice(lastDotIndex + 1).toLowerCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileIcon {
|
||||||
|
type: string
|
||||||
|
icon: string
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileIcon(filename: string): FileIcon {
|
||||||
|
const ext = parseFileName(filename).ext
|
||||||
|
|
||||||
|
switch (ext) {
|
||||||
|
case 'doc':
|
||||||
|
case 'docx':
|
||||||
|
return { type: 'doc', icon: 'pn-icon-file-doc', color: '#2B579A' }
|
||||||
|
|
||||||
|
case 'xls':
|
||||||
|
case 'xlsx':
|
||||||
|
case 'csv':
|
||||||
|
return { type: 'xls', icon: 'pn-icon-file-xls', color: '#217346' }
|
||||||
|
|
||||||
|
case 'vsd':
|
||||||
|
case 'vsdx':
|
||||||
|
return { type: 'vsd', icon: 'pn-icon-file-vsd', color: '#3955A3' }
|
||||||
|
|
||||||
|
case 'ppt':
|
||||||
|
case 'pptx':
|
||||||
|
return { type: 'ppt', icon: 'pn-icon-file-ppt', color: '#D24726' }
|
||||||
|
|
||||||
|
case 'pdf':
|
||||||
|
return { type: 'pdf', icon: 'pn-icon-file-pdf', color: '#D0021B' }
|
||||||
|
|
||||||
|
case 'png':
|
||||||
|
case 'jpg':
|
||||||
|
case 'jpeg':
|
||||||
|
case 'gif':
|
||||||
|
case 'bmp':
|
||||||
|
case 'webp':
|
||||||
|
case 'svg':
|
||||||
|
return { type: 'img', icon: 'pn-icon-file-img', color: '#4CAF50' }
|
||||||
|
|
||||||
|
case 'mp3':
|
||||||
|
case 'wav':
|
||||||
|
case 'ogg':
|
||||||
|
return { type: 'music', icon: 'pn-icon-file-audio', color: '#FF9800' }
|
||||||
|
|
||||||
|
case 'mp4':
|
||||||
|
case 'avi':
|
||||||
|
case 'mov':
|
||||||
|
case 'mkv':
|
||||||
|
return { type: 'video', icon: 'pn-icon-file-video', color: '#9C27B0' }
|
||||||
|
|
||||||
|
case 'js':
|
||||||
|
case 'ts':
|
||||||
|
case 'html':
|
||||||
|
case 'css':
|
||||||
|
case 'json':
|
||||||
|
case 'xml':
|
||||||
|
return { type: 'code', icon: 'pn-icon-file-code', color: '#999' }
|
||||||
|
|
||||||
|
case 'txt':
|
||||||
|
return { type: 'txt', icon: 'pn-icon-file-txt', color: '#757575' }
|
||||||
|
|
||||||
|
case 'zip':
|
||||||
|
case 'rar':
|
||||||
|
case '7z':
|
||||||
|
case 'tar':
|
||||||
|
case 'gz':
|
||||||
|
return { type: 'archive', icon: 'pn-icon-file-archive', color: '#F78E1E' }
|
||||||
|
|
||||||
|
case 'skp':
|
||||||
|
return { type: 'skp', icon: 'pn-icon-file-skp', color: '#CC0000' }
|
||||||
|
|
||||||
|
case 'dwg':
|
||||||
|
case 'dxf':
|
||||||
|
return { type: 'cad', icon: 'pn-icon-file-dwg', color: '#00579D' }
|
||||||
|
|
||||||
|
case 'ttf':
|
||||||
|
return { type: 'font', icon: 'pn-icon-file-ttf', color: '#607D8B' }
|
||||||
|
|
||||||
|
default:
|
||||||
|
return { type: 'common', icon: 'pn-icon-file-default', color: '#9E9E9E' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
fileSize,
|
||||||
|
parseFileName,
|
||||||
|
fileIcon
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user