fix chart

This commit is contained in:
2026-06-21 20:27:13 +03:30
parent bbca1c8991
commit 5586985d1e
21 changed files with 1727 additions and 1147 deletions
+127 -150
View File
@@ -1,6 +1,6 @@
<template>
<div class="h-full min-h-0">
<BasicShowContent title="تنظیمات" class="h-full">
<BasicShowContent :title="pageTitle" class="h-full">
<div class="settings-toolbar">
<a-input
v-model:value="search"
@@ -27,6 +27,15 @@
</div>
<!-- <a-button type="primary" :loading="loading" @click="add">افزودن تنظیم</a-button>-->
<a-button
v-if="isSystemMode"
type="primary"
class="settings-toolbar__action"
:loading="loading"
@click="add"
>
افزودن تنظیم
</a-button>
<a-button class="settings-toolbar__action" :loading="loading" @click="initConfig">
بروزرسانی
</a-button>
@@ -34,21 +43,20 @@
<a-spin :spinning="loading && !baseData.length">
<a-collapse
v-model:activeKey="activeKeys"
v-model:activeKey="activeKey"
accordion
class="settings-collapse"
:bordered="false"
>
<a-collapse-panel
v-for="group in SETTING_GROUPS"
v-for="group in visibleSettingGroups"
: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' }"
/>
@@ -57,7 +65,7 @@
<a-row :gutter="[16, 16]" class="settings-grid">
<a-col
v-for="row in groupedFiltered[group.id] || []"
v-for="row in groupedFiltered[group.id]"
:key="row.id"
:xs="24"
:sm="12"
@@ -74,16 +82,16 @@
/>
</a-col>
</a-row>
<div v-if="!groupedFiltered[group.id]?.length" class="settings-empty-group">
موردی در این گروه یافت نشد
</div>
</a-collapse-panel>
</a-collapse>
<a-empty
v-if="!loading && Object.keys(groupedFiltered).length === 0"
description="تنظیمی یافت نشد"
v-if="!loading && visibleSettingGroups.length === 0"
:description="
isSystemMode
? 'تنظیم سیستمی فعالی (can_edit=0) یافت نشد — deploy_settings_const.sql را اجرا کنید'
: 'تنظیمی یافت نشد'
"
class="mt-8"
/>
</a-spin>
@@ -161,20 +169,43 @@ import type { FormInstance } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import type { Const } from '@/model/configuration'
import { loading } from '@/utils'
import { getEditableConfiguration, setConfiguration } from '@/api/configuration'
import { getEditableConfiguration, getSystemConfiguration, setConfiguration, setSystemConfiguration } from '@/api/configuration'
import SettingValueField from './SettingValueField.vue'
import SettingCard from './SettingCard.vue'
import { refreshAppModeMenu } from '@/router'
import { useUserStore } from '@/store/userStore'
import { setStoreModeFromSettings } from '@/utils/config'
import {
EDITABLE_SETTING_GROUPS,
SYSTEM_SETTING_GROUPS,
getSettingGroupId
} from './settingGroups'
import {
SETTING_TYPE_OPTIONS,
formatValueForSave,
helpText,
isSettingVisible,
isSettingLocked,
isSettingActive,
isSwitchOn,
resolveSettingFieldType
} from './settingTypes'
const props = withDefaults(defineProps<{ mode?: 'editable' | 'system' }>(), {
mode: 'editable'
})
const userStore = useUserStore()
function refreshMenuAfterStoreChange() {
refreshAppModeMenu(userStore.userInfo?.mobile)
}
const isSystemMode = computed(() => props.mode === 'system')
const pageTitle = computed(() => (isSystemMode.value ? 'تنظیمات پیشرفته' : 'تنظیمات'))
const settingGroups = computed(() =>
isSystemMode.value ? SYSTEM_SETTING_GROUPS : EDITABLE_SETTING_GROUPS
)
const settingTypeOptions = SETTING_TYPE_OPTIONS
const typeSelectOptions = SETTING_TYPE_OPTIONS.map((t) => ({
@@ -224,7 +255,7 @@ function sortRows(rows: Const[]): Const[] {
}
function isEditable(row: Const): boolean {
return row.can_edit !== 0 && row.can_edit !== false
return !isSettingLocked(row.can_edit)
}
function matchesFilter(row: Const): boolean {
@@ -244,13 +275,14 @@ function matchesFilter(row: Const): boolean {
const filteredRows = computed(() =>
sortRows(
(baseData.value || []).filter(
(row) =>
row.id != null &&
isEditable(row) &&
isSettingVisible(row.key) &&
matchesFilter(row)
)
(baseData.value || []).filter((row) => {
if (row.id == null || !matchesFilter(row)) return false
if (isSystemMode.value) {
// API با system_only=1 فیلتر کرده؛ فقط جستجو اعمال می‌شود
return true
}
return isEditable(row) && isSettingVisible(row.key)
})
)
)
@@ -265,148 +297,75 @@ const otherRows = computed(() =>
// =============================================================================
// گروه‌بندی تنظیمات — اولین match برنده است؛ misc فقط برای باقی‌مانده
// =============================================================================
const SETTING_GROUPS: Array<{
id: string
title: string
icon: string
match: (key: string) => boolean
}> = [
{
id: 'general',
title: 'عمومی و برند اپ',
icon: 'vuesax-linear:home-2',
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
)
},
{
id: 'chart',
title: 'نمودار قیمت',
icon: 'vuesax-linear:chart-2',
match: (k) => /^chart_/.test(k)
},
{
id: 'pricing',
title: 'قیمت‌گذاری، کارمزد و تابلو',
icon: 'vuesax-linear:dollar-circle',
match: (k) =>
/^(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)
},
{
id: 'referral',
title: 'معرف، پاداش و ثبت‌نام',
icon: 'vuesax-linear:people',
match: (k) =>
/^(referral_|gift_for|register_referral|register_jibit|has_page_earn_money|jibit_verification|referral_bonus|invite_link_text)/.test(
k
)
},
{
id: 'montzar',
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',
match: (k) => k.startsWith('support_messenger') || k === 'deposit_sms_support_number'
},
{
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)
},
{
id: 'features',
title: 'محصولات و قابلیت‌های سیستم',
icon: 'vuesax-linear:setting-2',
match: (k) => /^(product_|main_wallet_products|price_from_nasiryan|profit_gold)/.test(k)
},
{
id: 'misc',
title: 'سایر تنظیمات',
icon: 'vuesax-linear:more-circle',
match: () => true
}
]
function getGroupId(key: string): string {
for (const g of SETTING_GROUPS) {
if (g.id === 'misc') continue
if (g.match(key)) return g.id
}
return 'misc'
return getSettingGroupId(key, settingGroups.value)
}
const groupedFiltered = computed(() => {
const map: Record<string, Const[]> = {}
for (const g of SETTING_GROUPS) map[g.id] = []
for (const g of settingGroups.value) 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 = ['general', 'pricing', 'store', 'payping', 'tahesab']
const activeKeys = ref<string[]>(defaultOpenGroups)
const visibleSettingGroups = computed(() =>
settingGroups.value.filter((g) => (groupedFiltered.value[g.id]?.length ?? 0) > 0)
)
const defaultOpenGroupId = computed(() => {
const visibleIds = visibleSettingGroups.value.map((g) => g.id)
const preferred = isSystemMode.value
? ['chart', 'pricing', 'payping', 'tahesab', 'store', 'general']
: ['general', 'content', 'pricing', 'referral']
const fromPreferred = preferred.find((id) => visibleIds.includes(id))
return fromPreferred ?? visibleIds[0]
})
const activeKey = ref<string | undefined>(undefined)
watch(
visibleSettingGroups,
(groups) => {
const visibleIds = new Set(groups.map((g) => g.id))
if (activeKey.value && !visibleIds.has(activeKey.value)) {
activeKey.value = groups[0]?.id
}
if (!activeKey.value && groups.length) {
activeKey.value = defaultOpenGroupId.value
}
},
{ flush: 'post' }
)
watch(
defaultOpenGroupId,
(id) => {
if (!activeKey.value && id) {
activeKey.value = id
}
},
{ immediate: true }
)
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]
if (!activeKey.value || !visibleSettingGroups.value.some((g) => g.id === activeKey.value)) {
activeKey.value = defaultOpenGroupId.value
}
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
const matching = Object.keys(groupedFiltered.value).find(
(gid) => (groupedFiltered.value[gid]?.length ?? 0) > 0
)
activeKey.value = matching ?? defaultOpenGroupId.value
}, { flush: 'post' })
function rowColSpan(row: Const): number {
@@ -414,7 +373,8 @@ function rowColSpan(row: Const): number {
}
function isReadonly(row: Const): boolean {
return row.can_edit === 0 || row.can_edit === false
if (isSystemMode.value) return false
return isSettingLocked(row.can_edit)
}
function draftValue(row: Const): string {
@@ -443,14 +403,29 @@ onMounted(() => {
initConfig()
})
watch(
() => props.mode,
() => {
search.value = ''
filterType.value = undefined
activeKey.value = undefined
initConfig()
}
)
async function initConfig() {
try {
loading.value = true
baseData.value = (await getEditableConfiguration()) ?? []
const storeRow = baseData.value.find((row) => row.key === 'is_store')
if (storeRow) {
setStoreModeFromSettings(isSwitchOn(storeRow.value))
refreshAppModeMenu()
baseData.value =
(isSystemMode.value
? await getSystemConfiguration()
: await getEditableConfiguration()) ?? []
if (!isSystemMode.value) {
const storeRow = baseData.value.find((row) => row.key === 'is_store')
if (storeRow) {
setStoreModeFromSettings(isSwitchOn(storeRow.value))
refreshMenuAfterStoreChange()
}
}
syncDraftsFromBase()
} catch (e) {
@@ -534,7 +509,8 @@ async function saveSettingRow(row: Const) {
savedFlashIds.value.delete(id)
try {
await setConfiguration({
const saveFn = isSystemMode.value ? setSystemConfiguration : setConfiguration
await saveFn({
id: row.id,
key: row.key,
value,
@@ -551,9 +527,9 @@ async function saveSettingRow(row: Const) {
} else {
scheduleSave(row)
}
if (row.key === 'is_store') {
if (row.key === 'is_store' && !isSystemMode.value) {
setStoreModeFromSettings(isSwitchOn(value))
refreshAppModeMenu()
refreshMenuAfterStoreChange()
}
savedFlashIds.value.add(id)
if (saveTimers.has(id)) clearTimeout(saveTimers.get(id))
@@ -610,7 +586,8 @@ async function ok() {
try {
await formRef.value?.validateFields()
loading.value = true
await setConfiguration(payload)
const saveFn = isSystemMode.value ? setSystemConfiguration : setConfiguration
await saveFn(payload)
message.success('تنظیم جدید اضافه شد')
addConfigVisible.value = false
await initConfig()
@@ -0,0 +1,158 @@
export type SettingGroupDef = {
id: string
title: string
icon: string
match: (key: string) => boolean
}
/** گروه‌های صفحه تنظیمات عادی (فقط can_edit=1) */
export const EDITABLE_SETTING_GROUPS: SettingGroupDef[] = [
{
id: 'general',
title: 'عمومی و برند',
icon: 'vuesax-linear:home-2',
match: (k) =>
/^(name_fa|name_en|bace_url|server_url|logo_|title_pwa|title_fav|open_register|auto_active_customer|store_status|key$)/.test(
k
)
},
{
id: 'pricing',
title: 'کارمزد و محدودیت‌ها',
icon: 'vuesax-linear:dollar-circle',
match: (k) =>
/^(fee_|buy_fee|sell_fee|min_|minimum_|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)
},
{
id: 'referral',
title: 'معرف و پاداش',
icon: 'vuesax-linear:people',
match: (k) =>
/^(referral_|gift_for|register_referral|register_jibit|jibit_verification|referral_bonus|invite_link_text)/.test(
k
)
},
{
id: 'montzar',
title: 'همکار Montzar',
icon: 'vuesax-linear:link-2',
match: (k) =>
(k.startsWith('montzar_') && !['montzar_base_url', 'montzar_closed'].includes(k)) ||
k === 'display_montzar'
},
{
id: 'store',
title: 'سفارش خارجی',
icon: 'vuesax-linear:shop',
match: (k) => /^external_order/.test(k)
},
{
id: 'support',
title: 'پشتیبانی',
icon: 'vuesax-linear:headphone',
match: (k) => k.startsWith('support_messenger') || k === 'deposit_sms_support_number'
},
{
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/.test(k)
},
{
id: 'features',
title: 'قابلیت‌های سیستم',
icon: 'vuesax-linear:setting-2',
match: (k) => /^(product_|main_wallet_products|price_from_nasiryan|profit_gold)/.test(k)
},
{
id: 'misc',
title: 'سایر',
icon: 'vuesax-linear:more-circle',
match: () => true
}
]
/** گروه‌های صفحه دیپلوی — شامل تنظیمات can_edit=0 */
export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
{
id: 'general',
title: 'عمومی و برند',
icon: 'vuesax-linear:home-2',
match: (k) =>
/^(name_fa|name_en|app_url|admin_url|apk_download_link|banner_enabled|bace_url|server_url|logo_|title_pwa|title_fav|metal_type|unit$|open_register|auto_active_customer|store_status|key$|is_store|group_sms|has_page_earn_money)/.test(
k
)
},
{
id: 'chart',
title: 'نمودار قیمت',
icon: 'vuesax-linear:chart-2',
match: (k) => /^chart_/.test(k)
},
{
id: 'pricing',
title: 'منبع قیمت و تابلو',
icon: 'vuesax-linear:dollar-circle',
match: (k) =>
/^(price_provider|zaarbox_|main_product$|default_vc_number)/.test(k) ||
k.includes('charge_percentage')
},
{
id: 'montzar',
title: 'Montzar',
icon: 'vuesax-linear:link-2',
match: (k) => /^montzar_/.test(k) || k === 'display_montzar'
},
{
id: 'store',
title: 'اتصال فروشگاه',
icon: 'vuesax-linear:shop',
match: (k) => /^node_/.test(k) || k === 'montify_name' || k === 'external_order_api_enabled'
},
{
id: 'payping',
title: 'درگاه Payping',
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: 'infra',
title: 'زیرساخت',
icon: 'vuesax-linear:cpu',
match: (k) => /^(wss_enabled|sse_path|wss_path|wss_link|wss_trigger_path|profit_gold_api_enabled)/.test(k)
},
{
id: 'misc',
title: 'سایر',
icon: 'vuesax-linear:more-circle',
match: () => true
}
]
export function getSettingGroupId(key: string, groups: SettingGroupDef[]): string {
for (const g of groups) {
if (g.id === 'misc') continue
if (g.match(key)) return g.id
}
return 'misc'
}
@@ -19,6 +19,15 @@ export const HIDDEN_LEGACY_SETTING_KEYS = new Set([
/** فقط وقتی is_store فعال است در پنل دیده می‌شوند */
export const STORE_ONLY_SETTING_KEYS = new Set(['store_status', 'price_from_nasiryan'])
export function isSettingLocked(canEdit: Const['can_edit']): boolean {
return canEdit === 0 || canEdit === false || canEdit === '0'
}
export function isSettingActive(isActive: Const['is_active']): boolean {
if (isActive === 0 || isActive === false || isActive === '0') return false
return isActive !== null && isActive !== undefined
}
export function isSettingVisible(key: string | undefined | null): boolean {
const k = (key || '').trim()
if (!k) return false
@@ -78,6 +87,8 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
external_order_api_enabled: 'switch',
is_store: 'switch',
price_from_nasiryan: 'switch',
banner_enabled: 'switch',
apk_download_link: 'text',
node_client_id: 'text',
node_shared_secret: 'text',
tahesab_host: 'text',
+15 -2
View File
@@ -11,7 +11,10 @@
class="hidden md:inline overflow-auto min-w-[13.5rem]"
/>
<!-- <component :is="selectedComponent" class="mr-5" />-->
<RouterView class="mt-5 min-h-0 w-full flex-1 overflow-hidden md:mr-5 md:mt-0" />
<RouterView
:key="route.name"
class="mt-5 min-h-0 w-full flex-1 overflow-hidden md:mr-5 md:mt-0"
/>
</div>
</BasicMainCard>
</div>
@@ -20,14 +23,24 @@
<script setup lang="ts">
import BasicMenu from '@/components/BasicMenu/index.vue'
import { useRouterStore } from '@core/stores/router'
import { useUserStore } from '@/store/userStore'
import { isDeployAdmin } from '@/utils/deployAdmin'
import { get } from 'lodash'
import type { RouteRecordRaw } from 'vue-router'
const routerStore = useRouterStore()
const route = useRoute()
const userStore = useUserStore()
const menu = computed(() => {
const item = routerStore.routes.find((item) => item.name === 'settings')
const children = get(item, 'children[0].children', []) as RouteRecordRaw[]
return children.filter((child) => !child.meta?.hideMenu)
return children
.filter((child) => !child.meta?.hideMenu)
.filter(
(child) =>
!child.meta?.deployAdminOnly || isDeployAdmin(userStore.userInfo?.mobile)
)
.sort((a, b) => (Number(a.meta?.order) || 99) - (Number(b.meta?.order) || 99))
})
</script>