Port modular price, product, and PWA fixes into boursecu
Sync live quote handling, SSE product images, store hours, file URL normalization, PWA icon cache, TV monitor, about software, SuperAdmin menu gates, price alerts, and related UX from modular while preserving boursecu branding and config constraints. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
export function getDefaultAboutSoftwareText(projectName: string) {
|
||||
const name = projectName.trim() || 'این نرمافزار'
|
||||
return `${name} بستری امن و آنلاین برای خرید و فروش طلا فراهم کرده تا کاربران بتوانند سرمایههای خود را بهسادگی مدیریت کنند.\n\nهدف ما ارائه تجربهای سریع، شفاف و کاربرپسند در معاملات طلا است.`
|
||||
}
|
||||
|
||||
function looksLikeHtml(value: string) {
|
||||
return /<\/?[a-z][\s\S]*>/i.test(value)
|
||||
}
|
||||
|
||||
export type AboutSoftwareBody =
|
||||
| { kind: 'html'; html: string }
|
||||
| { kind: 'text'; text: string }
|
||||
|
||||
/** اولویت با تنظیم about_software؛ در صورت خالی بودن، متن پیشفرض */
|
||||
export function resolveAboutSoftwareBody(
|
||||
raw: unknown,
|
||||
projectName: string,
|
||||
): AboutSoftwareBody {
|
||||
const value = String(raw ?? '').trim()
|
||||
if (!value) {
|
||||
return { kind: 'text', text: getDefaultAboutSoftwareText(projectName) }
|
||||
}
|
||||
if (looksLikeHtml(value)) {
|
||||
const stripped = value
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ /gi, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
if (!stripped) {
|
||||
return { kind: 'text', text: getDefaultAboutSoftwareText(projectName) }
|
||||
}
|
||||
return { kind: 'html', html: value }
|
||||
}
|
||||
return { kind: 'text', text: value }
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div class="about-software-page-root">
|
||||
<div v-if="isDesktop" class="daboutus">
|
||||
<div class="daboutus__container">
|
||||
<header class="daboutus-topbar">
|
||||
<button type="button" class="daboutus-topbar__back" @click="router.back()">
|
||||
<BasicIcon icon="vuesax-linear:arrow-right-1" size="18" />
|
||||
<span>بازگشت</span>
|
||||
</button>
|
||||
<div class="daboutus-topbar__meta">
|
||||
<p class="daboutus-topbar__eyebrow">اطلاعات</p>
|
||||
<h1 class="daboutus-topbar__title">درباره نرمافزار</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="daboutus-surface daboutus-main">
|
||||
<div class="daboutus-main__intro">
|
||||
<div class="daboutus-main__logo">
|
||||
<img :src="config.config?.avatar" alt="" class="daboutus-main__avatar" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="daboutus-main__project">{{ projectName }}</h2>
|
||||
<p class="daboutus-main__tagline">معرفی نرمافزار و خدمات</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="daboutus-main__scroll">
|
||||
<div class="daboutus-content about-software-card">
|
||||
<div
|
||||
v-if="body.kind === 'html'"
|
||||
class="about-software-body about-software-html"
|
||||
v-html="body.html"
|
||||
/>
|
||||
<div v-else class="about-software-body whitespace-pre-line">
|
||||
{{ body.text }}
|
||||
</div>
|
||||
<div class="about-software-enamad">
|
||||
<EnamadBadge size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="mobile-info-page">
|
||||
<div class="mobile-info-page__bg" aria-hidden="true">
|
||||
<img :src="img" alt="" />
|
||||
</div>
|
||||
<div class="wrap-fix mobile-top-chrome mobile-info-page__header bg-white px-3 pb-3 text-black 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="mobile-info-page__main about-software-main">
|
||||
<div class="border-solid border-2 border-[#093D51] text-[#093D51] rounded-[12px] about-software-card mobile-content-box">
|
||||
<div class="mobile-content-box__scroll about-software-card__scroll">
|
||||
<img
|
||||
:src="config.config?.avatar"
|
||||
class="about-software-avatar"
|
||||
alt=""
|
||||
/>
|
||||
<h2 class="text-sm font-bold mb-2">درباره نرمافزار</h2>
|
||||
<div
|
||||
v-if="body.kind === 'html'"
|
||||
class="about-software-body about-software-html"
|
||||
v-html="body.html"
|
||||
/>
|
||||
<div v-else class="about-software-body whitespace-pre-line">
|
||||
{{ body.text }}
|
||||
</div>
|
||||
<div class="about-software-enamad">
|
||||
<EnamadBadge size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted } from 'vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import EnamadBadge from '@/components/EnamadBadge.vue'
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import router from '@/router'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { resolveAboutSoftwareBody } from './aboutSoftwareContent'
|
||||
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
const config = useConfigStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const projectName = computed(
|
||||
() => config.config?.name_fa || config.config?.name || '',
|
||||
)
|
||||
|
||||
const body = computed(() => {
|
||||
void settingsStore.settings
|
||||
return resolveAboutSoftwareBody(
|
||||
settingsStore.getSettingByKey('about_software'),
|
||||
projectName.value,
|
||||
)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void settingsStore.fetchSettings()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.about-software-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.about-software-main {
|
||||
padding-right: 1rem !important;
|
||||
padding-left: 1rem !important;
|
||||
}
|
||||
|
||||
.about-software-card {
|
||||
padding: 0.5rem 0.625rem;
|
||||
}
|
||||
|
||||
.about-software-card__scroll {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.about-software-avatar {
|
||||
width: 6.5rem;
|
||||
margin: 0.75rem auto 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.about-software-body {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.7;
|
||||
text-align: justify;
|
||||
text-justify: inter-word;
|
||||
}
|
||||
|
||||
.about-software-html :deep(p) {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.7;
|
||||
text-align: justify;
|
||||
text-justify: inter-word;
|
||||
}
|
||||
|
||||
.about-software-html :deep(p + p) {
|
||||
margin-top: 0.65rem;
|
||||
}
|
||||
|
||||
.about-software-html :deep(ul) {
|
||||
margin: 0.5rem 0 0;
|
||||
padding-right: 1.1rem;
|
||||
list-style: disc;
|
||||
font-size: 0.75rem;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.about-software-html :deep(li + li) {
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
|
||||
.about-software-enamad {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1rem;
|
||||
padding-top: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
@@ -155,39 +155,20 @@
|
||||
|
||||
<BasicActionSheet v-model:show="confirmAction">
|
||||
<div class="font-bold text-lg"></div>
|
||||
<div class="w-full rounded-lg bg-gray-50 p-3">
|
||||
<div class="flex justify-between ">
|
||||
<div>وزن:</div>
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold">{{ form.goldValue }}</div>
|
||||
<div class="text-xs mr-1">گرم</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-3">
|
||||
<div>مظنه:</div>
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold">{{ usePriceWithoutType(Math.round(basePriceValue ?? 0)) }}</div>
|
||||
<div class="text-xs mr-1">ریال</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-3">
|
||||
<div>کارمزد:</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="font-bold text-xl">{{ feeInGrams }}</div>
|
||||
<div class="text-xs mr-1">گرم</div>
|
||||
</div>
|
||||
</div>
|
||||
<div >(معادل {{ equivalentPrice }} ریال)</div>
|
||||
<div class="flex justify-between mt-3">
|
||||
<div>{{ isBuy ? 'قیمت قابل پرداخت:' : 'مبلغ دریافتی:' }}</div>
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold">{{ usePriceWithoutType(payablePrice.toFixed(0)) }}</div>
|
||||
<div class="text-xs mr-1">ریال</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<BasicButton class="mb-20 mt-5 w-full" type="primary" @click="ok(); confirmAction= false">ثبت نهایی</BasicButton>
|
||||
<TradeFeeConfirmSummary
|
||||
:is-buy="isBuy"
|
||||
input-lock="gram"
|
||||
:display-trade-gold-amount="form.goldValue ?? 0"
|
||||
weight-unit="گرم"
|
||||
:unit-price-rial="basePriceValue ?? unitPrice"
|
||||
:fee-in-grams="feeInGrams"
|
||||
:equivalent-price="equivalentPrice"
|
||||
:payable-price="payablePrice"
|
||||
:order-gold-amount="form.goldValue ?? 0"
|
||||
:fee-in-rial="feeInRial"
|
||||
:sell-gold-debit="sellGoldDebit"
|
||||
/>
|
||||
<BasicButton class="mb-20 mt-5 w-full" type="primary" :disabled="loading" @click="ok">ثبت نهایی</BasicButton>
|
||||
</BasicActionSheet>
|
||||
<van-dialog v-model:show="noCompleteVisible" class="completeButtonStyle" confirm-button-text="تکمیل اطلاعات"
|
||||
show-confirm-button @confirm="router.push({name: 'user',query:{showEdit: true}})">
|
||||
@@ -222,6 +203,7 @@ import { getLastPrice } from '@/api/home'
|
||||
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||
import TradeFeeConfirmSummary from '@/components/TradeFeeConfirmSummary.vue'
|
||||
import { justNumber } from '@/utils/inputRules'
|
||||
import GoldSkeleton from '@/views/base/gold/components/GoldSkeleton.vue'
|
||||
import GoldLoading from '@/components/GoldLoading.vue'
|
||||
|
||||
@@ -65,10 +65,12 @@ export function useEducatePage() {
|
||||
formRef.value?.validateFields().then(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
await updateMe(userFormState.value)
|
||||
const res: any = await updateMe(userFormState.value)
|
||||
await userStore.getMyInfo()
|
||||
editPassVisible.value = false
|
||||
message.success('اطلاعات شما با موفقیت به روز رسانی شد')
|
||||
message.success(
|
||||
res?.message || 'اطلاعات شما با موفقیت به روز رسانی شد',
|
||||
)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
}
|
||||
|
||||
.gold-embed {
|
||||
max-height: min(70vh, 35rem);
|
||||
max-height: min(78vh, 40rem);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
|
||||
+57
-108
@@ -18,33 +18,45 @@
|
||||
<div :class="panelClass">
|
||||
<div
|
||||
:class="[
|
||||
'!overflow-y-scroll bg-white py-3 text-primary font-bold flex justify-evenly',
|
||||
'bg-white px-3 pt-3 pb-1',
|
||||
isStandaloneMobile ? 'mpage-subheader wrap-fix' : '',
|
||||
]"
|
||||
>
|
||||
<!-- <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>-->
|
||||
<div class="flex justify-evenly w-full px-2 h-11 items-center bg-gray-50 relative rounded ">
|
||||
<div :class="isBuy?'text-tertiary':'text-gray-400'" class="w-full text-center z-2"
|
||||
@click.stop="setIsBuy(true) ">
|
||||
خرید {{ tradeProductLabel }}
|
||||
<div class="flex w-full p-1 items-center border border-gray-200 rounded-full bg-white gap-1">
|
||||
<div
|
||||
class="w-full h-10 flex items-center justify-center font-bold rounded-full transition-all"
|
||||
:class="isBuy ? 'bg-green-600 text-white shadow-sm' : 'text-green-600'"
|
||||
@click.stop="setIsBuy(true)"
|
||||
:style="!isProductBuyOpen(props?.ProductItem) ? 'pointer-events: none; opacity: 0.35;' : ''"
|
||||
>
|
||||
خرید
|
||||
</div>
|
||||
<div :class="!isBuy?'text-tertiary':'text-gray-400'" class="w-full text-center z-2"
|
||||
@click.stop="setIsBuy(false) ">
|
||||
فروش {{ tradeProductLabel }}
|
||||
<div
|
||||
class="w-full h-10 flex items-center justify-center font-bold rounded-full transition-all"
|
||||
:class="!isBuy ? 'bg-red-600 text-white shadow-sm' : 'text-red-600'"
|
||||
@click.stop="setIsBuy(false)"
|
||||
:style="!isProductSellOpen(props?.ProductItem) ? 'pointer-events: none; opacity: 0.35;' : ''"
|
||||
>
|
||||
فروش
|
||||
</div>
|
||||
<!-- <div ref="bganim" class="absolute bg-white h-8 w-1/2 rounded-lg " :style="`transform: translateX(${isbuy? '0':'-100%'}); left: ${isbuy? '0':'100%'}`"></div>-->
|
||||
<div ref="bganim" :style="useStyle()"
|
||||
class="absolute bg-white h-8 w-1/2 rounded-lg animate right"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="userStore" :class="contentClass">
|
||||
<a-spin :spinning="!price">
|
||||
<div v-if="!loading" :class="!isBuy? ' bg-red-100' : ' bg-green-100'"
|
||||
class="w-full relative rounded-lg h-[88px] flex flex-col justify-center background-color-transition">
|
||||
class="w-full relative rounded-2xl h-[88px] flex flex-col justify-center background-color-transition">
|
||||
<img :src="img1" alt="" class="h-[88px] absolute inset-0 w-full object-cover">
|
||||
<div class="w-full items-center flex flex-col">
|
||||
<div class="text-gray-700 font-bold">{{ !isBuy ? 'قیمت فروش' : 'قیمت خرید' }}</div>
|
||||
<div class="flex items-center justify-center gap-2 flex-wrap px-3">
|
||||
<span class="text-gray-700 font-bold">{{ !isBuy ? 'قیمت فروش' : 'قیمت خرید' }}</span>
|
||||
<span
|
||||
v-if="tradeProductLabel"
|
||||
class="text-sm font-extrabold px-3 py-1 rounded-full max-w-[14rem] truncate shadow-sm"
|
||||
:class="!isBuy ? 'bg-red-200/90 text-red-900' : 'bg-green-200/90 text-green-900'"
|
||||
>
|
||||
{{ tradeProductLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="price" class="flex gap-1 mt-3 text-xs items-center">
|
||||
<div :class="!isBuy? 'text-red-700' : 'text-green-700'" class="font-bold text-2xl color-transition">
|
||||
{{ usePriceWithoutType(goldPriceValue) }}
|
||||
@@ -58,7 +70,7 @@
|
||||
</div>
|
||||
</a-spin>
|
||||
<a-form :model="form" class="!mt-5" name="basic" @finish="okConfirm()">
|
||||
<div class="border-gray-200 border-1 rounded-xl p-5 gold-form-card relative">
|
||||
<div class="border-gray-200 border-1 rounded-2xl p-5 gold-form-card relative">
|
||||
<bg_main />
|
||||
<div class="z-100 relative">
|
||||
<a-form-item>
|
||||
@@ -137,7 +149,7 @@
|
||||
موجودی کیف پول ناکافی می باشد.
|
||||
</div>
|
||||
</a-form-item>
|
||||
<div class="w-full px-4 rounded-xl py-2 border-1 bg-white gold-fee-box">
|
||||
<div class="w-full px-4 rounded-2xl py-2 border-1 bg-white gold-fee-box">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>کارمزد:</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
@@ -162,51 +174,19 @@
|
||||
|
||||
<BasicActionSheet v-if="!isStandaloneDesktop" v-model:show="confirmAction">
|
||||
<div class="font-bold text-lg"></div>
|
||||
<div class="w-full rounded-lg bg-gray-50 p-3">
|
||||
<div class="flex justify-between ">
|
||||
<div>وزن:</div>
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold">{{ displayTradeGoldAmount }}</div>
|
||||
<div class="text-xs mr-1">{{ weightUnit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-3">
|
||||
<div>مظنه:</div>
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold">{{ usePriceWithoutType(Math.round(goldPriceValue)) }}</div>
|
||||
<div class="text-xs mr-1">ریال</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-3">
|
||||
<div>کارمزد:</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="font-bold text-xl">{{ feeInGrams }}</div>
|
||||
<div class="text-xs mr-1">{{ weightUnit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div >(معادل {{ equivalentPrice }} ریال)</div>
|
||||
<div v-if="isBuy && feeDeductFromGold" class="flex justify-between mt-3">
|
||||
<div>طلا دریافتی:</div>
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold">{{ displayTradeGoldAmount }}</div>
|
||||
<div class="text-xs mr-1">{{ weightUnit }} (دقیق — کارمزد از ریال)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isBuy && feeDeductFromGold" class="flex justify-between mt-3">
|
||||
<div>کسر از کیف طلا:</div>
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold">{{ sellGoldDebit }}</div>
|
||||
<div class="text-xs mr-1">{{ weightUnit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-3">
|
||||
<div>{{ isBuy ? 'قیمت قابل پرداخت:' : 'مبلغ دریافتی:' }}</div>
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold">{{ usePriceWithoutType(payablePrice.toFixed(0)) }}</div>
|
||||
<div class="text-xs mr-1">ریال</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TradeFeeConfirmSummary
|
||||
:is-buy="isBuy"
|
||||
:input-lock="inputLock"
|
||||
:display-trade-gold-amount="displayTradeGoldAmount"
|
||||
:weight-unit="weightUnit"
|
||||
:unit-price-rial="goldPriceValue"
|
||||
:fee-in-grams="feeInGrams"
|
||||
:equivalent-price="equivalentPrice"
|
||||
:payable-price="payablePrice"
|
||||
:order-gold-amount="orderGoldAmount"
|
||||
:fee-in-rial="feeInRial"
|
||||
:sell-gold-debit="sellGoldDebit"
|
||||
/>
|
||||
<BasicButton
|
||||
class="mb-20 mt-5 w-full"
|
||||
type="primary"
|
||||
@@ -224,51 +204,19 @@
|
||||
destroy-on-close
|
||||
custom-footer
|
||||
>
|
||||
<div class="w-full rounded-lg bg-gray-50 p-3">
|
||||
<div class="flex justify-between ">
|
||||
<div>وزن:</div>
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold">{{ displayTradeGoldAmount }}</div>
|
||||
<div class="text-xs mr-1">{{ weightUnit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-3">
|
||||
<div>مظنه:</div>
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold">{{ usePriceWithoutType(Math.round(goldPriceValue)) }}</div>
|
||||
<div class="text-xs mr-1">ریال</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-3">
|
||||
<div>کارمزد:</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="font-bold text-xl">{{ feeInGrams }}</div>
|
||||
<div class="text-xs mr-1">{{ weightUnit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>(معادل {{ equivalentPrice }} ریال)</div>
|
||||
<div v-if="isBuy && feeDeductFromGold" class="flex justify-between mt-3">
|
||||
<div>طلا دریافتی:</div>
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold">{{ displayTradeGoldAmount }}</div>
|
||||
<div class="text-xs mr-1">{{ weightUnit }} (دقیق — کارمزد از ریال)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isBuy && feeDeductFromGold" class="flex justify-between mt-3">
|
||||
<div>کسر از کیف طلا:</div>
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold">{{ sellGoldDebit }}</div>
|
||||
<div class="text-xs mr-1">{{ weightUnit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-3">
|
||||
<div>{{ isBuy ? 'قیمت قابل پرداخت:' : 'مبلغ دریافتی:' }}</div>
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold">{{ usePriceWithoutType(payablePrice.toFixed(0)) }}</div>
|
||||
<div class="text-xs mr-1">ریال</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TradeFeeConfirmSummary
|
||||
:is-buy="isBuy"
|
||||
:input-lock="inputLock"
|
||||
:display-trade-gold-amount="displayTradeGoldAmount"
|
||||
:weight-unit="weightUnit"
|
||||
:unit-price-rial="goldPriceValue"
|
||||
:fee-in-grams="feeInGrams"
|
||||
:equivalent-price="equivalentPrice"
|
||||
:payable-price="payablePrice"
|
||||
:order-gold-amount="orderGoldAmount"
|
||||
:fee-in-rial="feeInRial"
|
||||
:sell-gold-debit="sellGoldDebit"
|
||||
/>
|
||||
<BasicButton class="mt-5 w-full" type="primary" :disabled="loading" @click="ok">
|
||||
ثبت نهایی
|
||||
</BasicButton>
|
||||
@@ -311,6 +259,7 @@ import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
|
||||
import TradeFeeConfirmSummary from '@/components/TradeFeeConfirmSummary.vue'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
|
||||
import { justNumber } from '@/utils/inputRules'
|
||||
@@ -398,7 +347,7 @@ const contentClass = computed(() => {
|
||||
if (isStandaloneMobile.value) {
|
||||
return 'px-3 mpage-content mpage-content--with-subheader pb-[calc(var(--app-bottom-chrome-height)+1rem)]'
|
||||
}
|
||||
return 'mt-5 px-3 pb-4'
|
||||
return 'mt-5 px-3 pb-10'
|
||||
})
|
||||
const isBuy = ref<boolean>(true)
|
||||
const inputLock = ref<TradeInputLock>(null)
|
||||
|
||||
@@ -3,7 +3,18 @@
|
||||
class="w-full relative mt-2 rounded-lg py-3 flex flex-col justify-center home_appear_fade_in transition-colors duration-300"
|
||||
:class="containerClass"
|
||||
>
|
||||
<template v-if="displayState !== 'open'">
|
||||
<div
|
||||
v-if="quotePending"
|
||||
class="flex flex-col items-center gap-2 px-4 py-2"
|
||||
aria-busy="true"
|
||||
aria-label="در حال بارگذاری قیمت"
|
||||
>
|
||||
<div class="h-3 w-32 rounded boursecu-price-skeleton animate-pulse" />
|
||||
<div class="h-6 w-48 rounded boursecu-price-skeleton animate-pulse" />
|
||||
<div class="h-8 w-24 rounded-lg boursecu-price-skeleton animate-pulse" />
|
||||
</div>
|
||||
|
||||
<template v-else-if="displayState !== 'open'">
|
||||
<div class="status-box status-box--boursecu">
|
||||
<div class="status-box__icon-wrap">
|
||||
<Icon
|
||||
@@ -57,12 +68,14 @@ import {
|
||||
STORE_CLOSED_TITLE,
|
||||
getPriceDisplayState,
|
||||
getStoreClosedHint,
|
||||
isHomeQuotePending,
|
||||
} from '@/utils/storeHours'
|
||||
import { formatSignedPercentChange, usePriceWithoutType } from '@/utils/usePrice'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const displayState = computed(() => getPriceDisplayState())
|
||||
const quotePending = computed(() => isHomeQuotePending())
|
||||
|
||||
const statusTitle = computed(() =>
|
||||
displayState.value === 'closed' ? STORE_CLOSED_TITLE : NO_QUOTE_MESSAGE
|
||||
@@ -76,6 +89,9 @@ const statusHint = computed(() => {
|
||||
})
|
||||
|
||||
const containerClass = computed(() => {
|
||||
if (quotePending.value) {
|
||||
return 'boursecu-price-box boursecu-price-box--open'
|
||||
}
|
||||
if (displayState.value !== 'open') {
|
||||
return 'boursecu-price-box boursecu-price-box--closed'
|
||||
}
|
||||
@@ -129,6 +145,10 @@ void price
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.boursecu-price-skeleton {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.status-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<DotLottieVue
|
||||
class="w-11rem h-11rem"
|
||||
autoplay
|
||||
src="https://lottie.host/129cf3a1-df84-4758-a856-58c133ae6013/R5RqclXA8G.json"
|
||||
v-bind="lottieBind"
|
||||
/>
|
||||
<div class="p-5 text-sm">آیا از خروج خود اطمینان دارید؟</div>
|
||||
</div>
|
||||
@@ -23,7 +23,8 @@
|
||||
<script setup lang="ts">
|
||||
import DialogVant from '@/components/DialogVant.vue'
|
||||
import { DotLottieVue } from '@lottiefiles/dotlottie-vue'
|
||||
import {computed} from "vue";
|
||||
import {computed, ref, watch} from 'vue'
|
||||
import {getLottieData, lottieSrc, preloadLottieAssets} from '@/utils/lottieAssets'
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
@@ -34,8 +35,29 @@ const emit = defineEmits<{
|
||||
confirm: []
|
||||
}>()
|
||||
|
||||
const cachedData = ref(getLottieData('logoutConfirm'))
|
||||
|
||||
const lottieBind = computed(() => {
|
||||
if (cachedData.value) return {data: cachedData.value}
|
||||
return {src: lottieSrc('logoutConfirm')}
|
||||
})
|
||||
|
||||
async function ensureLottie() {
|
||||
if (cachedData.value) return
|
||||
await preloadLottieAssets()
|
||||
cachedData.value = getLottieData('logoutConfirm')
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(open) => {
|
||||
if (open) void ensureLottie()
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.show,
|
||||
set: (val) => emit('update:show', val)
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { getConstByKey } from '@/api/const'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { normalizePublicFileUrl } from '@/utils/file'
|
||||
|
||||
function parseJsonArray(raw: unknown): unknown[] {
|
||||
if (raw == null || raw === '') {
|
||||
@@ -69,7 +70,14 @@ async function fetchBanners(force = false) {
|
||||
}
|
||||
}
|
||||
|
||||
banners.value = list.filter((banner) => (banner as { show?: boolean }).show === true)
|
||||
banners.value = list
|
||||
.filter((banner) => (banner as { show?: boolean }).show === true)
|
||||
.map((banner) => {
|
||||
if (!banner || typeof banner !== 'object') return banner
|
||||
const row = banner as { icon?: unknown }
|
||||
if (typeof row.icon !== 'string') return banner
|
||||
return { ...row, icon: normalizePublicFileUrl(row.icon) ?? '' }
|
||||
})
|
||||
bannersLoaded = true
|
||||
} catch (error) {
|
||||
console.error('Error loading banners:', error)
|
||||
|
||||
@@ -2,11 +2,22 @@
|
||||
<div
|
||||
class="dhome-surface dhome-hero-price"
|
||||
:class="{
|
||||
'dhome-hero-price--boursecu-open': displayState === 'open',
|
||||
'dhome-hero-price--boursecu-closed': displayState !== 'open',
|
||||
'dhome-hero-price--boursecu-open': displayState === 'open' || quotePending,
|
||||
'dhome-hero-price--boursecu-closed': displayState !== 'open' && !quotePending,
|
||||
}"
|
||||
>
|
||||
<template v-if="displayState !== 'open'">
|
||||
<div
|
||||
v-if="quotePending"
|
||||
class="flex flex-col items-center gap-2 py-4"
|
||||
aria-busy="true"
|
||||
aria-label="در حال بارگذاری قیمت"
|
||||
>
|
||||
<div class="h-3 w-24 rounded boursecu-price-skeleton animate-pulse" />
|
||||
<div class="h-4 w-32 rounded boursecu-price-skeleton animate-pulse" />
|
||||
<div class="h-8 w-40 rounded boursecu-price-skeleton animate-pulse" />
|
||||
</div>
|
||||
|
||||
<template v-else-if="displayState !== 'open'">
|
||||
<div class="dhome-hero-price__closed">
|
||||
<div class="dhome-hero-price__closed-icon">
|
||||
<BasicIcon
|
||||
@@ -56,12 +67,14 @@ import {
|
||||
STORE_CLOSED_TITLE,
|
||||
getPriceDisplayState,
|
||||
getStoreClosedHint,
|
||||
isHomeQuotePending,
|
||||
} from '@/utils/storeHours'
|
||||
import { formatSignedPercentChange, usePriceWithoutType } from '@/utils/usePrice'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const displayState = computed(() => getPriceDisplayState())
|
||||
const quotePending = computed(() => isHomeQuotePending())
|
||||
|
||||
const statusTitle = computed(() =>
|
||||
displayState.value === 'closed' ? STORE_CLOSED_TITLE : NO_QUOTE_MESSAGE
|
||||
@@ -110,6 +123,10 @@ const percentChange = computed(() => {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.boursecu-price-skeleton {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.dhome-hero-price__closed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -36,7 +36,13 @@ const resolvedActions = computed(() => {
|
||||
})
|
||||
|
||||
function handleClick(action: HomeQuickActionItem) {
|
||||
router.push(action.route)
|
||||
if (action.handler) {
|
||||
action.handler()
|
||||
return
|
||||
}
|
||||
if (action.route) {
|
||||
router.push(action.route)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -38,7 +38,13 @@ const resolvedActions = computed(() => {
|
||||
})
|
||||
|
||||
function navigate(action: HomeQuickActionItem) {
|
||||
router.push(action.route)
|
||||
if (action.handler) {
|
||||
action.handler()
|
||||
return
|
||||
}
|
||||
if (action.route) {
|
||||
router.push(action.route)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
class="w-5rem h-5rem mt-[-1.6rem] ml-[-2rem]"
|
||||
autoplay
|
||||
loop
|
||||
src="https://lottie.host/3d46ebb3-a580-41eb-ade7-b8829c32b071/PJPuSKbpze.json"
|
||||
src="/lottie/goldBars.json"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -315,7 +315,7 @@
|
||||
<DotLottieVue
|
||||
class="w-11rem h-11rem"
|
||||
autoplay
|
||||
src="https://lottie.host/129cf3a1-df84-4758-a856-58c133ae6013/R5RqclXA8G.json"
|
||||
src="/lottie/logoutConfirm.json"
|
||||
/>
|
||||
<div class="p-5 text-sm">آیا از خروج خود اطمینان دارید؟</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getBoursecuActionIconSrc } from '@/utils/config'
|
||||
import { openGoldDayCalculator } from '@/composables/useGoldDayCalculator'
|
||||
import type { HomeQuickActionItem } from '../types'
|
||||
|
||||
export const boursecuQuickActions: HomeQuickActionItem[] = [
|
||||
@@ -44,4 +45,10 @@ export const boursecuQuickActions: HomeQuickActionItem[] = [
|
||||
iconSrc: getBoursecuActionIconSrc('convert'),
|
||||
route: { name: 'buyProduct' },
|
||||
},
|
||||
{
|
||||
id: 'calculator',
|
||||
label: 'محاسبهگر',
|
||||
icon: 'vuesax-linear:chart-2',
|
||||
handler: () => openGoldDayCalculator(),
|
||||
},
|
||||
]
|
||||
@@ -35,7 +35,13 @@ const resolvedActions = computed(() => {
|
||||
})
|
||||
|
||||
function handleClick(action: HomeQuickActionItem) {
|
||||
router.push(action.route)
|
||||
if (action.handler) {
|
||||
action.handler()
|
||||
return
|
||||
}
|
||||
if (action.route) {
|
||||
router.push(action.route)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ export interface HomeQuickActionItem {
|
||||
label: string
|
||||
icon: string
|
||||
iconSrc?: string
|
||||
route: { name: string; query?: Record<string, string> }
|
||||
route?: { name: string; query?: Record<string, string> }
|
||||
handler?: () => void
|
||||
}
|
||||
|
||||
export interface HomeLayoutSectionConfig {
|
||||
|
||||
@@ -70,7 +70,7 @@ import {
|
||||
isCountBasedProduct,
|
||||
} from '@/utils/productDealMethod'
|
||||
import { formatGoldAmount, usePriceWithoutType } from '@/utils/usePrice'
|
||||
import { persianToEnglishDigits } from '@/utils/persianDigits'
|
||||
import { normalizeDecimalInput } from '@/utils/persianDigits'
|
||||
|
||||
interface Props {
|
||||
item: CountPInventory
|
||||
@@ -121,7 +121,9 @@ function selectCountInput(e: FocusEvent) {
|
||||
|
||||
function onCountInput(raw: string | number) {
|
||||
countError.value = ''
|
||||
const normalized = persianToEnglishDigits(String(raw ?? '').trim())
|
||||
const normalized = normalizeDecimalInput(String(raw ?? '').trim(), {
|
||||
commaAsThousand: isCountBased.value,
|
||||
})
|
||||
if (!normalized) {
|
||||
props.item.count = 0 as any
|
||||
return
|
||||
|
||||
@@ -19,6 +19,7 @@ usePhysicalReceiveList()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Mobile shell: fill viewport and scroll inside the list view */
|
||||
.physical-receive-list-root {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
@@ -34,19 +35,19 @@ usePhysicalReceiveList()
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Desktop: let list grow with content; page scroll handles overflow */
|
||||
/* Desktop: fill available height; page scrolls if content exceeds */
|
||||
.physical-receive-list-root--desktop {
|
||||
flex: none;
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.physical-receive-list-root--desktop > * {
|
||||
flex: none;
|
||||
flex: 1 1 auto;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
.dpr {
|
||||
min-height: 100%;
|
||||
height: auto;
|
||||
height: 100%;
|
||||
background: #f6f7f9;
|
||||
color: #111827;
|
||||
overflow: visible;
|
||||
@@ -12,6 +12,8 @@
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 28px 36px 40px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,17 +51,38 @@ const { isDesktop } = usePhysicalReceiveShell()
|
||||
}
|
||||
|
||||
.physical-receive-page-root--desktop {
|
||||
display: block;
|
||||
height: auto;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
overflow: visible;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.physical-receive-page-root--desktop :deep(.physical-receive-list-root),
|
||||
.physical-receive-page-root--desktop :deep(.dpr-list) {
|
||||
height: auto;
|
||||
.physical-receive-page-root--desktop > .dpr {
|
||||
flex: 1 1 auto;
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.physical-receive-page-root--desktop :deep(.dpr__container) {
|
||||
flex: 1 1 auto;
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.physical-receive-page-root--desktop :deep(.physical-receive-list-root--desktop) {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.physical-receive-page-root--desktop :deep(.physical-receive-list-root--desktop > .dpr-list) {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
|
||||
<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>
|
||||
<span class="dptr-card__label">محصول:</span>
|
||||
<span class="dptr-card__value">{{ productTitle }}</span>
|
||||
</div>
|
||||
<div class="dptr-card__row">
|
||||
<span class="dptr-card__label">نوع هشدار:</span>
|
||||
<span class="dptr-card__value">{{ returnAction(item.action) }}</span>
|
||||
<span class="dptr-card__label">جهت قیمت:</span>
|
||||
<span class="dptr-card__value">{{ returnPriceCondition(item.price_condition) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -76,8 +76,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 mt-2">
|
||||
<BasicInfoItem is-line title="محصول" :value="productTitle" />
|
||||
<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">
|
||||
@@ -108,7 +108,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { usePrice } from '../../../../utils/usePrice'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { toJalali } from '../../../../utils/useDateTime'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import type { PriceTrigger } from '@/models/PriceTrigger'
|
||||
@@ -132,14 +132,16 @@ const props = withDefaults(
|
||||
|
||||
const emits = defineEmits(['edit', 'refresh'])
|
||||
|
||||
const productTitle = computed(() => props.item.product?.title || 'طلا')
|
||||
|
||||
onMounted(() => {
|
||||
isActive.value = props.item.is_active
|
||||
})
|
||||
|
||||
function changeActive(val: boolean) {
|
||||
async function changeActive(val: boolean) {
|
||||
try {
|
||||
loading.value = true
|
||||
updatePriceTrigger(props?.item.id, { is_active: val })
|
||||
await updatePriceTrigger(props?.item.id, { is_active: val })
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
isActive.value = !isActive.value
|
||||
@@ -171,15 +173,4 @@ async function deleteItem() {
|
||||
deleteLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function returnAction(val: string) {
|
||||
switch (val) {
|
||||
case 'sms':
|
||||
return 'پیامک'
|
||||
case 'email':
|
||||
return 'ایمیل'
|
||||
case 'notify':
|
||||
return 'نوتیفیکیشن'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
@finish="ok()"
|
||||
>
|
||||
<div class="flex flex-col gap-4 w-full">
|
||||
<a-form-item :rules="[required()]" label="محصول" name="product_id">
|
||||
<BasicSelect
|
||||
v-model:value="form.product_id"
|
||||
:items="products"
|
||||
key-name="id"
|
||||
placeholder="آبشده، سکه، نقره و ..."
|
||||
size="large"
|
||||
value-name="title"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :rules="[required()]" label="جهت قیمت" name="price_condition">
|
||||
<BasicSelect
|
||||
v-model:value="form.price_condition"
|
||||
@@ -27,12 +37,7 @@
|
||||
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>
|
||||
<p class="m-0 text-sm text-gray-500">پس از فعال شدن هشدار، پیامک به شماره موبایل شما ارسال میشود.</p>
|
||||
<a-form-item>
|
||||
<a-spin :spinning="loading">
|
||||
<BasicButton class="w-full" html-type="submit" type="primary">
|
||||
@@ -56,8 +61,8 @@ const {
|
||||
formRef,
|
||||
editId,
|
||||
loading,
|
||||
radioStyle,
|
||||
typeItems,
|
||||
products,
|
||||
ok,
|
||||
} = usePriceTriggerPageContext()
|
||||
</script>
|
||||
|
||||
@@ -2,12 +2,25 @@ import { inject, onMounted, provide, reactive, ref, type InjectionKey, type Ref
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { type FormInstance, message } from 'ant-design-vue'
|
||||
import { createPriceTrigger, getPriceTriggers, updatePriceTrigger } from '@/api/triggerPrice'
|
||||
import { getProducts } from '@/api/products'
|
||||
import type { SelectItem } from '@/models/SelectItem'
|
||||
import type { PriceTrigger } from '@/models/PriceTrigger'
|
||||
import type { Product } from '@/models/products'
|
||||
import { loading } from '@/utils'
|
||||
|
||||
function emptyForm(): PriceTrigger {
|
||||
return {
|
||||
product_id: undefined,
|
||||
price_condition: '',
|
||||
is_active: true,
|
||||
action: 'sms',
|
||||
target_price: '',
|
||||
}
|
||||
}
|
||||
|
||||
export interface PriceTriggerPageContext {
|
||||
items: Ref<PriceTrigger[]>
|
||||
products: Ref<Product[]>
|
||||
addTriggerVisible: Ref<boolean>
|
||||
editId: Ref<number | undefined>
|
||||
form: Ref<PriceTrigger>
|
||||
@@ -27,6 +40,7 @@ export const PRICE_TRIGGER_PAGE_CONTEXT_KEY: InjectionKey<PriceTriggerPageContex
|
||||
|
||||
export function usePriceTriggerPage() {
|
||||
const items = ref<PriceTrigger[]>([])
|
||||
const products = ref<Product[]>([])
|
||||
const addTriggerVisible = ref(false)
|
||||
const editId = ref<number | undefined>(undefined)
|
||||
const formRef = ref<FormInstance>()
|
||||
@@ -37,12 +51,7 @@ export function usePriceTriggerPage() {
|
||||
lineHeight: '30px',
|
||||
})
|
||||
|
||||
const form = ref<PriceTrigger>({
|
||||
price_condition: '',
|
||||
is_active: true,
|
||||
action: '',
|
||||
target_price: '',
|
||||
})
|
||||
const form = ref<PriceTrigger>(emptyForm())
|
||||
|
||||
const typeItems: SelectItem[] = [
|
||||
{ value: 'بزرگتر', key: '>' },
|
||||
@@ -51,12 +60,7 @@ export function usePriceTriggerPage() {
|
||||
]
|
||||
|
||||
function resetFields() {
|
||||
form.value = {
|
||||
price_condition: '',
|
||||
is_active: true,
|
||||
action: '',
|
||||
target_price: '',
|
||||
}
|
||||
form.value = emptyForm()
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
@@ -66,7 +70,10 @@ export function usePriceTriggerPage() {
|
||||
}
|
||||
|
||||
function openEdit(val: PriceTrigger) {
|
||||
form.value = cloneDeep(val)
|
||||
form.value = cloneDeep({
|
||||
...val,
|
||||
product_id: val.product_id ?? val.product?.id,
|
||||
})
|
||||
editId.value = form.value.id
|
||||
addTriggerVisible.value = true
|
||||
}
|
||||
@@ -82,10 +89,19 @@ export function usePriceTriggerPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProducts() {
|
||||
try {
|
||||
products.value = ((await getProducts()) as Product[]) ?? []
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function ok() {
|
||||
await formRef.value?.validateFields().then(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
form.value.action = 'sms'
|
||||
if (editId.value) {
|
||||
await updatePriceTrigger(editId.value, form.value)
|
||||
} else {
|
||||
@@ -104,10 +120,12 @@ export function usePriceTriggerPage() {
|
||||
|
||||
onMounted(() => {
|
||||
initItems()
|
||||
loadProducts()
|
||||
})
|
||||
|
||||
const context: PriceTriggerPageContext = {
|
||||
items,
|
||||
products,
|
||||
addTriggerVisible,
|
||||
editId,
|
||||
form,
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
<template>
|
||||
<div class="h-35rem overflow-y-auto">
|
||||
<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>-->
|
||||
<div class="flex justify-evenly w-full px-2 h-11 items-center bg-gray-50 relative rounded ">
|
||||
<div :class="isBuy?'text-secondary':'text-gray-400'" class="w-full text-center z-2"
|
||||
@click.stop="setIsBuy(true) "
|
||||
:style="!props?.ProductItem?.open_buy ? 'pointer-events: none; opacity: 0.5;' : ''">
|
||||
خرید {{ props?.ProductItem?.title }}
|
||||
</div>
|
||||
<div :class="!isBuy?'text-secondary':'text-gray-400'" class="w-full text-center z-2"
|
||||
@click.stop="setIsBuy(false) "
|
||||
:style="!props?.ProductItem?.open_sell ? 'pointer-events: none; opacity: 0.5;' : ''"
|
||||
<div class="h-[40rem] overflow-y-auto">
|
||||
<div class="bg-white px-3 pt-3 pb-1">
|
||||
<div class="flex w-full p-1 items-center border border-gray-200 rounded-full bg-white gap-1">
|
||||
<div
|
||||
class="w-full h-10 flex items-center justify-center font-bold rounded-full transition-all"
|
||||
:class="isBuy ? 'bg-green-600 text-white shadow-sm' : 'text-green-600'"
|
||||
@click.stop="setIsBuy(true)"
|
||||
:style="!props?.ProductItem?.open_buy ? 'pointer-events: none; opacity: 0.35;' : ''"
|
||||
>
|
||||
فروش {{ props?.ProductItem?.title }}
|
||||
خرید
|
||||
</div>
|
||||
<div
|
||||
class="w-full h-10 flex items-center justify-center font-bold rounded-full transition-all"
|
||||
:class="!isBuy ? 'bg-red-600 text-white shadow-sm' : 'text-red-600'"
|
||||
@click.stop="setIsBuy(false)"
|
||||
:style="!props?.ProductItem?.open_sell ? 'pointer-events: none; opacity: 0.35;' : ''"
|
||||
>
|
||||
فروش
|
||||
</div>
|
||||
<!-- <div ref="bganim" class="absolute bg-white h-8 w-1/2 rounded-lg " :style="`transform: translateX(${isbuy? '0':'-100%'}); left: ${isbuy? '0':'100%'}`"></div>-->
|
||||
<div ref="bganim" :style="useStyle()"
|
||||
class="absolute bg-white h-8 w-1/2 rounded-lg animate right"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="userStore" class=" mt-5 pb-2 mb-19vh">
|
||||
<div v-if="userStore" class=" mt-5 pb-8 mb-16vh">
|
||||
<!-- <a-spin :spinning="loading">-->
|
||||
<div
|
||||
v-if="!loading"
|
||||
class="w-full relative rounded-lg flex flex-col justify-center background-color-transition"
|
||||
class="w-full relative rounded-2xl flex flex-col justify-center background-color-transition"
|
||||
:class="[
|
||||
!isBuy ? 'bg-red-100' : 'bg-green-100',
|
||||
selectedMeltedPaymentProduct ? 'min-h-[120px] py-3' : 'h-[88px]',
|
||||
@@ -32,7 +32,16 @@
|
||||
>
|
||||
<img :src="img1" alt="" class="h-full min-h-[88px] absolute inset-0 w-full object-cover">
|
||||
<div class="w-full items-center flex flex-col relative z-1">
|
||||
<div class="text-gray-700 font-bold">{{ !isBuy ? 'قیمت فروش' : 'قیمت خرید' }}</div>
|
||||
<div class="flex items-center justify-center gap-2 flex-wrap px-3">
|
||||
<span class="text-gray-700 font-bold">{{ !isBuy ? 'قیمت فروش' : 'قیمت خرید' }}</span>
|
||||
<span
|
||||
v-if="props?.ProductItem?.title"
|
||||
class="text-sm font-extrabold px-3 py-1 rounded-full max-w-[14rem] truncate shadow-sm"
|
||||
:class="!isBuy ? 'bg-red-200/90 text-red-900' : 'bg-green-200/90 text-green-900'"
|
||||
>
|
||||
{{ props.ProductItem.title }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="price" class="flex gap-1 mt-3 text-xs items-center">
|
||||
<div :class="!isBuy? 'text-red-700' : 'text-green-700'" class="font-bold text-2xl color-transition">
|
||||
{{ isBuy ? usePriceWithoutType(buyTotalPrice) : usePriceWithoutType(sellTotalPrice) }}
|
||||
@@ -56,7 +65,7 @@
|
||||
</div>
|
||||
<!-- </a-spin>-->
|
||||
<a-form ref="refInput" :model="form" class="!mt-5" name="basic" @finish="okConfirm()">
|
||||
<div class="border-gray-200 border-1 rounded-xl p-5 relative">
|
||||
<div class="border-gray-200 border-1 rounded-2xl p-5 relative">
|
||||
<bg_main />
|
||||
<div class="z-100 relative">
|
||||
<a-form-item v-if="showDealMethodSelect" name="method" :rules="[required()]">
|
||||
@@ -281,7 +290,7 @@ import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||
import { justNumber2, justNumber3, required } from '@/utils/inputRules'
|
||||
import { persianToEnglishDigits } from '@/utils/persianDigits'
|
||||
import { parseDecimalInput } from '@/utils/persianDigits'
|
||||
import GoldSkeleton from '@/views/base/gold/components/GoldSkeleton.vue'
|
||||
import GoldLoading from '@/components/GoldLoading.vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
@@ -710,9 +719,6 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// تابع تبدیل اعداد فارسی به انگلیسی
|
||||
const persianToEnglish = persianToEnglishDigits
|
||||
|
||||
watch(() => form.value.goldValue, (newVal) => {
|
||||
if (newVal !== undefined && newVal !== null && newVal !== '') {
|
||||
calculateCash(newVal)
|
||||
@@ -759,10 +765,7 @@ function handleGoldValueChange(val: string | number | undefined) {
|
||||
form.value.goldValue = undefined
|
||||
return
|
||||
}
|
||||
// تبدیل اعداد فارسی به انگلیسی
|
||||
const valStr = val.toString()
|
||||
const convertedValue = persianToEnglish(valStr)
|
||||
const numericValue = parseFloat(convertedValue)
|
||||
const numericValue = parseDecimalInput(val, { commaAsThousand: false })
|
||||
form.value.goldValue = isNaN(numericValue) ? undefined : numericValue
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,52 @@ async function loadCategoryTabs() {
|
||||
}
|
||||
}
|
||||
|
||||
function productFileId(product: Product): number {
|
||||
return Number(product.file_id ?? product.file?.id ?? 0)
|
||||
}
|
||||
|
||||
function productHasDisplayImage(product: Product): boolean {
|
||||
const file = product.file
|
||||
if (file && (file.full_src || file.url || file.src)) {
|
||||
return true
|
||||
}
|
||||
return !!(product.img && /^https?:\/\//i.test(product.img))
|
||||
}
|
||||
|
||||
/** استریم گاهی file خالی با همان file_id میفرستد — عکس خوب HTTP را پاک نکن. */
|
||||
function mergeLiveProductList(incoming: Product[], current?: Product[]): Product[] {
|
||||
if (!current?.length) return incoming
|
||||
|
||||
const currentById = new Map(current.map((item) => [item.id, item]))
|
||||
return incoming.map((item) => {
|
||||
const prev = currentById.get(item.id)
|
||||
if (!prev) return item
|
||||
|
||||
const prevId = productFileId(prev)
|
||||
const nextId = productFileId(item)
|
||||
|
||||
if (!productHasDisplayImage(item) && productHasDisplayImage(prev)) {
|
||||
return {
|
||||
...item,
|
||||
file: prev.file,
|
||||
file_id: prev.file_id ?? prev.file?.id ?? item.file_id,
|
||||
img: prev.img,
|
||||
}
|
||||
}
|
||||
|
||||
if (prevId > nextId) {
|
||||
return {
|
||||
...item,
|
||||
file: prev.file,
|
||||
file_id: prev.file_id,
|
||||
img: prev.img,
|
||||
}
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
function applyProductsList(products: Product[]) {
|
||||
if (products.length) {
|
||||
productsCloseTime.value = products[0].auto_close_at
|
||||
@@ -141,8 +187,12 @@ export function applyProductsListFromSocket(products: Product[]) {
|
||||
return
|
||||
}
|
||||
|
||||
getProductsCatalogRef().value = products
|
||||
applyProductsList(products)
|
||||
const merged = mergeLiveProductList(
|
||||
products,
|
||||
getProductsCatalogRef().value.length ? getProductsCatalogRef().value : productsFirstItems.value,
|
||||
)
|
||||
getProductsCatalogRef().value = merged
|
||||
applyProductsList(merged)
|
||||
}
|
||||
|
||||
function stopProductListPolling() {
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div class="recovery-sites-list" :class="`recovery-sites-list--${variant}`">
|
||||
<p v-if="loading" class="recovery-sites-list__empty">
|
||||
در حال بارگذاری سایتها...
|
||||
</p>
|
||||
|
||||
<p v-else-if="isEmpty" class="recovery-sites-list__empty">
|
||||
سایتی برای نمایش ثبت نشده است.
|
||||
</p>
|
||||
|
||||
<button
|
||||
v-for="site in sites"
|
||||
:key="site.id"
|
||||
type="button"
|
||||
class="recovery-sites-list__item"
|
||||
@click="openSite(site.url)"
|
||||
>
|
||||
<span class="recovery-sites-list__icon">
|
||||
<BasicIcon icon="vuesax-linear:global" :size="variant === 'desktop' ? 20 : 18" />
|
||||
</span>
|
||||
<span class="recovery-sites-list__content">
|
||||
<span class="recovery-sites-list__name">{{ site.name }}</span>
|
||||
<span class="recovery-sites-list__url" dir="ltr">{{ site.url }}</span>
|
||||
</span>
|
||||
<BasicIcon
|
||||
class="recovery-sites-list__chevron"
|
||||
icon="vuesax-linear:export"
|
||||
:size="variant === 'desktop' ? 18 : 16"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useRecoverySitesPageContext } from '../composables/useRecoverySitesPage'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
variant?: 'mobile' | 'desktop'
|
||||
}>(),
|
||||
{
|
||||
variant: 'mobile',
|
||||
},
|
||||
)
|
||||
|
||||
const { sites, loading, isEmpty, openSite } = useRecoverySitesPageContext()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.recovery-sites-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.recovery-sites-list__empty {
|
||||
margin: 0;
|
||||
color: var(--app-text-secondary, #6b7280);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.8;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.recovery-sites-list__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
border: 1px solid rgba(var(--app-gold-rgb, 212, 175, 55), 0.16);
|
||||
border-radius: 0.875rem;
|
||||
background: #fff;
|
||||
padding: 0.875rem 1rem;
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.recovery-sites-list__item:hover {
|
||||
border-color: rgba(var(--app-gold-rgb, 212, 175, 55), 0.35);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.recovery-sites-list__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(var(--app-gold-rgb, 212, 175, 55), 0.12);
|
||||
color: var(--app-primary, #b8860b);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.recovery-sites-list__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.recovery-sites-list__name {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: var(--app-text-primary, #111827);
|
||||
}
|
||||
|
||||
.recovery-sites-list__url {
|
||||
font-size: 0.75rem;
|
||||
color: var(--app-text-secondary, #6b7280);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.recovery-sites-list__chevron {
|
||||
color: var(--app-text-secondary, #6b7280);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.recovery-sites-list--desktop .recovery-sites-list__item {
|
||||
padding: 1rem 1.125rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
computed,
|
||||
inject,
|
||||
onMounted,
|
||||
provide,
|
||||
ref,
|
||||
type ComputedRef,
|
||||
type InjectionKey,
|
||||
type Ref,
|
||||
} from 'vue'
|
||||
import {
|
||||
fetchTrackingSettings,
|
||||
resolveTrackingSystems,
|
||||
type TrackingSystemSite,
|
||||
} from '@/utils/trackingSystemsSettings'
|
||||
|
||||
export type ReygiriSite = TrackingSystemSite
|
||||
|
||||
export interface RecoverySitesPageContext {
|
||||
sites: Ref<ReygiriSite[]>
|
||||
loading: Ref<boolean>
|
||||
isEmpty: ComputedRef<boolean>
|
||||
openSite: (url: string) => void
|
||||
}
|
||||
|
||||
export const RECOVERY_SITES_PAGE_CONTEXT_KEY: InjectionKey<RecoverySitesPageContext> =
|
||||
Symbol('recoverySitesPageContext')
|
||||
|
||||
function normalizeExternalUrl(url: string) {
|
||||
const trimmed = url.trim()
|
||||
if (!trimmed) {
|
||||
return ''
|
||||
}
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
return trimmed
|
||||
}
|
||||
return `https://${trimmed.replace(/^\/\//, '')}`
|
||||
}
|
||||
|
||||
export function useRecoverySitesPage() {
|
||||
const sites = ref<ReygiriSite[]>([])
|
||||
const loading = ref(false)
|
||||
const isEmpty = computed(() => !loading.value && sites.value.length === 0)
|
||||
|
||||
async function loadSites() {
|
||||
loading.value = true
|
||||
try {
|
||||
const settings = await fetchTrackingSettings(true)
|
||||
sites.value = resolveTrackingSystems(settings)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
sites.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadSites()
|
||||
})
|
||||
|
||||
function openSite(url: string) {
|
||||
const normalized = normalizeExternalUrl(url)
|
||||
if (!normalized) {
|
||||
return
|
||||
}
|
||||
window.open(normalized, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
const context: RecoverySitesPageContext = {
|
||||
sites,
|
||||
loading,
|
||||
isEmpty,
|
||||
openSite,
|
||||
}
|
||||
|
||||
provide(RECOVERY_SITES_PAGE_CONTEXT_KEY, context)
|
||||
return context
|
||||
}
|
||||
|
||||
export function useRecoverySitesPageContext() {
|
||||
const context = inject(RECOVERY_SITES_PAGE_CONTEXT_KEY)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useRecoverySitesPageContext must be used within a page that calls useRecoverySitesPage()',
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="drecovery">
|
||||
<div class="drecovery__container">
|
||||
<header class="drecovery-topbar">
|
||||
<button type="button" class="drecovery-topbar__back" @click="router.back()">
|
||||
<BasicIcon icon="vuesax-linear:arrow-right-1" size="18" />
|
||||
<span>بازگشت</span>
|
||||
</button>
|
||||
<div class="drecovery-topbar__meta">
|
||||
<p class="drecovery-topbar__eyebrow">خدمات</p>
|
||||
<h1 class="drecovery-topbar__title">سایتهای ریگیری</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="drecovery-surface drecovery-main">
|
||||
<div class="drecovery-main__intro">
|
||||
<div class="drecovery-main__logo">
|
||||
<BasicIcon icon="vuesax-linear:global" size="28" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="drecovery-main__heading">سایتهای ریگیری</h2>
|
||||
<p class="drecovery-main__tagline">دسترسی به سایتهای مرتبط با ریگیری</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RecoverySitesList variant="desktop" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import router from '@/router'
|
||||
import RecoverySitesList from '../components/RecoverySitesList.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.drecovery {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
padding: 1.5rem 1.75rem 2rem;
|
||||
}
|
||||
|
||||
.drecovery__container {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.drecovery-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.drecovery-topbar__back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--app-text-secondary, #6b7280);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.drecovery-topbar__eyebrow {
|
||||
margin: 0 0 0.25rem;
|
||||
color: var(--app-text-secondary, #6b7280);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.drecovery-topbar__title {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.drecovery-surface {
|
||||
border: 1px solid rgba(var(--app-gold-rgb, 212, 175, 55), 0.18);
|
||||
border-radius: 1rem;
|
||||
background: var(--app-surface, #fff);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.drecovery-main__intro {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.drecovery-main__logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(var(--app-gold-rgb, 212, 175, 55), 0.12);
|
||||
color: var(--app-primary, #b8860b);
|
||||
}
|
||||
|
||||
.drecovery-main__heading {
|
||||
margin: 0 0 0.25rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.drecovery-main__tagline {
|
||||
margin: 0;
|
||||
color: var(--app-text-secondary, #6b7280);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="recovery-sites-page-root">
|
||||
<DesktopRecoverySitesView v-show="isDesktop" />
|
||||
<MobileRecoverySitesView v-show="!isDesktop" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { useRecoverySitesPage } from './composables/useRecoverySitesPage'
|
||||
import DesktopRecoverySitesView from './desktop/DesktopRecoverySitesView.vue'
|
||||
import MobileRecoverySitesView from './mobile/MobileRecoverySitesView.vue'
|
||||
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
|
||||
useRecoverySitesPage()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.recovery-sites-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.recovery-sites-page-root.main-with-bottom-chrome-inner {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="mobile-recovery-sites mobile-info-page">
|
||||
<div class="mobile-info-page__bg" aria-hidden="true">
|
||||
<img :src="img" alt="" />
|
||||
</div>
|
||||
|
||||
<MobilePageHeader title="سایتهای ریگیری" show-back />
|
||||
|
||||
<div class="mobile-recovery-sites__main mobile-info-page__main">
|
||||
<div class="mobile-recovery-sites__card">
|
||||
<RecoverySitesList variant="mobile" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
|
||||
import RecoverySitesList from '../components/RecoverySitesList.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mobile-recovery-sites__main {
|
||||
padding-bottom: calc(var(--app-bottom-chrome-height, 108px) + 1.5rem);
|
||||
}
|
||||
|
||||
.mobile-recovery-sites__card {
|
||||
border: 1px solid rgba(var(--app-gold-rgb, 212, 175, 55), 0.16);
|
||||
border-radius: 0.875rem;
|
||||
background: #fff;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
</style>
|
||||
@@ -106,6 +106,10 @@
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">مهلت ارسال فیش:</div>
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__value' : ''">{{ item.eaj }}</div>
|
||||
</div>
|
||||
<div v-if="item.note" :class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'">
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">توضیحات سند:</div>
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__value' : ''">{{ item.note }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
let activeFetchId = 0
|
||||
|
||||
export const transactionsBoursecuMode = ref(true)
|
||||
export const transactionPagination = ref<TableEngineInput>({ filters: [], limit: 7, page: 0 })
|
||||
export const transactionPagination = ref<TableEngineInput>({ filters: [], limit: 10, page: 0 })
|
||||
export const transactionItems = ref<Transaction[]>([])
|
||||
export const physicalReceiveItems = ref<Physicalreceive[]>([])
|
||||
export const transactionTableOutput = ref<TableEngineOutput<Transaction | Physicalreceive> | null>(
|
||||
@@ -151,7 +151,7 @@ export function resetTransactionsPageData() {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
transactionsBoursecuMode.value = true
|
||||
transactionPagination.value = { filters: [], limit: 7, page: 0 }
|
||||
transactionPagination.value = { filters: [], limit: 10, page: 0 }
|
||||
transactionItems.value = []
|
||||
physicalReceiveItems.value = []
|
||||
transactionTableOutput.value = null
|
||||
|
||||
@@ -191,7 +191,7 @@ export function buildBoursecuTransactionPagination(
|
||||
|
||||
return {
|
||||
page: 0,
|
||||
limit: 7,
|
||||
limit: 10,
|
||||
filters,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<div class="tv-board">
|
||||
<header class="tv-board__header">
|
||||
<h1 class="tv-board__title">{{ shopName }}</h1>
|
||||
<time class="tv-board__clock" dir="ltr">{{ clockText }}</time>
|
||||
</header>
|
||||
|
||||
<p v-if="statusBanner" class="tv-board__banner">{{ statusBanner }}</p>
|
||||
|
||||
<div v-if="loading" class="tv-board__state">
|
||||
<a-spin />
|
||||
</div>
|
||||
<div v-else-if="!rows.length" class="tv-board__state">
|
||||
<BasicIcon icon="vuesax-linear:monitor" size="42" />
|
||||
<span>محصولی برای نمایش روی تابلو انتخاب نشده است</span>
|
||||
</div>
|
||||
<div v-else class="tv-board__table">
|
||||
<div class="tv-board__row tv-board__row--head">
|
||||
<span>محصول</span>
|
||||
<span>خرید</span>
|
||||
<span>فروش</span>
|
||||
</div>
|
||||
<div v-for="row in rows" :key="row.id" class="tv-board__row">
|
||||
<span class="tv-board__name">{{ row.title }}</span>
|
||||
<span class="tv-board__buy" dir="ltr">{{ row.buyText }}</span>
|
||||
<span class="tv-board__sell" dir="ltr">{{ row.sellText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { getTvProducts } from '@/api/products'
|
||||
import type { Product } from '@/models/products'
|
||||
import { getConfigApp } from '@/utils/config'
|
||||
import { getProductBuyPrice, getProductSellPrice, isProductPriceInactive } from '@/utils/productPrice'
|
||||
import { lastPrice, getLastPrices, startLastPricePolling, stopLastPricePolling } from '@/utils/price'
|
||||
import { getPriceDisplayMessage, getPriceDisplayState, NO_QUOTE_MESSAGE } from '@/utils/storeHours'
|
||||
import { usePriceWithoutType } from '@/utils/usePrice'
|
||||
import { initPriceRealtimeChannel } from '@/utils/priceSocket'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
|
||||
const REFRESH_MS = 15000
|
||||
|
||||
const loading = ref(true)
|
||||
const products = ref<Product[]>([])
|
||||
const now = ref(Date.now())
|
||||
|
||||
let clockTimer: ReturnType<typeof setInterval> | undefined
|
||||
let listTimer: ReturnType<typeof setInterval> | undefined
|
||||
|
||||
const shopName = computed(
|
||||
() => String(getConfigApp('name_fa') || getConfigApp('name') || 'تابلو قیمت'),
|
||||
)
|
||||
|
||||
const clockText = computed(() =>
|
||||
new Date(now.value).toLocaleTimeString('fa-IR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
}),
|
||||
)
|
||||
|
||||
const statusBanner = computed(() => {
|
||||
void lastPrice.value
|
||||
const state = getPriceDisplayState()
|
||||
return state === 'open' ? '' : getPriceDisplayMessage(state)
|
||||
})
|
||||
|
||||
function isShowOnTv(product: Product): boolean {
|
||||
const value = product.show_on_tv as unknown
|
||||
return value === true || value === 1 || value === '1' || value === 'true'
|
||||
}
|
||||
|
||||
const rows = computed(() => {
|
||||
void lastPrice.value
|
||||
// API فقط محصولات تابلو را میدهد؛ اگر فیلد show_on_tv نبود، همه را نشان بده
|
||||
const list = products.value
|
||||
const marked = list.filter(isShowOnTv)
|
||||
const source = marked.length ? marked : list
|
||||
return source.map((product) => {
|
||||
if (isProductPriceInactive(product)) {
|
||||
return {
|
||||
id: product.id,
|
||||
title: product.title,
|
||||
buyText: NO_QUOTE_MESSAGE,
|
||||
sellText: NO_QUOTE_MESSAGE,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: product.id,
|
||||
title: product.title,
|
||||
buyText: usePriceWithoutType(getProductBuyPrice(product)),
|
||||
sellText: usePriceWithoutType(getProductSellPrice(product)),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
async function loadProducts() {
|
||||
try {
|
||||
const list = await getTvProducts()
|
||||
products.value = Array.isArray(list) ? list : []
|
||||
} catch {
|
||||
if (!products.value.length) {
|
||||
products.value = []
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const settingsStore = useSettingsStore()
|
||||
initPriceRealtimeChannel((key) => settingsStore.getSettingByKey(key))
|
||||
startLastPricePolling()
|
||||
void getLastPrices()
|
||||
void loadProducts()
|
||||
clockTimer = setInterval(() => {
|
||||
now.value = Date.now()
|
||||
}, 1000)
|
||||
listTimer = setInterval(() => {
|
||||
void loadProducts()
|
||||
}, REFRESH_MS)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopLastPricePolling()
|
||||
if (clockTimer) {
|
||||
clearInterval(clockTimer)
|
||||
}
|
||||
if (listTimer) {
|
||||
clearInterval(listTimer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tv-board {
|
||||
@apply min-h-screen w-full flex flex-col gap-5 p-6 text-white bg-gray-950;
|
||||
}
|
||||
|
||||
.tv-board__header {
|
||||
@apply flex items-center justify-between gap-4;
|
||||
}
|
||||
|
||||
.tv-board__title {
|
||||
@apply m-0 text-2xl font-bold;
|
||||
}
|
||||
|
||||
.tv-board__clock {
|
||||
@apply text-xl font-bold text-gray-300;
|
||||
}
|
||||
|
||||
.tv-board__banner {
|
||||
@apply m-0 rounded-xl bg-gray-800 px-4 py-3 text-center text-sm text-gray-200;
|
||||
}
|
||||
|
||||
.tv-board__state {
|
||||
@apply flex flex-1 flex-col items-center justify-center gap-3 text-gray-400;
|
||||
}
|
||||
|
||||
.tv-board__table {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
|
||||
.tv-board__row {
|
||||
@apply grid grid-cols-3 items-center gap-4 rounded-xl bg-gray-900 px-4 py-3;
|
||||
}
|
||||
|
||||
.tv-board__row--head {
|
||||
@apply bg-gray-800 text-sm text-gray-400;
|
||||
}
|
||||
|
||||
.tv-board__name {
|
||||
@apply text-lg font-bold;
|
||||
}
|
||||
|
||||
.tv-board__buy {
|
||||
@apply text-xl font-bold text-green-400;
|
||||
}
|
||||
|
||||
.tv-board__sell {
|
||||
@apply text-xl font-bold text-red-400;
|
||||
}
|
||||
</style>
|
||||
@@ -21,6 +21,8 @@ import { getReferralShareText, getReferralShareTitle, shareReferralText } from '
|
||||
import { normalizeAuthFormFields } from '@/utils/persianDigits'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { useSupportMessenger } from '@/composables/useSupportMessenger'
|
||||
import { useTrackingSystemsFeature } from '@/composables/useTrackingSystemsFeature'
|
||||
import { openGoldDayCalculator } from '@/composables/useGoldDayCalculator'
|
||||
import { isUserSupportChatEnabled, unreadSupportCount } from '@/utils/chat'
|
||||
import { resetHomePageData } from '../../home/composables/useHomePageData'
|
||||
import { resetWalletPageData } from '../../wallet/composables/useWalletPageData'
|
||||
@@ -87,6 +89,7 @@ export function useUserPage() {
|
||||
const showLogoutConfirm = ref(false)
|
||||
const localAvatarUrl = ref<string | null>(null)
|
||||
const { openSupportMessenger } = useSupportMessenger()
|
||||
const { trackingSystemsEnabled } = useTrackingSystemsFeature()
|
||||
|
||||
const profileAvatarUrl = computed(
|
||||
() => localAvatarUrl.value || getUserAvatarUrl(userStore.userInfo),
|
||||
@@ -124,6 +127,13 @@ export function useUserPage() {
|
||||
onClick: () => { editPasswordVisible.value = true },
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'goldDayCalculator',
|
||||
label: 'قیمت روز طلا',
|
||||
icon: 'vuesax-linear:chart-2',
|
||||
onClick: () => openGoldDayCalculator(),
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'address',
|
||||
label: 'آدرس ها',
|
||||
@@ -131,13 +141,6 @@ export function useUserPage() {
|
||||
onClick: () => router.push({ name: 'address' }),
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'notify',
|
||||
label: 'هشدار قیمت',
|
||||
icon: 'vuesax-linear:bill',
|
||||
onClick: () => router.push({ name: 'notifyTrigger' }),
|
||||
visible: settingsStore.isSettingTruthy('has_page_price_trigger'),
|
||||
},
|
||||
{
|
||||
id: 'piggy',
|
||||
label: 'قلکها',
|
||||
@@ -170,6 +173,20 @@ export function useUserPage() {
|
||||
onClick: () => router.push({ name: 'physicalReceive' }),
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'giftCard',
|
||||
label: 'ثبت کارت هدیه',
|
||||
icon: 'vuesax-linear:gift',
|
||||
onClick: () => router.push({ name: 'giftCard' }),
|
||||
visible: settingsStore.isSettingTruthy('has_page_gift_card'),
|
||||
},
|
||||
{
|
||||
id: 'recoverySites',
|
||||
label: 'سایتهای ریگیری',
|
||||
icon: 'vuesax-linear:global',
|
||||
onClick: () => router.push({ name: 'recoverySites' }),
|
||||
visible: trackingSystemsEnabled.value,
|
||||
},
|
||||
{
|
||||
id: 'rules',
|
||||
label: 'قوانین و مقررات',
|
||||
@@ -227,11 +244,13 @@ export function useUserPage() {
|
||||
try {
|
||||
loading.value = true
|
||||
normalizeAuthFormFields(userFormState.value)
|
||||
await updateMe(userFormState.value)
|
||||
const res: any = await updateMe(userFormState.value)
|
||||
await userStore.getMyInfo()
|
||||
localAvatarUrl.value = getUserAvatarUrl(userStore.userInfo)
|
||||
editPassVisible.value = false
|
||||
message.success('اطلاعات شما با موفقیت به روز رسانی شد')
|
||||
message.success(
|
||||
res?.message || 'اطلاعات شما با موفقیت به روز رسانی شد',
|
||||
)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
|
||||
@@ -18,12 +18,11 @@
|
||||
</div>
|
||||
|
||||
<div class="deposit-guide__accounts">
|
||||
<div class="deposit-guide__account-row">
|
||||
<div v-if="hasDepositCardValue" class="deposit-guide__account-row">
|
||||
<span class="deposit-guide__account-label">{{ depositCardLabel }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="deposit-guide__account-value"
|
||||
:class="{ 'deposit-guide__account-value--empty': !hasDepositCardValue }"
|
||||
:aria-label="`کپی ${depositCardLabel}`"
|
||||
@click="copyDepositCardNumber"
|
||||
>
|
||||
@@ -33,17 +32,33 @@
|
||||
size="18"
|
||||
color="#15803d"
|
||||
class="deposit-guide__copy-icon"
|
||||
:class="{ 'deposit-guide__copy-icon--disabled': !hasDepositCardValue }"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="deposit-guide__account-row">
|
||||
<div v-if="hasDepositAccountNumberValue" class="deposit-guide__account-row">
|
||||
<span class="deposit-guide__account-label">{{ depositAccountNumberLabel }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="deposit-guide__account-value"
|
||||
:aria-label="`کپی ${depositAccountNumberLabel}`"
|
||||
@click="copyDepositAccountNumber"
|
||||
>
|
||||
<span class="deposit-guide__account-text" dir="ltr">{{ depositAccountNumber }}</span>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:copy"
|
||||
size="18"
|
||||
color="#15803d"
|
||||
class="deposit-guide__copy-icon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="hasDepositShebaValue" class="deposit-guide__account-row">
|
||||
<span class="deposit-guide__account-label">{{ depositShebaLabel }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="deposit-guide__account-value"
|
||||
:class="{ 'deposit-guide__account-value--empty': !hasDepositShebaValue }"
|
||||
:aria-label="`کپی ${depositShebaLabel}`"
|
||||
@click="copyDepositShebaNumber"
|
||||
>
|
||||
@@ -53,7 +68,6 @@
|
||||
size="18"
|
||||
color="#15803d"
|
||||
class="deposit-guide__copy-icon"
|
||||
:class="{ 'deposit-guide__copy-icon--disabled': !hasDepositShebaValue }"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
@@ -119,12 +133,16 @@ const props = withDefaults(
|
||||
|
||||
const {
|
||||
depositCardNumber,
|
||||
depositAccountNumber,
|
||||
depositSheba,
|
||||
depositCardLabel,
|
||||
depositAccountNumberLabel,
|
||||
depositShebaLabel,
|
||||
hasDepositCardValue,
|
||||
hasDepositAccountNumberValue,
|
||||
hasDepositShebaValue,
|
||||
copyDepositCardNumber,
|
||||
copyDepositAccountNumber,
|
||||
copyDepositShebaNumber,
|
||||
hasMessengerLink,
|
||||
openDepositSupportMessenger,
|
||||
|
||||
@@ -146,7 +146,7 @@ export function useWalletPage() {
|
||||
async function addCard() {
|
||||
try {
|
||||
loading.value = true
|
||||
await createCard(cardForm.value)
|
||||
const res: any = await createCard(cardForm.value)
|
||||
await getCards()
|
||||
showAddCard.value = false
|
||||
cardForm.value = {
|
||||
@@ -156,7 +156,9 @@ export function useWalletPage() {
|
||||
cart_number: '',
|
||||
shaba_number: '',
|
||||
}
|
||||
message.success('کارت وارد شده با موفقیت افزوده شد و پس از تایید نمایش داده خواهد شد.')
|
||||
message.success(
|
||||
res?.message || 'اطلاعات کارت بانکی شما ثبت شد و پس از تایید ادمین فعال سازی خواهد شد',
|
||||
)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user