From 3b34249a67cea8cc2770de62bf5c6ef38926d241 Mon Sep 17 00:00:00 2001 From: safavimohadeseh78 Date: Sat, 18 Jul 2026 09:55:51 +0200 Subject: [PATCH] dynamic banners of khanegi_theme --- components.d.ts | 3 - src/api/appBanners.ts | 116 ++++++ src/api/consts.ts | 2 +- src/composables/useAppBannersAdmin.ts | 75 ++++ src/model/appBanners.ts | 138 +++++++ src/model/settingsProfileBanners.ts | 10 + .../components/SettingsProfileBanners.vue | 339 +++++++++++++++ src/pages/menuPages/banners/index.vue | 390 +++++++++--------- src/utils/adminHelpCopy.ts | 6 + src/utils/config.ts | 10 + 10 files changed, 895 insertions(+), 194 deletions(-) create mode 100644 src/api/appBanners.ts create mode 100644 src/composables/useAppBannersAdmin.ts create mode 100644 src/model/appBanners.ts create mode 100644 src/model/settingsProfileBanners.ts create mode 100644 src/pages/menuPages/banners/components/SettingsProfileBanners.vue diff --git a/components.d.ts b/components.d.ts index 3d0dca9..cbd544e 100644 --- a/components.d.ts +++ b/components.d.ts @@ -10,7 +10,6 @@ export {} declare module '@vue/runtime-core' { export interface GlobalComponents { _index: typeof import('./src/core/components/_sample/_index.vue')['default'] - AAlert: typeof import('ant-design-vue/es')['Alert'] AAvatar: typeof import('ant-design-vue/es')['Avatar'] ABadge: typeof import('ant-design-vue/es')['Badge'] AButton: typeof import('ant-design-vue/es')['Button'] @@ -43,7 +42,6 @@ declare module '@vue/runtime-core' { APagination: typeof import('ant-design-vue/es')['Pagination'] APopconfirm: typeof import('ant-design-vue/es')['Popconfirm'] APopover: typeof import('ant-design-vue/es')['Popover'] - ARadio: typeof import('ant-design-vue/es')['Radio'] ARadioButton: typeof import('ant-design-vue/es')['RadioButton'] ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] ARow: typeof import('ant-design-vue/es')['Row'] @@ -55,7 +53,6 @@ declare module '@vue/runtime-core' { ATable: typeof import('ant-design-vue/es')['Table'] ATabPane: typeof import('ant-design-vue/es')['TabPane'] ATabs: typeof import('ant-design-vue/es')['Tabs'] - ATag: typeof import('ant-design-vue/es')['Tag'] ATextarea: typeof import('ant-design-vue/es')['Textarea'] ATimePicker: typeof import('ant-design-vue/es')['TimePicker'] ATooltip: typeof import('ant-design-vue/es')['Tooltip'] diff --git a/src/api/appBanners.ts b/src/api/appBanners.ts new file mode 100644 index 0000000..8d3d39f --- /dev/null +++ b/src/api/appBanners.ts @@ -0,0 +1,116 @@ +import useAxios from '@core/utils/useAxios' +import type { Const } from '@/model/const' +import type { IBenefits } from '@/model/banner' +import { + APP_BANNERS_CONST_KEY, + LEGACY_SLIDER_BANNERS_CONST_KEY, + LEGACY_THEME_BANNERS_CONST_KEY, + emptyAppBanners, + parseAppBanners, + serializeAppBanners, + type AppBannersValue, +} from '@/model/appBanners' + +function pickConstRow(res: Const | Const[] | null | undefined): Const | null { + if (!res) return null + if (Array.isArray(res)) return (res[0] as Const) ?? null + return res +} + +function parseLegacySlider(value: string | null | undefined): IBenefits[] { + if (!value) return [] + try { + const parsed = JSON.parse(value) + return Array.isArray(parsed) ? (parsed as IBenefits[]) : [] + } catch { + return [] + } +} + +export type AppBannersConstState = { + /** فقط وقتی رکورد واقعاً با key=app_banners است؛ وگرنه null تا create شود */ + id: number | null + data: AppBannersValue +} + +/** + * یک API واحد برای همهٔ بنرها. + * اگر `app_banners` نبود، از کلیدهای قدیمی migrate می‌کند (فقط خواندن). + */ +export async function loadAppBannersConst(): Promise { + try { + const res = await useAxios.get(`/api/const/key/${APP_BANNERS_CONST_KEY}`) + const row = pickConstRow(res) + if (row) { + return { + id: row.id ?? row.const_id ?? null, + data: parseAppBanners(row.value), + } + } + } catch { + // ادامه با مهاجرت از کلیدهای قدیمی + } + + const merged = emptyAppBanners() + + try { + const themeRes = await useAxios.get( + `/api/const/key/${LEGACY_THEME_BANNERS_CONST_KEY}` + ) + const themeRow = pickConstRow(themeRes) + if (themeRow) { + const themes = parseAppBanners(themeRow.value) + Object.assign(merged, { + homeAsset: themes.homeAsset, + piggyCreate: themes.piggyCreate, + productsHero1: themes.productsHero1, + productsHero2: themes.productsHero2, + productsHero3: themes.productsHero3, + referral: themes.referral, + share: themes.share, + invite: themes.invite, + slider: themes.slider?.length ? themes.slider : merged.slider, + }) + } + } catch { + // ignore + } + + try { + const sliderRes = await useAxios.get( + `/api/const/key/${LEGACY_SLIDER_BANNERS_CONST_KEY}` + ) + const sliderRow = pickConstRow(sliderRes) + if (sliderRow) { + const slides = parseLegacySlider(sliderRow.value) + if (slides.length) merged.slider = slides + } + } catch { + // ignore + } + + // id=null → اولین ذخیره، رکورد جدید با key=app_banners می‌سازد + return { id: null, data: merged } +} + +export async function saveAppBannersConst( + id: number | null, + data: AppBannersValue +): Promise { + const value = serializeAppBanners(data) + const payload = { + key: APP_BANNERS_CONST_KEY, + value, + title_fa: 'بنرهای اپ', + is_active: true, + description: 'همهٔ بنرهای اپ: اسلایدر صفحه اصلی + تصاویر تم لایت/دارک', + } + + if (id) { + await useAxios.put(`/admin/const/${id}`, { id, ...payload }) + return id + } + + const created = await useAxios.post(`/admin/const`, { id: 0, ...payload }) + return created?.id ?? created?.const_id ?? null +} diff --git a/src/api/consts.ts b/src/api/consts.ts index 4f718d5..8a180de 100644 --- a/src/api/consts.ts +++ b/src/api/consts.ts @@ -24,4 +24,4 @@ export const updateBannerConst = async (obj: Const) => await useAxios.put(`/admin/const/${obj.id}`, obj) export const getHeaderBannerConst = async () => - await useAxios.get('/api/const/key/header_banner') \ No newline at end of file + await useAxios.get('/api/const/key/header_banner') diff --git a/src/composables/useAppBannersAdmin.ts b/src/composables/useAppBannersAdmin.ts new file mode 100644 index 0000000..27fae8e --- /dev/null +++ b/src/composables/useAppBannersAdmin.ts @@ -0,0 +1,75 @@ +import { reactive, ref } from 'vue' +import { message } from 'ant-design-vue' +import { loadAppBannersConst, saveAppBannersConst } from '@/api/appBanners' +import { emptyAppBanners, type AppBannersValue } from '@/model/appBanners' + +const state = reactive(emptyAppBanners()) +const constId = ref(null) +const loaded = ref(false) +const loading = ref(false) +const saving = ref(false) +let loadPromise: Promise | null = null + +async function load(force = false) { + if (!force && loaded.value) return + if (!force && loadPromise) return loadPromise + + loadPromise = (async () => { + loading.value = true + try { + const res = await loadAppBannersConst() + constId.value = res.id + Object.assign(state, emptyAppBanners(), res.data) + loaded.value = true + } catch (error) { + console.error('خطا در دریافت بنرهای اپ:', error) + Object.assign(state, emptyAppBanners()) + } finally { + loading.value = false + loadPromise = null + } + })() + + return loadPromise +} + +async function save(successText = 'بنرهای اپ ذخیره شد') { + saving.value = true + try { + const nextId = await saveAppBannersConst(constId.value, { + slider: [...state.slider], + homeAsset: { ...state.homeAsset }, + piggyCreate: { ...state.piggyCreate }, + productsHero1: { ...state.productsHero1 }, + productsHero2: { ...state.productsHero2 }, + productsHero3: { ...state.productsHero3 }, + referral: { ...state.referral }, + share: { ...state.share }, + invite: { ...state.invite }, + }) + constId.value = nextId + loaded.value = true + message.success(successText) + return true + } catch (error) { + console.error(error) + message.error('خطا در ذخیره بنرهای اپ') + return false + } finally { + saving.value = false + } +} + +export function useAppBannersAdmin() { + void load() + + return { + state, + constId, + loaded, + loading, + saving, + load, + save, + } +} diff --git a/src/model/appBanners.ts b/src/model/appBanners.ts new file mode 100644 index 0000000..7d281db --- /dev/null +++ b/src/model/appBanners.ts @@ -0,0 +1,138 @@ +import type { IBenefits } from '@/model/banner' + +/** کلید واحد همهٔ بنرهای اپ (اسلایدر + تم لایت/دارک) */ +export const APP_BANNERS_CONST_KEY = 'app_banners' + +/** کلید قدیمی تم — فقط برای مهاجرت خواندن */ +export const LEGACY_THEME_BANNERS_CONST_KEY = 'settings_profile_banners' + +/** کلید قدیمی اسلایدر — فقط برای مهاجرت خواندن */ +export const LEGACY_SLIDER_BANNERS_CONST_KEY = 'header_banner' + +export type AppThemeBannerKey = + | 'homeAsset' + | 'piggyCreate' + | 'productsHero1' + | 'productsHero2' + | 'productsHero3' + | 'referral' + | 'share' + | 'invite' + +/** @deprecated از AppThemeBannerKey استفاده کنید */ +export type SettingsProfileBannerKey = AppThemeBannerKey + +export interface ThemeImagePair { + light: string | null + dark: string | null +} + +export interface AppBannersValue { + slider: IBenefits[] + homeAsset: ThemeImagePair + piggyCreate: ThemeImagePair + productsHero1: ThemeImagePair + productsHero2: ThemeImagePair + productsHero3: ThemeImagePair + referral: ThemeImagePair + share: ThemeImagePair + invite: ThemeImagePair +} + +/** @deprecated از AppBannersValue استفاده کنید */ +export type SettingsProfileBannersValue = Omit + +export const SETTINGS_PROFILE_BANNERS_CONST_KEY = APP_BANNERS_CONST_KEY + +export function emptyThemePair(): ThemeImagePair { + return { light: null, dark: null } +} + +export function emptyAppBanners(): AppBannersValue { + return { + slider: [], + homeAsset: emptyThemePair(), + piggyCreate: emptyThemePair(), + productsHero1: emptyThemePair(), + productsHero2: emptyThemePair(), + productsHero3: emptyThemePair(), + referral: emptyThemePair(), + share: emptyThemePair(), + invite: emptyThemePair(), + } +} + +export function emptySettingsProfileBanners(): SettingsProfileBannersValue { + const { slider: _slider, ...themes } = emptyAppBanners() + return themes +} + +function normalizePair(raw: unknown): ThemeImagePair { + if (!raw || typeof raw !== 'object') return emptyThemePair() + const obj = raw as Record + return { + light: typeof obj.light === 'string' && obj.light.trim() ? obj.light : null, + dark: typeof obj.dark === 'string' && obj.dark.trim() ? obj.dark : null, + } +} + +function parseSlider(raw: unknown): IBenefits[] { + if (!raw) return [] + if (Array.isArray(raw)) return raw as IBenefits[] + if (typeof raw === 'string') { + try { + const parsed = JSON.parse(raw) + return Array.isArray(parsed) ? (parsed as IBenefits[]) : [] + } catch { + return [] + } + } + return [] +} + +export function parseAppBanners(value: string | null | undefined): AppBannersValue { + const fallback = emptyAppBanners() + if (!value) return fallback + try { + const parsed = JSON.parse(value) + if (!parsed || typeof parsed !== 'object') return fallback + + // فرمت قدیمی: فقط آرایهٔ اسلایدر (header_banner) + if (Array.isArray(parsed)) { + return { ...fallback, slider: parsed as IBenefits[] } + } + + return { + slider: parseSlider(parsed.slider), + homeAsset: normalizePair(parsed.homeAsset), + piggyCreate: normalizePair(parsed.piggyCreate), + productsHero1: normalizePair(parsed.productsHero1), + productsHero2: normalizePair(parsed.productsHero2), + productsHero3: normalizePair(parsed.productsHero3), + referral: normalizePair(parsed.referral), + share: normalizePair(parsed.share), + invite: normalizePair(parsed.invite), + } + } catch { + return fallback + } +} + +export function parseSettingsProfileBanners(value: string | null | undefined): SettingsProfileBannersValue { + const { slider: _slider, ...themes } = parseAppBanners(value) + return themes +} + +export function serializeAppBanners(data: AppBannersValue): string { + return JSON.stringify({ + slider: data.slider ?? [], + homeAsset: { ...data.homeAsset }, + piggyCreate: { ...data.piggyCreate }, + productsHero1: { ...data.productsHero1 }, + productsHero2: { ...data.productsHero2 }, + productsHero3: { ...data.productsHero3 }, + referral: { ...data.referral }, + share: { ...data.share }, + invite: { ...data.invite }, + }) +} diff --git a/src/model/settingsProfileBanners.ts b/src/model/settingsProfileBanners.ts new file mode 100644 index 0000000..391b905 --- /dev/null +++ b/src/model/settingsProfileBanners.ts @@ -0,0 +1,10 @@ +/** سازگاری با importهای قبلی — منبع اصلی: appBanners.ts */ +export { + APP_BANNERS_CONST_KEY as SETTINGS_PROFILE_BANNERS_CONST_KEY, + emptySettingsProfileBanners, + emptyThemePair, + parseSettingsProfileBanners, + type AppThemeBannerKey as SettingsProfileBannerKey, + type SettingsProfileBannersValue, + type ThemeImagePair, +} from '@/model/appBanners' diff --git a/src/pages/menuPages/banners/components/SettingsProfileBanners.vue b/src/pages/menuPages/banners/components/SettingsProfileBanners.vue new file mode 100644 index 0000000..2481cea --- /dev/null +++ b/src/pages/menuPages/banners/components/SettingsProfileBanners.vue @@ -0,0 +1,339 @@ + + + + + diff --git a/src/pages/menuPages/banners/index.vue b/src/pages/menuPages/banners/index.vue index a70916e..28827dc 100644 --- a/src/pages/menuPages/banners/index.vue +++ b/src/pages/menuPages/banners/index.vue @@ -1,123 +1,140 @@