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:
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -246,6 +246,9 @@ export const captureSitesPageIntro =
|
||||
export const configurationPageIntro =
|
||||
'تنظیمات کلی سامانه (کلید–مقدار). با جستجو و فیلتر نوع فیلد، مورد را پیدا کنید؛ بعد از تغییر ذخیره کنید. روی آیکون i کنار هر تنظیم توضیح همان فیلد را ببینید.'
|
||||
|
||||
export const websiteSettingsPageIntro =
|
||||
'متن و تنظیمات صفحهٔ لندینگ وبسایت. هر زیرمنو کلیدهای همان بخش را نشان میدهد؛ بعد از تغییر ذخیره میشود و در اپ کاربر با get_settings در دسترس است.'
|
||||
|
||||
export const accountingReportsPageIntro =
|
||||
'گزارشهای حسابداری داخلی پنل. برای اسناد و همگامسازی مشتریان از تبهای تهحساب / زرین / پرگار استفاده کنید.'
|
||||
|
||||
|
||||
+17
-1
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user