fix ذشزنلقخعدی
This commit is contained in:
+7
-6
@@ -99,7 +99,7 @@ import {LoadingOutlined} from '@ant-design/icons-vue'
|
||||
import img_bottomGradiant from '@/views/login/assets/img/gr_bottom_white.svg'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import {useConfigStore} from "@/store/configStore";
|
||||
import {getProjectFontFamily, getProjectSplashBackground, getProjectSplashLogo, getProjectBrandRed, hasCustomLoginLogoLayout, isBoursecuProject, resetAppSurfaceBackground} from "@/utils/config";
|
||||
import {getProjectFontFamily, getProjectSplashBackground, getProjectSplashLogo, getProjectBrandRed, getProjectPrimaryColor, hasCustomLoginLogoLayout, isBoursecuProject, resetAppSurfaceBackground} from "@/utils/config";
|
||||
import {ConfigProvider as AConfigProvider} from 'ant-design-vue'
|
||||
import BasicIcon from "@/components/BasicPacks/BasicIcon.vue";
|
||||
import { useApplyDesktopLayoutClass, useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
@@ -211,16 +211,17 @@ function handleSupportPointerUp(event: PointerEvent) {
|
||||
}
|
||||
|
||||
const antTheme = computed(() => {
|
||||
if (!config.config?.color) return null
|
||||
const primaryColor = getProjectPrimaryColor()
|
||||
if (!primaryColor) return null
|
||||
|
||||
const brandRed = getProjectBrandRed()
|
||||
|
||||
return {
|
||||
token: {
|
||||
colorPrimary: config.config.color,
|
||||
colorLink: config.config.color,
|
||||
colorLinkActive: config.config.color,
|
||||
colorLinkHover: config.config.color,
|
||||
colorPrimary: primaryColor,
|
||||
colorLink: primaryColor,
|
||||
colorLinkActive: primaryColor,
|
||||
colorLinkHover: primaryColor,
|
||||
fontFamily: getProjectFontFamily(),
|
||||
...(brandRed ? {
|
||||
colorError: brandRed,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { startPwaManifest } from '@/modules/pwa/manifest'
|
||||
import { createVueApp } from '@/app/createApp'
|
||||
import { setupRouterGuards } from '@/app/setupRouter'
|
||||
import router from '@/router'
|
||||
import { applyProjectTheme } from '@/utils/config'
|
||||
import { applyProjectTheme, isBoursecuProject } from '@/utils/config'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { getToken } from '@/utils'
|
||||
@@ -71,7 +71,7 @@ function applyTenantThemeVars() {
|
||||
const secondaryColor = window.__APP_CONFIG__?.user?.secondary_color
|
||||
const backgroundColor = window.__APP_CONFIG__?.user?.background_color
|
||||
|
||||
if (color) {
|
||||
if (color && !isBoursecuProject()) {
|
||||
document.documentElement.style.setProperty('--App-primary-color', color)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
html.project-boursecu {
|
||||
.ant-btn-primary,
|
||||
.ant-btn-primary:not(:disabled):hover,
|
||||
.ant-btn-primary:not(:disabled):active {
|
||||
background-color: var(--App-primary-color) !important;
|
||||
border-color: var(--App-primary-color) !important;
|
||||
}
|
||||
|
||||
.ant-radio-group-solid .ant-radio-button-wrapper-checked,
|
||||
.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover {
|
||||
border-color: var(--App-primary-color) !important;
|
||||
background-color: var(--App-primary-color) !important;
|
||||
}
|
||||
|
||||
.ant-input-group-addon {
|
||||
background-color: @secondary-color !important;
|
||||
color: @App-primary-color !important;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 28 KiB |
@@ -1,6 +1,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { getProducts } from '@/api/products'
|
||||
import type { Product } from '@/models/products'
|
||||
import { syncProductTradingHoursFromProducts } from '@/utils/storeHours'
|
||||
|
||||
/** Shared product catalog — dedupes getProducts() across wallet assets and chart offset. */
|
||||
const catalogProducts = ref<Product[]>([])
|
||||
@@ -22,6 +23,7 @@ export function getProductsCatalog(force = false): Promise<Product[]> {
|
||||
catalogPromise = getProducts()
|
||||
.then((products) => {
|
||||
catalogProducts.value = products ?? []
|
||||
syncProductTradingHoursFromProducts(catalogProducts.value)
|
||||
return catalogProducts.value
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
@@ -20,7 +20,13 @@ const BOURSECU_PRIMARY_NAV: AppNavItem[] = [
|
||||
icon: 'vuesax-linear:receipt-item',
|
||||
iconSrc: getBoursecuTransactionsIcon(),
|
||||
},
|
||||
{ id: 'home', routeName: 'home', label: 'صفحه اصلی', icon: 'vuesax-linear:home' },
|
||||
{
|
||||
id: 'home',
|
||||
routeName: 'home',
|
||||
label: 'صفحه اصلی',
|
||||
icon: 'vuesax-linear:home',
|
||||
iconSrc: getBoursecuActionIconSrc('home'),
|
||||
},
|
||||
{
|
||||
id: 'wallet',
|
||||
routeName: 'wallet',
|
||||
@@ -28,7 +34,13 @@ const BOURSECU_PRIMARY_NAV: AppNavItem[] = [
|
||||
icon: 'vuesax-linear:wallet-1',
|
||||
iconSrc: getBoursecuActionIconSrc('wallet'),
|
||||
},
|
||||
{ id: 'user', routeName: 'user', label: 'تنظیمات', icon: 'vuesax-linear:setting-2' },
|
||||
{
|
||||
id: 'user',
|
||||
routeName: 'user',
|
||||
label: 'تنظیمات',
|
||||
icon: 'vuesax-linear:setting-2',
|
||||
iconSrc: getBoursecuActionIconSrc('settings'),
|
||||
},
|
||||
]
|
||||
|
||||
const DEFAULT_PRIMARY_NAV: AppNavItem[] = [
|
||||
|
||||
@@ -52,7 +52,11 @@
|
||||
@click="$emit('navigate', 'home')"
|
||||
>
|
||||
<div class="tab__icon">
|
||||
<Icon icon="vuesax-linear:home" size="24" />
|
||||
<img
|
||||
:src="boursecuHomeIcon"
|
||||
alt=""
|
||||
class="tab__icon-image"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-xs mt-1 font-bold">صفحه اصلی</div>
|
||||
</div>
|
||||
@@ -101,7 +105,13 @@
|
||||
@click="$emit('navigate', userNavItem.routeName)"
|
||||
>
|
||||
<div class="tab__icon">
|
||||
<Icon :icon="userNavItem.icon" size="24" />
|
||||
<img
|
||||
v-if="isBoursecu && userNavItem.routeName === 'user'"
|
||||
:src="boursecuSettingsIcon"
|
||||
alt=""
|
||||
class="tab__icon-image"
|
||||
/>
|
||||
<Icon v-else :icon="userNavItem.icon" size="24" />
|
||||
</div>
|
||||
<div class="text-xs mt-1 font-bold">{{ userNavItem.label }}</div>
|
||||
</div>
|
||||
@@ -137,6 +147,8 @@ const userNavItem = computed(() =>
|
||||
const boursecuBuySellIcon = getBoursecuActionIconSrc('buySell') ?? ''
|
||||
const boursecuTransactionsIcon = getBoursecuActionIconSrc('transactions') ?? ''
|
||||
const boursecuWalletIcon = getBoursecuActionIconSrc('wallet') ?? ''
|
||||
const boursecuHomeIcon = getBoursecuActionIconSrc('home') ?? ''
|
||||
const boursecuSettingsIcon = getBoursecuActionIconSrc('settings') ?? ''
|
||||
const whiteNavLogoProjects = ['goldenramin']
|
||||
const isWhiteNavLogo = computed(() => whiteNavLogoProjects.includes(projectName))
|
||||
</script>
|
||||
|
||||
@@ -7,6 +7,8 @@ import boursecuConvertIcon from '@/assets/img/boursecu-convert-icon.png'
|
||||
import boursecuWalletIcon from '@/assets/img/boursecu-wallet-icon.png'
|
||||
import boursecuBuySellIcon from '@/assets/img/boursecu-buy-sell-icon.png'
|
||||
import boursecuAssetsIcon from '@/assets/img/boursecu-assets-icon.png'
|
||||
import boursecuHomeIcon from '@/assets/img/boursecu-home-icon.png'
|
||||
import boursecuSettingsIcon from '@/assets/img/boursecu-settings-icon.png'
|
||||
import bazretalaLoginLogo from '@/assets/img/bazretala-login-logo.png'
|
||||
import bazretalaNavLogo from '@/assets/img/bazretala-nav-logo.png'
|
||||
import bazretalaFundBg from '@/assets/img/bazretala-fund-bg.png'
|
||||
@@ -53,6 +55,14 @@ export function getBoursecuAssetsIcon() {
|
||||
return boursecuAssetsIcon
|
||||
}
|
||||
|
||||
export function getBoursecuHomeIcon() {
|
||||
return boursecuHomeIcon
|
||||
}
|
||||
|
||||
export function getBoursecuSettingsIcon() {
|
||||
return boursecuSettingsIcon
|
||||
}
|
||||
|
||||
export function getBoursecuActionIconSrc(actionId: string): string | undefined {
|
||||
const icons: Record<string, string> = {
|
||||
buySell: boursecuBuySellIcon,
|
||||
@@ -61,6 +71,8 @@ export function getBoursecuActionIconSrc(actionId: string): string | undefined {
|
||||
transactions: boursecuTransactionsIcon,
|
||||
physicalReceive: boursecuPhysicalReceiveIcon,
|
||||
convert: boursecuConvertIcon,
|
||||
home: boursecuHomeIcon,
|
||||
settings: boursecuSettingsIcon,
|
||||
}
|
||||
|
||||
return icons[actionId]
|
||||
@@ -71,12 +83,26 @@ export function hasCustomLoginLogoLayout() {
|
||||
}
|
||||
|
||||
export const BOURSECU_BRAND_RED = '#bc3c3b'
|
||||
export const BOURSECU_BRAND_BLUE = '#2178b5'
|
||||
export const BOURSECU_BRAND_NAV = '#133050'
|
||||
export const BOURSECU_COPPER_BG = '#b87333'
|
||||
export const BOURSECU_COPPER_BG_LIGHT = '#fdf0e6'
|
||||
export const BOURSECU_COPPER_BG_GRADIENT = 'linear-gradient(135deg, #c98a4a 0%, #a86528 100%)'
|
||||
|
||||
export function getProjectBrandRed() {
|
||||
return isBoursecuProject() ? BOURSECU_BRAND_RED : null
|
||||
}
|
||||
|
||||
/** رنگ اصلی دکمهها و چارت — boursecu ثابت آبی، بقیه از تنظیمات tenant */
|
||||
export function getProjectPrimaryColor() {
|
||||
if (isBoursecuProject()) {
|
||||
return BOURSECU_BRAND_BLUE
|
||||
}
|
||||
|
||||
const color = getConfigApp('color')
|
||||
return color != null ? String(color) : undefined
|
||||
}
|
||||
|
||||
export function resolveProjectColor(color?: string | null) {
|
||||
if (!color || !isBoursecuProject()) {
|
||||
return color
|
||||
@@ -240,6 +266,7 @@ export function applyProjectTheme() {
|
||||
}
|
||||
|
||||
document.documentElement.classList.add('project-boursecu')
|
||||
document.documentElement.style.setProperty('--App-primary-color', BOURSECU_BRAND_BLUE)
|
||||
document.documentElement.style.setProperty('--App-brand-nav', BOURSECU_BRAND_NAV)
|
||||
document.documentElement.style.setProperty('--App-brand-red', BOURSECU_BRAND_RED)
|
||||
document.documentElement.style.setProperty('--App-brand-red-50', '#f8ecec')
|
||||
|
||||
@@ -394,10 +394,6 @@ export function formatMetalAssetsInfoText(metal: WalletMetalTotal): string {
|
||||
return `${metal.label}: مجموع کیف محصولات (${metal.product_weight} ${unit}) + موجودی کیف اصلی (${metal.melted_weight} ${unit})`
|
||||
}
|
||||
|
||||
function formatRialNumber(value: number): string {
|
||||
return usePriceWithoutType(value)
|
||||
}
|
||||
|
||||
/** متن توضیح مودال دارایی ریالی */
|
||||
export function formatRialAssetsInfoText(cashWallet: number, metalRialValue: number): string {
|
||||
const total = cashWallet + metalRialValue
|
||||
|
||||
@@ -86,7 +86,24 @@ export function isMeltedProductUsingAppPrice(product?: {
|
||||
group_type?: string | null
|
||||
reading_method?: string | null
|
||||
direct_price?: number | null
|
||||
trade_by_quantity?: boolean | number | null
|
||||
price?: number | null
|
||||
} | null) {
|
||||
return isMeltedTradeGroup(product?.group_type)
|
||||
&& resolveMeltedPriceSource(product) === 'app'
|
||||
if (!product || resolveMeltedPriceSource(product) !== 'app') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isMeltedTradeGroup(product.group_type)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!usesQuantityTrade(product)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (Number(product.direct_price) === 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
return Number(product.price ?? 0) <= 0
|
||||
}
|
||||
|
||||
@@ -42,6 +42,19 @@ export function isKatodMesGroup(groupType?: string | null) {
|
||||
return normalizeProductGroupType(groupType) === BOURSECU_GROUP_KATOD
|
||||
}
|
||||
|
||||
export function isShamshMesGroup(groupType?: string | null, title?: string | null) {
|
||||
if (!isBoursecuProject()) {
|
||||
return false
|
||||
}
|
||||
|
||||
const group = normalizeProductGroupType(groupType)
|
||||
if (group === 'شمش') {
|
||||
return true
|
||||
}
|
||||
|
||||
return (title ?? '').includes('شمش')
|
||||
}
|
||||
|
||||
export function isGoldOnlyDealGroup(groupType?: string | null) {
|
||||
return isMeltedTradeGroup(groupType)
|
||||
}
|
||||
@@ -51,6 +64,7 @@ export interface ProductUnitSource {
|
||||
direct_price?: number | null
|
||||
group_type?: string | null
|
||||
amount?: number | null
|
||||
trade_by_quantity?: boolean | number | null
|
||||
}
|
||||
|
||||
export function isWeightBasedProductUnit(unit?: string | null) {
|
||||
@@ -111,8 +125,17 @@ export function formatProductWeightAmount(amount?: number | null, product?: Prod
|
||||
return `${value} ${weightUnit}`
|
||||
}
|
||||
|
||||
/** فقط محصول تعدادی (مثل سکه): «هر عدد X گرم» — معامله مقداری با trade_by_quantity نیست */
|
||||
export function shouldShowPerUnitWeightHint(product?: ProductUnitSource | null) {
|
||||
if (!product || !hasProductUnitWeight(product) || usesQuantityTrade(product)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return isCountBasedProduct(product)
|
||||
}
|
||||
|
||||
export function getProductUnitWeightLabel(product?: ProductUnitSource | null) {
|
||||
if (!hasProductUnitWeight(product)) {
|
||||
if (!shouldShowPerUnitWeightHint(product)) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -152,12 +175,16 @@ export function formatProductInventoryBalance(product?: (ProductUnitSource & { w
|
||||
return `${quantityPart} · هر عدد ${unitWeight}`
|
||||
}
|
||||
|
||||
/** محصول تعدادی: موجودی wallet.gold = تعداد قلم، نه وزن */
|
||||
/** محصول تعدادی: موجودی wallet.gold = تعداد قلم، نه وزن — معامله مقداری با trade_by_quantity نیست */
|
||||
export function isCountBasedProduct(product?: ProductUnitSource | null) {
|
||||
if (!product) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (usesQuantityTrade(product)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isWeightBasedProductUnit(product.unit)) {
|
||||
return false
|
||||
}
|
||||
@@ -567,3 +594,23 @@ export function getMetalTypeTradeLabel() {
|
||||
export function getTradePriceUnitLabel() {
|
||||
return useSettingsStore().metalTypeLabel
|
||||
}
|
||||
|
||||
/** برچسب قیمت حین معامله — معامله مقداری: نام و واحد همان محصول */
|
||||
export function getTransactionTradePriceLabel(product?: (ProductUnitSource & { title?: string | null }) | null) {
|
||||
if (product && usesQuantityTrade(product)) {
|
||||
const unit = getProductQuantityUnit(product)
|
||||
const title = (product.title ?? '').trim()
|
||||
return title ? `قیمت هر ${unit} ${title}` : `قیمت هر ${unit}`
|
||||
}
|
||||
|
||||
return `قیمت هر ${getWeightUnit()} ${getTradePriceUnitLabel()}`
|
||||
}
|
||||
|
||||
/** وزن کل زیر مقدار مبادلهشده — فقط محصول تعدادی (نه trade_by_quantity) */
|
||||
export function shouldShowTransactionTotalWeightLine(product?: ProductUnitSource | null) {
|
||||
if (!product || usesQuantityTrade(product) || !hasProductUnitWeight(product)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return isCountBasedProduct(product)
|
||||
}
|
||||
|
||||
+69
-12
@@ -1,3 +1,4 @@
|
||||
import { ref } from 'vue'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { lastPrice } from '@/utils/price'
|
||||
|
||||
@@ -13,7 +14,13 @@ export interface EffectiveTradingHours {
|
||||
enforced: boolean
|
||||
}
|
||||
|
||||
let productTradingHours: { start: string; end: string } | null = null
|
||||
const productTradingHours = ref<{ start: string; end: string } | null>(null)
|
||||
const productTradingHoursVersion = ref(0)
|
||||
|
||||
function normalizeTradingTimeSetting(value: unknown, fallback: string): string {
|
||||
const raw = String(value ?? '').trim()
|
||||
return raw || fallback
|
||||
}
|
||||
|
||||
function parseTimeToSeconds(time: string): number {
|
||||
const parts = time.trim().split(':').map(Number)
|
||||
@@ -22,20 +29,56 @@ function parseTimeToSeconds(time: string): number {
|
||||
}
|
||||
|
||||
export function formatTradingTimeDisplay(time: string): string {
|
||||
const parts = time.trim().split(':')
|
||||
const normalized = normalizeTradingTimeSetting(time, '')
|
||||
if (!normalized) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const parts = normalized.split(':')
|
||||
if (parts.length >= 3) {
|
||||
return `${parts[0]}:${parts[1]}:${parts[2]}`
|
||||
}
|
||||
if (parts.length >= 2) {
|
||||
return `${parts[0]}:${parts[1]}:00`
|
||||
}
|
||||
return time
|
||||
return normalized
|
||||
}
|
||||
|
||||
function hasProductTradingWindow(start?: string | null, end?: string | null): boolean {
|
||||
const open = formatTradingTimeDisplay(normalizeTradingTimeSetting(start, '00:00:00'))
|
||||
const close = formatTradingTimeDisplay(normalizeTradingTimeSetting(end, '00:00:00'))
|
||||
if (!open || !close) {
|
||||
return false
|
||||
}
|
||||
|
||||
return open !== close
|
||||
}
|
||||
|
||||
export function setProductTradingHours(start?: string | null, end?: string | null) {
|
||||
if (start && end) {
|
||||
productTradingHours = { start, end }
|
||||
if (!hasProductTradingWindow(start, end)) {
|
||||
productTradingHours.value = null
|
||||
productTradingHoursVersion.value += 1
|
||||
return
|
||||
}
|
||||
|
||||
productTradingHours.value = {
|
||||
start: normalizeTradingTimeSetting(start, '00:00'),
|
||||
end: normalizeTradingTimeSetting(end, '23:59'),
|
||||
}
|
||||
productTradingHoursVersion.value += 1
|
||||
}
|
||||
|
||||
export function syncProductTradingHoursFromProducts(
|
||||
products: Array<{ auto_open_at?: string | null; auto_close_at?: string | null }>,
|
||||
) {
|
||||
for (const product of products) {
|
||||
if (hasProductTradingWindow(product.auto_open_at, product.auto_close_at)) {
|
||||
setProductTradingHours(product.auto_open_at, product.auto_close_at)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setProductTradingHours(null, null)
|
||||
}
|
||||
|
||||
export function getStoreTradingHours(): StoreTradingHours {
|
||||
@@ -47,13 +90,14 @@ export function getStoreTradingHours(): StoreTradingHours {
|
||||
String(closeRaw ?? 'false').trim().toLowerCase() === 'true'
|
||||
|
||||
return {
|
||||
start: String(settings.getSettingByKey('startDay') ?? '00:00'),
|
||||
end: String(settings.getSettingByKey('endDay') ?? '23:59'),
|
||||
start: normalizeTradingTimeSetting(settings.getSettingByKey('startDay'), '00:00'),
|
||||
end: normalizeTradingTimeSetting(settings.getSettingByKey('endDay'), '23:59'),
|
||||
closePriceEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
export function getEffectiveTradingHours(): EffectiveTradingHours {
|
||||
void productTradingHoursVersion.value
|
||||
const settings = getStoreTradingHours()
|
||||
if (settings.closePriceEnabled) {
|
||||
return {
|
||||
@@ -63,10 +107,10 @@ export function getEffectiveTradingHours(): EffectiveTradingHours {
|
||||
}
|
||||
}
|
||||
|
||||
if (productTradingHours?.start && productTradingHours?.end) {
|
||||
if (productTradingHours.value?.start && productTradingHours.value?.end) {
|
||||
return {
|
||||
start: productTradingHours.start,
|
||||
end: productTradingHours.end,
|
||||
start: productTradingHours.value.start,
|
||||
end: productTradingHours.value.end,
|
||||
enforced: true,
|
||||
}
|
||||
}
|
||||
@@ -92,10 +136,18 @@ export function isStoreClosedBySchedule(now = new Date()): boolean {
|
||||
}
|
||||
|
||||
export function isStoreClosed(): boolean {
|
||||
if (lastPrice.value?.closed === true) {
|
||||
return true
|
||||
void productTradingHoursVersion.value
|
||||
const { closePriceEnabled } = getStoreTradingHours()
|
||||
|
||||
if (closePriceEnabled) {
|
||||
if (lastPrice.value?.closed === true) {
|
||||
return true
|
||||
}
|
||||
|
||||
return isStoreClosedBySchedule()
|
||||
}
|
||||
|
||||
// وقتی close_price خاموش است، صفر بودن قیمت مرجع به معنی بسته بودن فروشگاه نیست.
|
||||
return isStoreClosedBySchedule()
|
||||
}
|
||||
|
||||
@@ -103,5 +155,10 @@ export function getStoreClosedMessage(): string {
|
||||
const { start, end } = getEffectiveTradingHours()
|
||||
const open = formatTradingTimeDisplay(start)
|
||||
const close = formatTradingTimeDisplay(end)
|
||||
|
||||
if (!open || !close) {
|
||||
return 'فروشگاه در حال حاضر بسته است.'
|
||||
}
|
||||
|
||||
return `فروشگاه در حال حاضر بسته است. ساعت کاری از ${open} تا ${close} میباشد.`
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="font-bold">{{ item.quantity ?? '-' }}</div>
|
||||
{{ item.unit }}
|
||||
</div>
|
||||
<div v-if="item.unit_weight" class="text-xs text-gray-500">
|
||||
<div v-if="item.unit_weight && item.display_type === 'count'" class="text-xs text-gray-500">
|
||||
هر عدد {{ item.unit_weight }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,23 +17,23 @@
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:info-circle" size="18" :color="iconColor" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="openRowId === row.id"
|
||||
class="boursecu-assets__popup"
|
||||
@click.stop
|
||||
>
|
||||
<div
|
||||
v-if="openRowId === row.id"
|
||||
class="boursecu-assets__popup"
|
||||
@click.stop
|
||||
v-for="line in row.popupLines"
|
||||
:key="line.label"
|
||||
class="boursecu-assets__popup-line"
|
||||
:class="{
|
||||
'boursecu-assets__popup-line--profit': line.tone === 'profit',
|
||||
'boursecu-assets__popup-line--loss': line.tone === 'loss',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="line in row.popupLines"
|
||||
:key="line.label"
|
||||
class="boursecu-assets__popup-line"
|
||||
:class="{
|
||||
'boursecu-assets__popup-line--profit': line.tone === 'profit',
|
||||
'boursecu-assets__popup-line--loss': line.tone === 'loss',
|
||||
}"
|
||||
>
|
||||
<span class="boursecu-assets__popup-label">{{ line.label }}</span>
|
||||
<span class="boursecu-assets__popup-value">{{ line.value }}</span>
|
||||
</div>
|
||||
<span class="boursecu-assets__popup-label">{{ line.label }}</span>
|
||||
<span class="boursecu-assets__popup-value">{{ line.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,6 +86,7 @@ onClickOutside(rootRef, () => {
|
||||
}
|
||||
|
||||
.boursecu-assets__row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -122,9 +123,12 @@ onClickOutside(rootRef, () => {
|
||||
.boursecu-assets__popup {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
left: 50%;
|
||||
right: auto;
|
||||
transform: translateX(-50%);
|
||||
z-index: 40;
|
||||
min-width: min(18rem, 78vw);
|
||||
width: min(18rem, calc(100vw - 2rem));
|
||||
max-width: calc(100vw - 2rem);
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
@@ -177,9 +181,11 @@ onClickOutside(rootRef, () => {
|
||||
}
|
||||
|
||||
.boursecu-assets__popup {
|
||||
right: auto;
|
||||
left: 0;
|
||||
min-width: 20rem;
|
||||
right: auto;
|
||||
transform: none;
|
||||
width: min(20rem, calc(100% - 1rem));
|
||||
max-width: 20rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,29 +1,47 @@
|
||||
<template>
|
||||
<div class="w-full relative mt-2 border-gray-200 border-1 rounded-lg py-3 flex flex-col justify-center home_appear_fade_in">
|
||||
<div class="text-gray-600 text-[12px] ml-2 w-full text-center">قیمت خرید از {{ config.config.name_fa }}</div>
|
||||
<div class="w-full items-center flex justify-center">
|
||||
<div v-if="price" class="flex gap-1 text-xs items-center">
|
||||
<div
|
||||
v-if="percentChange !== 0"
|
||||
class="price-change-badge"
|
||||
:class="percentChange > 0 ? 'price-change-badge--up' : 'price-change-badge--down'"
|
||||
>
|
||||
<Icon
|
||||
:key="displayGoldVal"
|
||||
:size="15"
|
||||
:icon="percentChange > 0 ? 'vuesax-linear:send' : 'vuesax-linear:received'"
|
||||
:color="percentChange > 0 ? 'green-600' : 'red-600'"
|
||||
/>
|
||||
<span class="price-change-badge__value">{{ formatSignedPercentChange(percentChange) }}</span>
|
||||
<div
|
||||
class="w-full relative mt-2 rounded-lg py-3 flex flex-col justify-center home_appear_fade_in transition-colors duration-300"
|
||||
:class="containerClass"
|
||||
>
|
||||
<template v-if="isBoursecu && showStoreClosed">
|
||||
<div class="boursecu-closed-box">
|
||||
<div class="boursecu-closed-box__icon-wrap">
|
||||
<Icon icon="vuesax-linear:lock" size="22" color="#ffffff" />
|
||||
</div>
|
||||
<Vue3Odometer
|
||||
:value="displayGoldVal"
|
||||
class="o-text !text-xl font-bold"
|
||||
style="font-family: iran-sans; direction: ltr"
|
||||
/>
|
||||
ریال
|
||||
<div class="boursecu-closed-box__title">فروشگاه بسته است</div>
|
||||
<div class="boursecu-closed-box__hint">{{ storeClosedHint }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="price-label" :class="isBoursecu ? 'price-label--boursecu' : 'text-gray-600 text-[12px] ml-2 w-full text-center'">
|
||||
{{ priceLabel }}
|
||||
</div>
|
||||
<div class="w-full items-center flex justify-center">
|
||||
<div v-if="price" class="flex gap-1 text-xs items-center">
|
||||
<div
|
||||
v-if="percentChange !== 0"
|
||||
class="price-change-badge"
|
||||
:class="percentChange > 0 ? 'price-change-badge--up' : 'price-change-badge--down'"
|
||||
>
|
||||
<Icon
|
||||
:key="displayGoldVal"
|
||||
:size="15"
|
||||
:icon="percentChange > 0 ? 'vuesax-linear:send' : 'vuesax-linear:received'"
|
||||
:color="percentChange > 0 ? 'green-600' : 'red-600'"
|
||||
/>
|
||||
<span class="price-change-badge__value">{{ formatSignedPercentChange(percentChange) }}</span>
|
||||
</div>
|
||||
<Vue3Odometer
|
||||
:value="displayGoldVal"
|
||||
class="o-text !text-xl font-bold"
|
||||
:class="isBoursecu ? 'price-odometer--boursecu' : ''"
|
||||
style="font-family: iran-sans; direction: ltr"
|
||||
/>
|
||||
<span :class="isBoursecu ? 'price-unit--boursecu' : ''">ریال</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -31,29 +49,128 @@
|
||||
import { computed } from 'vue'
|
||||
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { isBoursecuProject } from '@/utils/config'
|
||||
import { getTradeBuyPrice, lastPrice, percentPriceChanged, price } from '@/utils/price'
|
||||
import { getStoreClosedMessage, isStoreClosed } from '@/utils/storeHours'
|
||||
import Vue3Odometer from 'vue3-odometer'
|
||||
import 'odometer/themes/odometer-theme-default.css'
|
||||
import { formatSignedPercentChange } from '@/utils/usePrice'
|
||||
|
||||
const config = useConfigStore()
|
||||
const userStore = useUserStore()
|
||||
const isBoursecu = isBoursecuProject()
|
||||
|
||||
const priceLabel = computed(() =>
|
||||
isBoursecu ? 'قیمت لحظهای کاتد مس' : `قیمت خرید از ${config.config.name_fa}`
|
||||
)
|
||||
|
||||
const showStoreClosed = computed(() => {
|
||||
if (isBoursecu) {
|
||||
return isStoreClosed()
|
||||
}
|
||||
return lastPrice?.value?.goldir === 0 && getTradeBuyPrice(lastPrice.value) === 0
|
||||
})
|
||||
|
||||
const storeClosedHint = computed(() => {
|
||||
const custom = userStore.getValueByKey('closed_message')
|
||||
if (typeof custom === 'string' && custom.trim()) {
|
||||
return custom.replace(/<[^>]*>/g, '').trim()
|
||||
}
|
||||
return getStoreClosedMessage()
|
||||
})
|
||||
|
||||
const containerClass = computed(() => {
|
||||
if (!isBoursecu) {
|
||||
return 'border-gray-200 border-1'
|
||||
}
|
||||
if (showStoreClosed.value) {
|
||||
return 'boursecu-price-box boursecu-price-box--closed'
|
||||
}
|
||||
return 'boursecu-price-box boursecu-price-box--open'
|
||||
})
|
||||
|
||||
const percentChange = computed(() => {
|
||||
if (!getTradeBuyPrice(lastPrice.value)) return 0
|
||||
return percentPriceChanged()
|
||||
})
|
||||
|
||||
const isMarketClosed = computed(() => {
|
||||
return lastPrice?.value?.goldir === 0 && getTradeBuyPrice(lastPrice.value) === 0
|
||||
})
|
||||
|
||||
const displayGoldVal = computed(() => {
|
||||
if (isMarketClosed.value) return 0
|
||||
if (showStoreClosed.value) return 0
|
||||
return getTradeBuyPrice(lastPrice.value)
|
||||
})
|
||||
|
||||
void price
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.boursecu-price-box {
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.boursecu-price-box--open {
|
||||
background: linear-gradient(135deg, #c98a4a 0%, #a86528 100%);
|
||||
box-shadow: 0 4px 14px rgba(168, 101, 40, 0.28);
|
||||
}
|
||||
|
||||
.boursecu-price-box--closed {
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
box-shadow: 0 4px 14px rgba(45, 55, 72, 0.22);
|
||||
min-height: 88px;
|
||||
}
|
||||
|
||||
.price-label--boursecu {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.price-odometer--boursecu {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.price-unit--boursecu {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.boursecu-closed-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.boursecu-closed-box__icon-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.boursecu-closed-box__title {
|
||||
color: #ffffff;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.boursecu-closed-box__hint {
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.price-change-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -25,6 +25,14 @@
|
||||
>
|
||||
مفتول مس
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="chart-metal-segment__item"
|
||||
:class="{ 'chart-metal-segment__item--active': selectedMetal === 'shamsh' }"
|
||||
@click="emit('update:selectedMetal', 'shamsh')"
|
||||
>
|
||||
شمش مس
|
||||
</button>
|
||||
</div>
|
||||
<a-radio-group v-model:value="selectedValue" button-style="solid" class="chart-timeline-group">
|
||||
<a-radio-button value="DAILY" @click="handleUpdate('DAILY')">24 ساعت اخیر</a-radio-button>
|
||||
@@ -50,7 +58,7 @@
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import { usePrice } from '@/utils/usePrice'
|
||||
import { getProjectFontFamily } from '@/utils/config'
|
||||
import { getProjectFontFamily, getProjectPrimaryColor } from '@/utils/config'
|
||||
import {
|
||||
computeKatodMesVisualYBounds,
|
||||
prepareKatodMesChartPoints,
|
||||
@@ -100,7 +108,7 @@ const emit = defineEmits<{
|
||||
|
||||
const config = useConfigStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const { maftolChartRatio } = usePriceChart()
|
||||
const { selectedMetalChartRatio } = usePriceChart()
|
||||
const chartContainerRef = ref<HTMLElement | null>(null)
|
||||
const chartMounted = ref(false)
|
||||
const selectedValue = ref(props.chartSelection)
|
||||
@@ -126,7 +134,7 @@ const katodDayRange = computed(() => {
|
||||
price: price.value,
|
||||
metal: props.selectedMetal,
|
||||
isBoursecuLayout: true,
|
||||
maftolRatio: maftolChartRatio.value,
|
||||
metalRatio: selectedMetalChartRatio.value,
|
||||
})
|
||||
})
|
||||
const displayChartSeries = computed(() => {
|
||||
@@ -287,7 +295,7 @@ const chartOptions = computed(() => {
|
||||
}),
|
||||
style: { fontFamily },
|
||||
},
|
||||
colors: [config.config.color],
|
||||
colors: [getProjectPrimaryColor() ?? config.config?.color ?? '#2178b5'],
|
||||
fill: {
|
||||
type: 'gradient',
|
||||
gradient: {
|
||||
@@ -324,8 +332,8 @@ const handleUpdate = (timeline: string) => {
|
||||
.chart-metal-segment__item {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
min-width: 6.5rem;
|
||||
padding: 0.4rem 0.75rem;
|
||||
min-width: 5.5rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
|
||||
@@ -46,7 +46,7 @@ withDefaults(
|
||||
}
|
||||
)
|
||||
|
||||
const { selectedChartMetal, maftolChartRatio } = usePriceChart()
|
||||
const { selectedChartMetal, selectedMetalChartRatio } = usePriceChart()
|
||||
const isBoursecuLayout = computed(() => getHomeLayoutId() === 'boursecu')
|
||||
|
||||
const rangePrices = computed(() =>
|
||||
@@ -54,7 +54,7 @@ const rangePrices = computed(() =>
|
||||
price: price.value,
|
||||
metal: selectedChartMetal.value,
|
||||
isBoursecuLayout: isBoursecuLayout.value,
|
||||
maftolRatio: maftolChartRatio.value,
|
||||
metalRatio: selectedMetalChartRatio.value,
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import type { Product } from '@/models/products'
|
||||
import { getProductsCatalog, getProductsCatalogRef } from '@/composables/useProductsCatalog'
|
||||
import { getHomeLayoutId } from '../layouts/registry'
|
||||
import { getProductBuyPrice, resolvePriceOffsetRatio } from '@/utils/productPrice'
|
||||
import { isKatodMesGroup, isMaftolMesGroup } from '@/utils/productDealMethod'
|
||||
import { isKatodMesGroup, isMaftolMesGroup, isShamshMesGroup } from '@/utils/productDealMethod'
|
||||
import { getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@/utils/price'
|
||||
|
||||
export type ChartMetalType = 'katod' | 'maftol'
|
||||
export type ChartMetalType = 'katod' | 'maftol' | 'shamsh'
|
||||
|
||||
type ChartPoint = [number, number]
|
||||
|
||||
@@ -25,6 +25,7 @@ function resolveChartProducts(products: Product[]) {
|
||||
return {
|
||||
katod: findKatodProduct(products),
|
||||
maftol: findMaftolProduct(products),
|
||||
shamsh: findShamshProduct(products),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,17 +35,38 @@ const maftolChartRatio = computed(() => {
|
||||
return 0
|
||||
}
|
||||
|
||||
// lastPrice را track کن تا بعد از polling نسبت بهروز شود
|
||||
void lastPrice.value?.price_last
|
||||
|
||||
const { katod, maftol } = resolveChartProducts(catalogProducts.value)
|
||||
return deriveMaftolChartRatio(maftol, katod)
|
||||
return deriveMetalChartRatio(maftol, katod)
|
||||
})
|
||||
|
||||
/** نسبت شمش مس به کاتد */
|
||||
const shamshChartRatio = computed(() => {
|
||||
if (getHomeLayoutId() !== 'boursecu') {
|
||||
return 0
|
||||
}
|
||||
|
||||
void lastPrice.value?.price_last
|
||||
|
||||
const { katod, shamsh } = resolveChartProducts(catalogProducts.value)
|
||||
return deriveMetalChartRatio(shamsh, katod)
|
||||
})
|
||||
|
||||
const selectedMetalChartRatio = computed(() => {
|
||||
if (selectedChartMetal.value === 'maftol') {
|
||||
return maftolChartRatio.value
|
||||
}
|
||||
if (selectedChartMetal.value === 'shamsh') {
|
||||
return shamshChartRatio.value
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
let chartFetchPromise: Promise<void> | null = null
|
||||
let chartInitialized = false
|
||||
let maftolOffsetPromise: Promise<void> | null = null
|
||||
let maftolOffsetLoaded = false
|
||||
let metalOffsetPromise: Promise<void> | null = null
|
||||
let metalOffsetLoaded = false
|
||||
|
||||
function findKatodProduct(products: Product[]) {
|
||||
return (
|
||||
@@ -60,6 +82,12 @@ function findMaftolProduct(products: Product[]) {
|
||||
)
|
||||
}
|
||||
|
||||
function findShamshProduct(products: Product[]) {
|
||||
return products.find((product) =>
|
||||
isShamshMesGroup(product.group_type, product.title)
|
||||
)
|
||||
}
|
||||
|
||||
function applyChartRatio(basePrice: number, ratio: number) {
|
||||
if (!ratio) {
|
||||
return basePrice
|
||||
@@ -68,12 +96,12 @@ function applyChartRatio(basePrice: number, ratio: number) {
|
||||
return Math.round(basePrice + basePrice * ratio)
|
||||
}
|
||||
|
||||
/** بالاترین/پایینترین امروز — برای boursecu با نسبت مفتول نسبت به کاتد */
|
||||
/** بالاترین/پایینترین امروز — برای boursecu با نسبت مفتول/شمش نسبت به کاتد */
|
||||
export function resolveMetalDayRangePrices(options: {
|
||||
price: { up?: { buy?: number; sell?: number }; buy?: number; sell?: number; price_last?: number } | null | undefined
|
||||
metal: ChartMetalType
|
||||
isBoursecuLayout: boolean
|
||||
maftolRatio: number
|
||||
metalRatio: number
|
||||
}) {
|
||||
const baseHigh = options.price?.up?.buy ?? getTradeBuyPrice(options.price as Parameters<typeof getTradeBuyPrice>[0])
|
||||
const baseLow = options.price?.up?.sell ?? getTradeSellPrice(options.price as Parameters<typeof getTradeSellPrice>[0])
|
||||
@@ -83,37 +111,36 @@ export function resolveMetalDayRangePrices(options: {
|
||||
}
|
||||
|
||||
return {
|
||||
high: applyChartRatio(baseHigh, options.maftolRatio),
|
||||
low: applyChartRatio(baseLow, options.maftolRatio),
|
||||
high: applyChartRatio(baseHigh, options.metalRatio),
|
||||
low: applyChartRatio(baseLow, options.metalRatio),
|
||||
}
|
||||
}
|
||||
|
||||
function deriveMaftolChartRatio(maftolProduct?: Product, katodProduct?: Product) {
|
||||
const maftolOffset = resolvePriceOffsetRatio(maftolProduct?.price_offset_percent)
|
||||
if (maftolOffset) {
|
||||
return maftolOffset
|
||||
function deriveMetalChartRatio(metalProduct?: Product, katodProduct?: Product) {
|
||||
const metalOffset = resolvePriceOffsetRatio(metalProduct?.price_offset_percent)
|
||||
if (metalOffset) {
|
||||
return metalOffset
|
||||
}
|
||||
|
||||
if (!maftolProduct) {
|
||||
if (!metalProduct) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const liveBuy = getTradeBuyPrice(lastPrice.value)
|
||||
const katodLive = katodProduct ? getProductBuyPrice(katodProduct) : liveBuy
|
||||
const maftolLive = getProductBuyPrice(maftolProduct)
|
||||
const metalLive = getProductBuyPrice(metalProduct)
|
||||
|
||||
// وقتی فروشگاه بسته است قیمت لحظهای صفر میشود — از قیمت ثابت کاتالوگ استفاده کن
|
||||
const katodStatic = Number(katodProduct?.price ?? 0) + Number(katodProduct?.price_profit ?? 0)
|
||||
const maftolStatic = Number(maftolProduct.price ?? 0) + Number(maftolProduct.price_profit ?? 0)
|
||||
const metalStatic = Number(metalProduct.price ?? 0) + Number(metalProduct.price_profit ?? 0)
|
||||
|
||||
const referencePrice = katodLive || liveBuy || katodStatic
|
||||
const maftolPrice = maftolLive || maftolStatic
|
||||
const derivedMetalPrice = metalLive || metalStatic
|
||||
|
||||
if (!referencePrice || !maftolPrice || referencePrice === maftolPrice) {
|
||||
if (!referencePrice || !derivedMetalPrice || referencePrice === derivedMetalPrice) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return (maftolPrice - referencePrice) / referencePrice
|
||||
return (derivedMetalPrice - referencePrice) / referencePrice
|
||||
}
|
||||
|
||||
function normalizeChartTimestamp(timestamp: number) {
|
||||
@@ -147,18 +174,19 @@ function buildChartPoints(items: Array<{ timestamp: number; value: number }>): C
|
||||
}
|
||||
|
||||
function syncChartSeries() {
|
||||
const ratio = selectedMetalChartRatio.value
|
||||
const data =
|
||||
getHomeLayoutId() === 'boursecu' && selectedChartMetal.value === 'maftol'
|
||||
getHomeLayoutId() === 'boursecu' && selectedChartMetal.value !== 'katod'
|
||||
? rawChartPoints.value.map(
|
||||
([timestamp, value]) =>
|
||||
[timestamp, applyChartRatio(value, maftolChartRatio.value)] as ChartPoint
|
||||
[timestamp, applyChartRatio(value, ratio)] as ChartPoint
|
||||
)
|
||||
: rawChartPoints.value
|
||||
|
||||
priceChartSeries.value[0].data = data
|
||||
}
|
||||
|
||||
watch([selectedChartMetal, maftolChartRatio], () => {
|
||||
watch([selectedChartMetal, maftolChartRatio, shamshChartRatio], () => {
|
||||
syncChartSeries()
|
||||
})
|
||||
|
||||
@@ -192,34 +220,34 @@ async function fetchPrices(force = false) {
|
||||
return chartFetchPromise
|
||||
}
|
||||
|
||||
async function loadMaftolOffset(force = false) {
|
||||
async function loadMetalChartOffsets(force = false) {
|
||||
if (getHomeLayoutId() !== 'boursecu') {
|
||||
syncChartSeries()
|
||||
return
|
||||
}
|
||||
|
||||
if (!force && maftolOffsetLoaded && catalogProducts.value.length > 0) {
|
||||
if (!force && metalOffsetLoaded && catalogProducts.value.length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!force && maftolOffsetPromise) {
|
||||
return maftolOffsetPromise
|
||||
if (!force && metalOffsetPromise) {
|
||||
return metalOffsetPromise
|
||||
}
|
||||
|
||||
maftolOffsetPromise = (async () => {
|
||||
metalOffsetPromise = (async () => {
|
||||
try {
|
||||
await getProductsCatalog(force)
|
||||
syncChartSeries()
|
||||
maftolOffsetLoaded = true
|
||||
metalOffsetLoaded = true
|
||||
} catch (error) {
|
||||
console.error('Error loading maftol chart ratio:', error)
|
||||
console.error('Error loading boursecu chart metal ratios:', error)
|
||||
syncChartSeries()
|
||||
} finally {
|
||||
maftolOffsetPromise = null
|
||||
metalOffsetPromise = null
|
||||
}
|
||||
})()
|
||||
|
||||
return maftolOffsetPromise
|
||||
return metalOffsetPromise
|
||||
}
|
||||
|
||||
function updateChartData(timeline: string) {
|
||||
@@ -230,8 +258,8 @@ function updateChartData(timeline: string) {
|
||||
function setChartMetal(metal: ChartMetalType) {
|
||||
selectedChartMetal.value = metal
|
||||
|
||||
if (metal === 'maftol' && getHomeLayoutId() === 'boursecu' && !catalogProducts.value.length) {
|
||||
void loadMaftolOffset(true)
|
||||
if (metal !== 'katod' && getHomeLayoutId() === 'boursecu' && !catalogProducts.value.length) {
|
||||
void loadMetalChartOffsets(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,9 +270,12 @@ export function usePriceChart() {
|
||||
chartLoading,
|
||||
selectedChartMetal,
|
||||
maftolChartRatio,
|
||||
shamshChartRatio,
|
||||
selectedMetalChartRatio,
|
||||
updateChartData,
|
||||
fetchPrices,
|
||||
loadMaftolOffset,
|
||||
loadMaftolOffset: loadMetalChartOffsets,
|
||||
loadMetalChartOffsets,
|
||||
setChartMetal,
|
||||
}
|
||||
}
|
||||
@@ -254,7 +285,7 @@ export function fetchHomeChartPrices(force = false) {
|
||||
}
|
||||
|
||||
export function loadHomeMaftolOffset(force = false) {
|
||||
return loadMaftolOffset(force)
|
||||
return loadMetalChartOffsets(force)
|
||||
}
|
||||
|
||||
export function resetHomePriceChart() {
|
||||
@@ -264,6 +295,6 @@ export function resetHomePriceChart() {
|
||||
selectedChartMetal.value = 'katod'
|
||||
chartInitialized = false
|
||||
chartFetchPromise = null
|
||||
maftolOffsetLoaded = false
|
||||
maftolOffsetPromise = null
|
||||
metalOffsetLoaded = false
|
||||
metalOffsetPromise = null
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
import { computed, inject } from 'vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { usePriceWithoutType } from '@/utils/usePrice'
|
||||
import { isBoursecuProject } from '@/utils/config'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { HOME_PAGE_CONTEXT_KEY } from '../../composables/useHomePage'
|
||||
|
||||
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
|
||||
const userStore = useUserStore()
|
||||
const isBoursecu = isBoursecuProject()
|
||||
|
||||
const showLimitedPrice = computed(
|
||||
() =>
|
||||
@@ -28,7 +30,7 @@ const limitedPriceText = computed(() =>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="ctx.showClosedMessage.value"
|
||||
v-if="!isBoursecu && ctx.showClosedMessage.value"
|
||||
class="dhome-alert dhome-alert--info"
|
||||
v-html="ctx.closedMessage.value"
|
||||
/>
|
||||
|
||||
@@ -1,26 +1,47 @@
|
||||
<template>
|
||||
<div class="dhome-surface dhome-hero-price">
|
||||
<span class="dhome-label">قیمت لحظهای</span>
|
||||
<p class="dhome-hero-price__brand">{{ config.config?.name_fa }}</p>
|
||||
<div class="dhome-hero-price__value-row">
|
||||
<span
|
||||
v-if="percentChange !== 0"
|
||||
class="dhome-hero-price__delta"
|
||||
:class="percentChange > 0 ? 'dhome-hero-price__delta--up' : 'dhome-hero-price__delta--down'"
|
||||
>
|
||||
<BasicIcon
|
||||
:icon="percentChange > 0 ? 'vuesax-linear:arrow-up-2' : 'vuesax-linear:arrow-down-2'"
|
||||
size="14"
|
||||
/>
|
||||
<span class="dhome-hero-price__delta-value">{{ formatSignedPercentChange(percentChange) }}</span>
|
||||
<div
|
||||
class="dhome-surface dhome-hero-price"
|
||||
:class="{
|
||||
'dhome-hero-price--boursecu-open': isBoursecu && !showStoreClosed,
|
||||
'dhome-hero-price--boursecu-closed': isBoursecu && showStoreClosed,
|
||||
}"
|
||||
>
|
||||
<template v-if="isBoursecu && showStoreClosed">
|
||||
<div class="dhome-hero-price__closed">
|
||||
<div class="dhome-hero-price__closed-icon">
|
||||
<BasicIcon icon="vuesax-linear:lock" size="22" color="#ffffff" />
|
||||
</div>
|
||||
<p class="dhome-hero-price__closed-title">فروشگاه بسته است</p>
|
||||
<p class="dhome-hero-price__closed-hint">{{ storeClosedHint }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<span class="dhome-label" :class="{ 'dhome-label--boursecu': isBoursecu }">
|
||||
{{ isBoursecu ? 'قیمت لحظهای کاتد مس' : 'قیمت لحظهای' }}
|
||||
</span>
|
||||
<Vue3Odometer
|
||||
:value="displayGoldVal"
|
||||
class="dhome-hero-price__odometer o-text"
|
||||
style="font-family: iran-sans"
|
||||
/>
|
||||
<span class="dhome-hero-price__unit">ریال</span>
|
||||
</div>
|
||||
<p v-if="!isBoursecu" class="dhome-hero-price__brand">{{ config.config?.name_fa }}</p>
|
||||
<div class="dhome-hero-price__value-row">
|
||||
<span
|
||||
v-if="percentChange !== 0"
|
||||
class="dhome-hero-price__delta"
|
||||
:class="percentChange > 0 ? 'dhome-hero-price__delta--up' : 'dhome-hero-price__delta--down'"
|
||||
>
|
||||
<BasicIcon
|
||||
:icon="percentChange > 0 ? 'vuesax-linear:arrow-up-2' : 'vuesax-linear:arrow-down-2'"
|
||||
size="14"
|
||||
/>
|
||||
<span class="dhome-hero-price__delta-value">{{ formatSignedPercentChange(percentChange) }}</span>
|
||||
</span>
|
||||
<Vue3Odometer
|
||||
:value="displayGoldVal"
|
||||
class="dhome-hero-price__odometer o-text"
|
||||
:class="{ 'dhome-hero-price__odometer--boursecu': isBoursecu }"
|
||||
style="font-family: iran-sans"
|
||||
/>
|
||||
<span class="dhome-hero-price__unit" :class="{ 'dhome-hero-price__unit--boursecu': isBoursecu }">ریال</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -30,17 +51,33 @@ import Vue3Odometer from 'vue3-odometer'
|
||||
import 'odometer/themes/odometer-theme-default.css'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { isBoursecuProject } from '@/utils/config'
|
||||
import { getTradeBuyPrice, lastPrice, percentPriceChanged } from '@/utils/price'
|
||||
import { getStoreClosedMessage, isStoreClosed } from '@/utils/storeHours'
|
||||
import { formatSignedPercentChange } from '@/utils/usePrice'
|
||||
|
||||
const config = useConfigStore()
|
||||
const userStore = useUserStore()
|
||||
const isBoursecu = isBoursecuProject()
|
||||
|
||||
const isMarketClosed = computed(
|
||||
() => lastPrice?.value?.goldir === 0 && getTradeBuyPrice(lastPrice.value) === 0,
|
||||
)
|
||||
const showStoreClosed = computed(() => {
|
||||
if (isBoursecu) {
|
||||
return isStoreClosed()
|
||||
}
|
||||
return lastPrice?.value?.goldir === 0 && getTradeBuyPrice(lastPrice.value) === 0
|
||||
})
|
||||
|
||||
const storeClosedHint = computed(() => {
|
||||
const custom = userStore.getValueByKey('closed_message')
|
||||
if (typeof custom === 'string' && custom.trim()) {
|
||||
return custom.replace(/<[^>]*>/g, '').trim()
|
||||
}
|
||||
return getStoreClosedMessage()
|
||||
})
|
||||
|
||||
const displayGoldVal = computed(() => {
|
||||
if (isMarketClosed.value) return 0
|
||||
if (showStoreClosed.value) return 0
|
||||
return getTradeBuyPrice(lastPrice.value)
|
||||
})
|
||||
|
||||
@@ -49,3 +86,61 @@ const percentChange = computed(() => {
|
||||
return percentPriceChanged()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.dhome-hero-price--boursecu-open {
|
||||
background: linear-gradient(135deg, #c98a4a 0%, #a86528 100%);
|
||||
border-color: transparent;
|
||||
box-shadow: 0 4px 14px rgba(168, 101, 40, 0.28);
|
||||
}
|
||||
|
||||
.dhome-hero-price--boursecu-closed {
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
border-color: transparent;
|
||||
box-shadow: 0 4px 14px rgba(45, 55, 72, 0.22);
|
||||
}
|
||||
|
||||
.dhome-label--boursecu {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.dhome-hero-price__odometer--boursecu {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.dhome-hero-price__unit--boursecu {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.dhome-hero-price__closed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dhome-hero-price__closed-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dhome-hero-price__closed-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.dhome-hero-price__closed-hint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { usePriceWithoutType } from '@/utils/usePrice'
|
||||
import { getHomeLayoutId } from '../../layouts/registry'
|
||||
import { resolveMetalDayRangePrices, usePriceChart } from '../../composables/usePriceChart'
|
||||
|
||||
const { selectedChartMetal, maftolChartRatio } = usePriceChart()
|
||||
const { selectedChartMetal, selectedMetalChartRatio } = usePriceChart()
|
||||
const isBoursecuLayout = computed(() => getHomeLayoutId() === 'boursecu')
|
||||
|
||||
const rangePrices = computed(() =>
|
||||
@@ -13,7 +13,7 @@ const rangePrices = computed(() =>
|
||||
price: price.value,
|
||||
metal: selectedChartMetal.value,
|
||||
isBoursecuLayout: isBoursecuLayout.value,
|
||||
maftolRatio: maftolChartRatio.value,
|
||||
metalRatio: selectedMetalChartRatio.value,
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { isBoursecuProject } from '@/utils/config'
|
||||
import MarketClosedMessage from '../../components/MarketClosedMessage.vue'
|
||||
import { HOME_PAGE_CONTEXT_KEY } from '../../composables/useHomePage'
|
||||
|
||||
@@ -8,7 +9,7 @@ const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
|
||||
|
||||
<template>
|
||||
<MarketClosedMessage
|
||||
v-if="ctx.showClosedMessage.value"
|
||||
v-if="!isBoursecuProject() && ctx.showClosedMessage.value"
|
||||
:message="ctx.closedMessage.value as string"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
<div>{{ totalWeightDisplay }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="isCountBasedProductItem && hasProductUnitWeight(props?.ProductItem)"
|
||||
v-if="shouldShowPerUnitWeightHint(props?.ProductItem)"
|
||||
class="flex justify-between text-xs text-gray-500"
|
||||
>
|
||||
<div>وزن هر عدد</div>
|
||||
@@ -284,6 +284,7 @@ import type { Transaction } from '@/models/transaction'
|
||||
import type { TableEngineInput, TableOutput } from '@/models/table'
|
||||
import { getTransactionTable } from '@/api/transaction'
|
||||
import { getLastPrices, getTradeBuyPrice, getTradeSellPrice, lastPrice, setIntervalGetAllPrice, stopPriceInterval } from '@/utils/price'
|
||||
import { getProductBuyPrice, getProductSellPrice, isProductPriceInactive } from '@/utils/productPrice'
|
||||
import { Product } from '@/models/products'
|
||||
import BasicSelect from '@/components/BasicPacks/BasicSelect.vue'
|
||||
import Bg_main from '@/components/bg_main.vue'
|
||||
@@ -311,6 +312,7 @@ import {
|
||||
getProductUnitWeightLabel,
|
||||
hasProductUnitWeight,
|
||||
isCountBasedProduct,
|
||||
shouldShowPerUnitWeightHint,
|
||||
shouldShowProductTotalWeight,
|
||||
} from '@/utils/productDealMethod'
|
||||
import {
|
||||
@@ -468,8 +470,11 @@ const disableTransaction = computed(() =>
|
||||
: (form.value?.goldValue ?? 0) < 0.001)) ||
|
||||
(maxOrderCash.value != null && (form.value?.goldCash ?? 0) > maxOrderCash.value) ||
|
||||
cashValidationMessage.value !== '' ||
|
||||
!lastPrice.value ||
|
||||
(isBuy.value ? getTradeBuyPrice(lastPrice.value) <= 0 : getTradeSellPrice(lastPrice.value) <= 0))
|
||||
!props.ProductItem ||
|
||||
isProductPriceInactive(props.ProductItem) ||
|
||||
(isBuy.value
|
||||
? getProductBuyPrice(props.ProductItem) <= 0
|
||||
: getProductSellPrice(props.ProductItem) <= 0))
|
||||
|
||||
function refreshTable() {
|
||||
sellItems.value = []
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<template>
|
||||
<div class="flex bg-white border-1 border-gray-200 rounded-lg"
|
||||
:class="isClickable ? 'bg-white opacity-100' : 'bg-gray-200 opacity-50 cursor-not-allowed'">
|
||||
<div class="bg-gray-100 m-2 rounded-lg w-28 h-28 flex justify-center p-2">
|
||||
<div class="bg-gray-100 m-1.5 rounded-lg w-24 h-24 flex justify-center p-1.5 shrink-0">
|
||||
<img :src="displayImage" class="object-contain w-full h-full" alt="">
|
||||
</div>
|
||||
<div class="m-1 flex flex-col gap-0 justify-center leading-tight">
|
||||
<div class="text-base font-500">{{ props.ProductItem.title }}</div>
|
||||
<div class="text-base font-500">{{ usePrice(getProductBuyPrice(props.ProductItem)) }}</div>
|
||||
<div class="text-base font-500">
|
||||
<template v-if="props.inactive">بدون قیمت</template>
|
||||
<template v-else>{{ usePrice(getProductBuyPrice(props.ProductItem)) }}</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="absolute inset-0 bg-cover bg-center opacity-60"></div>
|
||||
<div class="absolute inset-0 bg-cover bg-center opacity-60 pointer-events-none"></div>
|
||||
<MobilePageHeader title="تراکنش ها">
|
||||
<template #trailing>
|
||||
<BasicIcon icon="vuesax-linear:filter" @click="showFilter = true" />
|
||||
@@ -8,7 +8,7 @@
|
||||
</MobilePageHeader>
|
||||
|
||||
<!-- <div ref="el" :class="loading ? 'h-screen' : ''" class="px-3 mpage-content pb-10">-->
|
||||
<div ref="el" class="transactions-page-body px-3 pt-16">
|
||||
<div ref="el" class="transactions-page-body px-3 pt-16 pb-24">
|
||||
<list-anim v-if="!loading">
|
||||
<Item v-for="item in items" :key="item.id" :item="item" />
|
||||
<div :ref="bindMobileObserver" v-show="!loading" class="w-full opacity-0 relative">
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
<div v-if="item.order" :class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'">
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">
|
||||
قیمت هر گرم {{ getTradePriceUnitLabel() }} حین معامله:
|
||||
{{ getTransactionTradePriceLabel(item.wallet?.product) }} حین معامله:
|
||||
</div>
|
||||
<div :class="['flex text-xs items-center number-ltr', variant === 'desktop' ? 'dtxn-detail__value' : '']">
|
||||
<div class="text-base ml-1">{{ usePriceWithoutType(item.order?.price ?? 0) }}</div>
|
||||
@@ -38,10 +38,10 @@
|
||||
{{ item.unit }}
|
||||
</div>
|
||||
<div
|
||||
v-if="item.wallet?.product && getProductTotalWeightLabel(Math.abs(item.gold ?? 0), item.wallet.product)"
|
||||
v-if="shouldShowTransactionTotalWeightLine(item.wallet?.product)"
|
||||
class="text-gray-500"
|
||||
>
|
||||
{{ getProductTotalWeightLabel(Math.abs(item.gold ?? 0), item.wallet.product) }}
|
||||
{{ getProductTotalWeightLabel(Math.abs(item.gold ?? 0), item.wallet?.product) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,8 +116,9 @@ import {
|
||||
getProductQuantityUnit,
|
||||
getProductTotalWeightLabel,
|
||||
getProductUnitWeightLabel,
|
||||
getTradePriceUnitLabel,
|
||||
getTransactionTradePriceLabel,
|
||||
getWeightUnit,
|
||||
shouldShowTransactionTotalWeightLine,
|
||||
} from '@/utils/productDealMethod'
|
||||
import { toJalaliNotime, toTime } from '@/utils/useDateTime'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user