feat: website settings menu gated by has_landing

Add تنظیمات وبسایت before پیکربندی with landing_* subsections, and show unit addon on numeric setting fields.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-05 17:01:51 +03:30
co-authored by Cursor
parent dec36cc31e
commit e0b89223ac
11 changed files with 325 additions and 21 deletions
+2 -1
View File
@@ -29,7 +29,7 @@ import BasicMobileOverflowMenu from '@/components/BasicMobileOverflowMenu.vue'
import { sortBy } from 'lodash' import { sortBy } from 'lodash'
import { computed } from 'vue' import { computed } from 'vue'
import type { RouteRecordName, RouteRecordRaw } from 'vue-router' import type { RouteRecordName, RouteRecordRaw } from 'vue-router'
import { supportChatMenuEnabledSetting } from '@/utils/config' import { hasLandingSetting, supportChatMenuEnabledSetting } from '@/utils/config'
const BOTTOM_NAV_PRIORITY: (string | symbol)[] = ['dashboard', 'customers', 'orders', 'transactions'] const BOTTOM_NAV_PRIORITY: (string | symbol)[] = ['dashboard', 'customers', 'orders', 'transactions']
@@ -44,6 +44,7 @@ const allMenuItems = computed(() =>
if (item.name === 'support' || item.meta?.permissionsKey === 'support') { if (item.name === 'support' || item.meta?.permissionsKey === 'support') {
return supportChatMenuEnabledSetting.value return supportChatMenuEnabledSetting.value
} }
if (item.name === 'websiteSettings') return hasLandingSetting.value
return true return true
}), }),
(item) => item.meta?.order (item) => item.meta?.order
+2 -1
View File
@@ -36,7 +36,7 @@ import BaseMenuItem from '@core/layouts/composnts/BaseMenuItem.vue'
import type { RouteRecordName, RouteRecordRaw } from 'vue-router' import type { RouteRecordName, RouteRecordRaw } from 'vue-router'
import { useRouterStore } from '@core/stores/router' import { useRouterStore } from '@core/stores/router'
import { useAdminTheme } from '@core/composable/useAdminTheme' import { useAdminTheme } from '@core/composable/useAdminTheme'
import { supportChatMenuEnabledSetting } from '@/utils/config' import { hasLandingSetting, supportChatMenuEnabledSetting } from '@/utils/config'
const { prefixCls } = usePrefix('basic-menu') const { prefixCls } = usePrefix('basic-menu')
const layoutStore = useLayoutStore() const layoutStore = useLayoutStore()
@@ -79,6 +79,7 @@ const menu: ComputedRef<RouteRecordRaw[]> = computed(() => {
}).filter((item) => { }).filter((item) => {
if (item.meta?.hideMenu) return false if (item.meta?.hideMenu) return false
if (isSupportMenuRoute(item)) return supportChatMenuEnabledSetting.value if (isSupportMenuRoute(item)) return supportChatMenuEnabledSetting.value
if (item.name === 'websiteSettings') return hasLandingSetting.value
return true return true
}) })
}) })
@@ -97,6 +97,7 @@
:value="priceValue" :value="priceValue"
:disabled="disabled" :disabled="disabled"
class="w-full" class="w-full"
:addon-after="unitLabel || undefined"
:formatter="(v: string) => `${v}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')" :formatter="(v: string) => `${v}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')"
:parser="(v: string) => sanitizeNumericString(v, { allowDecimal: false })" :parser="(v: string) => sanitizeNumericString(v, { allowDecimal: false })"
@update:value="onPriceChange" @update:value="onPriceChange"
@@ -109,6 +110,7 @@
inputmode="numeric" inputmode="numeric"
allow-clear allow-clear
class="w-full" class="w-full"
:addon-after="unitLabel || undefined"
@update:value="onPlainNumberChange" @update:value="onPlainNumberChange"
@blur="emit('blur')" @blur="emit('blur')"
/> />
@@ -144,6 +146,7 @@ import TemplateTextField from './TemplateTextField.vue'
import ZaarboxPriceKeyField from './ZaarboxPriceKeyField.vue' import ZaarboxPriceKeyField from './ZaarboxPriceKeyField.vue'
import { hasTemplateField } from './templateFields' import { hasTemplateField } from './templateFields'
import { import {
getSettingUnitLabel,
normalizeTimeValue, normalizeTimeValue,
parseSelectOptions, parseSelectOptions,
resolveSettingFieldType, resolveSettingFieldType,
@@ -182,6 +185,8 @@ const fieldType = computed<SettingFieldType>(() =>
}) })
) )
const unitLabel = computed(() => getSettingUnitLabel(props.settingKey, fieldType.value))
const isZaarboxPriceKey = computed(() => props.settingKey === 'zaarbox_price_key') const isZaarboxPriceKey = computed(() => props.settingKey === 'zaarbox_price_key')
const isMontzarProductId = computed(() => props.settingKey === 'montzar_product_id') const isMontzarProductId = computed(() => props.settingKey === 'montzar_product_id')
const isMainWalletProducts = computed(() => props.settingKey === 'main_wallet_products') const isMainWalletProducts = computed(() => props.settingKey === 'main_wallet_products')
+48 -10
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="h-full min-h-0"> <div class="h-full min-h-0">
<BasicShowContent :title="pageTitle" :help="configurationPageIntro" class="h-full"> <BasicShowContent :title="pageTitle" :help="pageHelp" class="h-full">
<div class="settings-toolbar"> <div class="settings-toolbar">
<a-input <a-input
v-model:value="search" v-model:value="search"
@@ -207,10 +207,10 @@ import { loading } from '@/utils'
import { getEditableConfiguration, getSystemConfiguration, setConfiguration, setSystemConfiguration, refreshSuperAdminSettings } from '@/api/configuration' import { getEditableConfiguration, getSystemConfiguration, setConfiguration, setSystemConfiguration, refreshSuperAdminSettings } from '@/api/configuration'
import SettingValueField from './SettingValueField.vue' import SettingValueField from './SettingValueField.vue'
import SettingCard from './SettingCard.vue' import SettingCard from './SettingCard.vue'
import { configurationPageIntro } from '@/utils/adminHelpCopy' import { configurationPageIntro, websiteSettingsPageIntro } from '@/utils/adminHelpCopy'
import { refreshAppModeMenu } from '@/router' import { refreshAppModeMenu } from '@/router'
import { useUserStore } from '@/store/userStore' import { useUserStore } from '@/store/userStore'
import { applyBannerEnabledFromRows, applyTrackingSystemsEnabledFromRows, applyConditionalOrderEnabledFromRows, applyAutomationEnabledFromRows, applyProductFundsEnabledFromRows, applyPriceTriggerEnabledFromRows, applyAppNotificationEnabledFromRows, applyPriceProviderFromRows, applyPriceChannelAccessFromRows, applySupportChatMenuEnabledFromRows, applyGroupSmsFromRows, applyCustomerSpecialPricingFromRows, applyGoldCreditEnabledFromRows, applyCustomerDirectSmsFromRows, applyCustomerLevelProfitEnabledFromRows, applySuperAdminAccessFlagsFromItems, invalidateSettingsBootstrap, setBannerEnabledFromSettings, setConditionalOrderEnabledFromSettings, setAutomationEnabledFromSettings, setProductFundsEnabledFromSettings, setPriceTriggerEnabledFromSettings, setAppNotificationEnabledFromSettings, setPriceProviderFromSettings, setPriceChannelAccessFromSettings, setStoreModeFromSettings, setTrackingSystemsEnabledFromSettings, setGroupSmsEnabledFromSettings, setCustomerSpecialPricingEnabledFromSettings, setGoldCreditEnabledFromSettings } from '@/utils/config' import { applyBannerEnabledFromRows, applyTrackingSystemsEnabledFromRows, applyConditionalOrderEnabledFromRows, applyAutomationEnabledFromRows, applyProductFundsEnabledFromRows, applyPriceTriggerEnabledFromRows, applyAppNotificationEnabledFromRows, applyPriceProviderFromRows, applyPriceChannelAccessFromRows, applySupportChatMenuEnabledFromRows, applyGroupSmsFromRows, applyCustomerSpecialPricingFromRows, applyGoldCreditEnabledFromRows, applyCustomerDirectSmsFromRows, applyCustomerLevelProfitEnabledFromRows, applySuperAdminAccessFlagsFromItems, invalidateSettingsBootstrap, setBannerEnabledFromSettings, setConditionalOrderEnabledFromSettings, setAutomationEnabledFromSettings, setProductFundsEnabledFromSettings, setPriceTriggerEnabledFromSettings, setAppNotificationEnabledFromSettings, setPriceProviderFromSettings, setPriceChannelAccessFromSettings, setStoreModeFromSettings, setTrackingSystemsEnabledFromSettings, setGroupSmsEnabledFromSettings, setCustomerSpecialPricingEnabledFromSettings, setGoldCreditEnabledFromSettings, applyHasLandingFromRows, setHasLandingFromSettings } from '@/utils/config'
import { import {
EDITABLE_SETTING_GROUPS, EDITABLE_SETTING_GROUPS,
SYSTEM_SETTING_GROUPS, SYSTEM_SETTING_GROUPS,
@@ -218,6 +218,12 @@ import {
OTHER_PAGE_SETTING_MENUS, OTHER_PAGE_SETTING_MENUS,
type SettingGroupDef type SettingGroupDef
} from './settingGroups' } from './settingGroups'
import {
LANDING_SETTING_GROUPS,
isLandingContentKey,
landingSectionByPrefix,
matchesLandingPrefix
} from './landingSettings'
import { import {
SETTING_TYPE_OPTIONS, SETTING_TYPE_OPTIONS,
formatValueForSave, formatValueForSave,
@@ -233,8 +239,9 @@ import {
} from './settingTypes' } from './settingTypes'
import { TRADING_HOURS_SETTING_KEY_SET } from './tradingHoursKeys' import { TRADING_HOURS_SETTING_KEY_SET } from './tradingHoursKeys'
const props = withDefaults(defineProps<{ mode?: 'editable' | 'system' }>(), { const props = withDefaults(defineProps<{ mode?: 'editable' | 'system' | 'landing'; landingPrefix?: string }>(), {
mode: 'editable' mode: 'editable',
landingPrefix: ''
}) })
const userStore = useUserStore() const userStore = useUserStore()
@@ -243,11 +250,24 @@ function refreshMenuAfterStoreChange() {
} }
const isSystemMode = computed(() => props.mode === 'system') const isSystemMode = computed(() => props.mode === 'system')
const pageTitle = computed(() => (isSystemMode.value ? 'تنظیمات پیشرفته' : 'تنظیمات')) const isLandingMode = computed(() => props.mode === 'landing')
const pageTitle = computed(() => {
if (isLandingMode.value) {
return landingSectionByPrefix(props.landingPrefix)?.title ?? 'تنظیمات وبسایت'
}
return isSystemMode.value ? 'تنظیمات پیشرفته' : 'تنظیمات'
})
const pageHelp = computed(() => (isLandingMode.value ? websiteSettingsPageIntro : configurationPageIntro))
const refreshingSuperAdmin = ref(false) const refreshingSuperAdmin = ref(false)
const settingGroups = computed(() => const settingGroups = computed(() => {
isSystemMode.value ? SYSTEM_SETTING_GROUPS : EDITABLE_SETTING_GROUPS if (isLandingMode.value) {
) const prefix = props.landingPrefix || ''
return prefix
? LANDING_SETTING_GROUPS.filter((g) => g.id === prefix)
: LANDING_SETTING_GROUPS
}
return isSystemMode.value ? SYSTEM_SETTING_GROUPS : EDITABLE_SETTING_GROUPS
})
const settingTypeOptions = SETTING_TYPE_OPTIONS const settingTypeOptions = SETTING_TYPE_OPTIONS
@@ -327,6 +347,13 @@ const filteredRows = computed(() =>
(baseData.value || []).filter((row) => { (baseData.value || []).filter((row) => {
if (row.id == null || !matchesFilter(row)) return false if (row.id == null || !matchesFilter(row)) return false
if (!isSettingVisible(row.key)) return false if (!isSettingVisible(row.key)) return false
if (isLandingMode.value) {
if (!isEditable(row)) return false
const prefix = props.landingPrefix || ''
if (prefix) return matchesLandingPrefix(row.key || '', prefix)
return isLandingContentKey(row.key)
}
if (isLandingContentKey(row.key)) return false
const menu = String(row.menu ?? '').trim() const menu = String(row.menu ?? '').trim()
if (OTHER_PAGE_SETTING_MENUS.has(menu)) return false if (OTHER_PAGE_SETTING_MENUS.has(menu)) return false
if (!menu && !isSystemMode.value && TRADING_HOURS_SETTING_KEY_SET.has(row.key || '')) { if (!menu && !isSystemMode.value && TRADING_HOURS_SETTING_KEY_SET.has(row.key || '')) {
@@ -486,7 +513,7 @@ onMounted(() => {
}) })
watch( watch(
() => props.mode, () => [props.mode, props.landingPrefix],
() => { () => {
search.value = '' search.value = ''
filterType.value = undefined filterType.value = undefined
@@ -608,6 +635,7 @@ async function initConfig() {
applyPriceProviderFromRows(baseData.value) applyPriceProviderFromRows(baseData.value)
applyPriceChannelAccessFromRows(baseData.value) applyPriceChannelAccessFromRows(baseData.value)
applySupportChatMenuEnabledFromRows(baseData.value) applySupportChatMenuEnabledFromRows(baseData.value)
applyHasLandingFromRows(baseData.value)
applyGroupSmsFromRows(baseData.value) applyGroupSmsFromRows(baseData.value)
applyCustomerSpecialPricingFromRows(baseData.value) applyCustomerSpecialPricingFromRows(baseData.value)
applyGoldCreditEnabledFromRows(baseData.value) applyGoldCreditEnabledFromRows(baseData.value)
@@ -616,6 +644,7 @@ async function initConfig() {
!isSystemMode.value || !isSystemMode.value ||
!baseData.value.some((row) => row.key === 'price_provider') || !baseData.value.some((row) => row.key === 'price_provider') ||
!baseData.value.some((row) => row.key === 'show_to_users_chat') || !baseData.value.some((row) => row.key === 'show_to_users_chat') ||
!baseData.value.some((row) => row.key === 'has_landing') ||
!baseData.value.some((row) => row.key === 'price_channel_access') || !baseData.value.some((row) => row.key === 'price_channel_access') ||
!baseData.value.some((row) => row.key === 'group_sms') || !baseData.value.some((row) => row.key === 'group_sms') ||
!baseData.value.some((row) => row.key === 'customer_special_pricing') || !baseData.value.some((row) => row.key === 'customer_special_pricing') ||
@@ -627,6 +656,7 @@ async function initConfig() {
const rows = Array.isArray(all) ? all : [] const rows = Array.isArray(all) ? all : []
applyPriceProviderFromRows(rows) applyPriceProviderFromRows(rows)
applySupportChatMenuEnabledFromRows(rows) applySupportChatMenuEnabledFromRows(rows)
applyHasLandingFromRows(rows)
applyPriceChannelAccessFromRows(rows) applyPriceChannelAccessFromRows(rows)
applyGroupSmsFromRows(rows) applyGroupSmsFromRows(rows)
applyCustomerSpecialPricingFromRows(rows) applyCustomerSpecialPricingFromRows(rows)
@@ -701,6 +731,14 @@ function syncMenuFlagsFromSetting(key: string | undefined, value: string) {
if (row) row.value = value if (row) row.value = value
} }
} }
if (key === 'has_landing') {
setHasLandingFromSettings(isSwitchOn(value))
const list = userStore.userInfo?.settings
if (Array.isArray(list)) {
const row = list.find((s: any) => s?.key === key)
if (row) row.value = value
}
}
if (key === 'price_channel_access') { if (key === 'price_channel_access') {
setPriceChannelAccessFromSettings(isSwitchOn(value)) setPriceChannelAccessFromSettings(isSwitchOn(value))
} }
@@ -0,0 +1,40 @@
export type LandingSection = {
prefix: string
title: string
icon: string
name: string
path: string
}
export const LANDING_SETTING_SECTIONS: LandingSection[] = [
{ prefix: 'landing_site', title: 'تنظیمات کلی سایت', icon: 'vuesax-linear:global', name: 'websiteLandingSite', path: 'site' },
{ prefix: 'landing_header', title: 'هدر و ناوبری', icon: 'vuesax-linear:menu', name: 'websiteLandingHeader', path: 'header' },
{ prefix: 'landing_hero', title: 'هیرو', icon: 'vuesax-linear:image', name: 'websiteLandingHero', path: 'hero' },
{ prefix: 'landing_live_price', title: 'لیبل‌های قیمت لحظه‌ای', icon: 'vuesax-linear:chart-2', name: 'websiteLandingLivePrice', path: 'live-price' },
{ prefix: 'landing_calculator', title: 'عنوان ماشین حساب', icon: 'vuesax-linear:calculator', name: 'websiteLandingCalculator', path: 'calculator' },
{ prefix: 'landing_feature', title: 'امکانات (چرا نیازی طلا)', icon: 'vuesax-linear:star', name: 'websiteLandingFeature', path: 'feature' },
{ prefix: 'landing_steps', title: 'مراحل شروع', icon: 'vuesax-linear:ranking', name: 'websiteLandingSteps', path: 'steps' },
{ prefix: 'landing_trust', title: 'اعتماد و مجوزها', icon: 'vuesax-linear:shield-tick', name: 'websiteLandingTrust', path: 'trust' },
{ prefix: 'landing_faq', title: 'سوالات متداول', icon: 'vuesax-linear:message-question', name: 'websiteLandingFaq', path: 'faq' },
{ prefix: 'landing_buttom_cta', title: 'سکشن پایین صفحه لندینگ برای معرفی', icon: 'vuesax-linear:arrow-down', name: 'websiteLandingBottomCta', path: 'bottom-cta' },
{ prefix: 'landing_footer', title: 'فوتر', icon: 'vuesax-linear:document', name: 'websiteLandingFooter', path: 'footer' }
]
export function matchesLandingPrefix(key: string, prefix: string): boolean {
return key === prefix || key.startsWith(`${prefix}_`)
}
export function isLandingContentKey(key?: string | null): boolean {
return String(key ?? '').trim().startsWith('landing_')
}
export function landingSectionByPrefix(prefix: string): LandingSection | undefined {
return LANDING_SETTING_SECTIONS.find((s) => s.prefix === prefix)
}
export const LANDING_SETTING_GROUPS = LANDING_SETTING_SECTIONS.map((section) => ({
id: section.prefix,
title: section.title,
icon: section.icon,
match: (key: string) => matchesLandingPrefix(key, section.prefix)
}))
@@ -158,7 +158,7 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
icon: 'vuesax-linear:people', icon: 'vuesax-linear:people',
// فقط فلگ‌های مالک سوپرادمین — banner/tracking/نمایش محصول در «تنظیمات» عادی‌اند // فقط فلگ‌های مالک سوپرادمین — banner/tracking/نمایش محصول در «تنظیمات» عادی‌اند
match: (k) => match: (k) =>
/^(enable_automation|enable_conditional_order|enable_product_funds|has_page_price_trigger|has_page_notification|has_page_gift_card|enable_customer_level_profit|jibit_enabled|customer_direct_sms|has_page_earn_money|group_sms|customer_special_pricing|enable_gold_credit|permision__enabled|show_to_users_chat)$/.test( /^(enable_automation|enable_conditional_order|enable_product_funds|has_page_price_trigger|has_page_notification|has_page_gift_card|enable_customer_level_profit|jibit_enabled|customer_direct_sms|has_page_earn_money|group_sms|customer_special_pricing|enable_gold_credit|permision__enabled|show_to_users_chat|has_landing)$/.test(
k k
) )
}, },
@@ -310,7 +310,7 @@ export function resolveSettingMenu(
} }
/** این کلیدها صفحهٔ جدا دارند و در لیست تنظیمات عادی/پیشرفته نمی‌آیند */ /** این کلیدها صفحهٔ جدا دارند و در لیست تنظیمات عادی/پیشرفته نمی‌آیند */
export const OTHER_PAGE_SETTING_MENUS = new Set(['trading_hours', 'capture_sites']) export const OTHER_PAGE_SETTING_MENUS = new Set(['trading_hours', 'capture_sites', 'website_settings'])
export function getSettingSubsectionId( export function getSettingSubsectionId(
key: string, key: string,
@@ -207,6 +207,7 @@ export const SUPERADMIN_OWNED_SETTING_KEYS = new Set([
'jibit_enabled', 'jibit_enabled',
'permision__enabled', 'permision__enabled',
'show_to_users_chat', 'show_to_users_chat',
'has_landing',
]) ])
export function isSuperAdminOwnedSettingKey(key?: string | null): boolean { export function isSuperAdminOwnedSettingKey(key?: string | null): boolean {
@@ -324,7 +325,7 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
minimum_sell: 'price', minimum_sell: 'price',
gift_for_referrer: 'number', gift_for_referrer: 'number',
gift_for_referral: 'number', gift_for_referral: 'number',
referral_bonus_amount: 'text', referral_bonus_amount: 'price',
montzar_product_id: 'number', montzar_product_id: 'number',
montzar_account_zarin: 'text', montzar_account_zarin: 'text',
montzar_account_tahesab: 'text', montzar_account_tahesab: 'text',
@@ -429,7 +430,19 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
pargar_item_id_gold: 'number', pargar_item_id_gold: 'number',
support_messengers: 'longtext', support_messengers: 'longtext',
show_to_users_chat: 'switch', show_to_users_chat: 'switch',
has_landing: 'switch',
has_chat: 'switch', has_chat: 'switch',
landing_site: 'longtext',
landing_header: 'longtext',
landing_hero: 'longtext',
landing_live_price: 'longtext',
landing_calculator: 'longtext',
landing_feature: 'longtext',
landing_steps: 'longtext',
landing_trust: 'longtext',
landing_faq: 'longtext',
landing_buttom_cta: 'longtext',
landing_footer: 'longtext',
chat_api: 'text', chat_api: 'text',
chat_api_key: 'text', chat_api_key: 'text',
chat_upload_url: 'text', chat_upload_url: 'text',
@@ -623,6 +636,45 @@ export function formatValueForSave(
return value ?? '' return value ?? ''
} }
/** واحد نمایش روی فیلدهای عددی — شناسه/پورت واحد ندارند */
const SETTING_UNIT_BY_KEY: Record<string, string> = {
buy_fee: 'ریال',
sell_fee: 'ریال',
fee_single: 'ریال',
fee_conditional_order: 'ریال',
min_withdraw: 'ریال',
min_deposit: 'ریال',
min_first_buy: 'ریال',
minimum_sell: 'ریال',
price_channel_diff: 'ریال',
referral_bonus_amount: 'ریال',
gold_credit_penalty_amount: 'ریال / گرم',
gift_for_referrer: 'گرم',
gift_for_referral: 'گرم',
gold_credit_min_balance_gram: 'گرم',
change_mesghal: 'گرم',
fee_service_charge_percentage: 'درصد (اعشاری)',
gold_credit_grace_hours: 'ساعت',
gold_credit_default_months: 'ماه',
price_channel_min_seconds: 'ثانیه',
referral_limit: 'دعوت',
fee_free: 'سفارش',
}
export function getSettingUnitLabel(
key?: string,
type?: SettingFieldType,
): string {
const k = (key ?? '').trim()
if (k && SETTING_UNIT_BY_KEY[k]) {
return SETTING_UNIT_BY_KEY[k]
}
if (type === 'price') {
return 'ریال'
}
return ''
}
export function switchLabels(key: string | undefined): { on: string; off: string } { export function switchLabels(key: string | undefined): { on: string; off: string } {
if (key === REFERRAL_REQUIRED_KEY) { if (key === REFERRAL_REQUIRED_KEY) {
return { on: 'اجباری', off: 'اختیاری' } return { on: 'اجباری', off: 'اختیاری' }
@@ -800,7 +852,7 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
referral_limit: referral_limit:
'حداکثر تعداد دعوت مجاز برای هر کاربر. خالی = بدون محدودیت.', 'حداکثر تعداد دعوت مجاز برای هر کاربر. خالی = بدون محدودیت.',
referral_bonus_amount: referral_bonus_amount:
'مبلغ یا مقدار پاداش معرف برای نمایش در پیامک‌ها و متن‌های دعوت.', 'مبلغ پاداش معرف (ریال) برای نمایش در پیامک‌ها و متن‌های دعوت. واحد روی خود فیلد مشخص است.',
jibit_verification: jibit_verification:
'روشن: هنگام ثبت یا ویرایش کارت بانکی، تطبیق با سرویس احراز هویت جیبیت انجام می‌شود. خاموش: کارت بدون تطبیق جیبیت ثبت می‌شود.', 'روشن: هنگام ثبت یا ویرایش کارت بانکی، تطبیق با سرویس احراز هویت جیبیت انجام می‌شود. خاموش: کارت بدون تطبیق جیبیت ثبت می‌شود.',
register_jibit_verify: register_jibit_verify:
@@ -863,6 +915,19 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
'فقط مواردی که در این لیست ثبت شوند در منوی شناور پشتیبانی اپ مشتری نمایش داده می‌شوند. برای تماس تلفنی یا پیامک، نوع «تماس تلفنی» / «پیامک» را با شماره اضافه کنید. لیست می‌تواند خالی باشد؛ اگر چت فعال باشد، پشتیبانی مستقیم به صفحه چت می‌رود.', 'فقط مواردی که در این لیست ثبت شوند در منوی شناور پشتیبانی اپ مشتری نمایش داده می‌شوند. برای تماس تلفنی یا پیامک، نوع «تماس تلفنی» / «پیامک» را با شماره اضافه کنید. لیست می‌تواند خالی باشد؛ اگر چت فعال باشد، پشتیبانی مستقیم به صفحه چت می‌رود.',
show_to_users_chat: show_to_users_chat:
'روشن: قابلیت چت پشتیبانی برای این دیپلوی فعال است (منوی ادمین و پیش‌نیاز نمایش به کاربر). خاموش: چت در ادمین و اپ کاربر مخفی است و سوییچ «چت پشتیبانی» (has_chat) از تنظیمات عادی هم برداشته می‌شود.', 'روشن: قابلیت چت پشتیبانی برای این دیپلوی فعال است (منوی ادمین و پیش‌نیاز نمایش به کاربر). خاموش: چت در ادمین و اپ کاربر مخفی است و سوییچ «چت پشتیبانی» (has_chat) از تنظیمات عادی هم برداشته می‌شود.',
has_landing:
'روشن: منوی تنظیمات وبسایت در ادمین و تنظیمات لندینگ برای اپ کاربر فعال است. خاموش: منو مخفی است.',
landing_site: 'محتوای تنظیمات کلی لندینگ وبسایت.',
landing_header: 'محتوای هدر و ناوبری لندینگ.',
landing_hero: 'محتوای سکشن هیرو لندینگ.',
landing_live_price: 'لیبل‌ها و متن قیمت لحظه‌ای لندینگ.',
landing_calculator: 'عنوان و متن ماشین حساب لندینگ.',
landing_feature: 'متن امکانات لندینگ (چرا نیازی طلا).',
landing_steps: 'متن مراحل شروع لندینگ.',
landing_trust: 'متن اعتماد و مجوزها (بدون اینماد).',
landing_faq: 'سوالات متداول لندینگ.',
landing_buttom_cta: 'متن دعوت به اقدام پایین صفحه لندینگ.',
landing_footer: 'محتوای فوتر لندینگ.',
has_chat: has_chat:
'روشن: چت پشتیبانی در اپ مشتری نمایش داده می‌شود (فقط وقتی show_to_users_chat هم روشن باشد). خاموش: دکمه و صفحه چت برای کاربر مخفی است.', 'روشن: چت پشتیبانی در اپ مشتری نمایش داده می‌شود (فقط وقتی show_to_users_chat هم روشن باشد). خاموش: دکمه و صفحه چت برای کاربر مخفی است.',
chat_api: chat_api:
@@ -1119,6 +1184,8 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
minimum_sell: 'حداقل مبلغ فروش (ریال)', minimum_sell: 'حداقل مبلغ فروش (ریال)',
referral_id: 'کد معرف اجباری', referral_id: 'کد معرف اجباری',
referral_bonus_amount: 'مقدار پاداش معرف', referral_bonus_amount: 'مقدار پاداش معرف',
gift_for_referrer: 'پاداش معرف',
gift_for_referral: 'پاداش معرف‌شونده',
referral_gift_on_first_order: 'پاداش معرف پس از اولین سفارش', referral_gift_on_first_order: 'پاداش معرف پس از اولین سفارش',
referral_gift_buy_from_montzar: 'خرید پاداش معرفی از همکار', referral_gift_buy_from_montzar: 'خرید پاداش معرفی از همکار',
open_register: 'ثبت‌نام باز', open_register: 'ثبت‌نام باز',
@@ -1187,7 +1254,19 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
zarin_idbank: 'کد حساب بانک زرین', zarin_idbank: 'کد حساب بانک زرین',
support_messengers: 'پیام‌رسان‌های پشتیبانی', support_messengers: 'پیام‌رسان‌های پشتیبانی',
show_to_users_chat: 'نمایش چت به کاربران', show_to_users_chat: 'نمایش چت به کاربران',
has_landing: 'لندینگ وبسایت',
has_chat: 'چت پشتیبانی', has_chat: 'چت پشتیبانی',
landing_site: 'تنظیمات کلی سایت',
landing_header: 'هدر و ناوبری',
landing_hero: 'هیرو',
landing_live_price: 'لیبل‌های قیمت لحظه‌ای',
landing_calculator: 'عنوان ماشین حساب',
landing_feature: 'امکانات (چرا نیازی طلا)',
landing_steps: 'مراحل شروع',
landing_trust: 'اعتماد و مجوزها',
landing_faq: 'سوالات متداول',
landing_buttom_cta: 'سکشن پایین صفحه لندینگ برای معرفی',
landing_footer: 'فوتر',
chat_api: 'API چت', chat_api: 'API چت',
chat_api_key: 'کلید API چت', chat_api_key: 'کلید API چت',
chat_upload_url: 'آدرس آپلود فایل چت', chat_upload_url: 'آدرس آپلود فایل چت',
@@ -1286,7 +1365,7 @@ export const SETTING_LOCKED_KEYS = new Set([
'price_channel_bale_to', 'price_channel_telegram_to', 'price_channel_rubika_to', 'price_channel_bale_to', 'price_channel_telegram_to', 'price_channel_rubika_to',
'unit', 'unit',
'wss_enabled', 'wss_enabled',
'show_to_users_chat', 'chat_api', 'chat_api_key', 'chat_upload_url', 'chat_download_url' 'show_to_users_chat', 'has_landing', 'chat_api', 'chat_api_key', 'chat_upload_url', 'chat_download_url'
]) ])
/** قابل ویرایش در پنل عادی — هم‌تراز deploy_settings_const.sql */ /** قابل ویرایش در پنل عادی — هم‌تراز deploy_settings_const.sql */
@@ -1326,6 +1405,8 @@ export const SETTING_EDITABLE_KEYS = new Set([
'support_messengers', 'support_messenger_text', 'support_messengers', 'support_messenger_text',
'do_send_liman', 'do_send_liman',
'has_chat', 'has_chat',
'landing_site', 'landing_header', 'landing_hero', 'landing_live_price', 'landing_calculator',
'landing_feature', 'landing_steps', 'landing_trust', 'landing_faq', 'landing_buttom_cta', 'landing_footer',
'text_about', 'text_rule', 'about_software', 'enamad_url', 'enamad_logo', 'text_about', 'text_rule', 'about_software', 'enamad_url', 'enamad_logo',
'login_hero_eyebrow', 'login_hero_title', 'login_hero_eyebrow', 'login_hero_title',
'login_hero_feature_1', 'login_hero_feature_2', 'login_hero_feature_3', 'login_hero_feature_4', 'login_hero_feature_1', 'login_hero_feature_2', 'login_hero_feature_3', 'login_hero_feature_4',
@@ -1336,10 +1417,14 @@ export const SETTING_EDITABLE_KEYS = new Set([
]) ])
/** عنوان فارسی — کاتالوگ پنل بر title_fa دیتابیس اولویت دارد */ /** عنوان فارسی — کاتالوگ پنل بر title_fa دیتابیس اولویت دارد */
export function resolveSettingTitle(row: Pick<Const, 'key' | 'title_fa'>): string { export function resolveSettingTitle(row: Pick<Const, 'key' | 'title_fa' | 'type'>): string {
const key = (row.key ?? '').trim() const key = (row.key ?? '').trim()
if (key && KEY_LABEL_FALLBACK[key]) return KEY_LABEL_FALLBACK[key] const base = (key && KEY_LABEL_FALLBACK[key]) ? KEY_LABEL_FALLBACK[key] : (row.title_fa?.trim() || key || '—')
return row.title_fa?.trim() || key || '—' const unit = getSettingUnitLabel(key, resolveSettingFieldType(row))
if (!unit || base.includes(unit) || base.includes(`(${unit})`)) {
return base
}
return `${base} (${unit})`
} }
/** توضیح — کاتالوگ پنل بر description دیتابیس اولویت دارد */ /** توضیح — کاتالوگ پنل بر description دیتابیس اولویت دارد */
@@ -0,0 +1,63 @@
<template>
<div class="website-settings">
<BasicMainCard>
<div class="flex h-full min-h-0 flex-col md:flex-row">
<BasicMenu
width="100%"
:menu="menu"
mode="horizontal"
class="website-settings__menu website-settings__menu--mobile overflow-auto md:hidden"
/>
<BasicMenu
width="17rem"
:menu="menu"
class="website-settings__menu website-settings__menu--side hidden min-w-[17rem] overflow-auto md:inline"
/>
<RouterView
:key="route.name"
class="mt-5 min-h-0 w-full flex-1 overflow-hidden md:mr-5 md:mt-0"
/>
</div>
</BasicMainCard>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import BasicMenu from '@/components/BasicMenu/index.vue'
import { useRouterStore } from '@core/stores/router'
import { get } from 'lodash'
import type { RouteRecordRaw } from 'vue-router'
const routerStore = useRouterStore()
const route = useRoute()
const menu = computed(() => {
const item = routerStore.routes.find((row) => row.name === 'websiteSettings')
const children = get(item, 'children[0].children', []) as RouteRecordRaw[]
return children
.filter((child) => !child.meta?.hideMenu)
.sort((a, b) => (Number(a.meta?.order) || 99) - (Number(b.meta?.order) || 99))
})
</script>
<style scoped lang="less">
.website-settings__menu {
:deep(.ant-menu-item),
:deep(.ant-menu-submenu-title) {
height: auto !important;
min-height: 2.5rem;
line-height: 1.35 !important;
white-space: normal !important;
padding-block: 0.55rem !important;
}
:deep(.ant-menu-title-content) {
white-space: normal !important;
overflow: visible !important;
text-overflow: clip !important;
line-height: 1.35;
}
}
</style>
+52
View File
@@ -0,0 +1,52 @@
import type { RouteRecordRaw } from 'vue-router'
import { hasLandingSetting } from '@/utils/config'
import { LANDING_SETTING_SECTIONS } from '@/pages/menuPages/configuration/landingSettings'
const websiteSettings: RouteRecordRaw = {
name: 'websiteSettings',
path: '/website-settings',
component: () => import('@core/layouts/BasicLayout.vue'),
redirect: { name: 'websiteLandingSite' },
meta: {
icon: 'vuesax-linear:global',
permissionsKey: 'config',
title: 'تنظیمات وبسایت',
disabled: false,
hideMenu: false,
hideChildren: true,
order: 7.4
},
beforeEnter() {
if (!hasLandingSetting.value) {
return { name: 'configuration' }
}
},
children: [
{
name: 'websiteSettingsMain',
path: '',
component: () => import('@/pages/menuPages/websiteSettings/index.vue'),
redirect: { name: 'websiteLandingSite' },
meta: {
permissionsKey: 'config',
icon: 'vuesax-linear:global',
title: 'تنظیمات وبسایت',
disabled: false,
hideMenu: true
},
children: LANDING_SETTING_SECTIONS.map((section, index) => ({
name: section.name,
path: section.path,
component: () => import('@/pages/menuPages/configuration/index.vue'),
props: { mode: 'landing' as const, landingPrefix: section.prefix },
meta: {
icon: section.icon,
title: section.title,
order: index + 1
}
}))
}
]
}
export default websiteSettings
+3
View File
@@ -246,6 +246,9 @@ export const captureSitesPageIntro =
export const configurationPageIntro = export const configurationPageIntro =
'تنظیمات کلی سامانه (کلید–مقدار). با جستجو و فیلتر نوع فیلد، مورد را پیدا کنید؛ بعد از تغییر ذخیره کنید. روی آیکون i کنار هر تنظیم توضیح همان فیلد را ببینید.' 'تنظیمات کلی سامانه (کلید–مقدار). با جستجو و فیلتر نوع فیلد، مورد را پیدا کنید؛ بعد از تغییر ذخیره کنید. روی آیکون i کنار هر تنظیم توضیح همان فیلد را ببینید.'
export const websiteSettingsPageIntro =
'متن و تنظیمات صفحهٔ لندینگ وبسایت. هر زیرمنو کلیدهای همان بخش را نشان می‌دهد؛ بعد از تغییر ذخیره می‌شود و در اپ کاربر با get_settings در دسترس است.'
export const accountingReportsPageIntro = export const accountingReportsPageIntro =
'گزارش‌های حسابداری داخلی پنل. برای اسناد و همگام‌سازی مشتریان از تب‌های ته‌حساب / زرین / پرگار استفاده کنید.' 'گزارش‌های حسابداری داخلی پنل. برای اسناد و همگام‌سازی مشتریان از تب‌های ته‌حساب / زرین / پرگار استفاده کنید.'
+17 -1
View File
@@ -86,6 +86,9 @@ export const supportChatMenuEnabledSetting = ref(false)
/** reactive — سوییچ دیپلوی show_to_users_chat (برای مخفی کردن has_chat در تنظیمات عادی) */ /** reactive — سوییچ دیپلوی show_to_users_chat (برای مخفی کردن has_chat در تنظیمات عادی) */
export const showToUsersChatSetting = ref(false) export const showToUsersChatSetting = ref(false)
/** reactive — منوی تنظیمات وبسایت وقتی has_landing روشن است */
export const hasLandingSetting = ref(false)
/** reactive — سوییچ دیپلوی price_channel_access (گروه کانال در تنظیمات عادی) */ /** reactive — سوییچ دیپلوی price_channel_access (گروه کانال در تنظیمات عادی) */
export const priceChannelAccessSetting = ref(false) export const priceChannelAccessSetting = ref(false)
@@ -215,6 +218,10 @@ export function setShowToUsersChatFromSettings(value: boolean): void {
showToUsersChatSetting.value = value showToUsersChatSetting.value = value
} }
export function setHasLandingFromSettings(value: boolean): void {
hasLandingSetting.value = value
}
export function setPriceChannelAccessFromSettings(value: boolean): void { export function setPriceChannelAccessFromSettings(value: boolean): void {
priceChannelAccessSetting.value = value priceChannelAccessSetting.value = value
} }
@@ -313,7 +320,7 @@ export function invalidateSettingsBootstrap(): void {
} }
/** /**
* اعمال مستقیم ۱۵ دسترسی سوپرادمین روی فلگ‌های منو/UI * اعمال مستقیم ۱۶ دسترسی سوپرادمین روی فلگ‌های منو/UI
* (پاسخ refresh-superadmin.items یا ردیف‌های get_settings). * (پاسخ refresh-superadmin.items یا ردیف‌های get_settings).
*/ */
export function applySuperAdminAccessFlagsFromItems( export function applySuperAdminAccessFlagsFromItems(
@@ -333,6 +340,7 @@ export function applySuperAdminAccessFlagsFromItems(
applyPriceTriggerEnabledFromRows(rows) applyPriceTriggerEnabledFromRows(rows)
applyAppNotificationEnabledFromRows(rows) applyAppNotificationEnabledFromRows(rows)
applySupportChatMenuEnabledFromRows(rows) applySupportChatMenuEnabledFromRows(rows)
applyHasLandingFromRows(rows)
} }
export function applyConditionalOrderEnabledFromRows( export function applyConditionalOrderEnabledFromRows(
@@ -377,6 +385,13 @@ export function applyAppNotificationEnabledFromRows(
if (row) setAppNotificationEnabledFromSettings(isSwitchOn(row.value)) if (row) setAppNotificationEnabledFromSettings(isSwitchOn(row.value))
} }
export function applyHasLandingFromRows(
rows: Array<{ key?: string; value?: string | null }>
) {
const row = rows.find((item) => item.key === 'has_landing')
if (row) setHasLandingFromSettings(isSwitchOn(row.value))
}
export function applyPriceProviderFromRows( export function applyPriceProviderFromRows(
rows: Array<{ key?: string; value?: string | null }> rows: Array<{ key?: string; value?: string | null }>
) { ) {
@@ -419,6 +434,7 @@ async function bootstrapSettingsFromApi(): Promise<void> {
applyPriceTriggerEnabledFromRows(list) applyPriceTriggerEnabledFromRows(list)
applyAppNotificationEnabledFromRows(list) applyAppNotificationEnabledFromRows(list)
applySupportChatMenuEnabledFromRows(list) applySupportChatMenuEnabledFromRows(list)
applyHasLandingFromRows(list)
applyPriceProviderFromRows(list) applyPriceProviderFromRows(list)
applyPriceChannelAccessFromRows(list) applyPriceChannelAccessFromRows(list)
setCachedMainMetal(mainMetalRow?.value) setCachedMainMetal(mainMetalRow?.value)