refactor(settings): render SuperAdmin keys from API only, flat list
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,45 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch, type Component } from 'vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
AccountBookOutlined,
|
||||
ArrowRightOutlined,
|
||||
BankOutlined,
|
||||
CalculatorOutlined,
|
||||
ClusterOutlined,
|
||||
CreditCardOutlined,
|
||||
DeleteOutlined,
|
||||
DollarOutlined,
|
||||
EllipsisOutlined,
|
||||
FileTextOutlined,
|
||||
GlobalOutlined,
|
||||
HomeOutlined,
|
||||
LineChartOutlined,
|
||||
LinkOutlined,
|
||||
MessageOutlined,
|
||||
SearchOutlined,
|
||||
SendOutlined,
|
||||
ShopOutlined,
|
||||
TeamOutlined,
|
||||
WalletOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import api from '@/api/http'
|
||||
import VersionPageTabs from '@/components/VersionPageTabs.vue'
|
||||
import SettingCard from '@/components/program-settings/SettingCard.vue'
|
||||
import {
|
||||
KHANEGI_SYSTEM_SETTING_GROUPS,
|
||||
getKhanegiSystemGroupId,
|
||||
type SettingGroupDef,
|
||||
} from '@/config/khanegiSystemSettingGroups'
|
||||
import {
|
||||
SETTING_TYPE_OPTIONS,
|
||||
formatValueForSave,
|
||||
isSettingVisible,
|
||||
normalizeSettingRow,
|
||||
resolveSettingFieldType,
|
||||
} from '@/config/khanegiSettingCatalog'
|
||||
import { SUPERADMIN_EDITABLE_SETTING_KEYS, SUPERADMIN_SYSTEM_SETTING_KEYS } from '@/config/superAdminSettingKeys'
|
||||
import type { KhanegiSettingRow } from '@/config/khanegiSettingRow'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
@@ -72,7 +50,6 @@ const savingKeys = ref<Set<string>>(new Set())
|
||||
const savedFlashKeys = ref<Set<string>>(new Set())
|
||||
const saveTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
const flashTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
const activeKey = ref<string | undefined>(undefined)
|
||||
|
||||
const creds = reactive({
|
||||
mobile: '',
|
||||
@@ -98,25 +75,6 @@ const typeSelectOptions = SETTING_TYPE_OPTIONS.map((t) => ({
|
||||
label: t.value,
|
||||
}))
|
||||
|
||||
const GROUP_ICONS: Record<string, Component> = {
|
||||
customer_features: TeamOutlined,
|
||||
support_chat: MessageOutlined,
|
||||
general: HomeOutlined,
|
||||
chart: LineChartOutlined,
|
||||
pricing: DollarOutlined,
|
||||
montzar: LinkOutlined,
|
||||
store: ShopOutlined,
|
||||
deposit_methods: CreditCardOutlined,
|
||||
payping: WalletOutlined,
|
||||
accounting_provider: AccountBookOutlined,
|
||||
accounting_zarin: BankOutlined,
|
||||
accounting_tahesab: CalculatorOutlined,
|
||||
accounting_pargar: FileTextOutlined,
|
||||
price_channel: SendOutlined,
|
||||
infra: ClusterOutlined,
|
||||
misc: EllipsisOutlined,
|
||||
}
|
||||
|
||||
const count = computed(() => items.value.length)
|
||||
|
||||
function asRow(raw: Record<string, unknown>): KhanegiSettingRow {
|
||||
@@ -132,15 +90,19 @@ function asRow(raw: Record<string, unknown>): KhanegiSettingRow {
|
||||
})
|
||||
}
|
||||
|
||||
/** فقط پاسخ API — بدون merge با لیست hardcode فرانت */
|
||||
function applyItems(raw: unknown[]) {
|
||||
items.value = raw
|
||||
.map((row) => asRow((row || {}) as Record<string, unknown>))
|
||||
.filter((row) => String(row.key || '').trim() !== '')
|
||||
}
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
const q = search.value.trim().toLowerCase()
|
||||
return [...items.value]
|
||||
.filter((row) => {
|
||||
const key = String(row.key || '').trim()
|
||||
if (!key) return false
|
||||
// کلیدهای مالک نصب را نشان نده؛ بقیه (از API یا کاتالوگ SA) بمانند
|
||||
if (SUPERADMIN_EDITABLE_SETTING_KEYS.has(key)) return false
|
||||
if (!isSettingVisible(key)) return false
|
||||
if (filterType.value && resolveSettingFieldType(row) !== filterType.value) return false
|
||||
if (!q) return true
|
||||
return (
|
||||
@@ -155,44 +117,6 @@ const filteredRows = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const groupedFiltered = computed(() => {
|
||||
const map: Record<string, KhanegiSettingRow[]> = {}
|
||||
for (const g of KHANEGI_SYSTEM_SETTING_GROUPS) map[g.id] = []
|
||||
for (const row of filteredRows.value) {
|
||||
const gid = getKhanegiSystemGroupId(row.key || '')
|
||||
map[gid].push(row)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const visibleSettingGroups = computed(() =>
|
||||
KHANEGI_SYSTEM_SETTING_GROUPS.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<string>()
|
||||
const blocks = group.subsections
|
||||
.map((sub) => {
|
||||
const subRows = rows.filter((row) => {
|
||||
const key = row.key || ''
|
||||
if (used.has(key) || !sub.match(key)) return false
|
||||
used.add(key)
|
||||
return true
|
||||
})
|
||||
return { id: sub.id, title: sub.title, rows: subRows }
|
||||
})
|
||||
.filter((b) => b.rows.length > 0)
|
||||
const leftovers = rows.filter((row) => !used.has(row.key || ''))
|
||||
if (leftovers.length) {
|
||||
blocks.push({ id: `${group.id}-other`, title: 'سایر', rows: leftovers })
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
function rowColSpan(row: KhanegiSettingRow): number {
|
||||
return resolveSettingFieldType(row) === 'longtext' ? 24 : 8
|
||||
}
|
||||
@@ -311,39 +235,6 @@ function errText(e: any) {
|
||||
return Array.isArray(msg) ? msg.join('، ') : msg || 'خطا'
|
||||
}
|
||||
|
||||
function mergeSystemCatalogRows(rows: KhanegiSettingRow[]): KhanegiSettingRow[] {
|
||||
const byKey = new Map<string, KhanegiSettingRow>()
|
||||
// اول پاسخ API — تا کلید جدید سوپرادمین (مثل enable_automation) قبل از آپدیت کاتالوگ فرانت هم دیده شود
|
||||
for (const row of rows) {
|
||||
const key = rowKey(row)
|
||||
if (!key) continue
|
||||
byKey.set(key, normalizeSettingRow(row))
|
||||
}
|
||||
for (const key of SUPERADMIN_SYSTEM_SETTING_KEYS) {
|
||||
if (byKey.has(key)) continue
|
||||
byKey.set(
|
||||
key,
|
||||
normalizeSettingRow({
|
||||
key,
|
||||
value: '',
|
||||
title_fa: '',
|
||||
description: '',
|
||||
type: '',
|
||||
is_active: 1,
|
||||
can_edit: 0,
|
||||
}),
|
||||
)
|
||||
}
|
||||
return [...byKey.values()].sort((a, b) =>
|
||||
(a.title_fa || a.key || '').localeCompare(b.title_fa || b.key || '', 'fa'),
|
||||
)
|
||||
}
|
||||
|
||||
function applyItems(raw: unknown[]) {
|
||||
const rows = raw.map((row) => asRow((row || {}) as Record<string, unknown>))
|
||||
items.value = mergeSystemCatalogRows(rows)
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -425,7 +316,7 @@ function confirmPurgeNonOwned() {
|
||||
Modal.confirm({
|
||||
title: 'حذف تنظیمات غیر دسترسی مشتری؟',
|
||||
content:
|
||||
'فقط ۱۳ کلید «دسترسی مشتری به قابلیتها» روی این پروژه در سوپرادمین میماند. ' +
|
||||
'فقط کلیدهای «دسترسی مشتری به قابلیتها» روی این پروژه در سوپرادمین میماند. ' +
|
||||
'بقیه از جدول settings همین پروژه در سوپرادمین پاک میشود. ' +
|
||||
'قبل از این کار باید روی سرور همان نصب، settings:restore-from-backup را اجرا کرده باشی.',
|
||||
okText: 'حذف کن',
|
||||
@@ -454,42 +345,6 @@ async function purgeNonOwned() {
|
||||
}
|
||||
}
|
||||
|
||||
const collapsedSettingGroups = computed(() =>
|
||||
visibleSettingGroups.value.filter((g) => g.id !== 'customer_features'),
|
||||
)
|
||||
|
||||
const customerFeaturesGroup = computed(() =>
|
||||
visibleSettingGroups.value.find((g) => g.id === 'customer_features') ?? null,
|
||||
)
|
||||
|
||||
const customerFeaturesRows = computed(
|
||||
() => groupedFiltered.value.customer_features ?? [],
|
||||
)
|
||||
|
||||
const hasAnyVisibleSettings = computed(
|
||||
() => visibleSettingGroups.value.length > 0,
|
||||
)
|
||||
|
||||
const defaultOpenGroupId = computed(() => {
|
||||
const visibleIds = collapsedSettingGroups.value.map((g) => g.id)
|
||||
const preferred = ['chart', 'pricing', 'payping', 'store', 'general']
|
||||
return preferred.find((id) => visibleIds.includes(id)) ?? visibleIds[0]
|
||||
})
|
||||
|
||||
watch(
|
||||
collapsedSettingGroups,
|
||||
(groups) => {
|
||||
const ids = new Set(groups.map((g) => g.id))
|
||||
if (activeKey.value && !ids.has(String(activeKey.value))) {
|
||||
activeKey.value = groups[0]?.id
|
||||
}
|
||||
if (!activeKey.value && groups.length) {
|
||||
activeKey.value = defaultOpenGroupId.value
|
||||
}
|
||||
},
|
||||
{ flush: 'post' },
|
||||
)
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
@@ -636,19 +491,19 @@ onMounted(load)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="hasAnyVisibleSettings">
|
||||
<section v-if="customerFeaturesGroup && customerFeaturesRows.length" class="features-open-section">
|
||||
<template v-if="filteredRows.length">
|
||||
<section class="features-open-section">
|
||||
<div class="features-open-section__head">
|
||||
<component :is="GROUP_ICONS.customer_features || TeamOutlined" class="settings-collapse-icon" />
|
||||
<span class="settings-collapse-title">{{ customerFeaturesGroup.title }}</span>
|
||||
<TeamOutlined class="settings-collapse-icon" />
|
||||
<span class="settings-collapse-title">دسترسی مشتری به قابلیتها</span>
|
||||
<a-badge
|
||||
:count="customerFeaturesRows.length"
|
||||
:count="filteredRows.length"
|
||||
:number-style="{ backgroundColor: '#d4af37', color: '#fff', fontSize: '11px' }"
|
||||
/>
|
||||
</div>
|
||||
<a-row :gutter="[16, 16]" class="settings-grid">
|
||||
<a-col
|
||||
v-for="row in customerFeaturesRows"
|
||||
v-for="row in filteredRows"
|
||||
:key="rowKey(row)"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
@@ -666,71 +521,6 @@ onMounted(load)
|
||||
</a-col>
|
||||
</a-row>
|
||||
</section>
|
||||
|
||||
<a-collapse
|
||||
v-if="collapsedSettingGroups.length"
|
||||
v-model:activeKey="activeKey"
|
||||
accordion
|
||||
class="settings-collapse"
|
||||
:bordered="false"
|
||||
>
|
||||
<a-collapse-panel v-for="group in collapsedSettingGroups" :key="group.id">
|
||||
<template #header>
|
||||
<div class="settings-collapse-header">
|
||||
<component :is="GROUP_ICONS[group.id] || EllipsisOutlined" 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="rowKey(row)"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:lg="rowColSpan(row)"
|
||||
>
|
||||
<SettingCard
|
||||
:row="row"
|
||||
:model-value="draftValue(row)"
|
||||
:disabled="!canWrite"
|
||||
:saving="isSaving(row)"
|
||||
:saved-flash="isSavedFlash(row)"
|
||||
@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="rowKey(row)"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:lg="rowColSpan(row)"
|
||||
>
|
||||
<SettingCard
|
||||
:row="row"
|
||||
:model-value="draftValue(row)"
|
||||
:disabled="!canWrite"
|
||||
:saving="isSaving(row)"
|
||||
:saved-flash="isSavedFlash(row)"
|
||||
@update:value="onValueChange(row, $event)"
|
||||
@blur="onFieldBlur(row)"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</template>
|
||||
<a-empty
|
||||
v-else
|
||||
|
||||
Reference in New Issue
Block a user