Lets deploy admins clear overlay cache and reload can_edit=0 switches like enable_automation without SSH. Co-authored-by: Cursor <cursoragent@cursor.com>
941 lines
28 KiB
Vue
941 lines
28 KiB
Vue
<template>
|
||
<div class="h-full min-h-0">
|
||
<BasicShowContent :title="pageTitle" class="h-full">
|
||
<div class="settings-toolbar">
|
||
<a-input
|
||
v-model:value="search"
|
||
allow-clear
|
||
placeholder="جستجو کلید یا عنوان..."
|
||
class="settings-toolbar__search"
|
||
>
|
||
<template #prefix>
|
||
<BaseIcon icon="vuesax-linear:search-normal-1" size="16" class="text-gray-400" />
|
||
</template>
|
||
</a-input>
|
||
|
||
<div class="settings-toolbar__filters">
|
||
<label class="settings-filter-field">
|
||
<span class="settings-filter-field__label">نوع فیلد</span>
|
||
<a-select
|
||
v-model:value="filterType"
|
||
allow-clear
|
||
placeholder="همه"
|
||
class="settings-filter-field__control"
|
||
:options="typeSelectOptions"
|
||
/>
|
||
</label>
|
||
</div>
|
||
|
||
<!-- <a-button type="primary" :loading="loading" @click="add">افزودن تنظیم</a-button>-->
|
||
<a-button
|
||
v-if="isSystemMode"
|
||
type="primary"
|
||
class="settings-toolbar__action"
|
||
:loading="loading"
|
||
@click="add"
|
||
>
|
||
افزودن تنظیم
|
||
</a-button>
|
||
<a-button
|
||
v-if="isSystemMode"
|
||
class="settings-toolbar__action"
|
||
:loading="refreshingSuperAdmin"
|
||
@click="refreshFromSuperAdmin"
|
||
>
|
||
همگامسازی از سوپرادمین
|
||
</a-button>
|
||
<a-button class="settings-toolbar__action" :loading="loading" @click="initConfig">
|
||
بروزرسانی
|
||
</a-button>
|
||
</div>
|
||
|
||
<a-spin :spinning="loading && !baseData.length">
|
||
<a-collapse
|
||
v-model:activeKey="activeKey"
|
||
accordion
|
||
class="settings-collapse"
|
||
:bordered="false"
|
||
>
|
||
<a-collapse-panel
|
||
v-for="group in visibleSettingGroups"
|
||
:key="group.id"
|
||
>
|
||
<template #header>
|
||
<div class="settings-collapse-header">
|
||
<BaseIcon :icon="group.icon" size="18" class="settings-collapse-icon" />
|
||
<span class="settings-collapse-title">{{ group.title }}</span>
|
||
<a-badge
|
||
:count="groupedFiltered[group.id].length"
|
||
:number-style="{ backgroundColor: '#d4af37', color: '#fff', fontSize: '11px' }"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-if="group.subsections?.length">
|
||
<div
|
||
v-for="block in subsectionBlocks(group)"
|
||
:key="block.id"
|
||
class="settings-subsection"
|
||
>
|
||
<h3 class="settings-subsection__title">{{ block.title }}</h3>
|
||
<a-row :gutter="[16, 16]" class="settings-grid">
|
||
<a-col
|
||
v-for="row in block.rows"
|
||
:key="row.id"
|
||
:xs="24"
|
||
:sm="12"
|
||
:lg="rowColSpan(row)"
|
||
>
|
||
<SettingCard
|
||
:row="row"
|
||
:model-value="draftValue(row)"
|
||
:readonly="isReadonly(row)"
|
||
:saving="isSaving(row.id)"
|
||
:saved-flash="isSavedFlash(row.id)"
|
||
@update:value="onValueChange(row, $event)"
|
||
@blur="onFieldBlur(row)"
|
||
/>
|
||
</a-col>
|
||
</a-row>
|
||
</div>
|
||
</template>
|
||
<a-row v-else :gutter="[16, 16]" class="settings-grid">
|
||
<a-col
|
||
v-for="row in groupedFiltered[group.id]"
|
||
:key="row.id"
|
||
:xs="24"
|
||
:sm="12"
|
||
:lg="rowColSpan(row)"
|
||
>
|
||
<SettingCard
|
||
:row="row"
|
||
:model-value="draftValue(row)"
|
||
:readonly="isReadonly(row)"
|
||
:saving="isSaving(row.id)"
|
||
:saved-flash="isSavedFlash(row.id)"
|
||
@update:value="onValueChange(row, $event)"
|
||
@blur="onFieldBlur(row)"
|
||
/>
|
||
</a-col>
|
||
</a-row>
|
||
</a-collapse-panel>
|
||
</a-collapse>
|
||
|
||
<a-empty
|
||
v-if="!loading && visibleSettingGroups.length === 0"
|
||
:description="
|
||
isSystemMode
|
||
? 'تنظیم سیستمی فعالی (can_edit=0) یافت نشد — deploy_settings_const.sql را اجرا کنید'
|
||
: 'تنظیمی یافت نشد'
|
||
"
|
||
class="mt-8"
|
||
/>
|
||
</a-spin>
|
||
</BasicShowContent>
|
||
|
||
<BasicModal
|
||
v-model:visible="addConfigVisible"
|
||
title="افزودن تنظیم"
|
||
:width="modalWidth"
|
||
@close="addConfigVisible = false"
|
||
@ok="ok"
|
||
:loading="loading"
|
||
>
|
||
<a-form ref="formRef" :model="form" layout="vertical" autocomplete="off">
|
||
<a-form-item label="عنوان فارسی" name="title_fa">
|
||
<a-input v-model:value="form.title_fa" placeholder="عنوان در پنل" />
|
||
</a-form-item>
|
||
|
||
<a-form-item label="کلید (key)" name="key" :rules="[inputRules()]">
|
||
<a-input v-model:value="form.key" placeholder="مثلاً price_provider" />
|
||
</a-form-item>
|
||
|
||
<a-form-item label="نوع فیلد" name="type">
|
||
<a-select v-model:value="form.type" placeholder="نوع ورودی">
|
||
<a-select-option v-for="t in settingTypeOptions" :key="t.key" :value="t.key">
|
||
{{ t.value }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
|
||
<a-form-item
|
||
v-if="form.type === 'select'"
|
||
label="گزینهها (با کاما)"
|
||
name="selectOptions"
|
||
>
|
||
<a-input
|
||
v-model:value="selectOptionsDraft"
|
||
placeholder="montzar,zaarbox,main_product"
|
||
/>
|
||
</a-form-item>
|
||
|
||
<a-form-item name="is_active">
|
||
<a-checkbox v-model:checked="form.is_active">فعال در سیستم</a-checkbox>
|
||
</a-form-item>
|
||
|
||
<a-form-item name="value" :label="valueLabel" :extra="valueHelp">
|
||
<SettingValueField
|
||
v-model:value="form.value"
|
||
:setting-key="form.key"
|
||
:setting-type="effectiveType"
|
||
:description="form.description"
|
||
:rows="effectiveType === 'longtext' ? 12 : 6"
|
||
/>
|
||
</a-form-item>
|
||
|
||
<a-form-item label="توضیحات / راهنما" name="description">
|
||
<a-textarea
|
||
v-model:value="form.description"
|
||
:rows="2"
|
||
placeholder="برای select: گزینهها با کاما — montzar,zaarbox"
|
||
/>
|
||
</a-form-item>
|
||
</a-form>
|
||
</BasicModal>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { debounce } from 'lodash'
|
||
import { watch } from 'vue'
|
||
import BasicShowContent from '@/components/BasicShowContext.vue'
|
||
import BasicModal from '@/components/BasicModal.vue'
|
||
import { inputRules } from '@/utils/inputRules'
|
||
import type { FormInstance } from 'ant-design-vue'
|
||
import { message } from 'ant-design-vue'
|
||
import type { Const } from '@/model/configuration'
|
||
import { loading } from '@/utils'
|
||
import { getEditableConfiguration, getSystemConfiguration, setConfiguration, setSystemConfiguration, refreshSuperAdminSettings } from '@/api/configuration'
|
||
import SettingValueField from './SettingValueField.vue'
|
||
import SettingCard from './SettingCard.vue'
|
||
import { refreshAppModeMenu } from '@/router'
|
||
import { useUserStore } from '@/store/userStore'
|
||
import { applyBannerEnabledFromRows, applyTrackingSystemsEnabledFromRows, applyConditionalOrderEnabledFromRows, applyAutomationEnabledFromRows, applyProductFundsEnabledFromRows, applyPriceTriggerEnabledFromRows, applyAppNotificationEnabledFromRows, applyPriceProviderFromRows, applyPriceChannelAccessFromRows, applySupportChatMenuEnabledFromRows, applyGroupSmsFromRows, applyCustomerSpecialPricingFromRows, setBannerEnabledFromSettings, setConditionalOrderEnabledFromSettings, setAutomationEnabledFromSettings, setProductFundsEnabledFromSettings, setPriceTriggerEnabledFromSettings, setAppNotificationEnabledFromSettings, setPriceProviderFromSettings, setPriceChannelAccessFromSettings, setStoreModeFromSettings, setTrackingSystemsEnabledFromSettings, setGroupSmsEnabledFromSettings, setCustomerSpecialPricingEnabledFromSettings } from '@/utils/config'
|
||
import {
|
||
EDITABLE_SETTING_GROUPS,
|
||
SYSTEM_SETTING_GROUPS,
|
||
resolveSettingMenu,
|
||
OTHER_PAGE_SETTING_MENUS,
|
||
type SettingGroupDef
|
||
} from './settingGroups'
|
||
import {
|
||
SETTING_TYPE_OPTIONS,
|
||
formatValueForSave,
|
||
helpText,
|
||
isSettingVisible,
|
||
isCodeManagedSettingKey,
|
||
isSuperAdminOwnedSettingKey,
|
||
isSettingLocked,
|
||
isSettingActive,
|
||
isSwitchOn,
|
||
normalizeSettingRow,
|
||
resolveSettingFieldType
|
||
} from './settingTypes'
|
||
import { TRADING_HOURS_SETTING_KEY_SET } from './tradingHoursKeys'
|
||
|
||
const props = withDefaults(defineProps<{ mode?: 'editable' | 'system' }>(), {
|
||
mode: 'editable'
|
||
})
|
||
|
||
const userStore = useUserStore()
|
||
function refreshMenuAfterStoreChange() {
|
||
refreshAppModeMenu(userStore.userInfo?.mobile)
|
||
}
|
||
|
||
const isSystemMode = computed(() => props.mode === 'system')
|
||
const pageTitle = computed(() => (isSystemMode.value ? 'تنظیمات پیشرفته' : 'تنظیمات'))
|
||
const refreshingSuperAdmin = ref(false)
|
||
const settingGroups = computed(() =>
|
||
isSystemMode.value ? SYSTEM_SETTING_GROUPS : EDITABLE_SETTING_GROUPS
|
||
)
|
||
|
||
const settingTypeOptions = SETTING_TYPE_OPTIONS
|
||
|
||
const typeSelectOptions = SETTING_TYPE_OPTIONS.map((t) => ({
|
||
value: t.key,
|
||
label: t.value
|
||
}))
|
||
|
||
const baseData = ref<Array<Const>>([])
|
||
const drafts = ref<Record<number, string>>({})
|
||
const addConfigVisible = ref(false)
|
||
const search = ref('')
|
||
const filterType = ref<string | undefined>()
|
||
|
||
const savingIds = ref<Set<number>>(new Set())
|
||
const savedFlashIds = ref<Set<number>>(new Set())
|
||
const saveTimers = new Map<number, ReturnType<typeof setTimeout>>()
|
||
const debouncedSave = new Map<number, ReturnType<typeof debounce>>()
|
||
|
||
const form = ref<Const>({
|
||
key: '',
|
||
value: '',
|
||
title_fa: '',
|
||
description: '',
|
||
type: 'text',
|
||
is_active: true,
|
||
can_edit: 1
|
||
})
|
||
const formRef = ref<FormInstance>()
|
||
const selectOptionsDraft = ref('')
|
||
|
||
const effectiveType = computed(() =>
|
||
resolveSettingFieldType({ key: form.value.key, type: form.value.type })
|
||
)
|
||
|
||
const modalWidth = computed(() =>
|
||
effectiveType.value === 'longtext' ? '720px' : '520px'
|
||
)
|
||
|
||
const valueLabel = computed(() => form.value.title_fa || form.value.key || 'مقدار')
|
||
const valueHelp = computed(() => helpText(form.value))
|
||
|
||
function sortRows(rows: Const[]): Const[] {
|
||
return [...rows].sort((a, b) => {
|
||
const ta = (a.title_fa || a.key || '').localeCompare(b.title_fa || b.key || '', 'fa')
|
||
return ta
|
||
})
|
||
}
|
||
|
||
function isEditable(row: Const): boolean {
|
||
return !isSettingLocked(row.can_edit)
|
||
}
|
||
|
||
function matchesFilter(row: Const): boolean {
|
||
const q = search.value.trim().toLowerCase()
|
||
if (filterType.value) {
|
||
const t = resolveSettingFieldType(row)
|
||
if (t !== filterType.value) return false
|
||
}
|
||
if (!q) return true
|
||
return (
|
||
(row.key || '').toLowerCase().includes(q) ||
|
||
(row.title_fa || '').toLowerCase().includes(q) ||
|
||
(row.value || '').toLowerCase().includes(q) ||
|
||
(drafts.value[row.id!] || '').toLowerCase().includes(q)
|
||
)
|
||
}
|
||
|
||
const filteredRows = computed(() =>
|
||
sortRows(
|
||
(baseData.value || []).filter((row) => {
|
||
if (row.id == null || !matchesFilter(row)) return false
|
||
if (!isSettingVisible(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 || '')) {
|
||
return false
|
||
}
|
||
if (isSystemMode.value) return true
|
||
return isEditable(row)
|
||
})
|
||
)
|
||
)
|
||
|
||
const switchRows = computed(() =>
|
||
filteredRows.value.filter((row) => resolveSettingFieldType(row) === 'switch')
|
||
)
|
||
|
||
const otherRows = computed(() =>
|
||
filteredRows.value.filter((row) => resolveSettingFieldType(row) !== 'switch')
|
||
)
|
||
|
||
// =============================================================================
|
||
// گروهبندی تنظیمات — اولین match برنده است؛ misc فقط برای باقیمانده
|
||
// =============================================================================
|
||
|
||
function getGroupId(row: Const): string {
|
||
return resolveSettingMenu(row, settingGroups.value)
|
||
}
|
||
|
||
const groupedFiltered = computed(() => {
|
||
const map: Record<string, Const[]> = {}
|
||
for (const g of settingGroups.value) map[g.id] = []
|
||
for (const row of filteredRows.value) {
|
||
const gid = getGroupId(row)
|
||
if (!map[gid]) map[gid] = []
|
||
map[gid].push(row)
|
||
}
|
||
Object.keys(map).forEach((k) => {
|
||
if (map[k].length === 0) delete map[k]
|
||
})
|
||
return map
|
||
})
|
||
|
||
const visibleSettingGroups = computed(() =>
|
||
settingGroups.value.filter((g) => (groupedFiltered.value[g.id]?.length ?? 0) > 0)
|
||
)
|
||
|
||
function subsectionBlocks(group: SettingGroupDef) {
|
||
const rows = groupedFiltered.value[group.id] ?? []
|
||
if (!group.subsections?.length) {
|
||
return [{ id: 'all', title: group.title, rows }]
|
||
}
|
||
|
||
const used = new Set<number>()
|
||
const blocks = group.subsections
|
||
.map((sub) => {
|
||
const subRows = rows.filter((row) => {
|
||
if (row.id == null || used.has(row.id)) return false
|
||
if (!sub.match(row.key || '')) return false
|
||
used.add(row.id)
|
||
return true
|
||
})
|
||
return { id: sub.id, title: sub.title, rows: subRows }
|
||
})
|
||
.filter((b) => b.rows.length > 0)
|
||
|
||
const leftovers = rows.filter((row) => row.id != null && !used.has(row.id))
|
||
if (leftovers.length) {
|
||
blocks.push({ id: `${group.id}-other`, title: 'سایر', rows: leftovers })
|
||
}
|
||
return blocks
|
||
}
|
||
|
||
const defaultOpenGroupId = computed(() => {
|
||
const visibleIds = visibleSettingGroups.value.map((g) => g.id)
|
||
const preferred = isSystemMode.value
|
||
? ['chart', 'pricing', 'payping', 'accounting', 'store', 'general']
|
||
: ['general', 'content', 'fees', 'limits', 'price_source', 'referral']
|
||
const fromPreferred = preferred.find((id) => visibleIds.includes(id))
|
||
return fromPreferred ?? visibleIds[0]
|
||
})
|
||
|
||
const activeKey = ref<string | undefined>(undefined)
|
||
|
||
watch(
|
||
visibleSettingGroups,
|
||
(groups) => {
|
||
const visibleIds = new Set(groups.map((g) => g.id))
|
||
if (activeKey.value && !visibleIds.has(activeKey.value)) {
|
||
activeKey.value = groups[0]?.id
|
||
}
|
||
if (!activeKey.value && groups.length) {
|
||
activeKey.value = defaultOpenGroupId.value
|
||
}
|
||
},
|
||
{ flush: 'post' }
|
||
)
|
||
|
||
watch(
|
||
defaultOpenGroupId,
|
||
(id) => {
|
||
if (!activeKey.value && id) {
|
||
activeKey.value = id
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
watch(search, (q) => {
|
||
const query = q.trim().toLowerCase()
|
||
if (!query) {
|
||
if (!activeKey.value || !visibleSettingGroups.value.some((g) => g.id === activeKey.value)) {
|
||
activeKey.value = defaultOpenGroupId.value
|
||
}
|
||
return
|
||
}
|
||
const matching = Object.keys(groupedFiltered.value).find(
|
||
(gid) => (groupedFiltered.value[gid]?.length ?? 0) > 0
|
||
)
|
||
activeKey.value = matching ?? defaultOpenGroupId.value
|
||
}, { flush: 'post' })
|
||
|
||
function rowColSpan(row: Const): number {
|
||
return resolveSettingFieldType(row) === 'longtext' ? 24 : 8
|
||
}
|
||
|
||
function isReadonly(row: Const): boolean {
|
||
if (isCodeManagedSettingKey(row.key)) return true
|
||
if (isSuperAdminOwnedSettingKey(row.key)) return true
|
||
if (isSystemMode.value) return false
|
||
return isSettingLocked(row.can_edit)
|
||
}
|
||
|
||
function draftValue(row: Const): string {
|
||
const id = row.id!
|
||
if (drafts.value[id] !== undefined) return drafts.value[id]
|
||
return row.value ?? ''
|
||
}
|
||
|
||
function isSaving(id?: number): boolean {
|
||
return id != null && savingIds.value.has(id)
|
||
}
|
||
|
||
function isSavedFlash(id?: number): boolean {
|
||
return id != null && savedFlashIds.value.has(id) && !savingIds.value.has(id)
|
||
}
|
||
|
||
function syncDraftsFromBase() {
|
||
const next: Record<number, string> = {}
|
||
for (const row of baseData.value || []) {
|
||
if (row.id != null) next[row.id] = row.value ?? ''
|
||
}
|
||
drafts.value = next
|
||
}
|
||
|
||
onMounted(() => {
|
||
initConfig()
|
||
})
|
||
|
||
watch(
|
||
() => props.mode,
|
||
() => {
|
||
search.value = ''
|
||
filterType.value = undefined
|
||
activeKey.value = undefined
|
||
initConfig()
|
||
}
|
||
)
|
||
|
||
async function refreshFromSuperAdmin() {
|
||
if (!isSystemMode.value || refreshingSuperAdmin.value) return
|
||
refreshingSuperAdmin.value = true
|
||
try {
|
||
const res = await refreshSuperAdminSettings()
|
||
const auto = res?.enable_automation
|
||
message.success(
|
||
auto != null && auto !== ''
|
||
? `همگام شد (${res?.count ?? 0} کلید) — اتوماسیون: ${isSwitchOn(auto) ? 'روشن' : 'خاموش'}`
|
||
: (res?.message || `همگام شد (${res?.count ?? 0} کلید)`)
|
||
)
|
||
await initConfig()
|
||
refreshMenuAfterStoreChange()
|
||
} catch (e: any) {
|
||
console.error(e)
|
||
const msg = e?.response?.data?.message || e?.message || 'همگامسازی از سوپرادمین ناموفق بود'
|
||
message.error(Array.isArray(msg) ? msg.join('، ') : msg)
|
||
} finally {
|
||
refreshingSuperAdmin.value = false
|
||
}
|
||
}
|
||
|
||
async function initConfig() {
|
||
try {
|
||
loading.value = true
|
||
const rows =
|
||
(isSystemMode.value
|
||
? await getSystemConfiguration()
|
||
: await getEditableConfiguration()) ?? []
|
||
baseData.value = rows.map(normalizeSettingRow)
|
||
applyBannerEnabledFromRows(baseData.value)
|
||
applyTrackingSystemsEnabledFromRows(baseData.value)
|
||
applyConditionalOrderEnabledFromRows(baseData.value)
|
||
applyAutomationEnabledFromRows(baseData.value)
|
||
applyProductFundsEnabledFromRows(baseData.value)
|
||
applyPriceTriggerEnabledFromRows(baseData.value)
|
||
applyAppNotificationEnabledFromRows(baseData.value)
|
||
applyPriceProviderFromRows(baseData.value)
|
||
applyPriceChannelAccessFromRows(baseData.value)
|
||
applySupportChatMenuEnabledFromRows(baseData.value)
|
||
applyGroupSmsFromRows(baseData.value)
|
||
applyCustomerSpecialPricingFromRows(baseData.value)
|
||
// صفحهٔ عادی price_provider ندارد (can_edit=0) — همیشه از get_settings کامل بخوان
|
||
if (
|
||
!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 === 'price_channel_access') ||
|
||
!baseData.value.some((row) => row.key === 'group_sms') ||
|
||
!baseData.value.some((row) => row.key === 'customer_special_pricing')
|
||
) {
|
||
try {
|
||
const { getConfiguration } = await import('@/api/configuration')
|
||
const all = await getConfiguration()
|
||
const rows = Array.isArray(all) ? all : []
|
||
applyPriceProviderFromRows(rows)
|
||
applySupportChatMenuEnabledFromRows(rows)
|
||
applyPriceChannelAccessFromRows(rows)
|
||
applyGroupSmsFromRows(rows)
|
||
applyCustomerSpecialPricingFromRows(rows)
|
||
} catch {
|
||
/* کش قبلی میماند */
|
||
}
|
||
}
|
||
if (!isSystemMode.value) {
|
||
const storeRow = baseData.value.find((row) => row.key === 'is_store')
|
||
if (storeRow) {
|
||
setStoreModeFromSettings(isSwitchOn(storeRow.value))
|
||
refreshMenuAfterStoreChange()
|
||
}
|
||
}
|
||
syncDraftsFromBase()
|
||
} catch (e) {
|
||
console.error(e)
|
||
message.error('بارگذاری تنظیمات ناموفق بود')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function saveDebounceMs(row: Const): number {
|
||
const type = resolveSettingFieldType(row)
|
||
if (type === 'switch') return 200
|
||
if (type === 'longtext') return 3000
|
||
if (type === 'text') return 1200
|
||
return 800
|
||
}
|
||
|
||
function syncMenuFlagsFromSetting(key: string | undefined, value: string) {
|
||
if (key === 'price_provider') {
|
||
setPriceProviderFromSettings(value)
|
||
return
|
||
}
|
||
if (key === 'banner_enabled') {
|
||
setBannerEnabledFromSettings(isSwitchOn(value))
|
||
return
|
||
}
|
||
if (key === 'tracking_systems_enabled') {
|
||
setTrackingSystemsEnabledFromSettings(isSwitchOn(value))
|
||
return
|
||
}
|
||
if (key === 'enable_conditional_order') {
|
||
setConditionalOrderEnabledFromSettings(isSwitchOn(value))
|
||
return
|
||
}
|
||
if (key === 'enable_automation') {
|
||
setAutomationEnabledFromSettings(isSwitchOn(value))
|
||
return
|
||
}
|
||
if (key === 'enable_product_funds') {
|
||
setProductFundsEnabledFromSettings(isSwitchOn(value))
|
||
return
|
||
}
|
||
if (key === 'has_page_price_trigger') {
|
||
setPriceTriggerEnabledFromSettings(isSwitchOn(value))
|
||
return
|
||
}
|
||
if (key === 'has_page_notification') {
|
||
setAppNotificationEnabledFromSettings(isSwitchOn(value))
|
||
return
|
||
}
|
||
if (key === 'show_to_users_chat' || key === 'has_chat' || key === 'chat_api') {
|
||
applySupportChatMenuEnabledFromRows([{ key, value }])
|
||
// همگام با settings داخل userInfo تا گیت روت/هدر بعد از رفرش هم درست باشد
|
||
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))
|
||
}
|
||
if (key === 'group_sms') {
|
||
setGroupSmsEnabledFromSettings(isSwitchOn(value))
|
||
}
|
||
if (key === 'customer_special_pricing') {
|
||
setCustomerSpecialPricingEnabledFromSettings(isSwitchOn(value))
|
||
}
|
||
}
|
||
|
||
function onValueChange(row: Const, value: string) {
|
||
if (row.id == null || isReadonly(row)) return
|
||
|
||
drafts.value[row.id] = value
|
||
|
||
// منو و UI وابسته را همان لحظه با سوئیچ همگام کن
|
||
syncMenuFlagsFromSetting(row.key, value)
|
||
|
||
// longtext هم باید ذخیره شود، فقط با delay بیشتر
|
||
scheduleSave(row)
|
||
}
|
||
|
||
|
||
function onFieldBlur(row: Const) {
|
||
if (row.id == null || isReadonly(row)) return
|
||
scheduleSave(row, { flush: true })
|
||
}
|
||
|
||
function scheduleSave(row: Const, options?: { flush?: boolean }) {
|
||
const id = row.id!
|
||
const delay = saveDebounceMs(row)
|
||
|
||
if (!debouncedSave.has(id)) {
|
||
debouncedSave.set(
|
||
id,
|
||
debounce(() => {
|
||
void persistRowValue(id)
|
||
}, delay)
|
||
)
|
||
}
|
||
|
||
const saveFn = debouncedSave.get(id)!
|
||
|
||
if (options?.flush) {
|
||
// مهم: برای blur مستقیم ذخیره کن، نه فقط flush
|
||
saveFn.cancel()
|
||
void persistRowValue(id)
|
||
return
|
||
}
|
||
|
||
saveFn()
|
||
}
|
||
|
||
async function persistRowValue(id: number) {
|
||
const row = baseData.value.find((x) => x.id === id)
|
||
if (!row || isReadonly(row)) return
|
||
await saveSettingRow(row)
|
||
}
|
||
|
||
async function persistRow(row: Const) {
|
||
await saveSettingRow(row)
|
||
}
|
||
|
||
async function saveSettingRow(row: Const) {
|
||
if (row.id == null) return
|
||
const id = row.id
|
||
|
||
const type = resolveSettingFieldType(row)
|
||
const raw = drafts.value[id] ?? row.value ?? ''
|
||
const value = formatValueForSave(raw, type, row.key)
|
||
if (value === (row.value ?? '')) return
|
||
|
||
const draftSnapshot = raw
|
||
|
||
savingIds.value.add(id)
|
||
savedFlashIds.value.delete(id)
|
||
|
||
try {
|
||
const saveFn = isSystemMode.value ? setSystemConfiguration : setConfiguration
|
||
await saveFn({
|
||
id: row.id,
|
||
key: row.key,
|
||
value,
|
||
description: row.description ?? '',
|
||
is_active: true,
|
||
title_fa: row.title_fa ?? '',
|
||
type
|
||
})
|
||
row.value = value
|
||
row.is_active = true
|
||
const currentDraft = drafts.value[id] ?? ''
|
||
if (currentDraft === draftSnapshot) {
|
||
drafts.value[id] = value
|
||
} else {
|
||
scheduleSave(row)
|
||
}
|
||
if (row.key === 'is_store' && !isSystemMode.value) {
|
||
setStoreModeFromSettings(isSwitchOn(value))
|
||
refreshMenuAfterStoreChange()
|
||
}
|
||
syncMenuFlagsFromSetting(row.key, value)
|
||
savedFlashIds.value.add(id)
|
||
if (saveTimers.has(id)) clearTimeout(saveTimers.get(id))
|
||
saveTimers.set(
|
||
id,
|
||
setTimeout(() => {
|
||
savedFlashIds.value.delete(id)
|
||
savedFlashIds.value = new Set(savedFlashIds.value)
|
||
}, 2000)
|
||
)
|
||
} catch (e) {
|
||
console.error(e)
|
||
if ((drafts.value[id] ?? '') === draftSnapshot) {
|
||
drafts.value[id] = row.value ?? ''
|
||
}
|
||
// برگرداندن فلگ منو به مقدار ذخیرهشده قبلی
|
||
syncMenuFlagsFromSetting(row.key, row.value ?? '')
|
||
message.error(`ذخیره «${row.title_fa || row.key}» ناموفق بود`)
|
||
} finally {
|
||
savingIds.value.delete(id)
|
||
savingIds.value = new Set(savingIds.value)
|
||
}
|
||
}
|
||
|
||
async function add() {
|
||
form.value = {
|
||
key: '',
|
||
value: '',
|
||
title_fa: '',
|
||
description: '',
|
||
type: 'text',
|
||
is_active: true,
|
||
can_edit: 1
|
||
}
|
||
selectOptionsDraft.value = ''
|
||
formRef.value?.resetFields()
|
||
addConfigVisible.value = true
|
||
}
|
||
|
||
async function ok() {
|
||
const type = effectiveType.value
|
||
let description = form.value.description ?? ''
|
||
if (type === 'select' && selectOptionsDraft.value.trim()) {
|
||
description = selectOptionsDraft.value.trim()
|
||
}
|
||
|
||
const payload = {
|
||
key: form.value.key,
|
||
value: formatValueForSave(form.value.value ?? '', type, form.value.key),
|
||
description,
|
||
is_active: form.value.is_active,
|
||
title_fa: form.value.title_fa ?? '',
|
||
type
|
||
}
|
||
|
||
try {
|
||
await formRef.value?.validateFields()
|
||
loading.value = true
|
||
const saveFn = isSystemMode.value ? setSystemConfiguration : setConfiguration
|
||
await saveFn(payload)
|
||
message.success('تنظیم جدید اضافه شد')
|
||
addConfigVisible.value = false
|
||
await initConfig()
|
||
} catch (e) {
|
||
console.error(e)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.settings-toolbar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
align-items: flex-end;
|
||
margin-bottom: 20px;
|
||
padding: 14px 16px;
|
||
background: var(--admin-bg-elevated);
|
||
border: 1px solid var(--admin-border);
|
||
border-radius: 8px;
|
||
|
||
&__search {
|
||
flex: 1;
|
||
min-width: 200px;
|
||
max-width: 320px;
|
||
}
|
||
|
||
&__filters {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
&__action {
|
||
margin-inline-start: auto;
|
||
}
|
||
}
|
||
|
||
.settings-filter-field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
min-width: 148px;
|
||
margin: 0;
|
||
|
||
&__label {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: var(--admin-text-secondary);
|
||
line-height: 1;
|
||
}
|
||
|
||
&__control {
|
||
width: 148px;
|
||
}
|
||
}
|
||
|
||
.settings-section-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--admin-text-secondary);
|
||
margin-bottom: 12px;
|
||
padding-bottom: 6px;
|
||
border-bottom: 1px solid var(--admin-border);
|
||
|
||
&--spaced {
|
||
margin-top: 28px;
|
||
}
|
||
}
|
||
|
||
.settings-grid {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.settings-subsection {
|
||
& + & {
|
||
margin-top: 1.25rem;
|
||
padding-top: 1rem;
|
||
border-top: 1px solid var(--admin-border);
|
||
}
|
||
|
||
&__title {
|
||
margin: 0 0 0.75rem;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: var(--admin-text-primary);
|
||
}
|
||
}
|
||
|
||
:deep(.ant-form-item-extra) {
|
||
font-size: 12px;
|
||
color: rgba(0, 0, 0, 0.45);
|
||
}
|
||
|
||
/* ======================= Collapse Groups Attractive UI ======================= */
|
||
.settings-collapse {
|
||
background: transparent;
|
||
}
|
||
|
||
.settings-collapse :deep(.ant-collapse-item) {
|
||
margin-bottom: 12px;
|
||
background: var(--admin-bg-elevated);
|
||
border: 1px solid var(--admin-border);
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
box-shadow: 0 1px 3px var(--admin-shadow);
|
||
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
||
}
|
||
|
||
.settings-collapse :deep(.ant-collapse-item:hover) {
|
||
box-shadow: 0 4px 12px rgba(212, 175, 55, 0.08);
|
||
border-color: #f4e9c8;
|
||
}
|
||
|
||
.settings-collapse :deep(.ant-collapse-header) {
|
||
padding: 14px 18px !important;
|
||
background: var(--admin-bg-muted);
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
color: var(--admin-text);
|
||
}
|
||
|
||
.settings-collapse-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
width: 100%;
|
||
}
|
||
|
||
.settings-collapse-icon {
|
||
color: #d4af37;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.settings-collapse-title {
|
||
flex: 1;
|
||
font-weight: 600;
|
||
color: var(--admin-text);
|
||
}
|
||
|
||
.settings-empty-group {
|
||
padding: 12px 16px;
|
||
font-size: 12px;
|
||
color: var(--admin-text-secondary);
|
||
text-align: center;
|
||
background: var(--admin-bg-muted);
|
||
border-radius: 6px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
/* وقتی سرچ فعال است، پنلهای باز جلوه بهتری داشته باشند */
|
||
.settings-collapse :deep(.ant-collapse-item-active) {
|
||
border-color: #f4e9c8;
|
||
box-shadow: 0 6px 16px rgba(212, 175, 55, 0.12);
|
||
}
|
||
</style>
|