refactor(settings): render SuperAdmin keys from API only, flat list

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-24 16:34:31 +03:30
co-authored by Cursor
parent 7f3e537c24
commit 2e22af121d
+15 -225
View File
@@ -1,45 +1,23 @@
<script setup lang="ts"> <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 { useRoute, useRouter } from 'vue-router'
import { message, Modal } from 'ant-design-vue' import { message, Modal } from 'ant-design-vue'
import { import {
AccountBookOutlined,
ArrowRightOutlined, ArrowRightOutlined,
BankOutlined,
CalculatorOutlined,
ClusterOutlined,
CreditCardOutlined,
DeleteOutlined, DeleteOutlined,
DollarOutlined,
EllipsisOutlined,
FileTextOutlined,
GlobalOutlined, GlobalOutlined,
HomeOutlined,
LineChartOutlined,
LinkOutlined,
MessageOutlined,
SearchOutlined, SearchOutlined,
SendOutlined,
ShopOutlined,
TeamOutlined, TeamOutlined,
WalletOutlined,
} from '@ant-design/icons-vue' } from '@ant-design/icons-vue'
import api from '@/api/http' import api from '@/api/http'
import VersionPageTabs from '@/components/VersionPageTabs.vue' import VersionPageTabs from '@/components/VersionPageTabs.vue'
import SettingCard from '@/components/program-settings/SettingCard.vue' import SettingCard from '@/components/program-settings/SettingCard.vue'
import {
KHANEGI_SYSTEM_SETTING_GROUPS,
getKhanegiSystemGroupId,
type SettingGroupDef,
} from '@/config/khanegiSystemSettingGroups'
import { import {
SETTING_TYPE_OPTIONS, SETTING_TYPE_OPTIONS,
formatValueForSave, formatValueForSave,
isSettingVisible,
normalizeSettingRow, normalizeSettingRow,
resolveSettingFieldType, resolveSettingFieldType,
} from '@/config/khanegiSettingCatalog' } from '@/config/khanegiSettingCatalog'
import { SUPERADMIN_EDITABLE_SETTING_KEYS, SUPERADMIN_SYSTEM_SETTING_KEYS } from '@/config/superAdminSettingKeys'
import type { KhanegiSettingRow } from '@/config/khanegiSettingRow' import type { KhanegiSettingRow } from '@/config/khanegiSettingRow'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
@@ -72,7 +50,6 @@ const savingKeys = ref<Set<string>>(new Set())
const savedFlashKeys = ref<Set<string>>(new Set()) const savedFlashKeys = ref<Set<string>>(new Set())
const saveTimers = new Map<string, ReturnType<typeof setTimeout>>() const saveTimers = new Map<string, ReturnType<typeof setTimeout>>()
const flashTimers = new Map<string, ReturnType<typeof setTimeout>>() const flashTimers = new Map<string, ReturnType<typeof setTimeout>>()
const activeKey = ref<string | undefined>(undefined)
const creds = reactive({ const creds = reactive({
mobile: '', mobile: '',
@@ -98,25 +75,6 @@ const typeSelectOptions = SETTING_TYPE_OPTIONS.map((t) => ({
label: t.value, 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) const count = computed(() => items.value.length)
function asRow(raw: Record<string, unknown>): KhanegiSettingRow { 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 filteredRows = computed(() => {
const q = search.value.trim().toLowerCase() const q = search.value.trim().toLowerCase()
return [...items.value] return [...items.value]
.filter((row) => { .filter((row) => {
const key = String(row.key || '').trim() const key = String(row.key || '').trim()
if (!key) return false 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 (filterType.value && resolveSettingFieldType(row) !== filterType.value) return false
if (!q) return true if (!q) return true
return ( 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 { function rowColSpan(row: KhanegiSettingRow): number {
return resolveSettingFieldType(row) === 'longtext' ? 24 : 8 return resolveSettingFieldType(row) === 'longtext' ? 24 : 8
} }
@@ -311,39 +235,6 @@ function errText(e: any) {
return Array.isArray(msg) ? msg.join('، ') : msg || 'خطا' 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() { async function load() {
loading.value = true loading.value = true
try { try {
@@ -425,7 +316,7 @@ function confirmPurgeNonOwned() {
Modal.confirm({ Modal.confirm({
title: 'حذف تنظیمات غیر دسترسی مشتری؟', title: 'حذف تنظیمات غیر دسترسی مشتری؟',
content: content:
'فقط ۱۳ کلید «دسترسی مشتری به قابلیت‌ها» روی این پروژه در سوپرادمین می‌ماند. ' + 'فقط کلیدهای «دسترسی مشتری به قابلیت‌ها» روی این پروژه در سوپرادمین می‌ماند. ' +
'بقیه از جدول settings همین پروژه در سوپرادمین پاک می‌شود. ' + 'بقیه از جدول settings همین پروژه در سوپرادمین پاک می‌شود. ' +
'قبل از این کار باید روی سرور همان نصب، settings:restore-from-backup را اجرا کرده باشی.', 'قبل از این کار باید روی سرور همان نصب، settings:restore-from-backup را اجرا کرده باشی.',
okText: 'حذف کن', 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) onMounted(load)
</script> </script>
@@ -636,19 +491,19 @@ onMounted(load)
</div> </div>
</div> </div>
<template v-if="hasAnyVisibleSettings"> <template v-if="filteredRows.length">
<section v-if="customerFeaturesGroup && customerFeaturesRows.length" class="features-open-section"> <section class="features-open-section">
<div class="features-open-section__head"> <div class="features-open-section__head">
<component :is="GROUP_ICONS.customer_features || TeamOutlined" class="settings-collapse-icon" /> <TeamOutlined class="settings-collapse-icon" />
<span class="settings-collapse-title">{{ customerFeaturesGroup.title }}</span> <span class="settings-collapse-title">دسترسی مشتری به قابلیت‌ها</span>
<a-badge <a-badge
:count="customerFeaturesRows.length" :count="filteredRows.length"
:number-style="{ backgroundColor: '#d4af37', color: '#fff', fontSize: '11px' }" :number-style="{ backgroundColor: '#d4af37', color: '#fff', fontSize: '11px' }"
/> />
</div> </div>
<a-row :gutter="[16, 16]" class="settings-grid"> <a-row :gutter="[16, 16]" class="settings-grid">
<a-col <a-col
v-for="row in customerFeaturesRows" v-for="row in filteredRows"
:key="rowKey(row)" :key="rowKey(row)"
:xs="24" :xs="24"
:sm="12" :sm="12"
@@ -666,71 +521,6 @@ onMounted(load)
</a-col> </a-col>
</a-row> </a-row>
</section> </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> </template>
<a-empty <a-empty
v-else v-else