Merge remote-tracking branch 'origin/modular' into modular
This commit is contained in:
@@ -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.
|
||||
@@ -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'
|
||||
|
||||
@@ -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)}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user