admin big changes

This commit is contained in:
2026-06-13 17:38:35 +03:30
parent 213ef1a1b7
commit 0c404ee3e1
14 changed files with 188 additions and 276 deletions
@@ -3,24 +3,13 @@
class="setting-card"
:class="{
'setting-card--readonly': readonly,
'setting-card--saving': saving,
'setting-card--inactive': !isActive && fieldType !== 'switch'
'setting-card--saving': saving
}"
>
<div class="setting-card__head">
<div class="setting-card__title-row">
<span v-if="readonly" class="setting-card__lock" title="فقط‌خواندنی">🔒</span>
<span class="setting-card__title">{{ title }}</span>
<a-switch
v-if="fieldType !== 'switch'"
:checked="isActive"
size="small"
class="setting-card__active-switch"
@change="onActiveChange"
>
<template #checkedChildren>فعال</template>
<template #unCheckedChildren>غیرفعال</template>
</a-switch>
</div>
<div class="setting-card__key">کلید: {{ settingKey }}</div>
<div v-if="help" class="setting-card__help">{{ help }}</div>
@@ -62,7 +51,6 @@ const props = defineProps<{
const emit = defineEmits<{
'update:value': [value: string]
'update:isActive': [value: boolean]
}>()
const settingKey = computed(() => props.row.key ?? '')
@@ -74,12 +62,6 @@ const fieldType = computed<SettingFieldType>(() =>
resolveSettingFieldType({ key: props.row.key, type: props.row.type })
)
const isActive = computed(() => props.row.is_active !== false && props.row.is_active !== 0)
function onActiveChange(checked: boolean) {
emit('update:isActive', checked)
}
const help = computed(() => helpText(props.row))
</script>
@@ -108,11 +90,6 @@ const help = computed(() => helpText(props.row))
border-color: #91caff;
}
&--inactive {
opacity: 0.72;
background: #fafafa;
}
&__head {
flex-shrink: 0;
}
@@ -123,11 +100,6 @@ const help = computed(() => helpText(props.row))
gap: 6px;
}
&__active-switch {
margin-right: auto;
flex-shrink: 0;
}
&__lock {
flex-shrink: 0;
font-size: 13px;
@@ -6,8 +6,8 @@
:disabled="disabled"
@change="onSwitchChange"
>
<template #checkedChildren>فعال</template>
<template #unCheckedChildren>غیرفعال</template>
<template #checkedChildren>{{ labels.on }}</template>
<template #unCheckedChildren>{{ labels.off }}</template>
</a-switch>
<ZaarboxPriceKeyField
@@ -63,10 +63,11 @@
<script setup lang="ts">
import ZaarboxPriceKeyField from './ZaarboxPriceKeyField.vue'
import {
isSwitchOn,
parseSelectOptions,
resolveSettingFieldType,
switchToString,
resolveSwitchChecked,
switchLabels,
switchValueFromChecked,
type SettingFieldType
} from './settingTypes'
@@ -101,10 +102,12 @@ const selectItems = computed(() =>
})
)
const switchChecked = computed(() => isSwitchOn(props.modelValue))
const labels = computed(() => switchLabels(props.settingKey))
const switchChecked = computed(() => resolveSwitchChecked(props.settingKey, props.modelValue))
function onSwitchChange(checked: boolean) {
emit('update:value', switchToString(checked))
emit('update:value', switchValueFromChecked(props.settingKey, checked))
}
function onSelectChange(val: string | undefined) {
+30 -49
View File
@@ -14,16 +14,6 @@
</a-input>
<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"
/>
</label>
<label class="settings-filter-field">
<span class="settings-filter-field__label">نوع فیلد</span>
<a-select
@@ -80,7 +70,6 @@
:saving="isSaving(row.id)"
:saved-flash="isSavedFlash(row.id)"
@update:value="onValueChange(row, $event)"
@update:is-active="onActiveChange(row, $event)"
/>
</a-col>
</a-row>
@@ -187,11 +176,6 @@ import {
const settingTypeOptions = SETTING_TYPE_OPTIONS
const statusFilterOptions = [
{ value: 'active', label: 'فعال' },
{ value: 'inactive', label: 'غیرفعال' }
]
const typeSelectOptions = SETTING_TYPE_OPTIONS.map((t) => ({
value: t.key,
label: t.value
@@ -202,11 +186,6 @@ 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())
@@ -249,8 +228,6 @@ function isEditable(row: Const): boolean {
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
@@ -297,16 +274,29 @@ const SETTING_GROUPS: Array<{
id: 'general',
title: 'عمومی و اطلاعات اپ',
icon: 'vuesax-linear:home-2',
match: (k) => /^(name_|app_|admin_|server_|bace_|key$|color_|logo_|title_pwa|title_fav|metal_type|unit$)/.test(k)
match: (k) => /^(name_fa|app_url|admin_url|metal_type|unit$|open_register|auto_active_customer|is_store)/.test(k)
},
{
id: 'chart',
title: 'نمودار قیمت',
icon: 'vuesax-linear:chart-2',
match: (k) => /^chart_/.test(k)
},
{
id: 'pricing',
title: 'قیمت‌گذاری و کارمزدها',
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) ||
/^(fee_|buy_fee|sell_fee|min_|price_provider|zaarbox_|main_product$|show_last_price|close_price|startDay|endDay)/.test(k) ||
k.includes('charge_percentage')
},
{
id: 'referral',
title: 'معرف، پاداش و ثبت‌نام',
icon: 'vuesax-linear:people',
match: (k) =>
/^(referral_|gift_for|register_referral|register_jibit|has_page_earn_money|jibit_verification)/.test(k)
},
{
id: 'montzar',
title: 'سرویس همکار (montzar)',
@@ -315,22 +305,22 @@ const SETTING_GROUPS: Array<{
},
{
id: 'features',
title: 'ویژگی‌ها و صفحات',
title: 'ویژگی‌ها و اعلان‌ها',
icon: 'vuesax-linear:menu',
match: (k) =>
/^(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|is_store|store_status)/.test(k)
match: (k) => /^(charge_notif|group_sms|main_wallet_products|price_from_nasiryan)/.test(k)
},
{
id: 'integrations',
title: 'اتصالات خارجی و درگاه‌ها',
icon: 'vuesax-linear:card',
match: (k) => /^(tahesab_|payping_|jibit_|telegram_|bale_|liman_|support_messenger|external_order|profit_gold)/.test(k)
match: (k) =>
/^(tahesab_|payping_|telegram_|bale_|support_messenger|external_order|profit_gold|node_)/.test(k)
},
{
id: 'misc',
title: 'سایر تنظیمات (متفرقه)',
title: 'سایر تنظیمات',
icon: 'vuesax-linear:more-circle',
match: () => true // catch-all
match: () => true
}
]
@@ -434,12 +424,6 @@ function onValueChange(row: Const, value: string) {
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)) {
@@ -457,21 +441,18 @@ async function persistRowValue(id: number) {
await saveSettingRow(row)
}
async function persistRow(row: Const, overrides?: { isActive?: boolean }) {
await saveSettingRow(row, overrides)
async function persistRow(row: Const) {
await saveSettingRow(row)
}
async function saveSettingRow(row: Const, overrides?: { isActive?: boolean }) {
async function saveSettingRow(row: Const) {
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
const value = formatValueForSave(raw, type, row.key)
if (value === (row.value ?? '')) return
savingIds.value.add(id)
savedFlashIds.value.delete(id)
@@ -482,12 +463,12 @@ async function saveSettingRow(row: Const, overrides?: { isActive?: boolean }) {
key: row.key,
value,
description: row.description ?? '',
is_active: isActive,
is_active: true,
title_fa: row.title_fa ?? '',
type
})
row.value = value
row.is_active = isActive
row.is_active = true
drafts.value[id] = value
if (row.key === 'is_store') {
setStoreModeFromSettings(isSwitchOn(value))
@@ -536,7 +517,7 @@ async function ok() {
const payload = {
key: form.value.key,
value: formatValueForSave(form.value.value ?? '', type),
value: formatValueForSave(form.value.value ?? '', type, form.value.key),
description,
is_active: form.value.is_active,
title_fa: form.value.title_fa ?? '',
@@ -3,18 +3,21 @@ import { isStoreApp } from '@/utils/config'
export type SettingFieldType = 'text' | 'number' | 'switch' | 'select' | 'longtext'
/** کلیدهای legacy — در پنل نمایش داده نمی‌شوند (جایگزین: price_provider) */
/** کلیدهای legacy — در پنل نمایش داده نمی‌شوند */
export const HIDDEN_LEGACY_SETTING_KEYS = new Set([
'main_product_price',
'montzar_price',
'etehadie_api_url',
'goldir_price',
'price_primary_source',
'price_method'
'price_method',
'montzar_last_product_log',
'montzar_order_response',
'service_charge_percentage'
])
/** فقط وقتی is_store فعال است در پنل دیده می‌شوند */
export const STORE_ONLY_SETTING_KEYS = new Set(['store_status'])
export const STORE_ONLY_SETTING_KEYS = new Set(['store_status', 'price_from_nasiryan'])
export function isSettingVisible(key: string | undefined | null): boolean {
const k = (key || '').trim()
@@ -33,8 +36,9 @@ export const SETTING_TYPE_OPTIONS = [
]
const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
chart_use_local_data: 'switch',
price_provider: 'select',
referral_id: 'select',
referral_id: 'switch',
payping_gateway_name: 'select',
text_rule: 'longtext',
text_about: 'longtext',
@@ -43,66 +47,43 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
buy_fee: 'number',
sell_fee: 'number',
fee_cash: 'number',
fee_free: 'number',
min_withdraw: 'number',
min_first_buy: 'number',
minimum_sell: 'number',
gift_for_referrer: 'number',
gift_for_referral: 'number',
referral_bonus_amount: 'text',
referral_count_for_gift: 'number',
montzar_product_id: 'number',
balance_report_exclude_customer_id: 'number',
jibit_verification: 'switch',
register_jibit_verify: 'switch',
register_referral_sms_to_referrer: 'switch',
register_referral_bonus_message: 'switch',
open_register: 'switch',
auto_active_customer: 'switch',
close_price: 'switch',
montzar_price: 'switch',
main_product_price: 'switch',
show_last_price: 'switch',
send_to_tahesab: 'switch',
profit_gold_api_enabled: 'switch',
payping_round_toman: 'switch',
payping_strict_failed_withdraw: 'switch',
has_page_earn_money: 'switch',
has_page_fund: 'switch',
has_page_permission: 'switch',
has_page_price_trigger: 'switch',
has_page_physical_gold: 'switch',
has_chat: 'switch',
charge_notif: 'switch',
group_sms: 'switch',
display_montzar: 'switch',
credit: 'switch',
credit_daily: 'switch',
credit_error_in_orders: 'switch',
credit_rial_dynamic: 'switch',
open_closed_messages: 'switch',
new_style_create_order: 'switch',
new_style_admin_layout: 'switch',
register: 'switch',
sharh: 'switch',
multi_source: 'switch',
get_orders_pdf: 'switch',
get_orders_excel: 'switch',
auto_order_buy_sell: 'switch',
invite_link: 'switch',
montzar_closed: 'switch',
montzar_open_buy: 'switch',
montzar_open_sell: 'switch',
product_update_price_on_list: 'switch',
product_auto_close_open_on_list: 'switch',
payping_fin_sync_enabled: 'switch',
external_order_api_enabled: 'switch',
is_store: 'switch',
store_status: 'select',
credit_rial_price_unit: 'select',
zaarbox_price_key: 'select',
price_from_nasiryan: 'switch',
node_client_id: 'text',
node_shared_secret: 'text',
support_messenger_id: 'text',
support_messenger_text: 'text',
support_messenger: 'select'
support_messenger: 'select',
zaarbox_price_key: 'select'
}
const SELECT_LABELS_BY_KEY: Record<string, Record<string, string>> = {
@@ -118,14 +99,22 @@ const SELECT_LABELS_BY_KEY: Record<string, Record<string, string>> = {
const SELECT_OPTIONS_BY_KEY: Record<string, string> = {
price_provider: 'montzar,zaarbox,main_product',
referral_id: 'required,',
payping_gateway_name: 'sepal,',
credit_rial_price_unit: 'gram,mesghal,unit',
store_status: 'باز,بسته',
unit: 'گرم,کیلوگرم',
support_messenger: 'bale,telegram,whatsapp,rubika,eitaa,soroush'
}
/** سوییچ‌هایی که مقدار required/خالی دارند (نه true/false) */
export const REFERRAL_REQUIRED_KEY = 'referral_id'
export function isReferralRequired(value: string | undefined): boolean {
return (value ?? '').trim().toLowerCase() === 'required'
}
export function referralRequiredToString(checked: boolean): string {
return checked ? 'required' : ''
}
export function resolveSettingFieldType(row: Pick<Const, 'key' | 'type'>): SettingFieldType {
const raw = (row.type || '').trim().toLowerCase()
if (['text', 'number', 'switch', 'select', 'longtext'].includes(raw)) {
@@ -169,9 +158,27 @@ export function switchToString(checked: boolean): string {
return checked ? 'true' : 'false'
}
export function formatValueForSave(value: string, type: SettingFieldType): string {
export function resolveSwitchChecked(key: string | undefined, value: string | undefined): boolean {
if (key === REFERRAL_REQUIRED_KEY) {
return isReferralRequired(value)
}
return isSwitchOn(value)
}
export function switchValueFromChecked(key: string | undefined, checked: boolean): string {
if (key === REFERRAL_REQUIRED_KEY) {
return referralRequiredToString(checked)
}
return switchToString(checked)
}
export function formatValueForSave(
value: string,
type: SettingFieldType,
key?: string
): string {
if (type === 'switch') {
return switchToString(isSwitchOn(value))
return switchValueFromChecked(key, isSwitchOn(value) || (key === REFERRAL_REQUIRED_KEY && isReferralRequired(value)))
}
if (type === 'number') {
return value === '' ? '0' : String(value).replace(/,/g, '')
@@ -179,6 +186,16 @@ export function formatValueForSave(value: string, type: SettingFieldType): strin
return value ?? ''
}
export function switchLabels(key: string | undefined): { on: string; off: string } {
if (key === REFERRAL_REQUIRED_KEY) {
return { on: 'اجباری', off: 'اختیاری' }
}
if (key === 'chart_use_local_data') {
return { on: 'داده خود سایت', off: 'Zaarbox' }
}
return { on: 'فعال', off: 'غیرفعال' }
}
export function helpText(row: Const): string {
const desc = (row.description || '').trim()
if (!desc) return ''