Merge remote-tracking branch 'origin/kashkool' into kashkool

This commit is contained in:
2026-07-02 09:43:22 +03:30
52 changed files with 2885 additions and 1051 deletions
+1 -1
View File
@@ -155,7 +155,7 @@ const {
} = useDraggableFloatingButton('ghatreh_support_button_position_v1', {
width: 118,
height: 42,
defaultBottomOffset: 100,
defaultBottomGap: 12,
defaultRightOffset: 16,
})
+2
View File
@@ -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')
+8
View File
@@ -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()))
}
+22 -5
View File
@@ -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'
}
}
+1
View File
@@ -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' },
+30 -10
View File
@@ -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>
+28 -8
View File
@@ -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>
+24 -13
View File
@@ -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"
+37 -7
View File
@@ -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;
}
+59
View File
@@ -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;
}
}
}
+208
View File
@@ -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
View File
@@ -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',
+89 -15
View File
@@ -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 -1
View File
@@ -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>
+70 -13
View File
@@ -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>
+23 -16
View File
@@ -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>
+20 -30
View File
@@ -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>
+56 -185
View File
@@ -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>-->
<!-- &lt;!&ndash; <BasicActionSheet v-model:show="showFilter">&ndash;&gt;-->
<!-- &lt;!&ndash; <indexGold ></indexGold>&ndash;&gt;-->
<!-- &lt;!&ndash; </BasicActionSheet>&ndash;&gt;-->
.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="">-->
<!--&lt;!&ndash; <indexGold v-if="selectedProduct?.group_type=='آبشده'" :ProductItem="selectedProduct"></indexGold>&ndash;&gt;-->
<!-- <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 {-->
<!-- &#45;&#45;un-text-opacity: 1;-->
<!-- color: rgba(206, 163, 76, var(&#45;&#45;un-text-opacity))!important;-->
<!--}-->
<!--</style>-->
<!--&lt;!&ndash;-->
<!--<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>-->
<!--&ndash;&gt;-->
<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>
+118 -94
View File
@@ -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;
}
}
+18 -184
View File
@@ -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>
+81 -61
View File
@@ -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: 'درباره ما',
+18 -33
View File
@@ -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
View File
@@ -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: {