Files
boursecu_admin/src/pages/menuPages/configuration/index.vue
T

717 lines
20 KiB
Vue
Raw Normal View History

2026-02-12 11:10:19 +03:30
<template>
2026-06-05 22:01:10 +03:30
<div class="h-full min-h-0">
<BasicShowContent title="تنظیمات" 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>
2026-06-05 22:01:10 +03:30
<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>
2026-06-05 22:01:10 +03:30
<!-- <a-button type="primary" :loading="loading" @click="add">افزودن تنظیم</a-button>-->
<a-button class="settings-toolbar__action" :loading="loading" @click="initConfig">
بروزرسانی
</a-button>
</div>
<a-spin :spinning="loading && !baseData.length">
2026-06-07 10:58:23 +03:30
<a-collapse
v-model:activeKey="activeKeys"
class="settings-collapse"
:bordered="false"
>
<a-collapse-panel
v-for="group in SETTING_GROUPS"
:key="group.id"
:collapsible="groupedFiltered[group.id]?.length ? undefined : 'disabled'"
>
<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
v-if="groupedFiltered[group.id]"
:count="groupedFiltered[group.id].length"
:number-style="{ backgroundColor: '#d4af37', color: '#fff', fontSize: '11px' }"
/>
</div>
</template>
2026-06-05 22:01:10 +03:30
2026-06-07 10:58:23 +03:30
<a-row :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)"
/>
</a-col>
</a-row>
<div v-if="!groupedFiltered[group.id]?.length" class="settings-empty-group">
موردی در این گروه یافت نشد
</div>
</a-collapse-panel>
</a-collapse>
2026-06-05 22:01:10 +03:30
<a-empty
2026-06-07 10:58:23 +03:30
v-if="!loading && Object.keys(groupedFiltered).length === 0"
2026-06-05 22:01:10 +03:30
description="تنظیمی یافت نشد"
class="mt-8"
/>
</a-spin>
2026-02-12 11:10:19 +03:30
</BasicShowContent>
<BasicModal
2026-06-05 22:01:10 +03:30
v-model:visible="addConfigVisible"
title="افزودن تنظیم"
:width="modalWidth"
@close="addConfigVisible = false"
@ok="ok"
:loading="loading"
>
2026-06-05 22:01:10 +03:30
<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="عنوان در پنل" />
2026-02-12 11:10:19 +03:30
</a-form-item>
2026-06-05 22:01:10 +03:30
<a-form-item label="کلید (key)" name="key" :rules="[inputRules()]">
<a-input v-model:value="form.key" placeholder="مثلاً price_provider" />
</a-form-item>
2026-06-05 22:01:10 +03:30
<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>
2026-02-12 11:10:19 +03:30
</a-form-item>
2026-06-05 22:01:10 +03:30
<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>
2026-06-05 22:01:10 +03:30
<a-form-item name="is_active">
<a-checkbox v-model:checked="form.is_active">فعال در سیستم</a-checkbox>
2026-02-12 11:10:19 +03:30
</a-form-item>
2026-06-05 22:01:10 +03:30
<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"
/>
2026-02-12 11:10:19 +03:30
</a-form-item>
</a-form>
</BasicModal>
</div>
</template>
<script setup lang="ts">
2026-06-05 22:01:10 +03:30
import { debounce } from 'lodash'
2026-06-07 10:58:23 +03:30
import { watch } from 'vue'
2026-02-12 11:10:19 +03:30
import BasicShowContent from '@/components/BasicShowContext.vue'
import BasicModal from '@/components/BasicModal.vue'
2026-06-05 22:01:10 +03:30
import { inputRules } from '@/utils/inputRules'
2026-02-12 11:10:19 +03:30
import type { FormInstance } from 'ant-design-vue'
import { message } from 'ant-design-vue'
2026-06-05 22:01:10 +03:30
import type { Const } from '@/model/configuration'
2026-02-12 11:10:19 +03:30
import { loading } from '@/utils'
2026-06-12 20:30:00 +03:30
import { getEditableConfiguration, setConfiguration } from '@/api/configuration'
2026-06-05 22:01:10 +03:30
import SettingValueField from './SettingValueField.vue'
import SettingCard from './SettingCard.vue'
2026-06-13 12:23:07 +03:30
import { refreshAppModeMenu } from '@/router'
2026-06-12 20:30:00 +03:30
import { setStoreModeFromSettings } from '@/utils/config'
2026-06-05 22:01:10 +03:30
import {
SETTING_TYPE_OPTIONS,
formatValueForSave,
helpText,
2026-06-12 16:09:04 +03:30
isSettingVisible,
2026-06-12 20:30:00 +03:30
isSwitchOn,
2026-06-06 17:31:43 +03:30
resolveSettingFieldType
2026-06-05 22:01:10 +03:30
} from './settingTypes'
2026-02-12 11:10:19 +03:30
2026-06-05 22:01:10 +03:30
const settingTypeOptions = SETTING_TYPE_OPTIONS
2026-02-12 11:10:19 +03:30
2026-06-05 22:01:10 +03:30
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>>()
2026-02-12 11:10:19 +03:30
const form = ref<Const>({
key: '',
value: '',
2026-06-05 22:01:10 +03:30
title_fa: '',
description: '',
2026-06-05 22:01:10 +03:30
type: 'text',
is_active: true,
can_edit: 1
2026-02-12 11:10:19 +03:30
})
const formRef = ref<FormInstance>()
2026-06-05 22:01:10 +03:30
const selectOptionsDraft = ref('')
2026-02-12 11:10:19 +03:30
2026-06-05 22:01:10 +03:30
const effectiveType = computed(() =>
resolveSettingFieldType({ key: form.value.key, type: form.value.type })
)
2026-02-12 11:10:19 +03:30
2026-06-05 22:01:10 +03:30
const modalWidth = computed(() =>
effectiveType.value === 'longtext' ? '720px' : '520px'
)
2026-02-12 11:10:19 +03:30
2026-06-05 22:01:10 +03:30
const valueLabel = computed(() => form.value.title_fa || form.value.key || 'مقدار')
const valueHelp = computed(() => helpText(form.value))
2026-02-12 11:10:19 +03:30
2026-06-05 22:01:10 +03:30
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
2026-02-12 11:10:19 +03:30
})
}
2026-05-25 10:49:08 +03:30
2026-06-05 22:01:10 +03:30
function isEditable(row: Const): boolean {
return row.can_edit !== 0 && row.can_edit !== false
}
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(
2026-06-12 16:09:04 +03:30
(row) =>
row.id != null &&
isEditable(row) &&
isSettingVisible(row.key) &&
matchesFilter(row)
2026-06-05 22:01:10 +03:30
)
)
)
const switchRows = computed(() =>
filteredRows.value.filter((row) => resolveSettingFieldType(row) === 'switch')
)
const otherRows = computed(() =>
2026-06-06 17:31:43 +03:30
filteredRows.value.filter((row) => resolveSettingFieldType(row) !== 'switch')
2026-06-05 22:01:10 +03:30
)
2026-06-07 10:58:23 +03:30
// =============================================================================
2026-06-14 16:27:18 +03:30
// گروه‌بندی تنظیمات — اولین match برنده است؛ misc فقط برای باقی‌مانده
2026-06-07 10:58:23 +03:30
// =============================================================================
const SETTING_GROUPS: Array<{
id: string
title: string
icon: string
match: (key: string) => boolean
}> = [
{
id: 'general',
2026-06-14 16:27:18 +03:30
title: 'عمومی و برند اپ',
2026-06-07 10:58:23 +03:30
icon: 'vuesax-linear:home-2',
2026-06-14 16:27:18 +03:30
match: (k) =>
/^(name_fa|name_en|app_url|admin_url|bace_url|server_url|logo_|title_pwa|title_fav|metal_type|unit$|open_register|auto_active_customer|store_status|key$)/.test(
k
)
2026-06-13 17:38:35 +03:30
},
{
id: 'chart',
title: 'نمودار قیمت',
icon: 'vuesax-linear:chart-2',
match: (k) => /^chart_/.test(k)
2026-06-07 10:58:23 +03:30
},
{
id: 'pricing',
2026-06-14 16:27:18 +03:30
title: 'قیمت‌گذاری، کارمزد و تابلو',
2026-06-07 10:58:23 +03:30
icon: 'vuesax-linear:dollar-circle',
match: (k) =>
2026-06-14 16:27:18 +03:30
/^(fee_|buy_fee|sell_fee|min_|minimum_|price_provider|zaarbox_|main_product$|show_last_price|close_price|startDay|endDay|up_price|down_price|price_extreme_date|balance_report_exclude)/.test(
k
) || k.includes('charge_percentage')
},
{
id: 'content',
title: 'متن‌های اپ (قوانین و درباره)',
icon: 'vuesax-linear:document-text',
match: (k) => /^text_/.test(k)
2026-06-07 10:58:23 +03:30
},
2026-06-13 17:38:35 +03:30
{
id: 'referral',
title: 'معرف، پاداش و ثبت‌نام',
icon: 'vuesax-linear:people',
match: (k) =>
2026-06-14 16:27:18 +03:30
/^(referral_|gift_for|register_referral|register_jibit|has_page_earn_money|jibit_verification|referral_bonus)/.test(
k
)
2026-06-13 17:38:35 +03:30
},
2026-06-07 10:58:23 +03:30
{
id: 'montzar',
2026-06-14 16:27:18 +03:30
title: 'همکار قیمت (Montzar)',
icon: 'vuesax-linear:link-2',
match: (k) => k.startsWith('montzar_') || k === 'display_montzar'
},
{
id: 'store',
title: 'اتصال به فروشگاه (Montify)',
icon: 'vuesax-linear:shop',
match: (k) => /^(external_order|node_|montify_name)/.test(k)
},
{
id: 'payping',
title: 'درگاه Payping / Zify',
icon: 'vuesax-linear:card',
match: (k) => k.startsWith('payping_')
},
{
id: 'tahesab',
title: 'ته‌حساب',
icon: 'vuesax-linear:receipt-2',
match: (k) => k.startsWith('tahesab_') || k === 'send_to_tahesab'
},
{
id: 'support',
title: 'پشتیبانی اپ کاربر',
icon: 'vuesax-linear:headphone',
2026-06-15 15:48:10 +03:30
match: (k) => k.startsWith('support_messenger') || k === 'deposit_sms_support_number'
2026-06-14 16:27:18 +03:30
},
{
id: 'bots',
title: 'بات تلگرام و بله',
icon: 'vuesax-linear:messages-2',
match: (k) => /^(telegram_bot|bale_bot)/.test(k)
},
{
id: 'notifications',
title: 'پیامک و اعلان',
icon: 'vuesax-linear:notification',
match: (k) => /^(charge_notif|group_sms)/.test(k)
2026-06-07 10:58:23 +03:30
},
{
id: 'features',
2026-06-14 16:27:18 +03:30
title: 'محصولات و قابلیت‌های سیستم',
icon: 'vuesax-linear:setting-2',
match: (k) => /^(product_|main_wallet_products|price_from_nasiryan|profit_gold)/.test(k)
2026-06-07 10:58:23 +03:30
},
{
id: 'misc',
2026-06-13 17:38:35 +03:30
title: 'سایر تنظیمات',
2026-06-07 10:58:23 +03:30
icon: 'vuesax-linear:more-circle',
2026-06-13 17:38:35 +03:30
match: () => true
2026-06-07 10:58:23 +03:30
}
]
function getGroupId(key: string): string {
for (const g of SETTING_GROUPS) {
2026-06-14 16:27:18 +03:30
if (g.id === 'misc') continue
2026-06-07 10:58:23 +03:30
if (g.match(key)) return g.id
}
return 'misc'
}
const groupedFiltered = computed(() => {
const map: Record<string, Const[]> = {}
for (const g of SETTING_GROUPS) map[g.id] = []
for (const row of filteredRows.value) {
const gid = getGroupId(row.key || '')
map[gid].push(row)
}
// حذف گروه‌های خالی
Object.keys(map).forEach((k) => {
if (map[k].length === 0) delete map[k]
})
return map
})
// کالپس‌های باز — وقتی سرچ فعال است فقط گروه‌های دارای نتیجه باز می‌شوند
2026-06-14 16:27:18 +03:30
const defaultOpenGroups = ['general', 'pricing', 'store', 'payping', 'tahesab']
2026-06-07 10:58:23 +03:30
const activeKeys = ref<string[]>(defaultOpenGroups)
watch(search, (q) => {
const query = q.trim().toLowerCase()
if (!query) {
// برگشت به حالت پیش‌فرض
if (activeKeys.value.length === 0 || activeKeys.value.every(k => !defaultOpenGroups.includes(k))) {
activeKeys.value = [...defaultOpenGroups]
}
return
}
// فقط گروه‌هایی که حداقل یک آیتم matching دارند
const matching: string[] = []
for (const gid of Object.keys(groupedFiltered.value)) {
if (groupedFiltered.value[gid].length > 0) matching.push(gid)
}
activeKeys.value = matching.length ? matching : defaultOpenGroups
}, { flush: 'post' })
2026-06-05 22:01:10 +03:30
function rowColSpan(row: Const): number {
return resolveSettingFieldType(row) === 'longtext' ? 24 : 8
}
function isReadonly(row: Const): boolean {
return row.can_edit === 0 || row.can_edit === false
}
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
}
2026-05-25 10:49:08 +03:30
onMounted(() => {
initConfig()
})
2026-06-05 22:01:10 +03:30
async function initConfig() {
try {
loading.value = true
2026-06-12 20:30:00 +03:30
baseData.value = (await getEditableConfiguration()) ?? []
const storeRow = baseData.value.find((row) => row.key === 'is_store')
if (storeRow) {
setStoreModeFromSettings(isSwitchOn(storeRow.value))
2026-06-13 12:23:07 +03:30
refreshAppModeMenu()
2026-06-12 20:30:00 +03:30
}
2026-06-05 22:01:10 +03:30
syncDraftsFromBase()
} catch (e) {
console.error(e)
message.error('بارگذاری تنظیمات ناموفق بود')
} finally {
loading.value = false
}
}
function onValueChange(row: Const, value: string) {
if (row.id == null || isReadonly(row)) return
drafts.value[row.id] = value
scheduleSave(row)
}
function scheduleSave(row: Const) {
const id = row.id!
if (!debouncedSave.has(id)) {
debouncedSave.set(
id,
debounce(() => persistRowValue(id), resolveSettingFieldType(row) === 'switch' ? 200 : 600)
)
}
debouncedSave.get(id)!()
}
async function persistRowValue(id: number) {
const row = baseData.value.find((x) => x.id === id)
if (!row || isReadonly(row)) return
await saveSettingRow(row)
}
2026-06-13 17:38:35 +03:30
async function persistRow(row: Const) {
await saveSettingRow(row)
2026-06-05 22:01:10 +03:30
}
2026-06-13 17:38:35 +03:30
async function saveSettingRow(row: Const) {
2026-06-05 22:01:10 +03:30
if (row.id == null) return
const id = row.id
const type = resolveSettingFieldType(row)
const raw = drafts.value[id] ?? row.value ?? ''
2026-06-13 17:38:35 +03:30
const value = formatValueForSave(raw, type, row.key)
if (value === (row.value ?? '')) return
2026-06-05 22:01:10 +03:30
savingIds.value.add(id)
savedFlashIds.value.delete(id)
try {
await setConfiguration({
id: row.id,
key: row.key,
value,
description: row.description ?? '',
2026-06-13 17:38:35 +03:30
is_active: true,
2026-06-05 22:01:10 +03:30
title_fa: row.title_fa ?? '',
type
})
row.value = value
2026-06-13 17:38:35 +03:30
row.is_active = true
2026-06-05 22:01:10 +03:30
drafts.value[id] = value
2026-06-12 20:30:00 +03:30
if (row.key === 'is_store') {
setStoreModeFromSettings(isSwitchOn(value))
2026-06-13 12:23:07 +03:30
refreshAppModeMenu()
2026-06-12 20:30:00 +03:30
}
2026-06-05 22:01:10 +03:30
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)
drafts.value[id] = 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,
2026-06-13 17:38:35 +03:30
value: formatValueForSave(form.value.value ?? '', type, form.value.key),
2026-06-05 22:01:10 +03:30
description,
is_active: form.value.is_active,
title_fa: form.value.title_fa ?? '',
type
}
try {
await formRef.value?.validateFields()
loading.value = true
await setConfiguration(payload)
message.success('تنظیم جدید اضافه شد')
addConfigVisible.value = false
await initConfig()
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
2026-02-12 11:10:19 +03:30
</script>
2026-06-05 22:01:10 +03:30
<style lang="less" scoped>
.settings-toolbar {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: flex-end;
margin-bottom: 20px;
padding: 14px 16px;
2026-06-14 21:04:26 +03:30
background: var(--admin-bg-elevated);
border: 1px solid var(--admin-border);
2026-06-05 22:01:10 +03:30
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;
2026-06-14 21:04:26 +03:30
color: var(--admin-text-secondary);
2026-06-05 22:01:10 +03:30
line-height: 1;
}
&__control {
width: 148px;
}
}
.settings-section-title {
font-size: 13px;
font-weight: 600;
2026-06-14 21:04:26 +03:30
color: var(--admin-text-secondary);
2026-06-05 22:01:10 +03:30
margin-bottom: 12px;
padding-bottom: 6px;
2026-06-14 21:04:26 +03:30
border-bottom: 1px solid var(--admin-border);
2026-06-05 22:01:10 +03:30
&--spaced {
margin-top: 28px;
}
}
.settings-grid {
margin-bottom: 8px;
}
:deep(.ant-form-item-extra) {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
2026-06-07 10:58:23 +03:30
/* ======================= Collapse Groups Attractive UI ======================= */
.settings-collapse {
background: transparent;
}
.settings-collapse :deep(.ant-collapse-item) {
margin-bottom: 12px;
2026-06-14 21:04:26 +03:30
background: var(--admin-bg-elevated);
border: 1px solid var(--admin-border);
2026-06-07 10:58:23 +03:30
border-radius: 10px;
overflow: hidden;
2026-06-14 21:04:26 +03:30
box-shadow: 0 1px 3px var(--admin-shadow);
2026-06-07 10:58:23 +03:30
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;
2026-06-14 21:04:26 +03:30
background: var(--admin-bg-muted);
2026-06-07 10:58:23 +03:30
font-weight: 600;
font-size: 14px;
2026-06-14 21:04:26 +03:30
color: var(--admin-text);
2026-06-07 10:58:23 +03:30
}
.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;
2026-06-14 21:04:26 +03:30
color: var(--admin-text);
2026-06-07 10:58:23 +03:30
}
.settings-empty-group {
padding: 12px 16px;
font-size: 12px;
2026-06-14 21:04:26 +03:30
color: var(--admin-text-secondary);
2026-06-07 10:58:23 +03:30
text-align: center;
2026-06-14 21:04:26 +03:30
background: var(--admin-bg-muted);
2026-06-07 10:58:23 +03:30
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);
}
2026-06-05 22:01:10 +03:30
</style>