Merge remote-tracking branch 'origin/modular' into modular

This commit is contained in:
2026-06-17 16:03:34 +03:30
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>
import {usePrefix} from "@/utils/usePrefix";
import {computed} from "vue";
import {usePrice} from "@/components/BasicPacks/BasicPriceInput/src/utils/usePrice";
import {usePrice} from "@/utils/usePrice";
//Props
interface Prop {
@@ -59,13 +59,14 @@ const addonTextColor = computed(() => props.addonTextColor)
//emits
const localValue = computed({
set(value) {
// console.log(value?.toString().replaceAll(',', ''))
props.type === 'price' ? emits('update:value', value?.toString().replaceAll(',', '')) : emits('update:value', value)
props.type === 'price'
? emits('update:value', value?.toString().replaceAll(',', ''))
: emits('update:value', value)
},
get() {
let a = props.type === 'price' ? props.value ? usePrice(props.value as number, true, '').trim() : '' : props.value
// console.log(props.value)
return a
return props.type === 'price'
? props.value ? usePrice(props.value as number, true, '').trim() : ''
: props.value
},
})
@@ -75,10 +76,12 @@ const props = withDefaults(defineProps<Prop>(), {
const emits = defineEmits(['update:value', 'onchange'])
const handlerChange = (e: any) => {
// emits('onchange', e.target.value)
props.type === 'price' ? emits('update:value', e.target.value?.toString().replaceAll(',', '')) : emits('update:value', e.target.value)
props.type === 'price' ? emits('onchange', e.target.value?.toString().replaceAll(',', '')) : emits('onchange', e.target.value)
// console.log('sas')
props.type === 'price'
? emits('update:value', e.target.value?.toString().replaceAll(',', ''))
: emits('update:value', e.target.value)
props.type === 'price'
? emits('onchange', e.target.value?.toString().replaceAll(',', ''))
: emits('onchange', e.target.value)
}
//css style
@@ -7,7 +7,7 @@
</template>
<script lang="ts" setup>
import {usePrice} from './utils/usePrice'
import {usePrice} from '@/utils/usePrice'
import {computed} from "vue";
import BasicInput from "../../BasicInput.vue";
@@ -23,9 +23,7 @@ const props = withDefaults(defineProps<Props>(), {})
const _val = computed({
set(val: string) {
// const temp = val.toString().replaceAll(',', '')
const temp = val.toString().replaceAll(',', '')
emit('update:value', temp)
emit('update:value', val.toString().replaceAll(',', ''))
},
get() {
return props.value ? usePrice(props.value as number, true, '').trim() : ''
@@ -1,17 +1 @@
export function usePrice(price: number, digit = true, unit = 'ریال') {
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)
}
export { usePrice, usePriceWithoutType, useTowDecimal, useFormat } from '@/utils/usePrice'
+1 -1
View File
@@ -5,4 +5,4 @@ export function useCardNumber(val: number) {
export function useShabaNumber(val: number) {
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)}`
}
}
+6 -4
View File
@@ -1,11 +1,13 @@
const PERSIAN_DIGITS = '۰۱۲۳۴۵۶۷۸۹'
function formatGroupedPrice(price: string | number | null | undefined, digit: boolean) {
let value = price === null || price === undefined ? '' : String(price)
if (digit) {
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 = 'ریال') {
@@ -87,6 +89,6 @@ export function getTransactionGoldAmount(item: TransactionPriceSource): number {
export function formatGoldAmount(amount?: number | null): string {
const num = Number(amount)
if (!Number.isFinite(num)) return '۰'
if (!Number.isFinite(num)) return '0'
return formatGroupedPrice(Number(num.toFixed(3)), false)
}
@@ -11,7 +11,7 @@
<div class="flex items-center justify-between">
<div class="flex items-center">
<div>مبلغ پاداش :</div>
<div class="mr-4">{{item.gold}}</div>
<div class="mr-4">{{ formatGoldAmount(item.gold) }}</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="flex text-[#9CA3AF]">
@@ -26,6 +26,7 @@
<script setup lang="ts">
import { formatGoldAmount } from '@/utils/usePrice'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import { toJalali, toJalaliNotime } from '../../../../utils/useDateTime'
import type { Gifts } from '@/models/gifts'
+4 -3
View File
@@ -17,7 +17,7 @@
</div>
<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">
{{ item.gold }}
{{ formatGoldAmount(item.gold) }}
</div>
{{item.unit}}
</div>
@@ -39,14 +39,14 @@
<div class="flex justify-between mb-3">
<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="text-base ml-1 number-ltr">{{ item.gold }}</div>
<div class="text-base ml-1 number-ltr">{{ formatGoldAmount(item.gold) }}</div>
{{item.unit}}
</div>
</div>
<div class="flex justify-between mb-3">
<div class="text-primary">موجودی {{ props.title }}:</div>
<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}}
</div>
</div>
@@ -88,6 +88,7 @@ import BasicActionSheet from "@/components/BasicPacks/BasicActionSheet.vue";
import {computed, ref} from "vue";
import BasicButton from "@/components/BasicPacks/BasicButton.vue";
import type {Transaction} from "@/models/transaction";
import {formatGoldAmount, usePriceWithoutType} from "../../../../../utils/usePrice";
import {toJalaliNotime, toTime} from "../../../../../utils/useDateTime";
const infoVisible = ref<boolean>(false)
@@ -19,7 +19,7 @@
<Vue3Odometer
:value="displayGoldVal"
class="o-text !text-xl font-bold"
style="font-family: iran-sans; direction: ltr"
style="direction: ltr"
/>
ریال
</div>
@@ -113,10 +113,6 @@ onMounted(async () => {
chartMounted.value = true
})
const toPersianNumber = (num: number) => {
return num.toString().replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[d])
}
const chartOptions = computed(() => {
const fontFamily = getProjectFontFamily()
@@ -144,7 +140,7 @@ const chartOptions = computed(() => {
tickAmount: 8,
show: false,
labels: {
formatter: (val: number) => toPersianNumber(usePrice(val, true, 'ه.ریال')),
formatter: (val: number) => usePrice(val, true, 'ه.ریال'),
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 },
},
colors: [config.config.color],
@@ -17,7 +17,6 @@
<Vue3Odometer
:value="displayGoldVal"
class="dhome-hero-price__odometer o-text"
style="font-family: iran-sans"
/>
<span class="dhome-hero-price__unit">ریال</span>
</div>
@@ -34,7 +34,7 @@
]"
>
<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 }}
</div>
<div
@@ -66,7 +66,7 @@
</div>
<div class="flex flex-col items-end text-xs text-yellow-700">
<div class="flex items-center">
<div class="text-base ml-1">{{ item.balance_gold }}</div>
<div class="text-base ml-1">{{ formatGoldAmount(item.balance_gold) }}</div>
{{ getProductQuantityUnit(item.wallet.product) }}
</div>
<div v-if="getProductUnitWeightLabel(item.wallet.product)" class="text-gray-500">
@@ -79,7 +79,7 @@
{{ getGoldWalletLabel(item.wallet?.product?.group_type) }}:
</div>
<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() }}
</div>
</div>
@@ -107,7 +107,7 @@
</template>
<script lang="ts" setup>
import { usePriceWithoutType } from '@/utils/usePrice'
import { formatGoldAmount, usePriceWithoutType } from '@/utils/usePrice'
import type { Transaction } from '@/models/transaction'
import {
getCashWalletLabel,
+3 -3
View File
@@ -67,7 +67,7 @@
<div class="flex justify-between mb-3">
<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="text-base ml-1 number-ltr">{{ item.gold }}</div>
<div class="text-base ml-1 number-ltr">{{ formatGoldAmount(item.gold) }}</div>
گرم
</div>
</div>
@@ -88,7 +88,7 @@
<div class="flex justify-between mb-3">
<div class="text-black">{{ getGoldWalletLabel() }}:</div>
<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() }}
</div>
</div>
@@ -138,7 +138,7 @@
<script lang="ts" setup>
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 {computed, ref} from "vue";
import BasicButton from "@/components/BasicPacks/BasicButton.vue";