feat(product): add show_in_list toggle and TV price formula UI

- show_in_list switch in product editor
- TvFormulaPriceSection component (blue theme) shown when show_on_tv is on
- Form defaults, model type, and submit logic for new fields

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-19 11:51:57 +03:30
co-authored by Cursor
parent c332e9df24
commit 2ab99255da
4 changed files with 224 additions and 0 deletions
+3
View File
@@ -27,6 +27,7 @@ export interface Product{
show: boolean
show_in_assets?: boolean
show_on_tv?: boolean
show_in_list?: boolean
trade_by_quantity?: boolean
override_trading_hours?: boolean
use_last_price_if_zero?: boolean
@@ -40,6 +41,8 @@ export interface Product{
app_price?: boolean
price_formula_enabled?: boolean
price_formula?: string | null
tv_price_formula_enabled?: boolean
tv_price_formula?: string | null
price: number
price_profit?: number
price_offset_percent?: number
@@ -411,6 +411,21 @@
<a-switch v-model:checked="form.show_on_tv" />
</a-form-item>
</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 class="product-editor-panel product-spec-switch-row">
<div class="product-spec-label">
<DashboardLabelWithHelp label="نمایش در لیست محصولات" :help="showInListHelp" size="sm" />
</div>
<a-form-item name="show_in_list" class="!mb-0">
<a-switch v-model:checked="form.show_in_list" />
</a-form-item>
</div>
</div>
</a-tab-pane>
@@ -590,6 +605,7 @@ import { resolveTradeWeightUnit, weightFieldLabelForUnit } from '@/utils/weightU
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
import FormulaPriceSection from './FormulaPriceSection.vue'
import TvFormulaPriceSection from './TvFormulaPriceSection.vue'
import dayjs from 'dayjs'
import { computed, onMounted, ref, watch } from 'vue'
@@ -669,6 +685,10 @@ const showOnTvHelp: DashboardHelp = {
description: 'روشن: این محصول در صفحه تابلو قیمت مشتری (/tv) نمایش داده می‌شود.',
}
const showInListHelp: DashboardHelp = {
description: 'روشن: این محصول در صفحه لیست محصولات اپ مشتری نمایش داده می‌شود.',
}
const autoOpenHelp: DashboardHelp = {
description: 'ساعت شروع معامله این محصول — فقط وقتی ساعت اختصاصی روشن باشد.',
}
@@ -820,6 +840,11 @@ function onFormulaEnabledChange(enabled: boolean) {
}
}
function onTvFormulaFormPatch(next: Record<string, unknown>) {
props.form.tv_price_formula_enabled = !!next.tv_price_formula_enabled
props.form.tv_price_formula = next.tv_price_formula ?? props.form.tv_price_formula
}
watch(
() => props.methodOption,
(option) => {
@@ -0,0 +1,183 @@
<template>
<div class="tv-formula-section mb-4 flex flex-col gap-2 rounded-lg border border-solid px-3 py-2">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-1.5">
<DashboardLabelWithHelp
label="قیمت فرمولی TV"
:help="formulaHelp"
size="sm"
text-class="tv-formula-section__label"
/>
</div>
<a-switch
:checked="!!modelValue.tv_price_formula_enabled"
checked-children="فعال"
un-checked-children="خاموش"
@change="onToggle"
/>
</div>
<template v-if="modelValue.tv_price_formula_enabled">
<a-form-item class="!mb-0" name="tv_price_formula">
<template #label>
<span class="tv-formula-section__label text-sm">فرمول قیمت TV</span>
</template>
<a-input
v-model:value="localFormula"
dir="ltr"
class="tv-formula-input font-mono text-sm"
style="direction: ltr; text-align: left"
placeholder="مثال: ROUND(((P)/MG)*W+100000, -3)"
@focus="isEditingFormula = true"
@blur="onFormulaBlur"
@update:value="onFormulaInput"
/>
</a-form-item>
<div class="tv-formula-section__result flex items-center justify-between gap-2 rounded-md px-2 py-1 text-sm">
<span class="tv-formula-section__result-label">قیمت TV (محاسبه‌شده)</span>
<span class="tv-formula-section__result-value font-bold">{{ usePrice(computedProductPrice) }}</span>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import {
DEFAULT_PRICE_FORMULA,
getFormulaVariableHelp,
resolveFormulaWeight,
computeFormulaProductPrice
} from '@/utils/formulaPrice'
import { usePrice } from '@core/utils/usePrice'
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
export type TvFormulaPriceForm = {
tv_price_formula_enabled?: boolean
tv_price_formula?: string | null
}
const props = defineProps<{
modelValue: TvFormulaPriceForm
referenceMiddlePrice: number
productAmount: number
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: TvFormulaPriceForm): void
}>()
const isEditingFormula = ref(false)
const localFormula = ref(props.modelValue.tv_price_formula || DEFAULT_PRICE_FORMULA)
watch(
() => props.modelValue.tv_price_formula,
(value) => {
if (!isEditingFormula.value) {
localFormula.value = value || DEFAULT_PRICE_FORMULA
}
}
)
const formulaWeight = computed(() => resolveFormulaWeight(props.productAmount))
const formulaHelp = computed((): DashboardHelp => {
const lines = getFormulaVariableHelp(formulaWeight.value)
.map((item) => `${item.symbol} = ${item.label}`)
.join(' | ')
return {
description:
'فرمول جداگانه برای قیمت نمایشی روی تابلو TV. این فرمول فقط روی صفحه TV تأثیر دارد و قیمت عادی محصول را تغییر نمی‌دهد.',
formula: `${lines}. مثال: ROUND(((P)/MG)*W+100000, -3)`,
}
})
const computedProductPrice = computed(() =>
computeFormulaProductPrice(
props.referenceMiddlePrice,
localFormula.value,
formulaWeight.value
)
)
function patch(partial: Partial<TvFormulaPriceForm>) {
emit('update:modelValue', { ...props.modelValue, ...partial })
}
function onFormulaInput(value: string) {
localFormula.value = value
patch({ tv_price_formula: value })
}
function onFormulaBlur() {
isEditingFormula.value = false
patch({ tv_price_formula: localFormula.value })
}
function onToggle(checked: boolean) {
const nextFormula = checked
? localFormula.value || props.modelValue.tv_price_formula || DEFAULT_PRICE_FORMULA
: props.modelValue.tv_price_formula
localFormula.value = nextFormula || DEFAULT_PRICE_FORMULA
patch({
tv_price_formula_enabled: checked,
tv_price_formula: nextFormula,
})
}
</script>
<style scoped>
.tv-formula-section {
border-color: #93c5fd;
background: #eff6ff;
color: #1e3a5f;
}
.tv-formula-section__label {
color: #1e3a5f;
}
.tv-formula-section__result {
background: #fff;
}
.tv-formula-section__result-label {
color: #4b5563;
}
.tv-formula-section__result-value {
color: #1d4ed8;
}
.tv-formula-input :deep(input) {
direction: ltr !important;
text-align: left !important;
unicode-bidi: plaintext;
}
</style>
<style>
html.is-dark .tv-formula-section {
border-color: rgba(59, 130, 246, 0.55);
background: rgba(30, 58, 95, 0.45);
color: #93c5fd;
}
html.is-dark .tv-formula-section__label {
color: #93c5fd;
}
html.is-dark .tv-formula-section__result {
background: rgba(0, 0, 0, 0.28);
}
html.is-dark .tv-formula-section__result-label {
color: rgba(147, 197, 253, 0.85);
}
html.is-dark .tv-formula-section__result-value {
color: #60a5fa;
}
</style>
@@ -367,10 +367,13 @@ async function edit(record: Product) {
...productData,
show_in_assets: productData.show_in_assets ?? false,
show_on_tv: productData.show_on_tv ?? false,
show_in_list: productData.show_in_list ?? true,
trade_by_quantity: productData.trade_by_quantity ?? false,
app_price: Boolean(productData.app_price),
price_formula_enabled: Boolean(productData.price_formula_enabled),
price_formula: productData.price_formula ?? '',
tv_price_formula_enabled: Boolean(productData.tv_price_formula_enabled),
tv_price_formula: productData.tv_price_formula ?? '',
override_trading_hours: productData.override_trading_hours ?? false,
has_physical_delivery: productData.has_physical_delivery ?? false,
min_physical_delivery_quantity:
@@ -439,11 +442,14 @@ function resetData() {
app_price: false,
price_formula_enabled: false,
price_formula: '',
tv_price_formula_enabled: false,
tv_price_formula: '',
file_id: undefined,
file: undefined,
order_limit_enabled: false,
show_in_assets: false,
show_on_tv: false,
show_in_list: true,
trade_by_quantity: false,
override_trading_hours: false,
use_last_price_if_zero: false,
@@ -470,6 +476,8 @@ const PRICING_FIELDS = new Set([
'use_last_price_if_zero',
'price_formula',
'price_formula_enabled',
'tv_price_formula',
'tv_price_formula_enabled',
])
const DETAILS_FIELDS = new Set([
'title',
@@ -483,6 +491,7 @@ const DETAILS_FIELDS = new Set([
'trade_by_quantity',
'show_in_assets',
'show_on_tv',
'show_in_list',
])
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'])
@@ -609,6 +618,10 @@ async function saveProduct() {
payload.app_price = false
payload.price_formula_enabled = false
}
if (!payload.show_on_tv) {
payload.tv_price_formula_enabled = false
}
if (weightOption.value === '2') {
payload.amount = 0
if (!isMeltedTradeGroup(payload.group_type)) {