remove toPersianDigits

This commit is contained in:
2026-06-17 13:51:29 +03:30
parent b42b123957
commit 61e10dd45a
13 changed files with 65 additions and 55 deletions
@@ -0,0 +1,26 @@
# Responsive layout vs data fetching
When adding or changing pages with mobile/desktop views:
## UI layer (responsive)
- Switch layouts with **`v-show`**, not **`v-if`**, so resize does not destroy child components.
- Prefer `ResponsiveViewPair` (`src/layouts/components/ResponsiveViewPair.vue`) or the home shell pattern.
- Keep **`<router-view>`** outside responsive chrome wrappers (header/topbar only in chrome components).
- Modal vs ActionSheet may use `v-if` by breakpoint; they must not own initial page data fetching.
## Data layer (fetch once)
- Call the page composable **once** in the route `index.vue` shell (e.g. `useXPage()` / `useXPageProvider()`).
- Put `onMounted` API calls only in that shell composable, never in `mobile/*` or `desktop/*` views.
- Mobile/Desktop views use **`useXPageContext()`** (provide/inject) — read-only UI.
- Do not call APIs inside `useDesktopBreakpoint()` watchers.
## Shared caches
- Use `runOnceAsync` (`src/composables/useOnceAsync.ts`) for store-level dedup when the same endpoint may be hit from multiple mounts.
- Reuse existing module caches (`useWalletAssets`, Pinia stores) before adding new fetch logic.
## Verify
- Open Network tab, load a page, resize across 768px — **no duplicate requests** for the same endpoint.
+13 -10
View File
@@ -35,7 +35,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import {usePrefix} from "@/utils/usePrefix"; import {usePrefix} from "@/utils/usePrefix";
import {computed} from "vue"; import {computed} from "vue";
import {usePrice} from "@/components/BasicPacks/BasicPriceInput/src/utils/usePrice"; import {usePrice} from "@/utils/usePrice";
//Props //Props
interface Prop { interface Prop {
@@ -59,13 +59,14 @@ const addonTextColor = computed(() => props.addonTextColor)
//emits //emits
const localValue = computed({ const localValue = computed({
set(value) { set(value) {
// console.log(value?.toString().replaceAll(',', '')) props.type === 'price'
props.type === 'price' ? emits('update:value', value?.toString().replaceAll(',', '')) : emits('update:value', value) ? emits('update:value', value?.toString().replaceAll(',', ''))
: emits('update:value', value)
}, },
get() { get() {
let a = props.type === 'price' ? props.value ? usePrice(props.value as number, true, '').trim() : '' : props.value return props.type === 'price'
// console.log(props.value) ? props.value ? usePrice(props.value as number, true, '').trim() : ''
return a : props.value
}, },
}) })
@@ -75,10 +76,12 @@ const props = withDefaults(defineProps<Prop>(), {
const emits = defineEmits(['update:value', 'onchange']) const emits = defineEmits(['update:value', 'onchange'])
const handlerChange = (e: any) => { const handlerChange = (e: any) => {
// emits('onchange', e.target.value) props.type === 'price'
props.type === 'price' ? emits('update:value', e.target.value?.toString().replaceAll(',', '')) : emits('update:value', e.target.value) ? emits('update:value', e.target.value?.toString().replaceAll(',', ''))
props.type === 'price' ? emits('onchange', e.target.value?.toString().replaceAll(',', '')) : emits('onchange', e.target.value) : emits('update:value', e.target.value)
// console.log('sas') props.type === 'price'
? emits('onchange', e.target.value?.toString().replaceAll(',', ''))
: emits('onchange', e.target.value)
} }
//css style //css style
@@ -7,7 +7,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {usePrice} from './utils/usePrice' import {usePrice} from '@/utils/usePrice'
import {computed} from "vue"; import {computed} from "vue";
import BasicInput from "../../BasicInput.vue"; import BasicInput from "../../BasicInput.vue";
@@ -23,9 +23,7 @@ const props = withDefaults(defineProps<Props>(), {})
const _val = computed({ const _val = computed({
set(val: string) { set(val: string) {
// const temp = val.toString().replaceAll(',', '') emit('update:value', val.toString().replaceAll(',', ''))
const temp = val.toString().replaceAll(',', '')
emit('update:value', temp)
}, },
get() { get() {
return props.value ? usePrice(props.value as number, true, '').trim() : '' return props.value ? usePrice(props.value as number, true, '').trim() : ''
@@ -1,17 +1 @@
export function usePrice(price: number, digit = true, unit = 'ریال') { export { usePrice, usePriceWithoutType, useTowDecimal, useFormat } from '@/utils/usePrice'
let _price = price.toString()
if (digit) {
_price = _price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
return `${_price} ${unit}`
}
export function useTowDecimal(num: string | number): string | number {
const temp = num.toString().match(/^-?\d+(?:\.\d{0,2})?/)
if (temp && temp.length > 0) return temp[0]
return Math.round(+num * 100) / 100
}
export function useFormat(num: string | number, unit = '') {
return usePrice(Math.abs(+useTowDecimal(num)), true, unit)
}
+1 -1
View File
@@ -5,4 +5,4 @@ export function useCardNumber(val: number) {
export function useShabaNumber(val: number) { export function useShabaNumber(val: number) {
const str = val.toString() const str = val.toString()
return `IR${str.substring(0,4)}\xa0\xa0\xa0${str.substring(4,8)}\xa0\xa0\xa0${str.substring(8,12)}\xa0\xa0\xa0${str.substring(12,16)}\xa0\xa0\xa0${str.substring(16,20)}\xa0\xa0\xa0${str.substring(20,24)}` return `IR${str.substring(0,4)}\xa0\xa0\xa0${str.substring(4,8)}\xa0\xa0\xa0${str.substring(8,12)}\xa0\xa0\xa0${str.substring(12,16)}\xa0\xa0\xa0${str.substring(16,20)}\xa0\xa0\xa0${str.substring(20,24)}`
} }
+6 -4
View File
@@ -1,11 +1,13 @@
const PERSIAN_DIGITS = '۰۱۲۳۴۵۶۷۸۹'
function formatGroupedPrice(price: string | number | null | undefined, digit: boolean) { function formatGroupedPrice(price: string | number | null | undefined, digit: boolean) {
let value = price === null || price === undefined ? '' : String(price) let value = price === null || price === undefined ? '' : String(price)
if (digit) { if (digit) {
value = value.replace(/\B(?=(\d{3})+(?!\d))/g, ',') value = value.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
} }
return value.replace(/\d/g, (d) => PERSIAN_DIGITS[Number(d)] ?? d) return value
}
export function formatNumber(value: string | number | null | undefined, grouped = false): string {
return formatGroupedPrice(value, grouped)
} }
export function usePrice(price: number, digit = true, unit = 'ریال') { export function usePrice(price: number, digit = true, unit = 'ریال') {
@@ -87,6 +89,6 @@ export function getTransactionGoldAmount(item: TransactionPriceSource): number {
export function formatGoldAmount(amount?: number | null): string { export function formatGoldAmount(amount?: number | null): string {
const num = Number(amount) const num = Number(amount)
if (!Number.isFinite(num)) return '۰' if (!Number.isFinite(num)) return '0'
return formatGroupedPrice(Number(num.toFixed(3)), false) return formatGroupedPrice(Number(num.toFixed(3)), false)
} }
@@ -11,7 +11,7 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center"> <div class="flex items-center">
<div>مبلغ پاداش :</div> <div>مبلغ پاداش :</div>
<div class="mr-4">{{item.gold}}</div> <div class="mr-4">{{ formatGoldAmount(item.gold) }}</div>
</div> </div>
<!-- <div class="mt-2 rounded-[8px] p-2 text-xs" :class="item.is_legal === 0? 'bg-red-100 text-red-600':'bg-green-100 test-green-600'">{{item.is_legal === 0? 'احراز هویت نشده':'احراز هویت شده'}}</div>--> <!-- <div class="mt-2 rounded-[8px] p-2 text-xs" :class="item.is_legal === 0? 'bg-red-100 text-red-600':'bg-green-100 test-green-600'">{{item.is_legal === 0? 'احراز هویت نشده':'احراز هویت شده'}}</div>-->
<div class="flex text-[#9CA3AF]"> <div class="flex text-[#9CA3AF]">
@@ -26,6 +26,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { formatGoldAmount } from '@/utils/usePrice'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue' import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import { toJalali, toJalaliNotime } from '../../../../utils/useDateTime' import { toJalali, toJalaliNotime } from '../../../../utils/useDateTime'
import type { Gifts } from '@/models/gifts' import type { Gifts } from '@/models/gifts'
+4 -3
View File
@@ -17,7 +17,7 @@
</div> </div>
<div class="text-gray-500 flex gap-1 text-xs items-center"> <div class="text-gray-500 flex gap-1 text-xs items-center">
<div :class="item.gold > 0 ? 'text-green-600':'text-red-600'" class="text-base number-ltr"> <div :class="item.gold > 0 ? 'text-green-600':'text-red-600'" class="text-base number-ltr">
{{ item.gold }} {{ formatGoldAmount(item.gold) }}
</div> </div>
{{item.unit}} {{item.unit}}
</div> </div>
@@ -39,14 +39,14 @@
<div class="flex justify-between mb-3"> <div class="flex justify-between mb-3">
<div class="text-primary">مقدار {{props.title}} مبادله شده:</div> <div class="text-primary">مقدار {{props.title}} مبادله شده:</div>
<div :class="item.gold >= 0? 'text-green-600' : 'text-red-600'" class="flex text-xs items-center"> <div :class="item.gold >= 0? 'text-green-600' : 'text-red-600'" class="flex text-xs items-center">
<div class="text-base ml-1 number-ltr">{{ item.gold }}</div> <div class="text-base ml-1 number-ltr">{{ formatGoldAmount(item.gold) }}</div>
{{item.unit}} {{item.unit}}
</div> </div>
</div> </div>
<div class="flex justify-between mb-3"> <div class="flex justify-between mb-3">
<div class="text-primary">موجودی {{ props.title }}:</div> <div class="text-primary">موجودی {{ props.title }}:</div>
<div class="flex text-xs text-yellow-700 items-center"> <div class="flex text-xs text-yellow-700 items-center">
<div class="text-base ml-1">{{ item.balance_gold }}</div> <div class="text-base ml-1">{{ formatGoldAmount(item.balance_gold) }}</div>
{{item.unit}} {{item.unit}}
</div> </div>
</div> </div>
@@ -88,6 +88,7 @@ import BasicActionSheet from "@/components/BasicPacks/BasicActionSheet.vue";
import {computed, ref} from "vue"; import {computed, ref} from "vue";
import BasicButton from "@/components/BasicPacks/BasicButton.vue"; import BasicButton from "@/components/BasicPacks/BasicButton.vue";
import type {Transaction} from "@/models/transaction"; import type {Transaction} from "@/models/transaction";
import {formatGoldAmount, usePriceWithoutType} from "../../../../../utils/usePrice";
import {toJalaliNotime, toTime} from "../../../../../utils/useDateTime"; import {toJalaliNotime, toTime} from "../../../../../utils/useDateTime";
const infoVisible = ref<boolean>(false) const infoVisible = ref<boolean>(false)
@@ -19,7 +19,7 @@
<Vue3Odometer <Vue3Odometer
:value="displayGoldVal" :value="displayGoldVal"
class="o-text !text-xl font-bold" class="o-text !text-xl font-bold"
style="font-family: iran-sans; direction: ltr" style="direction: ltr"
/> />
ریال ریال
</div> </div>
@@ -113,10 +113,6 @@ onMounted(async () => {
chartMounted.value = true chartMounted.value = true
}) })
const toPersianNumber = (num: number) => {
return num.toString().replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[d])
}
const chartOptions = computed(() => { const chartOptions = computed(() => {
const fontFamily = getProjectFontFamily() const fontFamily = getProjectFontFamily()
@@ -144,7 +140,7 @@ const chartOptions = computed(() => {
tickAmount: 8, tickAmount: 8,
show: false, show: false,
labels: { labels: {
formatter: (val: number) => toPersianNumber(usePrice(val, true, 'ه.ریال')), formatter: (val: number) => usePrice(val, true, 'ه.ریال'),
style: { fontSize: '13px', fontFamily }, style: { fontSize: '13px', fontFamily },
}, },
}, },
@@ -165,7 +161,7 @@ const chartOptions = computed(() => {
})}` })}`
}, },
}, },
formatter: (val: number) => toPersianNumber(usePrice(val, true, 'ه.ریال')), formatter: (val: number) => usePrice(val, true, 'ه.ریال'),
style: { fontFamily }, style: { fontFamily },
}, },
colors: [config.config.color], colors: [config.config.color],
@@ -17,7 +17,6 @@
<Vue3Odometer <Vue3Odometer
:value="displayGoldVal" :value="displayGoldVal"
class="dhome-hero-price__odometer o-text" class="dhome-hero-price__odometer o-text"
style="font-family: iran-sans"
/> />
<span class="dhome-hero-price__unit">ریال</span> <span class="dhome-hero-price__unit">ریال</span>
</div> </div>
@@ -34,7 +34,7 @@
]" ]"
> >
<div class="flex items-center"> <div class="flex items-center">
<div class="text-base ml-1">{{ item.gold }}</div> <div class="text-base ml-1">{{ formatGoldAmount(item.gold) }}</div>
{{ item.unit }} {{ item.unit }}
</div> </div>
<div <div
@@ -66,7 +66,7 @@
</div> </div>
<div class="flex flex-col items-end text-xs text-yellow-700"> <div class="flex flex-col items-end text-xs text-yellow-700">
<div class="flex items-center"> <div class="flex items-center">
<div class="text-base ml-1">{{ item.balance_gold }}</div> <div class="text-base ml-1">{{ formatGoldAmount(item.balance_gold) }}</div>
{{ getProductQuantityUnit(item.wallet.product) }} {{ getProductQuantityUnit(item.wallet.product) }}
</div> </div>
<div v-if="getProductUnitWeightLabel(item.wallet.product)" class="text-gray-500"> <div v-if="getProductUnitWeightLabel(item.wallet.product)" class="text-gray-500">
@@ -79,7 +79,7 @@
{{ getGoldWalletLabel(item.wallet?.product?.group_type) }}: {{ getGoldWalletLabel(item.wallet?.product?.group_type) }}:
</div> </div>
<div class="flex text-xs text-yellow-700 items-center"> <div class="flex text-xs text-yellow-700 items-center">
<div class="text-base ml-1">{{ item.balance_gold }}</div> <div class="text-base ml-1">{{ formatGoldAmount(item.balance_gold) }}</div>
{{ getWeightUnit() }} {{ getWeightUnit() }}
</div> </div>
</div> </div>
@@ -107,7 +107,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { usePriceWithoutType } from '@/utils/usePrice' import { formatGoldAmount, usePriceWithoutType } from '@/utils/usePrice'
import type { Transaction } from '@/models/transaction' import type { Transaction } from '@/models/transaction'
import { import {
getCashWalletLabel, getCashWalletLabel,
+3 -3
View File
@@ -67,7 +67,7 @@
<div class="flex justify-between mb-3"> <div class="flex justify-between mb-3">
<div class="text-black">مقدار {{ getMetalTypeTradeLabel() }} مبادله شده:</div> <div class="text-black">مقدار {{ getMetalTypeTradeLabel() }} مبادله شده:</div>
<div :class="item.gold >= 0? 'text-green-600' : 'text-red-600'" class="flex text-xs items-center"> <div :class="item.gold >= 0? 'text-green-600' : 'text-red-600'" class="flex text-xs items-center">
<div class="text-base ml-1 number-ltr">{{ item.gold }}</div> <div class="text-base ml-1 number-ltr">{{ formatGoldAmount(item.gold) }}</div>
گرم گرم
</div> </div>
</div> </div>
@@ -88,7 +88,7 @@
<div class="flex justify-between mb-3"> <div class="flex justify-between mb-3">
<div class="text-black">{{ getGoldWalletLabel() }}:</div> <div class="text-black">{{ getGoldWalletLabel() }}:</div>
<div class="flex text-xs text-yellow-700 items-center"> <div class="flex text-xs text-yellow-700 items-center">
<div class="text-base ml-1">{{ item.balance_gold }}</div> <div class="text-base ml-1">{{ formatGoldAmount(item.balance_gold) }}</div>
{{ getWeightUnit() }} {{ getWeightUnit() }}
</div> </div>
</div> </div>
@@ -138,7 +138,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import Icon from "@/components/BasicPacks/BasicIcon.vue"; import Icon from "@/components/BasicPacks/BasicIcon.vue";
import {getTransactionTradePrice, usePriceWithoutType} from "../../../../utils/usePrice"; import {formatGoldAmount, getTransactionTradePrice, usePriceWithoutType} from "../../../../utils/usePrice";
import BasicActionSheet from "@/components/BasicPacks/BasicActionSheet.vue"; import BasicActionSheet from "@/components/BasicPacks/BasicActionSheet.vue";
import {computed, ref} from "vue"; import {computed, ref} from "vue";
import BasicButton from "@/components/BasicPacks/BasicButton.vue"; import BasicButton from "@/components/BasicPacks/BasicButton.vue";