Sync live quote handling, SSE product images, store hours, file URL normalization, PWA icon cache, TV monitor, about software, SuperAdmin menu gates, price alerts, and related UX from modular while preserving boursecu branding and config constraints. Co-authored-by: Cursor <cursoragent@cursor.com>
304 lines
7.4 KiB
Vue
304 lines
7.4 KiB
Vue
<template>
|
||
<BasicModal
|
||
v-model:visible="visible"
|
||
width="400px"
|
||
destroy-on-close
|
||
custom-footer
|
||
wrap-class-name="gold-day-calc-modal"
|
||
>
|
||
<template #title>
|
||
<div class="gold-day-calc-modal__title">
|
||
<BasicIcon icon="vuesax-linear:chart-2" size="20" class="gold-day-calc-modal__title-icon" />
|
||
<span>محاسبهگر طلای شما</span>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="gold-day-calc" dir="rtl">
|
||
<BasicInput
|
||
v-model:value="weight"
|
||
title="وزن طلا (گرم)"
|
||
type="tel"
|
||
place-holder="مثلاً ۱۰.۵"
|
||
inputmode="decimal"
|
||
/>
|
||
|
||
<BasicInput
|
||
:value="unitPriceInput"
|
||
title="قیمت روز طلا"
|
||
type="tel"
|
||
disabled
|
||
addon-before="ریال"
|
||
place-holder="قیمت صفحه اصلی"
|
||
/>
|
||
|
||
<BasicInput
|
||
v-model:value="feePercent"
|
||
title="اجرت ساخت (%)"
|
||
type="tel"
|
||
place-holder="مثلاً ۷"
|
||
inputmode="decimal"
|
||
/>
|
||
|
||
<BasicInput
|
||
v-model:value="taxPercent"
|
||
title="سود و مالیات (%)"
|
||
type="tel"
|
||
place-holder="مثلاً ۹"
|
||
inputmode="decimal"
|
||
/>
|
||
|
||
<div class="gold-day-calc__result rounded-xl border border-solid border-gray-200 bg-gray-50 p-3">
|
||
<div class="text-xs text-gray-500 mb-1">کل نهایی</div>
|
||
<div class="gold-day-calc__total text-lg font-bold text-primary">
|
||
<span class="gold-day-calc__num">{{ formattedTotal }}</span>
|
||
<span class="text-sm font-medium text-gray-600">ریال</span>
|
||
</div>
|
||
<div class="mt-2 flex flex-col gap-1 text-xs text-gray-600">
|
||
<div class="flex justify-between gap-2">
|
||
<span>هزینه طلا</span>
|
||
<span class="gold-day-calc__num">{{ formatRial(breakdown.goldCost) }}</span>
|
||
</div>
|
||
<div class="flex justify-between gap-2">
|
||
<span>اجرت</span>
|
||
<span class="gold-day-calc__num">{{ formatRial(breakdown.feeCost) }}</span>
|
||
</div>
|
||
<div class="flex justify-between gap-2">
|
||
<span>سود و مالیات</span>
|
||
<span class="gold-day-calc__num">{{ formatRial(breakdown.taxCost) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<BasicButton type="primary" class="!w-full" size="middle" @click="recalculate">
|
||
محاسبه مجدد
|
||
</BasicButton>
|
||
</template>
|
||
</BasicModal>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, ref, watch } from 'vue'
|
||
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
|
||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||
import { goldDayCalculatorVisible } from '@/composables/useGoldDayCalculator'
|
||
import {
|
||
getLastPrices,
|
||
getTradeBuyPrice,
|
||
lastPrice,
|
||
startLastPricePolling,
|
||
stopLastPricePolling,
|
||
} from '@/utils/price'
|
||
import { getPriceDisplayState, NO_QUOTE_MESSAGE } from '@/utils/storeHours'
|
||
import { usePriceWithoutType } from '@/utils/usePrice'
|
||
import { persianToEnglishDigits } from '@/utils/persianDigits'
|
||
|
||
const visible = goldDayCalculatorVisible
|
||
|
||
const weight = ref<string | number>('')
|
||
const feePercent = ref<string | number>('')
|
||
const taxPercent = ref<string | number>('')
|
||
const tick = ref(0)
|
||
|
||
function parseNum(raw: string | number | undefined): number {
|
||
const s = persianToEnglishDigits(String(raw ?? ''))
|
||
.replace(/,/g, '')
|
||
.replace(/[^\d.]/g, '')
|
||
const n = Number.parseFloat(s)
|
||
return Number.isFinite(n) ? n : 0
|
||
}
|
||
|
||
const liveUnitPrice = computed(() => {
|
||
void tick.value
|
||
if (getPriceDisplayState() !== 'open') return 0
|
||
return getTradeBuyPrice(lastPrice.value) || 0
|
||
})
|
||
|
||
const unitPriceInput = computed(() => {
|
||
void tick.value
|
||
const state = getPriceDisplayState()
|
||
if (state === 'closed') return 'فروشگاه بسته است'
|
||
if (state === 'no_quote') return NO_QUOTE_MESSAGE
|
||
const p = liveUnitPrice.value
|
||
return p > 0 ? usePriceWithoutType(p) : ''
|
||
})
|
||
|
||
const breakdown = computed(() => {
|
||
void tick.value
|
||
const w = parseNum(weight.value)
|
||
const fee = parseNum(feePercent.value)
|
||
const tax = parseNum(taxPercent.value)
|
||
const unit = liveUnitPrice.value
|
||
const goldCost = w * unit
|
||
const feeCost = goldCost * (fee / 100)
|
||
const taxCost = (goldCost + feeCost) * (tax / 100)
|
||
const total = goldCost + feeCost + taxCost
|
||
return { goldCost, feeCost, taxCost, total }
|
||
})
|
||
|
||
const formattedTotal = computed(() => formatRial(breakdown.value.total))
|
||
|
||
function formatRial(n: number) {
|
||
if (!Number.isFinite(n) || n <= 0) return '۰'
|
||
return usePriceWithoutType(Math.round(n))
|
||
}
|
||
|
||
function recalculate() {
|
||
tick.value += 1
|
||
void getLastPrices()
|
||
}
|
||
|
||
watch(visible, async (open) => {
|
||
if (open) {
|
||
startLastPricePolling()
|
||
await getLastPrices()
|
||
tick.value += 1
|
||
} else {
|
||
stopLastPricePolling()
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.gold-day-calc {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.75rem;
|
||
direction: rtl;
|
||
text-align: right;
|
||
}
|
||
|
||
.gold-day-calc :deep(.ant-input-group) {
|
||
direction: ltr;
|
||
display: table;
|
||
width: 100%;
|
||
}
|
||
|
||
.gold-day-calc :deep(.ant-input-group > .ant-input) {
|
||
direction: ltr;
|
||
text-align: left;
|
||
}
|
||
|
||
.gold-day-calc :deep(.ant-input:not(.ant-input-group *)) {
|
||
text-align: right;
|
||
}
|
||
|
||
.gold-day-calc__result {
|
||
direction: rtl;
|
||
text-align: right;
|
||
}
|
||
|
||
.gold-day-calc__total {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: baseline;
|
||
justify-content: flex-start;
|
||
gap: 0.35rem;
|
||
}
|
||
|
||
.gold-day-calc__num {
|
||
direction: ltr;
|
||
unicode-bidi: isolate;
|
||
display: inline-block;
|
||
}
|
||
</style>
|
||
|
||
<style lang="less">
|
||
.gold-day-calc-modal {
|
||
.ant-modal-content {
|
||
direction: rtl;
|
||
display: flex;
|
||
flex-direction: column;
|
||
max-height: min(88dvh, 640px);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.ant-modal-header {
|
||
flex-shrink: 0;
|
||
direction: rtl;
|
||
text-align: right;
|
||
padding-left: 2.75rem;
|
||
padding-right: 1rem;
|
||
}
|
||
|
||
.ant-modal-title {
|
||
text-align: right;
|
||
width: 100%;
|
||
}
|
||
|
||
.gold-day-calc-modal__title {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
gap: 0.5rem;
|
||
direction: rtl;
|
||
width: 100%;
|
||
}
|
||
|
||
.gold-day-calc-modal__title-icon {
|
||
flex-shrink: 0;
|
||
color: var(--App-secondary-color, #c9a062);
|
||
}
|
||
|
||
.ant-modal-close {
|
||
inset-inline-start: 0 !important;
|
||
inset-inline-end: auto !important;
|
||
right: auto !important;
|
||
left: 0 !important;
|
||
}
|
||
|
||
.ant-modal-body {
|
||
flex: 1 1 auto;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
padding-bottom: 0.25rem;
|
||
max-height: none;
|
||
}
|
||
|
||
#body {
|
||
max-height: min(48dvh, 360px) !important;
|
||
overflow-y: auto !important;
|
||
overscroll-behavior: contain;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
.ant-modal-footer {
|
||
flex-shrink: 0;
|
||
margin-top: 0;
|
||
padding-top: 0.75rem;
|
||
border-top: 1px solid rgba(148, 163, 184, 0.25);
|
||
}
|
||
}
|
||
|
||
@media screen and (max-width: 767.98px) {
|
||
.gold-day-calc-modal {
|
||
.ant-modal {
|
||
max-width: calc(100vw - 1.5rem);
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.ant-modal-content {
|
||
max-height: min(86dvh, 100%);
|
||
}
|
||
|
||
#body {
|
||
max-height: min(42dvh, 280px) !important;
|
||
}
|
||
|
||
.ant-modal-body {
|
||
padding-top: 0.25rem;
|
||
padding-bottom: 0.25rem;
|
||
}
|
||
}
|
||
}
|
||
|
||
@media screen and (max-width: 400px) {
|
||
.gold-day-calc-modal #body {
|
||
max-height: min(38dvh, 240px) !important;
|
||
}
|
||
}
|
||
</style>
|