ui(product): restructure product specs tab with sections and fix pricing tab gap

- Product specs tab: group fields into bordered sections (اطلاعات پایه, وزن و واحد, تنظیمات نمایش)
- Visibility switches in a compact panel with divider lines
- Weight/unit/inventory fields in same grid row
- Pricing tab: wrap all content in flex-col gap-4
- Remove unnecessary product-editor-panel border from non-melted pricing block

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-19 12:23:52 +03:30
co-authored by Cursor
parent 6cb2488221
commit 0a0d1d8680
@@ -14,6 +14,7 @@
@update:activeKey="$emit('update:activeKey', $event)" @update:activeKey="$emit('update:activeKey', $event)"
> >
<a-tab-pane key="0" tab="قیمت‌گذاری" force-render> <a-tab-pane key="0" tab="قیمت‌گذاری" force-render>
<div class="flex flex-col gap-4">
<div <div
v-if="usesMeltedPricing" v-if="usesMeltedPricing"
class="product-editor-panel flex flex-col gap-3" class="product-editor-panel flex flex-col gap-3"
@@ -112,7 +113,7 @@
</div> </div>
</div> </div>
<div v-if="!usesMeltedPricing" class="product-editor-panel flex flex-col gap-4"> <div v-if="!usesMeltedPricing" class="flex flex-col gap-4">
<div class="product-spec-switch-row"> <div class="product-spec-switch-row">
<div class="product-spec-label"> <div class="product-spec-label">
<DashboardLabelWithHelp label="دریافت قیمت از" :help="priceSourceHelp" size="sm" /> <DashboardLabelWithHelp label="دریافت قیمت از" :help="priceSourceHelp" size="sm" />
@@ -265,91 +266,128 @@
</AButton> </AButton>
</div> </div>
</div> </div>
</div>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="1" tab="مشخصات محصول" force-render> <a-tab-pane key="1" tab="مشخصات محصول" force-render>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-5">
<div class="grid gap-4 md:grid-cols-3"> <!-- اطلاعات پایه -->
<a-form-item name="title" :rules="inputRules()" class="!mb-0"> <div class="product-section">
<div class="product-section__title">اطلاعات پایه</div>
<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()" 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()" 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>
<a-form-item name="description" class="!mb-0">
<template #label> <template #label>
<DashboardLabelWithHelp label="عنوان" :help="titleHelp" size="sm" /> <DashboardLabelWithHelp label="توضیح محصول" :help="productDescriptionHelp" size="sm" />
</template> </template>
<BasicInput v-model:value="form.title" /> <a-textarea
</a-form-item> v-model:value="form.description"
<a-form-item name="group_type" :rules="inputRules()" class="!mb-0"> :rows="2"
<template #label> placeholder="متن توضیح برای نمایش در اپ مشتری (اختیاری)"
<DashboardLabelWithHelp label="دسته‌بندی" :help="categoryHelp" size="sm" /> :maxlength="2000"
</template> show-count
<BasicSelect v-model:value="form.group_type" :items="categoryItems" valueName="value" keyName="key" /> />
</a-form-item>
<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> </a-form-item>
</div> </div>
<a-form-item name="description" class="!mb-0"> <!-- وزن و واحد -->
<template #label> <div class="product-section">
<DashboardLabelWithHelp label="توضیح محصول" :help="productDescriptionHelp" size="sm" /> <div class="product-section__title">وزن و واحد</div>
</template> <div v-if="!isMeltedGroup" class="flex flex-col gap-4">
<a-textarea <div class="grid gap-4 md:grid-cols-2">
v-model:value="form.description" <div class="flex items-center justify-between gap-3">
:rows="3" <DashboardLabelWithHelp label="نوع معامله" :help="tradeByQuantityHelp" size="sm" />
placeholder="متن توضیح برای نمایش در اپ مشتری (اختیاری)" <a-radio-group
:maxlength="2000" v-if="!editId"
show-count v-model:value="tradeTypeOption"
/> name="tradeTypeGroup"
</a-form-item> >
<a-radio value="count">تعدادی</a-radio>
<div v-if="!isMeltedGroup" class="product-editor-panel flex flex-col gap-4"> <a-radio value="quantity">مقداری</a-radio>
<div class="product-spec-switch-row"> </a-radio-group>
<div class="product-spec-label"> <span
<DashboardLabelWithHelp label="نوع معامله" :help="tradeByQuantityHelp" size="sm" /> 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 class="flex items-center justify-between gap-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> </div>
<a-radio-group <div
v-if="!editId" v-if="isQuantityTrade"
v-model:value="tradeTypeOption" class="rounded-md bg-amber-50 px-3 py-1.5 text-xs leading-5 text-amber-800"
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> </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>
<div class="grid gap-4 md:grid-cols-3">
<div v-if="weightOption === '1'" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3"> <a-form-item v-if="weightOption === '1' && !isMeltedGroup" name="amount" :rules="inputRulesNumber('وزن')" class="!mb-0">
<a-form-item name="amount" :rules="inputRulesNumber('وزن')" class="!mb-0">
<template #label> <template #label>
<DashboardLabelWithHelp :label="weightFieldLabel" :help="weightAmountHelp" size="sm" /> <DashboardLabelWithHelp :label="weightFieldLabel" :help="weightAmountHelp" size="sm" />
</template> </template>
<BasicInput v-model:value="form.amount" type="number" /> <BasicInput v-model:value="form.amount" type="number" />
</a-form-item> </a-form-item>
<a-form-item
v-if="showUnitField"
name="unit"
:rules="inputRules()"
class="!mb-0"
>
<template #label>
<DashboardLabelWithHelp label="واحد" :help="unitHelp" size="sm" />
</template>
<BasicSelect v-model:value="form.unit" :items="unitItems" valueName="value" keyName="value" />
</a-form-item>
<a-form-item
v-if="showDirectPriceField"
name="direct_price"
:rules="inputRules()"
class="!mb-0"
>
<template #label>
<DashboardLabelWithHelp label="نوع قیمت" :help="directPriceHelp" size="sm" />
</template>
<BasicSelect v-model:value="form.direct_price" :items="priceItems" valueName="value" keyName="key" />
</a-form-item>
<a-form-item
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>
<div <div
v-if="form.amount && form.amount < 1 && 0 < form.amount && tradeWeightUnit === 'گرم'" v-if="weightOption === '1' && !isMeltedGroup && form.amount && form.amount < 1 && 0 < form.amount && tradeWeightUnit === 'گرم'"
class="flex items-end pb-1 text-sm text-gray-500" class="flex items-end pb-1 text-sm text-gray-500"
> >
معادل {{ form.amount * 1000 }} سوت معادل {{ form.amount * 1000 }} سوت
@@ -357,73 +395,31 @@
</div> </div>
</div> </div>
<div <!-- نمایش -->
v-if="showUnitField || showDirectPriceField || (isStore && !isMeltedGroup)" <div class="product-section">
class="grid gap-4 md:grid-cols-2 xl:grid-cols-3" <div class="product-section__title">تنظیمات نمایش</div>
> <div class="product-visibility-panel">
<a-form-item <div class="product-visibility-row">
v-if="showUnitField" <span class="text-sm">نمایش در لیست محصولات</span>
name="unit"
:rules="inputRules()"
class="!mb-0"
>
<template #label>
<DashboardLabelWithHelp label="واحد" :help="unitHelp" size="sm" />
</template>
<BasicSelect v-model:value="form.unit" :items="unitItems" valueName="value" keyName="value" />
</a-form-item>
<a-form-item
v-if="showDirectPriceField"
name="direct_price"
:rules="inputRules()"
class="!mb-0"
>
<template #label>
<DashboardLabelWithHelp label="نوع قیمت" :help="directPriceHelp" size="sm" />
</template>
<BasicSelect v-model:value="form.direct_price" :items="priceItems" valueName="value" keyName="key" />
</a-form-item>
<a-form-item
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>
</div>
<div class="product-editor-panel flex flex-col gap-0 rounded-lg overflow-hidden">
<div class="product-visibility-row">
<DashboardLabelWithHelp label="نمایش در لیست محصولات" :help="showInListHelp" size="sm" />
<a-form-item name="show_in_list" class="!mb-0">
<a-switch v-model:checked="form.show_in_list" /> <a-switch v-model:checked="form.show_in_list" />
</a-form-item> </div>
</div> <div class="product-visibility-row">
<div class="product-visibility-row"> <span class="text-sm">نمایش در دارایی‌ها</span>
<DashboardLabelWithHelp label="نمایش در دارایی‌ها" :help="showInAssetsHelp" size="sm" />
<a-form-item name="show_in_assets" class="!mb-0">
<a-switch v-model:checked="form.show_in_assets" /> <a-switch v-model:checked="form.show_in_assets" />
</a-form-item> </div>
</div> <div class="product-visibility-row">
<div class="product-visibility-row"> <span class="text-sm">نمایش در تابلو TV</span>
<DashboardLabelWithHelp label="نمایش در تابلو TV" :help="showOnTvHelp" size="sm" />
<a-form-item name="show_on_tv" class="!mb-0">
<a-switch v-model:checked="form.show_on_tv" /> <a-switch v-model:checked="form.show_on_tv" />
</a-form-item> </div>
</div> </div>
<TvFormulaPriceSection
v-if="form.show_on_tv"
:model-value="form"
:reference-middle-price="formulaReferenceMiddlePrice"
:product-amount="Number(form.amount) || 0"
@update:model-value="onTvFormulaFormPatch"
/>
</div> </div>
<TvFormulaPriceSection
v-if="form.show_on_tv"
:model-value="form"
:reference-middle-price="formulaReferenceMiddlePrice"
:product-amount="Number(form.amount) || 0"
@update:model-value="onTvFormulaFormPatch"
/>
</div> </div>
</a-tab-pane> </a-tab-pane>
@@ -980,8 +976,23 @@ defineExpose({
@apply flex flex-col items-stretch gap-2; @apply flex flex-col items-stretch gap-2;
} }
.product-section {
@apply flex flex-col gap-4 rounded-lg p-4;
border: 1px solid var(--ant-color-border-secondary, rgba(0, 0, 0, 0.06));
}
.product-section__title {
@apply text-sm font-bold;
color: var(--ant-color-text-secondary, rgba(0, 0, 0, 0.45));
}
.product-visibility-panel {
@apply flex flex-col rounded-md overflow-hidden;
border: 1px solid var(--ant-color-border-secondary, rgba(0, 0, 0, 0.06));
}
.product-visibility-row { .product-visibility-row {
@apply flex items-center justify-between gap-3 px-3 py-2; @apply flex items-center justify-between gap-3 px-3 py-2.5;
border-bottom: 1px solid var(--ant-color-border-secondary, rgba(0, 0, 0, 0.06)); border-bottom: 1px solid var(--ant-color-border-secondary, rgba(0, 0, 0, 0.06));
} }
.product-visibility-row:last-child { .product-visibility-row:last-child {