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 { computed } from 'vue'
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']
@@ -44,6 +44,7 @@ const allMenuItems = computed(() =>
if (item.name === 'support' || item.meta?.permissionsKey === 'support') {
return supportChatMenuEnabledSetting.value
}
if (item.name === 'websiteSettings') return hasLandingSetting.value
return true
}),
(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 { useRouterStore } from '@core/stores/router'
import { useAdminTheme } from '@core/composable/useAdminTheme'
import { supportChatMenuEnabledSetting } from '@/utils/config'
import { hasLandingSetting, supportChatMenuEnabledSetting } from '@/utils/config'
const { prefixCls } = usePrefix('basic-menu')
const layoutStore = useLayoutStore()
@@ -79,6 +79,7 @@ const menu: ComputedRef<RouteRecordRaw[]> = computed(() => {
}).filter((item) => {
if (item.meta?.hideMenu) return false
if (isSupportMenuRoute(item)) return supportChatMenuEnabledSetting.value
if (item.name === 'websiteSettings') return hasLandingSetting.value
return true
})
})
@@ -97,6 +97,7 @@
:value="priceValue"
:disabled="disabled"
class="w-full"
:addon-after="unitLabel || undefined"
:formatter="(v: string) => `${v}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')"
:parser="(v: string) => sanitizeNumericString(v, { allowDecimal: false })"
@update:value="onPriceChange"
@@ -109,6 +110,7 @@
inputmode="numeric"
allow-clear
class="w-full"
:addon-after="unitLabel || undefined"
@update:value="onPlainNumberChange"
@blur="emit('blur')"
/>
@@ -144,6 +146,7 @@ import TemplateTextField from './TemplateTextField.vue'
import ZaarboxPriceKeyField from './ZaarboxPriceKeyField.vue'
import { hasTemplateField } from './templateFields'
import {
getSettingUnitLabel,
normalizeTimeValue,
parseSelectOptions,
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 isMontzarProductId = computed(() => props.settingKey === 'montzar_product_id')
const isMainWalletProducts = computed(() => props.settingKey === 'main_wallet_products')
+48 -10
View File
@@ -1,6 +1,6 @@
<template>
<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">
<a-input
v-model:value="search"
@@ -207,10 +207,10 @@ import { loading } from '@/utils'
import { getEditableConfiguration, getSystemConfiguration, setConfiguration, setSystemConfiguration, refreshSuperAdminSettings } from '@/api/configuration'
import SettingValueField from './SettingValueField.vue'
import SettingCard from './SettingCard.vue'
import { configurationPageIntro } from '@/utils/adminHelpCopy'
import { configurationPageIntro, websiteSettingsPageIntro } from '@/utils/adminHelpCopy'
import { refreshAppModeMenu } from '@/router'
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 {
EDITABLE_SETTING_GROUPS,
SYSTEM_SETTING_GROUPS,
@@ -218,6 +218,12 @@ import {
OTHER_PAGE_SETTING_MENUS,
type SettingGroupDef
} from './settingGroups'
import {
LANDING_SETTING_GROUPS,
isLandingContentKey,
landingSectionByPrefix,
matchesLandingPrefix
} from './landingSettings'
import {
SETTING_TYPE_OPTIONS,
formatValueForSave,
@@ -233,8 +239,9 @@ import {
} from './settingTypes'
import { TRADING_HOURS_SETTING_KEY_SET } from './tradingHoursKeys'
const props = withDefaults(defineProps<{ mode?: 'editable' | 'system' }>(), {
mode: 'editable'
const props = withDefaults(defineProps<{ mode?: 'editable' | 'system' | 'landing'; landingPrefix?: string }>(), {
mode: 'editable',
landingPrefix: ''
})
const userStore = useUserStore()
@@ -243,11 +250,24 @@ function refreshMenuAfterStoreChange() {
}
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 settingGroups = computed(() =>
isSystemMode.value ? SYSTEM_SETTING_GROUPS : EDITABLE_SETTING_GROUPS
)
const settingGroups = computed(() => {
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
@@ -327,6 +347,13 @@ const filteredRows = computed(() =>
(baseData.value || []).filter((row) => {
if (row.id == null || !matchesFilter(row)) 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()
if (OTHER_PAGE_SETTING_MENUS.has(menu)) return false
if (!menu && !isSystemMode.value && TRADING_HOURS_SETTING_KEY_SET.has(row.key || '')) {
@@ -486,7 +513,7 @@ onMounted(() => {
})
watch(
() => props.mode,
() => [props.mode, props.landingPrefix],
() => {
search.value = ''
filterType.value = undefined
@@ -608,6 +635,7 @@ async function initConfig() {
applyPriceProviderFromRows(baseData.value)
applyPriceChannelAccessFromRows(baseData.value)
applySupportChatMenuEnabledFromRows(baseData.value)
applyHasLandingFromRows(baseData.value)
applyGroupSmsFromRows(baseData.value)
applyCustomerSpecialPricingFromRows(baseData.value)
applyGoldCreditEnabledFromRows(baseData.value)
@@ -616,6 +644,7 @@ async function initConfig() {
!isSystemMode.value ||
!baseData.value.some((row) => row.key === 'price_provider') ||
!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 === 'group_sms') ||
!baseData.value.some((row) => row.key === 'customer_special_pricing') ||
@@ -627,6 +656,7 @@ async function initConfig() {
const rows = Array.isArray(all) ? all : []
applyPriceProviderFromRows(rows)
applySupportChatMenuEnabledFromRows(rows)
applyHasLandingFromRows(rows)
applyPriceChannelAccessFromRows(rows)
applyGroupSmsFromRows(rows)
applyCustomerSpecialPricingFromRows(rows)
@@ -701,6 +731,14 @@ function syncMenuFlagsFromSetting(key: string | undefined, value: string) {
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') {
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',
// فقط فلگ‌های مالک سوپرادمین — banner/tracking/نمایش محصول در «تنظیمات» عادی‌اند
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
)
},
@@ -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(
key: string,
@@ -207,6 +207,7 @@ export const SUPERADMIN_OWNED_SETTING_KEYS = new Set([
'jibit_enabled',
'permision__enabled',
'show_to_users_chat',
'has_landing',
])
export function isSuperAdminOwnedSettingKey(key?: string | null): boolean {
@@ -324,7 +325,7 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
minimum_sell: 'price',
gift_for_referrer: 'number',
gift_for_referral: 'number',
referral_bonus_amount: 'text',
referral_bonus_amount: 'price',
montzar_product_id: 'number',
montzar_account_zarin: 'text',
montzar_account_tahesab: 'text',
@@ -429,7 +430,19 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
pargar_item_id_gold: 'number',
support_messengers: 'longtext',
show_to_users_chat: 'switch',
has_landing: '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_key: 'text',
chat_upload_url: 'text',
@@ -623,6 +636,45 @@ export function formatValueForSave(
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 } {
if (key === REFERRAL_REQUIRED_KEY) {
return { on: 'اجباری', off: 'اختیاری' }
@@ -800,7 +852,7 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
referral_limit:
'حداکثر تعداد دعوت مجاز برای هر کاربر. خالی = بدون محدودیت.',
referral_bonus_amount:
'مبلغ یا مقدار پاداش معرف برای نمایش در پیامک‌ها و متن‌های دعوت.',
'مبلغ پاداش معرف (ریال) برای نمایش در پیامک‌ها و متن‌های دعوت. واحد روی خود فیلد مشخص است.',
jibit_verification:
'روشن: هنگام ثبت یا ویرایش کارت بانکی، تطبیق با سرویس احراز هویت جیبیت انجام می‌شود. خاموش: کارت بدون تطبیق جیبیت ثبت می‌شود.',
register_jibit_verify:
@@ -863,6 +915,19 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
'فقط مواردی که در این لیست ثبت شوند در منوی شناور پشتیبانی اپ مشتری نمایش داده می‌شوند. برای تماس تلفنی یا پیامک، نوع «تماس تلفنی» / «پیامک» را با شماره اضافه کنید. لیست می‌تواند خالی باشد؛ اگر چت فعال باشد، پشتیبانی مستقیم به صفحه چت می‌رود.',
show_to_users_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:
'روشن: چت پشتیبانی در اپ مشتری نمایش داده می‌شود (فقط وقتی show_to_users_chat هم روشن باشد). خاموش: دکمه و صفحه چت برای کاربر مخفی است.',
chat_api:
@@ -1119,6 +1184,8 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
minimum_sell: 'حداقل مبلغ فروش (ریال)',
referral_id: 'کد معرف اجباری',
referral_bonus_amount: 'مقدار پاداش معرف',
gift_for_referrer: 'پاداش معرف',
gift_for_referral: 'پاداش معرف‌شونده',
referral_gift_on_first_order: 'پاداش معرف پس از اولین سفارش',
referral_gift_buy_from_montzar: 'خرید پاداش معرفی از همکار',
open_register: 'ثبت‌نام باز',
@@ -1187,7 +1254,19 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
zarin_idbank: 'کد حساب بانک زرین',
support_messengers: 'پیام‌رسان‌های پشتیبانی',
show_to_users_chat: 'نمایش چت به کاربران',
has_landing: 'لندینگ وبسایت',
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_key: 'کلید API چت',
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',
'unit',
'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 */
@@ -1326,6 +1405,8 @@ export const SETTING_EDITABLE_KEYS = new Set([
'support_messengers', 'support_messenger_text',
'do_send_liman',
'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',
'login_hero_eyebrow', 'login_hero_title',
'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 دیتابیس اولویت دارد */
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()
if (key && KEY_LABEL_FALLBACK[key]) return KEY_LABEL_FALLBACK[key]
return row.title_fa?.trim() || key || '—'
const base = (key && KEY_LABEL_FALLBACK[key]) ? KEY_LABEL_FALLBACK[key] : (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 دیتابیس اولویت دارد */
@@ -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 =
'تنظیمات کلی سامانه (کلید–مقدار). با جستجو و فیلتر نوع فیلد، مورد را پیدا کنید؛ بعد از تغییر ذخیره کنید. روی آیکون i کنار هر تنظیم توضیح همان فیلد را ببینید.'
export const websiteSettingsPageIntro =
'متن و تنظیمات صفحهٔ لندینگ وبسایت. هر زیرمنو کلیدهای همان بخش را نشان می‌دهد؛ بعد از تغییر ذخیره می‌شود و در اپ کاربر با get_settings در دسترس است.'
export const accountingReportsPageIntro =
'گزارش‌های حسابداری داخلی پنل. برای اسناد و همگام‌سازی مشتریان از تب‌های ته‌حساب / زرین / پرگار استفاده کنید.'
+17 -1
View File
@@ -86,6 +86,9 @@ export const supportChatMenuEnabledSetting = ref(false)
/** reactive — سوییچ دیپلوی show_to_users_chat (برای مخفی کردن has_chat در تنظیمات عادی) */
export const showToUsersChatSetting = ref(false)
/** reactive — منوی تنظیمات وبسایت وقتی has_landing روشن است */
export const hasLandingSetting = ref(false)
/** reactive — سوییچ دیپلوی price_channel_access (گروه کانال در تنظیمات عادی) */
export const priceChannelAccessSetting = ref(false)
@@ -215,6 +218,10 @@ export function setShowToUsersChatFromSettings(value: boolean): void {
showToUsersChatSetting.value = value
}
export function setHasLandingFromSettings(value: boolean): void {
hasLandingSetting.value = value
}
export function setPriceChannelAccessFromSettings(value: boolean): void {
priceChannelAccessSetting.value = value
}
@@ -313,7 +320,7 @@ export function invalidateSettingsBootstrap(): void {
}
/**
* اعمال مستقیم ۱۵ دسترسی سوپرادمین روی فلگ‌های منو/UI
* اعمال مستقیم ۱۶ دسترسی سوپرادمین روی فلگ‌های منو/UI
* (پاسخ refresh-superadmin.items یا ردیف‌های get_settings).
*/
export function applySuperAdminAccessFlagsFromItems(
@@ -333,6 +340,7 @@ export function applySuperAdminAccessFlagsFromItems(
applyPriceTriggerEnabledFromRows(rows)
applyAppNotificationEnabledFromRows(rows)
applySupportChatMenuEnabledFromRows(rows)
applyHasLandingFromRows(rows)
}
export function applyConditionalOrderEnabledFromRows(
@@ -377,6 +385,13 @@ export function applyAppNotificationEnabledFromRows(
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(
rows: Array<{ key?: string; value?: string | null }>
) {
@@ -419,6 +434,7 @@ async function bootstrapSettingsFromApi(): Promise<void> {
applyPriceTriggerEnabledFromRows(list)
applyAppNotificationEnabledFromRows(list)
applySupportChatMenuEnabledFromRows(list)
applyHasLandingFromRows(list)
applyPriceProviderFromRows(list)
applyPriceChannelAccessFromRows(list)
setCachedMainMetal(mainMetalRow?.value)