feat(convert,assets): convert page route and assets portfolio chart
Add convert mobile/desktop views and assets chart from price history; polish calculator and wallet/home integration. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
فرمت هر خط:
|
||||
- YYYY-MM-DD | موضوع | مسیر فایل(ها) | نکتهٔ ۱ خطی
|
||||
|
||||
- 2026-10-22 | addon اینپوت | `assets/css/main.less` | پسزمینه addon = secondary تم، نه primary (سبز tenant)
|
||||
- 2026-10-21 | نمودار خانه = لندینگ | `LandingLiveChartMain.vue` + `useLandingLiveChart` + `zaarboxPriceHistory`؛ جایگزین `PriceChart` در PriceChartSection/DesktopChart* | همان `dlanding-live__main` SVG؛ داده از zaarbox + `/api/price/last`
|
||||
- 2026-10-20 | ظاهر خرید اقساطی | `views/base/installment/InstallmentPanel.vue` | دسکتاپ کلاسهای `dgift` مثل کارت هدیه؛ موبایل کارت `rounded-xl` و توضیح بالای فرم
|
||||
- 2026-10-20 | خرید اقساطی دیجیپی | سرور `DigipayInstallmentService` + تنظیمات `enable_digipay_installment`/`digipay_*`؛ اپ `views/base/installment`؛ ادمین `customers/digipayInstallment` | لینک احراز در تنظیمات؛ مانده اعتبار را ادمین با موبایل مینویسد؛ خرید مس را به کیف gold مینشاند
|
||||
|
||||
@@ -110,7 +110,6 @@
|
||||
/>
|
||||
|
||||
<GoldDayCalculatorModal />
|
||||
<CopperConvertModal />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -133,7 +132,6 @@ import { usePwaInstall } from '@/composables/usePwaInstall'
|
||||
import PwaInstallBanner from '@/components/pwa/PwaInstallBanner.vue'
|
||||
import PwaInstallGuideModal from '@/components/pwa/PwaInstallGuideModal.vue'
|
||||
import GoldDayCalculatorModal from '@/components/GoldDayCalculatorModal.vue'
|
||||
import CopperConvertModal from '@/components/CopperConvertModal.vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import {
|
||||
|
||||
@@ -13,8 +13,8 @@ html.project-boursecu {
|
||||
}
|
||||
|
||||
.ant-input-group-addon {
|
||||
background-color: @secondary-color !important;
|
||||
color: @App-primary-color !important;
|
||||
background-color: #e0be7f !important;
|
||||
color: #0a1b37 !important;
|
||||
}
|
||||
|
||||
.text-red,
|
||||
|
||||
@@ -451,8 +451,8 @@ body:has(.ant-modal-root .ant-modal-wrap:not([style*='display: none'])) {
|
||||
color:@App-primary-color !important;
|
||||
}
|
||||
.ant-input-group-addon{
|
||||
background-color: @App-primary-color !important;
|
||||
color: @secondary-color !important;
|
||||
background-color: #e0be7f !important;
|
||||
color: #0a1b37 !important;
|
||||
}
|
||||
.ant-slider-track{
|
||||
background-color:@App-primary-color !important;
|
||||
|
||||
@@ -61,8 +61,6 @@ interface Prop {
|
||||
placeHolder?: String
|
||||
}
|
||||
|
||||
const addonBgColor = computed(() => props.addonBgColor)
|
||||
const addonTextColor = computed(() => props.addonTextColor)
|
||||
//emits
|
||||
const localValue = computed({
|
||||
set(value) {
|
||||
@@ -132,8 +130,8 @@ const {prefixCls} = usePrefix('input')
|
||||
}
|
||||
|
||||
.ant-input-group-addon {
|
||||
background-color: v-bind(addonBgColor);
|
||||
color: v-bind(addonTextColor);
|
||||
background-color: #e0be7f !important;
|
||||
color: #0a1b37 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -56,6 +56,11 @@ const router = createRouter({
|
||||
name: 'wallet',
|
||||
component: () => import('../views/base/wallet/index.vue')
|
||||
},
|
||||
{
|
||||
path: 'assets',
|
||||
name: 'assets',
|
||||
component: () => import('../views/base/assets/index.vue')
|
||||
},
|
||||
{
|
||||
path: 'user',
|
||||
name: 'user',
|
||||
@@ -128,6 +133,11 @@ const router = createRouter({
|
||||
name: 'giftCard',
|
||||
component: () => import('@/views/base/giftCard/index.vue')
|
||||
},
|
||||
{
|
||||
path: 'convert',
|
||||
name: 'convert',
|
||||
component: () => import('@/views/base/convert/index.vue')
|
||||
},
|
||||
{
|
||||
path: 'installment',
|
||||
name: 'installment',
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
export type ChartSeriesPoint = {
|
||||
timeMs: number
|
||||
value: number
|
||||
}
|
||||
|
||||
const CHART_PAD_Y = 14
|
||||
const DEFAULT_MAX_POINTS = 22
|
||||
|
||||
function downsampleSeries(points: ChartSeriesPoint[], maxPoints: number): ChartSeriesPoint[] {
|
||||
if (points.length <= maxPoints) return points
|
||||
const result: ChartSeriesPoint[] = []
|
||||
const lastIndex = points.length - 1
|
||||
for (let i = 0; i < maxPoints; i += 1) {
|
||||
const index = Math.round((i / (maxPoints - 1)) * lastIndex)
|
||||
result.push(points[index])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function smoothSeries(points: ChartSeriesPoint[], window = 3): ChartSeriesPoint[] {
|
||||
if (points.length < window) return points
|
||||
const half = Math.floor(window / 2)
|
||||
return points.map((point, index) => {
|
||||
let sum = 0
|
||||
let count = 0
|
||||
for (let i = index - half; i <= index + half; i += 1) {
|
||||
if (i < 0 || i >= points.length) continue
|
||||
sum += points[i].value
|
||||
count += 1
|
||||
}
|
||||
return {
|
||||
timeMs: point.timeMs,
|
||||
value: count ? sum / count : point.value,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function buildSmoothLinePath(plot: Array<{ x: number; y: number }>): string {
|
||||
if (plot.length < 2) return ''
|
||||
if (plot.length === 2) {
|
||||
return `M${plot[0].x.toFixed(2)} ${plot[0].y.toFixed(2)} L${plot[1].x.toFixed(2)} ${plot[1].y.toFixed(2)}`
|
||||
}
|
||||
|
||||
let path = `M${plot[0].x.toFixed(2)} ${plot[0].y.toFixed(2)}`
|
||||
for (let i = 0; i < plot.length - 1; i += 1) {
|
||||
const p0 = plot[i - 1] ?? plot[i]
|
||||
const p1 = plot[i]
|
||||
const p2 = plot[i + 1]
|
||||
const p3 = plot[i + 2] ?? p2
|
||||
const cp1x = p1.x + (p2.x - p0.x) / 6
|
||||
const cp1y = p1.y + (p2.y - p0.y) / 6
|
||||
const cp2x = p2.x - (p3.x - p1.x) / 6
|
||||
const cp2y = p2.y - (p3.y - p1.y) / 6
|
||||
path += ` C${cp1x.toFixed(2)} ${cp1y.toFixed(2)} ${cp2x.toFixed(2)} ${cp2y.toFixed(2)} ${p2.x.toFixed(2)} ${p2.y.toFixed(2)}`
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
export function buildSeriesChartPaths(
|
||||
points: ChartSeriesPoint[],
|
||||
width = 560,
|
||||
height = 160,
|
||||
options?: { maxPoints?: number; smooth?: boolean },
|
||||
) {
|
||||
const empty = {
|
||||
hasData: false,
|
||||
linePath: '',
|
||||
areaPath: '',
|
||||
endDot: null as { x: number; y: number } | null,
|
||||
}
|
||||
if (points.length < 2) return empty
|
||||
|
||||
const maxPoints = options?.maxPoints ?? DEFAULT_MAX_POINTS
|
||||
const useSmooth = options?.smooth ?? true
|
||||
|
||||
let series = downsampleSeries(points, maxPoints)
|
||||
if (useSmooth) {
|
||||
series = smoothSeries(series, 3)
|
||||
}
|
||||
|
||||
const values = series.map((point) => point.value)
|
||||
let min = Math.min(...values)
|
||||
let max = Math.max(...values)
|
||||
const span = max - min || max * 0.02 || 1
|
||||
const pad = span * 0.12
|
||||
min -= pad
|
||||
max += pad
|
||||
const range = max - min
|
||||
const usableHeight = height - CHART_PAD_Y * 2
|
||||
|
||||
const plot = series.map((point, index) => {
|
||||
const x = (index / (series.length - 1)) * width
|
||||
const y = CHART_PAD_Y + (1 - (point.value - min) / range) * usableHeight
|
||||
return { x, y }
|
||||
})
|
||||
|
||||
const linePath = buildSmoothLinePath(plot)
|
||||
const areaPath = `${linePath} L${width} ${height} L0 ${height} Z`
|
||||
const last = plot[plot.length - 1]
|
||||
|
||||
return {
|
||||
hasData: true,
|
||||
linePath,
|
||||
areaPath,
|
||||
endDot: last ? { x: last.x, y: last.y } : null,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { computed, ref, watch, type ComputedRef } from 'vue'
|
||||
import {
|
||||
fetchZaanboxPriceHistory,
|
||||
type LandingHistoryRange,
|
||||
} from '@/api/zaarboxPriceHistory'
|
||||
import { buildSeriesChartPaths } from '@/utils/chartAreaPaths'
|
||||
const CHART_RANGE_OPTIONS = [
|
||||
{ value: 'DAILY' as const, shortLabel: '۲۴ ساعت', label: 'ارزش دارایی — ۲۴ ساعت گذشته' },
|
||||
{ value: 'WEEKLY' as const, shortLabel: 'هفته', label: 'ارزش دارایی — هفته گذشته' },
|
||||
{ value: 'MONTHLY' as const, shortLabel: 'ماه', label: 'ارزش دارایی — ماه گذشته' },
|
||||
]
|
||||
|
||||
export function useAssetsPortfolioChart(
|
||||
cashRial: ComputedRef<number>,
|
||||
metalRial: ComputedRef<number>,
|
||||
) {
|
||||
const chartRange = ref<LandingHistoryRange>('MONTHLY')
|
||||
const chartDrawn = ref(false)
|
||||
const chartLoading = ref(false)
|
||||
const priceHistory = ref<Array<{ timeMs: number; price: number }>>([])
|
||||
|
||||
async function loadChartHistory(range: LandingHistoryRange) {
|
||||
chartLoading.value = true
|
||||
try {
|
||||
priceHistory.value = await fetchZaanboxPriceHistory(range)
|
||||
} catch {
|
||||
priceHistory.value = []
|
||||
} finally {
|
||||
chartLoading.value = false
|
||||
chartDrawn.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function setChartRange(range: LandingHistoryRange) {
|
||||
chartRange.value = range
|
||||
void loadChartHistory(range)
|
||||
}
|
||||
|
||||
const portfolioSeries = computed(() => {
|
||||
const cash = Math.max(0, cashRial.value)
|
||||
const metal = Math.max(0, metalRial.value)
|
||||
const prices = priceHistory.value
|
||||
|
||||
if (prices.length < 2) {
|
||||
const total = cash + metal
|
||||
if (total <= 0) return []
|
||||
return prices.map((point) => ({ timeMs: point.timeMs, value: total }))
|
||||
}
|
||||
|
||||
const refPrice = prices[prices.length - 1]?.price
|
||||
if (!refPrice || metal <= 0) {
|
||||
const total = cash + metal
|
||||
return prices.map((point) => ({ timeMs: point.timeMs, value: total }))
|
||||
}
|
||||
|
||||
return prices.map((point) => ({
|
||||
timeMs: point.timeMs,
|
||||
value: Math.round(cash + metal * (point.price / refPrice)),
|
||||
}))
|
||||
})
|
||||
|
||||
const portfolioChart = computed(() => buildSeriesChartPaths(portfolioSeries.value))
|
||||
|
||||
watch(
|
||||
() => cashRial.value + metalRial.value,
|
||||
(total) => {
|
||||
if (total > 0 && priceHistory.value.length === 0) {
|
||||
void loadChartHistory(chartRange.value)
|
||||
}
|
||||
},
|
||||
{ flush: 'post' },
|
||||
)
|
||||
|
||||
async function initAssetsPortfolioChart() {
|
||||
await loadChartHistory(chartRange.value)
|
||||
}
|
||||
|
||||
return {
|
||||
chartRange,
|
||||
chartRangeOptions: CHART_RANGE_OPTIONS,
|
||||
chartDrawn,
|
||||
chartLoading,
|
||||
portfolioChart,
|
||||
setChartRange,
|
||||
initAssetsPortfolioChart,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
<template>
|
||||
<div class="assets-page" :class="{ 'assets-page--desktop': isDesktop }">
|
||||
<MobilePageHeader v-if="!isDesktop" title="داراییها" show-back />
|
||||
|
||||
<div class="assets-page__shell">
|
||||
<DesktopPageTopbar
|
||||
v-if="isDesktop"
|
||||
eyebrow="دارایی"
|
||||
title="داراییها"
|
||||
subtitle="خلاصه و جزئیات دارایی"
|
||||
show-back
|
||||
/>
|
||||
|
||||
<div class="assets-page__body" :class="{ 'mpage-content': !isDesktop }">
|
||||
<section class="assets-top">
|
||||
<article class="assets-card assets-chart">
|
||||
<div
|
||||
class="assets-chart__plot"
|
||||
:class="{ 'is-drawn': chartDrawn }"
|
||||
>
|
||||
<svg
|
||||
v-if="portfolioChart.hasData"
|
||||
class="assets-chart__svg"
|
||||
viewBox="0 0 560 160"
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="assetsLiveFill" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#c98a5f" stop-opacity="0.3" />
|
||||
<stop offset="100%" stop-color="#c98a5f" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path class="assets-chart__area" :d="portfolioChart.areaPath" fill="url(#assetsLiveFill)" />
|
||||
<path
|
||||
class="assets-chart__line"
|
||||
:d="portfolioChart.linePath"
|
||||
pathLength="1"
|
||||
fill="none"
|
||||
stroke="#c98a5f"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<g v-if="portfolioChart.endDot" class="assets-chart__endpoint">
|
||||
<circle :cx="portfolioChart.endDot.x" :cy="portfolioChart.endDot.y" r="5" fill="#c98a5f" />
|
||||
<circle
|
||||
:cx="portfolioChart.endDot.x"
|
||||
:cy="portfolioChart.endDot.y"
|
||||
r="10"
|
||||
fill="#c98a5f"
|
||||
fill-opacity="0.2"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<p v-else class="assets-chart__empty">نمودار ارزش دارایی در دسترس نیست</p>
|
||||
</div>
|
||||
<div class="assets-chart__ranges">
|
||||
<button
|
||||
v-for="option in chartRangeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
:class="{ 'is-active': chartRange === option.value }"
|
||||
@click="setChartRange(option.value)"
|
||||
>
|
||||
{{ option.shortLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="assets-card assets-balance">
|
||||
<span class="assets-balance__label">ارزش کل دارایی</span>
|
||||
<strong class="assets-balance__value">{{ usePrice(totalRialValue) }}</strong>
|
||||
<span class="assets-balance__profit" :class="profitClass">{{ profitSheetLabel }}</span>
|
||||
<span class="assets-balance__hint">سود / زیان تحققنیافته</span>
|
||||
|
||||
<div class="assets-facts">
|
||||
<div v-for="item in summaryItems" :key="item.label" class="assets-facts__row">
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<nav class="assets-actions" aria-label="عملیات دارایی">
|
||||
<button type="button" class="is-main" @click="go('wallet')">واریز</button>
|
||||
<button type="button" @click="go('wallet')">برداشت</button>
|
||||
<button type="button" @click="go('buyProduct')">معامله</button>
|
||||
<button type="button" @click="go('transactions')">تراکنشها</button>
|
||||
</nav>
|
||||
|
||||
<section class="assets-card assets-table-wrap">
|
||||
<div class="assets-table-wrap__head">
|
||||
<h2>داراییها</h2>
|
||||
<p>موجودی هر دارایی و ارزش ریالی آن</p>
|
||||
</div>
|
||||
<div class="assets-table">
|
||||
<div class="assets-table__row assets-table__row--head">
|
||||
<span>دارایی</span>
|
||||
<span>موجودی</span>
|
||||
<span>ارزش ریالی</span>
|
||||
<span>تغییرات</span>
|
||||
</div>
|
||||
<div v-for="row in assetRows" :key="row.key" class="assets-table__row">
|
||||
<span class="assets-table__name">{{ row.name }}</span>
|
||||
<span>{{ row.balance }}</span>
|
||||
<span>{{ row.rial }}</span>
|
||||
<span :class="row.changeClass">{{ row.change }}</span>
|
||||
</div>
|
||||
<div v-if="!assetRows.length" class="assets-table__empty">داراییای برای نمایش نیست.</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import router from '@/router'
|
||||
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
|
||||
import DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useResponsiveLayout'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import { usePrice } from '@/utils/usePrice'
|
||||
import { getTradeBuyPrice, lastPrice } from '@/utils/price'
|
||||
import {
|
||||
assetSheetInventoryCards,
|
||||
formatInventoryCardValue,
|
||||
formatPortfolioProfitPercent,
|
||||
formatPortfolioProfitText,
|
||||
resolveAssetsRialValue,
|
||||
resolveMetalAssetsDisplayLabel,
|
||||
} from '@/utils/productAssets'
|
||||
import type { WalletMetalTotal } from '@/models/walletAssets'
|
||||
import { useAssetsPortfolioChart } from './composables/useAssetsPortfolioChart'
|
||||
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
const { assetsSummary, loadWalletAssets } = useWalletAssets()
|
||||
|
||||
const inventoryCards = computed(() => assetsSummary.value?.inventory_cards ?? [])
|
||||
const metalTotals = computed(() => assetsSummary.value?.metal_totals ?? [])
|
||||
const cashWallet = computed(() => assetsSummary.value?.cash_wallet ?? 0)
|
||||
const metalRialValue = computed(() => {
|
||||
const fromSummary = Number(assetsSummary.value?.assets_rial_value ?? 0)
|
||||
if (fromSummary > 0) return fromSummary
|
||||
return resolveAssetsRialValue(assetsSummary.value, getTradeBuyPrice(lastPrice.value))
|
||||
})
|
||||
const totalRialValue = computed(() => {
|
||||
const fromSummary = Number(assetsSummary.value?.total_rial_value ?? 0)
|
||||
if (fromSummary > 0) return fromSummary
|
||||
return cashWallet.value + metalRialValue.value
|
||||
})
|
||||
|
||||
const {
|
||||
chartRange,
|
||||
chartRangeOptions,
|
||||
chartDrawn,
|
||||
portfolioChart,
|
||||
setChartRange,
|
||||
initAssetsPortfolioChart,
|
||||
} = useAssetsPortfolioChart(cashWallet, metalRialValue)
|
||||
const portfolioInsight = computed(() => assetsSummary.value?.portfolio)
|
||||
|
||||
const profitSheetLabel = computed(() => {
|
||||
const profit = portfolioInsight.value?.unrealized_profit_rial
|
||||
if (profit == null) return '—'
|
||||
const pct = formatPortfolioProfitPercent(portfolioInsight.value?.unrealized_profit_percent)
|
||||
return `${formatPortfolioProfitText(profit)}${pct ? ` (${pct})` : ''}`
|
||||
})
|
||||
|
||||
const profitClass = computed(() => {
|
||||
const profit = portfolioInsight.value?.unrealized_profit_rial
|
||||
if (profit == null || profit === 0) return ''
|
||||
return profit > 0 ? 'is-profit' : 'is-loss'
|
||||
})
|
||||
|
||||
const summaryItems = computed(() => {
|
||||
const items = metalTotals.value.map((metal) => ({
|
||||
label: assetsMetalLabel(metal),
|
||||
value: `${metal.total_weight} ${metal.unit}`,
|
||||
}))
|
||||
items.push(
|
||||
{ label: 'کیف پول ریالی', value: usePrice(cashWallet.value) },
|
||||
{ label: 'ارزش ریالی مس', value: usePrice(metalRialValue.value) },
|
||||
)
|
||||
if (portfolioInsight.value?.average_buy_price != null) {
|
||||
items.push({
|
||||
label: 'میانگین خرید',
|
||||
value: usePrice(portfolioInsight.value.average_buy_price ?? 0),
|
||||
})
|
||||
}
|
||||
return items
|
||||
})
|
||||
|
||||
const assetRows = computed(() => {
|
||||
const pct = formatPortfolioProfitPercent(portfolioInsight.value?.unrealized_profit_percent)
|
||||
const profit = portfolioInsight.value?.unrealized_profit_rial
|
||||
const changeClass = profit == null || profit === 0 ? '' : profit > 0 ? 'is-profit' : 'is-loss'
|
||||
const rows = metalTotals.value.flatMap((metal) => {
|
||||
const head = {
|
||||
key: `metal-${metal.metal_id}`,
|
||||
name: assetsMetalLabel(metal),
|
||||
balance: `${metal.total_weight} ${metal.unit}`,
|
||||
rial: usePrice(metalRialValue.value),
|
||||
change: pct || '—',
|
||||
changeClass,
|
||||
}
|
||||
const cards = assetSheetInventoryCards(inventoryCards.value, metal.metal_id).map((card) => ({
|
||||
key: `card-${card.product_id}`,
|
||||
name: assetsDisplayText(card.title),
|
||||
balance: formatInventoryCardValue(card),
|
||||
rial: '—',
|
||||
change: '—',
|
||||
changeClass: '',
|
||||
}))
|
||||
return [head, ...cards]
|
||||
})
|
||||
rows.push({
|
||||
key: 'cash',
|
||||
name: 'کیف پول ریالی',
|
||||
balance: usePrice(cashWallet.value),
|
||||
rial: usePrice(cashWallet.value),
|
||||
change: '—',
|
||||
changeClass: '',
|
||||
})
|
||||
return rows
|
||||
})
|
||||
|
||||
function assetsDisplayText(text: string | null | undefined): string {
|
||||
return String(text ?? '').replace(/طلا/g, 'مس')
|
||||
}
|
||||
|
||||
function assetsMetalLabel(metal: WalletMetalTotal): string {
|
||||
return assetsDisplayText(resolveMetalAssetsDisplayLabel(metal))
|
||||
}
|
||||
|
||||
function go(name: string) {
|
||||
router.push({ name })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadWalletAssets(true)
|
||||
await initAssetsPortfolioChart()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.assets-page {
|
||||
--assets-line: @App-primary-color;
|
||||
--assets-line-soft: color-mix(in srgb, @App-primary-color 65%, #c9a062);
|
||||
min-height: 100%;
|
||||
padding-bottom: 1.5rem;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.assets-page--desktop {
|
||||
background: #f6f7f9;
|
||||
color: #111827;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.assets-page--desktop .assets-page__shell {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 28px 36px 40px;
|
||||
}
|
||||
|
||||
.assets-page__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.assets-page:not(.assets-page--desktop) .assets-page__body {
|
||||
padding-top: var(--mpage-header-offset);
|
||||
}
|
||||
|
||||
.assets-page--desktop .assets-page__body {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.assets-top {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.assets-top {
|
||||
grid-template-columns: 1.15fr 0.85fr;
|
||||
}
|
||||
}
|
||||
|
||||
.assets-card {
|
||||
background: #fff;
|
||||
border: 1px solid color-mix(in srgb, @App-primary-color 14%, #fff);
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.assets-chart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
min-height: 0;
|
||||
padding: 0.85rem 0.75rem 0.75rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.assets-chart__plot {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 168px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.assets-chart__svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.assets-chart__line {
|
||||
stroke-dasharray: 1;
|
||||
stroke-dashoffset: 1;
|
||||
}
|
||||
|
||||
.assets-chart__area,
|
||||
.assets-chart__endpoint {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.assets-chart__plot.is-drawn .assets-chart__line {
|
||||
animation: assets-chart-draw 1.55s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
}
|
||||
|
||||
.assets-chart__plot.is-drawn .assets-chart__area {
|
||||
animation: assets-chart-fade 0.55s ease-out 0.85s forwards;
|
||||
}
|
||||
|
||||
.assets-chart__plot.is-drawn .assets-chart__endpoint {
|
||||
animation: assets-chart-fade 0.35s ease-out 1.35s forwards;
|
||||
}
|
||||
|
||||
@keyframes assets-chart-draw {
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes assets-chart-fade {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.assets-chart__empty {
|
||||
margin: 3rem 0;
|
||||
text-align: center;
|
||||
opacity: 0.55;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.assets-chart__ranges {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.assets-chart__ranges button,
|
||||
.assets-actions button {
|
||||
border: 1px solid color-mix(in srgb, @App-primary-color 16%, #fff);
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, @App-primary-color 6%, #fff);
|
||||
color: inherit;
|
||||
padding: 0.45rem 0.85rem;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.assets-chart__ranges button.is-active,
|
||||
.assets-actions button.is-main {
|
||||
background: @App-primary-color;
|
||||
border-color: @App-primary-color;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.assets-balance {
|
||||
padding: 1rem 1rem 0.7rem;
|
||||
}
|
||||
|
||||
.assets-balance__label,
|
||||
.assets-balance__hint,
|
||||
.assets-facts__row span,
|
||||
.assets-table-wrap__head p {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.62;
|
||||
}
|
||||
|
||||
.assets-balance__value {
|
||||
display: block;
|
||||
margin: 0.25rem 0;
|
||||
font-size: 1.7rem;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.assets-balance__profit {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.assets-balance__profit.is-profit,
|
||||
.assets-table__row .is-profit {
|
||||
color: #1f8a4c;
|
||||
}
|
||||
|
||||
.assets-balance__profit.is-loss,
|
||||
.assets-table__row .is-loss {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.assets-facts {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
margin-top: 0.85rem;
|
||||
}
|
||||
|
||||
.assets-facts__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding-top: 0.35rem;
|
||||
border-top: 1px solid color-mix(in srgb, @App-primary-color 12%, #fff);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.assets-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.assets-table-wrap {
|
||||
padding: 0.9rem;
|
||||
}
|
||||
|
||||
.assets-table-wrap__head h2 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.assets-table-wrap__head p {
|
||||
margin: 0.15rem 0 0.6rem;
|
||||
}
|
||||
|
||||
.assets-table__row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.3fr 1fr 1.2fr 0.7fr;
|
||||
gap: 0.35rem;
|
||||
align-items: center;
|
||||
padding: 0.6rem 0;
|
||||
border-top: 1px solid color-mix(in srgb, @App-primary-color 12%, #fff);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.assets-table__row--head {
|
||||
border-top: 0;
|
||||
opacity: 0.55;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.assets-table__name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.assets-table__empty {
|
||||
padding: 1rem 0;
|
||||
text-align: center;
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
@@ -164,8 +164,9 @@ function formatRings(value: number) {
|
||||
.copper-convert {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
gap: 0.75rem;
|
||||
text-align: right;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.copper-convert__lead {
|
||||
@@ -175,28 +176,13 @@ function formatRings(value: number) {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.copper-convert__panel {
|
||||
position: relative;
|
||||
padding: 0.75rem 0.875rem 0.7rem;
|
||||
border-radius: 1rem;
|
||||
background: linear-gradient(145deg, #0f2744 0%, #152f4f 48%, #1a365d 100%);
|
||||
border: 1px solid rgba(201, 160, 98, 0.35);
|
||||
box-shadow:
|
||||
0 12px 28px rgba(15, 39, 68, 0.22),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.copper-convert__panel::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
inset-inline-end: -30px;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(232, 180, 140, 0.28) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
.copper-convert__panel,
|
||||
.copper-convert__form,
|
||||
.copper-convert__result {
|
||||
padding: 0.875rem;
|
||||
border-radius: 0.875rem;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.copper-convert__panel-head {
|
||||
@@ -208,17 +194,18 @@ function formatRings(value: number) {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.copper-convert__panel-kicker {
|
||||
.copper-convert__panel-kicker,
|
||||
.copper-convert__result-kicker {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
color: rgba(201, 160, 98, 0.95);
|
||||
color: var(--colorPrimary, #2563eb);
|
||||
}
|
||||
|
||||
.copper-convert__panel-title {
|
||||
.copper-convert__panel-title,
|
||||
.copper-convert__result-title {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 800;
|
||||
color: #f8fafc;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.copper-convert__price-group {
|
||||
@@ -230,7 +217,7 @@ function formatRings(value: number) {
|
||||
.copper-convert__price-group--wire {
|
||||
margin-bottom: 0;
|
||||
padding-top: 0.65rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-top: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.copper-convert__group-label {
|
||||
@@ -238,7 +225,7 @@ function formatRings(value: number) {
|
||||
margin-bottom: 0.45rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
color: rgba(226, 232, 240, 0.75);
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.copper-convert__price-grid {
|
||||
@@ -258,29 +245,29 @@ function formatRings(value: number) {
|
||||
min-width: 0;
|
||||
padding: 0.45rem 0.5rem;
|
||||
border-radius: 0.65rem;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.copper-convert__chip--accent {
|
||||
background: rgba(201, 160, 98, 0.12);
|
||||
border-color: rgba(201, 160, 98, 0.35);
|
||||
background: #eff5f3;
|
||||
border-color: #d4e3de;
|
||||
}
|
||||
|
||||
.copper-convert__chip--muted .copper-convert__chip-value {
|
||||
color: rgba(248, 250, 252, 0.45);
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.copper-convert__chip-label {
|
||||
font-size: 0.625rem;
|
||||
line-height: 1.45;
|
||||
color: rgba(226, 232, 240, 0.85);
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.copper-convert__chip-value {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
color: #f8fafc;
|
||||
color: #0f172a;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -290,11 +277,6 @@ function formatRings(value: number) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.65rem;
|
||||
margin-top: 0.125rem;
|
||||
padding: 0.75rem 0.875rem;
|
||||
border-radius: 1rem;
|
||||
background: #faf8f5;
|
||||
border: 1px solid rgba(201, 160, 98, 0.22);
|
||||
}
|
||||
|
||||
.copper-convert__field-label {
|
||||
@@ -327,9 +309,9 @@ function formatRings(value: number) {
|
||||
}
|
||||
|
||||
.copper-convert__pill--active {
|
||||
color: #0f2744;
|
||||
color: var(--colorPrimary, #2563eb);
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(15, 39, 68, 0.12);
|
||||
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.copper-convert__weight-wrap {
|
||||
@@ -352,7 +334,7 @@ function formatRings(value: number) {
|
||||
background: transparent;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 800;
|
||||
color: #0f2744;
|
||||
color: #0f172a;
|
||||
text-align: right;
|
||||
direction: ltr;
|
||||
}
|
||||
@@ -367,16 +349,13 @@ function formatRings(value: number) {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 800;
|
||||
color: #c9a062;
|
||||
color: var(--colorPrimary, #2563eb);
|
||||
}
|
||||
|
||||
.copper-convert__result {
|
||||
margin-top: 0.125rem;
|
||||
padding: 0.875rem;
|
||||
border-radius: 1rem;
|
||||
background: linear-gradient(180deg, #fff 0%, #faf8f5 100%);
|
||||
border: 1px solid rgba(201, 160, 98, 0.35);
|
||||
box-shadow: 0 8px 24px rgba(15, 39, 68, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.copper-convert__result-head {
|
||||
@@ -386,17 +365,6 @@ function formatRings(value: number) {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.copper-convert__result-kicker {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
color: #c9a062;
|
||||
}
|
||||
|
||||
.copper-convert__result-title {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 800;
|
||||
color: #0f2744;
|
||||
}
|
||||
|
||||
.copper-convert__rings {
|
||||
display: grid;
|
||||
@@ -410,23 +378,11 @@ function formatRings(value: number) {
|
||||
padding: 0.75rem 0.5rem;
|
||||
border-radius: 0.85rem;
|
||||
text-align: center;
|
||||
background: linear-gradient(160deg, #0f2744 0%, #1a365d 100%);
|
||||
border: 1px solid rgba(201, 160, 98, 0.4);
|
||||
background: #eff5f3;
|
||||
border: 1px solid #d4e3de;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.copper-convert__ring-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline-start: 50%;
|
||||
bottom: -24px;
|
||||
width: 80px;
|
||||
height: 48px;
|
||||
transform: translateX(-50%);
|
||||
background: radial-gradient(ellipse, rgba(201, 160, 98, 0.35) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.copper-convert__ring-badge {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.35rem;
|
||||
@@ -434,8 +390,8 @@ function formatRings(value: number) {
|
||||
border-radius: 999px;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
color: #0f2744;
|
||||
background: rgba(201, 160, 98, 0.95);
|
||||
color: var(--colorPrimary, #2563eb);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.copper-convert__ring-count {
|
||||
@@ -443,20 +399,20 @@ function formatRings(value: number) {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
color: #f8fafc;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.copper-convert__ring-caption {
|
||||
margin: 0.15rem 0 0;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
color: rgba(226, 232, 240, 0.85);
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.copper-convert__ring-sub {
|
||||
margin: 0.35rem 0 0;
|
||||
font-size: 0.625rem;
|
||||
color: rgba(201, 160, 98, 0.9);
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.copper-convert__summary {
|
||||
@@ -479,7 +435,7 @@ function formatRings(value: number) {
|
||||
.copper-convert__summary-row strong {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 800;
|
||||
color: #0f2744;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.copper-convert__summary-row strong small {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
computed,
|
||||
inject,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
provide,
|
||||
ref,
|
||||
type ComputedRef,
|
||||
@@ -144,14 +146,14 @@ export function useCopperConvertPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshLivePrices() {
|
||||
onMounted(() => {
|
||||
startLastPricePolling()
|
||||
await loadCatalog()
|
||||
}
|
||||
void loadCatalog()
|
||||
})
|
||||
|
||||
function stopLivePrices() {
|
||||
onUnmounted(() => {
|
||||
stopLastPricePolling()
|
||||
}
|
||||
})
|
||||
|
||||
const ctx: CopperConvertPageContext = {
|
||||
loading,
|
||||
@@ -163,7 +165,7 @@ export function useCopperConvertPage() {
|
||||
}
|
||||
|
||||
provide(COPPER_CONVERT_PAGE_KEY, ctx)
|
||||
return { ...ctx, refreshLivePrices, stopLivePrices, loadCatalog }
|
||||
return ctx
|
||||
}
|
||||
|
||||
export function useCopperConvertPageContext() {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="dconvert">
|
||||
<div class="dconvert__container">
|
||||
<header class="dconvert-topbar">
|
||||
<button type="button" class="dconvert-topbar__back" @click="router.back()">
|
||||
<BasicIcon icon="vuesax-linear:arrow-right-1" size="18" />
|
||||
<span>بازگشت</span>
|
||||
</button>
|
||||
<div class="dconvert-topbar__meta">
|
||||
<p class="dconvert-topbar__eyebrow">محصول</p>
|
||||
<h1 class="dconvert-topbar__title">تبدیل محصول</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="dconvert-surface">
|
||||
<CopperConvertCalculator />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import router from '@/router'
|
||||
import CopperConvertCalculator from '../components/CopperConvertCalculator.vue'
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.dconvert {
|
||||
min-height: 100%;
|
||||
padding: 1.25rem 1.5rem 2rem;
|
||||
}
|
||||
|
||||
.dconvert__container {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.dconvert-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.dconvert-topbar__back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--colorPrimary, #2563eb);
|
||||
font-family: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dconvert-topbar__eyebrow {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.dconvert-topbar__title {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.dconvert-surface {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.875rem;
|
||||
background: #fff;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="convert-page-root">
|
||||
<DesktopConvertView v-show="isDesktop" />
|
||||
<MobileConvertView v-show="!isDesktop" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { useCopperConvertPage } from './composables/useCopperConvertPage'
|
||||
import DesktopConvertView from './desktop/DesktopConvertView.vue'
|
||||
import MobileConvertView from './mobile/MobileConvertView.vue'
|
||||
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
|
||||
useCopperConvertPage()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.convert-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<MobilePageHeader title="تبدیل محصول" show-back />
|
||||
<div class="px-5 mpage-content pb-2">
|
||||
<div class="border-gray-200 border-1 rounded-xl p-4 relative bg-white">
|
||||
<CopperConvertCalculator />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
|
||||
import CopperConvertCalculator from '../components/CopperConvertCalculator.vue'
|
||||
</script>
|
||||
@@ -42,14 +42,14 @@
|
||||
<bg_main />
|
||||
<div class="z-100 relative">
|
||||
<a-form-item>
|
||||
<BasicInput v-model:value="form.goldCash" addon-before="ریال" :addon-bg-color="useConfigStore().config.color"
|
||||
<BasicInput v-model:value="form.goldCash" addon-before="ریال"
|
||||
addon-text-color="white"
|
||||
place-holder="ارزش کل"
|
||||
size="large"
|
||||
type="price" @onchange="calculateGold" />
|
||||
</a-form-item>
|
||||
<a-form-item :rules="[justNumber()]">
|
||||
<BasicInput v-model:value="form.goldValue" addon-before="گرم" :addon-bg-color="useConfigStore().config.color"
|
||||
<BasicInput v-model:value="form.goldValue" addon-before="گرم"
|
||||
addon-text-color="white"
|
||||
place-holder="مقدار طلا"
|
||||
size="large" type="text" inputmode="decimal" @onchange="calculateCash" />
|
||||
|
||||
@@ -74,14 +74,14 @@
|
||||
<bg_main />
|
||||
<div class="z-100 relative">
|
||||
<a-form-item>
|
||||
<BasicInput v-model:value="form.goldCash" addon-before="ریال" :addon-bg-color="useConfigStore().config.color"
|
||||
<BasicInput v-model:value="form.goldCash" addon-before="ریال"
|
||||
addon-text-color="white"
|
||||
place-holder="ارزش کل"
|
||||
size="large"
|
||||
type="price" @onchange="calculateGold" />
|
||||
</a-form-item>
|
||||
<a-form-item :rules="[justNumber()]">
|
||||
<BasicInput v-model:value="form.goldValue" :addon-before="weightUnit" :addon-bg-color="useConfigStore().config.color"
|
||||
<BasicInput v-model:value="form.goldValue" :addon-before="weightUnit"
|
||||
addon-text-color="white"
|
||||
:place-holder="amountFieldLabel"
|
||||
size="large" type="text" inputmode="decimal" @onchange="calculateCash" />
|
||||
|
||||
@@ -102,7 +102,7 @@ export function useHomePage() {
|
||||
icon: 'vuesax-linear:receipt-item',
|
||||
label: 'موجودی',
|
||||
handler: () => {
|
||||
wholeBalanceVisible.value = true
|
||||
router.push({ name: 'assets' })
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -110,7 +110,7 @@ export function useHomePage() {
|
||||
icon: 'vuesax-linear:cup',
|
||||
label: 'دارایی ها',
|
||||
handler: () => {
|
||||
wholeBalanceVisible.value = true
|
||||
router.push({ name: 'assets' })
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -192,7 +192,7 @@ export function useHomePage() {
|
||||
}
|
||||
|
||||
function openAssetsSheet() {
|
||||
wholeBalanceVisible.value = true
|
||||
router.push({ name: 'assets' })
|
||||
}
|
||||
|
||||
function handleBannerClick(banner: { typeClick?: string; modalText?: string; link?: string }) {
|
||||
|
||||
@@ -4,12 +4,6 @@
|
||||
|
||||
<LogoutConfirmDialog v-model:show="showLogoutConfirm" @confirm="logout" />
|
||||
|
||||
<BalanceSheet
|
||||
v-model:show="wholeBalanceVisible"
|
||||
:wallet-info="walletStore.walletInfo"
|
||||
:gold-val="goldVal"
|
||||
/>
|
||||
|
||||
<BasicModal v-model:visible="showTextBanner" :custom-footer="true" destroy-on-close>
|
||||
<div class="max-h-[54vh] ql-editor" v-html="TextBanner" />
|
||||
</BasicModal>
|
||||
@@ -21,7 +15,6 @@
|
||||
<script lang="tsx" setup>
|
||||
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
|
||||
import LogoutConfirmDialog from './components/LogoutConfirmDialog.vue'
|
||||
import BalanceSheet from './components/BalanceSheet.vue'
|
||||
import ShareModal from './components/ShareModal.vue'
|
||||
import HomePageShell from './shell/HomePageShell.vue'
|
||||
import { useHomePage } from './composables/useHomePage'
|
||||
@@ -30,13 +23,10 @@ import { recoverMobileViewportAfterAuth } from '@/layouts/composables/useMobileS
|
||||
|
||||
const {
|
||||
userStore,
|
||||
walletStore,
|
||||
showLogoutConfirm,
|
||||
wholeBalanceVisible,
|
||||
showTextBanner,
|
||||
showShare,
|
||||
TextBanner,
|
||||
goldVal,
|
||||
logout,
|
||||
} = useHomePage()
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { openCopperConvert } from '@/composables/useCopperConvertModal'
|
||||
import { getBoursecuActionIconSrc } from '@/utils/config'
|
||||
import type { HomeQuickActionItem } from '../types'
|
||||
|
||||
@@ -22,7 +21,7 @@ export const boursecuQuickActions: HomeQuickActionItem[] = [
|
||||
label: 'دارایی ها',
|
||||
icon: 'vuesax-linear:cup',
|
||||
iconSrc: getBoursecuActionIconSrc('assets'),
|
||||
// شیت موجودیها — نه مسیر fund (با enable_product_funds=false گارد بیصدا برمیگرداند)
|
||||
route: { name: 'assets' },
|
||||
},
|
||||
{
|
||||
id: 'transactions',
|
||||
@@ -43,6 +42,6 @@ export const boursecuQuickActions: HomeQuickActionItem[] = [
|
||||
label: 'تبدیل',
|
||||
icon: 'vuesax-linear:refresh-circle',
|
||||
iconSrc: getBoursecuActionIconSrc('convert'),
|
||||
handler: () => openCopperConvert(),
|
||||
route: { name: 'convert' },
|
||||
},
|
||||
]
|
||||
@@ -43,8 +43,6 @@
|
||||
<BasicInput
|
||||
v-model:value="item.count"
|
||||
:addon-before="quantityUnit"
|
||||
:addon-bg-color="config.config?.color"
|
||||
addon-text-color="white"
|
||||
type="number"
|
||||
:inputmode="isCountBased ? 'numeric' : 'decimal'"
|
||||
@update:value="onCountInput"
|
||||
@@ -61,7 +59,6 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import type { CountPInventory } from '@/models/products'
|
||||
import { getProductDisplayImage } from '@/utils/productAssets'
|
||||
import {
|
||||
@@ -110,8 +107,6 @@ const displayMinDelivery = computed(() => {
|
||||
: formatGoldAmount(minDeliveryQuantity.value)
|
||||
})
|
||||
|
||||
const config = useConfigStore()
|
||||
|
||||
function selectCountInput(e: FocusEvent) {
|
||||
const el = e.target as HTMLInputElement | null
|
||||
if (!el || el.tagName !== 'INPUT' || typeof el.select !== 'function') return
|
||||
|
||||
@@ -243,8 +243,11 @@ export function useWalletPage() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (router.currentRoute.value.query.state === 'deposit') {
|
||||
const walletState = router.currentRoute.value.query.state
|
||||
if (walletState === 'deposit') {
|
||||
isDeposit.value = true
|
||||
} else if (walletState === 'withdraw') {
|
||||
isDeposit.value = false
|
||||
}
|
||||
cardForm.value.birth_date = userStore.userInfo?.birth_date
|
||||
await initWalletPageData()
|
||||
|
||||
@@ -45,8 +45,6 @@
|
||||
<BasicInput
|
||||
v-model:value="form.value"
|
||||
addon-before="ریال"
|
||||
:addon-bg-color="useConfigStore().config.color"
|
||||
addon-text-color="white"
|
||||
place-holder="مبلغ"
|
||||
size="large"
|
||||
type="price"
|
||||
@@ -175,7 +173,6 @@ import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||
import ListAnim from '@/components/ListAnim.vue'
|
||||
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import { required } from '@/utils/inputRules'
|
||||
import BoursecuBankAccountPicker from '../components/BoursecuBankAccountPicker.vue'
|
||||
import BoursecuWithdrawGuide from '../components/BoursecuWithdrawGuide.vue'
|
||||
|
||||
@@ -50,8 +50,6 @@
|
||||
<BasicInput
|
||||
v-model:value="form.value"
|
||||
addon-before="ریال"
|
||||
:addon-bg-color="useConfigStore().config.color"
|
||||
addon-text-color="white"
|
||||
place-holder="مبلغ"
|
||||
size="large"
|
||||
type="price"
|
||||
@@ -145,7 +143,6 @@ import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||
import Bg_main from '@/components/bg_main.vue'
|
||||
import ListAnim from '@/components/ListAnim.vue'
|
||||
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import { required } from '@/utils/inputRules'
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import BoursecuBankAccountPicker from '../components/BoursecuBankAccountPicker.vue'
|
||||
|
||||
Reference in New Issue
Block a user