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:
2026-10-22 17:38:27 +03:30
co-authored by Cursor
parent 819e007e60
commit 20c871e66c
23 changed files with 872 additions and 131 deletions
@@ -3,6 +3,7 @@
فرمت هر خط: فرمت هر خط:
- YYYY-MM-DD | موضوع | مسیر فایل(ها) | نکتهٔ ۱ خطی - 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-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 | ظاهر خرید اقساطی | `views/base/installment/InstallmentPanel.vue` | دسکتاپ کلاس‌های `dgift` مثل کارت هدیه؛ موبایل کارت `rounded-xl` و توضیح بالای فرم
- 2026-10-20 | خرید اقساطی دیجی‌پی | سرور `DigipayInstallmentService` + تنظیمات `enable_digipay_installment`/`digipay_*`؛ اپ `views/base/installment`؛ ادمین `customers/digipayInstallment` | لینک احراز در تنظیمات؛ مانده اعتبار را ادمین با موبایل می‌نویسد؛ خرید مس را به کیف gold می‌نشاند - 2026-10-20 | خرید اقساطی دیجی‌پی | سرور `DigipayInstallmentService` + تنظیمات `enable_digipay_installment`/`digipay_*`؛ اپ `views/base/installment`؛ ادمین `customers/digipayInstallment` | لینک احراز در تنظیمات؛ مانده اعتبار را ادمین با موبایل می‌نویسد؛ خرید مس را به کیف gold می‌نشاند
-2
View File
@@ -110,7 +110,6 @@
/> />
<GoldDayCalculatorModal /> <GoldDayCalculatorModal />
<CopperConvertModal />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -133,7 +132,6 @@ import { usePwaInstall } from '@/composables/usePwaInstall'
import PwaInstallBanner from '@/components/pwa/PwaInstallBanner.vue' import PwaInstallBanner from '@/components/pwa/PwaInstallBanner.vue'
import PwaInstallGuideModal from '@/components/pwa/PwaInstallGuideModal.vue' import PwaInstallGuideModal from '@/components/pwa/PwaInstallGuideModal.vue'
import GoldDayCalculatorModal from '@/components/GoldDayCalculatorModal.vue' import GoldDayCalculatorModal from '@/components/GoldDayCalculatorModal.vue'
import CopperConvertModal from '@/components/CopperConvertModal.vue'
import { useUserStore } from '@/store/userStore' import { useUserStore } from '@/store/userStore'
import { useSettingsStore } from '@/store/settingsStore' import { useSettingsStore } from '@/store/settingsStore'
import { import {
+2 -2
View File
@@ -13,8 +13,8 @@ html.project-boursecu {
} }
.ant-input-group-addon { .ant-input-group-addon {
background-color: @secondary-color !important; background-color: #e0be7f !important;
color: @App-primary-color !important; color: #0a1b37 !important;
} }
.text-red, .text-red,
+2 -2
View File
@@ -451,8 +451,8 @@ body:has(.ant-modal-root .ant-modal-wrap:not([style*='display: none'])) {
color:@App-primary-color !important; color:@App-primary-color !important;
} }
.ant-input-group-addon{ .ant-input-group-addon{
background-color: @App-primary-color !important; background-color: #e0be7f !important;
color: @secondary-color !important; color: #0a1b37 !important;
} }
.ant-slider-track{ .ant-slider-track{
background-color:@App-primary-color !important; background-color:@App-primary-color !important;
+2 -4
View File
@@ -61,8 +61,6 @@ interface Prop {
placeHolder?: String placeHolder?: String
} }
const addonBgColor = computed(() => props.addonBgColor)
const addonTextColor = computed(() => props.addonTextColor)
//emits //emits
const localValue = computed({ const localValue = computed({
set(value) { set(value) {
@@ -132,8 +130,8 @@ const {prefixCls} = usePrefix('input')
} }
.ant-input-group-addon { .ant-input-group-addon {
background-color: v-bind(addonBgColor); background-color: #e0be7f !important;
color: v-bind(addonTextColor); color: #0a1b37 !important;
} }
} }
</style> </style>
+10
View File
@@ -56,6 +56,11 @@ const router = createRouter({
name: 'wallet', name: 'wallet',
component: () => import('../views/base/wallet/index.vue') component: () => import('../views/base/wallet/index.vue')
}, },
{
path: 'assets',
name: 'assets',
component: () => import('../views/base/assets/index.vue')
},
{ {
path: 'user', path: 'user',
name: 'user', name: 'user',
@@ -128,6 +133,11 @@ const router = createRouter({
name: 'giftCard', name: 'giftCard',
component: () => import('@/views/base/giftCard/index.vue') component: () => import('@/views/base/giftCard/index.vue')
}, },
{
path: 'convert',
name: 'convert',
component: () => import('@/views/base/convert/index.vue')
},
{ {
path: 'installment', path: 'installment',
name: 'installment', name: 'installment',
+107
View File
@@ -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,
}
}
+485
View File
@@ -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 { .copper-convert {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: 0.75rem;
text-align: right; text-align: right;
color: #334155;
} }
.copper-convert__lead { .copper-convert__lead {
@@ -175,28 +176,13 @@ function formatRings(value: number) {
color: #64748b; color: #64748b;
} }
.copper-convert__panel { .copper-convert__panel,
position: relative; .copper-convert__form,
padding: 0.75rem 0.875rem 0.7rem; .copper-convert__result {
border-radius: 1rem; padding: 0.875rem;
background: linear-gradient(145deg, #0f2744 0%, #152f4f 48%, #1a365d 100%); border-radius: 0.875rem;
border: 1px solid rgba(201, 160, 98, 0.35); background: #fff;
box-shadow: border: 1px solid #e5e7eb;
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-head { .copper-convert__panel-head {
@@ -208,17 +194,18 @@ function formatRings(value: number) {
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
.copper-convert__panel-kicker { .copper-convert__panel-kicker,
.copper-convert__result-kicker {
font-size: 0.6875rem; font-size: 0.6875rem;
font-weight: 700; font-weight: 700;
letter-spacing: 0.04em; color: var(--colorPrimary, #2563eb);
color: rgba(201, 160, 98, 0.95);
} }
.copper-convert__panel-title { .copper-convert__panel-title,
.copper-convert__result-title {
font-size: 0.9375rem; font-size: 0.9375rem;
font-weight: 800; font-weight: 800;
color: #f8fafc; color: #0f172a;
} }
.copper-convert__price-group { .copper-convert__price-group {
@@ -230,7 +217,7 @@ function formatRings(value: number) {
.copper-convert__price-group--wire { .copper-convert__price-group--wire {
margin-bottom: 0; margin-bottom: 0;
padding-top: 0.65rem; padding-top: 0.65rem;
border-top: 1px solid rgba(255, 255, 255, 0.08); border-top: 1px solid #f1f5f9;
} }
.copper-convert__group-label { .copper-convert__group-label {
@@ -238,7 +225,7 @@ function formatRings(value: number) {
margin-bottom: 0.45rem; margin-bottom: 0.45rem;
font-size: 0.6875rem; font-size: 0.6875rem;
font-weight: 600; font-weight: 600;
color: rgba(226, 232, 240, 0.75); color: #64748b;
} }
.copper-convert__price-grid { .copper-convert__price-grid {
@@ -258,29 +245,29 @@ function formatRings(value: number) {
min-width: 0; min-width: 0;
padding: 0.45rem 0.5rem; padding: 0.45rem 0.5rem;
border-radius: 0.65rem; border-radius: 0.65rem;
background: rgba(255, 255, 255, 0.06); background: #f8fafc;
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid #e5e7eb;
} }
.copper-convert__chip--accent { .copper-convert__chip--accent {
background: rgba(201, 160, 98, 0.12); background: #eff5f3;
border-color: rgba(201, 160, 98, 0.35); border-color: #d4e3de;
} }
.copper-convert__chip--muted .copper-convert__chip-value { .copper-convert__chip--muted .copper-convert__chip-value {
color: rgba(248, 250, 252, 0.45); color: #94a3b8;
} }
.copper-convert__chip-label { .copper-convert__chip-label {
font-size: 0.625rem; font-size: 0.625rem;
line-height: 1.45; line-height: 1.45;
color: rgba(226, 232, 240, 0.85); color: #64748b;
} }
.copper-convert__chip-value { .copper-convert__chip-value {
font-size: 0.6875rem; font-size: 0.6875rem;
font-weight: 700; font-weight: 700;
color: #f8fafc; color: #0f172a;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -290,11 +277,6 @@ function formatRings(value: number) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.65rem; 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 { .copper-convert__field-label {
@@ -327,9 +309,9 @@ function formatRings(value: number) {
} }
.copper-convert__pill--active { .copper-convert__pill--active {
color: #0f2744; color: var(--colorPrimary, #2563eb);
background: #fff; 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 { .copper-convert__weight-wrap {
@@ -352,7 +334,7 @@ function formatRings(value: number) {
background: transparent; background: transparent;
font-size: 1.125rem; font-size: 1.125rem;
font-weight: 800; font-weight: 800;
color: #0f2744; color: #0f172a;
text-align: right; text-align: right;
direction: ltr; direction: ltr;
} }
@@ -367,16 +349,13 @@ function formatRings(value: number) {
flex-shrink: 0; flex-shrink: 0;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 800; font-weight: 800;
color: #c9a062; color: var(--colorPrimary, #2563eb);
} }
.copper-convert__result { .copper-convert__result {
margin-top: 0.125rem; display: flex;
padding: 0.875rem; flex-direction: column;
border-radius: 1rem; gap: 0.75rem;
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);
} }
.copper-convert__result-head { .copper-convert__result-head {
@@ -386,17 +365,6 @@ function formatRings(value: number) {
margin-bottom: 0.75rem; 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 { .copper-convert__rings {
display: grid; display: grid;
@@ -410,23 +378,11 @@ function formatRings(value: number) {
padding: 0.75rem 0.5rem; padding: 0.75rem 0.5rem;
border-radius: 0.85rem; border-radius: 0.85rem;
text-align: center; text-align: center;
background: linear-gradient(160deg, #0f2744 0%, #1a365d 100%); background: #eff5f3;
border: 1px solid rgba(201, 160, 98, 0.4); border: 1px solid #d4e3de;
overflow: hidden; 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 { .copper-convert__ring-badge {
display: inline-block; display: inline-block;
margin-bottom: 0.35rem; margin-bottom: 0.35rem;
@@ -434,8 +390,8 @@ function formatRings(value: number) {
border-radius: 999px; border-radius: 999px;
font-size: 0.625rem; font-size: 0.625rem;
font-weight: 700; font-weight: 700;
color: #0f2744; color: var(--colorPrimary, #2563eb);
background: rgba(201, 160, 98, 0.95); background: #fff;
} }
.copper-convert__ring-count { .copper-convert__ring-count {
@@ -443,20 +399,20 @@ function formatRings(value: number) {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 800; font-weight: 800;
line-height: 1.2; line-height: 1.2;
color: #f8fafc; color: #0f172a;
} }
.copper-convert__ring-caption { .copper-convert__ring-caption {
margin: 0.15rem 0 0; margin: 0.15rem 0 0;
font-size: 0.6875rem; font-size: 0.6875rem;
font-weight: 600; font-weight: 600;
color: rgba(226, 232, 240, 0.85); color: #64748b;
} }
.copper-convert__ring-sub { .copper-convert__ring-sub {
margin: 0.35rem 0 0; margin: 0.35rem 0 0;
font-size: 0.625rem; font-size: 0.625rem;
color: rgba(201, 160, 98, 0.9); color: #64748b;
} }
.copper-convert__summary { .copper-convert__summary {
@@ -479,7 +435,7 @@ function formatRings(value: number) {
.copper-convert__summary-row strong { .copper-convert__summary-row strong {
font-size: 0.8125rem; font-size: 0.8125rem;
font-weight: 800; font-weight: 800;
color: #0f2744; color: #0f172a;
} }
.copper-convert__summary-row strong small { .copper-convert__summary-row strong small {
@@ -1,6 +1,8 @@
import { import {
computed, computed,
inject, inject,
onMounted,
onUnmounted,
provide, provide,
ref, ref,
type ComputedRef, type ComputedRef,
@@ -144,14 +146,14 @@ export function useCopperConvertPage() {
} }
} }
async function refreshLivePrices() { onMounted(() => {
startLastPricePolling() startLastPricePolling()
await loadCatalog() void loadCatalog()
} })
function stopLivePrices() { onUnmounted(() => {
stopLastPricePolling() stopLastPricePolling()
} })
const ctx: CopperConvertPageContext = { const ctx: CopperConvertPageContext = {
loading, loading,
@@ -163,7 +165,7 @@ export function useCopperConvertPage() {
} }
provide(COPPER_CONVERT_PAGE_KEY, ctx) provide(COPPER_CONVERT_PAGE_KEY, ctx)
return { ...ctx, refreshLivePrices, stopLivePrices, loadCatalog } return ctx
} }
export function useCopperConvertPageContext() { 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>
+24
View File
@@ -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>
+2 -2
View File
@@ -42,14 +42,14 @@
<bg_main /> <bg_main />
<div class="z-100 relative"> <div class="z-100 relative">
<a-form-item> <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" addon-text-color="white"
place-holder="ارزش کل" place-holder="ارزش کل"
size="large" size="large"
type="price" @onchange="calculateGold" /> type="price" @onchange="calculateGold" />
</a-form-item> </a-form-item>
<a-form-item :rules="[justNumber()]"> <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" addon-text-color="white"
place-holder="مقدار طلا" place-holder="مقدار طلا"
size="large" type="text" inputmode="decimal" @onchange="calculateCash" /> size="large" type="text" inputmode="decimal" @onchange="calculateCash" />
+2 -2
View File
@@ -74,14 +74,14 @@
<bg_main /> <bg_main />
<div class="z-100 relative"> <div class="z-100 relative">
<a-form-item> <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" addon-text-color="white"
place-holder="ارزش کل" place-holder="ارزش کل"
size="large" size="large"
type="price" @onchange="calculateGold" /> type="price" @onchange="calculateGold" />
</a-form-item> </a-form-item>
<a-form-item :rules="[justNumber()]"> <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" addon-text-color="white"
:place-holder="amountFieldLabel" :place-holder="amountFieldLabel"
size="large" type="text" inputmode="decimal" @onchange="calculateCash" /> size="large" type="text" inputmode="decimal" @onchange="calculateCash" />
@@ -102,7 +102,7 @@ export function useHomePage() {
icon: 'vuesax-linear:receipt-item', icon: 'vuesax-linear:receipt-item',
label: 'موجودی', label: 'موجودی',
handler: () => { handler: () => {
wholeBalanceVisible.value = true router.push({ name: 'assets' })
}, },
}, },
{ {
@@ -110,7 +110,7 @@ export function useHomePage() {
icon: 'vuesax-linear:cup', icon: 'vuesax-linear:cup',
label: 'دارایی ها', label: 'دارایی ها',
handler: () => { handler: () => {
wholeBalanceVisible.value = true router.push({ name: 'assets' })
}, },
}, },
] ]
@@ -192,7 +192,7 @@ export function useHomePage() {
} }
function openAssetsSheet() { function openAssetsSheet() {
wholeBalanceVisible.value = true router.push({ name: 'assets' })
} }
function handleBannerClick(banner: { typeClick?: string; modalText?: string; link?: string }) { function handleBannerClick(banner: { typeClick?: string; modalText?: string; link?: string }) {
-10
View File
@@ -4,12 +4,6 @@
<LogoutConfirmDialog v-model:show="showLogoutConfirm" @confirm="logout" /> <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> <BasicModal v-model:visible="showTextBanner" :custom-footer="true" destroy-on-close>
<div class="max-h-[54vh] ql-editor" v-html="TextBanner" /> <div class="max-h-[54vh] ql-editor" v-html="TextBanner" />
</BasicModal> </BasicModal>
@@ -21,7 +15,6 @@
<script lang="tsx" setup> <script lang="tsx" setup>
import BasicModal from '@/components/BasicPacks/BasicModal.vue' import BasicModal from '@/components/BasicPacks/BasicModal.vue'
import LogoutConfirmDialog from './components/LogoutConfirmDialog.vue' import LogoutConfirmDialog from './components/LogoutConfirmDialog.vue'
import BalanceSheet from './components/BalanceSheet.vue'
import ShareModal from './components/ShareModal.vue' import ShareModal from './components/ShareModal.vue'
import HomePageShell from './shell/HomePageShell.vue' import HomePageShell from './shell/HomePageShell.vue'
import { useHomePage } from './composables/useHomePage' import { useHomePage } from './composables/useHomePage'
@@ -30,13 +23,10 @@ import { recoverMobileViewportAfterAuth } from '@/layouts/composables/useMobileS
const { const {
userStore, userStore,
walletStore,
showLogoutConfirm, showLogoutConfirm,
wholeBalanceVisible,
showTextBanner, showTextBanner,
showShare, showShare,
TextBanner, TextBanner,
goldVal,
logout, logout,
} = useHomePage() } = useHomePage()
@@ -1,4 +1,3 @@
import { openCopperConvert } from '@/composables/useCopperConvertModal'
import { getBoursecuActionIconSrc } from '@/utils/config' import { getBoursecuActionIconSrc } from '@/utils/config'
import type { HomeQuickActionItem } from '../types' import type { HomeQuickActionItem } from '../types'
@@ -22,7 +21,7 @@ export const boursecuQuickActions: HomeQuickActionItem[] = [
label: 'دارایی ها', label: 'دارایی ها',
icon: 'vuesax-linear:cup', icon: 'vuesax-linear:cup',
iconSrc: getBoursecuActionIconSrc('assets'), iconSrc: getBoursecuActionIconSrc('assets'),
// شیت موجودی‌ها — نه مسیر fund (با enable_product_funds=false گارد بی‌صدا برمی‌گرداند) route: { name: 'assets' },
}, },
{ {
id: 'transactions', id: 'transactions',
@@ -43,6 +42,6 @@ export const boursecuQuickActions: HomeQuickActionItem[] = [
label: 'تبدیل', label: 'تبدیل',
icon: 'vuesax-linear:refresh-circle', icon: 'vuesax-linear:refresh-circle',
iconSrc: getBoursecuActionIconSrc('convert'), iconSrc: getBoursecuActionIconSrc('convert'),
handler: () => openCopperConvert(), route: { name: 'convert' },
}, },
] ]
@@ -43,8 +43,6 @@
<BasicInput <BasicInput
v-model:value="item.count" v-model:value="item.count"
:addon-before="quantityUnit" :addon-before="quantityUnit"
:addon-bg-color="config.config?.color"
addon-text-color="white"
type="number" type="number"
:inputmode="isCountBased ? 'numeric' : 'decimal'" :inputmode="isCountBased ? 'numeric' : 'decimal'"
@update:value="onCountInput" @update:value="onCountInput"
@@ -61,7 +59,6 @@
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import BasicButton from '@/components/BasicPacks/BasicButton.vue' import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import BasicInput from '@/components/BasicPacks/BasicInput.vue' import BasicInput from '@/components/BasicPacks/BasicInput.vue'
import { useConfigStore } from '@/store/configStore'
import type { CountPInventory } from '@/models/products' import type { CountPInventory } from '@/models/products'
import { getProductDisplayImage } from '@/utils/productAssets' import { getProductDisplayImage } from '@/utils/productAssets'
import { import {
@@ -110,8 +107,6 @@ const displayMinDelivery = computed(() => {
: formatGoldAmount(minDeliveryQuantity.value) : formatGoldAmount(minDeliveryQuantity.value)
}) })
const config = useConfigStore()
function selectCountInput(e: FocusEvent) { function selectCountInput(e: FocusEvent) {
const el = e.target as HTMLInputElement | null const el = e.target as HTMLInputElement | null
if (!el || el.tagName !== 'INPUT' || typeof el.select !== 'function') return if (!el || el.tagName !== 'INPUT' || typeof el.select !== 'function') return
@@ -243,8 +243,11 @@ export function useWalletPage() {
} }
onMounted(async () => { onMounted(async () => {
if (router.currentRoute.value.query.state === 'deposit') { const walletState = router.currentRoute.value.query.state
if (walletState === 'deposit') {
isDeposit.value = true isDeposit.value = true
} else if (walletState === 'withdraw') {
isDeposit.value = false
} }
cardForm.value.birth_date = userStore.userInfo?.birth_date cardForm.value.birth_date = userStore.userInfo?.birth_date
await initWalletPageData() await initWalletPageData()
@@ -45,8 +45,6 @@
<BasicInput <BasicInput
v-model:value="form.value" v-model:value="form.value"
addon-before="ریال" addon-before="ریال"
:addon-bg-color="useConfigStore().config.color"
addon-text-color="white"
place-holder="مبلغ" place-holder="مبلغ"
size="large" size="large"
type="price" type="price"
@@ -175,7 +173,6 @@ import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import BasicInput from '@/components/BasicPacks/BasicInput.vue' import BasicInput from '@/components/BasicPacks/BasicInput.vue'
import ListAnim from '@/components/ListAnim.vue' import ListAnim from '@/components/ListAnim.vue'
import TransactionSkeleton from '@/components/TransactionSkeleton.vue' import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
import { useConfigStore } from '@/store/configStore'
import { required } from '@/utils/inputRules' import { required } from '@/utils/inputRules'
import BoursecuBankAccountPicker from '../components/BoursecuBankAccountPicker.vue' import BoursecuBankAccountPicker from '../components/BoursecuBankAccountPicker.vue'
import BoursecuWithdrawGuide from '../components/BoursecuWithdrawGuide.vue' import BoursecuWithdrawGuide from '../components/BoursecuWithdrawGuide.vue'
@@ -50,8 +50,6 @@
<BasicInput <BasicInput
v-model:value="form.value" v-model:value="form.value"
addon-before="ریال" addon-before="ریال"
:addon-bg-color="useConfigStore().config.color"
addon-text-color="white"
place-holder="مبلغ" place-holder="مبلغ"
size="large" size="large"
type="price" type="price"
@@ -145,7 +143,6 @@ import BasicInput from '@/components/BasicPacks/BasicInput.vue'
import Bg_main from '@/components/bg_main.vue' import Bg_main from '@/components/bg_main.vue'
import ListAnim from '@/components/ListAnim.vue' import ListAnim from '@/components/ListAnim.vue'
import TransactionSkeleton from '@/components/TransactionSkeleton.vue' import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
import { useConfigStore } from '@/store/configStore'
import { required } from '@/utils/inputRules' import { required } from '@/utils/inputRules'
import img from '@/assets/img/user_ly1.svg' import img from '@/assets/img/user_ly1.svg'
import BoursecuBankAccountPicker from '../components/BoursecuBankAccountPicker.vue' import BoursecuBankAccountPicker from '../components/BoursecuBankAccountPicker.vue'