dynamic banners of khanegi_theme
This commit is contained in:
Vendored
-3
@@ -10,7 +10,6 @@ export {}
|
|||||||
declare module '@vue/runtime-core' {
|
declare module '@vue/runtime-core' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
_index: typeof import('./src/core/components/_sample/_index.vue')['default']
|
_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']
|
AAvatar: typeof import('ant-design-vue/es')['Avatar']
|
||||||
ABadge: typeof import('ant-design-vue/es')['Badge']
|
ABadge: typeof import('ant-design-vue/es')['Badge']
|
||||||
AButton: typeof import('ant-design-vue/es')['Button']
|
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']
|
APagination: typeof import('ant-design-vue/es')['Pagination']
|
||||||
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
||||||
APopover: typeof import('ant-design-vue/es')['Popover']
|
APopover: typeof import('ant-design-vue/es')['Popover']
|
||||||
ARadio: typeof import('ant-design-vue/es')['Radio']
|
|
||||||
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
||||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||||
ARow: typeof import('ant-design-vue/es')['Row']
|
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']
|
ATable: typeof import('ant-design-vue/es')['Table']
|
||||||
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
||||||
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
||||||
ATag: typeof import('ant-design-vue/es')['Tag']
|
|
||||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||||
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
|
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
|
||||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||||
|
|||||||
@@ -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<AppBannersConstState> {
|
||||||
|
try {
|
||||||
|
const res = await useAxios.get<Const | Const[]>(`/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<Const | Const[]>(
|
||||||
|
`/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<Const | Const[]>(
|
||||||
|
`/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<number | null> {
|
||||||
|
const value = serializeAppBanners(data)
|
||||||
|
const payload = {
|
||||||
|
key: APP_BANNERS_CONST_KEY,
|
||||||
|
value,
|
||||||
|
title_fa: 'بنرهای اپ',
|
||||||
|
is_active: true,
|
||||||
|
description: 'همهٔ بنرهای اپ: اسلایدر صفحه اصلی + تصاویر تم لایت/دارک',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
await useAxios.put<Const>(`/admin/const/${id}`, { id, ...payload })
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await useAxios.post<Const>(`/admin/const`, { id: 0, ...payload })
|
||||||
|
return created?.id ?? created?.const_id ?? null
|
||||||
|
}
|
||||||
@@ -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<AppBannersValue>(emptyAppBanners())
|
||||||
|
const constId = ref<number | null>(null)
|
||||||
|
const loaded = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
let loadPromise: Promise<void> | 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<AppBannersValue, 'slider'>
|
||||||
|
|
||||||
|
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<string, unknown>
|
||||||
|
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 },
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
<template>
|
||||||
|
<section class="profile-banners">
|
||||||
|
<header class="profile-banners__header">
|
||||||
|
<div class="profile-banners__header-text">
|
||||||
|
<div class="profile-banners__title-row">
|
||||||
|
<h2 class="profile-banners__title">تصاویر تم اپ (لایت / دارک)</h2>
|
||||||
|
<AdminFieldHelp :text="settingsProfileBannersIntro" size="sm" />
|
||||||
|
</div>
|
||||||
|
<p class="profile-banners__subtitle">
|
||||||
|
هر بخش را باز کنید — صفحهٔ اصلی، محصولات، قلکها و تنظیمات
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<BaseButton type="primary" :loading="saving" @click="handleSubmit">
|
||||||
|
ذخیره تصاویر تم
|
||||||
|
</BaseButton>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<a-collapse
|
||||||
|
v-model:activeKey="activeKey"
|
||||||
|
:bordered="false"
|
||||||
|
class="profile-banners__collapse"
|
||||||
|
>
|
||||||
|
<a-collapse-panel v-for="group in groups" :key="group.id">
|
||||||
|
<template #header>
|
||||||
|
<div class="profile-banners__collapse-header">
|
||||||
|
<BaseIcon :icon="group.icon" size="18" class="profile-banners__collapse-icon" />
|
||||||
|
<div class="profile-banners__collapse-text">
|
||||||
|
<span class="profile-banners__collapse-title">{{ group.title }}</span>
|
||||||
|
<span class="profile-banners__collapse-desc">{{ group.description }}</span>
|
||||||
|
</div>
|
||||||
|
<a-badge
|
||||||
|
:count="group.slots.length"
|
||||||
|
:number-style="{ backgroundColor: '#d4af37', color: '#fff', fontSize: '11px' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="profile-banners__list">
|
||||||
|
<article v-for="slot in group.slots" :key="slot.key" class="profile-banner-card">
|
||||||
|
<header class="profile-banner-card__head">
|
||||||
|
<div>
|
||||||
|
<div class="profile-banner-card__title">{{ slot.title }}</div>
|
||||||
|
<p class="profile-banner-card__desc">{{ slot.description }}</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="profile-banner-card__themes">
|
||||||
|
<div class="profile-banner-card__theme">
|
||||||
|
<AdminLabelWithHelp label="تم روشن" :help="settingsProfileBannersThemeHelp" />
|
||||||
|
<div class="profile-banner-card__upload">
|
||||||
|
<BannerImageUpload
|
||||||
|
:image-url="state[slot.key].light"
|
||||||
|
@upload="state[slot.key].light = $event"
|
||||||
|
@remove="state[slot.key].light = null"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="profile-banner-card__theme">
|
||||||
|
<AdminLabelWithHelp label="تم تیره" :help="settingsProfileBannersThemeHelp" />
|
||||||
|
<div class="profile-banner-card__upload profile-banner-card__upload--dark">
|
||||||
|
<BannerImageUpload
|
||||||
|
:image-url="state[slot.key].dark"
|
||||||
|
@upload="state[slot.key].dark = $event"
|
||||||
|
@remove="state[slot.key].dark = null"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</a-collapse-panel>
|
||||||
|
</a-collapse>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { AppThemeBannerKey } from '@/model/appBanners'
|
||||||
|
import { useAppBannersAdmin } from '@/composables/useAppBannersAdmin'
|
||||||
|
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
||||||
|
import AdminLabelWithHelp from '@/components/AdminLabelWithHelp.vue'
|
||||||
|
import {
|
||||||
|
settingsProfileBannersIntro,
|
||||||
|
settingsProfileBannersThemeHelp,
|
||||||
|
} from '@/utils/adminHelpCopy'
|
||||||
|
import BannerImageUpload from './BannerImageUpload.vue'
|
||||||
|
|
||||||
|
const { state, saving, save } = useAppBannersAdmin()
|
||||||
|
/** پیشفرض همه کالپسها بسته — فقط هدرها دیده میشوند */
|
||||||
|
const activeKey = ref<(string | number)[]>([])
|
||||||
|
|
||||||
|
type BannerSlot = {
|
||||||
|
key: AppThemeBannerKey
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups: Array<{
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
icon: string
|
||||||
|
slots: BannerSlot[]
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
id: 'home',
|
||||||
|
title: 'صفحهٔ اصلی',
|
||||||
|
description: 'کارت ارزش کل دارایی',
|
||||||
|
icon: 'vuesax-linear:home-2',
|
||||||
|
slots: [
|
||||||
|
{
|
||||||
|
key: 'homeAsset',
|
||||||
|
title: 'بنر کارت دارایی',
|
||||||
|
description: 'پسزمینهٔ کارت «ارزش کل دارایی» بالای صفحهٔ اصلی',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'products',
|
||||||
|
title: 'محصولات (موبایل)',
|
||||||
|
description: 'اسلایدر بنرهای بالای صفحهٔ محصولات',
|
||||||
|
icon: 'vuesax-linear:box',
|
||||||
|
slots: [
|
||||||
|
{
|
||||||
|
key: 'productsHero1',
|
||||||
|
title: 'بنر محصولات ۱',
|
||||||
|
description: 'اولین اسلاید اسلایدر',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'productsHero2',
|
||||||
|
title: 'بنر محصولات ۲',
|
||||||
|
description: 'دومین اسلاید اسلایدر',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'productsHero3',
|
||||||
|
title: 'بنر محصولات ۳',
|
||||||
|
description: 'سومین اسلاید اسلایدر',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fund',
|
||||||
|
title: 'قلکهای من',
|
||||||
|
description: 'تصویر قلک در صفحهٔ ساخت قلک',
|
||||||
|
icon: 'vuesax-linear:wallet-3',
|
||||||
|
slots: [
|
||||||
|
{
|
||||||
|
key: 'piggyCreate',
|
||||||
|
title: 'تصویر قلک',
|
||||||
|
description: 'تصویر قلک طلایی کنار دکمهٔ «ساخت قلک جدید»',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'settings',
|
||||||
|
title: 'تنظیمات (پروفایل)',
|
||||||
|
description: 'بنرهای کد معرف، اشتراکگذاری و دعوت',
|
||||||
|
icon: 'vuesax-linear:setting-2',
|
||||||
|
slots: [
|
||||||
|
{
|
||||||
|
key: 'referral',
|
||||||
|
title: 'بنر کد معرف',
|
||||||
|
description: 'کارت «کد معرف شما» در صفحهٔ تنظیمات',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'share',
|
||||||
|
title: 'بنر اشتراکگذاری',
|
||||||
|
description: 'کارت لینک اختصاصی و دکمهٔ اشتراکگذاری',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'invite',
|
||||||
|
title: 'بنر دعوت از دوستان',
|
||||||
|
description: 'کارت دعوت از دوستان و دریافت هدیه',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
await save('تصاویر تم اپ ذخیره شد')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.profile-banners {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banners__header {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banners__title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banners__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--admin-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banners__subtitle {
|
||||||
|
margin: 0.35rem 0 0;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--admin-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banners__collapse {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banners__collapse :deep(.ant-collapse-item) {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--admin-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--admin-bg-elevated);
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banners__collapse :deep(.ant-collapse-item:last-child) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banners__collapse :deep(.ant-collapse-header) {
|
||||||
|
padding: 0.875rem 1rem !important;
|
||||||
|
align-items: center !important;
|
||||||
|
background: color-mix(in srgb, var(--admin-bg-muted) 70%, transparent);
|
||||||
|
color: var(--admin-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banners__collapse :deep(.ant-collapse-content) {
|
||||||
|
border-top: 1px solid var(--admin-border);
|
||||||
|
background: var(--admin-bg-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banners__collapse :deep(.ant-collapse-content-box) {
|
||||||
|
padding: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banners__collapse-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
padding-inline-end: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banners__collapse-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banners__collapse-text {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banners__collapse-title {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--admin-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banners__collapse-desc {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--admin-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banners__list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banner-card {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--admin-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--admin-bg-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banner-card__head {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-bottom: 1px solid var(--admin-border);
|
||||||
|
background: color-mix(in srgb, var(--admin-bg-muted) 55%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banner-card__title {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--admin-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banner-card__desc {
|
||||||
|
margin: 0.2rem 0 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--admin-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banner-card__themes {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
@media (min-width: 900px) {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banner-card__theme {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-banner-card__upload {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
min-height: 10rem;
|
||||||
|
|
||||||
|
&--dark :deep(.banner-image-upload__filled) {
|
||||||
|
background: #121212;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,123 +1,140 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="banners-page">
|
<div class="banners-page">
|
||||||
<header class="banners-page__header">
|
|
||||||
<div class="banners-page__header-text">
|
|
||||||
<div class="banners-page__title-row">
|
|
||||||
<h1 class="banners-page__title">بنرهای اسلایدر</h1>
|
|
||||||
<AdminFieldHelp :text="bannersPageIntro" size="sm" />
|
|
||||||
</div>
|
|
||||||
<p class="banners-page__subtitle">تصاویر بالای صفحهٔ اصلی اپ مشتری — هر ردیف یک اسلاید</p>
|
|
||||||
</div>
|
|
||||||
<div class="banners-page__header-actions">
|
|
||||||
<BaseButton size="small" type="primary" @click="addRow">
|
|
||||||
<span>افزودن اسلاید</span>
|
|
||||||
<BaseIcon icon="vuesax-linear:add" size="17" />
|
|
||||||
</BaseButton>
|
|
||||||
<BaseButton type="primary" :loading="saving" @click="handleSubmit">
|
|
||||||
ذخیره تغییرات
|
|
||||||
</BaseButton>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="banners-page__body">
|
<div class="banners-page__body">
|
||||||
<div v-if="!benefitsList.length" class="banners-page__empty">
|
<a-collapse
|
||||||
<div class="banners-page__empty-inner">
|
v-if="!showSettingsProfileBanners"
|
||||||
<BaseIcon icon="vuesax-linear:image" size="48" class="banners-page__empty-icon" />
|
v-model:activeKey="sliderActiveKey"
|
||||||
<p class="banners-page__empty-title">هنوز بنری اضافه نشده</p>
|
:bordered="false"
|
||||||
<p class="banners-page__empty-text">اولین اسلاید را بسازید تا در اپ مشتری نمایش داده شود</p>
|
class="banners-page__collapse"
|
||||||
<BaseButton type="primary" @click="addRow">
|
>
|
||||||
<span>افزودن اسلاید</span>
|
<a-collapse-panel key="slider">
|
||||||
<BaseIcon icon="vuesax-linear:add" size="17" />
|
<template #header>
|
||||||
</BaseButton>
|
<div class="banners-page__collapse-header">
|
||||||
</div>
|
<BaseIcon icon="vuesax-linear:slider-horizontal-1" size="18" class="banners-page__collapse-icon" />
|
||||||
</div>
|
<div class="banners-page__collapse-text">
|
||||||
|
<span class="banners-page__collapse-title">بنرهای اسلایدر</span>
|
||||||
<div v-else class="banners-page__list">
|
<span class="banners-page__collapse-desc">تصاویر بالای صفحهٔ اصلی اپ مشتری — هر ردیف یک اسلاید</span>
|
||||||
<article
|
|
||||||
v-for="(item, index) in benefitsList"
|
|
||||||
:key="index"
|
|
||||||
class="banner-card"
|
|
||||||
:class="{ 'banner-card--hidden': !item.show }"
|
|
||||||
>
|
|
||||||
<header class="banner-card__head">
|
|
||||||
<div class="banner-card__head-start">
|
|
||||||
<span class="banner-card__index">{{ index + 1 }}</span>
|
|
||||||
<div>
|
|
||||||
<div class="banner-card__title">اسلاید {{ index + 1 }}</div>
|
|
||||||
<div class="banner-card__meta">
|
|
||||||
<span class="banner-card__pill" :class="{ 'banner-card__pill--off': !item.show }">
|
|
||||||
{{ item.show ? 'فعال در اپ' : 'مخفی' }}
|
|
||||||
</span>
|
|
||||||
<span class="banner-card__ratio">۱۶:۹</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<AdminFieldHelp :text="bannersPageIntro" size="sm" />
|
||||||
|
<a-badge
|
||||||
|
:count="state.slider.length"
|
||||||
|
:number-style="{ backgroundColor: '#d4af37', color: '#fff', fontSize: '11px' }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="banner-card__head-end">
|
<div class="banners-page__panel-actions">
|
||||||
<label class="banner-card__switch">
|
<BaseButton size="small" type="primary" @click="addRow">
|
||||||
<span>نمایش</span>
|
<span>افزودن اسلاید</span>
|
||||||
<a-switch v-model:checked="item.show" size="small" />
|
<BaseIcon icon="vuesax-linear:add" size="17" />
|
||||||
<AdminFieldHelp :text="bannersShowHelp" />
|
</BaseButton>
|
||||||
</label>
|
<BaseButton type="primary" :loading="saving" @click="handleSubmit">
|
||||||
<button type="button" class="banner-card__remove" title="حذف اسلاید" @click="removeRow(index)">
|
ذخیره تغییرات
|
||||||
<BaseIcon icon="vuesax-linear:trash" size="18" />
|
</BaseButton>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="banner-card__content">
|
<div v-if="!state.slider.length" class="banners-page__empty">
|
||||||
<div class="banner-card__preview-wrap">
|
<div class="banners-page__empty-inner">
|
||||||
<AdminLabelWithHelp label="پیشنمایش تصویر" :help="bannersImageSectionHelp" />
|
<BaseIcon icon="vuesax-linear:image" size="48" class="banners-page__empty-icon" />
|
||||||
<div class="banner-card__preview">
|
<p class="banners-page__empty-title">هنوز بنری اضافه نشده</p>
|
||||||
<BannerImageUpload
|
<p class="banners-page__empty-text">اولین اسلاید را بسازید تا در اپ مشتری نمایش داده شود</p>
|
||||||
:image-url="item.icon"
|
<BaseButton type="primary" @click="addRow">
|
||||||
@upload="item.icon = $event"
|
<span>افزودن اسلاید</span>
|
||||||
@remove="item.icon = null"
|
<BaseIcon icon="vuesax-linear:add" size="17" />
|
||||||
/>
|
</BaseButton>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="banner-card__fields" :class="{ 'banner-card__fields--stack': item.typeClick === 'modal' }">
|
|
||||||
<div class="banner-card__field">
|
|
||||||
<AdminLabelWithHelp label="بعد از کلیک مشتری" :help="clickEventHelp(item.typeClick)" />
|
|
||||||
<BasicSelect
|
|
||||||
v-model:value="item.typeClick"
|
|
||||||
:items="typeList"
|
|
||||||
value-name="lable"
|
|
||||||
key-name="value"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="item.typeClick === 'link'" class="banner-card__field banner-card__field--wide">
|
|
||||||
<AdminLabelWithHelp label="آدرس لینک" :help="bannersLinkHelp" />
|
|
||||||
<BasicInput v-model:value="item.link" placeholder="https://example.com" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="item.typeClick === 'modal'" class="banner-card__field banner-card__field--wide">
|
|
||||||
<AdminLabelWithHelp label="متن پیام" :help="bannersModalHelp" />
|
|
||||||
<a-textarea
|
|
||||||
:value="htmlToPlain(item.modalText)"
|
|
||||||
:rows="7"
|
|
||||||
class="banner-card__message"
|
|
||||||
placeholder="متن پیامی که مشتری در پنجرهٔ بازشو میبیند..."
|
|
||||||
@update:value="item.modalText = plainToHtml($event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
|
||||||
</div>
|
<div v-else class="banners-page__list">
|
||||||
|
<article
|
||||||
|
v-for="(item, index) in state.slider"
|
||||||
|
:key="index"
|
||||||
|
class="banner-card"
|
||||||
|
:class="{ 'banner-card--hidden': !item.show }"
|
||||||
|
>
|
||||||
|
<header class="banner-card__head">
|
||||||
|
<div class="banner-card__head-start">
|
||||||
|
<span class="banner-card__index">{{ index + 1 }}</span>
|
||||||
|
<div>
|
||||||
|
<div class="banner-card__title">اسلاید {{ index + 1 }}</div>
|
||||||
|
<div class="banner-card__meta">
|
||||||
|
<span class="banner-card__pill" :class="{ 'banner-card__pill--off': !item.show }">
|
||||||
|
{{ item.show ? 'فعال در اپ' : 'مخفی' }}
|
||||||
|
</span>
|
||||||
|
<span class="banner-card__ratio">۱۶:۹</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="banner-card__head-end">
|
||||||
|
<label class="banner-card__switch">
|
||||||
|
<span>نمایش</span>
|
||||||
|
<a-switch v-model:checked="item.show" size="small" />
|
||||||
|
<AdminFieldHelp :text="bannersShowHelp" />
|
||||||
|
</label>
|
||||||
|
<button type="button" class="banner-card__remove" title="حذف اسلاید" @click="removeRow(index)">
|
||||||
|
<BaseIcon icon="vuesax-linear:trash" size="18" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="banner-card__content">
|
||||||
|
<div class="banner-card__preview-wrap">
|
||||||
|
<AdminLabelWithHelp label="پیشنمایش تصویر" :help="bannersImageSectionHelp" />
|
||||||
|
<div class="banner-card__preview">
|
||||||
|
<BannerImageUpload
|
||||||
|
:image-url="item.icon"
|
||||||
|
@upload="item.icon = $event"
|
||||||
|
@remove="item.icon = null"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="banner-card__fields" :class="{ 'banner-card__fields--stack': item.typeClick === 'modal' }">
|
||||||
|
<div class="banner-card__field">
|
||||||
|
<AdminLabelWithHelp label="بعد از کلیک مشتری" :help="clickEventHelp(item.typeClick)" />
|
||||||
|
<BasicSelect
|
||||||
|
v-model:value="item.typeClick"
|
||||||
|
:items="typeList"
|
||||||
|
value-name="lable"
|
||||||
|
key-name="value"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="item.typeClick === 'link'" class="banner-card__field banner-card__field--wide">
|
||||||
|
<AdminLabelWithHelp label="آدرس لینک" :help="bannersLinkHelp" />
|
||||||
|
<BasicInput v-model:value="item.link" placeholder="https://example.com" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="item.typeClick === 'modal'" class="banner-card__field banner-card__field--wide">
|
||||||
|
<AdminLabelWithHelp label="متن پیام" :help="bannersModalHelp" />
|
||||||
|
<a-textarea
|
||||||
|
:value="htmlToPlain(item.modalText)"
|
||||||
|
:rows="7"
|
||||||
|
class="banner-card__message"
|
||||||
|
placeholder="متن پیامی که مشتری در پنجرهٔ بازشو میبیند..."
|
||||||
|
@update:value="item.modalText = plainToHtml($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</a-collapse-panel>
|
||||||
|
</a-collapse>
|
||||||
|
|
||||||
|
<SettingsProfileBanners v-if="showSettingsProfileBanners" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
import type { IBenefits } from '@/model/banner'
|
|
||||||
import BannerImageUpload from './components/BannerImageUpload.vue'
|
import BannerImageUpload from './components/BannerImageUpload.vue'
|
||||||
|
import SettingsProfileBanners from './components/SettingsProfileBanners.vue'
|
||||||
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
|
||||||
import AdminLabelWithHelp from '@/components/AdminLabelWithHelp.vue'
|
import AdminLabelWithHelp from '@/components/AdminLabelWithHelp.vue'
|
||||||
import { updateBannerConst, getHeaderBannerConst } from '@/api/consts'
|
import { useAppBannersAdmin } from '@/composables/useAppBannersAdmin'
|
||||||
|
import { hasAppUiConfig } from '@/utils/config'
|
||||||
import {
|
import {
|
||||||
bannersEventHelp,
|
bannersEventHelp,
|
||||||
bannersImageSectionHelp,
|
bannersImageSectionHelp,
|
||||||
@@ -129,9 +146,10 @@ import {
|
|||||||
bannersShowHelp,
|
bannersShowHelp,
|
||||||
} from '@/utils/adminHelpCopy'
|
} from '@/utils/adminHelpCopy'
|
||||||
|
|
||||||
const benefitsList = ref<IBenefits[]>([])
|
const { state, saving, save } = useAppBannersAdmin()
|
||||||
const saving = ref(false)
|
/** پیشفرض کالپس بنرهای اسلایدر بسته است */
|
||||||
let id: number | null = null
|
const sliderActiveKey = ref<(string | number)[]>([])
|
||||||
|
const showSettingsProfileBanners = computed(() => hasAppUiConfig())
|
||||||
|
|
||||||
const typeList = ref([
|
const typeList = ref([
|
||||||
{ lable: 'بدون رویداد', value: null },
|
{ lable: 'بدون رویداد', value: null },
|
||||||
@@ -167,21 +185,11 @@ function plainToHtml(text: string | null | undefined): string | null {
|
|||||||
return `<p>${escaped.replace(/\n/g, '<br>')}</p>`
|
return `<p>${escaped.replace(/\n/g, '<br>')}</p>`
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseBannerList(value: string | null | undefined): IBenefits[] {
|
|
||||||
if (!value) return []
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(value)
|
|
||||||
return Array.isArray(parsed) ? parsed : []
|
|
||||||
} catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addRow() {
|
function addRow() {
|
||||||
if (!Array.isArray(benefitsList.value)) {
|
if (!Array.isArray(state.slider)) {
|
||||||
benefitsList.value = []
|
state.slider = []
|
||||||
}
|
}
|
||||||
benefitsList.value.push({
|
state.slider.push({
|
||||||
icon: null,
|
icon: null,
|
||||||
show: true,
|
show: true,
|
||||||
caption: null,
|
caption: null,
|
||||||
@@ -192,43 +200,12 @@ function addRow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeRow(index: number) {
|
function removeRow(index: number) {
|
||||||
benefitsList.value.splice(index, 1)
|
state.slider.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
if (!id) {
|
await save('اطلاعات با موفقیت ذخیره شد')
|
||||||
message.error('شناسه بنر یافت نشد؛ صفحه را رفرش کنید')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
saving.value = true
|
|
||||||
await updateBannerConst({
|
|
||||||
id,
|
|
||||||
value: JSON.stringify(benefitsList.value),
|
|
||||||
key: 'header_banner',
|
|
||||||
})
|
|
||||||
message.success('اطلاعات با موفقیت ذخیره شد')
|
|
||||||
} catch (error) {
|
|
||||||
message.error('خطا در ذخیره اطلاعات')
|
|
||||||
} finally {
|
|
||||||
saving.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
const res = await getHeaderBannerConst()
|
|
||||||
if (!res) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
id = res.id ?? res.const_id ?? null
|
|
||||||
benefitsList.value = parseBannerList(res.value)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('خطا در دریافت بنرها:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
@@ -241,43 +218,6 @@ onMounted(async () => {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.banners-page__header {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 1rem 1.25rem;
|
|
||||||
border-bottom: 1px solid var(--admin-border);
|
|
||||||
background: var(--admin-bg-elevated);
|
|
||||||
}
|
|
||||||
|
|
||||||
.banners-page__title-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banners-page__title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--admin-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.banners-page__subtitle {
|
|
||||||
margin: 0.35rem 0 0;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
color: var(--admin-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.banners-page__header-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.625rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banners-page__body {
|
.banners-page__body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -285,10 +225,80 @@ onMounted(async () => {
|
|||||||
padding: 1rem 1.25rem 1.5rem;
|
padding: 1rem 1.25rem 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.banners-page__collapse {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banners-page__collapse :deep(.ant-collapse-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--admin-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--admin-bg-elevated);
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banners-page__collapse :deep(.ant-collapse-header) {
|
||||||
|
padding: 0.875rem 1rem !important;
|
||||||
|
align-items: center !important;
|
||||||
|
background: color-mix(in srgb, var(--admin-bg-muted) 70%, transparent);
|
||||||
|
color: var(--admin-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banners-page__collapse :deep(.ant-collapse-content) {
|
||||||
|
border-top: 1px solid var(--admin-border);
|
||||||
|
background: var(--admin-bg-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banners-page__collapse :deep(.ant-collapse-content-box) {
|
||||||
|
padding: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banners-page__collapse-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
padding-inline-end: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banners-page__collapse-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banners-page__collapse-text {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banners-page__collapse-title {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--admin-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banners-page__collapse-desc {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--admin-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banners-page__panel-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.625rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.banners-page__empty {
|
.banners-page__empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: calc(100vh - 12rem);
|
min-height: 14rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ export const bannersShareHelp =
|
|||||||
export const bannersNoEventHelp =
|
export const bannersNoEventHelp =
|
||||||
'فقط تصویر دیده میشود؛ کلیک هیچ کاری نمیکند. برای بنرهای تزئینی مناسب است.'
|
'فقط تصویر دیده میشود؛ کلیک هیچ کاری نمیکند. برای بنرهای تزئینی مناسب است.'
|
||||||
|
|
||||||
|
export const settingsProfileBannersIntro =
|
||||||
|
'تصاویر تم اپ: کارت دارایی صفحهٔ اصلی، تصویر قلک، اسلایدر بنرهای موبایل صفحهٔ محصولات، و بنرهای صفحهٔ تنظیمات. برای هر مورد دو تصویر جدا — تم روشن و تم تیره — آپلود کنید.'
|
||||||
|
|
||||||
|
export const settingsProfileBannersThemeHelp =
|
||||||
|
'تم روشن: وقتی مشتری حالت روشن را انتخاب کرده. تم تیره: وقتی حالت تیره فعال است. اگر فقط یکی را بگذارید، اپ برای دیگری از تصویر پیشفرض خودش استفاده میکند.'
|
||||||
|
|
||||||
export const pricingPageIntro =
|
export const pricingPageIntro =
|
||||||
'اینجا تاریخچهٔ همگامسازی قیمت را میبینید. دکمهٔ «بهروزرسانی قیمت» همان لحظه قیمت را از منبعی که در تنظیمات مشخص کردهاید میگیرد و روی محصولات اعمال میکند. اگر قیمت عوض نشده باشد پیام «تغییری نکرد» میآید.'
|
'اینجا تاریخچهٔ همگامسازی قیمت را میبینید. دکمهٔ «بهروزرسانی قیمت» همان لحظه قیمت را از منبعی که در تنظیمات مشخص کردهاید میگیرد و روی محصولات اعمال میکند. اگر قیمت عوض نشده باشد پیام «تغییری نکرد» میآید.'
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ export function getConfigApp(key: string) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** true اگر ui_config یا app_ui_config در get_user مقدار داشته باشد */
|
||||||
|
export function hasAppUiConfig(): boolean {
|
||||||
|
const cfg = getConfigApp('ui_config') ?? getConfigApp('app_ui_config')
|
||||||
|
if (cfg == null) return false
|
||||||
|
if (typeof cfg === 'string') return cfg.trim().length > 0
|
||||||
|
if (Array.isArray(cfg)) return cfg.length > 0
|
||||||
|
if (typeof cfg === 'object') return Object.keys(cfg as object).length > 0
|
||||||
|
return Boolean(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
let storeModeFromSettings: boolean | null = null
|
let storeModeFromSettings: boolean | null = null
|
||||||
let settingsBootstrapped = false
|
let settingsBootstrapped = false
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user