edit panel sms show
This commit is contained in:
Vendored
-4
@@ -10,7 +10,6 @@ export {}
|
|||||||
declare module '@vue/runtime-core' {
|
declare module '@vue/runtime-core' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
_index: typeof import('./src/core/components/_sample/_index.vue')['default']
|
_index: typeof import('./src/core/components/_sample/_index.vue')['default']
|
||||||
AAlert: typeof import('ant-design-vue/es')['Alert']
|
|
||||||
AAvatar: typeof import('ant-design-vue/es')['Avatar']
|
AAvatar: typeof import('ant-design-vue/es')['Avatar']
|
||||||
ABadge: typeof import('ant-design-vue/es')['Badge']
|
ABadge: typeof import('ant-design-vue/es')['Badge']
|
||||||
AButton: typeof import('ant-design-vue/es')['Button']
|
AButton: typeof import('ant-design-vue/es')['Button']
|
||||||
@@ -43,7 +42,6 @@ declare module '@vue/runtime-core' {
|
|||||||
APagination: typeof import('ant-design-vue/es')['Pagination']
|
APagination: typeof import('ant-design-vue/es')['Pagination']
|
||||||
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
||||||
APopover: typeof import('ant-design-vue/es')['Popover']
|
APopover: typeof import('ant-design-vue/es')['Popover']
|
||||||
ARadio: typeof import('ant-design-vue/es')['Radio']
|
|
||||||
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
||||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||||
ARow: typeof import('ant-design-vue/es')['Row']
|
ARow: typeof import('ant-design-vue/es')['Row']
|
||||||
@@ -55,11 +53,9 @@ declare module '@vue/runtime-core' {
|
|||||||
ATable: typeof import('ant-design-vue/es')['Table']
|
ATable: typeof import('ant-design-vue/es')['Table']
|
||||||
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
||||||
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
||||||
ATag: typeof import('ant-design-vue/es')['Tag']
|
|
||||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||||
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
|
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
|
||||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||||
AUpload: typeof import('ant-design-vue/es')['Upload']
|
|
||||||
BaseButton: typeof import('./src/core/components/BaseButton/index.ts')['default']
|
BaseButton: typeof import('./src/core/components/BaseButton/index.ts')['default']
|
||||||
BaseCard: typeof import('./src/core/components/BaseCard/index.ts')['default']
|
BaseCard: typeof import('./src/core/components/BaseCard/index.ts')['default']
|
||||||
BaseCollapse: typeof import('./src/components/BaseCollapse.vue')['default']
|
BaseCollapse: typeof import('./src/components/BaseCollapse.vue')['default']
|
||||||
|
|||||||
@@ -1,15 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="store.visible" class="sms-credit" :class="`sms-credit--${variant}`">
|
<div
|
||||||
|
v-if="store.visible"
|
||||||
|
class="sms-credit"
|
||||||
|
:class="[`sms-credit--${variant}`, { 'sms-credit--label-hint': showLabelHint && isCompactDesktop }]"
|
||||||
|
>
|
||||||
<div class="sms-credit__info">
|
<div class="sms-credit__info">
|
||||||
<span class="sms-credit__label">اعتبار پیامک:</span>
|
<span v-if="!isCompactDesktop" class="sms-credit__label">اعتبار پیامک:</span>
|
||||||
<span class="sms-credit__amount" :class="amountClass">
|
|
||||||
{{ formattedBalance }}
|
<Transition name="sms-credit-swap" mode="out-in">
|
||||||
<span class="sms-credit__unit">ریال</span>
|
<span
|
||||||
</span>
|
v-if="isCompactDesktop && showLabelHint"
|
||||||
|
key="hint"
|
||||||
|
class="sms-credit__label-hint"
|
||||||
|
>
|
||||||
|
اعتبار پیامک:
|
||||||
|
</span>
|
||||||
|
<span v-else key="amount" class="sms-credit__amount" :class="amountClass">
|
||||||
|
{{ formattedBalance }}
|
||||||
|
<span class="sms-credit__unit">ریال</span>
|
||||||
|
</span>
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" class="sms-credit__charge" @click="modalVisible = true">
|
<button type="button" class="sms-credit__charge" @click="modalVisible = true">
|
||||||
<Icon icon="vuesax-linear:wallet-add" :size="15" />
|
<Icon icon="vuesax-linear:wallet-add" :size="15" />
|
||||||
<span>شارژ</span>
|
<span class="sms-credit__charge-label">شارژ</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<BasicModal
|
<BasicModal
|
||||||
@@ -70,25 +85,40 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useClipboard } from '@vueuse/core'
|
import { useClipboard, useWindowSize } from '@vueuse/core'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@core/components/BaseButton'
|
import BaseButton from '@core/components/BaseButton'
|
||||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
import { useSmsWalletStore } from '@/store/smsWalletStore'
|
import { useSmsWalletStore } from '@/store/smsWalletStore'
|
||||||
|
|
||||||
withDefaults(defineProps<{ variant?: 'desktop' | 'mobile' }>(), {
|
const props = withDefaults(defineProps<{ variant?: 'desktop' | 'mobile' }>(), {
|
||||||
variant: 'desktop'
|
variant: 'desktop'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const COMPACT_BREAKPOINT = 1280
|
||||||
|
const LABEL_HINT_INTERVAL_MS = 30_000
|
||||||
|
const LABEL_HINT_FADE_MS = 500
|
||||||
|
const LABEL_HINT_HOLD_MS = 4_000
|
||||||
|
const LABEL_HINT_DURATION_MS = LABEL_HINT_FADE_MS + LABEL_HINT_HOLD_MS
|
||||||
|
|
||||||
const store = useSmsWalletStore()
|
const store = useSmsWalletStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const modalVisible = ref(false)
|
const modalVisible = ref(false)
|
||||||
|
const showLabelHint = ref(false)
|
||||||
|
const { width: windowWidth } = useWindowSize()
|
||||||
|
|
||||||
const { copy, isSupported } = useClipboard()
|
const { copy, isSupported } = useClipboard()
|
||||||
|
|
||||||
|
let hintIntervalId: ReturnType<typeof setInterval> | undefined
|
||||||
|
let hintEndTimeoutId: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
|
const isCompactDesktop = computed(
|
||||||
|
() => props.variant === 'desktop' && windowWidth.value < COMPACT_BREAKPOINT
|
||||||
|
)
|
||||||
|
|
||||||
const deposit = computed(() => store.deposit)
|
const deposit = computed(() => store.deposit)
|
||||||
const depositNote = computed(
|
const depositNote = computed(
|
||||||
() =>
|
() =>
|
||||||
@@ -139,6 +169,53 @@ watch(
|
|||||||
() => route.fullPath,
|
() => route.fullPath,
|
||||||
() => store.refresh()
|
() => store.refresh()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function stopLabelHintTimers() {
|
||||||
|
if (hintIntervalId) {
|
||||||
|
clearInterval(hintIntervalId)
|
||||||
|
hintIntervalId = undefined
|
||||||
|
}
|
||||||
|
if (hintEndTimeoutId) {
|
||||||
|
clearTimeout(hintEndTimeoutId)
|
||||||
|
hintEndTimeoutId = undefined
|
||||||
|
}
|
||||||
|
showLabelHint.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function playLabelHint() {
|
||||||
|
if (!isCompactDesktop.value || modalVisible.value || showLabelHint.value) return
|
||||||
|
|
||||||
|
showLabelHint.value = true
|
||||||
|
hintEndTimeoutId = setTimeout(() => {
|
||||||
|
showLabelHint.value = false
|
||||||
|
hintEndTimeoutId = undefined
|
||||||
|
}, LABEL_HINT_DURATION_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
function startLabelHintLoop() {
|
||||||
|
stopLabelHintTimers()
|
||||||
|
if (!isCompactDesktop.value) return
|
||||||
|
|
||||||
|
hintIntervalId = setInterval(playLabelHint, LABEL_HINT_INTERVAL_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(isCompactDesktop, (enabled) => {
|
||||||
|
if (enabled) {
|
||||||
|
startLabelHintLoop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stopLabelHintTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(modalVisible, (open) => {
|
||||||
|
if (open) showLabelHint.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (isCompactDesktop.value) startLabelHintLoop()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(stopLabelHintTimers)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
@@ -151,17 +228,20 @@ watch(
|
|||||||
|
|
||||||
.sms-credit--desktop {
|
.sms-credit--desktop {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
column-gap: 0.5rem;
|
column-gap: 0.5rem;
|
||||||
padding: 0.375rem 0.5rem;
|
padding: 0.375rem 0.5rem;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sms-credit--desktop .sms-credit__info {
|
.sms-credit--desktop .sms-credit__info {
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sms-credit--desktop .sms-credit__charge {
|
.sms-credit--desktop .sms-credit__charge {
|
||||||
@@ -177,6 +257,7 @@ watch(
|
|||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
min-height: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sms-credit__label {
|
.sms-credit__label {
|
||||||
@@ -186,11 +267,43 @@ watch(
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sms-credit-swap-enter-active,
|
||||||
|
.sms-credit-swap-leave-active {
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sms-credit-swap-enter-from,
|
||||||
|
.sms-credit-swap-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sms-credit__label-hint {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
animation: sms-credit-label-blink 1.15s ease-in-out infinite;
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sms-credit-label-blink {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
color: #047857;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sms-credit__amount {
|
.sms-credit__amount {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sms-credit__unit {
|
.sms-credit__unit {
|
||||||
@@ -267,7 +380,6 @@ watch(
|
|||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* modal content */
|
|
||||||
.sms-charge-modal {
|
.sms-charge-modal {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -349,6 +461,45 @@ watch(
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--admin-text);
|
color: var(--admin-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1279px) {
|
||||||
|
.sms-credit--desktop {
|
||||||
|
column-gap: 0.35rem;
|
||||||
|
padding: 0.3rem 0.45rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1279px) and (prefers-reduced-motion: reduce) {
|
||||||
|
.sms-credit-swap-enter-active,
|
||||||
|
.sms-credit-swap-leave-active {
|
||||||
|
transition-duration: 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sms-credit__label-hint {
|
||||||
|
animation: none;
|
||||||
|
animation-delay: 0;
|
||||||
|
color: var(--admin-text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1099px) {
|
||||||
|
.sms-credit--desktop .sms-credit__charge-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sms-credit--desktop .sms-credit__charge {
|
||||||
|
padding: 0.25rem;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sms-credit--desktop .sms-credit__amount {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sms-credit--desktop .sms-credit__label-hint {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
@@ -364,5 +515,20 @@ html.is-dark {
|
|||||||
.sms-credit__amount--danger {
|
.sms-credit__amount--danger {
|
||||||
color: #fda4af;
|
color: #fda4af;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sms-credit__label-hint {
|
||||||
|
animation-name: sms-credit-label-blink-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sms-credit-label-blink-dark {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
color: #6ee7b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
color: #fda4af;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
:style="`height:${layoutStore.header.height}`"
|
:style="`height:${layoutStore.header.height}`"
|
||||||
>
|
>
|
||||||
<!-- :style="`height:${layoutStore.header.height};line-height:${layoutStore.header.height}`"-->
|
<!-- :style="`height:${layoutStore.header.height};line-height:${layoutStore.header.height}`"-->
|
||||||
<div class="flex w-full min-w-0 items-center overflow-hidden">
|
<div class="flex w-full min-w-0 items-center gap-2 overflow-hidden">
|
||||||
<div class="flex min-w-0 flex-1 items-center overflow-hidden">
|
<div class="header-menu-zone flex min-w-0 flex-1 items-center overflow-hidden">
|
||||||
<img :src="getConfigApp('avatar')" style="height: 38px" class="mr-5 mt-1 shrink-0" />
|
<img :src="getConfigApp('avatar')" style="height: 38px" class="mr-5 mt-1 shrink-0" />
|
||||||
<BaseButton
|
<BaseButton
|
||||||
type="text"
|
type="text"
|
||||||
@@ -17,10 +17,9 @@
|
|||||||
/>
|
/>
|
||||||
<BasicMenu />
|
<BasicMenu />
|
||||||
</div>
|
</div>
|
||||||
<!-- <BaseButton icon="vuesax-linear:menu" @click="handleCollapseSideMenu"></BaseButton>-->
|
<div class="header-actions flex shrink-0 items-center gap-2">
|
||||||
<!-- <div @click="handleCollapseSideMenu">بستن منو</div>-->
|
<SmsCreditWidget variant="desktop" class="header-actions__credit min-w-0" />
|
||||||
<div class="flex shrink-0 items-center gap-3">
|
<div class="header-actions__fixed flex shrink-0 items-center gap-3">
|
||||||
<SmsCreditWidget variant="desktop" />
|
|
||||||
<ADropdown placement="bottomLeft" class="ml-5">
|
<ADropdown placement="bottomLeft" class="ml-5">
|
||||||
<a class="ant-dropdown-link" @click.prevent>
|
<a class="ant-dropdown-link" @click.prevent>
|
||||||
<!-- Hover me-->
|
<!-- Hover me-->
|
||||||
@@ -63,6 +62,7 @@
|
|||||||
v-if="layoutStore.showSettingsIcon"
|
v-if="layoutStore.showSettingsIcon"
|
||||||
@click="handleShowSettings"
|
@click="handleShowSettings"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -172,7 +172,6 @@ const userStore = useUserStore()
|
|||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
const { menuTheme } = useAdminTheme()
|
const { menuTheme } = useAdminTheme()
|
||||||
|
|
||||||
|
|
||||||
const { width } = useWindowSize()
|
const { width } = useWindowSize()
|
||||||
//my info modal
|
//my info modal
|
||||||
const loading = ref<boolean>(false)
|
const loading = ref<boolean>(false)
|
||||||
@@ -252,6 +251,26 @@ function clearLocalStorage() {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-menu-zone {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 0%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: var(--admin-bg-surface);
|
||||||
|
padding-inline-start: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions__credit {
|
||||||
|
max-width: min(220px, 38vw);
|
||||||
|
}
|
||||||
|
|
||||||
.header-avatar {
|
.header-avatar {
|
||||||
border: 2px solid var(--admin-border-strong);
|
border: 2px solid var(--admin-border-strong);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,58 +7,28 @@
|
|||||||
: `${layoutStore.layout}-menu`
|
: `${layoutStore.layout}-menu`
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<div
|
<AMenu
|
||||||
ref="menuScrollRef"
|
:mode="layoutStore.sideMenu.mode"
|
||||||
:class="[isTopMenu ? 'top-menu top-menu-scroll scroll-none min-w-0 flex-1' : '']"
|
:theme="menuTheme"
|
||||||
@scroll="updateScrollArrows"
|
v-model:selectedKeys="selectedKeys"
|
||||||
>
|
>
|
||||||
<AMenu
|
<template v-for="item in menu">
|
||||||
:mode="layoutStore.sideMenu.mode"
|
<BasicSubMenu
|
||||||
:theme="menuTheme"
|
v-if="!item.meta?.hideChildren && item.children && item.children.length > 0"
|
||||||
v-model:selectedKeys="selectedKeys"
|
:menu-item="item"
|
||||||
:disabled-overflow="isTopMenu"
|
:key="item.name"
|
||||||
>
|
:level="1"
|
||||||
<template v-for="item in menu">
|
/>
|
||||||
<BasicSubMenu
|
<BaseMenuItem v-else :item="item" :key="item.name" />
|
||||||
v-if="!item.meta?.hideChildren && item.children && item.children.length > 0"
|
</template>
|
||||||
:menu-item="item"
|
</AMenu>
|
||||||
:key="item.name"
|
|
||||||
:level="1"
|
|
||||||
/>
|
|
||||||
<BaseMenuItem v-else :item="item" :key="item.name" />
|
|
||||||
</template>
|
|
||||||
</AMenu>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
v-if="isTopMenu"
|
|
||||||
v-show="showLeftArrow"
|
|
||||||
type="button"
|
|
||||||
class="top-menu-nav__btn top-menu-nav__btn--left"
|
|
||||||
aria-label="مشاهده ادامه منو"
|
|
||||||
@click="scrollMenu('left')"
|
|
||||||
>
|
|
||||||
<BaseIcon icon="vuesax-linear:arrow-left-1" size="18" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
v-if="isTopMenu"
|
|
||||||
v-show="showRightArrow"
|
|
||||||
type="button"
|
|
||||||
class="top-menu-nav__btn top-menu-nav__btn--right"
|
|
||||||
aria-label="بازگشت در منو"
|
|
||||||
@click="scrollMenu('right')"
|
|
||||||
>
|
|
||||||
<BaseIcon icon="vuesax-linear:arrow-right-1" size="18" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import type { ComputedRef } from 'vue'
|
import type { ComputedRef } from 'vue'
|
||||||
import { sortBy } from 'lodash'
|
import { sortBy } from 'lodash'
|
||||||
import { useResizeObserver } from '@vueuse/core'
|
|
||||||
import { usePrefix } from '@core/composable/usePrefix'
|
import { usePrefix } from '@core/composable/usePrefix'
|
||||||
import BasicSubMenu from '@core/layouts/composnts/BasicSubMenu.vue'
|
import BasicSubMenu from '@core/layouts/composnts/BasicSubMenu.vue'
|
||||||
import { useLayoutStore } from '@core/stores/layout'
|
import { useLayoutStore } from '@core/stores/layout'
|
||||||
@@ -67,157 +37,42 @@ import type { RouteRecordName, RouteRecordRaw } from 'vue-router'
|
|||||||
import { useRouterStore } from '@core/stores/router'
|
import { useRouterStore } from '@core/stores/router'
|
||||||
import { useAdminTheme } from '@core/composable/useAdminTheme'
|
import { useAdminTheme } from '@core/composable/useAdminTheme'
|
||||||
|
|
||||||
const SCROLL_STEP = 180
|
|
||||||
const EDGE_THRESHOLD = 2
|
|
||||||
|
|
||||||
const { prefixCls } = usePrefix('basic-menu')
|
const { prefixCls } = usePrefix('basic-menu')
|
||||||
const layoutStore = useLayoutStore()
|
const layoutStore = useLayoutStore()
|
||||||
const currentRoute = useRoute()
|
const currentRoute = useRoute()
|
||||||
const selectedKeys = ref<(RouteRecordName | undefined)[]>([])
|
const selectedKeys = ref<(RouteRecordName | undefined)[]>([])
|
||||||
const routerStore = useRouterStore()
|
const routerStore = useRouterStore()
|
||||||
const { menuTheme } = useAdminTheme()
|
const { menuTheme } = useAdminTheme()
|
||||||
const menuScrollRef = ref<HTMLElement | null>(null)
|
|
||||||
const showLeftArrow = ref(false)
|
|
||||||
const showRightArrow = ref(false)
|
|
||||||
const rtlScrollMode = ref<'ltr' | 'negative' | 'positive'>('ltr')
|
|
||||||
|
|
||||||
const isTopMenu = computed(() => layoutStore.layout === 'top')
|
const path: (RouteRecordName | undefined)[] = []
|
||||||
|
|
||||||
function detectRtlScrollMode(el: HTMLElement): 'ltr' | 'negative' | 'positive' {
|
function walkInRoutes(routes: RouteRecordRaw[]) {
|
||||||
if (getComputedStyle(el).direction !== 'rtl') return 'ltr'
|
for (const route of routes) {
|
||||||
|
if (route.name == currentRoute.name) {
|
||||||
const max = el.scrollWidth - el.clientWidth
|
path.push(route.name)
|
||||||
if (max <= 0) return 'ltr'
|
return true
|
||||||
|
}
|
||||||
const original = el.scrollLeft
|
if (route.children && route.children.length > 0) {
|
||||||
el.scrollLeft = 1
|
path.push(route.name)
|
||||||
const mode = el.scrollLeft === 0 ? 'positive' : 'negative'
|
const res = walkInRoutes(route.children)
|
||||||
el.scrollLeft = original
|
if (!res) {
|
||||||
return mode
|
path.pop()
|
||||||
}
|
} else {
|
||||||
|
|
||||||
function ensureRtlScrollMode(el: HTMLElement) {
|
|
||||||
if (rtlScrollMode.value !== 'ltr') return
|
|
||||||
rtlScrollMode.value = detectRtlScrollMode(el)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNormalizedScroll(el: HTMLElement) {
|
|
||||||
const max = Math.max(0, el.scrollWidth - el.clientWidth)
|
|
||||||
if (max <= 1) return { max: 0, current: 0 }
|
|
||||||
|
|
||||||
ensureRtlScrollMode(el)
|
|
||||||
|
|
||||||
let current = el.scrollLeft
|
|
||||||
if (rtlScrollMode.value === 'negative') {
|
|
||||||
current = Math.abs(current)
|
|
||||||
} else if (rtlScrollMode.value === 'positive') {
|
|
||||||
current = max - current
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
max,
|
|
||||||
current: Math.max(0, Math.min(max, current))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNormalizedScroll(el: HTMLElement, normalized: number) {
|
|
||||||
const max = Math.max(0, el.scrollWidth - el.clientWidth)
|
|
||||||
const value = Math.max(0, Math.min(max, normalized))
|
|
||||||
|
|
||||||
if (rtlScrollMode.value === 'negative') {
|
|
||||||
el.scrollTo({ left: -value, behavior: 'smooth' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rtlScrollMode.value === 'positive') {
|
|
||||||
el.scrollTo({ left: max - value, behavior: 'smooth' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
el.scrollTo({ left: value, behavior: 'smooth' })
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateScrollArrows() {
|
|
||||||
const el = menuScrollRef.value
|
|
||||||
if (!el || !isTopMenu.value) {
|
|
||||||
showLeftArrow.value = false
|
|
||||||
showRightArrow.value = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { max, current } = getNormalizedScroll(el)
|
|
||||||
if (max <= 1) {
|
|
||||||
showLeftArrow.value = false
|
|
||||||
showRightArrow.value = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
showLeftArrow.value = current < max - EDGE_THRESHOLD
|
|
||||||
showRightArrow.value = current > EDGE_THRESHOLD
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollMenu(side: 'left' | 'right') {
|
|
||||||
const el = menuScrollRef.value
|
|
||||||
if (!el) return
|
|
||||||
|
|
||||||
const { max, current } = getNormalizedScroll(el)
|
|
||||||
if (max <= 1) return
|
|
||||||
|
|
||||||
setNormalizedScroll(el, side === 'left' ? current + SCROLL_STEP : current - SCROLL_STEP)
|
|
||||||
scheduleArrowUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleArrowUpdate() {
|
|
||||||
window.setTimeout(updateScrollArrows, 350)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const path: (RouteRecordName | undefined)[] = []
|
|
||||||
|
|
||||||
function walkInRoutes(routes: RouteRecordRaw[]) {
|
|
||||||
for (const route of routes) {
|
|
||||||
if (route.name == currentRoute.name) {
|
|
||||||
path.push(route.name)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (route.children && route.children.length > 0) {
|
|
||||||
path.push(route.name)
|
|
||||||
const res = walkInRoutes(route.children)
|
|
||||||
if (!res) {
|
|
||||||
path.pop()
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
walkInRoutes(routerStore.routes)
|
walkInRoutes(routerStore.routes)
|
||||||
selectedKeys.value = path
|
selectedKeys.value = path
|
||||||
|
|
||||||
nextTick(updateScrollArrows)
|
|
||||||
})
|
|
||||||
|
|
||||||
const menu: ComputedRef<RouteRecordRaw[]> = computed(() => {
|
const menu: ComputedRef<RouteRecordRaw[]> = computed(() => {
|
||||||
return sortBy(routerStore.routes, (item) => {
|
return sortBy(routerStore.routes, (item) => {
|
||||||
return item.meta?.order
|
return item.meta?.order
|
||||||
}).filter((item) => !item.meta?.hideMenu)
|
}).filter((item) => !item.meta?.hideMenu)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(menu, () => nextTick(updateScrollArrows), { deep: true })
|
|
||||||
watch(isTopMenu, () => {
|
|
||||||
rtlScrollMode.value = 'ltr'
|
|
||||||
nextTick(updateScrollArrows)
|
|
||||||
})
|
|
||||||
watch(
|
|
||||||
() => currentRoute.name,
|
|
||||||
() => nextTick(updateScrollArrows)
|
|
||||||
)
|
|
||||||
|
|
||||||
useResizeObserver(menuScrollRef, () => {
|
|
||||||
updateScrollArrows()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
@@ -231,21 +86,20 @@ useResizeObserver(menuScrollRef, () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.top-menu-scroll .ant-menu.ant-menu-horizontal {
|
.ant-menu.ant-menu-horizontal {
|
||||||
display: flex;
|
width: 100%;
|
||||||
flex-wrap: nowrap;
|
min-width: 0;
|
||||||
align-items: center;
|
|
||||||
width: max-content;
|
|
||||||
height: 40px;
|
height: 40px;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
border-bottom: none !important;
|
border-bottom: none !important;
|
||||||
padding: 0 4px !important;
|
padding: 0 4px !important;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-menu-scroll .ant-menu-horizontal > .ant-menu-item,
|
.ant-menu-horizontal > .ant-menu-item,
|
||||||
.top-menu-scroll .ant-menu-horizontal > .ant-menu-submenu,
|
.ant-menu-horizontal > .ant-menu-submenu,
|
||||||
.top-menu-scroll .ant-menu-horizontal > .ant-menu-submenu > .ant-menu-submenu-title {
|
.ant-menu-horizontal > .ant-menu-submenu > .ant-menu-submenu-title {
|
||||||
top: 0 !important;
|
top: 0 !important;
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
height: 36px !important;
|
height: 36px !important;
|
||||||
@@ -258,96 +112,52 @@ useResizeObserver(menuScrollRef, () => {
|
|||||||
transition: background-color 0.15s ease, color 0.15s ease !important;
|
transition: background-color 0.15s ease, color 0.15s ease !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-menu-scroll .ant-menu-horizontal > .ant-menu-item::after,
|
.ant-menu-horizontal > .ant-menu-item::after,
|
||||||
.top-menu-scroll .ant-menu-horizontal > .ant-menu-submenu::after {
|
.ant-menu-horizontal > .ant-menu-submenu::after {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-menu-scroll .ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-item-selected,
|
.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-item-selected,
|
||||||
.top-menu-scroll .ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-submenu-selected {
|
.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-submenu-selected {
|
||||||
background-color: var(--admin-menu-selected);
|
background-color: var(--admin-menu-selected);
|
||||||
color: var(--admin-menu-selected-text);
|
color: var(--admin-menu-selected-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-menu-scroll .ant-menu-horizontal.ant-menu-dark > .ant-menu-item-selected,
|
.ant-menu-horizontal.ant-menu-dark > .ant-menu-item-selected,
|
||||||
.top-menu-scroll .ant-menu-horizontal.ant-menu-dark > .ant-menu-submenu-selected > .ant-menu-submenu-title {
|
.ant-menu-horizontal.ant-menu-dark > .ant-menu-submenu-selected > .ant-menu-submenu-title {
|
||||||
background-color: var(--admin-menu-selected) !important;
|
background-color: var(--admin-menu-selected) !important;
|
||||||
color: var(--admin-menu-selected-text) !important;
|
color: var(--admin-menu-selected-text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-menu-scroll .ant-menu-horizontal.ant-menu-dark > .ant-menu-item-selected .ant-menu-item-icon,
|
.ant-menu-horizontal.ant-menu-dark > .ant-menu-item-selected .ant-menu-item-icon,
|
||||||
.top-menu-scroll .ant-menu-horizontal.ant-menu-dark > .ant-menu-submenu-selected .ant-menu-item-icon,
|
.ant-menu-horizontal.ant-menu-dark > .ant-menu-submenu-selected .ant-menu-item-icon,
|
||||||
.top-menu-scroll .ant-menu-horizontal.ant-menu-dark > .ant-menu-submenu-selected .ant-menu-submenu-arrow {
|
.ant-menu-horizontal.ant-menu-dark > .ant-menu-submenu-selected .ant-menu-submenu-arrow {
|
||||||
color: var(--admin-menu-selected-text) !important;
|
color: var(--admin-menu-selected-text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-menu-scroll .ant-menu-horizontal.ant-menu-dark > .ant-menu-item:not(.ant-menu-item-selected):hover,
|
.ant-menu-horizontal.ant-menu-dark > .ant-menu-item:not(.ant-menu-item-selected):hover,
|
||||||
.top-menu-scroll .ant-menu-horizontal.ant-menu-dark > .ant-menu-submenu:not(.ant-menu-submenu-selected):hover > .ant-menu-submenu-title {
|
.ant-menu-horizontal.ant-menu-dark > .ant-menu-submenu:not(.ant-menu-submenu-selected):hover > .ant-menu-submenu-title {
|
||||||
background-color: var(--admin-bg-hover) !important;
|
background-color: var(--admin-bg-hover) !important;
|
||||||
color: var(--admin-gold-light) !important;
|
color: var(--admin-gold-light) !important;
|
||||||
box-shadow: inset 0 0 0 1px var(--admin-border-strong) !important;
|
box-shadow: inset 0 0 0 1px var(--admin-border-strong) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-menu-scroll .ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-item-selected::after,
|
.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-item-selected::after,
|
||||||
.top-menu-scroll .ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-submenu-selected::after {
|
.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-submenu-selected::after {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-menu-scroll .ant-menu-horizontal .ant-menu-item-icon + .ant-menu-title-content,
|
.ant-menu-horizontal .ant-menu-item-icon + .ant-menu-title-content,
|
||||||
.top-menu-scroll .ant-menu-horizontal .ant-menu-item-icon + span {
|
.ant-menu-horizontal .ant-menu-item-icon + span {
|
||||||
margin-inline-start: 6px !important;
|
margin-inline-start: 6px !important;
|
||||||
margin-inline-end: 0 !important;
|
margin-inline-end: 0 !important;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.top-menu-nav__btn {
|
.ant-menu-overflow-item-rest .ant-menu-submenu-title {
|
||||||
position: absolute;
|
height: 36px !important;
|
||||||
top: 50%;
|
line-height: 36px !important;
|
||||||
z-index: 2;
|
padding: 0 10px !important;
|
||||||
display: flex;
|
border-radius: 6px;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 28px;
|
|
||||||
height: 32px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--admin-bg-elevated);
|
|
||||||
color: var(--admin-gold-light);
|
|
||||||
cursor: pointer;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
box-shadow: 0 0 0 1px var(--admin-border);
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--admin-bg-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--left {
|
|
||||||
left: 0;
|
|
||||||
padding-right: 2px;
|
|
||||||
background: linear-gradient(to right, var(--admin-gradient-surface) 60%, var(--admin-gradient-surface-fade));
|
|
||||||
}
|
|
||||||
|
|
||||||
&--right {
|
|
||||||
right: 0;
|
|
||||||
padding-left: 2px;
|
|
||||||
background: linear-gradient(to left, var(--admin-gradient-surface) 60%, var(--admin-gradient-surface-fade));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.top-menu,
|
|
||||||
.top-menu-scroll {
|
|
||||||
overflow-x: auto;
|
|
||||||
overflow-y: hidden;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
scrollbar-width: none;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user