Files
boursecu_admin/src/pages/menuPages/configuration/index.vue
T
mohmdsdgandCursor 71bf8dfc47 feat(settings): add SuperAdmin sync button on advanced settings
Lets deploy admins clear overlay cache and reload can_edit=0 switches like enable_automation without SSH.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 21:22:49 +03:30

941 lines
28 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>