setting better performance
This commit is contained in:
Vendored
+2
@@ -16,6 +16,8 @@ declare module '@vue/runtime-core' {
|
|||||||
ACard: typeof import('ant-design-vue/es')['Card']
|
ACard: typeof import('ant-design-vue/es')['Card']
|
||||||
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
||||||
ACol: typeof import('ant-design-vue/es')['Col']
|
ACol: typeof import('ant-design-vue/es')['Col']
|
||||||
|
ACollapse: typeof import('ant-design-vue/es')['Collapse']
|
||||||
|
ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
|
||||||
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
|
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
|
||||||
ADivider: typeof import('ant-design-vue/es')['Divider']
|
ADivider: typeof import('ant-design-vue/es')['Divider']
|
||||||
ADrawer: typeof import('ant-design-vue/es')['Drawer']
|
ADrawer: typeof import('ant-design-vue/es')['Drawer']
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ export interface Product{
|
|||||||
percent_fee_buy: number,
|
percent_fee_buy: number,
|
||||||
cash_fee_buy: number,
|
cash_fee_buy: number,
|
||||||
percent_fee_sell: number,
|
percent_fee_sell: number,
|
||||||
cash_fee_sell: number
|
cash_fee_sell: number,
|
||||||
|
order_limit_enabled?: boolean,
|
||||||
|
min_order_cash?: number | null,
|
||||||
|
max_order_cash?: number | null,
|
||||||
group_type: string
|
group_type: string
|
||||||
unit: string
|
unit: string
|
||||||
active: boolean
|
active: boolean
|
||||||
|
|||||||
@@ -43,54 +43,56 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-spin :spinning="loading && !baseData.length">
|
<a-spin :spinning="loading && !baseData.length">
|
||||||
<template v-if="switchRows.length">
|
<a-collapse
|
||||||
<div class="settings-section-title">روشن / خاموش</div>
|
v-model:activeKey="activeKeys"
|
||||||
<a-row :gutter="[16, 16]" class="settings-grid">
|
class="settings-collapse"
|
||||||
<a-col
|
:bordered="false"
|
||||||
v-for="row in switchRows"
|
>
|
||||||
:key="row.id"
|
<a-collapse-panel
|
||||||
:xs="24"
|
v-for="group in SETTING_GROUPS"
|
||||||
:sm="12"
|
:key="group.id"
|
||||||
:lg="8"
|
:collapsible="groupedFiltered[group.id]?.length ? undefined : 'disabled'"
|
||||||
>
|
>
|
||||||
<SettingCard
|
<template #header>
|
||||||
:row="row"
|
<div class="settings-collapse-header">
|
||||||
:model-value="draftValue(row)"
|
<BaseIcon :icon="group.icon" size="18" class="settings-collapse-icon" />
|
||||||
:readonly="isReadonly(row)"
|
<span class="settings-collapse-title">{{ group.title }}</span>
|
||||||
:saving="isSaving(row.id)"
|
<a-badge
|
||||||
:saved-flash="isSavedFlash(row.id)"
|
v-if="groupedFiltered[group.id]"
|
||||||
@update:value="onValueChange(row, $event)"
|
:count="groupedFiltered[group.id].length"
|
||||||
@update:is-active="onActiveChange(row, $event)"
|
:number-style="{ backgroundColor: '#d4af37', color: '#fff', fontSize: '11px' }"
|
||||||
/>
|
/>
|
||||||
</a-col>
|
</div>
|
||||||
</a-row>
|
</template>
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-if="otherRows.length">
|
<a-row :gutter="[16, 16]" class="settings-grid">
|
||||||
<div class="settings-section-title settings-section-title--spaced">سایر تنظیمات</div>
|
<a-col
|
||||||
<a-row :gutter="[16, 16]" class="settings-grid">
|
v-for="row in groupedFiltered[group.id] || []"
|
||||||
<a-col
|
:key="row.id"
|
||||||
v-for="row in otherRows"
|
:xs="24"
|
||||||
:key="row.id"
|
:sm="12"
|
||||||
:xs="24"
|
:lg="rowColSpan(row)"
|
||||||
:sm="12"
|
>
|
||||||
:lg="rowColSpan(row)"
|
<SettingCard
|
||||||
>
|
:row="row"
|
||||||
<SettingCard
|
:model-value="draftValue(row)"
|
||||||
:row="row"
|
:readonly="isReadonly(row)"
|
||||||
:model-value="draftValue(row)"
|
:saving="isSaving(row.id)"
|
||||||
:readonly="isReadonly(row)"
|
:saved-flash="isSavedFlash(row.id)"
|
||||||
:saving="isSaving(row.id)"
|
@update:value="onValueChange(row, $event)"
|
||||||
:saved-flash="isSavedFlash(row.id)"
|
@update:is-active="onActiveChange(row, $event)"
|
||||||
@update:value="onValueChange(row, $event)"
|
/>
|
||||||
@update:is-active="onActiveChange(row, $event)"
|
</a-col>
|
||||||
/>
|
</a-row>
|
||||||
</a-col>
|
|
||||||
</a-row>
|
<div v-if="!groupedFiltered[group.id]?.length" class="settings-empty-group">
|
||||||
</template>
|
موردی در این گروه یافت نشد
|
||||||
|
</div>
|
||||||
|
</a-collapse-panel>
|
||||||
|
</a-collapse>
|
||||||
|
|
||||||
<a-empty
|
<a-empty
|
||||||
v-if="!loading && !switchRows.length && !otherRows.length"
|
v-if="!loading && Object.keys(groupedFiltered).length === 0"
|
||||||
description="تنظیمی یافت نشد"
|
description="تنظیمی یافت نشد"
|
||||||
class="mt-8"
|
class="mt-8"
|
||||||
/>
|
/>
|
||||||
@@ -161,6 +163,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
|
import { watch } from 'vue'
|
||||||
import BasicShowContent from '@/components/BasicShowContext.vue'
|
import BasicShowContent from '@/components/BasicShowContext.vue'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import { inputRules } from '@/utils/inputRules'
|
import { inputRules } from '@/utils/inputRules'
|
||||||
@@ -273,6 +276,98 @@ const otherRows = computed(() =>
|
|||||||
filteredRows.value.filter((row) => resolveSettingFieldType(row) !== 'switch')
|
filteredRows.value.filter((row) => resolveSettingFieldType(row) !== 'switch')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// گروهبندی تنظیمات (برای کالپس جذاب + سرچ هوشمند)
|
||||||
|
// =============================================================================
|
||||||
|
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_|app_|admin_|server_|bace_|key$|color_|logo_|title_pwa|title_fav|metal_type)/.test(k)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pricing',
|
||||||
|
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) ||
|
||||||
|
k.includes('charge_percentage')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'montzar',
|
||||||
|
title: 'سرویس همکار (montzar)',
|
||||||
|
icon: 'vuesax-linear:link',
|
||||||
|
match: (k) => k.startsWith('montzar_')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'features',
|
||||||
|
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|goldir_price|tabanGohar_price|main_product_price)/.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)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'misc',
|
||||||
|
title: 'سایر تنظیمات (متفرقه)',
|
||||||
|
icon: 'vuesax-linear:more-circle',
|
||||||
|
match: () => true // catch-all
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
function getGroupId(key: string): string {
|
||||||
|
for (const g of SETTING_GROUPS) {
|
||||||
|
if (g.match(key)) return g.id
|
||||||
|
}
|
||||||
|
return 'misc'
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupedFiltered = computed(() => {
|
||||||
|
const map: Record<string, Const[]> = {}
|
||||||
|
for (const g of SETTING_GROUPS) 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: string[] = []
|
||||||
|
const activeKeys = ref<string[]>(defaultOpenGroups)
|
||||||
|
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}, { flush: 'post' })
|
||||||
|
|
||||||
function rowColSpan(row: Const): number {
|
function rowColSpan(row: Const): number {
|
||||||
return resolveSettingFieldType(row) === 'longtext' ? 24 : 8
|
return resolveSettingFieldType(row) === 'longtext' ? 24 : 8
|
||||||
}
|
}
|
||||||
@@ -516,4 +611,66 @@ async function ok() {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: rgba(0, 0, 0, 0.45);
|
color: rgba(0, 0, 0, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ======================= Collapse Groups Attractive UI ======================= */
|
||||||
|
.settings-collapse {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-collapse :deep(.ant-collapse-item) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||||
|
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-collapse :deep(.ant-collapse-item:hover) {
|
||||||
|
box-shadow: 0 4px 12px rgba(212, 175, 55, 0.08);
|
||||||
|
border-color: #f4e9c8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-collapse :deep(.ant-collapse-header) {
|
||||||
|
padding: 14px 18px !important;
|
||||||
|
background: linear-gradient(90deg, #fafafa 0%, #fff 100%);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-collapse-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-collapse-icon {
|
||||||
|
color: #d4af37;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-collapse-title {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-empty-group {
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
text-align: center;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* وقتی سرچ فعال است، پنلهای باز جلوه بهتری داشته باشند */
|
||||||
|
.settings-collapse :deep(.ant-collapse-item-active) {
|
||||||
|
border-color: #f4e9c8;
|
||||||
|
box-shadow: 0 6px 16px rgba(212, 175, 55, 0.12);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -96,6 +96,20 @@
|
|||||||
<a-form-item label="زمان پایان فعال سازی" name="auto_close_at">
|
<a-form-item label="زمان پایان فعال سازی" name="auto_close_at">
|
||||||
<TimePicker v-model:value="form.auto_close_at" :locale="customLocale" placeholder="انتخاب زمان" :defaultValue="dayjs('00:00:00', 'HH:mm:ss')" />
|
<TimePicker v-model:value="form.auto_close_at" :locale="customLocale" placeholder="انتخاب زمان" :defaultValue="dayjs('00:00:00', 'HH:mm:ss')" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form-item label="محدودیت مبلغ سفارش" name="order_limit_enabled">
|
||||||
|
<a-switch v-model:checked="form.order_limit_enabled" @change="onOrderLimitChange" />
|
||||||
|
</a-form-item>
|
||||||
|
<template v-if="form.order_limit_enabled">
|
||||||
|
<div class="col-span-2 text-xs text-gray-500 -mt-2 mb-1">
|
||||||
|
حداقل یکی از «حداقل» یا «حداکثر» الزامی است؛ میتوانید فقط یکی را پر کنید.
|
||||||
|
</div>
|
||||||
|
<a-form-item label="حداقل مبلغ سفارش (ریال)" name="min_order_cash">
|
||||||
|
<BasicInput v-model:value="form.min_order_cash" addon-before="ریال" type="price" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="حداکثر مبلغ سفارش (ریال)" name="max_order_cash">
|
||||||
|
<BasicInput v-model:value="form.max_order_cash" addon-before="ریال" type="price" />
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</a-form>
|
</a-form>
|
||||||
</template>
|
</template>
|
||||||
@@ -124,7 +138,7 @@ import IndexImageUploader from '@/components/ProductUploader/IndexImageUploader.
|
|||||||
import { getConfigApp } from '@/utils/config'
|
import { getConfigApp } from '@/utils/config'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
form: any
|
form: any
|
||||||
formRef: any
|
formRef: any
|
||||||
methodOption: string
|
methodOption: string
|
||||||
@@ -142,6 +156,13 @@ defineProps<{
|
|||||||
editId?: number
|
editId?: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
function onOrderLimitChange(checked: boolean) {
|
||||||
|
if (!checked) {
|
||||||
|
props.form.min_order_cash = undefined
|
||||||
|
props.form.max_order_cash = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
(e: 'addPrice'): void
|
(e: 'addPrice'): void
|
||||||
(e: 'minusPrice'): void
|
(e: 'minusPrice'): void
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ import {
|
|||||||
resolveProductImageSrc,
|
resolveProductImageSrc,
|
||||||
unwrapApiRecord
|
unwrapApiRecord
|
||||||
} from '@/utils/file'
|
} from '@/utils/file'
|
||||||
|
import { normalizeProductOrderLimitForm, validateProductOrderLimitForm } from '@/utils/productOrderLimit'
|
||||||
|
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
|
|
||||||
@@ -241,13 +242,13 @@ async function edit(record: Product) {
|
|||||||
? dayjs(productData.auto_close_at, 'HH:mm:ss')
|
? dayjs(productData.auto_close_at, 'HH:mm:ss')
|
||||||
: dayjs('00:00:00', 'HH:mm:ss')
|
: dayjs('00:00:00', 'HH:mm:ss')
|
||||||
|
|
||||||
form.value = {
|
form.value = normalizeProductOrderLimitForm({
|
||||||
...productData,
|
...productData,
|
||||||
price_offset_percent: productData.price_offset_percent ?? 0,
|
price_offset_percent: productData.price_offset_percent ?? 0,
|
||||||
direct_price: productData.direct_price ?? 0,
|
direct_price: productData.direct_price ?? 0,
|
||||||
auto_open_at: start.isValid() ? start : dayjs('00:00:00', 'HH:mm:ss'),
|
auto_open_at: start.isValid() ? start : dayjs('00:00:00', 'HH:mm:ss'),
|
||||||
auto_close_at: close.isValid() ? close : dayjs('00:00:00', 'HH:mm:ss')
|
auto_close_at: close.isValid() ? close : dayjs('00:00:00', 'HH:mm:ss')
|
||||||
}
|
})
|
||||||
|
|
||||||
await setProductIndexImage(productData, record.id)
|
await setProductIndexImage(productData, record.id)
|
||||||
}
|
}
|
||||||
@@ -281,6 +282,9 @@ function resetData() {
|
|||||||
direct_price: 0,
|
direct_price: 0,
|
||||||
file_id: undefined,
|
file_id: undefined,
|
||||||
file: undefined,
|
file: undefined,
|
||||||
|
order_limit_enabled: false,
|
||||||
|
min_order_cash: undefined,
|
||||||
|
max_order_cash: undefined,
|
||||||
auto_open_at: dayjs('00:00:00', 'HH:mm:ss'),
|
auto_open_at: dayjs('00:00:00', 'HH:mm:ss'),
|
||||||
auto_close_at: dayjs('00:00:00', 'HH:mm:ss')
|
auto_close_at: dayjs('00:00:00', 'HH:mm:ss')
|
||||||
}
|
}
|
||||||
@@ -296,6 +300,12 @@ async function ok() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const payload = cloneDeep(form.value)
|
const payload = cloneDeep(form.value)
|
||||||
|
const limitError = validateProductOrderLimitForm(payload)
|
||||||
|
if (limitError) {
|
||||||
|
message.error(limitError)
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
if (methodOption.value === '1') {
|
if (methodOption.value === '1') {
|
||||||
delete payload.reading_method
|
delete payload.reading_method
|
||||||
payload.price_offset_percent = 0
|
payload.price_offset_percent = 0
|
||||||
@@ -322,6 +332,8 @@ async function ok() {
|
|||||||
delete payload.file
|
delete payload.file
|
||||||
}
|
}
|
||||||
|
|
||||||
|
normalizeProductOrderLimitForm(payload)
|
||||||
|
|
||||||
if (editId) {
|
if (editId) {
|
||||||
await updateCustomer(editId, payload)
|
await updateCustomer(editId, payload)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
export function validateProductOrderLimitForm(form: {
|
||||||
|
order_limit_enabled?: boolean
|
||||||
|
min_order_cash?: number | string | null
|
||||||
|
max_order_cash?: number | string | null
|
||||||
|
}): string | null {
|
||||||
|
if (!form.order_limit_enabled) return null
|
||||||
|
|
||||||
|
const min = form.min_order_cash
|
||||||
|
const max = form.max_order_cash
|
||||||
|
const hasMin = min != null && min !== ''
|
||||||
|
const hasMax = max != null && max !== ''
|
||||||
|
|
||||||
|
if (!hasMin && !hasMax) {
|
||||||
|
return 'با فعال بودن محدودیت مبلغ سفارش، حداقل یکی از حداقل یا حداکثر مبلغ باید وارد شود'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasMin && hasMax && Number(min) > Number(max)) {
|
||||||
|
return 'حداقل مبلغ نمیتواند بیشتر از حداکثر باشد'
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeProductOrderLimitForm<T extends Record<string, any>>(data: T): T {
|
||||||
|
data.order_limit_enabled = !!data.order_limit_enabled
|
||||||
|
if (!data.order_limit_enabled) {
|
||||||
|
data.min_order_cash = undefined
|
||||||
|
data.max_order_cash = undefined
|
||||||
|
} else {
|
||||||
|
data.min_order_cash = data.min_order_cash != null && data.min_order_cash !== '' ? data.min_order_cash : undefined
|
||||||
|
data.max_order_cash = data.max_order_cash != null && data.max_order_cash !== '' ? data.max_order_cash : undefined
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user