Merge origin/modular into feature/responsive-layout-refactor.
Keep the responsive layout refactor while bringing in modular updates for support UI, pricing, categories, and config loading. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Vendored
-4
@@ -14,16 +14,13 @@ declare module 'vue' {
|
|||||||
AInput: typeof import('ant-design-vue/es')['Input']
|
AInput: typeof import('ant-design-vue/es')['Input']
|
||||||
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
||||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
AModal: typeof import('ant-design-vue/es')['Modal']
|
||||||
ARadio: typeof import('ant-design-vue/es')['Radio']
|
|
||||||
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
||||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
||||||
ASlider: typeof import('ant-design-vue/es')['Slider']
|
ASlider: typeof import('ant-design-vue/es')['Slider']
|
||||||
ASpin: typeof import('ant-design-vue/es')['Spin']
|
ASpin: typeof import('ant-design-vue/es')['Spin']
|
||||||
ASwitch: typeof import('ant-design-vue/es')['Switch']
|
|
||||||
ATag: typeof import('ant-design-vue/es')['Tag']
|
ATag: typeof import('ant-design-vue/es')['Tag']
|
||||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
|
||||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||||
BaseDatePicker: typeof import('./src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue')['default']
|
BaseDatePicker: typeof import('./src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue')['default']
|
||||||
Basic: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/basic.vue')['default']
|
Basic: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/basic.vue')['default']
|
||||||
@@ -69,6 +66,5 @@ declare module 'vue' {
|
|||||||
TransactionSkeleton: typeof import('./src/components/TransactionSkeleton.vue')['default']
|
TransactionSkeleton: typeof import('./src/components/TransactionSkeleton.vue')['default']
|
||||||
VanActionSheet: typeof import('vant/es')['ActionSheet']
|
VanActionSheet: typeof import('vant/es')['ActionSheet']
|
||||||
VanDialog: typeof import('vant/es')['Dialog']
|
VanDialog: typeof import('vant/es')['Dialog']
|
||||||
VanField: typeof import('vant/es')['Field']
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+137
-19
@@ -71,15 +71,23 @@
|
|||||||
<div class="home_appear_fade_in_up mt-2">نسخه جدید منتشر شد!</div>
|
<div class="home_appear_fade_in_up mt-2">نسخه جدید منتشر شد!</div>
|
||||||
</div>
|
</div>
|
||||||
</van-dialog>
|
</van-dialog>
|
||||||
<a v-if="config.config?.name === 'garoosi'" href="https://ble.ir/garoosigallery" target="_blank"
|
<div
|
||||||
id="support-button">
|
v-if="showSupportButton && !showSplash"
|
||||||
<div class="bg-green-100 text-green-800 flex gap-2 p-2 rounded-lg mr-2">
|
id="support-button"
|
||||||
<span>پشتیبانی</span>
|
class="draggable-support-button"
|
||||||
<div class="w-5">
|
:class="{ 'is-dragging': supportDragging }"
|
||||||
<img src="/src/assets/img/support.png" alt=""/>
|
:style="supportButtonStyle"
|
||||||
</div>
|
title="پشتیبانی"
|
||||||
|
@pointerdown="onSupportPointerDown"
|
||||||
|
@pointermove="onSupportPointerMove"
|
||||||
|
@pointerup="handleSupportPointerUp"
|
||||||
|
@pointercancel="onSupportPointerCancel"
|
||||||
|
>
|
||||||
|
<div class="support-button-inner" :style="{ '--support-accent': supportAccentColor }">
|
||||||
|
<BasicIcon :icon="supportIcon" size="18" class="support-button-icon" />
|
||||||
|
<span class="support-button-label">پشتیبانی</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!showSplash"
|
v-if="!showSplash"
|
||||||
@@ -114,6 +122,12 @@ import {getProjectFontFamily, getProjectLoginLogo, getProjectSplashBackground, g
|
|||||||
import {ConfigProvider as AConfigProvider} from 'ant-design-vue'
|
import {ConfigProvider as AConfigProvider} from 'ant-design-vue'
|
||||||
import BasicIcon from "@/components/BasicPacks/BasicIcon.vue";
|
import BasicIcon from "@/components/BasicPacks/BasicIcon.vue";
|
||||||
import { useApplyDesktopLayoutClass } from '@/layouts/composables/useAppLayout'
|
import { useApplyDesktopLayoutClass } from '@/layouts/composables/useAppLayout'
|
||||||
|
import {
|
||||||
|
buildSupportMessengerUrl,
|
||||||
|
loadSupportMessengerFromSettings,
|
||||||
|
supportMessengerSettings,
|
||||||
|
} from '@/utils/supportMessenger'
|
||||||
|
import {useDraggableFloatingButton} from '@/utils/useDraggableFloatingButton'
|
||||||
|
|
||||||
useApplyDesktopLayoutClass()
|
useApplyDesktopLayoutClass()
|
||||||
|
|
||||||
@@ -129,6 +143,78 @@ const splashIndicator = computed(() => h(LoadingOutlined, {
|
|||||||
const showSplash = ref(true)
|
const showSplash = ref(true)
|
||||||
const popVisible = ref(false)
|
const popVisible = ref(false)
|
||||||
|
|
||||||
|
const GAROOSI_SUPPORT_URL = 'https://ble.ir/garoosigallery'
|
||||||
|
|
||||||
|
const supportLink = computed(() => {
|
||||||
|
if (config.config?.name === 'garoosi') {
|
||||||
|
return GAROOSI_SUPPORT_URL
|
||||||
|
}
|
||||||
|
return buildSupportMessengerUrl(
|
||||||
|
supportMessengerSettings.value.messenger,
|
||||||
|
supportMessengerSettings.value.messengerId
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const showSupportButton = computed(() => !!supportLink.value)
|
||||||
|
|
||||||
|
const supportIcon = computed(() => {
|
||||||
|
const messenger = (supportMessengerSettings.value.messenger ?? '').toLowerCase()
|
||||||
|
switch (messenger) {
|
||||||
|
case 'whatsapp':
|
||||||
|
return 'vuesax-linear:whatsapp'
|
||||||
|
case 'telegram':
|
||||||
|
return 'vuesax-linear:send-2'
|
||||||
|
case 'bale':
|
||||||
|
return 'vuesax-linear:messages-2'
|
||||||
|
default:
|
||||||
|
return 'vuesax-linear:messages-2'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const supportAccentColor = computed(() => {
|
||||||
|
const messenger = (supportMessengerSettings.value.messenger ?? '').toLowerCase()
|
||||||
|
switch (messenger) {
|
||||||
|
case 'whatsapp':
|
||||||
|
return '#22c55e'
|
||||||
|
case 'telegram':
|
||||||
|
return '#0ea5e9'
|
||||||
|
case 'bale':
|
||||||
|
return '#16a34a'
|
||||||
|
case 'rubika':
|
||||||
|
return '#7c3aed'
|
||||||
|
case 'eitaa':
|
||||||
|
return '#f59e0b'
|
||||||
|
case 'soroush':
|
||||||
|
return '#2563eb'
|
||||||
|
default:
|
||||||
|
return config.config?.color || '#334155'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
dragging: supportDragging,
|
||||||
|
fabStyle: supportButtonStyle,
|
||||||
|
handlePointerDown: onSupportPointerDown,
|
||||||
|
handlePointerMove: onSupportPointerMove,
|
||||||
|
handlePointerUp: supportPointerUp,
|
||||||
|
handlePointerCancel: onSupportPointerCancel
|
||||||
|
} = useDraggableFloatingButton('khaneghi_support_button_position', {
|
||||||
|
width: 118,
|
||||||
|
height: 42,
|
||||||
|
defaultBottomOffset: 120
|
||||||
|
})
|
||||||
|
|
||||||
|
function openSupportLink() {
|
||||||
|
if (!supportLink.value) return
|
||||||
|
window.open(supportLink.value, '_blank', 'noopener,noreferrer')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSupportPointerUp(event: PointerEvent) {
|
||||||
|
if (supportPointerUp(event)) {
|
||||||
|
openSupportLink()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const antTheme = computed(() => {
|
const antTheme = computed(() => {
|
||||||
if (!config.config?.color) return null
|
if (!config.config?.color) return null
|
||||||
|
|
||||||
@@ -154,6 +240,7 @@ const antTheme = computed(() => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await config.getConfigData()
|
await config.getConfigData()
|
||||||
|
await loadSupportMessengerFromSettings()
|
||||||
console.log(config?.config?.color)
|
console.log(config?.config?.color)
|
||||||
checkVersion()
|
checkVersion()
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -242,18 +329,49 @@ function update() {
|
|||||||
|
|
||||||
#support-button {
|
#support-button {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: calc(80px + var(--app-credit-footer-offset, 28px) + 12px);
|
|
||||||
right: 20px;
|
|
||||||
width: 70px;
|
|
||||||
height: 70px;
|
|
||||||
//border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
//box-shadow: 0 4px 10px rgba(0,0,0,0.3);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
border-radius: 9999px;
|
||||||
|
|
||||||
|
&.is-dragging {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-button-inner {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 9px 14px 9px 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||||
|
border-radius: 9999px;
|
||||||
|
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.14);
|
||||||
|
color: #1e293b;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
direction: rtl;
|
||||||
|
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
||||||
|
|
||||||
|
.support-button-icon {
|
||||||
|
color: var(--support-accent, #334155);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-button-label {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#support-button:not(.is-dragging):hover .support-button-inner {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
#support-button.is-dragging .support-button-inner {
|
||||||
|
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 426px) and (max-width: 767.98px) {
|
@media screen and (min-width: 426px) and (max-width: 767.98px) {
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import useAxios from '@/utils/useAxios'
|
||||||
|
|
||||||
|
export interface StoreCategory {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
sort_order?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getStoreCategories = async (): Promise<StoreCategory[]> =>
|
||||||
|
await useAxios.get<StoreCategory[]>('/customer/categories')
|
||||||
@@ -107,6 +107,22 @@
|
|||||||
if (metaTheme) metaTheme.setAttribute('content', splashBg)
|
if (metaTheme) metaTheme.setAttribute('content', splashBg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config?.user?.name_fa) {
|
||||||
|
document.title = config.user.name_fa
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config?.user?.avatar) {
|
||||||
|
const faviconUrl = config.user.avatar
|
||||||
|
const existingIcons = document.querySelectorAll('link[rel*="icon"]')
|
||||||
|
existingIcons.forEach((icon) => icon.remove())
|
||||||
|
|
||||||
|
const link = document.createElement('link')
|
||||||
|
link.rel = 'icon'
|
||||||
|
link.type = 'image/png'
|
||||||
|
link.href = faviconUrl + '?v=' + Date.now()
|
||||||
|
document.head.appendChild(link)
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve the promise
|
// Resolve the promise
|
||||||
if (configResolve) {
|
if (configResolve) {
|
||||||
configResolve(config)
|
configResolve(config)
|
||||||
|
|||||||
@@ -27,6 +27,5 @@ export interface Price {
|
|||||||
closed?: boolean
|
closed?: boolean
|
||||||
time?: string
|
time?: string
|
||||||
main_product?: number
|
main_product?: number
|
||||||
taban_gohar?: number
|
|
||||||
montzar?: number
|
montzar?: number
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,7 @@ export interface Product {
|
|||||||
max_order_cash?: number | null
|
max_order_cash?: number | null
|
||||||
direct_price: number | undefined
|
direct_price: number | undefined
|
||||||
price_offset_percent?: number | null
|
price_offset_percent?: number | null
|
||||||
|
use_last_price_if_zero?: boolean
|
||||||
reading_method: any
|
reading_method: any
|
||||||
created_at: any
|
created_at: any
|
||||||
updated_at: any
|
updated_at: any
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { getStoreCategories } from '@/api/categories'
|
||||||
|
|
||||||
|
/** تبهای فیلتر محصول — فقط از API دستهبندی (هماهنگ با پنل ادمین) */
|
||||||
|
export async function loadProductCategoryTabNames(): Promise<string[]> {
|
||||||
|
const categories = await getStoreCategories()
|
||||||
|
return categories.map((item) => item.name)
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ import {computed, ref} from 'vue'
|
|||||||
import type {Rule} from 'ant-design-vue/es/form'
|
import type {Rule} from 'ant-design-vue/es/form'
|
||||||
import {useWalletStore} from "@/store/walletStore";
|
import {useWalletStore} from "@/store/walletStore";
|
||||||
import { isCashLikeDealMethod, isMainMetalWalletDealMethod, getInsufficientCashWalletMessage, getInsufficientGoldWalletMessage } from '@/utils/productDealMethod'
|
import { isCashLikeDealMethod, isMainMetalWalletDealMethod, getInsufficientCashWalletMessage, getInsufficientGoldWalletMessage } from '@/utils/productDealMethod'
|
||||||
|
import { getCashWalletBalance } from '@/utils/productAssets'
|
||||||
|
|
||||||
export const mobileValidation = (title?: string): Rule => {
|
export const mobileValidation = (title?: string): Rule => {
|
||||||
const label = title ?? 'شماره تلفن'
|
const label = title ?? 'شماره تلفن'
|
||||||
@@ -160,13 +161,14 @@ const checkNumber = async (_rule: Rule, value: number) => {
|
|||||||
return Promise.reject('استفاده از اعداد اعشاری مجاز نمی باشد')
|
return Promise.reject('استفاده از اعداد اعشاری مجاز نمی باشد')
|
||||||
}
|
}
|
||||||
if (isBuyP){
|
if (isBuyP){
|
||||||
if (!walletCash?.walletInfo?.cash && isCashLikeDealMethod(methodP)){
|
const availableCash = getCashWalletBalance(walletCash?.walletInfo)
|
||||||
|
if (!availableCash && isCashLikeDealMethod(methodP)){
|
||||||
return Promise.reject(getInsufficientCashWalletMessage(groupTypeP))
|
return Promise.reject(getInsufficientCashWalletMessage(groupTypeP))
|
||||||
}
|
}
|
||||||
if (!walletCash?.walletInfo?.gold && isMainMetalWalletDealMethod(methodP)){
|
if (!walletCash?.walletInfo?.gold && isMainMetalWalletDealMethod(methodP)){
|
||||||
return Promise.reject(getInsufficientGoldWalletMessage(groupTypeP, methodP))
|
return Promise.reject(getInsufficientGoldWalletMessage(groupTypeP, methodP))
|
||||||
}
|
}
|
||||||
if (priceP * value > walletCash?.walletInfo?.cash && isCashLikeDealMethod(methodP))
|
if (priceP * value > availableCash && isCashLikeDealMethod(methodP))
|
||||||
return Promise.reject(getInsufficientCashWalletMessage(groupTypeP))
|
return Promise.reject(getInsufficientCashWalletMessage(groupTypeP))
|
||||||
if (amountP * value > walletCash?.walletInfo?.gold && isMainMetalWalletDealMethod(methodP))
|
if (amountP * value > walletCash?.walletInfo?.gold && isMainMetalWalletDealMethod(methodP))
|
||||||
return Promise.reject(getInsufficientGoldWalletMessage(groupTypeP, methodP))
|
return Promise.reject(getInsufficientGoldWalletMessage(groupTypeP, methodP))
|
||||||
|
|||||||
@@ -335,17 +335,32 @@ export function getTotalMetalWeight(
|
|||||||
return Number(walletInfo?.treasure ?? walletInfo?.gold_sum ?? walletInfo?.count_gold ?? 0)
|
return Number(walletInfo?.treasure ?? walletInfo?.gold_sum ?? walletInfo?.count_gold ?? 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** فقط موجودی نقدی کیف اصلی — فیلد gold (مثل 0.3 کیلوگرم) جزو دارایی ریالی نیست */
|
/** موجودی ریالی کیف اصلی — با main_wallet_products فیلد cash ممکن است کیف محصول باشد */
|
||||||
export function getRialCashAmount(
|
export function getRialCashAmount(
|
||||||
walletInfo?: Record<string, unknown> | null,
|
walletInfo?: Record<string, unknown> | null,
|
||||||
wallets?: UserWalletLine[] | null
|
wallets?: UserWalletLine[] | null
|
||||||
): number {
|
): number {
|
||||||
|
const aggregated = walletInfo?.count_cash_wallet ?? walletInfo?.count_cash
|
||||||
|
if (aggregated != null && aggregated !== '') {
|
||||||
|
const parsed = Number(aggregated)
|
||||||
|
if (!Number.isNaN(parsed)) {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const mainCash = getMainWalletCash(wallets)
|
const mainCash = getMainWalletCash(wallets)
|
||||||
if (mainCash > 0) {
|
if (mainCash > 0) {
|
||||||
return mainCash
|
return mainCash
|
||||||
}
|
}
|
||||||
|
|
||||||
return Number(walletInfo?.count_cash ?? walletInfo?.cash ?? 0)
|
return Number(walletInfo?.cash ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCashWalletBalance(
|
||||||
|
walletInfo?: Record<string, unknown> | null,
|
||||||
|
wallets?: UserWalletLine[] | null
|
||||||
|
): number {
|
||||||
|
return getRialCashAmount(walletInfo, wallets)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTotalRialEquivalent(walletInfo?: Record<string, unknown> | null): number {
|
export function getTotalRialEquivalent(walletInfo?: Record<string, unknown> | null): number {
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ export const UNIFIED_PRODUCT_CATEGORIES = [
|
|||||||
'گوی',
|
'گوی',
|
||||||
'سکه',
|
'سکه',
|
||||||
'سکه پارسیان',
|
'سکه پارسیان',
|
||||||
|
'شمش',
|
||||||
|
'شمش ونوس',
|
||||||
|
'شمش برنا',
|
||||||
'مس',
|
'مس',
|
||||||
'نقره',
|
'نقره',
|
||||||
] as const
|
] as const
|
||||||
|
|||||||
+91
-17
@@ -2,6 +2,10 @@ import type { Product } from '@/models/products'
|
|||||||
import { getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@/utils/price'
|
import { getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@/utils/price'
|
||||||
import { isMetalCommodityGroup, isMeltedProductUsingAppPrice, isMeltedTradeGroup } from '@/utils/productCategories'
|
import { isMetalCommodityGroup, isMeltedProductUsingAppPrice, isMeltedTradeGroup } from '@/utils/productCategories'
|
||||||
|
|
||||||
|
function productUsesLastPriceIfZero(product: Product): boolean {
|
||||||
|
return Boolean(product.use_last_price_if_zero)
|
||||||
|
}
|
||||||
|
|
||||||
export function resolvePriceOffsetRatio(offsetPercent?: number | null) {
|
export function resolvePriceOffsetRatio(offsetPercent?: number | null) {
|
||||||
const offset = Number(offsetPercent ?? 0)
|
const offset = Number(offsetPercent ?? 0)
|
||||||
if (!offset) {
|
if (!offset) {
|
||||||
@@ -35,44 +39,114 @@ export function applyChartPriceOffset(basePrice: number, offsetPercent?: number
|
|||||||
return Math.round(basePrice + basePrice * ratio)
|
return Math.round(basePrice + basePrice * ratio)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** قیمت خرید نمایشی محصول (همان مقداری که در لیست و اکشنشیت نشان داده میشود) */
|
/** قیمت پایه بدون کارمزد — اگر صفر باشد نباید فقط cash_fee بهعنوان قیمت نمایش داده شود */
|
||||||
export function getProductBuyPrice(product: Product): number {
|
export function getProductBaseBuyPrice(product: Product): number {
|
||||||
if (isMeltedProductUsingAppPrice(product)) {
|
if (isMeltedProductUsingAppPrice(product)) {
|
||||||
const base = applyPriceOffsetPercent(getTradeBuyPrice(lastPrice.value), product.price_offset_percent)
|
const apiPrice = Number(product.price ?? 0)
|
||||||
return base + (product.cash_fee_buy ?? 0)
|
const useLast = productUsesLastPriceIfZero(product)
|
||||||
|
|
||||||
|
if (!useLast) {
|
||||||
|
const liveBuy = getTradeBuyPrice(lastPrice.value)
|
||||||
|
if (liveBuy > 0) {
|
||||||
|
return applyPriceOffsetPercent(liveBuy, product.price_offset_percent)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiPrice > 0) {
|
||||||
|
return apiPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
const liveBuy = getTradeBuyPrice(lastPrice.value)
|
||||||
|
if (liveBuy > 0) {
|
||||||
|
return applyPriceOffsetPercent(liveBuy, product.price_offset_percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMeltedTradeGroup(product.group_type)) {
|
if (isMeltedTradeGroup(product.group_type)) {
|
||||||
return (product.price ?? 0) + (product.cash_fee_buy ?? 0)
|
return Number(product.price ?? 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMetalCommodityGroup(product.group_type)) {
|
if (isMetalCommodityGroup(product.group_type)) {
|
||||||
const base = (product.price_profit ?? 0) + (product.price ?? 0)
|
return Number(product.price_profit ?? 0) + Number(product.price ?? 0)
|
||||||
return base + (product.cash_fee_buy ?? 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const basePrice = (product.price ?? 0) + (product.cash_fee_buy ?? 0)
|
return Number(product.price ?? 0)
|
||||||
return product.direct_price === 0
|
|
||||||
? basePrice * (product.amount ?? 0)
|
|
||||||
: basePrice
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** قیمت فروش نمایشی محصول آبشدهای */
|
/** قیمت خرید نمایشی محصول (همان مقداری که در لیست و اکشنشیت نشان داده میشود) */
|
||||||
export function getProductSellPrice(product: Product): number {
|
export function getProductBuyPrice(product: Product): number {
|
||||||
|
const base = getProductBaseBuyPrice(product)
|
||||||
|
if (base <= 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const fee = Number(product.cash_fee_buy ?? 0)
|
||||||
|
|
||||||
|
if (
|
||||||
|
isMeltedProductUsingAppPrice(product)
|
||||||
|
|| isMeltedTradeGroup(product.group_type)
|
||||||
|
|| isMetalCommodityGroup(product.group_type)
|
||||||
|
) {
|
||||||
|
return base + fee
|
||||||
|
}
|
||||||
|
|
||||||
|
const withFee = base + fee
|
||||||
|
return product.direct_price === 0
|
||||||
|
? withFee * Number(product.amount ?? 0)
|
||||||
|
: withFee
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProductBaseSellPrice(product: Product): number {
|
||||||
if (isMeltedProductUsingAppPrice(product)) {
|
if (isMeltedProductUsingAppPrice(product)) {
|
||||||
const base = applyPriceOffsetPercent(getTradeSellPrice(lastPrice.value), product.price_offset_percent)
|
const apiPrice = Number(product.price ?? 0)
|
||||||
return base - (product.cash_fee_sell ?? 0)
|
const useLast = productUsesLastPriceIfZero(product)
|
||||||
|
|
||||||
|
if (!useLast) {
|
||||||
|
const liveSell = getTradeSellPrice(lastPrice.value)
|
||||||
|
if (liveSell > 0) {
|
||||||
|
return applyPriceOffsetPercent(liveSell, product.price_offset_percent)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiPrice > 0) {
|
||||||
|
return apiPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
const liveSell = getTradeSellPrice(lastPrice.value)
|
||||||
|
if (liveSell > 0) {
|
||||||
|
return applyPriceOffsetPercent(liveSell, product.price_offset_percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMeltedTradeGroup(product.group_type)) {
|
if (isMeltedTradeGroup(product.group_type)) {
|
||||||
return (product.price ?? 0) - (product.cash_fee_sell ?? 0)
|
return Number(product.price ?? 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return getTradeSellPrice(lastPrice.value)
|
return getTradeSellPrice(lastPrice.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** قیمت فروش نمایشی محصول آبشدهای */
|
||||||
|
export function getProductSellPrice(product: Product): number {
|
||||||
|
const base = getProductBaseSellPrice(product)
|
||||||
|
if (base <= 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMeltedProductUsingAppPrice(product) || isMeltedTradeGroup(product.group_type)) {
|
||||||
|
return base - Number(product.cash_fee_sell ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
export function isProductPriceInactive(product: Product): boolean {
|
export function isProductPriceInactive(product: Product): boolean {
|
||||||
return getProductBuyPrice(product) <= 0
|
return getProductBaseBuyPrice(product) <= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export { isMeltedTradeGroup, isMeltedProductUsingAppPrice } from '@/utils/productCategories'
|
export { isMeltedTradeGroup, isMeltedProductUsingAppPrice } from '@/utils/productCategories'
|
||||||
|
|||||||
@@ -20,10 +20,16 @@ export const supportMessengerSettings = ref<SupportMessengerSettings>({})
|
|||||||
export const DEFAULT_SUPPORT_MESSENGER_TEXT =
|
export const DEFAULT_SUPPORT_MESSENGER_TEXT =
|
||||||
'جهت شارژ حساب کاربری خود، کلمه "شماره حساب" را برای پشتیبانی ارسال نمایید.'
|
'جهت شارژ حساب کاربری خود، کلمه "شماره حساب" را برای پشتیبانی ارسال نمایید.'
|
||||||
|
|
||||||
function getActiveSettingValue(settings: Array<{ key?: string; is_active?: boolean; value?: unknown }>, key: string) {
|
function getSupportSettingValue(
|
||||||
const setting = settings.find((item) => item?.key === key && item?.is_active === true)
|
settings: Array<{ key?: string; is_active?: boolean | number; value?: unknown }>,
|
||||||
const value = setting?.value
|
key: string
|
||||||
|
) {
|
||||||
|
const setting = settings.find((item) => item?.key === key)
|
||||||
|
if (!setting) return ''
|
||||||
|
|
||||||
|
const value = setting.value
|
||||||
if (value === undefined || value === null) return ''
|
if (value === undefined || value === null) return ''
|
||||||
|
|
||||||
return String(value).trim()
|
return String(value).trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +39,7 @@ export function buildSupportMessengerUrl(messenger?: string, messengerId?: strin
|
|||||||
|
|
||||||
if (/^https?:\/\//i.test(id)) return id
|
if (/^https?:\/\//i.test(id)) return id
|
||||||
|
|
||||||
const type = (messenger ?? '').trim().toLowerCase()
|
const type = (messenger ?? '').trim().toLowerCase() || 'bale'
|
||||||
const username = id.replace(/^@/, '')
|
const username = id.replace(/^@/, '')
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -53,7 +59,7 @@ export function buildSupportMessengerUrl(messenger?: string, messengerId?: strin
|
|||||||
case 'soroush':
|
case 'soroush':
|
||||||
return `https://splus.ir/splus/${username}`
|
return `https://splus.ir/splus/${username}`
|
||||||
default:
|
default:
|
||||||
return ''
|
return `https://ble.ir/${username}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,9 +69,9 @@ export async function loadSupportMessengerFromSettings() {
|
|||||||
if (!settings || !Array.isArray(settings)) return
|
if (!settings || !Array.isArray(settings)) return
|
||||||
|
|
||||||
supportMessengerSettings.value = {
|
supportMessengerSettings.value = {
|
||||||
messenger: getActiveSettingValue(settings, 'support_messenger'),
|
messenger: getSupportSettingValue(settings, 'support_messenger') || 'bale',
|
||||||
messengerId: getActiveSettingValue(settings, 'support_messenger_id'),
|
messengerId: getSupportSettingValue(settings, 'support_messenger_id'),
|
||||||
messengerText: getActiveSettingValue(settings, 'support_messenger_text'),
|
messengerText: getSupportSettingValue(settings, 'support_messenger_text'),
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const DRAG_THRESHOLD = 6
|
||||||
|
|
||||||
|
type FabPosition = { x: number; y: number }
|
||||||
|
|
||||||
|
type DraggableFloatingOptions = {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
defaultBottomOffset?: number
|
||||||
|
defaultRightOffset?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDraggableFloatingButton(
|
||||||
|
storageKey: string,
|
||||||
|
options: DraggableFloatingOptions = {}
|
||||||
|
) {
|
||||||
|
const width = options.width ?? 96
|
||||||
|
const height = options.height ?? 44
|
||||||
|
const defaultBottomOffset = options.defaultBottomOffset ?? 120
|
||||||
|
const defaultRightOffset = options.defaultRightOffset ?? 20
|
||||||
|
|
||||||
|
const pos = ref<FabPosition>({ x: 0, y: 0 })
|
||||||
|
const dragging = ref(false)
|
||||||
|
|
||||||
|
let pointerId: number | null = null
|
||||||
|
let startX = 0
|
||||||
|
let startY = 0
|
||||||
|
let originX = 0
|
||||||
|
let originY = 0
|
||||||
|
let hasMoved = false
|
||||||
|
|
||||||
|
const fabStyle = computed(() => ({
|
||||||
|
left: `${pos.value.x}px`,
|
||||||
|
top: `${pos.value.y}px`,
|
||||||
|
width: `${width}px`,
|
||||||
|
touchAction: 'none'
|
||||||
|
}))
|
||||||
|
|
||||||
|
function getViewportWidth() {
|
||||||
|
return Math.min(window.innerWidth, 426)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHorizontalOffset() {
|
||||||
|
const vw = window.innerWidth
|
||||||
|
const contentWidth = getViewportWidth()
|
||||||
|
return Math.max(0, (vw - contentWidth) / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetToDefault() {
|
||||||
|
const vh = window.innerHeight
|
||||||
|
const offsetX = getHorizontalOffset()
|
||||||
|
const contentWidth = getViewportWidth()
|
||||||
|
|
||||||
|
pos.value = {
|
||||||
|
x: offsetX + contentWidth - width - defaultRightOffset,
|
||||||
|
y: vh - defaultBottomOffset - height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampToViewport() {
|
||||||
|
const vw = window.innerWidth
|
||||||
|
const vh = window.innerHeight
|
||||||
|
|
||||||
|
pos.value = {
|
||||||
|
x: Math.max(0, Math.min(vw - width, pos.value.x)),
|
||||||
|
y: Math.max(0, Math.min(vh - height, pos.value.y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPosition() {
|
||||||
|
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()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePosition() {
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(pos.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResize() {
|
||||||
|
clampToViewport()
|
||||||
|
savePosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerDown(event: PointerEvent) {
|
||||||
|
dragging.value = true
|
||||||
|
hasMoved = false
|
||||||
|
pointerId = event.pointerId
|
||||||
|
startX = event.clientX
|
||||||
|
startY = event.clientY
|
||||||
|
originX = pos.value.x
|
||||||
|
originY = pos.value.y
|
||||||
|
;(event.currentTarget as HTMLElement)?.setPointerCapture?.(event.pointerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerMove(event: PointerEvent) {
|
||||||
|
if (!dragging.value || pointerId !== event.pointerId) return
|
||||||
|
|
||||||
|
const dx = event.clientX - startX
|
||||||
|
const dy = event.clientY - startY
|
||||||
|
|
||||||
|
if (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD) {
|
||||||
|
hasMoved = true
|
||||||
|
}
|
||||||
|
|
||||||
|
pos.value = {
|
||||||
|
x: originX + dx,
|
||||||
|
y: originY + dy
|
||||||
|
}
|
||||||
|
clampToViewport()
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishPointer(event: PointerEvent) {
|
||||||
|
if (!dragging.value || pointerId !== event.pointerId) return false
|
||||||
|
|
||||||
|
dragging.value = false
|
||||||
|
pointerId = null
|
||||||
|
savePosition()
|
||||||
|
|
||||||
|
const clicked = !hasMoved
|
||||||
|
hasMoved = false
|
||||||
|
return clicked
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerUp(event: PointerEvent) {
|
||||||
|
return finishPointer(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerCancel(event: PointerEvent) {
|
||||||
|
finishPointer(event)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!loadPosition()) {
|
||||||
|
resetToDefault()
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
dragging,
|
||||||
|
fabStyle,
|
||||||
|
handlePointerDown,
|
||||||
|
handlePointerMove,
|
||||||
|
handlePointerUp,
|
||||||
|
handlePointerCancel,
|
||||||
|
resetToDefault
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,14 +68,14 @@
|
|||||||
</div>-->
|
</div>-->
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="font-bold text-xs">
|
<div v-else class="font-bold text-xs">
|
||||||
{{ formatCashWalletBalanceText(usePriceWithoutType(walletStore?.walletInfo?.cash ?? 0), props?.ProductItem?.group_type) }}
|
{{ formatCashWalletBalanceText(usePriceWithoutType(cashWalletBalance), props?.ProductItem?.group_type) }}
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
<Transition mode="out-in" name="fade">
|
<Transition mode="out-in" name="fade">
|
||||||
<a-form-item v-if="walletStore?.walletInfo" class="!mt-3 ">
|
<a-form-item v-if="walletStore?.walletInfo" class="!mt-3 ">
|
||||||
<!-- {{ isBuy }}-->
|
<!-- {{ isBuy }}-->
|
||||||
<a-slider v-model:value="form.percentWallet"
|
<a-slider v-model:value="form.percentWallet"
|
||||||
:disabled="isBuy? walletStore?.walletInfo?.cash <= 0: false"
|
:disabled="isBuy? cashWalletBalance <= 0: false"
|
||||||
:max="100"
|
:max="100"
|
||||||
:min="0"
|
:min="0"
|
||||||
:step="10"
|
:step="10"
|
||||||
@@ -238,10 +238,14 @@ import { useFundUserStore } from '@/store/fundStore'
|
|||||||
import {useConfigStore} from "@/store/configStore";
|
import {useConfigStore} from "@/store/configStore";
|
||||||
import { calcFeeInGrams } from '@/utils/serviceFee'
|
import { calcFeeInGrams } from '@/utils/serviceFee'
|
||||||
import { formatCashWalletBalanceText, formatGoldWalletBalanceText } from '@/utils/productDealMethod'
|
import { formatCashWalletBalanceText, formatGoldWalletBalanceText } from '@/utils/productDealMethod'
|
||||||
|
import { getCashWalletBalance } from '@/utils/productAssets'
|
||||||
|
|
||||||
const infoVisible = ref(false)
|
const infoVisible = ref(false)
|
||||||
const walletStore = useWalletStore()
|
const walletStore = useWalletStore()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const cashWalletBalance = computed(() =>
|
||||||
|
getCashWalletBalance(walletStore.walletInfo, userStore.userInfo?.wallets)
|
||||||
|
)
|
||||||
const fundStore = useFundUserStore()
|
const fundStore = useFundUserStore()
|
||||||
const confirmAction = ref<boolean>(false)
|
const confirmAction = ref<boolean>(false)
|
||||||
const noCompleteVisible = ref<boolean>(false)
|
const noCompleteVisible = ref<boolean>(false)
|
||||||
@@ -490,7 +494,7 @@ const unitGoldCalculator = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const unitCashCalculator = computed(() => {
|
const unitCashCalculator = computed(() => {
|
||||||
return (walletStore?.walletInfo?.cash ?? 0) / 100
|
return cashWalletBalance.value / 100
|
||||||
})
|
})
|
||||||
|
|
||||||
const limitedPrice = computed(() => {
|
const limitedPrice = computed(() => {
|
||||||
|
|||||||
@@ -81,14 +81,14 @@
|
|||||||
</div>-->
|
</div>-->
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="font-bold text-xs">
|
<div v-else class="font-bold text-xs">
|
||||||
{{ formatCashWalletBalanceText(usePriceWithoutType(walletStore?.walletInfo?.cash ?? 0), props?.ProductItem?.group_type) }}
|
{{ formatCashWalletBalanceText(usePriceWithoutType(cashWalletBalance), props?.ProductItem?.group_type) }}
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
<Transition mode="out-in" name="fade">
|
<Transition mode="out-in" name="fade">
|
||||||
<a-form-item v-if="walletStore?.walletInfo" class="!mt-3 ">
|
<a-form-item v-if="walletStore?.walletInfo" class="!mt-3 ">
|
||||||
<!-- {{ isBuy }}-->
|
<!-- {{ isBuy }}-->
|
||||||
<a-slider v-model:value="form.percentWallet"
|
<a-slider v-model:value="form.percentWallet"
|
||||||
:disabled="isBuy? walletStore?.walletInfo?.cash <= 0: false"
|
:disabled="isBuy? cashWalletBalance <= 0: false"
|
||||||
:max="100"
|
:max="100"
|
||||||
:min="0"
|
:min="0"
|
||||||
:step="10"
|
:step="10"
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
({{ productWalletBalance }}{{ weightUnit }}) می باشد.
|
({{ productWalletBalance }}{{ weightUnit }}) می باشد.
|
||||||
</div>
|
</div>
|
||||||
<!-- <div>{{form?.goldValue? true: false}}</div>-->
|
<!-- <div>{{form?.goldValue? true: false}}</div>-->
|
||||||
<div v-if="isBuy ? (form?.goldCash > walletStore?.walletInfo?.cash) : false"
|
<div v-if="isBuy ? (form?.goldCash > cashWalletBalance) : false"
|
||||||
class="text-red-700 text-xs font-bold mt-2">
|
class="text-red-700 text-xs font-bold mt-2">
|
||||||
موجودی کیف پول ناکافی می باشد.
|
موجودی کیف پول ناکافی می باشد.
|
||||||
</div>
|
</div>
|
||||||
@@ -297,7 +297,7 @@ import { useFundUserStore } from '@/store/fundStore'
|
|||||||
import {useConfigStore} from "@/store/configStore";
|
import {useConfigStore} from "@/store/configStore";
|
||||||
import { calcFeeInGrams, loadServiceFeeFromSettings } from '@/utils/serviceFee'
|
import { calcFeeInGrams, loadServiceFeeFromSettings } from '@/utils/serviceFee'
|
||||||
import { formatCashWalletBalanceText, getWeightUnit } from '@/utils/productDealMethod'
|
import { formatCashWalletBalanceText, getWeightUnit } from '@/utils/productDealMethod'
|
||||||
import { formatProductWalletBalanceText, getProductInventoryQuantity } from '@/utils/productAssets'
|
import { formatProductWalletBalanceText, getCashWalletBalance, getProductInventoryQuantity } from '@/utils/productAssets'
|
||||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||||
import { isMeltedTradeGroup } from '@/utils/productCategories'
|
import { isMeltedTradeGroup } from '@/utils/productCategories'
|
||||||
import { useSettingsStore } from '@/store/settingsStore'
|
import { useSettingsStore } from '@/store/settingsStore'
|
||||||
@@ -316,6 +316,9 @@ const { meltedGoldLabel } = storeToRefs(settingsStore)
|
|||||||
const weightUnit = computed(() => getWeightUnit())
|
const weightUnit = computed(() => getWeightUnit())
|
||||||
const fundStore = useFundUserStore()
|
const fundStore = useFundUserStore()
|
||||||
const { assetsSummary, loadWalletAssets } = useWalletAssets()
|
const { assetsSummary, loadWalletAssets } = useWalletAssets()
|
||||||
|
const cashWalletBalance = computed(() =>
|
||||||
|
getCashWalletBalance(walletStore.walletInfo, userStore.userInfo?.wallets)
|
||||||
|
)
|
||||||
const confirmAction = ref<boolean>(false)
|
const confirmAction = ref<boolean>(false)
|
||||||
const noCompleteVisible = ref<boolean>(false)
|
const noCompleteVisible = ref<boolean>(false)
|
||||||
const buyItems = ref<Transaction[]>([])
|
const buyItems = ref<Transaction[]>([])
|
||||||
@@ -576,7 +579,7 @@ function percentChange(val: number) {
|
|||||||
const unitGoldCalculator = computed(() => productWalletBalance.value / 100)
|
const unitGoldCalculator = computed(() => productWalletBalance.value / 100)
|
||||||
|
|
||||||
const unitCashCalculator = computed(() => {
|
const unitCashCalculator = computed(() => {
|
||||||
return (walletStore?.walletInfo?.cash ?? 0) / 100
|
return cashWalletBalance.value / 100
|
||||||
})
|
})
|
||||||
|
|
||||||
const limitedPrice = computed(() => {
|
const limitedPrice = computed(() => {
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ import {
|
|||||||
productMinOrderCash
|
productMinOrderCash
|
||||||
} from '@/utils/productOrderLimit'
|
} from '@/utils/productOrderLimit'
|
||||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||||
import { formatProductWalletBalanceText, getProductInventoryQuantity } from '@/utils/productAssets'
|
import { formatProductWalletBalanceText, getCashWalletBalance, getProductInventoryQuantity } from '@/utils/productAssets'
|
||||||
|
|
||||||
const typeDeal = computed(() => getProductDealMethodOptions(props?.ProductItem?.group_type, isBuy.value))
|
const typeDeal = computed(() => getProductDealMethodOptions(props?.ProductItem?.group_type, isBuy.value))
|
||||||
|
|
||||||
@@ -352,16 +352,20 @@ const buyMetalWalletBalanceText = computed(() =>
|
|||||||
: goldWalletBalanceText.value
|
: goldWalletBalanceText.value
|
||||||
)
|
)
|
||||||
|
|
||||||
const cashWalletBalanceText = computed(() =>
|
|
||||||
formatCashWalletBalanceText(
|
|
||||||
usePriceWithoutType(walletStore?.walletInfo?.cash ?? 0),
|
|
||||||
productGroupType.value
|
|
||||||
)
|
|
||||||
)
|
|
||||||
const infoVisible = ref(false)
|
const infoVisible = ref(false)
|
||||||
const walletStore = useWalletStore()
|
const walletStore = useWalletStore()
|
||||||
const { assetsSummary, loadWalletAssets } = useWalletAssets()
|
const { assetsSummary, loadWalletAssets } = useWalletAssets()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const cashWalletBalance = computed(() =>
|
||||||
|
getCashWalletBalance(walletStore.walletInfo, userStore.userInfo?.wallets)
|
||||||
|
)
|
||||||
|
|
||||||
|
const cashWalletBalanceText = computed(() =>
|
||||||
|
formatCashWalletBalanceText(
|
||||||
|
usePriceWithoutType(cashWalletBalance.value),
|
||||||
|
productGroupType.value
|
||||||
|
)
|
||||||
|
)
|
||||||
const confirmAction = ref<boolean>(false)
|
const confirmAction = ref<boolean>(false)
|
||||||
const noCompleteVisible = ref<boolean>(false)
|
const noCompleteVisible = ref<boolean>(false)
|
||||||
const buyItems = ref<Transaction[]>([])
|
const buyItems = ref<Transaction[]>([])
|
||||||
@@ -622,7 +626,7 @@ const unitGoldCalculator = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const unitCashCalculator = computed(() => {
|
const unitCashCalculator = computed(() => {
|
||||||
return (walletStore?.walletInfo?.cash ?? 0) / 100
|
return cashWalletBalance.value / 100
|
||||||
})
|
})
|
||||||
|
|
||||||
const limitedPrice = computed(() => {
|
const limitedPrice = computed(() => {
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
<!-- <div v-if="props.ProductItem.group_type!='آبشده'" class="text-secondary font-500">{{ props.ProductItem.amount }}گرم</div>-->
|
<!-- <div v-if="props.ProductItem.group_type!='آبشده'" class="text-secondary font-500">{{ props.ProductItem.amount }}گرم</div>-->
|
||||||
<!-- <div v-else class="text-secondary font-500">حداقل خرید یک گرم</div>-->
|
<!-- <div v-else class="text-secondary font-500">حداقل خرید یک گرم</div>-->
|
||||||
<!-- <div>{{usePrice((props.ProductItem.price ?? 0) + (props.ProductItem.cash_fee_buy ?? 0))}}</div>-->
|
<!-- <div>{{usePrice((props.ProductItem.price ?? 0) + (props.ProductItem.cash_fee_buy ?? 0))}}</div>-->
|
||||||
<div v-if="isMeltedTradeGroup(props.ProductItem.group_type)">{{usePrice(goldPriceValue)}}</div>
|
<div>{{ usePrice(getProductBuyPrice(props.ProductItem)) }}</div>
|
||||||
<div v-else>{{usePrice(price)}}</div>
|
|
||||||
<!-- <div v-if="props.ProductItem.group_type!='آبشده'" class="text-xs text-gray-400">شامل 33 هزار تومان اجرت</div>-->
|
<!-- <div v-if="props.ProductItem.group_type!='آبشده'" class="text-xs text-gray-400">شامل 33 هزار تومان اجرت</div>-->
|
||||||
<!-- <div v-else class="text-xs text-gray-400">محاسبه بر اساس قیمت روز</div>-->
|
<!-- <div v-else class="text-xs text-gray-400">محاسبه بر اساس قیمت روز</div>-->
|
||||||
</div>
|
</div>
|
||||||
@@ -33,7 +32,6 @@ import {usePrice} from '@/utils/usePrice';
|
|||||||
import {getFileUrl} from '@/utils/file';
|
import {getFileUrl} from '@/utils/file';
|
||||||
import {computed, ref} from "vue";
|
import {computed, ref} from "vue";
|
||||||
import { getProductBuyPrice } from '@/utils/productPrice'
|
import { getProductBuyPrice } from '@/utils/productPrice'
|
||||||
import { isMeltedTradeGroup } from '@/utils/productCategories'
|
|
||||||
|
|
||||||
interface Prop {
|
interface Prop {
|
||||||
ProductItem: Product
|
ProductItem: Product
|
||||||
@@ -68,29 +66,6 @@ const displayImage = computed(() => {
|
|||||||
if (fileUrl) return fileUrl
|
if (fileUrl) return fileUrl
|
||||||
return defaultImages[props.ProductItem.group_type] ?? defaultImg
|
return defaultImages[props.ProductItem.group_type] ?? defaultImg
|
||||||
})
|
})
|
||||||
// const typeOfProduct=(val:string)=>{
|
|
||||||
// if (val.includes('گوی'))
|
|
||||||
// return 'گوی'
|
|
||||||
// if (val.includes('شمش'))
|
|
||||||
// return 'شمش'
|
|
||||||
// if (val.includes('مکعب'))
|
|
||||||
// return 'مکعب'
|
|
||||||
// if (val.includes('24 عیار'))
|
|
||||||
// return '24 عیار'
|
|
||||||
//
|
|
||||||
// return 'آب شده'
|
|
||||||
// }
|
|
||||||
|
|
||||||
const price = computed(()=>{
|
|
||||||
if(props.ProductItem.direct_price === 0){
|
|
||||||
return ((props.ProductItem.price ?? 0) + (props.ProductItem.cash_fee_buy ?? 0)) * props.ProductItem.amount
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return ((props.ProductItem.price ?? 0) + (props.ProductItem.cash_fee_buy ?? 0))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const goldPriceValue = computed(() => getProductBuyPrice(props.ProductItem))
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ import { getProductsCatalog } from '@/composables/useProductsCatalog'
|
|||||||
import { loading } from '@/utils'
|
import { loading } from '@/utils'
|
||||||
import type { Product } from '@/models/products'
|
import type { Product } from '@/models/products'
|
||||||
import { loadServiceFeeFromSettings } from '@/utils/serviceFee'
|
import { loadServiceFeeFromSettings } from '@/utils/serviceFee'
|
||||||
|
import { loadProductCategoryTabNames } from '@/utils/categoryTabs'
|
||||||
|
import { getLastPrices, startLastPricePolling, stopLastPricePolling } from '@/utils/price'
|
||||||
|
|
||||||
/** Shared products page datasets — mobile/desktop views read this module state only. */
|
/** Shared products page datasets — mobile/desktop views read this module state only. */
|
||||||
let initPromise: Promise<void> | null = null
|
let initPromise: Promise<void> | null = null
|
||||||
let productListInterval: ReturnType<typeof setInterval> | undefined
|
let productListInterval: ReturnType<typeof setInterval> | undefined
|
||||||
let providerMountCount = 0
|
let providerMountCount = 0
|
||||||
|
let pricePollingMountCount = 0
|
||||||
|
|
||||||
const initialized = ref(false)
|
const initialized = ref(false)
|
||||||
|
|
||||||
@@ -40,6 +43,17 @@ function syncTypeItems(products: Product[]) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadCategoryTabs() {
|
||||||
|
try {
|
||||||
|
const tabs = await loadProductCategoryTabNames()
|
||||||
|
if (tabs.length) {
|
||||||
|
productsTypeItems.value = tabs
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Product category tabs error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyProductsList(products: Product[]) {
|
function applyProductsList(products: Product[]) {
|
||||||
if (products.length) {
|
if (products.length) {
|
||||||
productsCloseTime.value = products[0].auto_close_at
|
productsCloseTime.value = products[0].auto_close_at
|
||||||
@@ -100,10 +114,19 @@ function stopProductListPolling() {
|
|||||||
|
|
||||||
export function onProductsPageProviderMounted() {
|
export function onProductsPageProviderMounted() {
|
||||||
providerMountCount += 1
|
providerMountCount += 1
|
||||||
|
pricePollingMountCount += 1
|
||||||
|
if (pricePollingMountCount === 1) {
|
||||||
|
void getLastPrices()
|
||||||
|
startLastPricePolling()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onProductsPageProviderUnmounted() {
|
export function onProductsPageProviderUnmounted() {
|
||||||
providerMountCount = Math.max(0, providerMountCount - 1)
|
providerMountCount = Math.max(0, providerMountCount - 1)
|
||||||
|
pricePollingMountCount = Math.max(0, pricePollingMountCount - 1)
|
||||||
|
if (pricePollingMountCount === 0) {
|
||||||
|
stopLastPricePolling()
|
||||||
|
}
|
||||||
if (providerMountCount === 0) {
|
if (providerMountCount === 0) {
|
||||||
stopProductListPolling()
|
stopProductListPolling()
|
||||||
}
|
}
|
||||||
@@ -124,7 +147,10 @@ export async function initProductsPageData(force = false) {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
const products = await getProductsCatalog(force)
|
const products = await getProductsCatalog(force)
|
||||||
applyProductsList(products ?? [])
|
applyProductsList(products ?? [])
|
||||||
await loadServiceFeeFromSettings()
|
await Promise.all([
|
||||||
|
loadServiceFeeFromSettings(),
|
||||||
|
loadCategoryTabs(),
|
||||||
|
])
|
||||||
initialized.value = true
|
initialized.value = true
|
||||||
startProductListPolling()
|
startProductListPolling()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -142,6 +168,8 @@ export function resetProductsPageData() {
|
|||||||
initialized.value = false
|
initialized.value = false
|
||||||
initPromise = null
|
initPromise = null
|
||||||
providerMountCount = 0
|
providerMountCount = 0
|
||||||
|
pricePollingMountCount = 0
|
||||||
|
stopLastPricePolling()
|
||||||
stopProductListPolling()
|
stopProductListPolling()
|
||||||
productsActiveKey.value = 'همه'
|
productsActiveKey.value = 'همه'
|
||||||
productsTypeItems.value = []
|
productsTypeItems.value = []
|
||||||
|
|||||||
Reference in New Issue
Block a user