fix(price): apply live trading-hours settings without refresh
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,6 +17,7 @@ export interface AppSetting {
|
||||
export const useSettingsStore = defineStore('settings', () => {
|
||||
const settings = ref<AppSetting[]>([])
|
||||
const loaded = ref(false)
|
||||
const settingsRevision = ref(0)
|
||||
let fetchPromise: Promise<AppSetting[]> | null = null
|
||||
|
||||
function getSettingByKey(key: string) {
|
||||
@@ -42,6 +43,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
}
|
||||
|
||||
const merged = [...settings.value]
|
||||
let changed = false
|
||||
items.forEach((item) => {
|
||||
if (!item?.key) {
|
||||
return
|
||||
@@ -49,13 +51,25 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
|
||||
const index = merged.findIndex((setting) => setting.key === item.key)
|
||||
if (index >= 0) {
|
||||
merged[index] = { ...merged[index], ...item }
|
||||
} else {
|
||||
merged.push(item)
|
||||
const current = merged[index]
|
||||
if (current.value === item.value && current.is_active === item.is_active) {
|
||||
return
|
||||
}
|
||||
merged[index] = { ...current, ...item }
|
||||
changed = true
|
||||
return
|
||||
}
|
||||
|
||||
merged.push(item)
|
||||
changed = true
|
||||
})
|
||||
|
||||
if (!changed) {
|
||||
return
|
||||
}
|
||||
|
||||
settings.value = merged
|
||||
settingsRevision.value += 1
|
||||
}
|
||||
|
||||
async function fetchSettings(force = false) {
|
||||
@@ -89,6 +103,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
return {
|
||||
settings,
|
||||
loaded,
|
||||
settingsRevision,
|
||||
fetchSettings,
|
||||
mergeSettings,
|
||||
getSettingByKey,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
shouldUsePricePolling,
|
||||
} from '@/utils/priceSocketState'
|
||||
import { applyBoardSpecialToLastPayload } from '@/utils/specialPricing'
|
||||
import { useSettingsStore, type AppSetting } from '@/store/settingsStore'
|
||||
|
||||
const LAST_INTERVAL_MS = 10000
|
||||
|
||||
@@ -47,6 +48,19 @@ function hasValidPricePayload(data?: Price | null): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
function applyTradingHoursSettingsPatch(data: unknown) {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
const patch = (data as { settings_patch?: unknown }).settings_patch
|
||||
if (!Array.isArray(patch) || patch.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
useSettingsStore().mergeSettings(patch as AppSetting[])
|
||||
}
|
||||
|
||||
export function markPriceSourceReceived() {
|
||||
lastPriceSourceAt = Date.now()
|
||||
consecutivePriceFetchFailures = 0
|
||||
@@ -170,6 +184,7 @@ export async function getLastPrices(): Promise<boolean> {
|
||||
}
|
||||
|
||||
export function applyLastPricePayload(dataLast: Price) {
|
||||
applyTradingHoursSettingsPatch(dataLast)
|
||||
const normalized = normalizePriceResponse(applyBoardSpecialToLastPayload(dataLast as any) as Price)
|
||||
const nextBuy = getTradeBuyPrice(normalized)
|
||||
const prevBuy = getTradeBuyPrice(lastPrice.value)
|
||||
|
||||
@@ -399,13 +399,40 @@ async function applyLastFromStream(last: Price) {
|
||||
applyLastPricePayload(last)
|
||||
}
|
||||
|
||||
async function applySettingsFromStream(payload: unknown) {
|
||||
const items = Array.isArray(payload)
|
||||
? payload
|
||||
: payload && typeof payload === 'object' && Array.isArray((payload as { items?: unknown }).items)
|
||||
? (payload as { items: unknown[] }).items
|
||||
: null
|
||||
if (!items?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { useSettingsStore } = await import('@/store/settingsStore')
|
||||
const { touchStoreHoursSettings } = await import('@/utils/storeHours')
|
||||
useSettingsStore().mergeSettings(items as Array<{ key?: string; value?: unknown }>)
|
||||
touchStoreHoursSettings()
|
||||
} catch (error) {
|
||||
console.warn('Settings SSE apply failed', error)
|
||||
}
|
||||
}
|
||||
|
||||
function handleLastPayload(payload: unknown) {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
const last = applyBoardSpecialToLastPayload(payload as Price)
|
||||
if (last.price_last == null && last.price == null && last.buy == null) {
|
||||
const raw = payload as Price & { settings_patch?: unknown }
|
||||
const last = applyBoardSpecialToLastPayload(raw)
|
||||
const hasQuote = last.price_last != null || last.price != null || last.buy != null
|
||||
const hasStatus = last.status != null || typeof last.closed === 'boolean'
|
||||
if (!hasQuote && !hasStatus) {
|
||||
if (Array.isArray(raw.settings_patch) && raw.settings_patch.length) {
|
||||
touchStreamActivity()
|
||||
void applySettingsFromStream(raw.settings_patch)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -413,6 +440,11 @@ function handleLastPayload(payload: unknown) {
|
||||
void applyLastFromStream(last)
|
||||
}
|
||||
|
||||
function handleSettingsPayload(payload: unknown) {
|
||||
touchStreamActivity()
|
||||
void applySettingsFromStream(payload)
|
||||
}
|
||||
|
||||
function handleListPayload(payload: unknown) {
|
||||
if (!Array.isArray(payload) || !payload.length) {
|
||||
return
|
||||
@@ -467,6 +499,14 @@ function bindStreamEvents(source: AuthPriceStream) {
|
||||
}
|
||||
})
|
||||
|
||||
source.addEventListener('settings', (event) => {
|
||||
try {
|
||||
handleSettingsPayload(JSON.parse(event.data))
|
||||
} catch (error) {
|
||||
console.warn('Price SSE settings parse failed', error)
|
||||
}
|
||||
})
|
||||
|
||||
source.addEventListener('list', (event) => {
|
||||
try {
|
||||
handleListPayload(JSON.parse(event.data))
|
||||
|
||||
+140
-40
@@ -1,6 +1,6 @@
|
||||
import { ref } from 'vue'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@/utils/price'
|
||||
import { lastPrice } from '@/utils/price'
|
||||
|
||||
export interface StoreTradingHours {
|
||||
start: string
|
||||
@@ -12,8 +12,24 @@ export interface EffectiveTradingHours {
|
||||
start: string
|
||||
end: string
|
||||
enforced: boolean
|
||||
dayEnabled: boolean
|
||||
}
|
||||
|
||||
type WeekdayKey = 'sat' | 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri'
|
||||
|
||||
type DaySchedule = {
|
||||
enabled: boolean
|
||||
start: string
|
||||
end: string
|
||||
}
|
||||
|
||||
type WeeklySchedule = {
|
||||
mode: 'uniform' | 'per_day'
|
||||
days: Record<WeekdayKey, DaySchedule>
|
||||
}
|
||||
|
||||
const WEEKDAY_BY_JS: WeekdayKey[] = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
|
||||
|
||||
const productTradingHours = ref<{ start: string; end: string } | null>(null)
|
||||
const productTradingHoursVersion = ref(0)
|
||||
|
||||
@@ -54,6 +70,11 @@ function hasProductTradingWindow(start?: string | null, end?: string | null): bo
|
||||
return open !== close
|
||||
}
|
||||
|
||||
/** بعد از merge زندهٔ تنظیمات ساعت کاری — computedهای صفحه اصلی دوباره حساب شوند */
|
||||
export function touchStoreHoursSettings() {
|
||||
productTradingHoursVersion.value += 1
|
||||
}
|
||||
|
||||
export function setProductTradingHours(start?: string | null, end?: string | null) {
|
||||
if (!hasProductTradingWindow(start, end)) {
|
||||
productTradingHours.value = null
|
||||
@@ -81,29 +102,77 @@ export function syncProductTradingHoursFromProducts(
|
||||
setProductTradingHours(null, null)
|
||||
}
|
||||
|
||||
export function getStoreTradingHours(): StoreTradingHours {
|
||||
const settings = useSettingsStore()
|
||||
const closeRaw = settings.getSettingByKey('close_price')
|
||||
const closePriceEnabled =
|
||||
closeRaw === true ||
|
||||
closeRaw === 1 ||
|
||||
String(closeRaw ?? 'false').trim().toLowerCase() === 'true'
|
||||
function weekdayKey(now: Date): WeekdayKey {
|
||||
return WEEKDAY_BY_JS[now.getDay()] ?? 'sat'
|
||||
}
|
||||
|
||||
return {
|
||||
start: normalizeTradingTimeSetting(settings.getSettingByKey('startDay'), '00:00'),
|
||||
end: normalizeTradingTimeSetting(settings.getSettingByKey('endDay'), '23:59'),
|
||||
closePriceEnabled,
|
||||
function parseWeeklySchedule(raw: unknown): WeeklySchedule | null {
|
||||
const text = String(raw ?? '').trim()
|
||||
if (!text) return null
|
||||
try {
|
||||
const parsed = JSON.parse(text) as Partial<WeeklySchedule>
|
||||
if (!parsed?.days || typeof parsed.days !== 'object') return null
|
||||
const mode = parsed.mode === 'per_day' ? 'per_day' : 'uniform'
|
||||
const defaults = getStoreTradingHours()
|
||||
const keys: WeekdayKey[] = ['sat', 'sun', 'mon', 'tue', 'wed', 'thu', 'fri']
|
||||
const days = {} as Record<WeekdayKey, DaySchedule>
|
||||
for (const key of keys) {
|
||||
const row = parsed.days[key]
|
||||
days[key] = {
|
||||
enabled: row?.enabled !== false,
|
||||
start: normalizeTradingTimeSetting(row?.start, defaults.start),
|
||||
end: normalizeTradingTimeSetting(row?.end, defaults.end),
|
||||
}
|
||||
}
|
||||
return { mode, days }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function getEffectiveTradingHours(): EffectiveTradingHours {
|
||||
function isOutsideTimeWindow(now: Date, start: string, end: string): boolean {
|
||||
const currentSeconds = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds()
|
||||
const openSeconds = parseTimeToSeconds(start)
|
||||
const closeSeconds = parseTimeToSeconds(end)
|
||||
if (closeSeconds <= openSeconds) {
|
||||
return currentSeconds < openSeconds && currentSeconds > closeSeconds
|
||||
}
|
||||
return currentSeconds < openSeconds || currentSeconds > closeSeconds
|
||||
}
|
||||
|
||||
export function getStoreTradingHours(): StoreTradingHours {
|
||||
const settings = useSettingsStore()
|
||||
void settings.settingsRevision
|
||||
return {
|
||||
start: normalizeTradingTimeSetting(settings.getSettingByKey('startDay'), '00:00'),
|
||||
end: normalizeTradingTimeSetting(settings.getSettingByKey('endDay'), '23:59'),
|
||||
closePriceEnabled: settings.isSettingTruthy('close_price'),
|
||||
}
|
||||
}
|
||||
|
||||
export function getEffectiveTradingHours(now = new Date()): EffectiveTradingHours {
|
||||
void productTradingHoursVersion.value
|
||||
const settings = getStoreTradingHours()
|
||||
const weekly = parseWeeklySchedule(useSettingsStore().getSettingByKey('trading_hours_by_day'))
|
||||
|
||||
if (settings.closePriceEnabled && weekly?.mode === 'per_day') {
|
||||
const day = weekly.days[weekdayKey(now)]
|
||||
if (day) {
|
||||
return {
|
||||
start: day.start,
|
||||
end: day.end,
|
||||
enforced: true,
|
||||
dayEnabled: day.enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.closePriceEnabled) {
|
||||
return {
|
||||
start: settings.start,
|
||||
end: settings.end,
|
||||
enforced: true,
|
||||
dayEnabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +181,7 @@ export function getEffectiveTradingHours(): EffectiveTradingHours {
|
||||
start: productTradingHours.value.start,
|
||||
end: productTradingHours.value.end,
|
||||
enforced: true,
|
||||
dayEnabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,20 +189,40 @@ export function getEffectiveTradingHours(): EffectiveTradingHours {
|
||||
start: settings.start,
|
||||
end: settings.end,
|
||||
enforced: false,
|
||||
dayEnabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
export function isStoreClosedBySchedule(now = new Date()): boolean {
|
||||
const { start, end, enforced } = getEffectiveTradingHours()
|
||||
if (!enforced) {
|
||||
/** خارج از ساعت کاری سراسری — فقط وقتی close_price روشن باشد (صفحه اصلی) */
|
||||
export function isOutsideGlobalStoreHours(now = new Date()): boolean {
|
||||
const settings = getStoreTradingHours()
|
||||
if (!settings.closePriceEnabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
const currentSeconds = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds()
|
||||
const openSeconds = parseTimeToSeconds(start)
|
||||
const closeSeconds = parseTimeToSeconds(end)
|
||||
const weekly = parseWeeklySchedule(useSettingsStore().getSettingByKey('trading_hours_by_day'))
|
||||
if (weekly?.mode === 'per_day') {
|
||||
const day = weekly.days[weekdayKey(now)]
|
||||
if (day && !day.enabled) {
|
||||
return true
|
||||
}
|
||||
if (day) {
|
||||
return isOutsideTimeWindow(now, day.start, day.end)
|
||||
}
|
||||
}
|
||||
|
||||
return currentSeconds < openSeconds || currentSeconds > closeSeconds
|
||||
return isOutsideTimeWindow(now, settings.start, settings.end)
|
||||
}
|
||||
|
||||
export function isStoreClosedBySchedule(now = new Date()): boolean {
|
||||
const { start, end, enforced, dayEnabled } = getEffectiveTradingHours(now)
|
||||
if (!enforced) {
|
||||
return false
|
||||
}
|
||||
if (!dayEnabled) {
|
||||
return true
|
||||
}
|
||||
return isOutsideTimeWindow(now, start, end)
|
||||
}
|
||||
|
||||
export function isStoreClosed(): boolean {
|
||||
@@ -167,35 +257,45 @@ export type PriceDisplayState = 'open' | 'closed' | 'no_quote'
|
||||
|
||||
export const NO_QUOTE_MESSAGE = 'مظنه در دسترس نیست'
|
||||
|
||||
/** سوییچ: خارج از ساعت کاری هم قیمت صفحه اصلی نشان داده شود */
|
||||
export function shouldShowHomePriceWhenClosed(): boolean {
|
||||
return useSettingsStore().isSettingTruthy('show_home_price_when_closed')
|
||||
}
|
||||
|
||||
export function shouldShowLastSavedPrice(): boolean {
|
||||
return useSettingsStore().isSettingTruthy('show_last_price')
|
||||
}
|
||||
|
||||
function hasShowableHomePrice(p = lastPrice.value): boolean {
|
||||
if (!p) return false
|
||||
if (Number(p.buy) > 0 || Number(p.sell) > 0 || Number(p.price_last) > 0 || Number(p.price) > 0) {
|
||||
return true
|
||||
}
|
||||
if (shouldShowLastSavedPrice() && Number((p as { last_historical?: number }).last_historical ?? 0) > 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* وضعیت نمایش قیمت در صفحه اصلی:
|
||||
* - closed → فروشگاه بسته است (خارج از ساعت کاری close_price یا بازه محصول)
|
||||
* - no_quote → مظنه در دسترس نیست (قیمت زنده نداریم و show_last_price خاموش است)
|
||||
* - open → نمایش عادی قیمت
|
||||
*
|
||||
* اولویت با status ارسالی از سرور است؛ اگر نبود به منطق سمت کلاینت برمیگردیم.
|
||||
* وضعیت نمایش قیمت در صفحه اصلی — همان منطق بکاند:
|
||||
* close_price خاموش / داخل ساعت → قیمت هست open، وگرنه no_quote
|
||||
* close_price روشن و خارج ساعت → show_home_price_when_closed روشن: آخرین قیمت؛ خاموش: closed
|
||||
*/
|
||||
export function getPriceDisplayState(): PriceDisplayState {
|
||||
void productTradingHoursVersion.value
|
||||
void useSettingsStore().settingsRevision
|
||||
const p = lastPrice.value
|
||||
const status = p?.status
|
||||
const hasPrice = hasShowableHomePrice(p)
|
||||
|
||||
if (status === 'closed') {
|
||||
return 'closed'
|
||||
}
|
||||
if (status === 'no_quote') {
|
||||
return getTradeBuyPrice(p) > 0 ? 'open' : 'no_quote'
|
||||
}
|
||||
if (status === 'open') {
|
||||
return getTradeBuyPrice(p) > 0 || getTradeSellPrice(p) > 0 ? 'open' : 'no_quote'
|
||||
}
|
||||
|
||||
// سرور status نفرستاده (سازگاری با نسخه قدیمی) → استنتاج سمت کلاینت
|
||||
if (isStoreClosed()) {
|
||||
if (isOutsideGlobalStoreHours()) {
|
||||
if (shouldShowHomePriceWhenClosed()) {
|
||||
return hasPrice ? 'open' : 'no_quote'
|
||||
}
|
||||
return 'closed'
|
||||
}
|
||||
|
||||
return getTradeBuyPrice(p) > 0 || getTradeSellPrice(p) > 0 ? 'open' : 'no_quote'
|
||||
return hasPrice ? 'open' : 'no_quote'
|
||||
}
|
||||
|
||||
export function getPriceDisplayMessage(state: PriceDisplayState = getPriceDisplayState()): string {
|
||||
|
||||
Reference in New Issue
Block a user