feat(convert): read quantity from the asset so the leftover can be split across 1.5 and 2.5 wire.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -50,3 +50,4 @@
|
||||
- 2026-10-21 | نمودار خانه = کارت لندینگ | `home/components/LandingLiveChartMain.vue` + `useLandingLiveChart.ts` + `styles/landing-live-chart-main.less` | موبایل PriceChartSection و دسکتاپ DesktopChartSection/Panel همین کارت؛ استایل از main.less
|
||||
- 2026-10-21 | چیدمان کارت نمودار خانه | همان دو فایل vue/less | هدر: نام+وضعیت راست، تب محصول چپ؛ قیمت راست و تغییر چپ؛ تب بازه کنار عنوان نمودار
|
||||
- 2026-10-21 | تب نمودار = محصول فعال | `useLandingLiveChart.ts` + `LandingLiveChartMain.vue` | تب از کاتالوگ `active && show`؛ داده با `price_offset_percent` همان محصول
|
||||
- 2026-10-31 | مقدار تبدیل کاتد/مفتول/شمش | `convert/components/CopperConvertCalculator.vue` + `useCopperConvertPage.ts` | زبانه مقدار تا موجودی؛ مانده با زبانه ۱.۵ (تا کل مانده) و ۲.۵ (تا باقیمانده بعد از ۱.۵)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="copper-convert" dir="rtl">
|
||||
<p class="copper-convert__lead">
|
||||
وزن دارایی را وارد کنید؛ معادل مفتول و تعداد حلقه سیم ۱.۵ و ۲.۵ با قیمت روز محاسبه میشود.
|
||||
مقدار کاتد، مفتول یا شمش از دارایی شما خوانده میشود. با زبانه از صفر تا همان موجودی انتخاب کنید. معادل مفتول و حلقه سیم ۱.۵ و ۲.۵ با قیمت روز حساب میشود.
|
||||
</p>
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
@@ -59,18 +59,33 @@
|
||||
</div>
|
||||
|
||||
<div class="copper-convert__weight">
|
||||
<label class="copper-convert__field-label" for="copper-convert-kg">مقدار</label>
|
||||
<label class="copper-convert__field-label">مقدار</label>
|
||||
<div class="copper-convert__weight-wrap">
|
||||
<input
|
||||
id="copper-convert-kg"
|
||||
v-model="inputKg"
|
||||
class="copper-convert__weight-input"
|
||||
type="text"
|
||||
inputmode="decimal"
|
||||
placeholder="مثلاً ۱۰"
|
||||
autocomplete="off"
|
||||
<span class="copper-convert__weight-value" dir="ltr">{{ formatAssetAmount(sliderAmount) }}</span>
|
||||
<span class="copper-convert__weight-unit">{{ assetUnit }}</span>
|
||||
</div>
|
||||
<p class="copper-convert__asset-hint">
|
||||
<template v-if="assetAmount > 0">از دارایی شما؛ انتهای زبانه {{ formatAssetAmount(assetAmount) }} {{ assetUnit }} است</template>
|
||||
<template v-else>موجودی این دارایی صفر است.</template>
|
||||
</p>
|
||||
<div
|
||||
class="app-touch-slider copper-convert__slider"
|
||||
@touchstart.stop
|
||||
@touchmove.stop
|
||||
@pointerdown.stop
|
||||
>
|
||||
<a-slider
|
||||
v-model:value="sliderAmount"
|
||||
:min="0"
|
||||
:max="assetAmount > 0 ? assetAmount : 1"
|
||||
:step="sliderStep"
|
||||
:disabled="assetAmount <= 0"
|
||||
:tip-formatter="sliderTip"
|
||||
/>
|
||||
<span class="copper-convert__weight-unit">کیلوگرم</span>
|
||||
</div>
|
||||
<div class="copper-convert__slider-scale">
|
||||
<span>۰</span>
|
||||
<span dir="ltr">{{ formatAssetAmount(assetAmount) }} {{ assetUnit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -86,25 +101,11 @@
|
||||
<span class="copper-convert__ring-badge">۱.۵ mm²</span>
|
||||
<p class="copper-convert__ring-count" dir="ltr">{{ formatRings(result.rings15ByValue) }}</p>
|
||||
<p class="copper-convert__ring-caption">حلقه سیم</p>
|
||||
<p
|
||||
v-if="result.rings15ByWeight != null"
|
||||
class="copper-convert__ring-sub"
|
||||
dir="ltr"
|
||||
>
|
||||
≈ {{ formatRings(result.rings15ByWeight) }} بر اساس وزن حلقه
|
||||
</p>
|
||||
</article>
|
||||
<article class="copper-convert__ring-card">
|
||||
<span class="copper-convert__ring-badge">۲.۵ mm²</span>
|
||||
<p class="copper-convert__ring-count" dir="ltr">{{ formatRings(result.rings25ByValue) }}</p>
|
||||
<p class="copper-convert__ring-caption">حلقه سیم</p>
|
||||
<p
|
||||
v-if="result.rings25ByWeight != null"
|
||||
class="copper-convert__ring-sub"
|
||||
dir="ltr"
|
||||
>
|
||||
≈ {{ formatRings(result.rings25ByWeight) }} بر اساس وزن حلقه
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
@@ -120,7 +121,72 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p v-else-if="inputKg && !loading" class="copper-convert__warn">
|
||||
<section v-if="result && remainderAmount > 0" class="copper-convert__split">
|
||||
<div class="copper-convert__result-head">
|
||||
<span class="copper-convert__result-kicker">مانده دارایی</span>
|
||||
<span class="copper-convert__result-title">تقسیم بین دو سیم</span>
|
||||
</div>
|
||||
<p class="copper-convert__asset-hint">
|
||||
مانده {{ formatAssetAmount(remainderAmount) }} {{ assetUnit }} است. زبانهٔ ۱.۵ تا کل مانده میرود؛ زبانهٔ ۲.۵ فقط تا آنچه بعد از ۱.۵ میماند.
|
||||
</p>
|
||||
|
||||
<div class="copper-convert__weight">
|
||||
<label class="copper-convert__field-label">سیم ۱.۵ mm²</label>
|
||||
<div class="copper-convert__weight-wrap">
|
||||
<span class="copper-convert__weight-value" dir="ltr">{{ formatAssetAmount(wire15Amount) }}</span>
|
||||
<span class="copper-convert__weight-unit">{{ assetUnit }}</span>
|
||||
</div>
|
||||
<p class="copper-convert__asset-hint">معادل {{ formatRings(wire15Rings) }} حلقه</p>
|
||||
<div
|
||||
class="app-touch-slider copper-convert__slider"
|
||||
@touchstart.stop
|
||||
@touchmove.stop
|
||||
@pointerdown.stop
|
||||
>
|
||||
<a-slider
|
||||
v-model:value="wire15Amount"
|
||||
:min="0"
|
||||
:max="remainderAmount"
|
||||
:step="sliderStep"
|
||||
:tip-formatter="sliderTip"
|
||||
/>
|
||||
</div>
|
||||
<div class="copper-convert__slider-scale">
|
||||
<span>۰</span>
|
||||
<span dir="ltr">{{ formatAssetAmount(remainderAmount) }} {{ assetUnit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="copper-convert__weight">
|
||||
<label class="copper-convert__field-label">سیم ۲.۵ mm²</label>
|
||||
<div class="copper-convert__weight-wrap">
|
||||
<span class="copper-convert__weight-value" dir="ltr">{{ formatAssetAmount(wire25Amount) }}</span>
|
||||
<span class="copper-convert__weight-unit">{{ assetUnit }}</span>
|
||||
</div>
|
||||
<p class="copper-convert__asset-hint">معادل {{ formatRings(wire25Rings) }} حلقه</p>
|
||||
<div
|
||||
class="app-touch-slider copper-convert__slider"
|
||||
@touchstart.stop
|
||||
@touchmove.stop
|
||||
@pointerdown.stop
|
||||
>
|
||||
<a-slider
|
||||
v-model:value="wire25Amount"
|
||||
:min="0"
|
||||
:max="wire25Max > 0 ? wire25Max : 1"
|
||||
:step="sliderStep"
|
||||
:disabled="wire25Max <= 0"
|
||||
:tip-formatter="sliderTip"
|
||||
/>
|
||||
</div>
|
||||
<div class="copper-convert__slider-scale">
|
||||
<span>۰</span>
|
||||
<span dir="ltr">{{ formatAssetAmount(wire25Max) }} {{ assetUnit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p v-if="!result && amountAttempted && !loading" class="copper-convert__warn">
|
||||
مقدار معتبر وارد کنید و از فعال بودن قیمت کاتد/مفتول در سامانه مطمئن شوید.
|
||||
</p>
|
||||
</a-spin>
|
||||
@@ -132,7 +198,24 @@ import { computed } from 'vue'
|
||||
import { useCopperConvertPageContext } from '../composables/useCopperConvertPage'
|
||||
import type { CopperConvertSource } from '@/utils/copperWireConvert'
|
||||
|
||||
const { loading, source, inputKg, result, priceRows } = useCopperConvertPageContext()
|
||||
const {
|
||||
loading,
|
||||
source,
|
||||
assetAmount,
|
||||
assetUnit,
|
||||
sliderAmount,
|
||||
sliderStep,
|
||||
remainderAmount,
|
||||
wire15Amount,
|
||||
wire25Amount,
|
||||
wire25Max,
|
||||
wire15Rings,
|
||||
wire25Rings,
|
||||
result,
|
||||
priceRows,
|
||||
} = useCopperConvertPageContext()
|
||||
|
||||
const amountAttempted = computed(() => sliderAmount.value > 0)
|
||||
|
||||
const sourceOptions = computed(() => [
|
||||
{ label: 'کاتد', value: 'katod' as CopperConvertSource },
|
||||
@@ -143,6 +226,17 @@ const sourceOptions = computed(() => [
|
||||
const metalPriceRows = computed(() => priceRows.value.slice(0, 3))
|
||||
const wirePriceRows = computed(() => priceRows.value.slice(3))
|
||||
|
||||
function formatAssetAmount(value: number) {
|
||||
if (!Number.isFinite(value)) return '0'
|
||||
const rounded = Math.round(value * 1000) / 1000
|
||||
if (Number.isInteger(rounded)) return rounded.toLocaleString('en-US')
|
||||
return rounded.toLocaleString('en-US', { maximumFractionDigits: 3 })
|
||||
}
|
||||
|
||||
function sliderTip(value?: number) {
|
||||
return `${formatAssetAmount(Number(value ?? 0))} ${assetUnit.value}`
|
||||
}
|
||||
|
||||
function formatRial(value: number) {
|
||||
return Math.round(value).toLocaleString('en-US')
|
||||
}
|
||||
@@ -178,7 +272,8 @@ function formatRings(value: number) {
|
||||
|
||||
.copper-convert__panel,
|
||||
.copper-convert__form,
|
||||
.copper-convert__result {
|
||||
.copper-convert__result,
|
||||
.copper-convert__split {
|
||||
padding: 0.875rem;
|
||||
border-radius: 0.875rem;
|
||||
background: #fff;
|
||||
@@ -273,7 +368,8 @@ function formatRings(value: number) {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.copper-convert__form {
|
||||
.copper-convert__form,
|
||||
.copper-convert__split {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.65rem;
|
||||
@@ -326,23 +422,36 @@ function formatRings(value: number) {
|
||||
box-shadow: inset 0 1px 2px rgba(15, 39, 68, 0.04);
|
||||
}
|
||||
|
||||
.copper-convert__weight-input {
|
||||
.copper-convert__weight-value {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
text-align: right;
|
||||
direction: ltr;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.copper-convert__weight-input::placeholder {
|
||||
color: #94a3b8;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
.copper-convert__asset-hint {
|
||||
margin: 0.35rem 0 0;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.5;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.copper-convert__slider {
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.copper-convert__slider-scale {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin-top: -0.15rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.copper-convert__weight-unit {
|
||||
|
||||
@@ -5,12 +5,15 @@ import {
|
||||
onUnmounted,
|
||||
provide,
|
||||
ref,
|
||||
watch,
|
||||
type ComputedRef,
|
||||
type InjectionKey,
|
||||
type Ref,
|
||||
} from 'vue'
|
||||
import { getProducts } from '@/api/products'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import type { Product } from '@/models/products'
|
||||
import type { WalletInventoryCard } from '@/models/walletAssets'
|
||||
import {
|
||||
COPPER_CONVERT_WIRE_FALLBACK,
|
||||
calcCopperWireConvert,
|
||||
@@ -19,9 +22,10 @@ import {
|
||||
type CopperConvertSource,
|
||||
} from '@/utils/copperWireConvert'
|
||||
import { LEGACY_GROUP_KATOD, LEGACY_GROUP_MAFTOL, normalizeProductGroupType } from '@/utils/productCategories'
|
||||
import { getProductWeightUnit } from '@/utils/productDealMethod'
|
||||
import { getProductBuyPrice } from '@/utils/productPrice'
|
||||
import { getLastPrices, startLastPricePolling, stopLastPricePolling } from '@/utils/price'
|
||||
import { persianToEnglishDigits } from '@/utils/persianDigits'
|
||||
import { weightToGrams } from '@/utils/weightUnit'
|
||||
|
||||
export const COPPER_CONVERT_PAGE_KEY: InjectionKey<CopperConvertPageContext> =
|
||||
Symbol('copperConvertPageContext')
|
||||
@@ -29,18 +33,45 @@ export const COPPER_CONVERT_PAGE_KEY: InjectionKey<CopperConvertPageContext> =
|
||||
export interface CopperConvertPageContext {
|
||||
loading: Ref<boolean>
|
||||
source: Ref<CopperConvertSource>
|
||||
inputKg: Ref<string>
|
||||
assetAmount: ComputedRef<number>
|
||||
assetUnit: ComputedRef<string>
|
||||
sliderAmount: Ref<number>
|
||||
sliderStep: ComputedRef<number>
|
||||
remainderAmount: ComputedRef<number>
|
||||
wire15Amount: Ref<number>
|
||||
wire25Amount: Ref<number>
|
||||
wire25Max: ComputedRef<number>
|
||||
wire15Rings: ComputedRef<number>
|
||||
wire25Rings: ComputedRef<number>
|
||||
prices: ComputedRef<CopperConvertPrices>
|
||||
result: ComputedRef<CopperConvertResult | null>
|
||||
priceRows: ComputedRef<{ label: string; value: string; missing?: boolean }[]>
|
||||
}
|
||||
|
||||
function parseKg(raw: string): number {
|
||||
const s = persianToEnglishDigits(String(raw ?? ''))
|
||||
.replace(/,/g, '')
|
||||
.replace(/[^\d.]/g, '')
|
||||
const n = Number.parseFloat(s)
|
||||
return Number.isFinite(n) ? n : 0
|
||||
function roundAmount(value: number): number {
|
||||
return Math.round(value * 1000) / 1000
|
||||
}
|
||||
|
||||
function cardMatchesSource(card: WalletInventoryCard, src: CopperConvertSource) {
|
||||
const group = normalizeProductGroupType(card.group_type)
|
||||
const title = card.title ?? ''
|
||||
if (src === 'katod') return group === LEGACY_GROUP_KATOD || title.includes('کاتد')
|
||||
if (src === 'maftoul') return group === LEGACY_GROUP_MAFTOL || title.includes('مفتول')
|
||||
return group.includes('شمش') || title.includes('شمش')
|
||||
}
|
||||
|
||||
function cardQuantity(card: WalletInventoryCard): number {
|
||||
const qty = Number(card.quantity ?? 0)
|
||||
return Number.isFinite(qty) && qty > 0 ? qty : 0
|
||||
}
|
||||
|
||||
function quantityToKg(quantity: number, unit: string, product?: Product): number {
|
||||
if (quantity <= 0) return 0
|
||||
if (unit.includes('کیلو')) return quantity
|
||||
if (unit.includes('گرم')) return quantity / 1000
|
||||
const perPiece = Number(product?.amount ?? 0)
|
||||
if (perPiece > 0) return weightToGrams(quantity * perPiece, getProductWeightUnit(product)) / 1000
|
||||
return quantity
|
||||
}
|
||||
|
||||
function titleIncludes(product: Product, part: string) {
|
||||
@@ -90,8 +121,71 @@ function resolveWireRingPrice(product: Product | undefined, fallback: number): n
|
||||
export function useCopperConvertPage() {
|
||||
const loading = ref(true)
|
||||
const source = ref<CopperConvertSource>('katod')
|
||||
const inputKg = ref('')
|
||||
const sliderAmount = ref(0)
|
||||
const wire15Amount = ref(0)
|
||||
const wire25Amount = ref(0)
|
||||
const catalog = ref<Product[]>([])
|
||||
const { assetsSummary, loadWalletAssets } = useWalletAssets()
|
||||
|
||||
const assetCards = computed(() => {
|
||||
const summary = assetsSummary.value
|
||||
if (!summary) return [] as WalletInventoryCard[]
|
||||
const src = source.value
|
||||
const fromInventory = (summary.inventory_cards ?? []).filter((card) =>
|
||||
cardMatchesSource(card, src),
|
||||
)
|
||||
return fromInventory.length
|
||||
? fromInventory
|
||||
: (summary.count_products ?? []).filter((card) => cardMatchesSource(card, src))
|
||||
})
|
||||
|
||||
const assetUnit = computed(() => {
|
||||
const units = [...new Set(assetCards.value.map((card) => (card.unit ?? '').trim()).filter(Boolean))]
|
||||
if (units.length === 1) return units[0]
|
||||
return 'کیلوگرم'
|
||||
})
|
||||
|
||||
const assetAmount = computed(() => {
|
||||
const cards = assetCards.value
|
||||
if (!cards.length) return 0
|
||||
const units = [...new Set(cards.map((card) => (card.unit ?? '').trim()).filter(Boolean))]
|
||||
if (units.length > 1) {
|
||||
return roundAmount(
|
||||
cards.reduce((sum, card) => sum + quantityToKg(cardQuantity(card), card.unit ?? '', undefined), 0),
|
||||
)
|
||||
}
|
||||
return roundAmount(cards.reduce((sum, card) => sum + cardQuantity(card), 0))
|
||||
})
|
||||
|
||||
const sliderStep = computed(() => (assetUnit.value.includes('عدد') ? 1 : 0.001))
|
||||
|
||||
const remainderAmount = computed(() =>
|
||||
roundAmount(Math.max(0, assetAmount.value - sliderAmount.value)),
|
||||
)
|
||||
|
||||
const wire25Max = computed(() =>
|
||||
roundAmount(Math.max(0, remainderAmount.value - wire15Amount.value)),
|
||||
)
|
||||
|
||||
watch(source, () => {
|
||||
sliderAmount.value = assetAmount.value
|
||||
wire15Amount.value = 0
|
||||
wire25Amount.value = 0
|
||||
})
|
||||
|
||||
watch([remainderAmount, wire15Amount], () => {
|
||||
if (wire15Amount.value > remainderAmount.value) wire15Amount.value = remainderAmount.value
|
||||
if (wire25Amount.value > wire25Max.value) wire25Amount.value = wire25Max.value
|
||||
})
|
||||
|
||||
watch(
|
||||
assetAmount,
|
||||
(amount, prev) => {
|
||||
const previous = prev ?? 0
|
||||
if (sliderAmount.value === previous || sliderAmount.value > amount) sliderAmount.value = amount
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const katodProduct = computed(() => pickProduct(catalog.value, isKatodProduct))
|
||||
const maftolProduct = computed(() => pickProduct(catalog.value, isMaftolProduct))
|
||||
@@ -113,10 +207,38 @@ export function useCopperConvertPage() {
|
||||
wireWeightGrams25: COPPER_CONVERT_WIRE_FALLBACK.wireWeightGrams25,
|
||||
}))
|
||||
|
||||
const result = computed(() =>
|
||||
calcCopperWireConvert(parseKg(inputKg.value), source.value, prices.value),
|
||||
const sourceProduct = computed(() => {
|
||||
if (source.value === 'katod') return katodProduct.value
|
||||
if (source.value === 'maftoul') return maftolProduct.value
|
||||
return shamshProduct.value
|
||||
})
|
||||
|
||||
const selectedKg = computed(() =>
|
||||
quantityToKg(sliderAmount.value, assetUnit.value, sourceProduct.value),
|
||||
)
|
||||
|
||||
const result = computed(() =>
|
||||
calcCopperWireConvert(selectedKg.value, source.value, prices.value),
|
||||
)
|
||||
|
||||
const wire15Rings = computed(() => {
|
||||
const converted = calcCopperWireConvert(
|
||||
quantityToKg(wire15Amount.value, assetUnit.value, sourceProduct.value),
|
||||
source.value,
|
||||
prices.value,
|
||||
)
|
||||
return converted?.rings15ByValue ?? 0
|
||||
})
|
||||
|
||||
const wire25Rings = computed(() => {
|
||||
const converted = calcCopperWireConvert(
|
||||
quantityToKg(wire25Amount.value, assetUnit.value, sourceProduct.value),
|
||||
source.value,
|
||||
prices.value,
|
||||
)
|
||||
return converted?.rings25ByValue ?? 0
|
||||
})
|
||||
|
||||
const priceRows = computed(() => {
|
||||
const p = prices.value
|
||||
const row = (label: string, amount: number) => ({
|
||||
@@ -136,8 +258,11 @@ export function useCopperConvertPage() {
|
||||
async function loadCatalog() {
|
||||
loading.value = true
|
||||
try {
|
||||
await getLastPrices()
|
||||
const list = await getProducts()
|
||||
const [, list] = await Promise.all([
|
||||
getLastPrices(),
|
||||
getProducts(),
|
||||
loadWalletAssets(),
|
||||
])
|
||||
catalog.value = Array.isArray(list) ? list : []
|
||||
} catch {
|
||||
catalog.value = []
|
||||
@@ -158,7 +283,16 @@ export function useCopperConvertPage() {
|
||||
const ctx: CopperConvertPageContext = {
|
||||
loading,
|
||||
source,
|
||||
inputKg,
|
||||
assetAmount,
|
||||
assetUnit,
|
||||
sliderAmount,
|
||||
sliderStep,
|
||||
remainderAmount,
|
||||
wire15Amount,
|
||||
wire25Amount,
|
||||
wire25Max,
|
||||
wire15Rings,
|
||||
wire25Rings,
|
||||
prices,
|
||||
result,
|
||||
priceRows,
|
||||
|
||||
Reference in New Issue
Block a user