Merge remote-tracking branch 'origin/kashkool' into kashkool
This commit is contained in:
+1
-1
@@ -155,7 +155,7 @@ const {
|
||||
} = useDraggableFloatingButton('ghatreh_support_button_position_v1', {
|
||||
width: 118,
|
||||
height: 42,
|
||||
defaultBottomOffset: 100,
|
||||
defaultBottomGap: 12,
|
||||
defaultRightOffset: 16,
|
||||
})
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createVueApp } from '@/app/createApp'
|
||||
import { setupRouterGuards } from '@/app/setupRouter'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { initPriceRealtimeChannel } from '@/utils/priceSocket'
|
||||
import { initMobileScrollRestoration } from '@/layouts/composables/useMobileScrollReset'
|
||||
|
||||
type BootWindow = Window & {
|
||||
__BOOT_TIMEOUT__?: number
|
||||
@@ -74,6 +75,7 @@ export async function bootstrapApp() {
|
||||
await withTimeout(settingsStore.fetchSettings(), 45000, 'Settings loading timeout')
|
||||
initPriceRealtimeChannel((key) => settingsStore.getSettingByKey(key))
|
||||
|
||||
initMobileScrollRestoration()
|
||||
setupRouterGuards()
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
setFingerTimeOut,
|
||||
setHasFingerPrint
|
||||
} from '@/app/authBoot'
|
||||
import { recoverMobileViewportAfterAuth } from '@/layouts/composables/useMobileScrollReset'
|
||||
|
||||
export function setupRouterGuards() {
|
||||
const userStore = useUserStore()
|
||||
@@ -106,5 +107,12 @@ export function setupRouterGuards() {
|
||||
}
|
||||
})
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
if (document.documentElement.classList.contains('app-layout--desktop')) return
|
||||
if (from.name === 'login') {
|
||||
recoverMobileViewportAfterAuth()
|
||||
}
|
||||
})
|
||||
|
||||
watch(currentRoute, async () => (userStore.userInfo ? '' : await start()))
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
@import (less) './fonts.less';
|
||||
@import (less) '../../layouts/styles/desktop-shell.less';
|
||||
@import (less) '../../layouts/styles/mobile-frame.less';
|
||||
@import (less) '../../layouts/styles/mobile-page-common.less';
|
||||
@import (less) '../../layouts/desktop/styles/desktop-page-common.less';
|
||||
@import (less) '../../views/base/fund/desktop/styles/desktop-fund.less';
|
||||
@import (less) '../../views/base/transactions/desktop/styles/desktop-transactions.less';
|
||||
@import (less) '../../views/base/wallet/desktop/styles/desktop-wallet.less';
|
||||
@import (less) '../../views/base/user/desktop/styles/desktop-user.less';
|
||||
@import (less) '../../views/base/priceTrigger/desktop/styles/desktop-price-trigger.less';
|
||||
@import (less) '../../views/base/home/desktop/styles/desktop-home.less';
|
||||
@import (less) '../../layouts/desktop/styles/desktop-inner-pages.less';
|
||||
|
||||
@@ -59,6 +61,21 @@ body {
|
||||
|
||||
:root {
|
||||
color-scheme: only light;
|
||||
--app-safe-top: env(safe-area-inset-top, 0px);
|
||||
--app-safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||
--app-credit-bar-inner: 0px;
|
||||
--app-home-header-offset: calc(4.25rem + var(--app-safe-top));
|
||||
--app-credit-footer-offset: var(--app-safe-bottom);
|
||||
--app-nav-height: 80px;
|
||||
--app-bottom-chrome-height: calc(var(--app-nav-height) + var(--app-safe-bottom));
|
||||
--app-add-card-height: 88px;
|
||||
--wallet-card-dock-overlap: 52px;
|
||||
--app-z-nav: 5000;
|
||||
--app-z-wallet-dock: 4490;
|
||||
--app-z-wallet-add-bar: 4510;
|
||||
--app-z-action-sheet: 5200;
|
||||
--app-z-dialog: 5300;
|
||||
--app-z-modal: 5500;
|
||||
}
|
||||
|
||||
.ant-form-item {
|
||||
@@ -83,13 +100,13 @@ body {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/*.van-action-sheet{
|
||||
z-index: 2001 !important;
|
||||
.van-action-sheet{
|
||||
z-index: 5001 !important;
|
||||
}
|
||||
|
||||
.van-overlay{
|
||||
z-index: 2000 !important;
|
||||
}*/
|
||||
//.van-overlay{
|
||||
// z-index: 2000 !important;
|
||||
//}
|
||||
|
||||
.color-transition {
|
||||
transition: color 0.3s ease;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="mpage-header wrap-fix">
|
||||
<button
|
||||
v-if="showBack"
|
||||
type="button"
|
||||
class="mpage-header__back"
|
||||
@click="handleBack"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
||||
</button>
|
||||
<div class="mpage-header__title">{{ title }}</div>
|
||||
<div v-if="$slots.trailing" class="mpage-header__trailing">
|
||||
<slot name="trailing" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import router from '@/router'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title: string
|
||||
showBack?: boolean
|
||||
}>(),
|
||||
{
|
||||
showBack: false,
|
||||
},
|
||||
)
|
||||
|
||||
function handleBack() {
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,125 @@
|
||||
import { nextTick } from 'vue'
|
||||
import router from '@/router'
|
||||
|
||||
let recoveryTimers: ReturnType<typeof setTimeout>[] = []
|
||||
|
||||
function clearRecoveryTimers() {
|
||||
recoveryTimers.forEach(clearTimeout)
|
||||
recoveryTimers = []
|
||||
}
|
||||
|
||||
function blurFocusedField() {
|
||||
const active = document.activeElement
|
||||
if (active instanceof HTMLElement && active !== document.body) {
|
||||
active.blur()
|
||||
}
|
||||
}
|
||||
|
||||
function clearBodyScrollLock() {
|
||||
document.body.classList.remove('van-overflow-hidden')
|
||||
document.body.style.removeProperty('position')
|
||||
document.body.style.removeProperty('top')
|
||||
document.body.style.removeProperty('left')
|
||||
document.body.style.removeProperty('right')
|
||||
document.body.style.removeProperty('width')
|
||||
document.body.style.removeProperty('height')
|
||||
document.body.style.removeProperty('min-height')
|
||||
document.body.style.removeProperty('overflow')
|
||||
document.body.style.removeProperty('transform')
|
||||
document.documentElement.style.removeProperty('overflow')
|
||||
document.documentElement.style.removeProperty('height')
|
||||
document.documentElement.style.removeProperty('transform')
|
||||
|
||||
const appRoot = document.getElementById('app')
|
||||
if (appRoot) {
|
||||
appRoot.style.removeProperty('position')
|
||||
appRoot.style.removeProperty('top')
|
||||
appRoot.style.removeProperty('height')
|
||||
appRoot.style.removeProperty('min-height')
|
||||
appRoot.style.removeProperty('overflow')
|
||||
appRoot.style.removeProperty('transform')
|
||||
}
|
||||
}
|
||||
|
||||
function resetAllScrollPositions() {
|
||||
window.scrollTo(0, 0)
|
||||
document.documentElement.scrollTop = 0
|
||||
document.body.scrollTop = 0
|
||||
|
||||
document
|
||||
.querySelectorAll('.main-with-bottom-chrome-inner, .login-page, .login-page-root')
|
||||
.forEach((node) => {
|
||||
if (node instanceof HTMLElement) {
|
||||
node.scrollTop = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function kickViewportReflow() {
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
|
||||
if (window.visualViewport) {
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function runViewportRecovery() {
|
||||
clearBodyScrollLock()
|
||||
resetAllScrollPositions()
|
||||
kickViewportReflow()
|
||||
}
|
||||
|
||||
function scheduleViewportRecovery() {
|
||||
clearRecoveryTimers()
|
||||
runViewportRecovery()
|
||||
|
||||
nextTick(() => {
|
||||
requestAnimationFrame(() => {
|
||||
runViewportRecovery()
|
||||
|
||||
for (const delay of [50, 150, 300, 500]) {
|
||||
recoveryTimers.push(window.setTimeout(runViewportRecovery, delay))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** Reset scroll offsets after navigation — fixes iOS safe-area drift. */
|
||||
export function resetMobileAppScroll() {
|
||||
runViewportRecovery()
|
||||
|
||||
nextTick(() => {
|
||||
requestAnimationFrame(runViewportRecovery)
|
||||
})
|
||||
}
|
||||
|
||||
export async function dismissMobileKeyboard() {
|
||||
blurFocusedField()
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 120))
|
||||
}
|
||||
|
||||
export async function prepareMobileViewportForAppShell() {
|
||||
blurFocusedField()
|
||||
resetAllScrollPositions()
|
||||
await dismissMobileKeyboard()
|
||||
}
|
||||
|
||||
/** Full recovery after auth — runs after home shell is mounted. */
|
||||
export function recoverMobileViewportAfterAuth() {
|
||||
scheduleViewportRecovery()
|
||||
}
|
||||
|
||||
export const resetMobileAppScrollAfterLogin = recoverMobileViewportAfterAuth
|
||||
|
||||
export async function completeLoginNavigation() {
|
||||
await prepareMobileViewportForAppShell()
|
||||
await router.push({ name: 'buyProduct' })
|
||||
await router.isReady()
|
||||
recoverMobileViewportAfterAuth()
|
||||
}
|
||||
|
||||
export function initMobileScrollRestoration() {
|
||||
if (typeof history !== 'undefined' && 'scrollRestoration' in history) {
|
||||
history.scrollRestoration = 'manual'
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ export const GUEST_PRIMARY_NAV: AppNavItem[] = [
|
||||
|
||||
/** منوی FAB موبایل + بخش فرعی سایدبار دسکتاپ — کاربر لاگینشده */
|
||||
export const AUTH_SECONDARY_NAV: AppNavItem[] = [
|
||||
{ id: 'notifyTrigger', routeName: 'notifyTrigger', label: 'هشدار قیمت', icon: 'vuesax-linear:bill' },
|
||||
{ id: 'buyProduct', routeName: 'buyProduct', label: 'فروشگاه', icon: 'vuesax-linear:coin' },
|
||||
{ id: 'fund', routeName: 'fund', label: 'دارایی ها', icon: 'vuesax-linear:archive-1' },
|
||||
{ id: 'user', routeName: 'user', label: 'حساب کاربری', icon: 'vuesax-linear:user' },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="mobile-app-layout">
|
||||
<MobileOverflowMenu
|
||||
:show="popVisible"
|
||||
@close="closeOverflowMenu"
|
||||
@@ -7,27 +7,34 @@
|
||||
/>
|
||||
|
||||
<div :class="['main-with-bottom-chrome', { 'main-with-bottom-chrome--no-nav': hideMenu }]">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition :name="route.meta.transition || 'fade'">
|
||||
<component :is="Component" class="main-with-bottom-chrome-inner" />
|
||||
<router-view v-slot="{ Component, route: activeRoute }">
|
||||
<transition :name="activeRoute.meta.transition || 'fade'">
|
||||
<component :is="Component" :key="activeRoute.fullPath" class="main-with-bottom-chrome-inner" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
|
||||
<MobileBottomNav
|
||||
v-if="!hideMenu"
|
||||
:nav-item-class="navItemClass"
|
||||
@navigate="navigateTo"
|
||||
@toggle-overflow="toggleOverflowMenu"
|
||||
/>
|
||||
<Teleport to="body">
|
||||
<MobileBottomNav
|
||||
v-if="!hideMenu"
|
||||
:nav-item-class="navItemClass"
|
||||
@navigate="navigateTo"
|
||||
@toggle-overflow="toggleOverflowMenu"
|
||||
/>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAppNavigation } from '../composables/useAppNavigation'
|
||||
import { recoverMobileViewportAfterAuth, resetMobileAppScroll } from '../composables/useMobileScrollReset'
|
||||
import MobileBottomNav from './MobileBottomNav.vue'
|
||||
import MobileOverflowMenu from './MobileOverflowMenu.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const {
|
||||
popVisible,
|
||||
hideMenu,
|
||||
@@ -36,6 +43,19 @@ const {
|
||||
toggleOverflowMenu,
|
||||
closeOverflowMenu,
|
||||
} = useAppNavigation()
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
resetMobileAppScroll()
|
||||
},
|
||||
{ flush: 'post' },
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
resetMobileAppScroll()
|
||||
recoverMobileViewportAfterAuth()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" src="./mobile-layout.less"></style>
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
<div class="tabs bottom-0">
|
||||
<template v-for="item in leftTabs" :key="item.id">
|
||||
<div :class="navItemClass(item.routeName)" class="tab" @click="$emit('navigate', item.routeName)">
|
||||
<a-badge v-if="item.routeName === 'cart'" :count="cartCount">
|
||||
<Icon :icon="item.icon" size="24" />
|
||||
</a-badge>
|
||||
<Icon v-else :icon="item.icon" size="24" />
|
||||
<div class="tab__icon">
|
||||
<a-badge v-if="item.routeName === 'cart'" :count="cartCount">
|
||||
<Icon :icon="item.icon" size="24" />
|
||||
</a-badge>
|
||||
<Icon v-else :icon="item.icon" size="24" />
|
||||
</div>
|
||||
<div class="text-xs mt-1 font-bold">{{ item.label }}</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -21,10 +23,12 @@
|
||||
|
||||
<template v-for="item in rightTabs" :key="item.id">
|
||||
<div :class="navItemClass(item.routeName)" class="tab" @click="$emit('navigate', item.routeName)">
|
||||
<a-badge v-if="item.routeName === 'cart'" :count="cartCount">
|
||||
<Icon :icon="item.icon" size="24" />
|
||||
</a-badge>
|
||||
<Icon v-else :icon="item.icon" size="24" />
|
||||
<div class="tab__icon">
|
||||
<a-badge v-if="item.routeName === 'cart'" :count="cartCount">
|
||||
<Icon :icon="item.icon" size="24" />
|
||||
</a-badge>
|
||||
<Icon v-else :icon="item.icon" size="24" />
|
||||
</div>
|
||||
<div class="text-xs mt-1 font-bold">{{ item.label }}</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -61,3 +65,19 @@ const primaryItems = computed(() => getPrimaryNavItems(isToken()))
|
||||
const leftTabs = computed(() => primaryItems.value.slice(0, 2))
|
||||
const rightTabs = computed(() => primaryItems.value.slice(2))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tab__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.tab__icon :deep(.ant-badge) {
|
||||
color: inherit;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
<Transition>
|
||||
<div
|
||||
v-if="show"
|
||||
class="top-0 bottom-0 bg-black wrap-fix z-2000 opacity-60"
|
||||
class="top-0 bottom-0 bg-black wrap-fix z-5000 opacity-60"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</Transition>
|
||||
<Transition name="slide-fade">
|
||||
<div v-if="show" class="wrap-fix bottom-11vh z-2000">
|
||||
<div v-if="show" class="wrap-fix bottom-11vh z-5000 flex justify-center items-end">
|
||||
<div class="relative flex flex-col items-center">
|
||||
<div
|
||||
class="flex items-center justify-center bg-white rounded-full w-[30px] h-[30px] mb-2"
|
||||
@@ -37,6 +37,17 @@
|
||||
<div>دریافت طلای فیزیکی</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<BasicButton
|
||||
v-if="authenticated"
|
||||
icon-size="16"
|
||||
class="!font-bold w-15rem"
|
||||
@click="go('notifyTrigger')"
|
||||
>
|
||||
<div class="flex gap-2 items-center">
|
||||
<BasicIcon icon="vuesax-linear:bill" size="16" class="absolute right-2" />
|
||||
<div>هشدار قیمت</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<BasicButton
|
||||
v-if="authenticated"
|
||||
icon-size="16"
|
||||
@@ -48,17 +59,17 @@
|
||||
<div>خرید محصول</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<BasicButton
|
||||
v-if="authenticated"
|
||||
icon-size="16"
|
||||
class="!font-bold w-15rem"
|
||||
@click="go('list')"
|
||||
>
|
||||
<div class="flex gap-2 items-center">
|
||||
<BasicIcon icon="vuesax-linear:receipt-item" size="16" class="absolute right-2" />
|
||||
<div>لیست دریافتی ها</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<!-- <BasicButton-->
|
||||
<!-- v-if="authenticated"-->
|
||||
<!-- icon-size="16"-->
|
||||
<!-- class="!font-bold w-15rem"-->
|
||||
<!-- @click="go('list')"-->
|
||||
<!-- >-->
|
||||
<!-- <div class="flex gap-2 items-center">-->
|
||||
<!-- <BasicIcon icon="vuesax-linear:receipt-item" size="16" class="absolute right-2" />-->
|
||||
<!-- <div>لیست دریافتی ها</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </BasicButton>-->
|
||||
<BasicButton
|
||||
v-if="authenticated"
|
||||
icon-size="16"
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
.bottom-appbar {
|
||||
z-index: var(--app-z-nav, 5000);
|
||||
height: var(--app-nav-height, 80px);
|
||||
position: fixed;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
background: #fff;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
height: calc(var(--app-nav-height, 80px) + var(--app-safe-bottom, env(safe-area-inset-bottom, 0px)));
|
||||
padding-bottom: var(--app-safe-bottom, env(safe-area-inset-bottom, 0px));
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
background: #fff;
|
||||
|
||||
@media screen and (min-width: 426px) {
|
||||
width: 426px;
|
||||
@@ -39,6 +40,16 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
|
||||
&.text-primary .tab__icon,
|
||||
&.text-primary .tab__icon .ant-badge {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
&.text-gray-400 .tab__icon,
|
||||
&.text-gray-400 .tab__icon .ant-badge {
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
|
||||
.main_in {
|
||||
@@ -121,19 +132,38 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.mobile-app-layout {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-with-bottom-chrome {
|
||||
min-height: calc(100vh - var(--app-bottom-chrome-height, 80px));
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-with-bottom-chrome-inner {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
min-height: min(100%, calc(100vh - var(--app-bottom-chrome-height, 80px)));
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
.main-with-bottom-chrome-inner--contained {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-with-bottom-chrome--no-nav,
|
||||
.main-with-bottom-chrome--no-nav .main-with-bottom-chrome-inner {
|
||||
min-height: 100vh;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,42 @@
|
||||
/**
|
||||
* Mobile shell width rules.
|
||||
* Below 426px: full viewport (phones).
|
||||
* 426px–767px: centered 426px frame (tablet-ish mobile).
|
||||
*/
|
||||
|
||||
html:not(.app-layout--desktop) {
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
min-height: 0;
|
||||
height: 100dvh;
|
||||
max-height: 100dvh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
.desktopConfig,
|
||||
.app-main {
|
||||
min-height: 0;
|
||||
height: 100dvh;
|
||||
max-height: 100dvh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main > * {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-dock-add-bar,
|
||||
.wallet-card-slider {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 425.98px) {
|
||||
body {
|
||||
display: block !important;
|
||||
@@ -8,6 +46,7 @@ html:not(.app-layout--desktop) {
|
||||
|
||||
#app,
|
||||
.desktopConfig,
|
||||
.app-main,
|
||||
.main-with-bottom-chrome,
|
||||
.main-with-bottom-chrome-inner {
|
||||
width: 100% !important;
|
||||
@@ -25,6 +64,16 @@ html:not(.app-layout--desktop) {
|
||||
margin: 0 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.app-dock-add-bar,
|
||||
.wallet-card-slider {
|
||||
width: 100% !important;
|
||||
max-width: 100vw !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
margin: 0 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 426px) and (max-width: 767.98px) {
|
||||
@@ -47,5 +96,15 @@ html:not(.app-layout--desktop) {
|
||||
margin: 0 !important;
|
||||
transform: translateX(-50%) !important;
|
||||
}
|
||||
|
||||
.app-dock-add-bar,
|
||||
.wallet-card-slider {
|
||||
width: 426px !important;
|
||||
max-width: 426px !important;
|
||||
left: 50% !important;
|
||||
right: auto !important;
|
||||
margin: 0 !important;
|
||||
transform: translateX(-50%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
/* Shared mobile page chrome (inner pages — not home .app-header) */
|
||||
|
||||
:root {
|
||||
--mpage-header-bar: 2.75rem;
|
||||
--mpage-header-offset: calc(4rem + var(--app-safe-top, env(safe-area-inset-top, 0px)));
|
||||
--mpage-subheader-offset: calc(var(--mpage-header-bar) + var(--app-safe-top, env(safe-area-inset-top, 0px)));
|
||||
--mpage-content-with-subheader: calc(6.5rem + var(--app-safe-top, env(safe-area-inset-top, 0px)));
|
||||
--mpage-scroll-max-height: calc(100dvh - 50px - var(--app-safe-top, env(safe-area-inset-top, 0px)));
|
||||
--mobile-info-support-block: calc(5rem + var(--app-safe-bottom, env(safe-area-inset-bottom, 0px)));
|
||||
}
|
||||
|
||||
/* Fixed top bar: background fills under status bar; content sits below safe area */
|
||||
.mobile-top-chrome {
|
||||
top: 0;
|
||||
box-sizing: border-box;
|
||||
padding-top: calc(0.75rem + var(--app-safe-top, env(safe-area-inset-top, 0px)));
|
||||
}
|
||||
|
||||
.mobile-top-chrome.h-12,
|
||||
.mobile-top-chrome.min-h-12 {
|
||||
height: auto;
|
||||
min-height: calc(3rem + var(--app-safe-top, env(safe-area-inset-top, 0px)));
|
||||
}
|
||||
|
||||
.mpage-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
top: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 0.75rem;
|
||||
padding-top: calc(0.75rem + var(--app-safe-top, env(safe-area-inset-top, 0px)));
|
||||
background: #fff;
|
||||
color: @primary-color;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.5;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
|
||||
z-index: 1999;
|
||||
|
||||
&__back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
&__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__trailing {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mpage-subheader {
|
||||
top: var(--mpage-subheader-offset);
|
||||
z-index: 1999;
|
||||
padding: 0.75rem;
|
||||
background: #fff;
|
||||
color: @primary-color;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mpage-content {
|
||||
padding-top: var(--mpage-header-offset);
|
||||
}
|
||||
|
||||
.mpage-content--with-subheader {
|
||||
padding-top: var(--mpage-content-with-subheader);
|
||||
}
|
||||
|
||||
.mpage-scroll {
|
||||
overflow-y: auto;
|
||||
max-height: var(--mpage-scroll-max-height);
|
||||
}
|
||||
|
||||
.mobile-page-body--pt10 {
|
||||
padding-top: calc(2.5rem + var(--app-safe-top, env(safe-area-inset-top, 0px)));
|
||||
}
|
||||
|
||||
.mobile-page-body--pt15 {
|
||||
padding-top: calc(3.75rem + var(--app-safe-top, env(safe-area-inset-top, 0px)));
|
||||
}
|
||||
|
||||
.mobile-page-body--pt20 {
|
||||
padding-top: calc(5rem + var(--app-safe-top, env(safe-area-inset-top, 0px)));
|
||||
}
|
||||
|
||||
/* Info pages (about, educate): fixed shell, scroll inside content box only */
|
||||
.mobile-info-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-info-page__header {
|
||||
z-index: 1999;
|
||||
}
|
||||
|
||||
/* Decorative page background — fixed to viewport, never scrolls on iOS */
|
||||
.mobile-page-bg,
|
||||
.mobile-info-page__bg {
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
z-index: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: var(--app-bottom-chrome-height, 80px);
|
||||
overflow: hidden;
|
||||
max-width: 100vw;
|
||||
box-sizing: border-box;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-page-bg--no-nav,
|
||||
.mobile-info-page__bg {
|
||||
bottom: var(--app-credit-footer-offset, 0px);
|
||||
}
|
||||
|
||||
.mobile-page-bg--cover {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
html:not(.app-layout--desktop) {
|
||||
@media screen and (min-width: 426px) and (max-width: 767.98px) {
|
||||
.mobile-page-bg,
|
||||
.mobile-info-page__bg {
|
||||
width: 426px;
|
||||
max-width: 426px;
|
||||
left: 50%;
|
||||
right: auto;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-info-page__main {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding-top: calc(var(--mpage-header-offset) + 1rem);
|
||||
padding-right: 2.5rem;
|
||||
padding-left: 2.5rem;
|
||||
padding-bottom: calc(
|
||||
var(--mobile-info-support-block, 5rem) + var(--app-credit-footer-offset, 0px) + 1.25rem
|
||||
);
|
||||
pointer-events: none;
|
||||
|
||||
> * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-info-page__support {
|
||||
bottom: var(--app-credit-footer-offset, 0px);
|
||||
box-sizing: border-box;
|
||||
padding: 1rem 2.5rem;
|
||||
padding-bottom: calc(1rem + var(--app-safe-bottom, env(safe-area-inset-bottom, 0px)));
|
||||
background: #fff;
|
||||
box-shadow: 0 -2px 8px rgba(15, 23, 42, 0.06);
|
||||
z-index: 1998;
|
||||
}
|
||||
|
||||
.mobile-content-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.mobile-content-box__scroll {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
+3
-13
@@ -85,22 +85,12 @@ const router = createRouter({
|
||||
name: 'address',
|
||||
component: () => import('@/views/base/address/index.vue')
|
||||
},
|
||||
/* {
|
||||
path: 'physicalReceive',
|
||||
name: 'physicalReceive',
|
||||
component: () => import('@/views/base/physicalReceive/index.vue'),
|
||||
},*/
|
||||
// {
|
||||
// path: 'list',
|
||||
// name: 'list',
|
||||
// component: () => import('@/views/base/physicalReceive/components/RequestList.vue'),
|
||||
// },
|
||||
{
|
||||
path: 'physicalReceive',
|
||||
name: 'physicalReceive',
|
||||
redirect:{name: 'list'},
|
||||
// component: () => import('@/views/base/physicalReceive/index.vue'),
|
||||
children:[
|
||||
redirect: { name: 'list' },
|
||||
component: () => import('@/views/base/physicalReceive/index.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'list',
|
||||
|
||||
@@ -1,24 +1,53 @@
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const DRAG_THRESHOLD = 6
|
||||
const POSITION_VERSION = 1
|
||||
|
||||
type FabPosition = { x: number; y: number }
|
||||
|
||||
type SavedFabOffset = {
|
||||
version: number
|
||||
dx: number
|
||||
dy: number
|
||||
}
|
||||
|
||||
type DraggableFloatingOptions = {
|
||||
width?: number
|
||||
height?: number
|
||||
defaultBottomOffset?: number
|
||||
defaultBottomGap?: number
|
||||
defaultRightOffset?: number
|
||||
}
|
||||
|
||||
function readBottomChromeHeight(): number {
|
||||
const styles = getComputedStyle(document.documentElement)
|
||||
const direct = parseFloat(styles.getPropertyValue('--app-bottom-chrome-height'))
|
||||
if (!Number.isNaN(direct) && direct > 0) {
|
||||
return direct
|
||||
}
|
||||
|
||||
const nav = parseFloat(styles.getPropertyValue('--app-nav-height')) || 80
|
||||
const creditOffset = parseFloat(styles.getPropertyValue('--app-credit-footer-offset'))
|
||||
if (!Number.isNaN(creditOffset) && creditOffset > 0) {
|
||||
return nav + creditOffset
|
||||
}
|
||||
|
||||
const probe = document.createElement('div')
|
||||
probe.style.cssText = 'position:fixed;visibility:hidden;pointer-events:none;height:var(--app-bottom-chrome-height)'
|
||||
document.body.appendChild(probe)
|
||||
const measured = probe.getBoundingClientRect().height
|
||||
document.body.removeChild(probe)
|
||||
|
||||
return measured > 0 ? measured : nav
|
||||
}
|
||||
|
||||
export function useDraggableFloatingButton(
|
||||
storageKey: string,
|
||||
options: DraggableFloatingOptions = {}
|
||||
) {
|
||||
const width = options.width ?? 96
|
||||
const height = options.height ?? 44
|
||||
const defaultBottomOffset = options.defaultBottomOffset ?? 92
|
||||
const defaultRightOffset = options.defaultRightOffset ?? 20
|
||||
const defaultBottomGap = options.defaultBottomGap ?? 12
|
||||
const defaultRightOffset = options.defaultRightOffset ?? 16
|
||||
|
||||
const pos = ref<FabPosition>({ x: 0, y: 0 })
|
||||
const dragging = ref(false)
|
||||
@@ -44,26 +73,40 @@ export function useDraggableFloatingButton(
|
||||
function getHorizontalOffset() {
|
||||
const vw = window.innerWidth
|
||||
const contentWidth = getViewportWidth()
|
||||
|
||||
return Math.max(0, (vw - contentWidth) / 2)
|
||||
}
|
||||
|
||||
function resetToDefault() {
|
||||
function getDefaultPosition(): FabPosition {
|
||||
const vh = window.innerHeight
|
||||
const offsetX = getHorizontalOffset()
|
||||
const contentWidth = getViewportWidth()
|
||||
const bottomChrome = readBottomChromeHeight()
|
||||
|
||||
pos.value = {
|
||||
return {
|
||||
x: offsetX + contentWidth - width - defaultRightOffset,
|
||||
y: vh - defaultBottomOffset - height
|
||||
y: vh - bottomChrome - defaultBottomGap - height
|
||||
}
|
||||
}
|
||||
|
||||
function applySavedOffset(dx: number, dy: number) {
|
||||
const base = getDefaultPosition()
|
||||
pos.value = {
|
||||
x: base.x + dx,
|
||||
y: base.y + dy
|
||||
}
|
||||
clampToViewport()
|
||||
}
|
||||
|
||||
function clampToViewport() {
|
||||
const vw = window.innerWidth
|
||||
const vh = window.innerHeight
|
||||
const offsetX = getHorizontalOffset()
|
||||
const contentWidth = getViewportWidth()
|
||||
const minX = offsetX
|
||||
const maxX = offsetX + contentWidth - width
|
||||
|
||||
pos.value = {
|
||||
x: Math.max(0, Math.min(vw - width, pos.value.x)),
|
||||
x: Math.max(minX, Math.min(maxX, pos.value.x)),
|
||||
y: Math.max(0, Math.min(vh - height, pos.value.y))
|
||||
}
|
||||
}
|
||||
@@ -72,25 +115,56 @@ export function useDraggableFloatingButton(
|
||||
try {
|
||||
const raw = localStorage.getItem(storageKey)
|
||||
if (!raw) return false
|
||||
const parsed = JSON.parse(raw) as FabPosition
|
||||
if (typeof parsed?.x === 'number' && typeof parsed?.y === 'number') {
|
||||
pos.value = parsed
|
||||
clampToViewport()
|
||||
|
||||
const parsed = JSON.parse(raw) as SavedFabOffset
|
||||
if (
|
||||
parsed?.version === POSITION_VERSION
|
||||
&& typeof parsed.dx === 'number'
|
||||
&& typeof parsed.dy === 'number'
|
||||
) {
|
||||
applySavedOffset(parsed.dx, parsed.dy)
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function savePosition() {
|
||||
localStorage.setItem(storageKey, JSON.stringify(pos.value))
|
||||
const base = getDefaultPosition()
|
||||
const payload: SavedFabOffset = {
|
||||
version: POSITION_VERSION,
|
||||
dx: pos.value.x - base.x,
|
||||
dy: pos.value.y - base.y
|
||||
}
|
||||
localStorage.setItem(storageKey, JSON.stringify(payload))
|
||||
}
|
||||
|
||||
function resetToDefault() {
|
||||
pos.value = getDefaultPosition()
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
clampToViewport()
|
||||
savePosition()
|
||||
try {
|
||||
const raw = localStorage.getItem(storageKey)
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw) as SavedFabOffset
|
||||
if (
|
||||
parsed?.version === POSITION_VERSION
|
||||
&& typeof parsed.dx === 'number'
|
||||
&& typeof parsed.dy === 'number'
|
||||
) {
|
||||
applySavedOffset(parsed.dx, parsed.dy)
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
resetToDefault()
|
||||
}
|
||||
|
||||
function handlePointerDown(event: PointerEvent) {
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="bg-white wrap-fix z-200001 py-3 px-3 text-primary font-bold drop-shadow flex">
|
||||
<div class="cursor-pointer" @click="router.back()">
|
||||
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
||||
</div>
|
||||
<div class="mr-2">درباره ما</div>
|
||||
</div>
|
||||
<div class="pt-20 pb-24">
|
||||
<MobilePageHeader title="درباره ما" show-back />
|
||||
<div class="mpage-content pb-[calc(var(--app-bottom-chrome-height)+1rem)]">
|
||||
<div class="px-5">
|
||||
<AboutSectionsContent variant="mobile" />
|
||||
</div>
|
||||
@@ -16,8 +11,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import router from '@/router'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
|
||||
import AboutSectionsContent from '../components/AboutSectionsContent.vue'
|
||||
import AboutSupportActions from '../components/AboutSupportActions.vue'
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="absolute inset-0 bg-cover bg-center opacity-60 z--1" :style="{ backgroundImage: `url(${img})` }" />
|
||||
<div class="bg-white drop-shadow z-[1999] wrap-fix">
|
||||
<div class="py-3 px-3 font-bold flex items-center justify-between">
|
||||
<div class="mpage-header wrap-fix z-[1999]">
|
||||
<div class="py-3 px-3 font-bold flex items-center justify-between w-full">
|
||||
<div>سبد خرید</div>
|
||||
<div v-if="price" class="flex gap-1 items-center text-xs flex-1 justify-end">
|
||||
<span class="text-gray-600 text-xs">قیمت خرید از کشکول گلد:</span>
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
|
||||
<GoldLoading v-model:show="infoVisible" title="" />
|
||||
<div class="px-5 pt-26 pb-30" :class="loading ? 'h-[60vh]' : ''">
|
||||
<div class="px-5 mpage-content pb-[calc(var(--app-bottom-chrome-height)+1rem)]" :class="loading ? 'h-[60vh]' : ''">
|
||||
<template v-if="loading">
|
||||
<TransactionSkeleton v-for="i in 3" :key="i" class="mt-4" />
|
||||
</template>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import type { WalletMetalTotal } from '@/models/walletAssets'
|
||||
import { fundPageInventoryCards, formatMetalTotal as buildMetalTotalLabel } from '@/utils/productAssets'
|
||||
import { getLastPrices } from '@/utils/price'
|
||||
import img from '@/assets/img/cash_img.svg'
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import empty from '@/assets/img/cash_empty.svg'
|
||||
|
||||
export interface FundPageContext {
|
||||
@@ -80,7 +80,7 @@ export function useFundPage(_isDesktop: Ref<boolean>) {
|
||||
countBasedInventory,
|
||||
fundHeaderBackground: img,
|
||||
fundEmptyImage: empty,
|
||||
fundEmptyText: 'هنوز محصولی خریداری نکردهاید',
|
||||
fundEmptyText: 'یک گنج جدید بساز!',
|
||||
formatMetalTotal,
|
||||
openFundInfo,
|
||||
goBack,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="fund-page-root">
|
||||
<div class="fund-page-root main-with-bottom-chrome-inner--contained">
|
||||
<DesktopFundView v-show="isDesktop" />
|
||||
<MobileFundView v-show="!isDesktop" />
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="bg-white w-full py-3 px-3 text-black font-bold drop-shadow fixed md:absolute top-0 flex h-12 z-20">
|
||||
<div @click="goBack()">
|
||||
<div class="fund-mobile-root">
|
||||
<div
|
||||
class="fund-mobile-root__header bg-white w-full mobile-top-chrome px-3 pb-3 text-primary font-bold drop-shadow flex min-h-12 z-20"
|
||||
>
|
||||
<div class="cursor-pointer" @click="goBack()">
|
||||
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
||||
</div>
|
||||
<div class="mr-2">دارایی ها</div>
|
||||
</div>
|
||||
<div class="bg-[#EFCED9] w-screen md:w-full h-[70vh] fixed md:absolute overflow-hidden">
|
||||
|
||||
<div class="fund-mobile-root__hero text-tertiary bg-primary">
|
||||
<img
|
||||
:src="fundHeaderBackground"
|
||||
alt=""
|
||||
class="mt-[-4rem] mr-10 max-w-[22rem] self-end pointer-events-none absolute bottom-0 left-0 opacity-20"
|
||||
class="fund-mobile-root__hero-bg"
|
||||
/>
|
||||
<div class="pt-15 flex flex-col relative z-10">
|
||||
<div class="text-gray-700 font-bold mr-5 z-20">موجودی دارایی ها</div>
|
||||
<div class="fund-mobile-root__hero-content flex flex-col">
|
||||
<div class="font-bold mr-5 z-20">موجودی دارایی ها</div>
|
||||
|
||||
<div
|
||||
v-if="meta?.has_count_based_products"
|
||||
@@ -59,14 +62,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-[20rem]">
|
||||
<div
|
||||
class="bg-gray-100 w-screen md:w-full z-20 relative rounded-t-3xl h-[calc(100vh-20rem-70px)] overflow-y-auto pb-15"
|
||||
style="box-shadow: 0px -2px 4px -2px #0000001a"
|
||||
>
|
||||
<div class="fund-mobile-root__scroll">
|
||||
<div class="fund-mobile-root__panel bg-gray-100 rounded-t-3xl">
|
||||
<div
|
||||
v-if="inventoryCards.length === 0 && !loading"
|
||||
class="w-full h-full flex justify-center items-center"
|
||||
class="w-full min-h-[40vh] flex justify-center items-center"
|
||||
>
|
||||
<div>
|
||||
<img :src="fundEmptyImage" alt="" class="w-70" />
|
||||
@@ -112,3 +112,60 @@ const {
|
||||
goBack,
|
||||
} = useFundPageContext()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fund-mobile-root {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fund-mobile-root__header {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.fund-mobile-root__hero {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
overflow: hidden;
|
||||
padding-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.fund-mobile-root__hero-bg {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
opacity: 0.05;
|
||||
}
|
||||
|
||||
.fund-mobile-root__hero-content {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding: 1rem 0 0;
|
||||
}
|
||||
|
||||
.fund-mobile-root__scroll {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
margin-top: -2rem;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
.fund-mobile-root__panel {
|
||||
min-height: 100%;
|
||||
padding-bottom: calc(var(--app-bottom-chrome-height, 80px) + 1rem);
|
||||
box-shadow: 0 -2px 4px -2px #0000001a;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
type="text"
|
||||
/>
|
||||
</a-form-item>
|
||||
<p :class="variant === 'desktop' ? 'dgift-form__hint' : 'font-bold text-xs mt-1'">
|
||||
<p :class="variant === 'desktop' ? 'dgift-form__hint' : 'gift-card-form__hint'">
|
||||
کد کارت هدیه خود را در فیلد بالا وارد کنید و سپس دکمه ثبت را بزنید.
|
||||
</p>
|
||||
<p :class="variant === 'desktop' ? 'dgift-form__case-note' : 'font-bold text-xs mt-3'">
|
||||
<p :class="variant === 'desktop' ? 'dgift-form__case-note' : 'gift-card-form__case-note'">
|
||||
*حساس به حروف بزرگ و کوچک
|
||||
</p>
|
||||
<a-form-item>
|
||||
@@ -39,3 +39,24 @@ withDefaults(defineProps<{ variant?: 'mobile' | 'desktop' }>(), { variant: 'mobi
|
||||
|
||||
const { form, disableButton, registerGiftCard } = useGiftCardPageContext()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gift-card-form__hint,
|
||||
.gift-card-form__case-note {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.65;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.gift-card-form__hint {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.gift-card-form__case-note {
|
||||
margin-top: 0.75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,48 +1,26 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="bg-white py-3 px-3 text-black font-bold drop-shadow z-[1999] wrap-fix">کارت هدیه</div>
|
||||
<div class="px-5 pt-26 pb-2">
|
||||
<a-form :model="form" class="!mt-5" name="basic" @finish="registerGiftCard">
|
||||
<div class="border-gray-200 border-1 rounded-xl p-5 trans h-40 relative">
|
||||
<bg_main />
|
||||
<div class="z-100 relative">
|
||||
<a-form-item :rules="[required()]" label="کد کارت هدیه" name="code">
|
||||
<BasicInput
|
||||
v-model:value="form.code"
|
||||
place-holder="کد کارت هدیه را وارد کنید"
|
||||
size="large"
|
||||
type="text"
|
||||
/>
|
||||
</a-form-item>
|
||||
<div class="font-bold text-xs mt-1">
|
||||
کد کارت هدیه خود را در فیلد بالا وارد کنید و سپس دکمه ثبت را بزنید.
|
||||
</div>
|
||||
<div class="font-bold text-xs mt-3">*حساس به حروف بزرگ و کوچک</div>
|
||||
</div>
|
||||
<div class="gift-card-mobile">
|
||||
<MobilePageHeader title="کارت هدیه" show-back />
|
||||
|
||||
<div class="gift-card-mobile__content mpage-content px-5 pb-[calc(var(--app-bottom-chrome-height)+1rem)]">
|
||||
<div class="gift-card-mobile__card border-gray-200 border-1 rounded-xl p-5 relative overflow-hidden">
|
||||
<bg_main />
|
||||
<div class="relative z-10">
|
||||
<GiftCardForm variant="mobile" />
|
||||
</div>
|
||||
<a-form-item>
|
||||
<BasicButton
|
||||
:disabled="disableButton"
|
||||
class="w-full py-2 mt-5"
|
||||
html-type="submit"
|
||||
type="primary"
|
||||
>
|
||||
<a-spin v-if="loading" size="small" />
|
||||
<div v-else>ثبت کد</div>
|
||||
</BasicButton>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { loading } from '@/utils'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import Bg_main from '@/components/bg_main.vue'
|
||||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||
import { required } from '@/utils/inputRules'
|
||||
import { useGiftCardPageContext } from '../composables/useGiftCardPage'
|
||||
|
||||
const { form, disableButton, registerGiftCard } = useGiftCardPageContext()
|
||||
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
|
||||
import GiftCardForm from '../components/GiftCardForm.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gift-card-mobile__card {
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="h-35rem overflow-y-auto">
|
||||
<div class="h-35rem overflow-y-auto px-5">
|
||||
<div class=" !overflow-y-scroll bg-white py-3 text-primary font-bold flex justify-evenly">
|
||||
<!-- <div @click="isBuy = false" class="w-full text-center" :class="!isBuy?'text-primary':'text-gray-400'">خرید طلا</div>
|
||||
<div @click="isBuy = true" class="w-full text-center" :class="isBuy?'text-primary':'text-gray-400'">فروش طلا</div>-->
|
||||
@@ -25,11 +25,14 @@
|
||||
<div class="w-full items-center flex flex-col">
|
||||
<div class="text-gray-700 font-bold">{{ !isBuy ? 'قیمت فروش' : 'قیمت خرید' }}</div>
|
||||
<div class="flex gap-1 mt-3 text-xs items-center">
|
||||
<Vue3Odometer
|
||||
:value="displayedGoldPrice"
|
||||
:class="[!isBuy ? 'text-red-700' : 'text-green-700', 'o-text !text-2xl font-bold color-transition']"
|
||||
style="font-family: iran-sans; direction: ltr"
|
||||
/>
|
||||
<div class="gold-live-price">
|
||||
<Vue3Odometer
|
||||
:key="isBuy ? 'buy' : 'sell'"
|
||||
:value="displayedGoldPrice"
|
||||
:class="[!isBuy ? 'text-red-700' : 'text-green-700', 'o-text !text-2xl font-bold color-transition']"
|
||||
style="font-family: iran-sans; direction: ltr"
|
||||
/>
|
||||
</div>
|
||||
ریال
|
||||
</div>
|
||||
</div>
|
||||
@@ -626,13 +629,10 @@ const formatter = (value: number) => {
|
||||
|
||||
const price = computed((): Price => <Price>lastPrice.value ?? 0)
|
||||
|
||||
const displayedGoldPrice = ref<number>(0)
|
||||
|
||||
watch([() => lastPrice.value, isBuy], () => {
|
||||
if (lastPrice.value) {
|
||||
displayedGoldPrice.value = isBuy.value ? (lastPrice.value.buy ?? 0) : (lastPrice.value.sell ?? 0)
|
||||
}
|
||||
}, { immediate: true })
|
||||
const displayedGoldPrice = computed(() => {
|
||||
if (!lastPrice.value) return 0
|
||||
return isBuy.value ? (lastPrice.value.buy ?? 0) : (lastPrice.value.sell ?? 0)
|
||||
})
|
||||
|
||||
async function initPrice() {
|
||||
try {
|
||||
@@ -640,9 +640,6 @@ async function initPrice() {
|
||||
await setIntervalGetAllPrice()
|
||||
loading.value = true
|
||||
price.value = lastPrice.value
|
||||
if (lastPrice.value) {
|
||||
displayedGoldPrice.value = isBuy.value ? (lastPrice.value.buy ?? 0) : (lastPrice.value.sell ?? 0)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
@@ -719,4 +716,14 @@ async function ok() {
|
||||
right: 100%;
|
||||
transform: translate(100%, 0);
|
||||
}
|
||||
|
||||
.gold-live-price {
|
||||
:deep(.odometer) {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
:deep(.odometer-digit) {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div
|
||||
ref="headerRef"
|
||||
class="app-header px-4 pb-2 top-0 wrap-fix background-color-transition"
|
||||
:class="{ 'app-header--scrolled': isHeaderScrolled }"
|
||||
>
|
||||
<div class="flex justify-between my-2 items-center">
|
||||
<div class="flex items-center gap-2" @click.stop="router.push({ name: 'user' })">
|
||||
<div class="z-100000 rounded-full border-1 text-primary border-primary">
|
||||
<Icon icon="vuesax-linear:user" size="20" />
|
||||
</div>
|
||||
<div class="text-primary color-transition font-bold">
|
||||
{{ userStore.userInfo?.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="mhome-header-btn mhome-header-btn--logout"
|
||||
@click="$emit('logout')"
|
||||
>
|
||||
<Icon icon="vuesax-linear:logout" size="18" />
|
||||
<span>خروج</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onActivated, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import router from '@/router'
|
||||
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
const headerRef = ref<HTMLElement | null>(null)
|
||||
const isHeaderScrolled = ref(false)
|
||||
const route = useRoute()
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
let intersectionObserver: IntersectionObserver | null = null
|
||||
let scrollTarget: HTMLElement | Window | null = null
|
||||
|
||||
function isScrollableOverflow(value: string) {
|
||||
return value === 'auto' || value === 'scroll' || value === 'overlay'
|
||||
}
|
||||
|
||||
function findScrollParent(el: HTMLElement | null): HTMLElement | null {
|
||||
let node = el?.parentElement ?? null
|
||||
|
||||
while (node) {
|
||||
const style = getComputedStyle(node)
|
||||
const overflowY = style.overflowY
|
||||
const overflow = style.overflow
|
||||
|
||||
if (isScrollableOverflow(overflowY) || isScrollableOverflow(overflow)) {
|
||||
return node
|
||||
}
|
||||
|
||||
node = node.parentElement
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function resolveScrollRoot(): HTMLElement | null {
|
||||
const header = headerRef.value
|
||||
if (!header) return null
|
||||
|
||||
const chromeInner = header.closest('.main-with-bottom-chrome-inner')
|
||||
if (chromeInner instanceof HTMLElement) {
|
||||
return chromeInner
|
||||
}
|
||||
|
||||
const fromParent = findScrollParent(header)
|
||||
if (fromParent) {
|
||||
return fromParent
|
||||
}
|
||||
|
||||
const global = document.querySelector('.main-with-bottom-chrome-inner')
|
||||
return global instanceof HTMLElement ? global : null
|
||||
}
|
||||
|
||||
function resolveScrollSentinel(scrollRoot: HTMLElement | null): HTMLElement | null {
|
||||
if (!scrollRoot) return null
|
||||
|
||||
const local = scrollRoot.querySelector('.app-header-scroll-sentinel')
|
||||
if (local instanceof HTMLElement) {
|
||||
return local
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function updateScrollStateFromPosition() {
|
||||
if (!scrollTarget) return
|
||||
|
||||
const next =
|
||||
scrollTarget instanceof Window
|
||||
? scrollTarget.scrollY > 50
|
||||
: scrollTarget.scrollTop > 50
|
||||
|
||||
isHeaderScrolled.value = next
|
||||
}
|
||||
|
||||
function teardownScrollTracking() {
|
||||
intersectionObserver?.disconnect()
|
||||
intersectionObserver = null
|
||||
|
||||
scrollTarget?.removeEventListener('scroll', updateScrollStateFromPosition)
|
||||
scrollTarget = null
|
||||
}
|
||||
|
||||
function setupScrollTracking() {
|
||||
teardownScrollTracking()
|
||||
|
||||
const scrollRoot = resolveScrollRoot()
|
||||
const sentinel = resolveScrollSentinel(scrollRoot)
|
||||
|
||||
if (scrollRoot && sentinel && typeof IntersectionObserver !== 'undefined') {
|
||||
intersectionObserver = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
isHeaderScrolled.value = !entry?.isIntersecting
|
||||
},
|
||||
{
|
||||
root: scrollRoot,
|
||||
threshold: 0,
|
||||
rootMargin: '-1px 0px 0px 0px',
|
||||
},
|
||||
)
|
||||
intersectionObserver.observe(sentinel)
|
||||
return
|
||||
}
|
||||
|
||||
scrollTarget = scrollRoot ?? window
|
||||
scrollTarget.addEventListener('scroll', updateScrollStateFromPosition, { passive: true })
|
||||
updateScrollStateFromPosition()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
setupScrollTracking()
|
||||
await nextTick()
|
||||
setupScrollTracking()
|
||||
requestAnimationFrame(() => setupScrollTracking())
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
isHeaderScrolled.value = false
|
||||
setupScrollTracking()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
if (route.name === 'home') {
|
||||
isHeaderScrolled.value = false
|
||||
nextTick(() => setupScrollTracking())
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
teardownScrollTracking()
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
logout: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-header {
|
||||
z-index: 1999;
|
||||
box-sizing: border-box;
|
||||
padding-top: calc(0.5rem + var(--app-safe-top, env(safe-area-inset-top, 0px)));
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.app-header--scrolled {
|
||||
background-color: rgba(255, 255, 255, 0.92);
|
||||
box-shadow:
|
||||
0 1px 3px 0 rgb(0 0 0 / 0.1),
|
||||
0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.mhome-header-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #0a1b37;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mhome-header-btn--logout {
|
||||
color: #dc2626;
|
||||
border-color: #fecaca;
|
||||
background: #fef2f2;
|
||||
}
|
||||
</style>
|
||||
@@ -5,7 +5,8 @@
|
||||
@logout="showLogoutConfirm = true"
|
||||
@open-balance="openWholeBalanceVisible()"
|
||||
/>
|
||||
<div v-show="!isDesktop" class="mt-20">
|
||||
<div v-show="!isDesktop" class="home-mobile-root pb-[calc(var(--app-bottom-chrome-height)+1rem)]">
|
||||
<div class="app-header-scroll-sentinel" aria-hidden="true" />
|
||||
<div
|
||||
v-if="!loading"
|
||||
class="bg-white w-full md:w-auto h-fit px-5 m-[-1px]"
|
||||
@@ -326,7 +327,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=" w-full flex justify-evenly rounded-lg py-2 home_appear_fade_in mt-6 pb-25">
|
||||
<div class=" w-full flex justify-evenly rounded-lg py-2 home_appear_fade_in mt-6 pb-[calc(var(--app-bottom-chrome-height)+1rem)]">
|
||||
<div class="text-center w-full">
|
||||
<div>بالاترین قیمت امروز</div>
|
||||
<div v-if="lastPrice" class="flex flex-col items-center mt-3">
|
||||
@@ -354,32 +355,7 @@
|
||||
<div v-else class="bg-white w-full md:w-auto h-[80vh] px-5 m-[-1px] flex justify-center">
|
||||
<HomeSkeleton class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isDesktop"
|
||||
:class="y > 50 ? 'bg-white shadow opacity-95' : ''"
|
||||
class="px-4 py-2 top-0 z-5 wrap-fix background-color-transition"
|
||||
>
|
||||
<div class="flex justify-between my-2">
|
||||
<div class="flex items-center gap-2" @click.stop="router.push({ name: 'user' })">
|
||||
<!-- <img :src="testimg" alt="" class="w-[24px] h-[24px]">-->
|
||||
<div
|
||||
class="z-100000 rounded-full border-1"
|
||||
:class="y > 50 ? '' : 'border-black'"
|
||||
>
|
||||
<Icon icon="vuesax-linear:user" size="20" />
|
||||
</div>
|
||||
<div :class="y > 50 ? '' : ''" class="color-transition font-bold">
|
||||
{{ userStore.userInfo?.name }}
|
||||
</div>
|
||||
</div>
|
||||
<Icon
|
||||
:color="y > 50 ? '' : ''"
|
||||
class="color-transition"
|
||||
icon="vuesax-linear:logout"
|
||||
@click="showLogoutConfirm = true"
|
||||
/>
|
||||
</div>
|
||||
<AppHeader @logout="showLogoutConfirm = true" />
|
||||
</div>
|
||||
<DialogVant v-model:show="showLogoutConfirm" :closeOnClickOverlay="false" :timeOut="-1">
|
||||
<!-- <template v-slot:img>
|
||||
@@ -485,7 +461,7 @@ import { usePrice, usePriceWithoutType, useTowDecimal } from '../../../utils/use
|
||||
import {computed, createVNode, onActivated, onMounted, onUnmounted, ref, watch} from 'vue'
|
||||
import router from '@/router'
|
||||
import * as echarts from 'echarts'
|
||||
import { useIntersectionObserver, useWindowScroll } from '@vueuse/core'
|
||||
import { useIntersectionObserver } from '@vueuse/core'
|
||||
import { loading, removeToken } from '@/utils'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { useWalletStore } from '@/store/walletStore'
|
||||
@@ -517,6 +493,7 @@ import { message, Modal } from 'ant-design-vue'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import AppHeader from '@/views/base/home/components/AppHeader.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const walletStore = useWalletStore()
|
||||
@@ -524,7 +501,6 @@ const isDesktop = useDesktopBreakpoint()
|
||||
const chartLoading = ref<boolean>(false)
|
||||
const openModal = ref<boolean>(false)
|
||||
const wholeBalanceVisible = ref<boolean>(false)
|
||||
const { x, y } = useWindowScroll()
|
||||
const series = ref<Chart[]>([{ data: [] }])
|
||||
const key = ref<number>(0)
|
||||
const main = ref(null)
|
||||
@@ -926,6 +902,20 @@ function copyCode() {
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.home-mobile-root {
|
||||
position: relative;
|
||||
padding-top: var(--app-home-header-offset, calc(4.25rem + env(safe-area-inset-top, 0px)));
|
||||
}
|
||||
|
||||
.app-header-scroll-sentinel {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.home-page-root.main-with-bottom-chrome-inner {
|
||||
scroll-padding-top: var(--app-home-header-offset, calc(4.25rem + env(safe-area-inset-top, 0px)));
|
||||
}
|
||||
|
||||
.home-banner {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,221 +1,14 @@
|
||||
<template>
|
||||
<div class="">
|
||||
<div ref="el" class="px-3 pt-15"
|
||||
>
|
||||
<div class="flex gap-2">
|
||||
<BasicSelect v-model:value="selectedProduct" :items="items"
|
||||
class="w-full"
|
||||
key-name="id"
|
||||
:show-search="false"
|
||||
value-name="title"
|
||||
size="large"
|
||||
placeholder="لطفا محصول را انتخاب کنید"
|
||||
:has-tooltip="true"/>
|
||||
<BasicButton :disabled-b="selectedProduct==null" type="primary" @click="handleAdd">افزودن</BasicButton>
|
||||
</div>
|
||||
<div class="h-70vh rounded-lg bg-white mt-2 border-gray-200 border-1 overflow-y-auto p-2">
|
||||
<div v-for="(val,index) in selectedProducts" class="rounded-lg p-2 bg-gray-100 mb-2">
|
||||
<div class="flex">
|
||||
<div class="bg-gray-100 m-2 rounded-lg w-28 h-28 flex justify-center p-2">
|
||||
<img v-if="val.product.group_type=='گوی'" :src="goyImg">
|
||||
<img v-if="val.product.group_type=='مکعب'" :src="mokaabImg">
|
||||
<img v-if="val.product.group_type=='سکه'" :src="sekkeImg">
|
||||
<img v-if="val.product.group_type=='شمش ونوس'" :src="venusImg">
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="flex justify-between">
|
||||
<div class="font-bold text-lg">
|
||||
{{ val.product.title }}
|
||||
</div>
|
||||
<BasicButton icon="vuesax-linear:trash" icon-class="text-red" @click="handleRemove(val.product.id)"/>
|
||||
</div>
|
||||
<div class="flex mt-3 justify-between items-center">
|
||||
<div>تعداد موجودی:</div>
|
||||
<div class="flex gap-1 items-center">
|
||||
<div class="font-bold">{{ val.product.wallet.gold }}</div>
|
||||
<div>{{ val.product.unit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center mt-2">
|
||||
<div>تعداد درخواستی:</div>
|
||||
<div class="flex gap-1 items-center w-40%">
|
||||
<BasicInput v-model:value="val.count" addon-before="عدد"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BasicButton :disabled-b="selectedProducts.length==0" class="w-full absolute bottom-10 mt-2"
|
||||
@click="handelFirstSubmit()">ثبت
|
||||
</BasicButton>
|
||||
</div>
|
||||
<!-- <ActionSheet v-model:show="editPassVisible">salam</ActionSheet>-->
|
||||
</div>
|
||||
<BasicActionSheet v-model:show="showAddress">
|
||||
<div class="h-120 flex flex-col ">
|
||||
<div class="text-center">
|
||||
انتخاب آدرس
|
||||
</div>
|
||||
<div class="overflow-y-scroll h-85">
|
||||
<ARadioGroup v-model:value="selectedAddressID">
|
||||
<div class="flex flex-col ">
|
||||
<div class="text-base rounded-lg p-4 m-2 shadow-md font-medium" v-for="item in address">
|
||||
<ARadio :value="item.id">{{ item.province }} / {{ item.city }} / {{ item.address }}</ARadio>
|
||||
</div>
|
||||
</div>
|
||||
</ARadioGroup>
|
||||
</div>
|
||||
<div class="flex gap-2 pt-2">
|
||||
|
||||
<BasicButton class=" " @click="showAddress=false;addAddressVisible=true">افزودن آدرس +</BasicButton>
|
||||
<BasicButton class="w-2/3 " type="primary" @click="handelsubmit">ثبت نهایی</BasicButton>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</BasicActionSheet>
|
||||
<BasicActionSheet v-model:show="addAddressVisible" >
|
||||
<div class=" ">
|
||||
<New_EditAddress @onOk="handleAddress"></New_EditAddress>
|
||||
</div>
|
||||
|
||||
</BasicActionSheet>
|
||||
<MobileNewRequestView v-if="!isDesktop" />
|
||||
<DesktopNewRequestView v-if="isDesktop" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useNewPhysicalRequestPage } from '../composables/useNewPhysicalRequestPage'
|
||||
import { usePhysicalReceiveShellContext } from '../composables/usePhysicalReceiveShell'
|
||||
import DesktopNewRequestView from '../desktop/DesktopNewRequestView.vue'
|
||||
import MobileNewRequestView from '../mobile/MobileNewRequestView.vue'
|
||||
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import router from '@/router'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BasicSelect from '@/components/BasicPacks/BasicSelect.vue'
|
||||
import {onMounted, ref} from 'vue'
|
||||
import {loading} from '@/utils'
|
||||
import {getProductInventory} from '@/api/products'
|
||||
import type {CountPInventory, PInventory} from '@/models/products'
|
||||
import goyImg from '@/assets/img/goyImg.svg'
|
||||
import mokaabImg from '@/assets/img/mokaabImg.svg'
|
||||
import sekkeImg from '@/assets/img/sekkeImg.svg'
|
||||
import venusImg from "@/assets/img/venousImg.svg"
|
||||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||
import {cloneDeep} from 'lodash'
|
||||
import BasicActionSheet from "@/components/BasicPacks/BasicActionSheet.vue"
|
||||
import {getCity, tableAddress} from "@/api/address"
|
||||
import type {Address} from "@/models/address"
|
||||
import Item from "@/views/base/address/components/Item.vue"
|
||||
import {physicalReceiveApi} from "@/api/physicalreceive"
|
||||
import {message} from "ant-design-vue"
|
||||
import {required} from "@/utils/inputRules"
|
||||
import BasicTextArea from "@/components/BasicPacks/BasicTextArea.vue"
|
||||
import New_EditAddress from "@/views/base/address/components/New_EditAddress.vue"
|
||||
|
||||
const items = ref<PInventory[]>()
|
||||
const baseItems = ref<PInventory[]>()
|
||||
const selectedAddressID = ref<number>()
|
||||
const selectedProduct = ref<number>()
|
||||
const productsByWalletId = ref<Array>([])
|
||||
const address = ref<Address>()
|
||||
const selectedProducts = ref<CountPInventory[]>([])
|
||||
const showAddress = ref<boolean>(false)
|
||||
const addAddressVisible = ref<boolean>(false)
|
||||
|
||||
function handleAdd() {
|
||||
console.log(selectedProducts.value)
|
||||
selectedProducts.value?.push({
|
||||
count: 0,
|
||||
product: items.value?.find(x => x.id === selectedProduct.value)
|
||||
})
|
||||
items.value?.splice(items.value?.findIndex(x => x.id === selectedProduct.value), 1)
|
||||
selectedProduct.value = undefined
|
||||
}
|
||||
function handleAddress() {
|
||||
getAddress()
|
||||
addAddressVisible.value = false
|
||||
showAddress.value = true
|
||||
}
|
||||
|
||||
function handleRemove(id: number) {
|
||||
const findItem = baseItems.value?.find(x => x.id === id)
|
||||
console.log(baseItems.value, id)
|
||||
if (findItem) {
|
||||
items.value?.push(findItem)
|
||||
}
|
||||
selectedProducts.value?.splice(selectedProducts.value?.findIndex(x => x.product.id === id), 1)
|
||||
}
|
||||
|
||||
async function initProductWallet() {
|
||||
try {
|
||||
loading.value = true
|
||||
items.value = (await getProductInventory()).rows.filter(x => (x.title !== 'آبشده نقدی' && x.wallet?.gold && x.wallet?.gold > 0))
|
||||
baseItems.value = cloneDeep(items.value)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handelFirstSubmit() {
|
||||
try {
|
||||
const zeroAmount = selectedProducts.value.map(item => item.count <= 0)
|
||||
if (zeroAmount.includes(true)) {
|
||||
message.error(`لطفاً تعداد درخواستی محصول را وارد کنید.`)
|
||||
}
|
||||
else {
|
||||
showAddress.value = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function handelsubmit() {
|
||||
try {
|
||||
loading.value = true
|
||||
productsByWalletId.value = selectedProducts.value.map((val) => {
|
||||
return {
|
||||
wallet_id: val.product.wallet.id,
|
||||
value: val.count
|
||||
}
|
||||
})
|
||||
await physicalReceiveApi({
|
||||
address_id: selectedAddressID.value,
|
||||
wallets: productsByWalletId.value
|
||||
}
|
||||
)
|
||||
await initProductWallet()
|
||||
message.success("درخواست شما با موفقیت ارسال شد")
|
||||
selectedProducts.value = []
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
showAddress.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await initProductWallet()
|
||||
await getAddress()
|
||||
})
|
||||
async function getAddress(){
|
||||
let {rows} = await tableAddress({page: 0, limit: 150})
|
||||
address.value = rows
|
||||
}
|
||||
|
||||
const { isDesktop } = usePhysicalReceiveShellContext()
|
||||
useNewPhysicalRequestPage()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/*.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}*/
|
||||
/*.trans{
|
||||
transition: height 1s;
|
||||
}*/
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="h-120 flex flex-col">
|
||||
<div class="text-center">انتخاب آدرس</div>
|
||||
<div class="overflow-y-scroll h-85">
|
||||
<ARadioGroup v-model:value="selectedAddressID">
|
||||
<div class="flex flex-col">
|
||||
<div
|
||||
v-for="addr in addresses"
|
||||
:key="addr.id"
|
||||
class="text-base rounded-lg p-4 m-2 shadow-md font-medium"
|
||||
>
|
||||
<ARadio :value="addr.id">
|
||||
{{ addr.province }} / {{ addr.city }} / {{ addr.address }}
|
||||
</ARadio>
|
||||
</div>
|
||||
</div>
|
||||
</ARadioGroup>
|
||||
</div>
|
||||
<div class="flex gap-2 pt-2">
|
||||
<BasicButton @click="emit('addAddress')">افزودن آدرس +</BasicButton>
|
||||
<BasicButton class="w-2/3" type="primary" @click="emit('submit')">
|
||||
ثبت نهایی
|
||||
</BasicButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import type { Address } from '@/models/address'
|
||||
|
||||
interface Props {
|
||||
addresses?: Address[]
|
||||
selectedAddressId?: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
addAddress: []
|
||||
submit: []
|
||||
'update:selectedAddressId': [value: number | undefined]
|
||||
}>()
|
||||
|
||||
const selectedAddressID = computed({
|
||||
get: () => props.selectedAddressId,
|
||||
set: (value: number | undefined) => emit('update:selectedAddressId', value),
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex">
|
||||
<div class="bg-gray-100 m-2 rounded-lg w-18 h-18 flex justify-center p-1 shrink-0">
|
||||
<img :src="displayImage" class="object-contain w-full h-full" alt="" />
|
||||
</div>
|
||||
<div class="w-full min-w-0">
|
||||
<div class="pr-cart-row__head">
|
||||
<div class="truncate text-base font-bold">{{ item.product.title }}</div>
|
||||
<BasicButton
|
||||
icon="vuesax-linear:trash"
|
||||
icon-class="text-red"
|
||||
class="pr-cart-row__trash shrink-0"
|
||||
@click="emit('remove', item.product.id)"
|
||||
/>
|
||||
<div class="pr-cart-row__stock text-xs">
|
||||
<span>مقدار موجودی</span>
|
||||
<span class="font-bold">{{ item.product.wallet.gold }}</span>
|
||||
<span>{{ item.product.unit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex justify-between items-center gap-2">
|
||||
<div class="shrink-0">تعداد درخواستی:</div>
|
||||
<div class="flex items-center gap-1 pr-cart-row__input">
|
||||
<BasicInput
|
||||
v-model:value="item.count"
|
||||
:addon-before="item.product.unit"
|
||||
addon-bg-color="#a72b77"
|
||||
addon-text-color="white"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||
import type { CountPInventory } from '@/models/products'
|
||||
import { getProductDisplayImage } from '@/utils/productAssets'
|
||||
|
||||
interface Props {
|
||||
item: CountPInventory
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const displayImage = computed(() =>
|
||||
getProductDisplayImage({
|
||||
group_type: props.item.product.group_type,
|
||||
title: props.item.product.title,
|
||||
file: props.item.product.file,
|
||||
}),
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
remove: [id: number]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.pr-cart-row__head {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-rows: auto auto;
|
||||
column-gap: 0.5rem;
|
||||
row-gap: 0.125rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.pr-cart-row__trash {
|
||||
grid-column: 2;
|
||||
grid-row: 1 / span 2;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.pr-cart-row__stock {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.pr-cart-row__input {
|
||||
width: auto;
|
||||
max-width: 10rem;
|
||||
|
||||
:deep(.noname-input),
|
||||
:deep(.ant-input-group-wrapper) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-input-group-addon) {
|
||||
min-width: 3.75rem;
|
||||
padding-inline: 6px;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,59 +1,18 @@
|
||||
<template>
|
||||
<div class="physical-receive-page-root">
|
||||
<DesktopPhysicalReceiveListView
|
||||
v-show="isDesktop"
|
||||
:request-items="requestItems"
|
||||
@add="openAdd"
|
||||
/>
|
||||
<div v-show="!isDesktop">
|
||||
<div class="absolute inset-0 bg-cover bg-center opacity-60" :style="{ backgroundImage: `url(${img})` }" />
|
||||
<div class="bg-white w-full transaction-list py-3 px-3 font-bold drop-shadow flex justify-between z-10">
|
||||
<div>لیست دریافت فیزیکی</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 pt-13 pb-35 px-2">
|
||||
<RequestItem v-for="item in requestItems" :key="item.id" :item="item" />
|
||||
</div>
|
||||
<Transition mode="out-in" name="fade">
|
||||
<div
|
||||
class="wrap-fix bottom-0 h-[19vh] bg-[#FBFBFC] shadow-lg rounded-3xl flex justify-center pt-3 z-200"
|
||||
style="box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 26px 11px"
|
||||
@click="openAdd"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:add" />
|
||||
<div class="cursor-pointer">افزودن درخواست جدید</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
<MobileRequestListView v-show="!isDesktop" />
|
||||
<DesktopPhysicalReceiveListView
|
||||
v-show="isDesktop"
|
||||
:request-items="requestItems"
|
||||
@add="openAdd"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getPhysicalReceiveList } from '@/api/physicalreceive'
|
||||
import type { Physicalreceive } from '@/models/physicalreceive'
|
||||
import RequestItem from '@/views/base/physicalReceive/components/RequestItem.vue'
|
||||
import router from '@/router'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { usePhysicalReceiveList } from '../composables/usePhysicalReceiveList'
|
||||
import { usePhysicalReceiveShellContext } from '../composables/usePhysicalReceiveShell'
|
||||
import DesktopPhysicalReceiveListView from '../desktop/DesktopPhysicalReceiveListView.vue'
|
||||
import MobileRequestListView from '../mobile/MobileRequestListView.vue'
|
||||
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
const requestItems = ref<Physicalreceive[]>([])
|
||||
|
||||
const openAdd = () => {
|
||||
router.push({ name: 'new' })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const { rows } = await getPhysicalReceiveList()
|
||||
requestItems.value = rows
|
||||
})
|
||||
const { isDesktop } = usePhysicalReceiveShellContext()
|
||||
const { requestItems, openAdd } = usePhysicalReceiveList()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.physical-receive-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { inject, nextTick, onMounted, provide, ref, type InjectionKey, type Ref } from 'vue'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { message } from 'ant-design-vue'
|
||||
import router from '@/router'
|
||||
import { getProductInventory } from '@/api/products'
|
||||
import { physicalReceiveApi } from '@/api/physicalreceive'
|
||||
import { tableAddress } from '@/api/address'
|
||||
import type { CountPInventory, PInventory } from '@/models/products'
|
||||
import type { Address } from '@/models/address'
|
||||
import { loading } from '@/utils'
|
||||
import { usePhysicalReceiveShellContext } from './usePhysicalReceiveShell'
|
||||
|
||||
export interface NewPhysicalRequestContext {
|
||||
isDesktop: Ref<boolean>
|
||||
items: Ref<PInventory[] | undefined>
|
||||
selectedProduct: Ref<number | undefined>
|
||||
selectedProducts: Ref<CountPInventory[]>
|
||||
address: Ref<Address[] | undefined>
|
||||
selectedAddressID: Ref<number | undefined>
|
||||
showAddress: Ref<boolean>
|
||||
addAddressVisible: Ref<boolean>
|
||||
handleAdd: () => void
|
||||
handleRemove: (id: number) => void
|
||||
handelFirstSubmit: () => Promise<void>
|
||||
handelsubmit: () => Promise<void>
|
||||
handleAddress: () => void
|
||||
}
|
||||
|
||||
export const NEW_PHYSICAL_REQUEST_KEY: InjectionKey<NewPhysicalRequestContext> =
|
||||
Symbol('newPhysicalRequest')
|
||||
|
||||
export function useNewPhysicalRequestPage() {
|
||||
const { isDesktop } = usePhysicalReceiveShellContext()
|
||||
|
||||
const items = ref<PInventory[]>()
|
||||
const baseItems = ref<PInventory[]>()
|
||||
const selectedAddressID = ref<number>()
|
||||
const selectedProduct = ref<number>()
|
||||
const productsByWalletId = ref<Array<{ wallet_id: number; value: number }>>([])
|
||||
const address = ref<Address[]>()
|
||||
const selectedProducts = ref<CountPInventory[]>([])
|
||||
const showAddress = ref(false)
|
||||
const addAddressVisible = ref(false)
|
||||
|
||||
function handleAdd() {
|
||||
const product = items.value?.find((x) => x.id === selectedProduct.value)
|
||||
if (!product) return
|
||||
|
||||
selectedProducts.value.push({
|
||||
count: 0,
|
||||
product,
|
||||
})
|
||||
items.value?.splice(items.value.findIndex((x) => x.id === selectedProduct.value), 1)
|
||||
selectedProduct.value = undefined
|
||||
}
|
||||
|
||||
function handleAddress() {
|
||||
getAddress()
|
||||
addAddressVisible.value = false
|
||||
showAddress.value = true
|
||||
}
|
||||
|
||||
function handleRemove(id: number) {
|
||||
const findItem = baseItems.value?.find((x) => x.id === id)
|
||||
if (findItem) {
|
||||
items.value?.push(findItem)
|
||||
}
|
||||
selectedProducts.value.splice(
|
||||
selectedProducts.value.findIndex((x) => x.product.id === id),
|
||||
1,
|
||||
)
|
||||
}
|
||||
|
||||
async function initProductWallet() {
|
||||
try {
|
||||
loading.value = true
|
||||
const inventory = (await getProductInventory()).rows.filter(
|
||||
(x) => x.title !== 'آبشده نقدی' && x.wallet?.gold && x.wallet.gold > 0,
|
||||
)
|
||||
items.value = inventory
|
||||
baseItems.value = cloneDeep(inventory)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handelFirstSubmit() {
|
||||
try {
|
||||
const zeroAmount = selectedProducts.value.map((item) => item.count <= 0)
|
||||
if (zeroAmount.includes(true)) {
|
||||
message.error('لطفاً تعداد درخواستی محصول را وارد کنید.')
|
||||
} else {
|
||||
showAddress.value = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function handelsubmit() {
|
||||
if (!selectedAddressID.value) {
|
||||
message.error('لطفاً آدرس را انتخاب کنید.')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
productsByWalletId.value = selectedProducts.value.map((val) => ({
|
||||
wallet_id: val.product.wallet.id,
|
||||
value: val.count,
|
||||
}))
|
||||
await physicalReceiveApi({
|
||||
address_id: selectedAddressID.value,
|
||||
wallets: productsByWalletId.value,
|
||||
})
|
||||
|
||||
showAddress.value = false
|
||||
selectedAddressID.value = undefined
|
||||
selectedProducts.value = []
|
||||
await nextTick()
|
||||
|
||||
message.success('درخواست شما با موفقیت ارسال شد')
|
||||
await initProductWallet()
|
||||
await router.push({ name: 'list' })
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function getAddress() {
|
||||
const { rows } = await tableAddress({ page: 0, limit: 150 })
|
||||
address.value = rows
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await initProductWallet()
|
||||
await getAddress()
|
||||
})
|
||||
|
||||
const context: NewPhysicalRequestContext = {
|
||||
isDesktop,
|
||||
items,
|
||||
selectedProduct,
|
||||
selectedProducts,
|
||||
address,
|
||||
selectedAddressID,
|
||||
showAddress,
|
||||
addAddressVisible,
|
||||
handleAdd,
|
||||
handleRemove,
|
||||
handelFirstSubmit,
|
||||
handelsubmit,
|
||||
handleAddress,
|
||||
}
|
||||
|
||||
provide(NEW_PHYSICAL_REQUEST_KEY, context)
|
||||
return context
|
||||
}
|
||||
|
||||
export function useNewPhysicalRequestContext() {
|
||||
const context = inject(NEW_PHYSICAL_REQUEST_KEY)
|
||||
if (!context) {
|
||||
throw new Error('useNewPhysicalRequestContext must be used within useNewPhysicalRequestPage')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { inject, onMounted, provide, ref, type InjectionKey, type Ref } from 'vue'
|
||||
import { getPhysicalReceiveList } from '@/api/physicalreceive'
|
||||
import type { Physicalreceive } from '@/models/physicalreceive'
|
||||
import router from '@/router'
|
||||
|
||||
export interface PhysicalReceiveListContext {
|
||||
requestItems: Ref<Physicalreceive[]>
|
||||
openAdd: () => void
|
||||
}
|
||||
|
||||
export const PHYSICAL_RECEIVE_LIST_KEY: InjectionKey<PhysicalReceiveListContext> =
|
||||
Symbol('physicalReceiveList')
|
||||
|
||||
export function usePhysicalReceiveList() {
|
||||
const requestItems = ref<Physicalreceive[]>([])
|
||||
|
||||
function openAdd() {
|
||||
router.push({ name: 'new' })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const { rows } = await getPhysicalReceiveList()
|
||||
requestItems.value = rows
|
||||
})
|
||||
|
||||
const context: PhysicalReceiveListContext = { requestItems, openAdd }
|
||||
provide(PHYSICAL_RECEIVE_LIST_KEY, context)
|
||||
return context
|
||||
}
|
||||
|
||||
export function usePhysicalReceiveListContext() {
|
||||
const context = inject(PHYSICAL_RECEIVE_LIST_KEY)
|
||||
if (!context) {
|
||||
throw new Error('usePhysicalReceiveListContext must be used within usePhysicalReceiveList')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { inject, provide, type InjectionKey } from 'vue'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
|
||||
export interface PhysicalReceiveShellContext {
|
||||
isDesktop: ReturnType<typeof useDesktopBreakpoint>
|
||||
}
|
||||
|
||||
export const PHYSICAL_RECEIVE_SHELL_KEY: InjectionKey<PhysicalReceiveShellContext> =
|
||||
Symbol('physicalReceiveShell')
|
||||
|
||||
export function usePhysicalReceiveShell() {
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
provide(PHYSICAL_RECEIVE_SHELL_KEY, { isDesktop })
|
||||
return { isDesktop }
|
||||
}
|
||||
|
||||
export function usePhysicalReceiveShellContext() {
|
||||
const context = inject(PHYSICAL_RECEIVE_SHELL_KEY)
|
||||
if (!context) {
|
||||
throw new Error('usePhysicalReceiveShellContext must be used within physical receive shell')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div class="dinner physical-receive-page">
|
||||
<div class="dinner__container">
|
||||
<DesktopPageTopbar title="درخواست جدید" show-back />
|
||||
|
||||
<div class="dinner-surface p-5 space-y-4">
|
||||
<div class="physical-new-request__picker-row">
|
||||
<div class="physical-new-request__select">
|
||||
<BasicSelect
|
||||
v-model:value="selectedProduct"
|
||||
:items="items"
|
||||
key-name="id"
|
||||
:show-search="false"
|
||||
value-name="title"
|
||||
size="large"
|
||||
placeholder="لطفا محصول را انتخاب کنید"
|
||||
:has-tooltip="false"
|
||||
/>
|
||||
</div>
|
||||
<BasicButton
|
||||
class="physical-new-request__add-btn"
|
||||
:disabled-b="selectedProduct == null"
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
>
|
||||
افزودن
|
||||
</BasicButton>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="val in selectedProducts"
|
||||
:key="val.product.id"
|
||||
class="rounded-lg p-2 bg-gray-100"
|
||||
>
|
||||
<PhysicalReceiveCartRow :item="val" @remove="handleRemove" />
|
||||
</div>
|
||||
<div v-if="selectedProducts.length === 0" class="text-center text-gray-500 py-8">
|
||||
محصولی به سبد اضافه نشده است.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BasicButton
|
||||
:disabled-b="selectedProducts.length == 0"
|
||||
class="w-full"
|
||||
type="primary"
|
||||
@click="handelFirstSubmit()"
|
||||
>
|
||||
ادامه و انتخاب آدرس
|
||||
</BasicButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BasicActionSheet v-model:show="showAddress">
|
||||
<PhysicalReceiveAddressPicker
|
||||
v-model:selected-address-id="selectedAddressID"
|
||||
:addresses="address"
|
||||
@add-address="addAddressVisible = true; showAddress = false"
|
||||
@submit="handelsubmit"
|
||||
/>
|
||||
</BasicActionSheet>
|
||||
|
||||
<BasicActionSheet v-model:show="addAddressVisible">
|
||||
<New_EditAddress v-if="addAddressVisible" :visible="addAddressVisible" @onOk="handleAddress" />
|
||||
</BasicActionSheet>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BasicSelect from '@/components/BasicPacks/BasicSelect.vue'
|
||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||
import DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue'
|
||||
import New_EditAddress from '@/views/base/address/components/New_EditAddress.vue'
|
||||
import PhysicalReceiveCartRow from '../components/PhysicalReceiveCartRow.vue'
|
||||
import PhysicalReceiveAddressPicker from '../components/PhysicalReceiveAddressPicker.vue'
|
||||
import { useNewPhysicalRequestContext } from '../composables/useNewPhysicalRequestPage'
|
||||
|
||||
const {
|
||||
items,
|
||||
selectedProduct,
|
||||
selectedProducts,
|
||||
address,
|
||||
selectedAddressID,
|
||||
showAddress,
|
||||
addAddressVisible,
|
||||
handleAdd,
|
||||
handleRemove,
|
||||
handelFirstSubmit,
|
||||
handelsubmit,
|
||||
handleAddress,
|
||||
} = useNewPhysicalRequestContext()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.physical-new-request__picker-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.physical-new-request__select {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.physical-new-request__select :deep(.ant-select) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.physical-new-request__select :deep(.ant-select-selector) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.physical-new-request__select :deep(.ant-select-selection-item),
|
||||
.physical-new-request__select :deep(.ant-select-selection-placeholder) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.physical-new-request__add-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,196 +1,67 @@
|
||||
<template>
|
||||
<div class="flex justify-center items-center text-5">
|
||||
در حال بروزرسانی...
|
||||
<div
|
||||
class="physical-receive-page-root main-with-bottom-chrome-inner--contained"
|
||||
:class="{ 'physical-receive-page-root--desktop': isDesktop }"
|
||||
>
|
||||
<MobilePhysicalReceiveChrome v-show="!isDesktop" />
|
||||
|
||||
<div :class="isDesktop ? '' : 'physical-receive-page-root__shell'">
|
||||
<div :class="isDesktop ? '' : 'physical-receive-page-root__content'">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!--<template>-->
|
||||
<!-- <div class="bottom-0 fixed top-0">-->
|
||||
<!-- <img :src="img" alt="" class=" w-inherit "-->
|
||||
<!-- :style="{ backgroundRepeat: 'repeat', }"-->
|
||||
<!-- />-->
|
||||
<!-- </div>-->
|
||||
<!-- <a-spin :spinning="loading">-->
|
||||
<script lang="ts" setup>
|
||||
import { usePhysicalReceiveShell } from './composables/usePhysicalReceiveShell'
|
||||
import MobilePhysicalReceiveChrome from './mobile/MobilePhysicalReceiveChrome.vue'
|
||||
|
||||
<!-- <div class=" !h-full overflow-y-scroll">-->
|
||||
const { isDesktop } = usePhysicalReceiveShell()
|
||||
</script>
|
||||
|
||||
<!-- <div class="w-full flex justify-center top-0 py-5" style="align-self: center">-->
|
||||
<!-- <ARadioGroup @change="changeTab" v-model:value="activekey" button-style="solid">-->
|
||||
<!-- <ARadioButton class="cats" value="همه">همه</ARadioButton>-->
|
||||
<!-- <ARadioButton class="cats" v-for="item in typeItems.filter(a => a != 'آبشده')" :value="item">{{ item }}</ARadioButton>-->
|
||||
<!-- </ARadioGroup>-->
|
||||
<!-- </div>-->
|
||||
<style scoped>
|
||||
.physical-receive-page-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
<!-- <div class="mx-5 pb-30 pt-2" style="color: #1b2b07">-->
|
||||
<!-- <div class="mt-3" v-for="item in items?.filter(a => a.group_type != 'آبشده')">-->
|
||||
<!-- <ProductItem :ProductItem="item" @click="clickProduct(item)"></ProductItem>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </a-spin>-->
|
||||
<!-- <!– <BasicActionSheet v-model:show="showFilter">–>-->
|
||||
<!-- <!– <indexGold ></indexGold>–>-->
|
||||
<!-- <!– </BasicActionSheet>–>-->
|
||||
.physical-receive-page-root__shell {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
<!-- <BasicActionSheet placement="bottom" v-model:show="showFilter">-->
|
||||
<!-- <div class="">-->
|
||||
<!--<!– <indexGold v-if="selectedProduct?.group_type=='آبشده'" :ProductItem="selectedProduct"></indexGold>–>-->
|
||||
<!-- <ReceiveDealProduct :ProductItem="selectedProduct"></ReceiveDealProduct>-->
|
||||
<!-- </div>-->
|
||||
<!-- </BasicActionSheet>-->
|
||||
.physical-receive-page-root__content {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
<!--</template>-->
|
||||
.physical-receive-page-root__content > * {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
<!--<script setup lang="ts">-->
|
||||
<!--import { onMounted, onUnmounted, ref, watch } from 'vue'-->
|
||||
<!--import router from '@/router'-->
|
||||
<!--import ProductItem from '@/views/base/products/components/ProductItem.vue'-->
|
||||
<!--import { getProducts } from '@/api/products'-->
|
||||
<!--import {-->
|
||||
<!-- getLastPrices,-->
|
||||
<!-- lastPrice,-->
|
||||
<!-- stopPriceInterval-->
|
||||
<!--} from '@/utils/price'-->
|
||||
<!--import { loading } from '@/utils'-->
|
||||
<!--import { Price } from '@/models/home'-->
|
||||
<!--import type { Product } from '@/models/products'-->
|
||||
<!--import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'-->
|
||||
<!--import { cloneDeep } from 'lodash'-->
|
||||
<!--import img from "@/assets/img/user_ly1.svg";-->
|
||||
<!--import ReceiveDealProduct from '@/views/base/physicalReceive/components/ReceiveDealProduct.vue'-->
|
||||
.physical-receive-page-root--desktop {
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!--const activekey = ref('آبشده')-->
|
||||
<!--const showFilter = ref()-->
|
||||
<!--const selectedProduct = ref<Product>()-->
|
||||
|
||||
<!--function clickProduct(item: Product) {-->
|
||||
<!-- if(item?.show){-->
|
||||
<!-- showFilter.value = true-->
|
||||
<!-- selectedProduct.value = cloneDeep(item)-->
|
||||
<!-- }-->
|
||||
<!--}-->
|
||||
|
||||
<!--function changeTab() {-->
|
||||
<!-- if (activekey.value != 'همه') {-->
|
||||
<!-- items.value = firstItems.value?.filter((val) =>-->
|
||||
<!-- val.group_type == activekey.value-->
|
||||
<!-- )-->
|
||||
<!-- } else items.value = firstItems.value-->
|
||||
|
||||
<!--}-->
|
||||
|
||||
<!--const firstItems = ref<Product[]>()-->
|
||||
<!--const price = ref<Price>()-->
|
||||
<!--const items = ref<Product[]>()-->
|
||||
<!--const typeItems = ref([])-->
|
||||
<!--onMounted(async () => {-->
|
||||
<!-- try {-->
|
||||
<!-- loading.value = true-->
|
||||
<!-- items.value = await getProducts()-->
|
||||
<!-- items.value?.forEach((val, index) => {-->
|
||||
<!-- if (!typeItems.value.includes(val.group_type)){-->
|
||||
<!-- typeItems.value.push(val.group_type)-->
|
||||
<!-- }-->
|
||||
<!-- // typeItems.value[index] = val.group_type-->
|
||||
<!-- })-->
|
||||
<!-- firstItems.value = items.value-->
|
||||
<!-- await initPrice()-->
|
||||
<!-- // await setIntervalGetLastPrice()-->
|
||||
<!-- // console.log('run product')-->
|
||||
|
||||
<!-- } catch (e) {-->
|
||||
<!-- console.log(e)-->
|
||||
<!-- } finally {-->
|
||||
<!-- loading.value = false-->
|
||||
<!-- if (router.currentRoute.value.query.cat=="coin"){-->
|
||||
<!-- activekey.value="سکه"-->
|
||||
<!-- changeTab()-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- if (router.currentRoute.value.query.state) {-->
|
||||
<!-- if (router.currentRoute.value.query.product) {-->
|
||||
<!-- clickProduct(items.value?.find(x => x.title === 'سکه'))-->
|
||||
<!-- } else {-->
|
||||
<!-- clickProduct(items.value?.find(x => x.title === 'آبشده نقدی'))-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
<!--})-->
|
||||
<!--onUnmounted(() => {-->
|
||||
<!-- stopPriceInterval()-->
|
||||
<!-- // console.log('stop product')-->
|
||||
<!--})-->
|
||||
|
||||
<!--async function initPrice() {-->
|
||||
<!-- try {-->
|
||||
<!-- // chartLoading.value = true-->
|
||||
<!-- await getLastPrices()-->
|
||||
<!-- // await setIntervalGetPrice()-->
|
||||
<!-- loading.value = true-->
|
||||
<!-- price.value = lastPrice.value-->
|
||||
<!-- } catch (e) {-->
|
||||
<!-- console.log(e)-->
|
||||
<!-- } finally {-->
|
||||
<!-- loading.value = false-->
|
||||
<!-- // chartLoading.value = false-->
|
||||
<!-- }-->
|
||||
<!--}-->
|
||||
<!--</script>-->
|
||||
|
||||
<!--<style lang="less">-->
|
||||
<!--.cats{-->
|
||||
<!-- font-size: x-small;-->
|
||||
<!--}-->
|
||||
<!--.cats:hover span {-->
|
||||
<!-- --un-text-opacity: 1;-->
|
||||
<!-- color: rgba(206, 163, 76, var(--un-text-opacity))!important;-->
|
||||
<!--}-->
|
||||
|
||||
<!--</style>-->
|
||||
|
||||
|
||||
<!--<!–-->
|
||||
<!--<template>-->
|
||||
<!-- <div style="max-height: calc(100vh - 50px)">-->
|
||||
<!-- <div class="bg-white py-3 px-3 z-100000 text-primary font-bold drop-shadow flex flex-col wrap-fix">-->
|
||||
<!-- <div class="flex">-->
|
||||
<!-- <div @click="router.back()">-->
|
||||
<!-- <BasicIcon icon="vuesax-linear:arrow-right-1"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div v-if="route.name=='list'" class="mr-2">دریافت طلای فیزیکی</div>-->
|
||||
<!-- <div v-else class="mr-2">دریافت طلای فیزیکی-درخواست جدید</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div>-->
|
||||
<!-- <router-view></router-view>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!--</template>-->
|
||||
|
||||
<!--<script lang="ts" setup>-->
|
||||
<!--import {ref} from "vue";-->
|
||||
<!--import RequestList from "@/views/base/physicalReceive/components/RequestList.vue";-->
|
||||
<!--import NewRequest from "@/views/base/physicalReceive/components/NewRequest.vue";-->
|
||||
<!--import router from "@/router";-->
|
||||
<!--import BasicIcon from "@/components/BasicPacks/BasicIcon.vue";-->
|
||||
<!--import {useRoute} from "vue-router";-->
|
||||
|
||||
<!--const activekey=ref(1)-->
|
||||
<!--const route=useRoute()-->
|
||||
<!--</script>-->
|
||||
|
||||
<!--<style scoped>-->
|
||||
<!--/*.fade-enter-active,-->
|
||||
<!--.fade-leave-active {-->
|
||||
<!-- transition: opacity 0.2s ease;-->
|
||||
<!--}-->
|
||||
|
||||
<!--.fade-enter-from,-->
|
||||
<!--.fade-leave-to {-->
|
||||
<!-- opacity: 0;-->
|
||||
<!--}*/-->
|
||||
<!--/*.trans{-->
|
||||
<!-- transition: height 1s;-->
|
||||
<!--}*/-->
|
||||
<!--</style>-->
|
||||
<!--–>-->
|
||||
<style lang="less">
|
||||
.physical-receive-page-root.main-with-bottom-chrome-inner {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="physical-new-request">
|
||||
<div class="physical-new-request__toolbar mpage-content">
|
||||
<div class="physical-new-request__picker-row px-3">
|
||||
<div class="physical-new-request__select">
|
||||
<BasicSelect
|
||||
v-model:value="selectedProduct"
|
||||
:items="items"
|
||||
key-name="id"
|
||||
:show-search="false"
|
||||
value-name="title"
|
||||
size="large"
|
||||
placeholder="لطفا محصول را انتخاب کنید"
|
||||
:has-tooltip="false"
|
||||
/>
|
||||
</div>
|
||||
<BasicButton
|
||||
class="physical-new-request__add-btn"
|
||||
:disabled-b="selectedProduct == null"
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
>
|
||||
افزودن
|
||||
</BasicButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="physical-new-request__items px-3">
|
||||
<div class="physical-new-request__items-scroll">
|
||||
<div
|
||||
v-for="val in selectedProducts"
|
||||
:key="val.product.id"
|
||||
class="rounded-lg p-2 bg-gray-100 mb-2 last:mb-0"
|
||||
>
|
||||
<PhysicalReceiveCartRow :item="val" @remove="handleRemove" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="physical-new-request__footer px-3">
|
||||
<BasicButton
|
||||
:disabled-b="selectedProducts.length == 0"
|
||||
class="w-full"
|
||||
@click="handelFirstSubmit()"
|
||||
>
|
||||
ثبت
|
||||
</BasicButton>
|
||||
</div>
|
||||
|
||||
<BasicActionSheet v-model:show="showAddress">
|
||||
<PhysicalReceiveAddressPicker
|
||||
v-model:selected-address-id="selectedAddressID"
|
||||
:addresses="address"
|
||||
@add-address="addAddressVisible = true; showAddress = false"
|
||||
@submit="handelsubmit"
|
||||
/>
|
||||
</BasicActionSheet>
|
||||
|
||||
<BasicActionSheet v-model:show="addAddressVisible">
|
||||
<New_EditAddress v-if="addAddressVisible" :visible="addAddressVisible" @onOk="handleAddress" />
|
||||
</BasicActionSheet>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BasicSelect from '@/components/BasicPacks/BasicSelect.vue'
|
||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||
import New_EditAddress from '@/views/base/address/components/New_EditAddress.vue'
|
||||
import PhysicalReceiveCartRow from '../components/PhysicalReceiveCartRow.vue'
|
||||
import PhysicalReceiveAddressPicker from '../components/PhysicalReceiveAddressPicker.vue'
|
||||
import { useNewPhysicalRequestContext } from '../composables/useNewPhysicalRequestPage'
|
||||
|
||||
const {
|
||||
items,
|
||||
selectedProduct,
|
||||
selectedProducts,
|
||||
address,
|
||||
selectedAddressID,
|
||||
showAddress,
|
||||
addAddressVisible,
|
||||
handleAdd,
|
||||
handleRemove,
|
||||
handelFirstSubmit,
|
||||
handelsubmit,
|
||||
handleAddress,
|
||||
} = useNewPhysicalRequestContext()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.physical-new-request {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: var(--app-bottom-chrome-height, 80px);
|
||||
}
|
||||
|
||||
.physical-new-request__toolbar {
|
||||
flex-shrink: 0;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.physical-new-request__picker-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.physical-new-request__select {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.physical-new-request__select :deep(.ant-select) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.physical-new-request__select :deep(.ant-select-selector) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.physical-new-request__select :deep(.ant-select-selection-item),
|
||||
.physical-new-request__select :deep(.ant-select-selection-placeholder) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.physical-new-request__add-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.physical-new-request__items {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.physical-new-request__items-scroll {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-y: contain;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.5rem;
|
||||
background: #fff;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.physical-new-request__footer {
|
||||
flex-shrink: 0;
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
box-shadow: 0 -2px 8px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<MobilePageHeader :title="pageTitle" show-back />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const pageTitle = computed(() =>
|
||||
route.name === 'list' ? 'دریافت فیزیکی' : 'دریافت فیزیکی-درخواست جدید',
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="flex flex-col gap-4 mpage-content px-2 pb-[calc(var(--app-bottom-chrome-height)+var(--app-add-card-height)+1rem)]"
|
||||
>
|
||||
<RequestItem v-for="item in requestItems" :key="item.id" :item="item" />
|
||||
<div
|
||||
v-if="requestItems.length === 0"
|
||||
class="flex flex-col items-center justify-center min-h-[40vh] text-gray-500"
|
||||
>
|
||||
هنوز درخواستی ثبت نشده است.
|
||||
</div>
|
||||
</div>
|
||||
<Teleport to="body">
|
||||
<div class="app-dock-add-bar" @click="openAdd()">
|
||||
<BasicIcon icon="vuesax-linear:add" />
|
||||
<div>افزودن درخواست جدید</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import RequestItem from '../components/RequestItem.vue'
|
||||
import { usePhysicalReceiveListContext } from '../composables/usePhysicalReceiveList'
|
||||
|
||||
const { requestItems, openAdd } = usePhysicalReceiveListContext()
|
||||
</script>
|
||||
@@ -1,90 +1,145 @@
|
||||
<template>
|
||||
<div @click="infoVisible = true" class="shadow mb-2 p-2 rounded-lg">
|
||||
<div v-if="variant === 'desktop'" class="dptr-card">
|
||||
<p class="dptr-card__price">{{ usePrice(item.target_price) }}</p>
|
||||
|
||||
<div class="dptr-card__meta">
|
||||
<div class="dptr-card__row">
|
||||
<span class="dptr-card__label">جهت قیمت:</span>
|
||||
<span class="dptr-card__value">{{ returnPriceCondition(item.price_condition) }}</span>
|
||||
</div>
|
||||
<div class="dptr-card__row">
|
||||
<span class="dptr-card__label">نوع هشدار:</span>
|
||||
<span class="dptr-card__value">{{ returnAction(item.action) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dptr-card__actions">
|
||||
<button
|
||||
type="button"
|
||||
class="dptr-card__icon-btn dptr-card__icon-btn--edit"
|
||||
@click.stop="emits('edit', item)"
|
||||
>
|
||||
<BasicIcon color="green" icon="vuesax-linear:edit" size="18" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="dptr-card__icon-btn dptr-card__icon-btn--danger"
|
||||
@click.stop="deleteConfirmVisible = true"
|
||||
>
|
||||
<BasicIcon color="red" icon="vuesax-linear:trash" size="18" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="dptr-card__footer">
|
||||
<span class="dptr-card__date">{{ toJalali(item.created_at) }}</span>
|
||||
<a-switch
|
||||
v-model:checked="isActive"
|
||||
checked-children="فعال"
|
||||
un-checked-children="غیر فعال"
|
||||
@change="changeActive"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogVant v-model:show="deleteConfirmVisible" :closeOnClickOverlay="false" :timeOut="-1" teleport="body">
|
||||
<template #title>
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="p-5 text-sm">آیا از حذف هشدار اطمینان دارید؟</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #button>
|
||||
<div class="text-center text-red flex items-center justify-center" @click="deleteItem">
|
||||
<a-spin :spinning="deleteLoading" size="small">تایید</a-spin>
|
||||
</div>
|
||||
<div class="text-center" @click="deleteConfirmVisible = false">انصراف</div>
|
||||
</template>
|
||||
</DialogVant>
|
||||
</div>
|
||||
|
||||
<div v-else @click="infoVisible = true" class="shadow mb-2 p-2 rounded-lg">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<div class="font-bold text-base mb-2">{{usePrice(item.target_price) }}</div>
|
||||
<div class="font-bold text-base mb-2">{{ usePrice(item.target_price) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 ">
|
||||
<BasicIcon color="red" icon="vuesax-linear:trash" @click.stop="deleteConfirmVisible = true" class="p-1"/>
|
||||
<BasicIcon color="green" icon="vuesax-linear:edit" @click.stop="emits('edit',item)" class="p-1"/>
|
||||
<div class="flex gap-2">
|
||||
<BasicIcon
|
||||
class="p-1"
|
||||
color="red"
|
||||
icon="vuesax-linear:trash"
|
||||
@click.stop="deleteConfirmVisible = true"
|
||||
/>
|
||||
<BasicIcon
|
||||
class="p-1"
|
||||
color="green"
|
||||
icon="vuesax-linear:edit"
|
||||
@click.stop="emits('edit', item)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 mt-2">
|
||||
<BasicInfoItem is-line title="جهت قیمت" :value="returnPriceCondition(item.price_condition)"/>
|
||||
|
||||
<BasicInfoItem is-line title="نوع هشدار" :value="returnAction(item.action)"/>
|
||||
<BasicInfoItem is-line title="جهت قیمت" :value="returnPriceCondition(item.price_condition)" />
|
||||
<BasicInfoItem is-line title="نوع هشدار" :value="returnAction(item.action)" />
|
||||
</div>
|
||||
<a-divider class="!my-3" />
|
||||
|
||||
<div class="flex justify-between mt-1">
|
||||
<div class="mb-2">{{toJalali(item.created_at)}}</div>
|
||||
<a-switch v-model:checked="isActive" checked-children="فعال" un-checked-children="غیر فعال" @change="changeActive"/>
|
||||
<div class="mb-2">{{ toJalali(item.created_at) }}</div>
|
||||
<a-switch
|
||||
v-model:checked="isActive"
|
||||
checked-children="فعال"
|
||||
un-checked-children="غیر فعال"
|
||||
@change="changeActive"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- <teleport to="app">-->
|
||||
<DialogVant v-model:show="deleteConfirmVisible" teleport="body" :closeOnClickOverlay="false" :timeOut="-1">
|
||||
<!-- <template v-slot:img>
|
||||
<img :src="registerDelayImage" alt="" class="opacity-80">
|
||||
</template>-->
|
||||
|
||||
<template v-slot:title>
|
||||
<DialogVant v-model:show="deleteConfirmVisible" :closeOnClickOverlay="false" :timeOut="-1" teleport="body">
|
||||
<template #title>
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="p-5 text-sm">
|
||||
آیا از حذف هشدار اطمینان دارید؟
|
||||
</div>
|
||||
<div class="p-5 text-sm">آیا از حذف هشدار اطمینان دارید؟</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:button>
|
||||
<template #button>
|
||||
<div class="text-center text-red flex items-center justify-center" @click="deleteItem">
|
||||
<a-spin :spinning="deleteLoading" size="small">
|
||||
تایید
|
||||
</a-spin>
|
||||
</div>
|
||||
<div class="text-center" @click="deleteConfirmVisible = false">
|
||||
انصراف
|
||||
<a-spin :spinning="deleteLoading" size="small">تایید</a-spin>
|
||||
</div>
|
||||
<div class="text-center" @click="deleteConfirmVisible = false">انصراف</div>
|
||||
</template>
|
||||
|
||||
</DialogVant>
|
||||
<!-- </teleport>-->
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { usePrice, usePriceWithoutType } from '../../../../utils/usePrice'
|
||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import { toJalali, toJalaliNotime, toJalaliOutOfUTC, toTime } from '../../../../utils/useDateTime'
|
||||
import { usePrice } from '../../../../utils/usePrice'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { toJalali } from '../../../../utils/useDateTime'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import type { PriceTrigger } from '@/models/PriceTrigger'
|
||||
import BasicInfoItem from '@/components/BasicPacks/BasicInfoItem.vue'
|
||||
import DialogVant from '@/components/DialogVant.vue'
|
||||
import { DotLottieVue } from '@lottiefiles/dotlottie-vue'
|
||||
import { deletePriceTrigger, updatePriceTrigger } from '@/api/triggerPrice'
|
||||
import { loading } from '@/utils'
|
||||
|
||||
const infoVisible = ref<boolean>(false)
|
||||
|
||||
const isActive = ref<boolean>(false)
|
||||
|
||||
const infoVisible = ref(false)
|
||||
const isActive = ref(false)
|
||||
const deleteLoading = ref(false)
|
||||
const deleteConfirmVisible = ref(false)
|
||||
|
||||
interface Prop {
|
||||
item: PriceTrigger
|
||||
}
|
||||
onMounted(()=> isActive.value = props.item.is_active)
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
item: PriceTrigger
|
||||
variant?: 'mobile' | 'desktop'
|
||||
}>(),
|
||||
{ variant: 'mobile' },
|
||||
)
|
||||
|
||||
const emits = defineEmits(['edit', 'refresh'])
|
||||
|
||||
onMounted(() => {
|
||||
isActive.value = props.item.is_active
|
||||
})
|
||||
|
||||
function changeActive(val: boolean) {
|
||||
console.log(val)
|
||||
try {
|
||||
loading.value = true
|
||||
updatePriceTrigger(props?.item.id, {is_active: val})
|
||||
updatePriceTrigger(props?.item.id, { is_active: val })
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
isActive.value = !isActive.value
|
||||
@@ -95,15 +150,13 @@ function changeActive(val: boolean) {
|
||||
|
||||
function returnPriceCondition(val: string) {
|
||||
switch (val) {
|
||||
case '==': return 'مساوی'
|
||||
case '<': return 'کوچکتر'
|
||||
case '>': return 'بزرگتر'
|
||||
case '==':
|
||||
return 'مساوی'
|
||||
case '<':
|
||||
return 'کوچکتر'
|
||||
case '>':
|
||||
return 'بزرگتر'
|
||||
}
|
||||
/* if (val === '=='){
|
||||
return 'مساوی'
|
||||
} else if (val === '<'){
|
||||
return 'کوچکتر'
|
||||
}*/
|
||||
}
|
||||
|
||||
async function deleteItem() {
|
||||
@@ -121,41 +174,12 @@ async function deleteItem() {
|
||||
|
||||
function returnAction(val: string) {
|
||||
switch (val) {
|
||||
case 'sms': return 'پیامک'
|
||||
case 'email': return 'ایمیل'
|
||||
case 'notify': return 'نوتیفیکیشن'
|
||||
// case '==': return 'مساوی'
|
||||
case 'sms':
|
||||
return 'پیامک'
|
||||
case 'email':
|
||||
return 'ایمیل'
|
||||
case 'notify':
|
||||
return 'نوتیفیکیشن'
|
||||
}
|
||||
/* if (val === '=='){
|
||||
return 'مساوی'
|
||||
} else if (val === '<'){
|
||||
return 'کوچکتر'
|
||||
}*/
|
||||
}
|
||||
|
||||
|
||||
const props = withDefaults(defineProps<Prop>(), {})
|
||||
const emits = defineEmits(['edit','refresh'])
|
||||
|
||||
const isUp = computed(() => props.item.type === 3 || props.item.type === 1 || props.item.type === 8)
|
||||
const isGold = computed(() => props.item.type === 1 || props.item.type === 2 || props.item.type === 10)
|
||||
/*const title = computed(() => {
|
||||
if (isUp.value) {
|
||||
if (isGold.value) {
|
||||
return 'واریز ریالی'
|
||||
} else {
|
||||
return 'خرید طلا'
|
||||
}
|
||||
} else {
|
||||
if (isGold.value) {
|
||||
return 'برداشت ریالی'
|
||||
} else {
|
||||
return 'فروش طلا'
|
||||
}
|
||||
}
|
||||
})*/
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
class="!mt-0"
|
||||
name="price-trigger-form"
|
||||
@finish="ok()"
|
||||
>
|
||||
<div class="flex flex-col gap-4 w-full">
|
||||
<a-form-item :rules="[required()]" label="جهت قیمت" name="price_condition">
|
||||
<BasicSelect
|
||||
v-model:value="form.price_condition"
|
||||
:items="typeItems"
|
||||
:show-search="false"
|
||||
key-name="key"
|
||||
placeholder="انتخاب نوع جهت قیمت"
|
||||
size="large"
|
||||
value-name="value"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :rules="[required()]" label="قیمت" name="target_price">
|
||||
<BasicInput
|
||||
v-model:value="form.target_price"
|
||||
addon-before="ریال"
|
||||
place-holder="وارد کردن قیمت"
|
||||
size="large"
|
||||
type="price"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :rules="[required()]" label="نوع هشدار را انتخاب کنید" name="action">
|
||||
<a-radio-group v-model:value="form.action">
|
||||
<a-radio :style="radioStyle" value="sms">ارسال پیامک</a-radio>
|
||||
<a-radio :style="radioStyle" value="email">ارسال ایمیل</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-spin :spinning="loading">
|
||||
<BasicButton class="w-full" html-type="submit" type="primary">
|
||||
{{ editId ? 'ویرایش هشدار' : 'ثبت هشدار' }}
|
||||
</BasicButton>
|
||||
</a-spin>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||
import BasicSelect from '@/components/BasicPacks/BasicSelect.vue'
|
||||
import { required } from '@/utils/inputRules'
|
||||
import { usePriceTriggerPageContext } from '../composables/usePriceTriggerPage'
|
||||
|
||||
const {
|
||||
form,
|
||||
formRef,
|
||||
editId,
|
||||
loading,
|
||||
radioStyle,
|
||||
typeItems,
|
||||
ok,
|
||||
} = usePriceTriggerPageContext()
|
||||
</script>
|
||||
@@ -0,0 +1,135 @@
|
||||
import { inject, onMounted, provide, reactive, ref, type InjectionKey, type Ref } from 'vue'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { type FormInstance, message } from 'ant-design-vue'
|
||||
import { createPriceTrigger, getPriceTriggers, updatePriceTrigger } from '@/api/triggerPrice'
|
||||
import type { SelectItem } from '@/models/SelectItem'
|
||||
import type { PriceTrigger } from '@/models/PriceTrigger'
|
||||
import { loading } from '@/utils'
|
||||
|
||||
export interface PriceTriggerPageContext {
|
||||
items: Ref<PriceTrigger[]>
|
||||
addTriggerVisible: Ref<boolean>
|
||||
editId: Ref<number | undefined>
|
||||
form: Ref<PriceTrigger>
|
||||
formRef: Ref<FormInstance | undefined>
|
||||
loading: typeof loading
|
||||
radioStyle: ReturnType<typeof reactive<{ display: string; height: string; lineHeight: string }>>
|
||||
typeItems: SelectItem[]
|
||||
resetFields: () => void
|
||||
openAdd: () => void
|
||||
openEdit: (val: PriceTrigger) => void
|
||||
initItems: () => Promise<void>
|
||||
ok: () => Promise<void>
|
||||
}
|
||||
|
||||
export const PRICE_TRIGGER_PAGE_CONTEXT_KEY: InjectionKey<PriceTriggerPageContext> =
|
||||
Symbol('priceTriggerPageContext')
|
||||
|
||||
export function usePriceTriggerPage() {
|
||||
const items = ref<PriceTrigger[]>([])
|
||||
const addTriggerVisible = ref(false)
|
||||
const editId = ref<number | undefined>(undefined)
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const radioStyle = reactive({
|
||||
display: '',
|
||||
height: '30px',
|
||||
lineHeight: '30px',
|
||||
})
|
||||
|
||||
const form = ref<PriceTrigger>({
|
||||
price_condition: '',
|
||||
is_active: true,
|
||||
action: '',
|
||||
target_price: '',
|
||||
})
|
||||
|
||||
const typeItems: SelectItem[] = [
|
||||
{ value: 'بزرگتر', key: '>' },
|
||||
{ value: 'کوچکتر', key: '<' },
|
||||
{ value: 'مساوی', key: '==' },
|
||||
]
|
||||
|
||||
function resetFields() {
|
||||
form.value = {
|
||||
price_condition: '',
|
||||
is_active: true,
|
||||
action: '',
|
||||
target_price: '',
|
||||
}
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
resetFields()
|
||||
editId.value = undefined
|
||||
addTriggerVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(val: PriceTrigger) {
|
||||
form.value = cloneDeep(val)
|
||||
editId.value = form.value.id
|
||||
addTriggerVisible.value = true
|
||||
}
|
||||
|
||||
async function initItems() {
|
||||
try {
|
||||
items.value = []
|
||||
items.value = await getPriceTriggers()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function ok() {
|
||||
await formRef.value?.validateFields().then(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
if (editId.value) {
|
||||
await updatePriceTrigger(editId.value, form.value)
|
||||
} else {
|
||||
await createPriceTrigger(form.value)
|
||||
}
|
||||
message.success('هشدار با موفقیت ایجاد شد')
|
||||
await initItems()
|
||||
addTriggerVisible.value = false
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initItems()
|
||||
})
|
||||
|
||||
const context: PriceTriggerPageContext = {
|
||||
items,
|
||||
addTriggerVisible,
|
||||
editId,
|
||||
form,
|
||||
formRef,
|
||||
loading,
|
||||
radioStyle,
|
||||
typeItems,
|
||||
resetFields,
|
||||
openAdd,
|
||||
openEdit,
|
||||
initItems,
|
||||
ok,
|
||||
}
|
||||
|
||||
provide(PRICE_TRIGGER_PAGE_CONTEXT_KEY, context)
|
||||
return context
|
||||
}
|
||||
|
||||
export function usePriceTriggerPageContext() {
|
||||
const context = inject(PRICE_TRIGGER_PAGE_CONTEXT_KEY)
|
||||
if (!context) {
|
||||
throw new Error('usePriceTriggerPageContext must be used within a page that calls usePriceTriggerPage()')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="dptr">
|
||||
<div class="dptr__container">
|
||||
<header class="dptr-topbar">
|
||||
<p class="dptr-topbar__eyebrow">اعلانها</p>
|
||||
<h1 class="dptr-topbar__title">هشدار قیمت</h1>
|
||||
<p class="dptr-topbar__subtitle">هنگام رسیدن قیمت به مقدار تعیینشده، از طریق پیامک یا ایمیل مطلع شوید</p>
|
||||
</header>
|
||||
|
||||
<div class="dptr-grid">
|
||||
<section class="dptr-surface dptr-form">
|
||||
<div class="dptr-form__header">
|
||||
<h2 class="dptr-form__title">{{ editId ? 'ویرایش هشدار' : 'ثبت هشدار جدید' }}</h2>
|
||||
<button
|
||||
v-if="editId"
|
||||
type="button"
|
||||
class="dptr-form__reset"
|
||||
@click="openAdd()"
|
||||
>
|
||||
هشدار جدید
|
||||
</button>
|
||||
</div>
|
||||
<PriceTriggerForm />
|
||||
</section>
|
||||
|
||||
<section class="dptr-surface dptr-list">
|
||||
<div class="dptr-list__header">
|
||||
<h3 class="dptr-list__title">هشدارهای فعلی</h3>
|
||||
<span class="dptr-list__count">{{ items.length }} مورد</span>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="dptr-list__skeletons">
|
||||
<TransactionSkeleton v-for="i in 6" :key="i" class="mt-1" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="items.length" class="dptr-list__items">
|
||||
<list-anim>
|
||||
<Item
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
variant="desktop"
|
||||
@edit="openEdit"
|
||||
@refresh="initItems"
|
||||
/>
|
||||
</list-anim>
|
||||
</div>
|
||||
|
||||
<div v-else class="dptr-empty">
|
||||
<BasicIcon icon="vuesax-linear:notification-bing" size="36" />
|
||||
<p>هنوز هشداری ثبت نشده است.</p>
|
||||
<button type="button" class="dptr-empty__btn" @click="openAdd()">
|
||||
افزودن اولین هشدار
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import ListAnim from '@/components/ListAnim.vue'
|
||||
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||
import Item from '../components/Item.vue'
|
||||
import PriceTriggerForm from '../components/PriceTriggerForm.vue'
|
||||
import { usePriceTriggerPageContext } from '../composables/usePriceTriggerPage'
|
||||
|
||||
const {
|
||||
items,
|
||||
loading,
|
||||
editId,
|
||||
openAdd,
|
||||
openEdit,
|
||||
initItems,
|
||||
} = usePriceTriggerPageContext()
|
||||
</script>
|
||||
@@ -0,0 +1,273 @@
|
||||
/* Premium desktop price trigger page */
|
||||
|
||||
.dptr {
|
||||
min-height: 100%;
|
||||
background: #f6f7f9;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.dptr__container {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 28px 36px 40px;
|
||||
}
|
||||
|
||||
.dptr-topbar {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
||||
|
||||
&__eyebrow {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #9ca3af;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.03em;
|
||||
color: #111827;
|
||||
margin: 0 0 6px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
max-width: 520px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.dptr-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 380px) minmax(0, 1fr);
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.dptr-surface {
|
||||
background: #fff;
|
||||
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.dptr-form {
|
||||
padding: 24px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__reset {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: @App-primary-color;
|
||||
color: @App-primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dptr-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 420px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 20px 24px 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__count {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
background: #f3f4f6;
|
||||
border-radius: 999px;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
&__items {
|
||||
padding: 16px 24px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__skeletons {
|
||||
padding: 16px 24px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.dptr-card {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 12px 16px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid #eef0f3;
|
||||
border-radius: 12px;
|
||||
background: #fafbfc;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #dbe0e6;
|
||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
&__price {
|
||||
grid-column: 1 / -1;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
&__label {
|
||||
color: #9ca3af;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__value {
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__icon-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease, background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
&--danger:hover {
|
||||
border-color: #fecaca;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
&--edit:hover {
|
||||
border-color: #bbf7d0;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #eef0f3;
|
||||
}
|
||||
|
||||
&__date {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
|
||||
.dptr-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 48px 24px;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
margin-top: 4px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: @App-primary-color;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.92;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1023.98px) {
|
||||
.dptr-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -1,197 +1,31 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="bg-white py-3 px-3 text-primary font-bold drop-shadow wrap-fix flex z-10">
|
||||
<div @click="router.back()">
|
||||
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
||||
</div>
|
||||
<div class="mr-2">هشدارهای فعلی</div>
|
||||
</div>
|
||||
<!-- <a-spin :spinning="loading">-->
|
||||
<div ref="el" :class="loading? 'h-screen': ''" class="px-3 pt-16 pb-2 overflow-y-scroll"
|
||||
style="max-height: calc(100vh - 50px)">
|
||||
<div class="price-trigger-page-root">
|
||||
<DesktopPriceTriggerView v-show="isDesktop" />
|
||||
<MobilePriceTriggerView v-show="!isDesktop" />
|
||||
|
||||
<list-anim v-if="!loading" class="pb-12vh">
|
||||
<Item v-for="item in items" :key="item.id" :item="item" @edit="openEdit" @refresh="initItems" />
|
||||
<!-- <TransactionSkeleton v-if="loadingMore" class="mt-1 mb-8" />-->
|
||||
</list-anim>
|
||||
<div v-if="loading">
|
||||
<TransactionSkeleton v-for="i in 13" :key="i" class="mt-1" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- </a-spin>-->
|
||||
<BasicActionSheet v-model:show="addTriggerVisible" teleport="body">
|
||||
<a-form ref="formRef" :model="form" class="!mt-5" name="basic"
|
||||
@finish="ok()">
|
||||
<div class="flex flex-col gap-4 pb-12 w-full">
|
||||
<div class="font-bold text-lg">هشدار قیمت</div>
|
||||
<a-form-item :rules="[required()]" class="home_appear_fade_in_down" label="جهت قیمت" name="price_condition">
|
||||
<BasicSelect :items="typeItems" v-model:value="form.price_condition" :show-search="false" value-name="value"
|
||||
key-name="key"
|
||||
placeholder="انتخاب نوع جهت قیمت"
|
||||
size="large"></BasicSelect>
|
||||
</a-form-item>
|
||||
<a-form-item :rules="[required()]" class="home_appear_fade_in_down" label="قیمت" name="target_price">
|
||||
<BasicInput v-model:value="form.target_price" place-holder="وارد کردن قیمت" type="price" size="large"
|
||||
addon-before="ریال" />
|
||||
</a-form-item>
|
||||
<a-form-item :rules="[required()]" class="home_appear_fade_in_down" name="action"
|
||||
label="نوع هشدار را انتخاب کنید">
|
||||
<a-radio-group v-model:value="form.action">
|
||||
<a-radio :style="radioStyle" value="sms">ارسال پیامک</a-radio>
|
||||
<a-radio :style="radioStyle" value="email">ارسال ایمیل</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-spin :spinning="loading">
|
||||
<BasicButton class="w-full" type="primary" html-type="submit">
|
||||
{{ editId ? 'ویرایش هشدار' : 'ثبت هشدار' }}
|
||||
</BasicButton>
|
||||
</a-spin>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
<BasicActionSheet v-if="!isDesktop" v-model:show="addTriggerVisible" teleport="body">
|
||||
<div class="font-bold text-lg mb-2">هشدار قیمت</div>
|
||||
<PriceTriggerForm />
|
||||
</BasicActionSheet>
|
||||
<!-- <div class="bg-primary w-3rem h-3rem flex items-center justify-center rounded-lg fixed bottom-12vh left-8vw"
|
||||
@click="openAdd()">
|
||||
<BasicIcon icon="vuesax-linear:add" color="white" />
|
||||
</div>-->
|
||||
<Transition mode="out-in" name="fade">
|
||||
<div
|
||||
class="wrap-fix bottom-0 h-[16vh] bg-[#FBFBFC] shadow-lg rounded-3xl flex justify-center pt-3 z-200"
|
||||
style="box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 26px 11px"
|
||||
@click="openAdd()">
|
||||
<BasicIcon icon="vuesax-linear:add" />
|
||||
<div>افزودن هشدار جدید</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createPriceTrigger, getPriceTriggers, updatePriceTrigger } from '@/api/triggerPrice'
|
||||
|
||||
const radioStyle = reactive({
|
||||
display: '',
|
||||
height: '30px',
|
||||
lineHeight: '30px'
|
||||
})
|
||||
import type { SelectItem } from '@/models/SelectItem'
|
||||
|
||||
// import Item from "./components/Item.vue";
|
||||
/*import {lengthValidation, required} from "@/utils/inputRules";
|
||||
import {loading} from "@/utils";
|
||||
import BasicButton from "@/components/BasicPacks/BasicButton.vue";
|
||||
import BasicActionSheet from "@/components/BasicPacks/BasicActionSheet.vue";
|
||||
import BasicInput from "@/components/BasicPacks/BasicInput.vue";*/
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import type { TableEngineInput, TableOutput } from '@/models/table'
|
||||
import { loading, loadingMore } from '@/utils'
|
||||
// import {getOrders} from "@/api/order";
|
||||
import { getTransactionTable } from '@/api/transaction'
|
||||
import type { Transaction } from '@/models/transaction'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BaseDatePicker from '@/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue'
|
||||
import { useInfiniteScroll, useScroll } from '@vueuse/core'
|
||||
import ListAnim from '@/components/ListAnim.vue'
|
||||
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||
import { DotLottieVue } from '@lottiefiles/dotlottie-vue'
|
||||
import { mobileValidation, required } from '@/utils/inputRules'
|
||||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||
import type { User } from '@/models/login'
|
||||
import { type FormInstance, message } from 'ant-design-vue'
|
||||
import type { PriceTrigger } from '@/models/PriceTrigger'
|
||||
import BasicSelect from '@/components/BasicPacks/BasicSelect.vue'
|
||||
import Item from './components/Item.vue'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import router from '@/router'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { usePriceTriggerPage } from './composables/usePriceTriggerPage'
|
||||
import DesktopPriceTriggerView from './desktop/DesktopPriceTriggerView.vue'
|
||||
import MobilePriceTriggerView from './mobile/MobilePriceTriggerView.vue'
|
||||
import PriceTriggerForm from './components/PriceTriggerForm.vue'
|
||||
|
||||
const items = ref<PriceTrigger[]>([])
|
||||
const addTriggerVisible = ref<boolean>(false)
|
||||
const editId = ref<number | undefined>(undefined)
|
||||
const el = ref()
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
|
||||
const form = ref<PriceTrigger>({
|
||||
price_condition: '',
|
||||
is_active: true,
|
||||
action: '',
|
||||
target_price: ''
|
||||
})
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
onMounted(() => {
|
||||
initItems()
|
||||
})
|
||||
|
||||
function resetFields() {
|
||||
form.value = {
|
||||
price_condition: '',
|
||||
is_active: true,
|
||||
action: '',
|
||||
target_price: ''
|
||||
}
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
resetFields()
|
||||
editId.value = undefined
|
||||
addTriggerVisible.value = true
|
||||
}
|
||||
|
||||
async function initItems() {
|
||||
try {
|
||||
items.value = []
|
||||
items.value = await getPriceTriggers()
|
||||
// console.log(items.value)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openEdit(val: PriceTrigger) {
|
||||
form.value = cloneDeep(val)
|
||||
editId.value = form.value.id
|
||||
addTriggerVisible.value = true
|
||||
}
|
||||
|
||||
async function ok() {
|
||||
formRef.value?.validateFields().then(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
editId.value ? await updatePriceTrigger(editId.value, form.value) : await createPriceTrigger(form.value)
|
||||
message.success('هشدار با موفقیت ایجاد شد')
|
||||
await initItems()
|
||||
addTriggerVisible.value = false
|
||||
//
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const typeItems: SelectItem[] = [
|
||||
{
|
||||
value: 'بزرگتر',
|
||||
key: '>'
|
||||
},
|
||||
{
|
||||
value: 'کوچکتر',
|
||||
key: '<'
|
||||
},
|
||||
{
|
||||
value: 'مساوی',
|
||||
key: '=='
|
||||
}
|
||||
]
|
||||
const { addTriggerVisible } = usePriceTriggerPage()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
.price-trigger-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div>
|
||||
<MobilePageHeader title="هشدارهای فعلی" show-back />
|
||||
|
||||
<div
|
||||
:class="loading ? 'h-screen' : ''"
|
||||
class="px-3 mpage-content pb-2 overflow-y-scroll mpage-scroll"
|
||||
>
|
||||
<list-anim v-if="!loading" class="pb-[calc(var(--app-bottom-chrome-height)+var(--app-add-card-height)+1rem)]">
|
||||
<Item
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
@edit="openEdit"
|
||||
@refresh="initItems"
|
||||
/>
|
||||
</list-anim>
|
||||
<div v-if="loading">
|
||||
<TransactionSkeleton v-for="i in 13" :key="i" class="mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="!addTriggerVisible"
|
||||
class="app-dock-add-bar"
|
||||
@click="openAdd()"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:add" />
|
||||
<div>افزودن هشدار جدید</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
|
||||
import ListAnim from '@/components/ListAnim.vue'
|
||||
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||
import Item from '../components/Item.vue'
|
||||
import { usePriceTriggerPageContext } from '../composables/usePriceTriggerPage'
|
||||
|
||||
const {
|
||||
items,
|
||||
loading,
|
||||
addTriggerVisible,
|
||||
openAdd,
|
||||
openEdit,
|
||||
initItems,
|
||||
} = usePriceTriggerPageContext()
|
||||
</script>
|
||||
@@ -1,54 +1,68 @@
|
||||
<template>
|
||||
<div :class="isDesktop ? 'dpage products-page' : ''">
|
||||
<div class="bottom-0 fixed top-0 -z-10 pointer-events-none">
|
||||
<img :src="img" alt="" class=" w-inherit "
|
||||
:style="{ backgroundRepeat: 'repeat', }"
|
||||
/>
|
||||
</div>
|
||||
<a-spin :spinning="productsPageLoading">
|
||||
<div class="relative z-0 !h-full overflow-y-scroll">
|
||||
<div class="w-full flex justify-center top-0 py-5" style="align-self: center">
|
||||
<ARadioGroup @change="changeProductsTab" v-model:value="productsActiveKey" button-style="solid">
|
||||
<ARadioButton class="cats" value="همه">همه</ARadioButton>
|
||||
<ARadioButton class="cats" v-for="item in productsTypeItems" :key="item" :value="item">{{ item }}</ARadioButton>
|
||||
</ARadioGroup>
|
||||
<div
|
||||
class="products-page-root main-with-bottom-chrome-inner--contained"
|
||||
:class="isDesktop ? 'dpage products-page' : ''"
|
||||
>
|
||||
<MobileProductsView v-if="!isDesktop" @select="clickProduct" />
|
||||
|
||||
<template v-else>
|
||||
<div class="bottom-0 fixed top-0 -z-10 pointer-events-none">
|
||||
<img :src="img" alt="" class="w-inherit" :style="{ backgroundRepeat: 'repeat' }" />
|
||||
</div>
|
||||
<div class="mx-5 flex justify-end" @click="viewVertical=!viewVertical">
|
||||
<BasicIcon v-if="viewVertical" icon="vuesax-linear:row-vertical"></BasicIcon>
|
||||
<BasicIcon v-else icon="vuesax-linear:element-3"></BasicIcon>
|
||||
<a-spin :spinning="productsPageLoading">
|
||||
<div class="relative z-0 !h-full overflow-y-scroll">
|
||||
<div class="w-full flex justify-center top-0 py-5" style="align-self: center">
|
||||
<ARadioGroup
|
||||
v-model:value="productsActiveKey"
|
||||
button-style="solid"
|
||||
@change="changeProductsTab"
|
||||
>
|
||||
<ARadioButton class="cats" value="همه">همه</ARadioButton>
|
||||
<ARadioButton
|
||||
v-for="item in productsTypeItems"
|
||||
:key="item"
|
||||
class="cats"
|
||||
:value="item"
|
||||
>
|
||||
{{ item }}
|
||||
</ARadioButton>
|
||||
</ARadioGroup>
|
||||
</div>
|
||||
<div class="mx-5 flex justify-end" @click="viewVertical = !viewVertical">
|
||||
<BasicIcon v-if="viewVertical" icon="vuesax-linear:row-vertical" />
|
||||
<BasicIcon v-else icon="vuesax-linear:element-3" />
|
||||
</div>
|
||||
|
||||
<div class="mx-5 pb-30 pt-2 grid grid-cols-2 gap-3 items-stretch">
|
||||
<template v-if="viewVertical">
|
||||
<ProductItemVertical
|
||||
v-for="item in productsItems"
|
||||
:key="item.id"
|
||||
class="h-full"
|
||||
:ProductItem="item"
|
||||
@click="clickProduct(item)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ProductItem
|
||||
v-for="item in productsItems"
|
||||
:key="item.id"
|
||||
class="col-span-2 h-full"
|
||||
:ProductItem="item"
|
||||
@click="clickProduct(item)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<BasicActionSheet placement="bottom" v-model:show="showFilter">
|
||||
<div>
|
||||
<indexGold v-if="selectedProduct?.group_type == 'آبشده'" :ProductItem="selectedProduct" />
|
||||
<DealProduct v-else :ProductItem="selectedProduct" @close="showFilter = false" />
|
||||
</div>
|
||||
|
||||
<div class="mx-5 pb-30 pt-2 grid grid-cols-2 gap-3 items-stretch">
|
||||
<template v-if="viewVertical">
|
||||
<ProductItemVertical
|
||||
v-for="item in productsItems"
|
||||
:key="item.id"
|
||||
class="h-full"
|
||||
:ProductItem="item"
|
||||
@click="clickProduct(item)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ProductItem
|
||||
v-for="item in productsItems"
|
||||
:key="item.id"
|
||||
class="col-span-2 h-full"
|
||||
:ProductItem="item"
|
||||
@click="clickProduct(item)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</a-spin>
|
||||
|
||||
<BasicActionSheet placement="bottom" v-model:show="showFilter">
|
||||
<div class="">
|
||||
<indexGold v-if="selectedProduct?.group_type=='آبشده'" :ProductItem="selectedProduct"></indexGold>
|
||||
<DealProduct v-else :ProductItem="selectedProduct" @close="showFilter = false"></DealProduct>
|
||||
</div>
|
||||
</BasicActionSheet>
|
||||
|
||||
</BasicActionSheet>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -61,9 +75,10 @@ import type { Product } from '@/models/products'
|
||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||
import indexGold from '@/views/base/gold/index.vue'
|
||||
import DealProduct from '@/views/base/products/components/DealProduct.vue'
|
||||
import img from "@/assets/img/user_ly1.svg";
|
||||
import BasicIcon from "@/components/BasicPacks/BasicIcon.vue";
|
||||
import ProductItemVertical from "@/views/base/products/components/ProductItemVertical.vue";
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import ProductItemVertical from '@/views/base/products/components/ProductItemVertical.vue'
|
||||
import MobileProductsView from '@/views/base/products/mobile/MobileProductsView.vue'
|
||||
import { isProductVisible } from '@/utils/productPrice'
|
||||
import {
|
||||
productsActiveKey,
|
||||
@@ -76,7 +91,7 @@ import {
|
||||
onProductsPageProviderUnmounted,
|
||||
} from '@/views/base/products/composables/useProductsPageData'
|
||||
|
||||
const showFilter = ref()
|
||||
const showFilter = ref<boolean>()
|
||||
const selectedProduct = ref<Product>()
|
||||
const viewVertical = ref(true)
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
@@ -98,9 +113,9 @@ onMounted(async () => {
|
||||
|
||||
if (router.currentRoute.value.query.state) {
|
||||
if (router.currentRoute.value.query.product) {
|
||||
clickProduct(productsItems.value?.find((x) => x.title === 'سکه'))
|
||||
clickProduct(productsItems.value?.find((x) => x.title === 'سکه')!)
|
||||
} else {
|
||||
clickProduct(productsItems.value?.find((x) => x.title === 'آبشده نقدی'))
|
||||
clickProduct(productsItems.value?.find((x) => x.title === 'آبشده نقدی')!)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -110,13 +125,18 @@ onUnmounted(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.products-page-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.cats{
|
||||
font-size: x-small;
|
||||
.products-page-root.main-with-bottom-chrome-inner {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
.cats:hover span {
|
||||
--un-text-opacity: 1;
|
||||
color: rgba(206, 163, 76, var(--un-text-opacity))!important;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div class="products-mobile-root">
|
||||
<div class="mobile-page-bg" aria-hidden="true">
|
||||
<img :src="img" alt="" />
|
||||
</div>
|
||||
|
||||
<div class="products-mobile-root__chrome">
|
||||
<div class="products-mobile-root__tabs">
|
||||
<ARadioGroup
|
||||
v-model:value="productsActiveKey"
|
||||
button-style="solid"
|
||||
@change="changeProductsTab"
|
||||
>
|
||||
<ARadioButton class="cats" value="همه">همه</ARadioButton>
|
||||
<ARadioButton
|
||||
v-for="item in productsTypeItems"
|
||||
:key="item"
|
||||
class="cats"
|
||||
:value="item"
|
||||
>
|
||||
{{ item }}
|
||||
</ARadioButton>
|
||||
</ARadioGroup>
|
||||
</div>
|
||||
|
||||
<div class="products-mobile-root__toolbar mx-5">
|
||||
<button type="button" class="products-mobile-root__view-toggle" @click="viewVertical = !viewVertical">
|
||||
<BasicIcon v-if="viewVertical" icon="vuesax-linear:row-vertical" />
|
||||
<BasicIcon v-else icon="vuesax-linear:element-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="products-mobile-root__scroll">
|
||||
<a-spin :spinning="productsPageLoading">
|
||||
<div
|
||||
class="products-mobile-root__list mx-5 pt-2 pb-[calc(var(--app-bottom-chrome-height)+1rem)] grid gap-3 items-stretch"
|
||||
:class="viewVertical ? 'grid-cols-2' : 'grid-cols-1'"
|
||||
>
|
||||
<template v-if="viewVertical">
|
||||
<ProductItemVertical
|
||||
v-for="item in productsItems"
|
||||
:key="item.id"
|
||||
class="h-full"
|
||||
:ProductItem="item"
|
||||
@click="$emit('select', item)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ProductItem
|
||||
v-for="item in productsItems"
|
||||
:key="item.id"
|
||||
class="h-full"
|
||||
:ProductItem="item"
|
||||
@click="$emit('select', item)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { Product } from '@/models/products'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import ProductItem from '@/views/base/products/components/ProductItem.vue'
|
||||
import ProductItemVertical from '@/views/base/products/components/ProductItemVertical.vue'
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import {
|
||||
productsActiveKey,
|
||||
productsTypeItems,
|
||||
productsItems,
|
||||
productsPageLoading,
|
||||
changeProductsTab,
|
||||
} from '@/views/base/products/composables/useProductsPageData'
|
||||
|
||||
defineEmits<{
|
||||
select: [item: Product]
|
||||
}>()
|
||||
|
||||
const viewVertical = ref(true)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.products-mobile-root {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.products-mobile-root__chrome {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.products-mobile-root__tabs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 1.25rem 0.75rem 0.5rem;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.products-mobile-root__toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.products-mobile-root__view-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0.25rem;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.products-mobile-root__scroll {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
.products-mobile-root__list {
|
||||
min-height: min-content;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.cats {
|
||||
font-size: x-small;
|
||||
}
|
||||
|
||||
.cats:hover span {
|
||||
--un-text-opacity: 1;
|
||||
color: rgba(206, 163, 76, var(--un-text-opacity)) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="wrap-fix z-1 bg-white py-3 px-3 text-primary font-bold drop-shadow flex">
|
||||
<div class="cursor-pointer" @click="router.back()">
|
||||
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
||||
</div>
|
||||
<div class="mr-2">قوانین و مقررات</div>
|
||||
</div>
|
||||
<div class="pt-20 pb-24 px-5">
|
||||
<MobilePageHeader title="قوانین و مقررات" show-back />
|
||||
<div class="mpage-content pb-[calc(var(--app-bottom-chrome-height)+1rem)] px-5">
|
||||
<RulesContentPanel variant="mobile" />
|
||||
<RulesSupportActions variant="mobile" />
|
||||
</div>
|
||||
@@ -14,8 +9,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import router from '@/router'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
|
||||
import RulesContentPanel from '../components/RulesContentPanel.vue'
|
||||
import RulesSupportActions from '../components/RulesSupportActions.vue'
|
||||
</script>
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :style="{ backgroundImage: `url(${img})` }" class="fixed top-0 right-0 left-0 bottom-0"></div>
|
||||
<div class="absolute inset-0 bg-cover bg-center opacity-60"></div>
|
||||
<div
|
||||
class="bg-white w-full transaction-list py-3 px-3 font-bold drop-shadow flex justify-between z-10"
|
||||
>
|
||||
<div>لیست تراکنش ها</div>
|
||||
<BasicIcon icon="vuesax-linear:filter" @click="showFilter = true" />
|
||||
<div class="mobile-transactions-view">
|
||||
<div class="mobile-page-bg" aria-hidden="true">
|
||||
<img :src="img" alt="" />
|
||||
</div>
|
||||
|
||||
<MobilePageHeader title="لیست تراکنش ها">
|
||||
<template #trailing>
|
||||
<button type="button" class="mobile-transactions-view__filter" @click="showFilter = true">
|
||||
<BasicIcon icon="vuesax-linear:filter" />
|
||||
</button>
|
||||
</template>
|
||||
</MobilePageHeader>
|
||||
|
||||
<div
|
||||
ref="el"
|
||||
:class="loading ? 'h-screen' : ''"
|
||||
class="px-3 pt-16 pb-4"
|
||||
style="max-height: calc(100vh - 50px)"
|
||||
class="mobile-transactions-view__content mpage-content px-3 pb-[calc(var(--app-bottom-chrome-height)+1rem)]"
|
||||
>
|
||||
<list-anim v-if="!loading">
|
||||
<Item v-for="item in items" :key="item.id" :item="item" />
|
||||
@@ -22,7 +24,7 @@
|
||||
<TransactionSkeleton v-if="loadingMore" class="mt-1 mb-8" />
|
||||
</list-anim>
|
||||
<TransactionSkeleton v-for="i in 13" v-else :key="i" class="mt-1" />
|
||||
<div v-if="items.length <= 0 && !loading" class="flex flex-col items-center justify-center h-80vh">
|
||||
<div v-if="items.length <= 0 && !loading" class="flex flex-col items-center justify-center min-h-[50vh]">
|
||||
<DotLottieVue
|
||||
class="w-11rem h-11rem"
|
||||
autoplay
|
||||
@@ -31,9 +33,10 @@
|
||||
<div class="font-bold">{{ emptyText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BasicActionSheet v-model:show="showFilter">
|
||||
<div class="font-bold text-lg">فیلتر</div>
|
||||
<div class="pb-20 mt-5">
|
||||
<div class="pb-10 mt-5">
|
||||
<a-form-item class="w-full gap-2 !flex-col" label="تاریخ" name="password">
|
||||
<BaseDatePicker v-model:value="dateFilter" class="w-full" size="large" />
|
||||
</a-form-item>
|
||||
@@ -54,6 +57,7 @@ import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BaseDatePicker from '@/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue'
|
||||
import ListAnim from '@/components/ListAnim.vue'
|
||||
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
|
||||
import { DotLottieVue } from '@lottiefiles/dotlottie-vue'
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import { useTransactionsPageContext } from '../composables/useTransactionsPage'
|
||||
@@ -71,14 +75,27 @@ const {
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.transaction-list {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
left: 0;
|
||||
@media screen and (min-width: 426px) {
|
||||
width: 426px;
|
||||
left: auto;
|
||||
right: auto;
|
||||
}
|
||||
.mobile-transactions-view {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.mobile-transactions-view__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding-inline: 0.75rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mobile-transactions-view__filter {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -87,6 +87,12 @@ export function useUserPage() {
|
||||
icon: 'vuesax-linear:lock',
|
||||
onClick: () => { editPasswordVisible.value = true },
|
||||
},
|
||||
{
|
||||
id: 'notify',
|
||||
label: 'هشدار قیمت',
|
||||
icon: 'vuesax-linear:bill',
|
||||
onClick: () => router.push({ name: 'notifyTrigger' }),
|
||||
},
|
||||
{
|
||||
id: 'about',
|
||||
label: 'درباره ما',
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div>
|
||||
<!-- <img :src="gr_bottom" alt="" class="absolute bottom-20">-->
|
||||
<img :src="img" alt="" class="mt-2 w-full absolute bottom-0" />
|
||||
<div class="relative mobile-user-view">
|
||||
<div class="mobile-page-bg" aria-hidden="true">
|
||||
<img :src="img" alt="" />
|
||||
</div>
|
||||
<div class="bg-white w-full py-3 px-3 font-bold drop-shadow flex">
|
||||
<div @click="router.back()" class="cursor-pointer">
|
||||
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
||||
</div>
|
||||
<div class="mr-2 items-center">اطلاعات حساب کاربری</div>
|
||||
</div>
|
||||
<div class="pt-16 pb-8 z-30 relative">
|
||||
<MobilePageHeader title="اطلاعات حساب کاربری" show-back />
|
||||
<div class="mpage-content pb-[calc(var(--app-bottom-chrome-height)+1rem)] z-30 relative">
|
||||
<div
|
||||
class="border-gray-200 border-1 rounded-xl p-5 trans flex flex-col items-center mx-5 z-1000 relative bg-white shadow-lg"
|
||||
>
|
||||
@@ -92,16 +86,16 @@
|
||||
<div>آدرس ها</div>
|
||||
</div>
|
||||
</BasicButton>-->
|
||||
<!-- <BasicButton-->
|
||||
<!-- class="w-full mt-2"-->
|
||||
<!-- size="large"-->
|
||||
<!-- @click="$router.push({ name: 'notifyTrigger' })"-->
|
||||
<!-- >-->
|
||||
<!-- <div class="flex">-->
|
||||
<!-- <BasicIcon icon="vuesax-linear:bill" class="absolute right-8" />-->
|
||||
<!-- <div>هشدار قیمت</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </BasicButton>-->
|
||||
<BasicButton
|
||||
class="w-full mt-2"
|
||||
size="large"
|
||||
@click="$router.push({ name: 'notifyTrigger' })"
|
||||
>
|
||||
<div class="flex">
|
||||
<BasicIcon icon="vuesax-linear:bill" class="absolute right-8" />
|
||||
<div>هشدار قیمت</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<BasicButton
|
||||
class="mt-2 w-full"
|
||||
icon=""
|
||||
@@ -222,6 +216,7 @@ import { useWindowScroll } from '@vueuse/core/index'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { useUserPageContext } from '../composables/useUserPage'
|
||||
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const walletStore = useWalletStore()
|
||||
@@ -282,17 +277,7 @@ async function logout() {
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.relative {
|
||||
overflow-y: auto;
|
||||
max-height: 100vh;
|
||||
//scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 50; /* بالاتر از همه */
|
||||
.mobile-user-view {
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="absolute inset-0 bg-cover bg-center opacity-60" :style="{ backgroundImage: `url(${img})` }"></div>
|
||||
<div class="bg-white py-3 px-3 font-bold drop-shadow z-[1999] wrap-fix">کیف پول</div>
|
||||
<div class="mpage-header wrap-fix z-[1999]">کیف پول</div>
|
||||
<div
|
||||
class="bg-white wrap-fix py-3 top-11 px-3 text-primary font-bold flex justify-evenly z-[1999]"
|
||||
class="mpage-subheader wrap-fix fixed left-0 right-0 px-3 text-primary font-bold flex justify-evenly z-[1999]"
|
||||
>
|
||||
<div class="flex justify-evenly w-full px-2 h-11 items-center bg-gray-50 relative rounded">
|
||||
<div
|
||||
@@ -29,7 +29,7 @@
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-5 pt-26 pb-2">
|
||||
<div class="px-5 mpage-content--with-subheader pb-[calc(var(--app-bottom-chrome-height)+1rem)]">
|
||||
<a-form :model="form" class="!mt-5" name="basic" @finish="ok()">
|
||||
<div class="border-gray-200 border-1 rounded-xl p-5 trans h-40 relative">
|
||||
<bg_main />
|
||||
@@ -99,8 +99,7 @@
|
||||
v-if="isDeposit"
|
||||
:ref="(node) => { mobileDepositScrollEl = node as HTMLElement | null }"
|
||||
:class="loading ? 'h-screen' : ''"
|
||||
class="px-3 overflow-y-scroll w-full h-[49vh] pb-16"
|
||||
style="max-height: calc(100vh - 50px)"
|
||||
class="px-3 overflow-y-scroll w-full h-[49vh] pb-[calc(var(--app-bottom-chrome-height)+1rem)] mpage-scroll"
|
||||
>
|
||||
<list-anim v-if="!loading" :numberItemsAnim="4">
|
||||
<Item
|
||||
@@ -122,7 +121,7 @@
|
||||
:items="cardItems"
|
||||
v-model:id="form.card_id"
|
||||
@refresh="getCards"
|
||||
class="card-slider-wrap mt-[2vh] px-3 fixed bottom-10 right-0 left-0"
|
||||
class="card-slider-wrap mt-[2vh] px-3 wallet-dock-width fixed bottom-[calc(var(--app-bottom-chrome-height)+var(--app-add-card-height)-var(--wallet-card-dock-overlap))] z-[var(--app-z-wallet-dock)]"
|
||||
style="touch-action: none"
|
||||
/>
|
||||
<div
|
||||
@@ -137,8 +136,7 @@
|
||||
<Transition mode="out-in" name="fade">
|
||||
<div
|
||||
v-if="!isDeposit"
|
||||
class="add-card fixed bottom-0 h-[18vh] bg-[#FBFBFC] w-full shadow-lg rounded-3xl flex justify-center pt-3 z-200"
|
||||
style="box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 26px 11px"
|
||||
class="app-dock-add-bar"
|
||||
@click="showAddCard = true"
|
||||
>
|
||||
<div>افزودن کارت جدید</div>
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@ import {
|
||||
|
||||
export default defineConfig({
|
||||
shortcuts: [
|
||||
// ...
|
||||
['wallet-dock-width', 'fixed inset-x-0 w-full max-w-[426px] left-0 right-0'],
|
||||
['app-dock-add-bar', 'wallet-dock-width !fixed bottom-[var(--app-bottom-chrome-height)] z-[var(--app-z-wallet-add-bar)] h-[var(--app-add-card-height)] flex items-center justify-center gap-1 rounded-t-3xl bg-[#FBFBFC] shadow-[0_1px_26px_11px_rgba(0,0,0,0.1)] cursor-pointer box-border'],
|
||||
],
|
||||
theme: {
|
||||
colors: {
|
||||
|
||||
Reference in New Issue
Block a user