fix ui
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<span
|
||||
class="dashboard-label-with-help inline-flex max-w-full items-baseline gap-1.5"
|
||||
class="dashboard-label-with-help inline-flex max-w-full items-baseline justify-start gap-1.5 text-right"
|
||||
:class="[textClass || sizeTextClass, rootClass]"
|
||||
>
|
||||
<span class="leading-normal" :class="labelClass">{{ label }}</span>
|
||||
|
||||
@@ -115,7 +115,7 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
|
||||
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|main_metal|unit$|open_register|auto_active_customer|store_status|key$|is_store|group_sms|has_page_earn_money|sms_panel)/.test(
|
||||
/^(name_fa|name_en|app_url|admin_url|apk_download_link|banner_enabled|permision__enabled|bace_url|server_url|logo_|title_pwa|title_fav|metal_type|main_metal|unit$|open_register|auto_active_customer|store_status|key$|is_store|group_sms|has_page_earn_money|sms_panel)/.test(
|
||||
k
|
||||
)
|
||||
},
|
||||
|
||||
@@ -53,7 +53,16 @@ export type SupportMessengerEntryDraft = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const SUPPORT_MESSENGER_TYPES = ['bale', 'telegram', 'whatsapp', 'rubika', 'eitaa', 'soroush'] as const
|
||||
const SUPPORT_MESSENGER_TYPES = [
|
||||
'bale',
|
||||
'telegram',
|
||||
'whatsapp',
|
||||
'rubika',
|
||||
'eitaa',
|
||||
'soroush',
|
||||
'call',
|
||||
'sms',
|
||||
] as const
|
||||
|
||||
export function isSupportMessengerType(value: string | undefined | null): boolean {
|
||||
const type = (value ?? '').trim().toLowerCase()
|
||||
@@ -158,6 +167,9 @@ export function supportMessengerIdPlaceholder(messenger?: string): string {
|
||||
case 'eitaa':
|
||||
case 'soroush':
|
||||
return 'یوزرنیم یا آیدی'
|
||||
case 'call':
|
||||
case 'sms':
|
||||
return 'شماره موبایل، مثلاً 09123456789'
|
||||
default:
|
||||
return 'شناسه / یوزرنیم / شماره'
|
||||
}
|
||||
@@ -256,6 +268,7 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
|
||||
external_order_api_enabled: 'switch',
|
||||
is_store: 'switch',
|
||||
banner_enabled: 'switch',
|
||||
permision__enabled: 'switch',
|
||||
apk_download_link: 'text',
|
||||
node_client_id: 'text',
|
||||
node_shared_secret: 'text',
|
||||
@@ -307,7 +320,9 @@ const SELECT_LABELS_BY_KEY: Record<string, Record<string, string>> = {
|
||||
whatsapp: 'واتساپ',
|
||||
rubika: 'روبیکا',
|
||||
eitaa: 'ایتا',
|
||||
soroush: 'سروش'
|
||||
soroush: 'سروش',
|
||||
call: 'تماس تلفنی',
|
||||
sms: 'پیامک',
|
||||
},
|
||||
price_channel_messengers: {
|
||||
bale: 'بله',
|
||||
@@ -321,7 +336,7 @@ const SELECT_OPTIONS_BY_KEY: Record<string, string> = {
|
||||
price_provider: 'montzar,zaarbox,main_product',
|
||||
payping_gateway_name: 'sepal,',
|
||||
unit: 'گرم,کیلوگرم',
|
||||
support_messenger: 'bale,telegram,whatsapp,rubika,eitaa,soroush',
|
||||
support_messenger: 'bale,telegram,whatsapp,rubika,eitaa,soroush,call,sms',
|
||||
price_channel_messengers: 'bale,telegram,rubika'
|
||||
}
|
||||
|
||||
@@ -484,6 +499,8 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
'نام فارسی برند که در پیامکها، عنوان صفحات و بخشهای عمومی اپ به مشتری نشان داده میشود.',
|
||||
banner_enabled:
|
||||
'روشن: منوی «بنرها» در تنظیمات فعال میشود و میتوانید اسلایدر بالای صفحهٔ اصلی اپ را مدیریت کنید. خاموش: بنرها در اپ نشان داده نمیشوند.',
|
||||
permision__enabled:
|
||||
'روشن: آیتم «مجوز ها» در منوی تنظیمات اپ خانگی (تم جدید) نمایش داده میشود. خاموش: مخفی است.',
|
||||
open_register:
|
||||
'روشن: کاربران جدید میتوانند ثبتنام کنند. خاموش: فقط ورود کاربران قبلی مجاز است.',
|
||||
sms_panel:
|
||||
@@ -571,7 +588,7 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
node_shared_secret:
|
||||
'کلید مشترک امضای درخواستها به سرویس سفارش خارجی. محرمانه نگه دارید.',
|
||||
support_messengers:
|
||||
'لیست پیامرسانهای پشتیبانی. اگر چند مورد باشد، با کلیک دکمه شناور منوی انتخاب باز میشود؛ اگر یک مورد باشد مستقیم باز میشود. تماس تلفنی جداگانه از «شماره تماس پشتیبانی» است.',
|
||||
'فقط مواردی که در این لیست ثبت شوند در منوی شناور پشتیبانی اپ مشتری نمایش داده میشوند. برای تماس تلفنی یا پیامک، نوع «تماس تلفنی» / «پیامک» را با شماره اضافه کنید.',
|
||||
tracking_systems:
|
||||
'لیست JSON سامانههای ردیابی/آنالیتیکس در اپ مشتری. هر مورد: type (gtag، gtm، clarity، custom)، id (کد اندازهگیری یا شناسه پروژه)، enabled (روشن/خاموش). فقط از صفحه دیپلوی قابل ویرایش است.',
|
||||
tracking_systems_enabled:
|
||||
@@ -583,7 +600,7 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
support_messenger_text:
|
||||
'متن راهنمای واریز در مودال شارژ حساب. متن را بنویسید و متغیرها (نام صاحب حساب، شماره کارت، شبا) را با کلیک اضافه کنید؛ پیشنمایش نتیجه را با مقادیر نمونه نشان میدهد. خالی = متن پیشفرض اپ.',
|
||||
deposit_support_message_text:
|
||||
'متن پیام هنگام کلیک «پیام به پشتیبانی» در مودال واریز (SMS و پیامرسان). متن را بنویسید و متغیر «نام مشتری» را با کلیک اضافه کنید؛ پیشنمایش نتیجه را نشان میدهد. خالی = متن پیشفرض اپ.',
|
||||
'متن پیشنویس فقط وقتی کاربر از مودال راهنمای واریز روی «پیام به پشتیبانی» یا SMS میزند. منوی شناور پشتیبانی این متن را نمیگذارد. خالی = متن پیشفرض اپ.',
|
||||
deposit_account_holder:
|
||||
'نام صاحب حساب واریز — در متن راهنمای واریز با ٪نام یا %نام جایگزین میشود. در پیام پشتیبانی فقط وقتی نام مشتری موجود نباشد استفاده میشود.',
|
||||
deposit_card_number:
|
||||
@@ -591,7 +608,7 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
deposit_sheba:
|
||||
'شماره شبا واریز — در متنها با ٪شبا یا %شبا جایگزین میشود.',
|
||||
deposit_sms_support_number:
|
||||
'شماره تماس پشتیبانی؛ با کلیک روی «تماس با پشتیبانی» در اپ، تماس تلفنی برقرار میشود. در صفحه واریز، درباره ما و قوانین هم نمایش داده میشود.',
|
||||
'شماره تماس پشتیبانی برای راهنمای واریز و دکمه پیامک داخل مودال شارژ. برای نمایش در منوی شناور، باید «تماس تلفنی» یا «پیامک» را جداگانه در لیست پیامرسانهای پشتیبانی اضافه کنید.',
|
||||
charge_notif:
|
||||
'روشن: پس از شارژ کیف توسط ادمین، پیامک به مشتری ارسال میشود. خاموش: پیامکی پس از شارژ ارسال نمیشود.',
|
||||
group_sms:
|
||||
@@ -728,6 +745,7 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
|
||||
support_messengers: 'پیامرسانهای پشتیبانی',
|
||||
tracking_systems: 'سامانههای ردیابی',
|
||||
tracking_systems_enabled: 'نمایش سامانههای ردیابی',
|
||||
permision__enabled: 'نمایش منوی مجوزها',
|
||||
support_messenger: 'پیامرسان پشتیبانی',
|
||||
support_messenger_id: 'شناسه پشتیبانی',
|
||||
support_messenger_text: 'متن راهنمای واریز',
|
||||
@@ -773,6 +791,7 @@ export const SETTING_LOCKED_KEYS = new Set([
|
||||
'payping_fin_customer_id', 'payping_fin_sync_enabled', 'payping_fin_token', 'payping_fin_url',
|
||||
'payping_gateway_items', 'payping_gateway_name',
|
||||
'payping_round_toman', 'payping_start_url', 'payping_strict_failed_withdraw', 'payping_verify_url',
|
||||
'permision__enabled',
|
||||
'price_extreme_date', 'price_provider', 'profit_gold_api_enabled',
|
||||
'send_to_tahesab', 'send_to_pargar', 'accounting_provider', 'service_charge_percentage',
|
||||
'sse_path',
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
<template>
|
||||
<a-form
|
||||
<a-form
|
||||
:model="form"
|
||||
ref="innerFormRef"
|
||||
autocomplete="off"
|
||||
scroll-to-first-error
|
||||
layout="vertical"
|
||||
label-align="right"
|
||||
class="product-editor-form"
|
||||
>
|
||||
<Collapse class="grid grid-cols-1 gap-2" :activeKey="activeKey" @update:activeKey="$emit('update:activeKey', $event)">
|
||||
<BaseCollapse title="قیمتگذاری" active-key="0" :actives="activeKey">
|
||||
<template #content>
|
||||
<a-tabs
|
||||
class="product-editor-tabs"
|
||||
:activeKey="activeKey"
|
||||
@update:activeKey="$emit('update:activeKey', $event)"
|
||||
>
|
||||
<a-tab-pane key="0" tab="قیمتگذاری" force-render>
|
||||
<div
|
||||
v-if="isMeltedCategory"
|
||||
v-if="usesMeltedPricing"
|
||||
class="product-editor-panel flex flex-col gap-3"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<DashboardLabelWithHelp label="مرجع قیمت" :help="meltedPriceSourceHelp" size="sm" />
|
||||
<div class="product-spec-switch-row">
|
||||
<div class="product-spec-label">
|
||||
<DashboardLabelWithHelp label="مرجع قیمت" :help="meltedPriceSourceHelp" size="sm" />
|
||||
</div>
|
||||
<a-radio-group :value="meltedPriceOption" name="meltedPriceGroup" @update:value="$emit('update:meltedPriceOption', $event)">
|
||||
<a-radio value="app">قیمت اپ</a-radio>
|
||||
<a-radio value="fixed">ثابت</a-radio>
|
||||
@@ -76,9 +83,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isMeltedCategory" class="product-editor-panel flex flex-col gap-4">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<DashboardLabelWithHelp label="دریافت قیمت از" :help="priceSourceHelp" size="sm" />
|
||||
<div v-if="!usesMeltedPricing" class="product-editor-panel flex flex-col gap-4">
|
||||
<div class="product-spec-switch-row">
|
||||
<div class="product-spec-label">
|
||||
<DashboardLabelWithHelp label="دریافت قیمت از" :help="priceSourceHelp" size="sm" />
|
||||
</div>
|
||||
<a-radio-group :value="methodOption" name="radioGroup" @update:value="$emit('update:methodOption', $event)">
|
||||
<a-radio value="1">دستی</a-radio>
|
||||
<a-radio value="2">اتوماتیک</a-radio>
|
||||
@@ -178,61 +187,94 @@
|
||||
</AButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BaseCollapse>
|
||||
</a-tab-pane>
|
||||
|
||||
<BaseCollapse title="مشخصات محصول" active-key="1" :actives="activeKey">
|
||||
<template #content>
|
||||
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<a-form-item name="title" :rules="inputRules()">
|
||||
<a-tab-pane key="1" tab="مشخصات محصول" force-render>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<a-form-item name="title" :rules="inputRules()" class="!mb-0">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="عنوان" :help="titleHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.title" />
|
||||
</a-form-item>
|
||||
<a-form-item name="group_type" :rules="inputRules()">
|
||||
<a-form-item name="group_type" :rules="inputRules()" class="!mb-0">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="دستهبندی" :help="categoryHelp" size="sm" />
|
||||
</template>
|
||||
<BasicSelect v-model:value="form.group_type" :items="categoryItems" valueName="value" keyName="key" />
|
||||
</a-form-item>
|
||||
<a-form-item name="metal_id" :rules="inputRules()">
|
||||
<a-form-item name="metal_id" :rules="inputRules()" class="!mb-0">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="جنس فلز" :help="metalHelp" size="sm" />
|
||||
</template>
|
||||
<BasicSelect v-model:value="form.metal_id" :items="metalItems" valueName="value" keyName="key" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<template v-if="!isMeltedCategory">
|
||||
<div class="md:col-span-3 product-editor-panel">
|
||||
<div class="flex flex-wrap items-center gap-3 mb-3">
|
||||
<DashboardLabelWithHelp label="وزن محصول" :help="weightHelp" size="sm" />
|
||||
<a-radio-group :value="weightOption" name="weightGroup" @update:value="$emit('update:weightOption', $event)">
|
||||
<a-radio value="1">دارد</a-radio>
|
||||
<a-radio value="2">ندارد</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div v-if="weightOption === '1'" class="grid gap-4 md:grid-cols-3">
|
||||
<a-form-item name="amount" :rules="inputRulesNumber('وزن')" class="!mb-0">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp :label="weightFieldLabel" :help="weightAmountHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.amount" type="number" />
|
||||
</a-form-item>
|
||||
<div
|
||||
v-if="form.amount && form.amount < 1 && 0 < form.amount && tradeWeightUnit === 'گرم'"
|
||||
class="flex items-end pb-1 text-sm text-gray-500"
|
||||
>
|
||||
معادل {{ form.amount * 1000 }} سوت
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isMeltedGroup" class="product-editor-panel flex flex-col gap-4">
|
||||
<div class="product-spec-switch-row">
|
||||
<div class="product-spec-label">
|
||||
<DashboardLabelWithHelp label="نوع معامله" :help="tradeByQuantityHelp" size="sm" />
|
||||
</div>
|
||||
</template>
|
||||
<a-radio-group
|
||||
v-if="!editId"
|
||||
v-model:value="tradeTypeOption"
|
||||
name="tradeTypeGroup"
|
||||
>
|
||||
<a-radio value="count">معامله تعدادی</a-radio>
|
||||
<a-radio value="quantity">معامله مقداری</a-radio>
|
||||
</a-radio-group>
|
||||
<span
|
||||
v-else
|
||||
class="inline-flex items-center rounded-full px-3 py-1 text-sm font-600"
|
||||
:class="form.trade_by_quantity ? 'bg-emerald-100 text-emerald-800' : 'bg-sky-100 text-sky-800'"
|
||||
>
|
||||
{{ resolveProductTradeTypeLabel(form.trade_by_quantity) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isQuantityTrade"
|
||||
class="rounded-lg border border-solid border-amber-200 bg-amber-50 px-3 py-2 text-xs leading-6 text-amber-800"
|
||||
>
|
||||
معامله مقداری: مشتری بر اساس مقدار (گرم/کیلوگرم) خرید میکند. قیمتگذاری این محصول در تب «قیمتگذاری» بهصورت مقداری تنظیم میشود.
|
||||
</div>
|
||||
|
||||
<div class="product-spec-switch-row">
|
||||
<div class="product-spec-label">
|
||||
<DashboardLabelWithHelp label="وزن محصول" :help="weightHelp" size="sm" />
|
||||
</div>
|
||||
<a-radio-group :value="weightOption" name="weightGroup" @update:value="$emit('update:weightOption', $event)">
|
||||
<a-radio value="1">دارد</a-radio>
|
||||
<a-radio value="2">ندارد</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
<div v-if="weightOption === '1'" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<a-form-item name="amount" :rules="inputRulesNumber('وزن')" class="!mb-0">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp :label="weightFieldLabel" :help="weightAmountHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.amount" type="number" />
|
||||
</a-form-item>
|
||||
<div
|
||||
v-if="form.amount && form.amount < 1 && 0 < form.amount && tradeWeightUnit === 'گرم'"
|
||||
class="flex items-end pb-1 text-sm text-gray-500"
|
||||
>
|
||||
معادل {{ form.amount * 1000 }} سوت
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showUnitField || showDirectPriceField || (isStore && !isMeltedGroup)"
|
||||
class="grid gap-4 md:grid-cols-2 xl:grid-cols-3"
|
||||
>
|
||||
<a-form-item
|
||||
v-if="isMeltedCategory || weightOption === '1'"
|
||||
v-if="showUnitField"
|
||||
name="unit"
|
||||
:rules="showUnitField ? inputRules() : []"
|
||||
:rules="inputRules()"
|
||||
class="!mb-0"
|
||||
>
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="واحد" :help="unitHelp" size="sm" />
|
||||
@@ -243,6 +285,7 @@
|
||||
v-if="showDirectPriceField"
|
||||
name="direct_price"
|
||||
:rules="inputRules()"
|
||||
class="!mb-0"
|
||||
>
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="نوع قیمت" :help="directPriceHelp" size="sm" />
|
||||
@@ -250,109 +293,93 @@
|
||||
<BasicSelect v-model:value="form.direct_price" :items="priceItems" valueName="value" keyName="key" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="isStore && !isMeltedCategory"
|
||||
v-if="isStore && !isMeltedGroup"
|
||||
name="inventory"
|
||||
:rules="inputRules()"
|
||||
class="!mb-0"
|
||||
>
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="تعداد موجودی" :help="inventoryHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.inventory" type="number" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
v-if="!isMeltedCategory && !editId"
|
||||
name="trade_by_quantity"
|
||||
>
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="نوع معامله" :help="tradeByQuantityHelp" size="sm" />
|
||||
</template>
|
||||
<a-radio-group v-model:value="tradeTypeOption" name="tradeTypeGroup">
|
||||
<a-radio value="count">معامله تعدادی</a-radio>
|
||||
<a-radio value="quantity">معامله مقداری</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item v-else-if="!isMeltedCategory && editId">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="نوع معامله" :help="tradeByQuantityHelp" size="sm" />
|
||||
</template>
|
||||
<div class="pt-1">
|
||||
<span
|
||||
class="inline-flex items-center rounded-full px-3 py-1 text-sm font-600"
|
||||
:class="form.trade_by_quantity ? 'bg-emerald-100 text-emerald-800' : 'bg-sky-100 text-sky-800'"
|
||||
>
|
||||
{{ resolveProductTradeTypeLabel(form.trade_by_quantity) }}
|
||||
</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</template>
|
||||
</BaseCollapse>
|
||||
|
||||
<BaseCollapse title="کارمزد و محدودیتها" active-key="2" :actives="activeKey">
|
||||
<template #content>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<a-form-item name="cash_fee_buy" :rules="requiredPriceRules('هزینه نقدی موقع خرید')">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="هزینه خرید" :help="cashFeeBuyHelp" size="sm" />
|
||||
</template>
|
||||
<div class="product-cash-fee-field">
|
||||
<a-radio-group
|
||||
v-model:value="form.cash_fee_buy_is_percent"
|
||||
button-style="solid"
|
||||
class="product-cash-fee-mode"
|
||||
>
|
||||
<a-radio-button :value="false">نقدی (ریال)</a-radio-button>
|
||||
<a-radio-button :value="true">درصدی (%)</a-radio-button>
|
||||
</a-radio-group>
|
||||
<BasicInput
|
||||
v-model:value="form.cash_fee_buy"
|
||||
:addon-before="form.cash_fee_buy_is_percent ? '%' : 'ریال'"
|
||||
:type="form.cash_fee_buy_is_percent ? 'number' : 'price'"
|
||||
:step="form.cash_fee_buy_is_percent ? 'any' : undefined"
|
||||
size="large"
|
||||
class="w-full product-cash-fee-input"
|
||||
:placeHolder="form.cash_fee_buy_is_percent ? 'مثلاً 0.5 یا 5' : 'مثلاً 100000'"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item name="cash_fee_sell" :rules="requiredPriceRules('هزینه نقدی موقع فروش')">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="هزینه فروش" :help="cashFeeSellHelp" size="sm" />
|
||||
</template>
|
||||
<div class="product-cash-fee-field">
|
||||
<a-radio-group
|
||||
v-model:value="form.cash_fee_sell_is_percent"
|
||||
button-style="solid"
|
||||
class="product-cash-fee-mode"
|
||||
>
|
||||
<a-radio-button :value="false">نقدی (ریال)</a-radio-button>
|
||||
<a-radio-button :value="true">درصدی (%)</a-radio-button>
|
||||
</a-radio-group>
|
||||
<BasicInput
|
||||
v-model:value="form.cash_fee_sell"
|
||||
:addon-before="form.cash_fee_sell_is_percent ? '%' : 'ریال'"
|
||||
:type="form.cash_fee_sell_is_percent ? 'number' : 'price'"
|
||||
:step="form.cash_fee_sell_is_percent ? 'any' : undefined"
|
||||
size="large"
|
||||
class="w-full product-cash-fee-input"
|
||||
:placeHolder="form.cash_fee_sell_is_percent ? 'مثلاً 0.5 یا 10' : 'مثلاً 200000'"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item name="show_in_assets">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="نمایش در داراییها" :help="showInAssetsHelp" size="sm" />
|
||||
</template>
|
||||
<div class="product-editor-panel product-spec-switch-row">
|
||||
<div class="product-spec-label">
|
||||
<DashboardLabelWithHelp label="نمایش در داراییها" :help="showInAssetsHelp" size="sm" />
|
||||
</div>
|
||||
<a-form-item name="show_in_assets" class="!mb-0">
|
||||
<a-switch v-model:checked="form.show_in_assets" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-form-item name="override_trading_hours">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="ساعت کاری اختصاصی" :help="overrideTradingHoursHelp" size="sm" />
|
||||
</template>
|
||||
<a-tab-pane key="2" tab="اختلاف خرید و فروش" force-render>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<a-form-item name="cash_fee_buy" :rules="requiredPriceRules('هزینه نقدی موقع خرید')">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="هزینه خرید" :help="cashFeeBuyHelp" size="sm" />
|
||||
</template>
|
||||
<div class="product-cash-fee-field">
|
||||
<a-radio-group
|
||||
v-model:value="form.cash_fee_buy_is_percent"
|
||||
button-style="solid"
|
||||
class="product-cash-fee-mode"
|
||||
>
|
||||
<a-radio-button :value="false">نقدی (ریال)</a-radio-button>
|
||||
<a-radio-button :value="true">درصدی (%)</a-radio-button>
|
||||
</a-radio-group>
|
||||
<BasicInput
|
||||
v-model:value="form.cash_fee_buy"
|
||||
:addon-before="form.cash_fee_buy_is_percent ? '%' : 'ریال'"
|
||||
:type="form.cash_fee_buy_is_percent ? 'number' : 'price'"
|
||||
:step="form.cash_fee_buy_is_percent ? 'any' : undefined"
|
||||
size="large"
|
||||
class="w-full product-cash-fee-input"
|
||||
:placeHolder="form.cash_fee_buy_is_percent ? 'مثلاً 0.5 یا 5' : 'مثلاً 100000'"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item name="cash_fee_sell" :rules="requiredPriceRules('هزینه نقدی موقع فروش')">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="هزینه فروش" :help="cashFeeSellHelp" size="sm" />
|
||||
</template>
|
||||
<div class="product-cash-fee-field">
|
||||
<a-radio-group
|
||||
v-model:value="form.cash_fee_sell_is_percent"
|
||||
button-style="solid"
|
||||
class="product-cash-fee-mode"
|
||||
>
|
||||
<a-radio-button :value="false">نقدی (ریال)</a-radio-button>
|
||||
<a-radio-button :value="true">درصدی (%)</a-radio-button>
|
||||
</a-radio-group>
|
||||
<BasicInput
|
||||
v-model:value="form.cash_fee_sell"
|
||||
:addon-before="form.cash_fee_sell_is_percent ? '%' : 'ریال'"
|
||||
:type="form.cash_fee_sell_is_percent ? 'number' : 'price'"
|
||||
:step="form.cash_fee_sell_is_percent ? 'any' : undefined"
|
||||
size="large"
|
||||
class="w-full product-cash-fee-input"
|
||||
:placeHolder="form.cash_fee_sell_is_percent ? 'مثلاً 0.5 یا 10' : 'مثلاً 200000'"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="3" tab="ساعت کاری" force-render>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="product-editor-panel product-spec-switch-row">
|
||||
<div class="product-spec-label">
|
||||
<DashboardLabelWithHelp label="ساعت کاری اختصاصی" :help="overrideTradingHoursHelp" size="sm" />
|
||||
</div>
|
||||
<a-form-item name="override_trading_hours" class="!mb-0">
|
||||
<a-switch v-model:checked="form.override_trading_hours" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<a-form-item name="auto_open_at">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="زمان شروع" :help="autoOpenHelp" size="sm" />
|
||||
@@ -379,36 +406,49 @@
|
||||
class="w-full"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-form-item name="order_limit_enabled">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="محدودیت مبلغ سفارش" :help="orderLimitHelp" size="sm" />
|
||||
</template>
|
||||
<a-tab-pane key="4" tab="محدودیت سفارش" force-render>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="product-editor-panel product-spec-switch-row">
|
||||
<div class="product-spec-label">
|
||||
<DashboardLabelWithHelp label="محدودیت مبلغ سفارش" :help="orderLimitHelp" size="sm" />
|
||||
</div>
|
||||
<a-form-item name="order_limit_enabled" class="!mb-0">
|
||||
<a-switch v-model:checked="form.order_limit_enabled" @change="onOrderLimitChange" />
|
||||
</a-form-item>
|
||||
|
||||
<template v-if="form.order_limit_enabled">
|
||||
<a-form-item name="min_order_cash">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="حداقل مبلغ سفارش" :help="minOrderHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.min_order_cash" addon-before="ریال" type="price" />
|
||||
</a-form-item>
|
||||
<a-form-item name="max_order_cash">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="حداکثر مبلغ سفارش" :help="maxOrderHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.max_order_cash" addon-before="ریال" type="price" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<a-form-item name="has_physical_delivery">
|
||||
</div>
|
||||
<div v-if="form.order_limit_enabled" class="grid gap-4 md:grid-cols-2">
|
||||
<a-form-item name="min_order_cash">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="تحویل فیزیکی" :help="physicalDeliveryHelp" size="sm" />
|
||||
<DashboardLabelWithHelp label="حداقل مبلغ سفارش" :help="minOrderHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.min_order_cash" addon-before="ریال" type="price" />
|
||||
</a-form-item>
|
||||
<a-form-item name="max_order_cash">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="حداکثر مبلغ سفارش" :help="maxOrderHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.max_order_cash" addon-before="ریال" type="price" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="5" tab="تحویل فیزیکی" force-render>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="product-editor-panel product-spec-switch-row">
|
||||
<div class="product-spec-label">
|
||||
<DashboardLabelWithHelp label="تحویل فیزیکی" :help="physicalDeliveryHelp" size="sm" />
|
||||
</div>
|
||||
<a-form-item name="has_physical_delivery" class="!mb-0">
|
||||
<a-switch v-model:checked="form.has_physical_delivery" @change="onPhysicalDeliveryChange" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.has_physical_delivery" name="min_physical_delivery_quantity">
|
||||
</div>
|
||||
<div v-if="form.has_physical_delivery" class="grid gap-4 md:grid-cols-2">
|
||||
<a-form-item name="min_physical_delivery_quantity">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp :label="minPhysicalDeliveryLabel" :help="minPhysicalDeliveryHelp" size="sm" />
|
||||
</template>
|
||||
@@ -419,11 +459,10 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</template>
|
||||
</BaseCollapse>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<BaseCollapse title="تصاویر محصول" active-key="3" :actives="activeKey">
|
||||
<template #content>
|
||||
<a-tab-pane key="6" tab="تصاویر محصول" force-render>
|
||||
<IndexImageUploader
|
||||
:key="editId ?? 'new-product'"
|
||||
:file-list="productIndexImage"
|
||||
@@ -431,25 +470,23 @@
|
||||
@ok="(id, res) => $emit('uploadedIndexFile', id, res)"
|
||||
@delete="$emit('deleteIndexFile')"
|
||||
/>
|
||||
</template>
|
||||
</BaseCollapse>
|
||||
</Collapse>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import { Collapse, TimePicker } from 'ant-design-vue'
|
||||
import { TimePicker } from 'ant-design-vue'
|
||||
import { inputRules, inputRulesNumber, requiredPriceRules } from '@/utils/inputRules'
|
||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||
import BaseCollapse from '@/components/BaseCollapse.vue'
|
||||
import IndexImageUploader from '@/components/ProductUploader/IndexImageUploader.vue'
|
||||
import { getConfiguration } from '@/api/configuration'
|
||||
import { isSwitchOn } from '@/pages/menuPages/configuration/settingTypes'
|
||||
import type { last } from '@/model/price'
|
||||
import type { MethodePrices } from '@/model/product'
|
||||
import { usePrice } from '@core/utils/usePrice'
|
||||
import { resolveProductTradeTypeLabel, usesMeltedPriceEditor } from '@/utils/productCategories'
|
||||
import { isMeltedTradeGroup, resolveProductTradeTypeLabel, usesMeltedPriceEditor } from '@/utils/productCategories'
|
||||
import { computeProductEditorPricePreview } from '@/utils/productEditorPrice'
|
||||
import { resolveTradeWeightUnit, weightFieldLabelForUnit } from '@/utils/weightUnit'
|
||||
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
|
||||
@@ -590,7 +627,7 @@ const props = defineProps<{
|
||||
methodOption: string
|
||||
weightOption: string
|
||||
meltedPriceOption: string
|
||||
activeKey: string[]
|
||||
activeKey: string
|
||||
pricesList: Array<{ id: string; name: string }>
|
||||
pricesData: MethodePrices[]
|
||||
appLastPrice: (last & { price?: number }) | null
|
||||
@@ -607,18 +644,24 @@ const props = defineProps<{
|
||||
const isStore = ref(Boolean(props.isStore))
|
||||
|
||||
const tradeWeightUnit = ref('گرم')
|
||||
const isMeltedCategory = computed(() => usesMeltedPriceEditor(props.form))
|
||||
/** دسته آبشده/کاتد — ساختار فرم ثابت است */
|
||||
const isMeltedGroup = computed(() => isMeltedTradeGroup(props.form.group_type))
|
||||
/** قیمتگذاری مقداری: آبشده/کاتد یا معامله مقداری */
|
||||
const usesMeltedPricing = computed(() => usesMeltedPriceEditor(props.form))
|
||||
const isQuantityTrade = computed(
|
||||
() => props.form.trade_by_quantity === true || Number(props.form.trade_by_quantity) === 1
|
||||
)
|
||||
|
||||
const tradeTypeOption = computed({
|
||||
get: () => (props.form.trade_by_quantity ? 'quantity' : 'count'),
|
||||
get: () => (isQuantityTrade.value ? 'quantity' : 'count'),
|
||||
set: (value: 'quantity' | 'count') => {
|
||||
props.form.trade_by_quantity = value === 'quantity'
|
||||
},
|
||||
})
|
||||
|
||||
const showDirectPriceField = computed(() => !isMeltedCategory.value && props.methodOption === '1')
|
||||
const showDirectPriceField = computed(() => !usesMeltedPricing.value && props.methodOption === '1')
|
||||
|
||||
const showUnitField = computed(() => isMeltedCategory.value || props.weightOption === '1')
|
||||
const showUnitField = computed(() => usesMeltedPricing.value || props.weightOption === '1')
|
||||
|
||||
const livePricePreview = computed(() =>
|
||||
computeProductEditorPricePreview({
|
||||
@@ -633,7 +676,7 @@ const livePricePreview = computed(() =>
|
||||
watch(
|
||||
() => props.methodOption,
|
||||
(option) => {
|
||||
if (isMeltedCategory.value) {
|
||||
if (usesMeltedPricing.value) {
|
||||
return
|
||||
}
|
||||
if (option === '3') {
|
||||
@@ -697,7 +740,7 @@ const physicalDeliveryUnit = computed(() => {
|
||||
return productUnit
|
||||
}
|
||||
|
||||
if (isMeltedCategory.value) {
|
||||
if (usesMeltedPricing.value) {
|
||||
return tradeWeightUnit.value
|
||||
}
|
||||
|
||||
@@ -727,7 +770,7 @@ defineEmits<{
|
||||
(e: 'update:methodOption', value: string): void
|
||||
(e: 'update:weightOption', value: string): void
|
||||
(e: 'update:meltedPriceOption', value: string): void
|
||||
(e: 'update:activeKey', value: string[]): void
|
||||
(e: 'update:activeKey', value: string): void
|
||||
(e: 'update:productIndexImage', value: any[]): void
|
||||
(e: 'uploadedIndexFile', id: number, response?: Record<string, any>): void
|
||||
(e: 'deleteIndexFile'): void
|
||||
@@ -749,11 +792,25 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.product-editor-tabs :deep(.ant-tabs-nav) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.product-editor-tabs :deep(.ant-tabs-tab) {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.product-editor-panel {
|
||||
border: 1px solid var(--ant-color-border-secondary, rgba(0, 0, 0, 0.06));
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.product-spec-switch-row {
|
||||
@apply flex flex-col items-stretch gap-2;
|
||||
}
|
||||
|
||||
.product-spec-label {
|
||||
@apply w-full shrink-0 text-right;
|
||||
}
|
||||
|
||||
.product-editor-form :deep(.ant-form-item-label > label) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<span class="inline-block h-2.5 w-2.5 shrink-0 rounded-full" :class="statusDotClass" />
|
||||
<button
|
||||
type="button"
|
||||
class="truncate border-none bg-transparent p-0 text-right text-sm font-700 text-gray-900 cursor-pointer"
|
||||
class="product-mobile-item__title truncate border-none bg-transparent p-0 text-right text-sm font-700 cursor-pointer"
|
||||
@click="$emit('open', item)"
|
||||
>
|
||||
{{ item.title || '-' }}
|
||||
@@ -15,8 +15,8 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex items-center justify-between gap-2">
|
||||
<span class="text-xs text-gray-500">کد محصول: {{ item.id ?? '-' }}</span>
|
||||
<span class="text-sm font-600 text-gray-700">{{ priceLabel }}</span>
|
||||
<span class="product-mobile-item__meta text-xs">کد محصول: {{ item.id ?? '-' }}</span>
|
||||
<span class="product-mobile-item__price text-sm font-600">{{ priceLabel }}</span>
|
||||
</div>
|
||||
</MobileCard>
|
||||
</template>
|
||||
@@ -39,3 +39,17 @@ defineEmits<{
|
||||
const statusDotClass = computed(() => (props.item.active ? 'bg-green-500' : 'bg-red-400'))
|
||||
const priceLabel = computed(() => (props.item.price ? usePrice(props.item.price) : '-'))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.product-mobile-item__title {
|
||||
color: var(--admin-text);
|
||||
}
|
||||
|
||||
.product-mobile-item__meta {
|
||||
color: var(--admin-text-secondary);
|
||||
}
|
||||
|
||||
.product-mobile-item__price {
|
||||
color: var(--admin-text-muted);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
@cancel="closeProductModal"
|
||||
@close="closeProductModal"
|
||||
>
|
||||
<div v-if="isMobile" class="mb-3 text-base font-700">{{ `${editId ? 'ویرایش' : 'افزودن'} محصول` }}</div>
|
||||
<a-alert
|
||||
v-if="submitError"
|
||||
type="error"
|
||||
@@ -76,7 +75,6 @@ import type { MethodePrices, Product } from '@/model/product'
|
||||
import ProductEditorFormBody from '@/pages/menuPages/products/base/components/ProductEditorFormBody.vue'
|
||||
import { validateProductSubmitInput } from '@/pages/menuPages/products/base/productFormValidation'
|
||||
import tbl from './components/table.vue'
|
||||
import { isMobile } from '@core/utils/is'
|
||||
import { usePrice } from '@core/utils/usePrice'
|
||||
import { referenceProviderTotal } from '@/utils/productEditorPrice'
|
||||
import {
|
||||
@@ -110,7 +108,7 @@ const methodOption = ref('1')
|
||||
const weightOption = ref('1')
|
||||
const meltedPriceOption = ref('app')
|
||||
const tradeWeightUnit = ref('گرم')
|
||||
const activeKey_ = ref(['0'])
|
||||
const activeKey_ = ref('0')
|
||||
const ProductIndexImage = ref<Array<FileList>>([])
|
||||
const addUserVisible = ref(false)
|
||||
let editId: number | undefined
|
||||
@@ -414,6 +412,38 @@ function showSubmitError(text: string) {
|
||||
message.error(text, 4)
|
||||
}
|
||||
|
||||
const PRICING_FIELDS = new Set([
|
||||
'price',
|
||||
'reading_method',
|
||||
'price_offset_percent',
|
||||
'use_last_price_if_zero',
|
||||
])
|
||||
const DETAILS_FIELDS = new Set([
|
||||
'title',
|
||||
'group_type',
|
||||
'metal_id',
|
||||
'amount',
|
||||
'unit',
|
||||
'direct_price',
|
||||
'inventory',
|
||||
'trade_by_quantity',
|
||||
'show_in_assets',
|
||||
])
|
||||
const FEES_FIELDS = new Set(['cash_fee_buy', 'cash_fee_sell'])
|
||||
const HOURS_FIELDS = new Set(['override_trading_hours', 'auto_open_at', 'auto_close_at'])
|
||||
const ORDER_FIELDS = new Set(['order_limit_enabled', 'min_order_cash', 'max_order_cash'])
|
||||
const DELIVERY_FIELDS = new Set(['has_physical_delivery', 'min_physical_delivery_quantity'])
|
||||
|
||||
function tabKeyForField(fieldName: string): string {
|
||||
if (PRICING_FIELDS.has(fieldName)) return '0'
|
||||
if (DETAILS_FIELDS.has(fieldName)) return '1'
|
||||
if (FEES_FIELDS.has(fieldName)) return '2'
|
||||
if (HOURS_FIELDS.has(fieldName)) return '3'
|
||||
if (ORDER_FIELDS.has(fieldName)) return '4'
|
||||
if (DELIVERY_FIELDS.has(fieldName)) return '5'
|
||||
return '6'
|
||||
}
|
||||
|
||||
async function submitProduct() {
|
||||
try {
|
||||
await saveProduct()
|
||||
@@ -425,13 +455,6 @@ async function submitProduct() {
|
||||
}
|
||||
|
||||
async function saveProduct() {
|
||||
const formSectionKeys = ['0', '1', '2']
|
||||
const nextKeys = [...new Set([...formSectionKeys, ...activeKey_.value])]
|
||||
if (nextKeys.length !== activeKey_.value.length) {
|
||||
activeKey_.value = nextKeys
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
const businessError = validateProductSubmitInput({
|
||||
form: form.value as Record<string, unknown>,
|
||||
methodOption: methodOption.value,
|
||||
@@ -452,8 +475,12 @@ async function saveProduct() {
|
||||
try {
|
||||
await editorFormRef.value.validateFields()
|
||||
} catch (err: unknown) {
|
||||
const first = (err as { errorFields?: Array<{ errors?: string[] }> })?.errorFields?.[0]?.errors?.[0]
|
||||
showSubmitError(first || 'لطفاً فیلدهای الزامی را تکمیل کنید')
|
||||
const errorFields = (err as { errorFields?: Array<{ name?: Array<string | number>; errors?: string[] }> })?.errorFields
|
||||
const firstField = errorFields?.[0]
|
||||
const fieldName = String(firstField?.name?.[0] ?? '')
|
||||
activeKey_.value = tabKeyForField(fieldName)
|
||||
await nextTick()
|
||||
showSubmitError(firstField?.errors?.[0] || 'لطفاً فیلدهای الزامی را تکمیل کنید')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -463,6 +490,7 @@ async function saveProduct() {
|
||||
const payload = cloneDeep(form.value)
|
||||
const limitError = validateProductOrderLimitForm(payload)
|
||||
if (limitError) {
|
||||
activeKey_.value = '4'
|
||||
showSubmitError(limitError)
|
||||
loading.value = false
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user