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-04-29 11:20:59 +03:30
|
|
|
|
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="filterActive"
|
|
|
|
|
allow-clear
|
|
|
|
|
placeholder="همه"
|
|
|
|
|
class="settings-filter-field__control"
|
|
|
|
|
:options="statusFilterOptions"
|
2026-04-29 11:20:59 +03:30
|
|
|
/>
|
2026-06-05 22:01:10 +03:30
|
|
|
</label>
|
|
|
|
|
<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>
|
2026-04-29 11:20:59 +03:30
|
|
|
</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)"
|
|
|
|
|
@update:is-active="onActiveChange(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>
|
2026-04-29 11:20:59 +03:30
|
|
|
|
|
|
|
|
<BasicModal
|
2026-06-05 22:01:10 +03:30
|
|
|
v-model:visible="addConfigVisible"
|
|
|
|
|
title="افزودن تنظیم"
|
|
|
|
|
:width="modalWidth"
|
|
|
|
|
@close="addConfigVisible = false"
|
|
|
|
|
@ok="ok"
|
|
|
|
|
:loading="loading"
|
2026-04-29 11:20:59 +03:30
|
|
|
>
|
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" />
|
2026-04-29 11:20:59 +03:30
|
|
|
</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"
|
2026-04-29 11:20:59 +03:30
|
|
|
/>
|
|
|
|
|
</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'
|
|
|
|
|
import { getConfiguration, setConfiguration } from '@/api/configuration'
|
2026-06-05 22:01:10 +03:30
|
|
|
import SettingValueField from './SettingValueField.vue'
|
|
|
|
|
import SettingCard from './SettingCard.vue'
|
|
|
|
|
import {
|
|
|
|
|
SETTING_TYPE_OPTIONS,
|
|
|
|
|
formatValueForSave,
|
|
|
|
|
helpText,
|
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 statusFilterOptions = [
|
|
|
|
|
{ value: 'active', label: 'فعال' },
|
|
|
|
|
{ value: 'inactive', label: 'غیرفعال' }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
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 filterActive = ref<string | undefined>()
|
|
|
|
|
|
|
|
|
|
function isRowActive(row: Const): boolean {
|
|
|
|
|
return row.is_active !== false && row.is_active !== 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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: '',
|
2026-04-29 11:20:59 +03:30
|
|
|
value: '',
|
2026-06-05 22:01:10 +03:30
|
|
|
title_fa: '',
|
2026-04-29 11:20:59 +03:30
|
|
|
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-04-29 11:20:59 +03:30
|
|
|
)
|
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 (filterActive.value === 'active' && !isRowActive(row)) return false
|
|
|
|
|
if (filterActive.value === 'inactive' && isRowActive(row)) return false
|
|
|
|
|
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) => row.id != null && isEditable(row) && matchesFilter(row)
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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
|
|
|
// =============================================================================
|
|
|
|
|
// گروهبندی تنظیمات (برای کالپس جذاب + سرچ هوشمند)
|
|
|
|
|
// =============================================================================
|
|
|
|
|
const SETTING_GROUPS: Array<{
|
|
|
|
|
id: string
|
|
|
|
|
title: string
|
|
|
|
|
icon: string
|
|
|
|
|
match: (key: string) => boolean
|
|
|
|
|
}> = [
|
|
|
|
|
{
|
|
|
|
|
id: 'general',
|
|
|
|
|
title: 'عمومی و اطلاعات اپ',
|
|
|
|
|
icon: 'vuesax-linear:home-2',
|
2026-06-09 21:06:29 +03:30
|
|
|
match: (k) => /^(name_|app_|admin_|server_|bace_|key$|color_|logo_|title_pwa|title_fav|metal_type|unit$)/.test(k)
|
2026-06-07 10:58:23 +03:30
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'pricing',
|
|
|
|
|
title: 'قیمتگذاری و کارمزدها',
|
|
|
|
|
icon: 'vuesax-linear:dollar-circle',
|
|
|
|
|
match: (k) =>
|
|
|
|
|
/^(fee_|buy_fee|sell_fee|min_|referral_|gift_for|price_provider|zaarbox_api_url|chart_id)/.test(k) ||
|
|
|
|
|
k.includes('charge_percentage')
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'montzar',
|
|
|
|
|
title: 'سرویس همکار (montzar)',
|
|
|
|
|
icon: 'vuesax-linear:link',
|
|
|
|
|
match: (k) => k.startsWith('montzar_')
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'features',
|
|
|
|
|
title: 'ویژگیها و صفحات',
|
|
|
|
|
icon: 'vuesax-linear:menu',
|
|
|
|
|
match: (k) =>
|
2026-06-11 22:29:28 +03:30
|
|
|
/^(has_page_|has_chat|display_montzar|close_price|startDay|endDay|auto_active|open_register|charge_notif|group_sms|show_last_price|product_update|product_auto_close|goldir_price|tabanGohar_price|main_product_price|is_store|store_status)/.test(k)
|
2026-06-07 10:58:23 +03:30
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'integrations',
|
|
|
|
|
title: 'اتصالات خارجی و درگاهها',
|
|
|
|
|
icon: 'vuesax-linear:card',
|
|
|
|
|
match: (k) => /^(tahesab_|payping_|jibit_|telegram_|bale_|liman_|support_messenger|external_order|profit_gold)/.test(k)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'misc',
|
|
|
|
|
title: 'سایر تنظیمات (متفرقه)',
|
|
|
|
|
icon: 'vuesax-linear:more-circle',
|
|
|
|
|
match: () => true // catch-all
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
function getGroupId(key: string): string {
|
|
|
|
|
for (const g of SETTING_GROUPS) {
|
|
|
|
|
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
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// کالپسهای باز — وقتی سرچ فعال است فقط گروههای دارای نتیجه باز میشوند
|
|
|
|
|
const defaultOpenGroups: string[] = []
|
|
|
|
|
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
|
|
|
|
|
baseData.value = (await getConfiguration()) ?? []
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onActiveChange(row: Const, isActive: boolean) {
|
|
|
|
|
if (row.id == null) return
|
|
|
|
|
if (isRowActive(row) === isActive) return
|
|
|
|
|
await persistRow(row, { isActive })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function persistRow(row: Const, overrides?: { isActive?: boolean }) {
|
|
|
|
|
await saveSettingRow(row, overrides)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveSettingRow(row: Const, overrides?: { isActive?: boolean }) {
|
|
|
|
|
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)
|
|
|
|
|
const isActive =
|
|
|
|
|
overrides?.isActive !== undefined ? overrides.isActive : isRowActive(row)
|
|
|
|
|
|
|
|
|
|
if (overrides?.isActive === undefined && value === (row.value ?? '')) return
|
|
|
|
|
|
|
|
|
|
savingIds.value.add(id)
|
|
|
|
|
savedFlashIds.value.delete(id)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await setConfiguration({
|
|
|
|
|
id: row.id,
|
|
|
|
|
key: row.key,
|
|
|
|
|
value,
|
|
|
|
|
description: row.description ?? '',
|
|
|
|
|
is_active: isActive,
|
|
|
|
|
title_fa: row.title_fa ?? '',
|
|
|
|
|
type
|
|
|
|
|
})
|
|
|
|
|
row.value = value
|
|
|
|
|
row.is_active = isActive
|
|
|
|
|
drafts.value[id] = 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)
|
|
|
|
|
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,
|
|
|
|
|
value: formatValueForSave(form.value.value ?? '', type),
|
|
|
|
|
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;
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
border: 1px solid #f0f0f0;
|
|
|
|
|
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: rgba(0, 0, 0, 0.55);
|
|
|
|
|
line-height: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__control {
|
|
|
|
|
width: 148px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.settings-section-title {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: rgba(0, 0, 0, 0.55);
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
padding-bottom: 6px;
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
|
|
|
|
|
&--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;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border: 1px solid #f0f0f0;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
|
|
|
|
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: linear-gradient(90deg, #fafafa 0%, #fff 100%);
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: #333;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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: #2c2c2c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.settings-empty-group {
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #999;
|
|
|
|
|
text-align: center;
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
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>
|