fix ذشزنلقخعدی

This commit is contained in:
2026-06-23 16:02:41 +03:30
parent 29c58c4202
commit ac28eda09f
29 changed files with 624 additions and 167 deletions
+4
View File
@@ -276,6 +276,10 @@
var tenantSecondary = normalizeHex(user.secondary_color) var tenantSecondary = normalizeHex(user.secondary_color)
var tenantBackground = normalizeHex(user.background_color) var tenantBackground = normalizeHex(user.background_color)
if (user.name === 'boursecu') {
tenantColor = '#2178b5'
}
/** /**
* مهم: * مهم:
* background_color بک‌گراند اپ است. * background_color بک‌گراند اپ است.
+7 -6
View File
@@ -99,7 +99,7 @@ import {LoadingOutlined} from '@ant-design/icons-vue'
import img_bottomGradiant from '@/views/login/assets/img/gr_bottom_white.svg' import img_bottomGradiant from '@/views/login/assets/img/gr_bottom_white.svg'
import BasicButton from '@/components/BasicPacks/BasicButton.vue' import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import {useConfigStore} from "@/store/configStore"; 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 {ConfigProvider as AConfigProvider} from 'ant-design-vue'
import BasicIcon from "@/components/BasicPacks/BasicIcon.vue"; import BasicIcon from "@/components/BasicPacks/BasicIcon.vue";
import { useApplyDesktopLayoutClass, useDesktopBreakpoint } from '@/layouts/composables/useAppLayout' import { useApplyDesktopLayoutClass, useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
@@ -211,16 +211,17 @@ function handleSupportPointerUp(event: PointerEvent) {
} }
const antTheme = computed(() => { const antTheme = computed(() => {
if (!config.config?.color) return null const primaryColor = getProjectPrimaryColor()
if (!primaryColor) return null
const brandRed = getProjectBrandRed() const brandRed = getProjectBrandRed()
return { return {
token: { token: {
colorPrimary: config.config.color, colorPrimary: primaryColor,
colorLink: config.config.color, colorLink: primaryColor,
colorLinkActive: config.config.color, colorLinkActive: primaryColor,
colorLinkHover: config.config.color, colorLinkHover: primaryColor,
fontFamily: getProjectFontFamily(), fontFamily: getProjectFontFamily(),
...(brandRed ? { ...(brandRed ? {
colorError: brandRed, colorError: brandRed,
+2 -2
View File
@@ -5,7 +5,7 @@ import { startPwaManifest } from '@/modules/pwa/manifest'
import { createVueApp } from '@/app/createApp' import { createVueApp } from '@/app/createApp'
import { setupRouterGuards } from '@/app/setupRouter' import { setupRouterGuards } from '@/app/setupRouter'
import router from '@/router' import router from '@/router'
import { applyProjectTheme } from '@/utils/config' import { applyProjectTheme, isBoursecuProject } from '@/utils/config'
import { useSettingsStore } from '@/store/settingsStore' import { useSettingsStore } from '@/store/settingsStore'
import { useUserStore } from '@/store/userStore' import { useUserStore } from '@/store/userStore'
import { getToken } from '@/utils' import { getToken } from '@/utils'
@@ -71,7 +71,7 @@ function applyTenantThemeVars() {
const secondaryColor = window.__APP_CONFIG__?.user?.secondary_color const secondaryColor = window.__APP_CONFIG__?.user?.secondary_color
const backgroundColor = window.__APP_CONFIG__?.user?.background_color const backgroundColor = window.__APP_CONFIG__?.user?.background_color
if (color) { if (color && !isBoursecuProject()) {
document.documentElement.style.setProperty('--App-primary-color', color) document.documentElement.style.setProperty('--App-primary-color', color)
} }
+13
View File
@@ -1,4 +1,17 @@
html.project-boursecu { 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 { .ant-input-group-addon {
background-color: @secondary-color !important; background-color: @secondary-color !important;
color: @App-primary-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

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