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:
@@ -27,6 +27,7 @@ export interface Product{
|
|||||||
show: boolean
|
show: boolean
|
||||||
show_in_assets?: boolean
|
show_in_assets?: boolean
|
||||||
show_on_tv?: boolean
|
show_on_tv?: boolean
|
||||||
|
show_in_list?: boolean
|
||||||
trade_by_quantity?: boolean
|
trade_by_quantity?: boolean
|
||||||
override_trading_hours?: boolean
|
override_trading_hours?: boolean
|
||||||
use_last_price_if_zero?: boolean
|
use_last_price_if_zero?: boolean
|
||||||
@@ -40,6 +41,8 @@ export interface Product{
|
|||||||
app_price?: boolean
|
app_price?: boolean
|
||||||
price_formula_enabled?: boolean
|
price_formula_enabled?: boolean
|
||||||
price_formula?: string | null
|
price_formula?: string | null
|
||||||
|
tv_price_formula_enabled?: boolean
|
||||||
|
tv_price_formula?: string | null
|
||||||
price: number
|
price: number
|
||||||
price_profit?: number
|
price_profit?: number
|
||||||
price_offset_percent?: number
|
price_offset_percent?: number
|
||||||
|
|||||||
@@ -411,6 +411,21 @@
|
|||||||
<a-switch v-model:checked="form.show_on_tv" />
|
<a-switch v-model:checked="form.show_on_tv" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</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 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>
|
</div>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
|
||||||
@@ -590,6 +605,7 @@ import { resolveTradeWeightUnit, weightFieldLabelForUnit } from '@/utils/weightU
|
|||||||
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
|
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
|
||||||
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
|
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
|
||||||
import FormulaPriceSection from './FormulaPriceSection.vue'
|
import FormulaPriceSection from './FormulaPriceSection.vue'
|
||||||
|
import TvFormulaPriceSection from './TvFormulaPriceSection.vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
@@ -669,6 +685,10 @@ const showOnTvHelp: DashboardHelp = {
|
|||||||
description: 'روشن: این محصول در صفحه تابلو قیمت مشتری (/tv) نمایش داده میشود.',
|
description: 'روشن: این محصول در صفحه تابلو قیمت مشتری (/tv) نمایش داده میشود.',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showInListHelp: DashboardHelp = {
|
||||||
|
description: 'روشن: این محصول در صفحه لیست محصولات اپ مشتری نمایش داده میشود.',
|
||||||
|
}
|
||||||
|
|
||||||
const autoOpenHelp: DashboardHelp = {
|
const autoOpenHelp: DashboardHelp = {
|
||||||
description: 'ساعت شروع معامله این محصول — فقط وقتی ساعت اختصاصی روشن باشد.',
|
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(
|
watch(
|
||||||
() => props.methodOption,
|
() => props.methodOption,
|
||||||
(option) => {
|
(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,
|
...productData,
|
||||||
show_in_assets: productData.show_in_assets ?? false,
|
show_in_assets: productData.show_in_assets ?? false,
|
||||||
show_on_tv: productData.show_on_tv ?? 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,
|
trade_by_quantity: productData.trade_by_quantity ?? false,
|
||||||
app_price: Boolean(productData.app_price),
|
app_price: Boolean(productData.app_price),
|
||||||
price_formula_enabled: Boolean(productData.price_formula_enabled),
|
price_formula_enabled: Boolean(productData.price_formula_enabled),
|
||||||
price_formula: productData.price_formula ?? '',
|
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,
|
override_trading_hours: productData.override_trading_hours ?? false,
|
||||||
has_physical_delivery: productData.has_physical_delivery ?? false,
|
has_physical_delivery: productData.has_physical_delivery ?? false,
|
||||||
min_physical_delivery_quantity:
|
min_physical_delivery_quantity:
|
||||||
@@ -439,11 +442,14 @@ function resetData() {
|
|||||||
app_price: false,
|
app_price: false,
|
||||||
price_formula_enabled: false,
|
price_formula_enabled: false,
|
||||||
price_formula: '',
|
price_formula: '',
|
||||||
|
tv_price_formula_enabled: false,
|
||||||
|
tv_price_formula: '',
|
||||||
file_id: undefined,
|
file_id: undefined,
|
||||||
file: undefined,
|
file: undefined,
|
||||||
order_limit_enabled: false,
|
order_limit_enabled: false,
|
||||||
show_in_assets: false,
|
show_in_assets: false,
|
||||||
show_on_tv: false,
|
show_on_tv: false,
|
||||||
|
show_in_list: true,
|
||||||
trade_by_quantity: false,
|
trade_by_quantity: false,
|
||||||
override_trading_hours: false,
|
override_trading_hours: false,
|
||||||
use_last_price_if_zero: false,
|
use_last_price_if_zero: false,
|
||||||
@@ -470,6 +476,8 @@ const PRICING_FIELDS = new Set([
|
|||||||
'use_last_price_if_zero',
|
'use_last_price_if_zero',
|
||||||
'price_formula',
|
'price_formula',
|
||||||
'price_formula_enabled',
|
'price_formula_enabled',
|
||||||
|
'tv_price_formula',
|
||||||
|
'tv_price_formula_enabled',
|
||||||
])
|
])
|
||||||
const DETAILS_FIELDS = new Set([
|
const DETAILS_FIELDS = new Set([
|
||||||
'title',
|
'title',
|
||||||
@@ -483,6 +491,7 @@ const DETAILS_FIELDS = new Set([
|
|||||||
'trade_by_quantity',
|
'trade_by_quantity',
|
||||||
'show_in_assets',
|
'show_in_assets',
|
||||||
'show_on_tv',
|
'show_on_tv',
|
||||||
|
'show_in_list',
|
||||||
])
|
])
|
||||||
const FEES_FIELDS = new Set(['cash_fee_buy', 'cash_fee_sell'])
|
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 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.app_price = false
|
||||||
payload.price_formula_enabled = false
|
payload.price_formula_enabled = false
|
||||||
}
|
}
|
||||||
|
if (!payload.show_on_tv) {
|
||||||
|
payload.tv_price_formula_enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
if (weightOption.value === '2') {
|
if (weightOption.value === '2') {
|
||||||
payload.amount = 0
|
payload.amount = 0
|
||||||
if (!isMeltedTradeGroup(payload.group_type)) {
|
if (!isMeltedTradeGroup(payload.group_type)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user