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">
|
<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
|
||||||
|
|||||||
Reference in New Issue
Block a user