From 464995a43e45311e1a4e569390cb9e1ab195df1d Mon Sep 17 00:00:00 2001 From: mohammad sedighi Date: Wed, 26 Aug 2026 18:01:26 +0330 Subject: [PATCH] fix(banners): save relative storage paths so admin preview survives refresh Uploads were stored as host-specific URLs and a force-reload could wipe a just-saved image, leaving a broken dark-theme preview. Co-authored-by: Cursor --- src/api/appBanners.ts | 21 ++++++-- src/composables/useAppBannersAdmin.ts | 45 +++++++++++++++-- src/model/appBanners.ts | 4 +- .../banners/components/BannerImageUpload.vue | 47 ++++++++++++++++-- src/pages/menuPages/banners/index.vue | 2 +- src/utils/file.ts | 49 ++++++++++++++----- 6 files changed, 142 insertions(+), 26 deletions(-) diff --git a/src/api/appBanners.ts b/src/api/appBanners.ts index f544f34..e974fc2 100644 --- a/src/api/appBanners.ts +++ b/src/api/appBanners.ts @@ -14,8 +14,13 @@ import { function pickConstRow(res: Const | Const[] | null | undefined): Const | null { if (!res) return null if (Array.isArray(res)) { - const first = (res[0] as Const) ?? null - return isMeaningfulConstRow(first) ? first : null + const rows = res.filter(isMeaningfulConstRow) + if (!rows.length) return null + return rows.reduce((best, row) => { + const a = Number(best.id ?? best.const_id ?? 0) + const b = Number(row.id ?? row.const_id ?? 0) + return b > a ? row : best + }) } return isMeaningfulConstRow(res) ? res : null } @@ -159,8 +164,16 @@ export async function saveAppBannersConst( } let nextId = id - if (id) { - await useAxios.put(`/admin/const/${id}`, { id, ...payload }) + try { + const existingRes = await useAxios.get(`/api/const/key/${APP_BANNERS_CONST_KEY}`) + const existing = pickConstRow(existingRes) + nextId = existing?.id ?? existing?.const_id ?? id + } catch { + // create below + } + + if (nextId) { + await useAxios.put(`/admin/const/${nextId}`, { id: nextId, ...payload }) } else { const created = await useAxios.post(`/admin/const`, { id: 0, ...payload }) nextId = created?.id ?? created?.const_id ?? null diff --git a/src/composables/useAppBannersAdmin.ts b/src/composables/useAppBannersAdmin.ts index df155e7..c8556d3 100644 --- a/src/composables/useAppBannersAdmin.ts +++ b/src/composables/useAppBannersAdmin.ts @@ -2,7 +2,7 @@ import { reactive, ref } from 'vue' import { message } from 'ant-design-vue' import { loadAppBannersConst, saveAppBannersConst } from '@/api/appBanners' import { emptyAppBanners, type AppBannersValue, type ThemeImagePair } from '@/model/appBanners' -import { toAbsoluteFileUrl } from '@/utils/file' +import { toAbsoluteFileUrl, toStoredStoragePath } from '@/utils/file' import type { IBenefits } from '@/model/banner' const state = reactive(emptyAppBanners()) @@ -13,10 +13,36 @@ const saving = ref(false) let loadPromise: Promise | null = null let saveTimer: ReturnType | null = null let saveChain: Promise = Promise.resolve(true) +let localRevision = 0 + +function absolutizePair(pair: ThemeImagePair): ThemeImagePair { + return { + light: toAbsoluteFileUrl(pair.light) ?? pair.light, + dark: toAbsoluteFileUrl(pair.dark) ?? pair.dark, + } +} + +function applyLoadedData(data: AppBannersValue) { + const next = emptyAppBanners() + Object.assign(next, data) + next.homeAsset = absolutizePair(next.homeAsset) + next.piggyCreate = absolutizePair(next.piggyCreate) + next.productsHero1 = absolutizePair(next.productsHero1) + next.productsHero2 = absolutizePair(next.productsHero2) + next.productsHero3 = absolutizePair(next.productsHero3) + next.referral = absolutizePair(next.referral) + next.share = absolutizePair(next.share) + next.invite = absolutizePair(next.invite) + next.slider = (next.slider ?? []).map((slide) => ({ + ...slide, + icon: toAbsoluteFileUrl(slide.icon) ?? slide.icon, + })) + Object.assign(state, next) +} function httpsUrl(raw: string | null | undefined): string | null { if (typeof raw !== 'string' || !raw.trim()) return null - return toAbsoluteFileUrl(raw.trim()) ?? raw.trim() + return toStoredStoragePath(raw.trim()) } function httpsPair(pair: ThemeImagePair): ThemeImagePair { @@ -48,14 +74,22 @@ async function load(force = false) { if (!force && loaded.value) return if (!force && loadPromise) return loadPromise + const rev = localRevision + loadPromise = (async () => { loading.value = true try { const res = await loadAppBannersConst() + if (rev !== localRevision) { + return + } constId.value = res.id - Object.assign(state, emptyAppBanners(), res.data) + applyLoadedData(res.data) loaded.value = true } catch (error) { + if (rev !== localRevision) { + return + } console.error('خطا در دریافت بنرهای اپ:', error) Object.assign(state, emptyAppBanners()) } finally { @@ -84,13 +118,14 @@ async function save(successText = 'بنرهای اپ ذخیره شد') { } } -/** آپلود/حذف را با تأخیر کوتاه در const می‌نویسد تا رفرش تصویر را برنگرداند. */ +/** آپلود/حذف را سریع در const می‌نویسد تا رفرش تصویر را برنگرداند. */ function saveSoon(successText = 'بنر ذخیره شد') { + localRevision += 1 if (saveTimer) clearTimeout(saveTimer) saveTimer = setTimeout(() => { saveTimer = null saveChain = saveChain.then(() => save(successText)) - }, 450) + }, 80) } export function useAppBannersAdmin() { diff --git a/src/model/appBanners.ts b/src/model/appBanners.ts index 7d281db..2cc5cc6 100644 --- a/src/model/appBanners.ts +++ b/src/model/appBanners.ts @@ -90,11 +90,11 @@ function parseSlider(raw: unknown): IBenefits[] { return [] } -export function parseAppBanners(value: string | null | undefined): AppBannersValue { +export function parseAppBanners(value: string | Record | null | undefined): AppBannersValue { const fallback = emptyAppBanners() if (!value) return fallback try { - const parsed = JSON.parse(value) + const parsed = typeof value === 'string' ? JSON.parse(value) : value if (!parsed || typeof parsed !== 'object') return fallback // فرمت قدیمی: فقط آرایهٔ اسلایدر (header_banner) diff --git a/src/pages/menuPages/banners/components/BannerImageUpload.vue b/src/pages/menuPages/banners/components/BannerImageUpload.vue index a9c8a05..f528370 100644 --- a/src/pages/menuPages/banners/components/BannerImageUpload.vue +++ b/src/pages/menuPages/banners/components/BannerImageUpload.vue @@ -26,7 +26,15 @@