Merge remote-tracking branch 'origin/modular' into modular
This commit is contained in:
@@ -20,6 +20,8 @@ export interface Product {
|
||||
percent_fee_sell: number | undefined
|
||||
cash_fee_buy: number | undefined
|
||||
cash_fee_sell: number | undefined
|
||||
cash_fee_buy_is_percent?: boolean
|
||||
cash_fee_sell_is_percent?: boolean
|
||||
order_limit_enabled?: boolean
|
||||
min_order_cash?: number | null
|
||||
max_order_cash?: number | null
|
||||
@@ -56,6 +58,8 @@ export interface PInventory {
|
||||
percent_fee_sell: number
|
||||
cash_fee_buy: number
|
||||
cash_fee_sell: number
|
||||
cash_fee_buy_is_percent?: boolean
|
||||
cash_fee_sell_is_percent?: boolean
|
||||
reading_method: any
|
||||
created_at: any
|
||||
updated_at: any
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Product } from '@/models/products'
|
||||
import { formatWeightAmount, usePriceWithoutType } from '@/utils/usePrice'
|
||||
import { formatGoldAmount, formatWeightAmount, usePriceWithoutType } from '@/utils/usePrice'
|
||||
import type { UserWalletLine } from '@/models/wallet'
|
||||
import type { ProductFile } from '@/utils/file'
|
||||
import type { WalletInventoryCard, WalletMetalTotal } from '@/models/walletAssets'
|
||||
@@ -426,6 +426,48 @@ export function formatPortfolioProfitPercent(percent?: number | null): string {
|
||||
return `${sign}${value.toFixed(2)}٪`
|
||||
}
|
||||
|
||||
/** متن توضیح مودال سود/زیان تحققنیافته طلا */
|
||||
export function formatPortfolioProfitInfoText(
|
||||
portfolio: {
|
||||
average_buy_price?: number | null
|
||||
live_gold_price?: number | null
|
||||
cost_basis_rial?: number | null
|
||||
gold_weight?: number | null
|
||||
gold_rial_value?: number | null
|
||||
unrealized_profit_rial?: number | null
|
||||
unrealized_profit_percent?: number | null
|
||||
},
|
||||
weightUnit = 'گرم'
|
||||
): string {
|
||||
const lines: string[] = []
|
||||
|
||||
if (portfolio.average_buy_price != null && portfolio.average_buy_price > 0) {
|
||||
lines.push(`میانگین قیمت خرید: ${formatRialNumber(portfolio.average_buy_price)} ریال`)
|
||||
}
|
||||
if (portfolio.live_gold_price != null && portfolio.live_gold_price > 0) {
|
||||
lines.push(`قیمت فعلی طلا: ${formatRialNumber(portfolio.live_gold_price)} ریال`)
|
||||
}
|
||||
if (portfolio.gold_weight != null && portfolio.gold_weight > 0) {
|
||||
lines.push(`وزن طلای شما: ${formatWeightAmount(portfolio.gold_weight)} ${weightUnit}`)
|
||||
}
|
||||
if (portfolio.cost_basis_rial != null && portfolio.cost_basis_rial > 0) {
|
||||
lines.push(`بهای تمامشده طلا: ${formatRialNumber(portfolio.cost_basis_rial)} ریال`)
|
||||
}
|
||||
if (portfolio.gold_rial_value != null && portfolio.gold_rial_value > 0) {
|
||||
lines.push(`ارزش فعلی طلا: ${formatRialNumber(portfolio.gold_rial_value)} ریال`)
|
||||
}
|
||||
if (portfolio.unrealized_profit_rial != null) {
|
||||
const pct = formatPortfolioProfitPercent(portfolio.unrealized_profit_percent)
|
||||
lines.push(
|
||||
`سود/زیان تحققنیافته: ${formatPortfolioProfitText(portfolio.unrealized_profit_rial)}${pct ? ` (${pct})` : ''}`
|
||||
)
|
||||
}
|
||||
|
||||
lines.push('محاسبه بر اساس میانگین وزنی قیمت خرید و قیمت لحظهای بازار است.')
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/** متن توضیح مودال دارایی ریالی */
|
||||
export function formatRialAssetsInfoText(
|
||||
cashWallet: number,
|
||||
@@ -545,8 +587,10 @@ export function formatProductWalletBalanceText(
|
||||
const quantity = card ? Number(card.quantity ?? 0) : Number(mainWalletGold ?? 0)
|
||||
const unit = card?.unit ?? getProductQuantityUnit(product)
|
||||
const title = product?.title?.trim() || 'محصول'
|
||||
const formattedQty =
|
||||
unit === 'عدد' ? usePriceWithoutType(quantity, false) : formatGoldAmount(quantity)
|
||||
|
||||
return `موجودی کیف ${title}: ${quantity} ${unit}`
|
||||
return `موجودی کیف ${title}: ${formattedQty} ${unit}`
|
||||
}
|
||||
|
||||
export function isSingleMetalCatalog(metalIds: number[]): boolean {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
export type ProductCashFeeSource = {
|
||||
cash_fee_buy?: number | null
|
||||
cash_fee_sell?: number | null
|
||||
cash_fee_buy_is_percent?: boolean | null
|
||||
cash_fee_sell_is_percent?: boolean | null
|
||||
}
|
||||
|
||||
/** 0.5 => 0.5% ، 5 => 5% — همیشه عدد درصد */
|
||||
export function resolveCashFeePercentRatio(percentValue?: number | null) {
|
||||
const offset = Number(percentValue ?? 0)
|
||||
if (!offset || !Number.isFinite(offset)) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return offset / 100
|
||||
}
|
||||
|
||||
/** @deprecated از resolveCashFeePercentRatio استفاده کنید */
|
||||
export function resolveFeePercentRatio(percentValue?: number | null) {
|
||||
return resolveCashFeePercentRatio(percentValue)
|
||||
}
|
||||
|
||||
export function resolveProductCashFeeRial(
|
||||
feeValue: number | null | undefined,
|
||||
isPercent: boolean | null | undefined,
|
||||
basePriceRial: number,
|
||||
) {
|
||||
const raw = Number(feeValue ?? 0)
|
||||
if (!raw || !Number.isFinite(raw)) {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (!isPercent) {
|
||||
return Math.round(raw)
|
||||
}
|
||||
|
||||
const base = Number(basePriceRial)
|
||||
if (!base || base <= 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return Math.round(Math.abs(base * resolveCashFeePercentRatio(raw)))
|
||||
}
|
||||
|
||||
export function resolveProductCashFeeBuyRial(product: ProductCashFeeSource, basePriceRial: number) {
|
||||
return resolveProductCashFeeRial(
|
||||
product.cash_fee_buy,
|
||||
product.cash_fee_buy_is_percent,
|
||||
basePriceRial,
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveProductCashFeeSellRial(product: ProductCashFeeSource, basePriceRial: number) {
|
||||
return resolveProductCashFeeRial(
|
||||
product.cash_fee_sell,
|
||||
product.cash_fee_sell_is_percent,
|
||||
basePriceRial,
|
||||
)
|
||||
}
|
||||
|
||||
export function formatProductCashFeeLabel(
|
||||
feeValue: number | null | undefined,
|
||||
isPercent?: boolean | null,
|
||||
) {
|
||||
const raw = Number(feeValue ?? 0)
|
||||
if (!raw) {
|
||||
return '۰'
|
||||
}
|
||||
|
||||
if (isPercent) {
|
||||
const text = Number.isInteger(raw) ? String(raw) : String(parseFloat(raw.toFixed(4)))
|
||||
return `${text}%`
|
||||
}
|
||||
|
||||
return `${raw} ریال`
|
||||
}
|
||||
@@ -13,6 +13,9 @@ import {
|
||||
usesQuantityTrade,
|
||||
} from '@/utils/productCategories'
|
||||
import { formatInventoryCardValue } from '@/utils/productAssets'
|
||||
import { resolveProductCashFeeBuyRial, resolveProductCashFeeSellRial } from '@/utils/productCashFee'
|
||||
import { TRADE_GRAM_DISPLAY_DECIMALS } from '@/utils/serviceFee'
|
||||
import { formatGoldAmount } from '@/utils/usePrice'
|
||||
|
||||
/** @deprecated از isMeltedTradeGroup استفاده کنید */
|
||||
export const BOURSECU_GROUP_ABSHODE = PRODUCT_CATEGORY_ABSHODE
|
||||
@@ -369,7 +372,7 @@ export function resolveProductTradeBuyUnitPrice(product?: Product | null) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const basePrice = (product.price ?? 0) + (product.cash_fee_buy ?? 0)
|
||||
const basePrice = (product.price ?? 0) + resolveProductCashFeeBuyRial(product, product.price ?? 0)
|
||||
return basePrice * productPriceAmountFactor(product)
|
||||
}
|
||||
|
||||
@@ -378,7 +381,7 @@ export function resolveProductTradeSellUnitPrice(product?: Product | null) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const basePrice = (product.price ?? 0) - (product.cash_fee_sell ?? 0)
|
||||
const basePrice = (product.price ?? 0) - resolveProductCashFeeSellRial(product, product.price ?? 0)
|
||||
return basePrice * productPriceAmountFactor(product)
|
||||
}
|
||||
|
||||
@@ -635,6 +638,57 @@ export function resolveTransactionTradeProduct(
|
||||
return item.wallet?.product ?? null
|
||||
}
|
||||
|
||||
/** معامله آبشده / کیف طلای اصلی — نه سکه و محصول تعدادی */
|
||||
export function isMeltedGoldTransaction(
|
||||
item?: (TransactionTradeProductSource & { type?: number | null; title?: string | null }) | null,
|
||||
): boolean {
|
||||
if (!item) {
|
||||
return false
|
||||
}
|
||||
|
||||
const tradeProduct = resolveTransactionTradeProduct(item)
|
||||
if (tradeProduct) {
|
||||
if (usesQuantityTrade(tradeProduct) && !isMeltedTradeGroup(tradeProduct.group_type)) {
|
||||
return false
|
||||
}
|
||||
if (isMeltedTradeGroup(tradeProduct.group_type)) {
|
||||
return true
|
||||
}
|
||||
if (isCountBasedProduct(tradeProduct)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const title = (item.title ?? '').trim()
|
||||
const meltedLabel = getMeltedGoldTradeLabel()
|
||||
if (title.includes(PRODUCT_CATEGORY_ABSHODE) || (meltedLabel && title.includes(meltedLabel))) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (item.type !== 1 && item.type !== 2) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isMeltedTradeGroup(item.wallet?.product?.group_type)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const tradeTitle = (tradeProduct?.title ?? '').trim()
|
||||
if (tradeTitle === meltedLabel || tradeTitle.includes(PRODUCT_CATEGORY_ABSHODE)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return !item.wallet?.product && !tradeTitle
|
||||
}
|
||||
|
||||
export function formatTransactionGoldAmount(
|
||||
item?: (TransactionTradeProductSource & { type?: number | null; title?: string | null }) | null,
|
||||
amount?: number | null,
|
||||
): string {
|
||||
const decimals = isMeltedGoldTransaction(item) ? TRADE_GRAM_DISPLAY_DECIMALS : 3
|
||||
return formatGoldAmount(amount, decimals)
|
||||
}
|
||||
|
||||
/** برچسب قیمت حین معامله — نام و واحد همان محصول معاملهشده */
|
||||
export function getTransactionTradePriceLabel(product?: (ProductUnitSource & { title?: string | null }) | null) {
|
||||
if (!product) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Product } from '@/models/products'
|
||||
import { getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@/utils/price'
|
||||
import { resolveProductCashFeeBuyRial, resolveProductCashFeeSellRial } from '@/utils/productCashFee'
|
||||
import { isMetalCommodityGroup, isMeltedProductUsingAppPrice, isMeltedTradeGroup, usesQuantityTrade } from '@/utils/productCategories'
|
||||
import { productPriceAmountFactor } from '@/utils/productDealMethod'
|
||||
|
||||
@@ -104,7 +105,7 @@ export function getProductBuyPrice(product: Product): number {
|
||||
return 0
|
||||
}
|
||||
|
||||
const fee = Number(product.cash_fee_buy ?? 0)
|
||||
const fee = resolveProductCashFeeBuyRial(product, base)
|
||||
|
||||
if (
|
||||
isMeltedProductUsingAppPrice(product)
|
||||
@@ -159,7 +160,7 @@ export function getProductSellPrice(product: Product): number {
|
||||
}
|
||||
|
||||
if (isMeltedProductUsingAppPrice(product) || isMeltedTradeGroup(product.group_type) || usesQuantityTrade(product)) {
|
||||
return base - Number(product.cash_fee_sell ?? 0)
|
||||
return base - resolveProductCashFeeSellRial(product, base)
|
||||
}
|
||||
|
||||
return base
|
||||
|
||||
+115
-13
@@ -6,6 +6,31 @@ export const serviceFeePercentage = ref<number>(0.01)
|
||||
/** true = مبالغ رند: فروش → ریال دقیق (کارمزد از طلا)؛ خرید → وزن دقیق (کارمزد از ریال) */
|
||||
export const feeDeductFromGold = ref(false)
|
||||
|
||||
/** آخرین فیلدی که کاربر مستقیم ویرایش کرده — مبنای بهروزرسانی هنگام تغییر قیمت */
|
||||
export type TradeInputLock = 'gram' | 'rial' | null
|
||||
|
||||
/** حداکثر اعشار ذخیره/ارسال به سرور (مطابق orders.amount) */
|
||||
export const TRADE_GRAM_STORAGE_DECIMALS = 10
|
||||
|
||||
/** حداکثر اعشار نمایش به کاربر در اپ */
|
||||
export const TRADE_GRAM_DISPLAY_DECIMALS = 8
|
||||
|
||||
export function normalizeOrderGoldAmount(grams?: number | null): number {
|
||||
const value = Number(grams ?? 0)
|
||||
if (!Number.isFinite(value) || value === 0) return 0
|
||||
return parseFloat(value.toFixed(TRADE_GRAM_STORAGE_DECIMALS))
|
||||
}
|
||||
|
||||
export function formatTradeGramDisplay(grams?: number | null): number {
|
||||
const value = Number(grams ?? 0)
|
||||
if (!Number.isFinite(value)) return 0
|
||||
return parseFloat(value.toFixed(TRADE_GRAM_DISPLAY_DECIMALS))
|
||||
}
|
||||
|
||||
function feeRate() {
|
||||
return Number(serviceFeePercentage.value)
|
||||
}
|
||||
|
||||
export async function loadServiceFeeFromSettings() {
|
||||
try {
|
||||
const settings = await getSettings()
|
||||
@@ -39,7 +64,58 @@ export function calcFeeInGrams(goldValue?: number | null) {
|
||||
/** گرم معادل برای دریافت دقیق مبلغ ریالی در فروش */
|
||||
export function calcGoldAmountFromTargetRial(rialAmount: number, unitPriceRial: number) {
|
||||
if (!rialAmount || !unitPriceRial) return 0
|
||||
return parseFloat((rialAmount / unitPriceRial).toFixed(6))
|
||||
return normalizeOrderGoldAmount(rialAmount / unitPriceRial)
|
||||
}
|
||||
|
||||
/** وقتی کاربر گرم را قفل کرده — ریال نمایشی/قابل پرداخت از همان گرم */
|
||||
export function calcRialFromLockedGram(displayGram: number, unitPriceRial: number, isBuy: boolean) {
|
||||
const grams = Number(displayGram)
|
||||
const unit = Number(unitPriceRial)
|
||||
if (!grams || !unit) return 0
|
||||
const fee = feeRate()
|
||||
|
||||
if (isBuy) {
|
||||
if (feeDeductFromGold.value) {
|
||||
return Math.round(grams * unit * (1 + fee))
|
||||
}
|
||||
return Math.round(grams * unit)
|
||||
}
|
||||
|
||||
if (feeDeductFromGold.value) {
|
||||
return Math.round(grams * unit)
|
||||
}
|
||||
return Math.round(grams * unit * (1 - fee))
|
||||
}
|
||||
|
||||
/** وقتی کاربر ریال را قفل کرده — گرم نمایشی از همان مبلغ */
|
||||
export function calcGramFromLockedRial(rialAmount: number, unitPriceRial: number, isBuy: boolean) {
|
||||
const rial = Number(rialAmount)
|
||||
const unit = Number(unitPriceRial)
|
||||
if (!rial || !unit) return 0
|
||||
const fee = feeRate()
|
||||
|
||||
if (isBuy) {
|
||||
if (feeDeductFromGold.value) {
|
||||
return rial / (unit * (1 + fee))
|
||||
}
|
||||
return (rial / unit) * (1 - fee)
|
||||
}
|
||||
|
||||
if (feeDeductFromGold.value) {
|
||||
return rial / unit
|
||||
}
|
||||
return rial / (unit * (1 - fee))
|
||||
}
|
||||
|
||||
/** مبلغ ریالی قفلشده توسط کاربر — برای ارسال به سرور */
|
||||
export function calcTradeTargetCash(
|
||||
goldCash?: number | null,
|
||||
inputLock?: TradeInputLock,
|
||||
): number | undefined {
|
||||
if (inputLock !== 'rial') return undefined
|
||||
const cash = Number(goldCash ?? 0)
|
||||
if (cash <= 0) return undefined
|
||||
return Math.round(cash)
|
||||
}
|
||||
|
||||
/** مبلغ نهایی قابل پرداخت (خرید) یا دریافت (فروش) به ریال */
|
||||
@@ -49,41 +125,67 @@ export function calcPayablePriceRial(
|
||||
isBuy: boolean,
|
||||
targetRialCash?: number | null
|
||||
) {
|
||||
if (targetRialCash != null && Number(targetRialCash) > 0) {
|
||||
return Number(targetRialCash)
|
||||
}
|
||||
|
||||
if (feeDeductFromGold.value) {
|
||||
if (isBuy) return finalPriceRial + feeInRial
|
||||
if (targetRialCash != null && Number(targetRialCash) > 0) return Number(targetRialCash)
|
||||
return finalPriceRial
|
||||
}
|
||||
return isBuy ? finalPriceRial : Math.max(0, finalPriceRial - feeInRial)
|
||||
}
|
||||
|
||||
/** وزن ارسالی به سرور — فروش رند: از مبلغ ریالی هدف */
|
||||
/** وزن ارسالی به سرور */
|
||||
export function calcOrderGoldAmount(
|
||||
goldValue?: number | null,
|
||||
goldCash?: number | null,
|
||||
unitPriceRial?: number,
|
||||
isBuy?: boolean
|
||||
isBuy?: boolean,
|
||||
inputLock?: TradeInputLock
|
||||
) {
|
||||
const gold = Number(goldValue ?? 0)
|
||||
if (isBuy || !feeDeductFromGold.value) {
|
||||
return parseFloat(gold.toFixed(6))
|
||||
}
|
||||
const targetCash = calcSellTargetCash(goldCash, goldValue, unitPriceRial)
|
||||
const cash = Number(goldCash ?? 0)
|
||||
const unit = Number(unitPriceRial ?? 0)
|
||||
if (targetCash != null && unit > 0) {
|
||||
return calcGoldAmountFromTargetRial(targetCash, unit)
|
||||
|
||||
if (isBuy && inputLock === 'rial' && cash > 0 && unit > 0) {
|
||||
if (feeDeductFromGold.value) {
|
||||
const fee = feeRate()
|
||||
return normalizeOrderGoldAmount(cash / (unit * (1 + fee)))
|
||||
}
|
||||
return normalizeOrderGoldAmount(cash / unit)
|
||||
}
|
||||
return parseFloat(gold.toFixed(6))
|
||||
|
||||
if (!isBuy && feeDeductFromGold.value) {
|
||||
if (inputLock === 'rial' && cash > 0 && unit > 0) {
|
||||
return calcGoldAmountFromTargetRial(cash, unit)
|
||||
}
|
||||
if (inputLock !== 'rial') {
|
||||
return normalizeOrderGoldAmount(gold)
|
||||
}
|
||||
const targetCash = calcSellTargetCash(goldCash, goldValue, unitPriceRial, inputLock)
|
||||
if (targetCash != null && unit > 0) {
|
||||
return calcGoldAmountFromTargetRial(targetCash, unit)
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeOrderGoldAmount(gold)
|
||||
}
|
||||
|
||||
/** مبلغ ریالی دقیق دریافتی در فروش (فقط وقتی کاربر با ریال وارد کرده) */
|
||||
export function calcSellTargetCash(
|
||||
goldCash?: number | null,
|
||||
goldValue?: number | null,
|
||||
unitPriceRial?: number
|
||||
unitPriceRial?: number,
|
||||
inputLock?: TradeInputLock
|
||||
) {
|
||||
if (!feeDeductFromGold.value) return undefined
|
||||
if (inputLock === 'gram') return undefined
|
||||
|
||||
const cash = Number(goldCash ?? 0)
|
||||
if (!feeDeductFromGold.value || cash <= 0) return undefined
|
||||
if (cash <= 0) return undefined
|
||||
if (inputLock === 'rial') return Math.round(cash)
|
||||
|
||||
const gold = Number(goldValue ?? 0)
|
||||
const unit = Number(unitPriceRial ?? 0)
|
||||
if (gold > 0 && unit > 0) {
|
||||
|
||||
+49
-2
@@ -50,11 +50,26 @@ export function useFormat(num: string | number, unit = '') {
|
||||
type TransactionPriceSource = {
|
||||
cash?: number | null
|
||||
gold?: number | null
|
||||
type?: number | null
|
||||
order?: {
|
||||
price?: number | null
|
||||
total_price?: number | null
|
||||
amount?: number | null
|
||||
sts?: number | null
|
||||
sts_str?: string | number | null
|
||||
payment_sts?: number | null
|
||||
} | null
|
||||
withdraw?: {
|
||||
sts?: number | null
|
||||
sts_str?: string | number | null
|
||||
} | null
|
||||
order_id?: number | null
|
||||
}
|
||||
|
||||
const ORDER_STATUS_LABELS: Record<number, string> = {
|
||||
1: 'باز',
|
||||
2: 'موفق',
|
||||
3: 'ناموفق',
|
||||
}
|
||||
|
||||
export function getTransactionTradePrice(item: TransactionPriceSource): number {
|
||||
@@ -79,6 +94,38 @@ export function getTransactionTradePrice(item: TransactionPriceSource): number {
|
||||
return 0
|
||||
}
|
||||
|
||||
export function resolveTransactionStatusLabel(item?: TransactionPriceSource | null): string {
|
||||
if (!item) return ''
|
||||
|
||||
const raw = item.order?.sts_str ?? item.withdraw?.sts_str
|
||||
if (typeof raw === 'string') {
|
||||
const trimmed = raw.trim()
|
||||
if (trimmed && trimmed !== 'بدون مقدار') {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof raw === 'number' || (typeof raw === 'string' && /^\d+$/.test(String(raw).trim()))) {
|
||||
const mapped = ORDER_STATUS_LABELS[Number(raw)]
|
||||
if (mapped) return mapped
|
||||
}
|
||||
|
||||
const sts = item.order?.sts ?? item.withdraw?.sts
|
||||
if (sts != null && ORDER_STATUS_LABELS[Number(sts)]) {
|
||||
return ORDER_STATUS_LABELS[Number(sts)]
|
||||
}
|
||||
|
||||
if (item.order_id && Number(item.order?.payment_sts) === 4) {
|
||||
return 'موفق'
|
||||
}
|
||||
|
||||
if (item.order_id && (item.type === 1 || item.type === 2 || item.type === 10)) {
|
||||
return 'موفق'
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
export function getTransactionGoldAmount(item: TransactionPriceSource): number {
|
||||
const gold = Number(item.gold) || 0
|
||||
if (Math.abs(gold) > 0) {
|
||||
@@ -96,10 +143,10 @@ export function getTransactionGoldAmount(item: TransactionPriceSource): number {
|
||||
return gold
|
||||
}
|
||||
|
||||
export function formatGoldAmount(amount?: number | null): string {
|
||||
export function formatGoldAmount(amount?: number | null, decimals = 8): string {
|
||||
const num = Number(amount)
|
||||
if (!Number.isFinite(num)) return '0'
|
||||
return formatGroupedPrice(Number(num.toFixed(3)), false)
|
||||
return formatGroupedPrice(Number(num.toFixed(decimals)), false)
|
||||
}
|
||||
|
||||
/** وزن فلز (گرم/کیلو) — بدون گرد کردن به بالا (برخلاف usePriceWithoutType) */
|
||||
|
||||
@@ -48,7 +48,6 @@ import BasicSelect from '@/components/BasicPacks/BasicSelect.vue'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||
import BasicTextArea from '@/components/BasicPacks/BasicTextArea.vue'
|
||||
import { loading } from '@/utils'
|
||||
import { createAddress, getCity, updateAddress } from '@/api/address'
|
||||
import { FormInstance, message } from 'ant-design-vue'
|
||||
import { Address } from '@/models/address'
|
||||
@@ -130,14 +129,11 @@ const changeProvince = async (obj: any) => {
|
||||
return
|
||||
}
|
||||
try {
|
||||
loading.value = true
|
||||
cityList.value = undefined
|
||||
address.value.city = undefined
|
||||
cityList.value = await getCity(obj.id)
|
||||
} catch (e) {
|
||||
message.error('خطا در دریافت شهرها')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,14 +98,11 @@ export function useAddressPage() {
|
||||
|
||||
const changeProvince = async (obj: { id: number }) => {
|
||||
try {
|
||||
loading.value = true
|
||||
cityList.value = undefined
|
||||
address.value.city = undefined
|
||||
cityList.value = await getCity(obj.id)
|
||||
} catch (e) {
|
||||
message.error('خطا در دریافت شهرها')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -232,9 +232,11 @@ import type { Product } from '@/models/products'
|
||||
import { useFundUserStore } from '@/store/fundStore'
|
||||
import {useConfigStore} from "@/store/configStore";
|
||||
import { calcFeeInGrams, calcPayablePriceRial, calcSellGoldDebit, feeDeductFromGold, loadServiceFeeFromSettings } from '@/utils/serviceFee'
|
||||
import { resolveProductCashFeeBuyRial, resolveProductCashFeeSellRial } from '@/utils/productCashFee'
|
||||
import { formatCashWalletBalanceText, formatGoldWalletBalanceText } from '@/utils/productDealMethod'
|
||||
import { getCashWalletBalance } from '@/utils/productAssets'
|
||||
import { isStoreClosed, getStoreClosedMessage } from '@/utils/storeHours'
|
||||
import { resetTransactionsPageData } from '@/views/base/transactions/composables/useTransactionsPageData'
|
||||
|
||||
const infoVisible = ref(false)
|
||||
const walletStore = useWalletStore()
|
||||
@@ -282,13 +284,12 @@ const basePriceValue = computed(() => {
|
||||
const product = props?.ProductItem
|
||||
if (!product) return undefined
|
||||
|
||||
const { price_profit = 0, price = 0, cash_fee_buy = 0, cash_fee_sell = 0 } = product
|
||||
const { price_profit = 0, price = 0 } = product
|
||||
const base = price_profit + price
|
||||
const buyFee = resolveProductCashFeeBuyRial(product, base)
|
||||
const sellFee = resolveProductCashFeeSellRial(product, base)
|
||||
|
||||
const rawPrice = isBuy.value
|
||||
? base + cash_fee_buy
|
||||
: base - cash_fee_sell
|
||||
return rawPrice
|
||||
return isBuy.value ? base + buyFee : base - sellFee
|
||||
})
|
||||
|
||||
const finalPrice = computed(() => {
|
||||
@@ -298,15 +299,17 @@ const finalPrice = computed(() => {
|
||||
const product = props?.ProductItem
|
||||
if (!product) return 0
|
||||
|
||||
const { price_profit = 0, price = 0, cash_fee_buy = 0, cash_fee_sell = 0 } = product
|
||||
const { price_profit = 0, price = 0 } = product
|
||||
|
||||
const base = price_profit + price
|
||||
const buyFee = resolveProductCashFeeBuyRial(product, base)
|
||||
const sellFee = resolveProductCashFeeSellRial(product, base)
|
||||
|
||||
if (isBuy.value) {
|
||||
return gold * (base + cash_fee_buy)
|
||||
} else {
|
||||
return gold * (base - cash_fee_sell)
|
||||
return gold * (base + buyFee)
|
||||
}
|
||||
|
||||
return gold * (base - sellFee)
|
||||
})
|
||||
|
||||
const payablePrice = computed(() =>
|
||||
@@ -532,20 +535,30 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
function productFeeBase(product?: Product) {
|
||||
return (product?.price_profit ?? 0) + (product?.price ?? 0)
|
||||
}
|
||||
|
||||
function calculateCash(val: number) {
|
||||
priceErrorVisible.value = false
|
||||
if (!lastPrice.value) return
|
||||
|
||||
const product = props?.ProductItem
|
||||
if (!product) return
|
||||
|
||||
const base = productFeeBase(product)
|
||||
const buyFee = resolveProductCashFeeBuyRial(product, base)
|
||||
const sellFee = resolveProductCashFeeSellRial(product, base)
|
||||
const tradeBuy = getTradeBuyPrice(lastPrice.value)
|
||||
form.value.goldVazn = parseFloat((val * ((props?.ProductItem?.price) + (props?.ProductItem?.cash_fee_buy))) / tradeBuy).toFixed(1)
|
||||
form.value.goldVazn = parseFloat(String((val * (base + buyFee)) / tradeBuy)).toFixed(1)
|
||||
if (isBuy.value) {
|
||||
form.value.goldCash = val
|
||||
? parseFloat(String(val * ((props?.ProductItem?.price) + (props?.ProductItem?.cash_fee_buy))))
|
||||
? parseFloat(String(val * (base + buyFee)))
|
||||
: 0
|
||||
if (!val) form.value.goldVazn = 0
|
||||
} else {
|
||||
form.value.goldCash = val
|
||||
? parseFloat(String((val * props?.ProductItem?.price) - (props?.ProductItem?.cash_fee_sell)))
|
||||
? parseFloat(String(val * base - sellFee))
|
||||
: 0
|
||||
if (!val) form.value.goldVazn = 0
|
||||
}
|
||||
@@ -553,9 +566,16 @@ function calculateCash(val: number) {
|
||||
|
||||
function calculateGold(val: number) {
|
||||
if (price.value) {
|
||||
const product = props?.ProductItem
|
||||
if (!product) return
|
||||
|
||||
const base = productFeeBase(product)
|
||||
const buyFee = resolveProductCashFeeBuyRial(product, base)
|
||||
const sellFee = resolveProductCashFeeSellRial(product, base)
|
||||
|
||||
if (isBuy.value) {
|
||||
if (val) {
|
||||
form.value.goldValue = toFixedDownThreeChar(parseFloat(val / (((props?.ProductItem?.price_profit) + (props?.ProductItem?.price)) + (props?.ProductItem?.cash_fee_buy))))
|
||||
form.value.goldValue = toFixedDownThreeChar(parseFloat(String(val / (base + buyFee))))
|
||||
if (parseInt(finalPrice.value) > 0) priceErrorVisible.value = true
|
||||
if (parseInt(finalPrice.value) === 0) priceErrorVisible.value = false
|
||||
}
|
||||
@@ -570,7 +590,7 @@ function calculateGold(val: number) {
|
||||
}
|
||||
else {
|
||||
if (val) {
|
||||
form.value.goldValue = toFixedDownThreeChar(parseFloat(val / (((props?.ProductItem?.price_profit) + (props?.ProductItem?.price)) + (props?.ProductItem?.cash_fee_sell))))
|
||||
form.value.goldValue = toFixedDownThreeChar(parseFloat(String(val / (base - sellFee))))
|
||||
if (parseInt(finalPrice.value) > 0) priceErrorVisible.value = true
|
||||
if (parseInt(finalPrice.value) === 0) priceErrorVisible.value = false
|
||||
} else if (val.toString() === '') {
|
||||
@@ -658,6 +678,7 @@ async function ok() {
|
||||
})
|
||||
confirmAction.value = false
|
||||
await walletStore.getMyWallet()
|
||||
resetTransactionsPageData()
|
||||
await router.push({ name: 'transactions' })
|
||||
form.value = {
|
||||
goldValue: undefined,
|
||||
|
||||
@@ -102,20 +102,6 @@
|
||||
@change="percentChange" />
|
||||
</a-form-item>
|
||||
</Transition>
|
||||
<!-- <Transition mode="out-in" name="fade">
|
||||
<div v-if="priceErrorVisible" class="flex gap-1 items-center text-xs mt-[-15px] text-orange-600">
|
||||
<BasicIcon icon="vuesax-linear:danger" />
|
||||
ارزش کل وارد شده بر حسب گرم طلا محاسبه خواهد شد و قیمت وارد شده معادل {{ usePrice(finalPrice) }} می
|
||||
باشد!
|
||||
</div>
|
||||
</Transition>-->
|
||||
<Transition mode="out-in" name="fade">
|
||||
<div v-if="priceErrorVisible" class="flex gap-1 items-center text-xs mt-[-15px] text-blue-600">
|
||||
<BasicIcon icon="vuesax-linear:info-circle" />
|
||||
ارزش کل وارد شده بر حسب گرم طلا محاسبه خواهد شد و قیمت وارد شده معادل {{ usePrice(parseInt(finalPrice.toFixed(0))) }} می
|
||||
باشد!
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
<GoldLoading v-model:show="infoVisible" />
|
||||
@@ -173,7 +159,7 @@
|
||||
<div class="flex justify-between ">
|
||||
<div>وزن:</div>
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold">{{ form.goldValue }}</div>
|
||||
<div class="font-bold">{{ displayTradeGoldAmount }}</div>
|
||||
<div class="text-xs mr-1">{{ weightUnit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,7 +181,7 @@
|
||||
<div v-if="isBuy && feeDeductFromGold" class="flex justify-between mt-3">
|
||||
<div>طلا دریافتی:</div>
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold">{{ form.goldValue }}</div>
|
||||
<div class="font-bold">{{ displayTradeGoldAmount }}</div>
|
||||
<div class="text-xs mr-1">{{ weightUnit }} (دقیق — کارمزد از ریال)</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -214,7 +200,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BasicButton class="mb-20 mt-5 w-full" type="primary" @click="ok(); confirmAction= false">ثبت نهایی</BasicButton>
|
||||
<BasicButton
|
||||
class="mb-20 mt-5 w-full"
|
||||
type="primary"
|
||||
:disabled="loading"
|
||||
@click="ok"
|
||||
>
|
||||
ثبت نهایی
|
||||
</BasicButton>
|
||||
</BasicActionSheet>
|
||||
<BasicModal
|
||||
v-else
|
||||
@@ -228,7 +221,7 @@
|
||||
<div class="flex justify-between ">
|
||||
<div>وزن:</div>
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold">{{ form.goldValue }}</div>
|
||||
<div class="font-bold">{{ displayTradeGoldAmount }}</div>
|
||||
<div class="text-xs mr-1">{{ weightUnit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -250,7 +243,7 @@
|
||||
<div v-if="isBuy && feeDeductFromGold" class="flex justify-between mt-3">
|
||||
<div>طلا دریافتی:</div>
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold">{{ form.goldValue }}</div>
|
||||
<div class="font-bold">{{ displayTradeGoldAmount }}</div>
|
||||
<div class="text-xs mr-1">{{ weightUnit }} (دقیق — کارمزد از ریال)</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,7 +262,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BasicButton class="mt-5 w-full" type="primary" @click="ok(); confirmAction = false">
|
||||
<BasicButton class="mt-5 w-full" type="primary" :disabled="loading" @click="ok">
|
||||
ثبت نهایی
|
||||
</BasicButton>
|
||||
<template #footer>
|
||||
@@ -299,7 +292,7 @@ import img1 from '@/assets/img/home_ly2.svg'
|
||||
import { usePrice, usePriceWithoutType } from '@/utils/usePrice'
|
||||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||
import type { Gold } from '@/models/gold'
|
||||
import { isIphone, loading, loadingMore, toFixedDownThreeChar } from '@/utils'
|
||||
import { isIphone, loading, loadingMore } from '@/utils'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import router from '@/router'
|
||||
import { useWalletStore } from '@/store/walletStore'
|
||||
@@ -330,8 +323,24 @@ import { getLastPrices, getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@
|
||||
import type { Product } from '@/models/products'
|
||||
import { useFundUserStore } from '@/store/fundStore'
|
||||
import {useConfigStore} from "@/store/configStore";
|
||||
import { calcFeeInGrams, calcOrderGoldAmount, calcPayablePriceRial, calcSellGoldDebit, calcSellTargetCash, feeDeductFromGold, loadServiceFeeFromSettings } from '@/utils/serviceFee'
|
||||
import {
|
||||
calcFeeInGrams,
|
||||
calcGramFromLockedRial,
|
||||
calcOrderGoldAmount,
|
||||
calcPayablePriceRial,
|
||||
calcRialFromLockedGram,
|
||||
calcSellGoldDebit,
|
||||
calcSellTargetCash,
|
||||
calcTradeTargetCash,
|
||||
feeDeductFromGold,
|
||||
formatTradeGramDisplay,
|
||||
loadServiceFeeFromSettings,
|
||||
normalizeOrderGoldAmount,
|
||||
serviceFeePercentage,
|
||||
type TradeInputLock,
|
||||
} from '@/utils/serviceFee'
|
||||
import { isStoreClosed, getStoreClosedMessage } from '@/utils/storeHours'
|
||||
import { resetTransactionsPageData } from '@/views/base/transactions/composables/useTransactionsPageData'
|
||||
import { formatCashWalletBalanceText, getWeightUnit } from '@/utils/productDealMethod'
|
||||
import { formatProductWalletBalanceText, getCashWalletBalance, getProductInventoryQuantity } from '@/utils/productAssets'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
@@ -359,7 +368,6 @@ const confirmAction = ref<boolean>(false)
|
||||
const noCompleteVisible = ref<boolean>(false)
|
||||
const buyItems = ref<Transaction[]>([])
|
||||
const sellItems = ref<Transaction[]>([])
|
||||
const priceErrorVisible = ref<boolean>(false)
|
||||
const buyTableOutput = ref<TableOutput<Transaction> | null>(null)
|
||||
const sellTableOutput = ref<TableOutput<Transaction> | null>(null)
|
||||
const el = ref()
|
||||
@@ -386,6 +394,7 @@ const contentClass = computed(() => {
|
||||
return 'mt-5 px-3 pb-4'
|
||||
})
|
||||
const isBuy = ref<boolean>(true)
|
||||
const inputLock = ref<TradeInputLock>(null)
|
||||
|
||||
const tradeProductLabel = computed(() => {
|
||||
const title = props.ProductItem?.title?.trim()
|
||||
@@ -436,7 +445,13 @@ const unitPrice = computed(() => {
|
||||
return isBuy.value ? getTradeBuyPrice(lastPrice.value) : getTradeSellPrice(lastPrice.value)
|
||||
})
|
||||
const orderGoldAmount = computed(() =>
|
||||
calcOrderGoldAmount(form.value?.goldValue, form.value?.goldCash, unitPrice.value, isBuy.value)
|
||||
calcOrderGoldAmount(
|
||||
form.value?.goldValue,
|
||||
form.value?.goldCash,
|
||||
unitPrice.value,
|
||||
isBuy.value,
|
||||
inputLock.value
|
||||
)
|
||||
)
|
||||
const feeInGrams = computed(() => calcFeeInGrams(orderGoldAmount.value))
|
||||
const finalPrice = computed(() => orderGoldAmount.value * unitPrice.value)
|
||||
@@ -447,11 +462,15 @@ const payablePrice = computed(() =>
|
||||
finalPrice.value,
|
||||
feeInRial.value,
|
||||
isBuy.value,
|
||||
!isBuy.value ? form.value?.goldCash : undefined
|
||||
inputLock.value === 'rial' ? form.value?.goldCash : undefined
|
||||
)
|
||||
)
|
||||
const sellGoldDebit = computed(() => calcSellGoldDebit(orderGoldAmount.value, feeInGrams.value))
|
||||
|
||||
const displayTradeGoldAmount = computed(() =>
|
||||
formatTradeGramDisplay(orderGoldAmount.value)
|
||||
)
|
||||
|
||||
const minOrderCash = computed(() => productMinOrderCash(props.ProductItem, isBuy.value))
|
||||
const maxOrderCash = computed(() => productMaxOrderCash(props.ProductItem))
|
||||
const cashValidationMessage = computed(() =>
|
||||
@@ -620,20 +639,18 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
function percentChange(val: number) {
|
||||
if (val !== 0)
|
||||
if (val !== 0) {
|
||||
if (isBuy.value) {
|
||||
form.value.goldCash = unitCashCalculator.value * val
|
||||
calculateGold(form.value.goldCash)
|
||||
calculateCash(form.value.goldValue)
|
||||
} else {
|
||||
// console.log((unitGoldCalculator.value * val).toFixed(2))
|
||||
// form.value.goldValue = parseFloat((unitGoldCalculator.value * val).toFixed(2))
|
||||
form.value.goldValue = toFixedDownThreeChar(parseFloat(unitGoldCalculator.value * val))
|
||||
form.value.goldValue = formatTradeGramDisplay(parseFloat(String(unitGoldCalculator.value * val)))
|
||||
calculateCash(form.value.goldValue)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
form.value.goldValue = undefined
|
||||
form.value.goldCash = undefined
|
||||
inputLock.value = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,11 +662,13 @@ const unitCashCalculator = computed(() => {
|
||||
|
||||
function syncFormWithLatestPrice() {
|
||||
const unit = unitPrice.value
|
||||
if (!unit || Number.isNaN(unit)) return
|
||||
if (!unit || Number.isNaN(unit) || !inputLock.value) return
|
||||
|
||||
const goldValue = form.value.goldValue
|
||||
if (goldValue !== undefined && goldValue !== null && goldValue !== '' && !isNaN(Number(goldValue))) {
|
||||
calculateCash(Number(goldValue))
|
||||
if (inputLock.value === 'gram') {
|
||||
const goldValue = form.value.goldValue
|
||||
if (goldValue !== undefined && goldValue !== null && goldValue !== '' && !isNaN(Number(goldValue))) {
|
||||
calculateCash(Number(goldValue))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -666,42 +685,36 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
watch([feeDeductFromGold, () => serviceFeePercentage.value], () => {
|
||||
syncFormWithLatestPrice()
|
||||
})
|
||||
|
||||
function calculateCash(val: number) {
|
||||
priceErrorVisible.value = false
|
||||
inputLock.value = 'gram'
|
||||
const unit = unitPrice.value
|
||||
if (!unit || Number.isNaN(unit)) return
|
||||
|
||||
form.value.goldCash = val ? parseFloat((val * unit).toFixed(0)) : undefined
|
||||
if (val === undefined || val === null || Number.isNaN(val)) {
|
||||
form.value.goldCash = undefined
|
||||
inputLock.value = null
|
||||
return
|
||||
}
|
||||
|
||||
form.value.goldCash = calcRialFromLockedGram(val, unit, isBuy.value)
|
||||
}
|
||||
|
||||
function calculateGold(val: number) {
|
||||
inputLock.value = 'rial'
|
||||
const unit = unitPrice.value
|
||||
if (!unit || Number.isNaN(unit)) return
|
||||
|
||||
if (isBuy.value) {
|
||||
if (val) {
|
||||
form.value.goldValue = toFixedDownThreeChar(parseFloat(String(val / unit)))
|
||||
priceErrorVisible.value = parseInt(String(finalPrice.value)) > 0
|
||||
} else if (val.toString() === '') {
|
||||
form.value.goldValue = undefined
|
||||
priceErrorVisible.value = false
|
||||
} else {
|
||||
form.value.goldValue = 0
|
||||
priceErrorVisible.value = false
|
||||
}
|
||||
} else if (val) {
|
||||
if (!isBuy.value && feeDeductFromGold.value) {
|
||||
form.value.goldValue = parseFloat((val / unit).toFixed(6))
|
||||
} else {
|
||||
form.value.goldValue = toFixedDownThreeChar(parseFloat(String(val / unit)))
|
||||
}
|
||||
priceErrorVisible.value = parseInt(String(finalPrice.value)) > 0
|
||||
if (val) {
|
||||
form.value.goldValue = formatTradeGramDisplay(calcGramFromLockedRial(val, unit, isBuy.value))
|
||||
} else if (val.toString() === '') {
|
||||
form.value.goldValue = undefined
|
||||
priceErrorVisible.value = false
|
||||
inputLock.value = null
|
||||
} else {
|
||||
form.value.goldValue = 0
|
||||
priceErrorVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -724,7 +737,7 @@ function setIsBuy(val: boolean) {
|
||||
form.value.goldValue = undefined
|
||||
form.value.goldCash = undefined
|
||||
form.value.percentWallet = 0
|
||||
priceErrorVisible.value = false
|
||||
inputLock.value = null
|
||||
// console.log(bganim.value.classList)
|
||||
// bganim?.value?.classList.add(val?'right':'left')
|
||||
// bganim?.value?.style.transform = `translateX(${val ? '0' : '-100'});`
|
||||
@@ -767,6 +780,9 @@ function okConfirm() {
|
||||
}
|
||||
|
||||
async function ok() {
|
||||
if (loading.value) {
|
||||
return
|
||||
}
|
||||
if (isStoreClosed()) {
|
||||
message.warning(getStoreClosedMessage())
|
||||
return
|
||||
@@ -777,19 +793,27 @@ async function ok() {
|
||||
await createOrder({
|
||||
direction: isBuy.value ? 1 : 2,
|
||||
price: Math.round(unitPrice.value),
|
||||
amount: orderGoldAmount.value,
|
||||
target_cash: calcSellTargetCash(form.value.goldCash, form.value.goldValue, unitPrice.value),
|
||||
amount: normalizeOrderGoldAmount(orderGoldAmount.value),
|
||||
target_cash: calcTradeTargetCash(form.value.goldCash, inputLock.value)
|
||||
?? calcSellTargetCash(
|
||||
form.value.goldCash,
|
||||
form.value.goldValue,
|
||||
unitPrice.value,
|
||||
inputLock.value
|
||||
),
|
||||
abshode_id: props.ProductItem?.id,
|
||||
})
|
||||
confirmAction.value = false
|
||||
await walletStore.getMyWallet()
|
||||
await loadWalletAssets(true)
|
||||
resetTransactionsPageData()
|
||||
await router.push({ name: 'transactions' })
|
||||
form.value = {
|
||||
goldValue: undefined,
|
||||
goldCash: undefined,
|
||||
percentWallet: undefined
|
||||
}
|
||||
inputLock.value = null
|
||||
message.success('سفارش شما با موفقیت انجام شد')
|
||||
await fundStore.getMyFund()
|
||||
} catch (e) {
|
||||
|
||||
@@ -9,15 +9,13 @@
|
||||
<div class="text-l mr-2 font-bold">{{ usePriceWithoutType(countBasedInventory, false) }} عدد</div>
|
||||
</div>
|
||||
|
||||
<PortfolioOverview v-if="!boursecuLayout" variant="mobile" />
|
||||
|
||||
<BoursecuAssetsSummary
|
||||
v-else-if="boursecuLayout"
|
||||
v-if="boursecuLayout"
|
||||
variant="mobile"
|
||||
icon-color="primary"
|
||||
/>
|
||||
|
||||
<template v-else-if="showLegacyRows && meta?.single_metal && primaryMetalTotal">
|
||||
<template v-else-if="meta?.single_metal && primaryMetalTotal">
|
||||
<div :class="[rowClass, meta?.has_count_based_products ? 'mt-2' : '']" class="flex justify-center w-full z-20">
|
||||
{{ primaryMetalTotal.label }}:
|
||||
<div class="text-l mr-2 font-bold">{{ formatMetalTotal(primaryMetalTotal) }}</div>
|
||||
@@ -29,6 +27,8 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PortfolioProfitInsight variant="mobile" />
|
||||
|
||||
<div :class="[rowClass, 'mt-2']" class="flex justify-center w-full z-20">
|
||||
دارایی ریالی:
|
||||
<div class="text-l mr-2 font-bold">{{ usePriceWithoutType(cashWallet) }} ریال</div>
|
||||
@@ -41,7 +41,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="showLegacyRows && !boursecuLayout">
|
||||
<template v-else-if="!boursecuLayout">
|
||||
<div
|
||||
v-for="(metal, index) in metalTotals"
|
||||
:key="metal.metal_id"
|
||||
@@ -59,6 +59,8 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PortfolioProfitInsight variant="mobile" />
|
||||
|
||||
<div :class="[rowClass, 'mt-2']" class="flex justify-center w-full z-20">
|
||||
دارایی ریالی:
|
||||
<div class="text-l mr-2 font-bold">{{ usePriceWithoutType(cashWallet) }} ریال</div>
|
||||
@@ -77,7 +79,7 @@
|
||||
import { computed } from 'vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import BoursecuAssetsSummary from './BoursecuAssetsSummary.vue'
|
||||
import PortfolioOverview from './PortfolioOverview.vue'
|
||||
import PortfolioProfitInsight from './PortfolioProfitInsight.vue'
|
||||
import { formatWeightAmount, usePriceWithoutType } from '@/utils/usePrice'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import type { WalletMetalTotal } from '@/models/walletAssets'
|
||||
@@ -119,8 +121,6 @@ const inventoryCards = computed(() => assetsSummary.value?.inventory_cards ?? []
|
||||
const countBasedInventory = computed(() =>
|
||||
(assetsSummary.value?.count_products ?? []).reduce((sum, item) => sum + Number(item.quantity ?? 0), 0)
|
||||
)
|
||||
const showLegacyRows = computed(() => !assetsSummary.value?.portfolio)
|
||||
|
||||
function formatMetalTotal(metal: WalletMetalTotal) {
|
||||
return `${formatWeightAmount(resolveSummaryMetalWeight(metal, inventoryCards.value))}${metal.unit}`
|
||||
}
|
||||
|
||||
@@ -16,9 +16,21 @@
|
||||
'portfolio-overview__profit--down': profitTone === 'loss',
|
||||
}"
|
||||
>
|
||||
<span class="portfolio-overview__profit-amount">{{ profitText }}</span>
|
||||
<span v-if="profitPercent" class="portfolio-overview__profit-percent">{{ profitPercent }}</span>
|
||||
<span class="portfolio-overview__profit-hint">سود/زیان تحققنیافته طلا</span>
|
||||
<div class="portfolio-overview__profit-main">
|
||||
<span class="portfolio-overview__profit-amount">{{ profitText }}</span>
|
||||
<span v-if="profitPercent" class="portfolio-overview__profit-percent">{{ profitPercent }}</span>
|
||||
</div>
|
||||
<div class="portfolio-overview__profit-label">
|
||||
<span class="portfolio-overview__profit-hint">سود/زیان تحققنیافته طلا</span>
|
||||
<button
|
||||
type="button"
|
||||
class="portfolio-overview__profit-info"
|
||||
aria-label="اطلاعات بیشتر"
|
||||
@click="openProfitInfo"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:info-circle" size="14" color="tertiary" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="portfolio-overview__grid">
|
||||
@@ -36,17 +48,16 @@
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<p v-if="hasProfitInsight && averageBuyPrice" class="portfolio-overview__footnote">
|
||||
بر اساس میانگین خرید {{ usePriceWithoutType(averageBuyPrice) }} ریال و قیمت فعلی
|
||||
{{ usePriceWithoutType(liveGoldPrice) }} ریال
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, h } from 'vue'
|
||||
import { Modal } from 'ant-design-vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import {
|
||||
formatPortfolioProfitInfoText,
|
||||
formatPortfolioProfitLabel,
|
||||
formatPortfolioProfitPercent,
|
||||
formatPortfolioProfitText,
|
||||
@@ -79,8 +90,6 @@ const metalRial = computed(() => {
|
||||
return resolveAssetsRialValue(assetsSummary.value, getTradeBuyPrice(lastPrice.value))
|
||||
})
|
||||
const goldWeight = computed(() => Number(portfolio.value?.gold_weight ?? 0))
|
||||
const averageBuyPrice = computed(() => portfolio.value?.average_buy_price ?? null)
|
||||
const liveGoldPrice = computed(() => Number(portfolio.value?.live_gold_price ?? 0))
|
||||
|
||||
const hasProfitInsight = computed(() => portfolio.value?.unrealized_profit_rial != null)
|
||||
const profitTone = computed(() => formatPortfolioProfitLabel(portfolio.value?.unrealized_profit_rial))
|
||||
@@ -89,6 +98,33 @@ const profitPercent = computed(() =>
|
||||
formatPortfolioProfitPercent(portfolio.value?.unrealized_profit_percent)
|
||||
)
|
||||
|
||||
function renderModalLines(text: string) {
|
||||
return h(
|
||||
'div',
|
||||
{ style: { lineHeight: '1.9' } },
|
||||
text.split('\n').map((line, index) =>
|
||||
h(
|
||||
'div',
|
||||
{ key: index, style: index > 0 ? { marginTop: '6px' } : undefined },
|
||||
line
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function openProfitInfo() {
|
||||
const p = portfolio.value
|
||||
if (!p) return
|
||||
|
||||
Modal.confirm({
|
||||
title: 'سود/زیان تحققنیافته طلا',
|
||||
content: renderModalLines(formatPortfolioProfitInfoText(p, weightUnit.value)),
|
||||
okText: 'متوجه شدم',
|
||||
centered: true,
|
||||
cancelButtonProps: { style: { display: 'none' } },
|
||||
})
|
||||
}
|
||||
|
||||
const rootClass = computed(() => [
|
||||
'portfolio-overview',
|
||||
`portfolio-overview--${props.variant}`,
|
||||
@@ -146,17 +182,46 @@ const rootClass = computed(() => [
|
||||
|
||||
.portfolio-overview__profit {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 12px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.portfolio-overview__profit-main {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.portfolio-overview__profit-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.portfolio-overview__profit-info {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
line-height: 0;
|
||||
opacity: 0.85;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.portfolio-overview--desktop .portfolio-overview__profit {
|
||||
background: #f9fafb;
|
||||
}
|
||||
@@ -244,16 +309,4 @@ const rootClass = computed(() => [
|
||||
color: #9ca3af;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.portfolio-overview__footnote {
|
||||
margin: 10px 0 0;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.portfolio-overview--desktop .portfolio-overview__footnote {
|
||||
color: #6b7280;
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="hasProfitInsight"
|
||||
class="portfolio-profit"
|
||||
:class="[
|
||||
`portfolio-profit--${variant}`,
|
||||
{
|
||||
'portfolio-profit--up': profitTone === 'profit',
|
||||
'portfolio-profit--down': profitTone === 'loss',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div class="portfolio-profit__main">
|
||||
<span class="portfolio-profit__amount">{{ profitText }}</span>
|
||||
<span v-if="profitPercent" class="portfolio-profit__percent">{{ profitPercent }}</span>
|
||||
</div>
|
||||
|
||||
<div class="portfolio-profit__label-row">
|
||||
<span class="portfolio-profit__hint">سود/زیان تحققنیافته طلا</span>
|
||||
<button
|
||||
type="button"
|
||||
class="portfolio-profit__info"
|
||||
aria-label="اطلاعات بیشتر"
|
||||
@click="openInfoModal"
|
||||
>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:info-circle"
|
||||
:size="variant === 'desktop' ? 14 : 16"
|
||||
:color="infoIconColor"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h } from 'vue'
|
||||
import { Modal } from 'ant-design-vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import {
|
||||
formatPortfolioProfitInfoText,
|
||||
formatPortfolioProfitLabel,
|
||||
formatPortfolioProfitPercent,
|
||||
formatPortfolioProfitText,
|
||||
} from '@/utils/productAssets'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
variant?: 'mobile' | 'desktop'
|
||||
}>(),
|
||||
{ variant: 'mobile' }
|
||||
)
|
||||
|
||||
const { assetsSummary } = useWalletAssets()
|
||||
|
||||
const portfolio = computed(() => assetsSummary.value?.portfolio)
|
||||
const weightUnit = computed(() => assetsSummary.value?.meta?.weight_unit || 'گرم')
|
||||
|
||||
const hasProfitInsight = computed(() => portfolio.value?.unrealized_profit_rial != null)
|
||||
const profitTone = computed(() => formatPortfolioProfitLabel(portfolio.value?.unrealized_profit_rial))
|
||||
const profitText = computed(() => formatPortfolioProfitText(portfolio.value?.unrealized_profit_rial))
|
||||
const profitPercent = computed(() =>
|
||||
formatPortfolioProfitPercent(portfolio.value?.unrealized_profit_percent)
|
||||
)
|
||||
const infoIconColor = computed(() => (props.variant === 'desktop' ? 'tertiary' : 'tertiary'))
|
||||
|
||||
function renderModalLines(text: string) {
|
||||
return h(
|
||||
'div',
|
||||
{ style: { lineHeight: '1.9' } },
|
||||
text.split('\n').map((line, index) =>
|
||||
h(
|
||||
'div',
|
||||
{ key: index, style: index > 0 ? { marginTop: '6px' } : undefined },
|
||||
line
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function openInfoModal() {
|
||||
const p = portfolio.value
|
||||
if (!p) return
|
||||
|
||||
Modal.confirm({
|
||||
title: 'سود/زیان تحققنیافته طلا',
|
||||
content: renderModalLines(formatPortfolioProfitInfoText(p, weightUnit.value)),
|
||||
okText: 'متوجه شدم',
|
||||
centered: true,
|
||||
cancelButtonProps: { style: { display: 'none' } },
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.portfolio-profit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
margin-top: 10px;
|
||||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.06));
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.portfolio-profit--desktop {
|
||||
align-items: flex-start;
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
background: linear-gradient(145deg, #f9fafb, #f3f4f6);
|
||||
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.portfolio-profit__main {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.portfolio-profit--desktop .portfolio-profit__main {
|
||||
justify-content: flex-start;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.portfolio-profit--up {
|
||||
color: #bbf7d0;
|
||||
border-color: rgba(187, 247, 208, 0.35);
|
||||
}
|
||||
|
||||
.portfolio-profit--desktop.portfolio-profit--up {
|
||||
color: #027a48;
|
||||
background: linear-gradient(145deg, #ecfdf3, #d1fae5);
|
||||
border-color: rgba(2, 122, 72, 0.12);
|
||||
}
|
||||
|
||||
.portfolio-profit--down {
|
||||
color: #fecaca;
|
||||
border-color: rgba(254, 202, 202, 0.35);
|
||||
}
|
||||
|
||||
.portfolio-profit--desktop.portfolio-profit--down {
|
||||
color: #b42318;
|
||||
background: linear-gradient(145deg, #fef3f2, #fee4e2);
|
||||
border-color: rgba(180, 35, 24, 0.12);
|
||||
}
|
||||
|
||||
.portfolio-profit__percent {
|
||||
direction: ltr;
|
||||
unicode-bidi: isolate;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 0.92em;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.portfolio-profit__label-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.portfolio-profit--desktop .portfolio-profit__label-row {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.portfolio-profit__hint {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
opacity: 0.88;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.portfolio-profit--desktop .portfolio-profit__hint {
|
||||
color: #6b7280;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.portfolio-profit__info {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
opacity: 0.85;
|
||||
line-height: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed } from 'vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import BoursecuAssetsSummary from '../../components/BoursecuAssetsSummary.vue'
|
||||
import PortfolioOverview from '../../components/PortfolioOverview.vue'
|
||||
import PortfolioProfitInsight from '../../components/PortfolioProfitInsight.vue'
|
||||
import { formatWeightAmount, usePriceWithoutType } from '@/utils/usePrice'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import type { WalletMetalTotal } from '@/models/walletAssets'
|
||||
@@ -25,7 +25,6 @@ const cashWallet = computed(() => assetsSummary.value?.cash_wallet ?? 0)
|
||||
const countBasedInventory = computed(() =>
|
||||
(assetsSummary.value?.count_products ?? []).reduce((sum, item) => sum + Number(item.quantity ?? 0), 0),
|
||||
)
|
||||
const showLegacyMetrics = computed(() => !assetsSummary.value?.portfolio)
|
||||
|
||||
type MetricItem = {
|
||||
id: string
|
||||
@@ -104,9 +103,9 @@ function handleInfo(metric: MetricItem) {
|
||||
/>
|
||||
|
||||
<template v-else>
|
||||
<PortfolioOverview variant="desktop" />
|
||||
<PortfolioProfitInsight variant="desktop" />
|
||||
|
||||
<div v-if="showLegacyMetrics" class="dhome-metrics">
|
||||
<div class="dhome-metrics">
|
||||
<article v-for="metric in metrics" :key="metric.id" class="dhome-metric-tile">
|
||||
<div class="dhome-metric-tile__head">
|
||||
<span class="dhome-metric-tile__label">{{ metric.label }}</span>
|
||||
|
||||
@@ -37,13 +37,7 @@ const showSkeleton = computed(() => Boolean(getToken()) && !userInfo.value)
|
||||
<HomeSkeleton v-if="showSkeleton" />
|
||||
|
||||
<template v-if="showContent">
|
||||
<DesktopAssetsMetrics
|
||||
v-if="!ctx.isGaroosi.value"
|
||||
@open-gold-modal="ctx.openGoldModal"
|
||||
@open-rial-modal="ctx.openRialModal"
|
||||
/>
|
||||
|
||||
<section class="dhome-hero-row">
|
||||
<section v-if="ctx.isGaroosi.value" class="dhome-hero-row">
|
||||
<DesktopLivePrice />
|
||||
<DesktopPriceRange />
|
||||
</section>
|
||||
@@ -74,6 +68,17 @@ const showSkeleton = computed(() => Boolean(getToken()) && !userInfo.value)
|
||||
|
||||
<template v-else>
|
||||
<DesktopDefaultActions />
|
||||
|
||||
<DesktopAssetsMetrics
|
||||
@open-gold-modal="ctx.openGoldModal"
|
||||
@open-rial-modal="ctx.openRialModal"
|
||||
/>
|
||||
|
||||
<section class="dhome-hero-row">
|
||||
<DesktopLivePrice />
|
||||
<DesktopPriceRange />
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="ctx.bannerEnabled.value && ctx.banners.value?.length"
|
||||
class="dhome-surface dhome-chart-banner-combined"
|
||||
|
||||
@@ -24,6 +24,10 @@ const innerSections = computed(() =>
|
||||
layoutConfig.value.sections.filter((section) => section.visible !== false)
|
||||
)
|
||||
|
||||
const tailSections = computed(() =>
|
||||
(layoutConfig.value.tailSections ?? []).filter((section) => section.visible !== false)
|
||||
)
|
||||
|
||||
/** محتوا با userInfo — دادههای API در پسزمینه لود میشوند */
|
||||
const showContent = computed(() => Boolean(userInfo.value))
|
||||
const showSkeleton = computed(() => Boolean(getToken()) && !userInfo.value)
|
||||
@@ -56,6 +60,12 @@ function resolveSectionComponent(sectionId: HomeSectionId) {
|
||||
:key="section.id"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<component
|
||||
:is="resolveSectionComponent(section.id)"
|
||||
v-for="section in tailSections"
|
||||
:key="`tail-${section.id}`"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<HomeSkeleton v-else-if="showSkeleton" class="w-full" />
|
||||
|
||||
@@ -3,12 +3,14 @@ import type { HomeLayoutConfig } from '../types'
|
||||
/** چیدمان پیشفرض — همه پروژهها بهجز boursecu */
|
||||
export const defaultHomeLayoutConfig: HomeLayoutConfig = {
|
||||
id: 'default',
|
||||
wrapperClass: 'p-1 from-primary rounded-lg p-px to-transparent mt-5',
|
||||
wrapperClass: 'p-1 shadow-lg from-primary rounded-lg p-px to-transparent mt-5',
|
||||
sections: [
|
||||
{ id: 'garoosiAssets' },
|
||||
{ id: 'assets' },
|
||||
{ id: 'mainActions' },
|
||||
{ id: 'referral' },
|
||||
{ id: 'assets' },
|
||||
],
|
||||
tailSections: [
|
||||
{ id: 'marketClosed' },
|
||||
{ id: 'currentPrice' },
|
||||
{ id: 'banners' },
|
||||
|
||||
@@ -31,6 +31,8 @@ export interface HomeLayoutConfig {
|
||||
wrapperClass?: string
|
||||
outerSections?: HomeLayoutSectionConfig[]
|
||||
sections: HomeLayoutSectionConfig[]
|
||||
/** بعد از wrapper — مثل قیمت و نمودار در UI قدیمی */
|
||||
tailSections?: HomeLayoutSectionConfig[]
|
||||
}
|
||||
|
||||
export interface HomeActionItem {
|
||||
|
||||
@@ -281,11 +281,13 @@ import type { TableEngineInput, TableOutput } from '@/models/table'
|
||||
import { getTransactionTable } from '@/api/transaction'
|
||||
import { getLastPrices, getTradeBuyPrice, getTradeSellPrice, lastPrice, setIntervalGetAllPrice, stopPriceInterval } from '@/utils/price'
|
||||
import { getProductBuyPrice, getProductSellPrice, isProductPriceInactive } from '@/utils/productPrice'
|
||||
import { resolveProductCashFeeBuyRial, resolveProductCashFeeSellRial } from '@/utils/productCashFee'
|
||||
import { Product } from '@/models/products'
|
||||
import BasicSelect from '@/components/BasicPacks/BasicSelect.vue'
|
||||
import Bg_main from '@/components/bg_main.vue'
|
||||
import { useFundUserStore } from '@/store/fundStore'
|
||||
import { getProductsCatalogRef } from '@/composables/useProductsCatalog'
|
||||
import { resetTransactionsPageData } from '@/views/base/transactions/composables/useTransactionsPageData'
|
||||
import {
|
||||
getDefaultProductDealMethod,
|
||||
getProductDealMethodOptions,
|
||||
@@ -401,22 +403,9 @@ interface Prop {
|
||||
const props = withDefaults(defineProps<Prop>(), {})
|
||||
const emit = defineEmits<{ ordered: [] }>()
|
||||
const isBuy = ref<boolean>(true)
|
||||
const buyTotalPrice = computed(() => {
|
||||
const item = props?.ProductItem
|
||||
if (!item) return 0
|
||||
const basePrice = (item.price ?? 0) + (item.cash_fee_buy ?? 0)
|
||||
const buyTotalPrice = computed(() => resolveProductTradeBuyUnitPrice(props?.ProductItem))
|
||||
|
||||
return basePrice * productPriceAmountFactor(item)
|
||||
})
|
||||
|
||||
const sellTotalPrice = computed(() => {
|
||||
const item = props?.ProductItem
|
||||
if (!item) return 0
|
||||
|
||||
const basePrice = (item.price ?? 0) - (item.cash_fee_sell ?? 0)
|
||||
|
||||
return basePrice * productPriceAmountFactor(item)
|
||||
})
|
||||
const sellTotalPrice = computed(() => resolveProductTradeSellUnitPrice(props?.ProductItem))
|
||||
const buyFee = computed(() => ((props?.ProductItem?.amount * (props?.ProductItem?.percent_fee_buy ?? 0) * (props?.ProductItem?.price ?? 0)).toFixed(0)))
|
||||
const sellFee = computed(() => ((props?.ProductItem?.amount * (props?.ProductItem?.percent_fee_sell ?? 0) * (props?.ProductItem?.sellPrice ?? 0)).toFixed(0)))
|
||||
|
||||
@@ -737,7 +726,8 @@ function calculateCash(val: number) {
|
||||
// form.value.goldVazn = parseFloat((val * (( props?.ProductItem?.price) + (props?.ProductItem?.cash_fee_buy)))/lastPrice.value?.buy).toFixed(2)
|
||||
if (isBuy.value) {
|
||||
if (val){
|
||||
const unitPrice = (props?.ProductItem?.price ?? 0) + (props?.ProductItem?.cash_fee_buy ?? 0)
|
||||
const base = props?.ProductItem?.price ?? 0
|
||||
const unitPrice = base + resolveProductCashFeeBuyRial(props?.ProductItem, base)
|
||||
form.value.goldCash = parseFloat(String(val * unitPrice * productPriceAmountFactor(props?.ProductItem))).toFixed(0)
|
||||
}
|
||||
else {
|
||||
@@ -747,7 +737,8 @@ function calculateCash(val: number) {
|
||||
}
|
||||
else {
|
||||
if (val){
|
||||
const unitPrice = (props?.ProductItem?.price ?? 0) - (props?.ProductItem?.cash_fee_sell ?? 0)
|
||||
const base = props?.ProductItem?.price ?? 0
|
||||
const unitPrice = base - resolveProductCashFeeSellRial(props?.ProductItem, base)
|
||||
form.value.goldCash = parseFloat(String(val * unitPrice * productPriceAmountFactor(props?.ProductItem))).toFixed(0)
|
||||
}
|
||||
else{
|
||||
@@ -838,8 +829,11 @@ async function initPrice() {
|
||||
await getLastPrices()
|
||||
// await setIntervalGetPrice()
|
||||
loading.value = true
|
||||
props.ProductItem.price = lastPrice?.value?.price? (lastPrice?.value?.price + props.ProductItem?.cash_fee_buy) * props.ProductItem?.amount: 0
|
||||
props.ProductItem.sellPrice = lastPrice?.value?.price? (lastPrice?.value?.price - props.ProductItem?.percent_fee_sell) * props.ProductItem?.amount:0
|
||||
const livePrice = lastPrice?.value?.price ?? 0
|
||||
const buyFee = resolveProductCashFeeBuyRial(props.ProductItem, livePrice)
|
||||
const sellFee = resolveProductCashFeeSellRial(props.ProductItem, livePrice)
|
||||
props.ProductItem.price = livePrice ? (livePrice + buyFee) * props.ProductItem.amount : 0
|
||||
props.ProductItem.sellPrice = livePrice ? (livePrice - sellFee) * props.ProductItem.amount : 0
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
@@ -898,6 +892,7 @@ async function ok() {
|
||||
await loadWalletAssets(true)
|
||||
await fundStore.getMyFund()
|
||||
message.success('سفارش شما با موفقیت انجام شد')
|
||||
resetTransactionsPageData()
|
||||
await router.push({ name: 'transactions' })
|
||||
form.value = {
|
||||
goldValue: undefined,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
? 'drules-content'
|
||||
: 'rules-panel border-solid border-2 border-primary text-primary rounded-[12px] p-4 mobile-content-box'"
|
||||
>
|
||||
<div :class="variant !== 'desktop' ? 'mobile-content-box__scroll' : undefined">
|
||||
<div :class="variant === 'desktop' ? 'drules-content__scroll' : 'mobile-content-box__scroll'">
|
||||
<div :class="variant === 'desktop' ? 'drules-content__title' : 'font-bold text-sm text-primary'">
|
||||
{{ rulesTitle }}
|
||||
</div>
|
||||
|
||||
@@ -77,6 +77,10 @@
|
||||
|
||||
.drules-main {
|
||||
padding: 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
max-height: calc(100vh - 180px);
|
||||
|
||||
&__intro {
|
||||
display: flex;
|
||||
@@ -85,6 +89,7 @@
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #eef0f3;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__logo {
|
||||
@@ -114,12 +119,27 @@
|
||||
}
|
||||
|
||||
.drules-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
border: 1px solid rgba(9, 61, 81, 0.12);
|
||||
border-radius: 12px;
|
||||
background: rgba(9, 61, 81, 0.02);
|
||||
color: @App-primary-color;
|
||||
|
||||
&__scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior-y: contain;
|
||||
padding-left: 4px;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
@@ -206,6 +226,10 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.drules-main {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.drules-side {
|
||||
position: static;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="dtxn-row__meta">
|
||||
<span>{{ toJalaliOutOfUTC(props.item.expire_at) }}</span>
|
||||
<span :class="goldAmount >= 0 ? 'text-green-600' : 'text-red-600'">
|
||||
{{ formatGoldAmount(goldAmount) }} {{ props.item.unit }}
|
||||
{{ formatTransactionGoldAmount(props.item, goldAmount) }} {{ props.item.unit }}
|
||||
</span>
|
||||
<span v-if="isGold">
|
||||
قیمت معامله:
|
||||
@@ -32,8 +32,8 @@
|
||||
{{ usePriceWithoutType(props.item.cash ?? 0) }} ریال
|
||||
</div>
|
||||
<div class="dtxn-row__status">
|
||||
<ATag color="blue" class="!py-1 !px-4 !ml-0 !text-xs">
|
||||
{{ props.item.order?.sts_str ?? props.item.withdraw?.sts_str }}
|
||||
<ATag v-if="statusLabel" color="blue" class="!py-1 !px-4 !ml-0 !text-xs">
|
||||
{{ statusLabel }}
|
||||
</ATag>
|
||||
</div>
|
||||
</button>
|
||||
@@ -59,7 +59,7 @@
|
||||
:class="goldAmount >= 0 ? 'text-green-600' : 'text-red-600'"
|
||||
class="text-gray-500 text-xs number-ltr"
|
||||
>
|
||||
{{ formatGoldAmount(goldAmount) }}
|
||||
{{ formatTransactionGoldAmount(props.item, goldAmount) }}
|
||||
<div>{{ props.item.unit }}</div>
|
||||
</div>
|
||||
<div class="text-gray-500 flex gap-1 text-xs items-center number-ltr">
|
||||
@@ -83,8 +83,8 @@
|
||||
<div class="flex w-full justify-between mt-2">
|
||||
<div class="text-gray-500">وضعیت :</div>
|
||||
<div class="text-gray-500 flex gap-1 text-xs items-center">
|
||||
<ATag class="!py-1 !px-6 !ml-0" color="blue">
|
||||
{{ props.item.order?.sts_str ?? props.item.withdraw?.sts_str }}
|
||||
<ATag v-if="statusLabel" class="!py-1 !px-6 !ml-0" color="blue">
|
||||
{{ statusLabel }}
|
||||
</ATag>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,11 +115,12 @@
|
||||
<script lang="ts" setup>
|
||||
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import {
|
||||
formatGoldAmount,
|
||||
getTransactionGoldAmount,
|
||||
getTransactionTradePrice,
|
||||
resolveTransactionStatusLabel,
|
||||
usePriceWithoutType,
|
||||
} from '@/utils/usePrice'
|
||||
import { formatTransactionGoldAmount } from '@/utils/productDealMethod'
|
||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
|
||||
import { computed, ref } from 'vue'
|
||||
@@ -140,6 +141,7 @@ const props = withDefaults(
|
||||
|
||||
const goldAmount = computed(() => getTransactionGoldAmount(props.item))
|
||||
const tradePrice = computed(() => getTransactionTradePrice(props.item))
|
||||
const statusLabel = computed(() => resolveTransactionStatusLabel(props.item))
|
||||
|
||||
const isUp = computed(
|
||||
() =>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="text-base">{{ formatGoldAmount(item.gold) }}</div>
|
||||
<div class="text-base">{{ formatTransactionGoldAmount(item, item.gold) }}</div>
|
||||
{{ item.unit }}
|
||||
</div>
|
||||
<div
|
||||
@@ -82,7 +82,7 @@
|
||||
</div>
|
||||
<div class="flex flex-col items-end text-xs text-yellow-700">
|
||||
<div class="flex items-center">
|
||||
<div class="text-base ml-1">{{ formatGoldAmount(item.balance_gold) }}</div>
|
||||
<div class="text-base ml-1">{{ formatTransactionGoldAmount(item, item.balance_gold) }}</div>
|
||||
{{ metalWalletBalanceUnit }}
|
||||
</div>
|
||||
<div v-if="getProductUnitWeightLabel(metalWalletProduct)" class="text-gray-500">
|
||||
@@ -93,8 +93,8 @@
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'">
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">وضعیت:</div>
|
||||
<div class="flex text-xs items-center">
|
||||
<ATag color="blue" class="!py-1 !px-6 !ml-0">
|
||||
{{ item.order?.sts_str ?? item.withdraw?.sts_str }}
|
||||
<ATag v-if="statusLabel" color="blue" class="!py-1 !px-6 !ml-0">
|
||||
{{ statusLabel }}
|
||||
</ATag>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,9 +115,10 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { formatGoldAmount, usePriceWithoutType } from '@/utils/usePrice'
|
||||
import { resolveTransactionStatusLabel, usePriceWithoutType } from '@/utils/usePrice'
|
||||
import type { Transaction } from '@/models/transaction'
|
||||
import {
|
||||
formatTransactionGoldAmount,
|
||||
getCashWalletLabel,
|
||||
getProductTotalWeightLabel,
|
||||
getProductUnitWeightLabel,
|
||||
@@ -139,6 +140,7 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
const tradeProduct = computed(() => resolveTransactionTradeProduct(props.item))
|
||||
const statusLabel = computed(() => resolveTransactionStatusLabel(props.item))
|
||||
const tradeProductLabel = computed(
|
||||
() => (tradeProduct.value?.title ?? '').trim() || getMeltedGoldTradeLabel(),
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { computed, inject, onMounted, provide, ref, type ComponentPublicInstance, type InjectionKey } from 'vue'
|
||||
import { computed, inject, onActivated, onMounted, provide, ref, type ComponentPublicInstance, type InjectionKey } from 'vue'
|
||||
import { useIntersectionObserver } from '@vueuse/core'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { getProductsCatalog, getProductsCatalogRef } from '@/composables/useProductsCatalog'
|
||||
@@ -166,7 +166,7 @@ export function useTransactionsPageProvider(options?: { boursecu?: boolean }) {
|
||||
useIntersectionObserver(
|
||||
activeObserverEl,
|
||||
async ([entry]) => {
|
||||
if (entry?.isIntersecting) {
|
||||
if (entry?.isIntersecting && !loading.value && !loadingMore.value) {
|
||||
await loadMoreTransactions()
|
||||
}
|
||||
},
|
||||
@@ -184,6 +184,10 @@ export function useTransactionsPageProvider(options?: { boursecu?: boolean }) {
|
||||
await initTransactionsPageData(isBoursecu)
|
||||
})
|
||||
|
||||
onActivated(async () => {
|
||||
await reloadTransactionsTable()
|
||||
})
|
||||
|
||||
const context: TransactionsPageContext = {
|
||||
pagination: transactionPagination,
|
||||
items: transactionItems,
|
||||
|
||||
@@ -11,9 +11,8 @@ import {
|
||||
type BoursecuTransactionTabId,
|
||||
} from '../config/boursecuTabs'
|
||||
|
||||
/** Shared transactions datasets — mobile/desktop views read this module state only. */
|
||||
let initPromise: Promise<void> | null = null
|
||||
const initialized = ref(false)
|
||||
/** Shared transactions page state — refetched on every page visit (no list cache). */
|
||||
let activeFetchId = 0
|
||||
|
||||
export const transactionsBoursecuMode = ref(false)
|
||||
export const transactionPagination = ref<TableEngineInput>({ filters: [], limit: 7, page: 0 })
|
||||
@@ -27,10 +26,6 @@ export const transactionProductFilter = ref<number>()
|
||||
export const transactionActiveTabId = ref<BoursecuTransactionTabId>('trades')
|
||||
export const transactionTradeSide = ref<BoursecuTradeSide>('buy')
|
||||
|
||||
export function isTransactionsPageDataInitialized() {
|
||||
return initialized.value
|
||||
}
|
||||
|
||||
export function getActiveBoursecuTab(): BoursecuTransactionTab {
|
||||
return (
|
||||
BOURSECU_TRANSACTION_TABS.find((tab) => tab.id === transactionActiveTabId.value) ??
|
||||
@@ -65,46 +60,67 @@ export function resetTransactionsPagination() {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTransactionsPage(append: boolean) {
|
||||
function resolveTransactionFetcher() {
|
||||
const activeTab = getActiveBoursecuTab()
|
||||
return transactionsBoursecuMode.value && activeTab.fetchMode === 'deposit'
|
||||
? getTransactionTable2
|
||||
: getTransactionTable
|
||||
}
|
||||
|
||||
async function fetchTransactionsPage(append: boolean): Promise<boolean> {
|
||||
const fetchId = ++activeFetchId
|
||||
const isFirstPage = transactionPagination.value.page === 0 && !append
|
||||
|
||||
if (isFirstPage) {
|
||||
loading.value = true
|
||||
loadingMore.value = false
|
||||
} else {
|
||||
loadingMore.value = true
|
||||
}
|
||||
|
||||
try {
|
||||
const activeTab = getActiveBoursecuTab()
|
||||
const fetcher =
|
||||
transactionsBoursecuMode.value && activeTab.fetchMode === 'deposit'
|
||||
? getTransactionTable2
|
||||
: getTransactionTable
|
||||
const output = await resolveTransactionFetcher()(transactionPagination.value)
|
||||
if (fetchId !== activeFetchId) {
|
||||
return false
|
||||
}
|
||||
|
||||
transactionTableOutput.value = await fetcher(transactionPagination.value)
|
||||
const rows = transactionTableOutput.value.rows ?? []
|
||||
transactionTableOutput.value = output
|
||||
const rows = output.rows ?? []
|
||||
|
||||
if (isFirstPage) {
|
||||
transactionItems.value = [...rows]
|
||||
} else {
|
||||
transactionItems.value.push(...rows)
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Error fetching transactions:', error)
|
||||
if (fetchId === activeFetchId) {
|
||||
console.error('Error fetching transactions:', error)
|
||||
}
|
||||
return false
|
||||
} finally {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
if (fetchId === activeFetchId) {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** همیشه از سرور — بدون کش لیست */
|
||||
export async function reloadTransactionsTable() {
|
||||
transactionPagination.value.page = 0
|
||||
transactionItems.value = []
|
||||
transactionTableOutput.value = null
|
||||
resetTransactionsPagination()
|
||||
await fetchTransactionsPage(false)
|
||||
}
|
||||
|
||||
export async function loadMoreTransactions() {
|
||||
if (loading.value || loadingMore.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
!transactionTableOutput.value
|
||||
|| (transactionPagination.value.page + 1) * transactionPagination.value.limit
|
||||
@@ -117,40 +133,16 @@ export async function loadMoreTransactions() {
|
||||
await fetchTransactionsPage(true)
|
||||
}
|
||||
|
||||
export async function initTransactionsPageData(boursecu: boolean, force = false) {
|
||||
if (!force && initialized.value && transactionsBoursecuMode.value === boursecu) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!force && initPromise) {
|
||||
return initPromise
|
||||
}
|
||||
|
||||
initPromise = (async () => {
|
||||
try {
|
||||
if (force) {
|
||||
resetTransactionsPageData()
|
||||
}
|
||||
|
||||
transactionsBoursecuMode.value = boursecu
|
||||
transactionPagination.value.page = 0
|
||||
transactionItems.value = []
|
||||
resetTransactionsPagination()
|
||||
await fetchTransactionsPage(false)
|
||||
initialized.value = true
|
||||
} catch (error) {
|
||||
console.error('Transactions page initialization error:', error)
|
||||
} finally {
|
||||
initPromise = null
|
||||
}
|
||||
})()
|
||||
|
||||
return initPromise
|
||||
/** هر بار ورود به صفحه تراکنشها */
|
||||
export async function initTransactionsPageData(boursecu: boolean) {
|
||||
transactionsBoursecuMode.value = boursecu
|
||||
await reloadTransactionsTable()
|
||||
}
|
||||
|
||||
export function resetTransactionsPageData() {
|
||||
initialized.value = false
|
||||
initPromise = null
|
||||
activeFetchId += 1
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
transactionsBoursecuMode.value = false
|
||||
transactionPagination.value = { filters: [], limit: 7, page: 0 }
|
||||
transactionItems.value = []
|
||||
|
||||
Reference in New Issue
Block a user