fix(user): keep the chosen alert product and split convert weight across both wires
Alerts store the selected product instead of falling back to gold, and the convert sliders turn the chosen asset amount into complementary 1.5 and 2.5 rings. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -57,4 +57,8 @@
|
||||
- 2026-11-01 | جای واحد و عدد در تبدیل | `CopperConvertCalculator.vue` + `useCopperConvertPage.ts` | ریال/کیلو/عدد با `bdi` داخل `measure` قبل از عدد؛ باکس مقدار `dir=ltr`
|
||||
- 2026-11-01 | ماشینحساب اجرت/سود/مالیات | `components/GoldDayCalculatorModal.vue` | اجرت ساخت مفتول؛ سود مورد انتظار جدا؛ مالیات ارزش افزوده روی (هزینه+اجرت+سود)
|
||||
- 2026-11-01 | پلیسهولدر هشدار قیمت | user `priceTrigger/components/PriceTriggerForm.vue` + admin `priceTriggers/components/PriceTriggerEditorFormBody.vue` | «آبشده، سکه، نقره و ...» → «کاتد مس، مفتول مس و...»
|
||||
- 2026-11-02 | هشدار کاتد→طلا | user `priceTrigger/` + `BasicSelect.vue` + server `Customer/PriceTriggerController.php` | شناسه عددی محصول ارسال شود؛ fallback عنوان «طلا» نباشد. محصول پیشفرض const برابر id=1 است
|
||||
- 2026-11-02 | زبانه تبدیل کشیدنی | `convert/components/CopperConvertCalculator.vue` | `dragHandleOnly` در capture؛ لمس ریل مقدار را جابهجا نمیکند، فقط کشیدن دستگیره
|
||||
- 2026-11-02 | سه زبانه تبدیل از اول | همان `CopperConvertCalculator.vue` | زبانه ۱.۵ و ۲.۵ دیگر پشت `remainderAmount > 0` نیستند؛ زیر زبانه مقدار، قبل از نتیجه
|
||||
- 2026-11-02 | زبانه سیم مکمل | `useCopperConvertPage.ts` | بودجه = ارزش مقدار انتخابشده؛ نصف به ۱.۵ و بقیه به ۲.۵؛ جابهجایی یکی دیگری را پر میکند
|
||||
- 2026-11-01 | تاریخچه برداشت کیف پول | `wallet/mobile/MobileWalletView.vue` + `desktop/DesktopWalletView.vue` + `useWalletPageData.ts` | `walletWithdrawItems` از قبل لود میشد؛ زیر راهنمای برداشت و زیر حسابهای دسکتاپ نشان داده میشود
|
||||
|
||||
@@ -23,8 +23,9 @@
|
||||
<div class="w-full text-center font-bold">{{ noData }}</div>
|
||||
</template>
|
||||
<a-select-option
|
||||
:label="val[props.valueName]"
|
||||
v-for="val in props.items"
|
||||
:key="val[props.keyName]"
|
||||
:label="val[props.valueName]"
|
||||
:value="val[props.keyName]"
|
||||
>
|
||||
<a-tooltip v-if="props.hasTooltip" placement="left" class="cursor-pointer">
|
||||
|
||||
@@ -76,9 +76,8 @@
|
||||
</p>
|
||||
<div
|
||||
class="app-touch-slider copper-convert__slider"
|
||||
@touchstart.stop
|
||||
@touchmove.stop
|
||||
@pointerdown.stop
|
||||
@mousedown.capture="dragHandleOnly"
|
||||
@touchstart.capture="dragHandleOnly"
|
||||
>
|
||||
<a-slider
|
||||
v-model:value="sliderAmount"
|
||||
@@ -94,46 +93,9 @@
|
||||
<span class="copper-convert__measure"><bdi>{{ assetUnit }}</bdi><bdi>{{ formatAssetAmount(assetAmount) }}</bdi></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="result" class="copper-convert__result">
|
||||
<div class="copper-convert__result-head">
|
||||
<span class="copper-convert__result-kicker">خروجی</span>
|
||||
<span class="copper-convert__result-title">نتیجه تبدیل</span>
|
||||
</div>
|
||||
|
||||
<div class="copper-convert__rings">
|
||||
<article class="copper-convert__ring-card">
|
||||
<span class="copper-convert__ring-badge" dir="ltr">۱.۵ mm²</span>
|
||||
<p class="copper-convert__ring-count" dir="ltr">{{ formatRings(result.rings15ByValue) }}</p>
|
||||
<p class="copper-convert__ring-caption">حلقه سیم</p>
|
||||
</article>
|
||||
<article class="copper-convert__ring-card">
|
||||
<span class="copper-convert__ring-badge" dir="ltr">۲.۵ mm²</span>
|
||||
<p class="copper-convert__ring-count" dir="ltr">{{ formatRings(result.rings25ByValue) }}</p>
|
||||
<p class="copper-convert__ring-caption">حلقه سیم</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="copper-convert__summary">
|
||||
<div class="copper-convert__summary-row">
|
||||
<span>ارزش معادل</span>
|
||||
<strong class="copper-convert__measure"><small>ریال</small><bdi>{{ formatRial(result.totalValueRial) }}</bdi></strong>
|
||||
</div>
|
||||
<div class="copper-convert__summary-row">
|
||||
<span>معادل مفتول</span>
|
||||
<strong class="copper-convert__measure"><small>کیلو</small><bdi>{{ formatKg(result.equivalentMaftoulKg) }}</bdi></strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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">
|
||||
مانده <span class="copper-convert__measure"><bdi>{{ assetUnit }}</bdi><bdi>{{ formatAssetAmount(remainderAmount) }}</bdi></span> است. هر زبانه فقط عدد صحیح حلقه است؛ اعشاری که به یک حلقه نرسد در دارایی میماند.
|
||||
با این مقدار میتوانید <bdi>{{ wire15Rings }}</bdi> حلقه سیم ۱.۵ و <bdi>{{ wire25Rings }}</bdi> حلقه سیم ۲.۵ ببرید. این دو زبانه مکملاند و با هم روی همین مقدار تنظیم میشوند.
|
||||
</p>
|
||||
|
||||
<div class="copper-convert__weight">
|
||||
@@ -144,9 +106,8 @@
|
||||
</div>
|
||||
<div
|
||||
class="app-touch-slider copper-convert__slider"
|
||||
@touchstart.stop
|
||||
@touchmove.stop
|
||||
@pointerdown.stop
|
||||
@mousedown.capture="dragHandleOnly"
|
||||
@touchstart.capture="dragHandleOnly"
|
||||
>
|
||||
<a-slider
|
||||
v-model:value="wire15Rings"
|
||||
@@ -171,9 +132,8 @@
|
||||
</div>
|
||||
<div
|
||||
class="app-touch-slider copper-convert__slider"
|
||||
@touchstart.stop
|
||||
@touchmove.stop
|
||||
@pointerdown.stop
|
||||
@mousedown.capture="dragHandleOnly"
|
||||
@touchstart.capture="dragHandleOnly"
|
||||
>
|
||||
<a-slider
|
||||
v-model:value="wire25Rings"
|
||||
@@ -189,9 +149,42 @@
|
||||
<span class="copper-convert__measure"><bdi>عدد حلقه</bdi><bdi>{{ maxRings25 }}</bdi></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="result" class="copper-convert__result">
|
||||
<div class="copper-convert__result-head">
|
||||
<span class="copper-convert__result-kicker">خروجی</span>
|
||||
<span class="copper-convert__result-title">نتیجه تبدیل</span>
|
||||
</div>
|
||||
|
||||
<div class="copper-convert__rings">
|
||||
<article class="copper-convert__ring-card">
|
||||
<span class="copper-convert__ring-badge" dir="ltr">۱.۵ mm²</span>
|
||||
<p class="copper-convert__ring-count" dir="ltr">{{ wire15Rings }}</p>
|
||||
<p class="copper-convert__ring-caption">حلقه میتوانید ببرید</p>
|
||||
</article>
|
||||
<article class="copper-convert__ring-card">
|
||||
<span class="copper-convert__ring-badge" dir="ltr">۲.۵ mm²</span>
|
||||
<p class="copper-convert__ring-count" dir="ltr">{{ wire25Rings }}</p>
|
||||
<p class="copper-convert__ring-caption">حلقه میتوانید ببرید</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="copper-convert__summary">
|
||||
<div class="copper-convert__summary-row">
|
||||
<span>ارزش معادل</span>
|
||||
<strong class="copper-convert__measure"><small>ریال</small><bdi>{{ formatRial(result.totalValueRial) }}</bdi></strong>
|
||||
</div>
|
||||
<div class="copper-convert__summary-row">
|
||||
<span>معادل مفتول</span>
|
||||
<strong class="copper-convert__measure"><small>کیلو</small><bdi>{{ formatKg(result.equivalentMaftoulKg) }}</bdi></strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="copper-convert__split">
|
||||
<p class="copper-convert__asset-hint">
|
||||
اعشار مانده: <span class="copper-convert__measure"><bdi>{{ assetUnit }}</bdi><bdi>{{ formatAssetAmount(leftoverAmount) }}</bdi></span> نگه داشته میشود.
|
||||
از مقدار انتخابشده، <span class="copper-convert__measure"><bdi>{{ assetUnit }}</bdi><bdi>{{ formatAssetAmount(leftoverAmount) }}</bdi></span> به حلقهٔ کامل نمیرسد و در دارایی میماند. باقی دارایی که روی زبانه انتخاب نشده هم دستنخورده میماند.
|
||||
</p>
|
||||
<BasicButton
|
||||
type="primary"
|
||||
@@ -227,7 +220,6 @@ const {
|
||||
assetUnit,
|
||||
sliderAmount,
|
||||
sliderStep,
|
||||
remainderAmount,
|
||||
maxRings15,
|
||||
maxRings25,
|
||||
wire15Rings,
|
||||
@@ -241,6 +233,14 @@ const {
|
||||
|
||||
const amountAttempted = computed(() => sliderAmount.value > 0)
|
||||
|
||||
function dragHandleOnly(event: Event) {
|
||||
const target = event.target
|
||||
if (target instanceof Element && target.closest('.ant-slider-handle')) {
|
||||
return
|
||||
}
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
const sourceOptions = computed(() => [
|
||||
{ label: 'کاتد', value: 'katod' as CopperConvertSource },
|
||||
{ label: 'مفتول', value: 'maftoul' as CopperConvertSource },
|
||||
@@ -274,12 +274,6 @@ function formatKg(value: number) {
|
||||
if (value >= 10) return value.toFixed(2)
|
||||
return value.toFixed(3)
|
||||
}
|
||||
|
||||
function formatRings(value: number) {
|
||||
if (value >= 100) return value.toFixed(1)
|
||||
if (value >= 10) return value.toFixed(2)
|
||||
return value.toFixed(3)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -182,8 +182,7 @@ export function useCopperConvertPage() {
|
||||
|
||||
watch(source, () => {
|
||||
sliderAmount.value = assetAmount.value
|
||||
wire15Rings.value = 0
|
||||
wire25Rings.value = 0
|
||||
applyDefaultSplit()
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -235,37 +234,81 @@ export function useCopperConvertPage() {
|
||||
return prices.value.shamshPerKg
|
||||
})
|
||||
|
||||
const remainderValue = computed(() =>
|
||||
quantityToKg(remainderAmount.value, assetUnit.value, sourceProduct.value) * sourceUnitPrice.value,
|
||||
)
|
||||
const selectedValue = computed(() => result.value?.totalValueRial ?? 0)
|
||||
|
||||
const maxRings15 = computed(() => {
|
||||
const price = prices.value.wireRing15
|
||||
if (price <= 0 || remainderValue.value <= 0) return 0
|
||||
return Math.floor(remainderValue.value / price)
|
||||
if (price <= 0 || selectedValue.value <= 0) return 0
|
||||
return Math.floor(selectedValue.value / price)
|
||||
})
|
||||
|
||||
const maxRings25 = computed(() => {
|
||||
const price = prices.value.wireRing25
|
||||
const left = remainderValue.value - wire15Rings.value * prices.value.wireRing15
|
||||
if (price <= 0 || left <= 0) return 0
|
||||
return Math.floor(left / price)
|
||||
if (price <= 0 || selectedValue.value <= 0) return 0
|
||||
return Math.floor(selectedValue.value / price)
|
||||
})
|
||||
|
||||
function rings25For(rings15: number) {
|
||||
const price = prices.value.wireRing25
|
||||
const left = selectedValue.value - rings15 * prices.value.wireRing15
|
||||
if (price <= 0 || left <= 0) return 0
|
||||
return Math.min(maxRings25.value, Math.floor(left / price))
|
||||
}
|
||||
|
||||
function rings15For(rings25: number) {
|
||||
const price = prices.value.wireRing15
|
||||
const left = selectedValue.value - rings25 * prices.value.wireRing25
|
||||
if (price <= 0 || left <= 0) return 0
|
||||
return Math.min(maxRings15.value, Math.floor(left / price))
|
||||
}
|
||||
|
||||
function defaultSplit() {
|
||||
const value = selectedValue.value
|
||||
const price15 = prices.value.wireRing15
|
||||
if (value <= 0 || price15 <= 0) return { rings15: 0, rings25: 0 }
|
||||
const rings15 = Math.min(maxRings15.value, Math.floor(value / 2 / price15))
|
||||
return { rings15, rings25: rings25For(rings15) }
|
||||
}
|
||||
|
||||
let splitLock = false
|
||||
|
||||
function writeRings(rings15: number, rings25: number) {
|
||||
splitLock = true
|
||||
wire15Rings.value = rings15
|
||||
wire25Rings.value = rings25
|
||||
splitLock = false
|
||||
}
|
||||
|
||||
function applyDefaultSplit() {
|
||||
const split = defaultSplit()
|
||||
writeRings(split.rings15, split.rings25)
|
||||
}
|
||||
|
||||
const leftoverAmount = computed(() => {
|
||||
const spent = wire15Rings.value * prices.value.wireRing15 + wire25Rings.value * prices.value.wireRing25
|
||||
const leftValue = Math.max(0, remainderValue.value - spent)
|
||||
const leftValue = Math.max(0, selectedValue.value - spent)
|
||||
const perUnit = sourceUnitPrice.value
|
||||
if (perUnit <= 0) return remainderAmount.value
|
||||
if (perUnit <= 0) return 0
|
||||
return roundAmount(kgToNative(leftValue / perUnit, assetUnit.value, sourceProduct.value))
|
||||
})
|
||||
|
||||
watch([wire15Rings, maxRings15, maxRings25], () => {
|
||||
const next15 = Math.max(0, Math.min(maxRings15.value, Math.floor(wire15Rings.value)))
|
||||
if (next15 !== wire15Rings.value) wire15Rings.value = next15
|
||||
const next25 = Math.max(0, Math.min(maxRings25.value, Math.floor(wire25Rings.value)))
|
||||
if (next25 !== wire25Rings.value) wire25Rings.value = next25
|
||||
})
|
||||
watch(sliderAmount, () => applyDefaultSplit(), { flush: 'sync' })
|
||||
|
||||
watch(selectedValue, (value, prev) => {
|
||||
if ((prev ?? 0) <= 0 && value > 0) applyDefaultSplit()
|
||||
}, { flush: 'sync' })
|
||||
|
||||
watch(wire15Rings, (value) => {
|
||||
if (splitLock) return
|
||||
const next15 = Math.max(0, Math.min(maxRings15.value, Math.floor(value)))
|
||||
writeRings(next15, rings25For(next15))
|
||||
}, { flush: 'sync' })
|
||||
|
||||
watch(wire25Rings, (value) => {
|
||||
if (splitLock) return
|
||||
const next25 = Math.max(0, Math.min(maxRings25.value, Math.floor(value)))
|
||||
writeRings(rings15For(next25), next25)
|
||||
}, { flush: 'sync' })
|
||||
|
||||
async function submitRequest() {
|
||||
if (submitting.value) return
|
||||
|
||||
@@ -116,6 +116,7 @@ import BasicInfoItem from '@/components/BasicPacks/BasicInfoItem.vue'
|
||||
import DialogVant from '@/components/DialogVant.vue'
|
||||
import { deletePriceTrigger, updatePriceTrigger } from '@/api/triggerPrice'
|
||||
import { loading } from '@/utils'
|
||||
import { usePriceTriggerPageContext } from '../composables/usePriceTriggerPage'
|
||||
|
||||
const infoVisible = ref(false)
|
||||
const isActive = ref(false)
|
||||
@@ -132,7 +133,17 @@ const props = withDefaults(
|
||||
|
||||
const emits = defineEmits(['edit', 'refresh'])
|
||||
|
||||
const productTitle = computed(() => props.item.product?.title || 'طلا')
|
||||
const { products } = usePriceTriggerPageContext()
|
||||
|
||||
const productTitle = computed(() => {
|
||||
const nested = props.item.product?.title?.trim()
|
||||
if (nested) {
|
||||
return nested
|
||||
}
|
||||
const id = Number(props.item.product_id ?? props.item.product?.id)
|
||||
const match = products.value.find((product) => Number(product.id) === id)
|
||||
return match?.title?.trim() || '—'
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
isActive.value = props.item.is_active
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<a-form-item :rules="[required()]" label="محصول" name="product_id">
|
||||
<BasicSelect
|
||||
v-model:value="form.product_id"
|
||||
:items="products"
|
||||
:items="productOptions"
|
||||
key-name="id"
|
||||
placeholder="کاتد مس، مفتول مس و..."
|
||||
size="large"
|
||||
@@ -62,7 +62,7 @@ const {
|
||||
editId,
|
||||
loading,
|
||||
typeItems,
|
||||
products,
|
||||
productOptions,
|
||||
ok,
|
||||
} = usePriceTriggerPageContext()
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { inject, onMounted, provide, reactive, ref, type InjectionKey, type Ref } from 'vue'
|
||||
import { computed, inject, onMounted, provide, reactive, ref, type ComputedRef, type InjectionKey, type Ref } from 'vue'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { type FormInstance, message } from 'ant-design-vue'
|
||||
import { createPriceTrigger, getPriceTriggers, updatePriceTrigger } from '@/api/triggerPrice'
|
||||
@@ -18,9 +18,24 @@ function emptyForm(): PriceTrigger {
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveProductId(value: unknown): number | undefined {
|
||||
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
const id = Number(value)
|
||||
return Number.isFinite(id) && id > 0 ? id : undefined
|
||||
}
|
||||
if (value && typeof value === 'object' && 'id' in value) {
|
||||
return resolveProductId((value as { id?: unknown }).id)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export interface PriceTriggerPageContext {
|
||||
items: Ref<PriceTrigger[]>
|
||||
products: Ref<Product[]>
|
||||
productOptions: ComputedRef<Array<{ id: number; title: string }>>
|
||||
addTriggerVisible: Ref<boolean>
|
||||
editId: Ref<number | undefined>
|
||||
form: Ref<PriceTrigger>
|
||||
@@ -41,6 +56,14 @@ export const PRICE_TRIGGER_PAGE_CONTEXT_KEY: InjectionKey<PriceTriggerPageContex
|
||||
export function usePriceTriggerPage() {
|
||||
const items = ref<PriceTrigger[]>([])
|
||||
const products = ref<Product[]>([])
|
||||
const productOptions = computed(() =>
|
||||
products.value
|
||||
.map((product) => ({
|
||||
id: Number(product.id),
|
||||
title: String(product.title ?? '').trim(),
|
||||
}))
|
||||
.filter((product) => Number.isFinite(product.id) && product.id > 0 && product.title),
|
||||
)
|
||||
const addTriggerVisible = ref(false)
|
||||
const editId = ref<number | undefined>(undefined)
|
||||
const formRef = ref<FormInstance>()
|
||||
@@ -72,7 +95,7 @@ export function usePriceTriggerPage() {
|
||||
function openEdit(val: PriceTrigger) {
|
||||
form.value = cloneDeep({
|
||||
...val,
|
||||
product_id: val.product_id ?? val.product?.id,
|
||||
product_id: resolveProductId(val.product_id ?? val.product?.id),
|
||||
})
|
||||
editId.value = form.value.id
|
||||
addTriggerVisible.value = true
|
||||
@@ -91,21 +114,30 @@ export function usePriceTriggerPage() {
|
||||
|
||||
async function loadProducts() {
|
||||
try {
|
||||
products.value = ((await getProducts()) as Product[]) ?? []
|
||||
const list = await getProducts()
|
||||
products.value = Array.isArray(list) ? list : []
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function ok() {
|
||||
const productId = resolveProductId(form.value.product_id)
|
||||
form.value.product_id = productId
|
||||
await formRef.value?.validateFields().then(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
form.value.action = 'sms'
|
||||
const payload: PriceTrigger = {
|
||||
product_id: productId,
|
||||
price_condition: form.value.price_condition,
|
||||
is_active: form.value.is_active,
|
||||
action: 'sms',
|
||||
target_price: form.value.target_price,
|
||||
}
|
||||
if (editId.value) {
|
||||
await updatePriceTrigger(editId.value, form.value)
|
||||
await updatePriceTrigger(editId.value, payload)
|
||||
} else {
|
||||
await createPriceTrigger(form.value)
|
||||
await createPriceTrigger(payload)
|
||||
}
|
||||
message.success('هشدار با موفقیت ایجاد شد')
|
||||
await initItems()
|
||||
@@ -126,6 +158,7 @@ export function usePriceTriggerPage() {
|
||||
const context: PriceTriggerPageContext = {
|
||||
items,
|
||||
products,
|
||||
productOptions,
|
||||
addTriggerVisible,
|
||||
editId,
|
||||
form,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="price-trigger-page-root">
|
||||
<DesktopPriceTriggerView v-show="isDesktop" />
|
||||
<MobilePriceTriggerView v-show="!isDesktop" />
|
||||
<DesktopPriceTriggerView v-if="isDesktop" />
|
||||
<MobilePriceTriggerView v-else />
|
||||
|
||||
<BasicActionSheet v-if="!isDesktop" v-model:show="addTriggerVisible" teleport="body">
|
||||
<div class="font-bold text-lg mb-2">هشدار قیمت</div>
|
||||
|
||||
Reference in New Issue
Block a user