fix bug 22/4 2
This commit is contained in:
@@ -30,12 +30,13 @@ export interface Product{
|
||||
override_trading_hours?: boolean
|
||||
use_last_price_if_zero?: boolean
|
||||
has_physical_delivery?: boolean
|
||||
min_physical_delivery_grams?: number | null
|
||||
min_physical_delivery_quantity?: number | null
|
||||
title: string
|
||||
updated_at: string
|
||||
methodOption: number
|
||||
prices: MethodePrices[]
|
||||
direct_price: number
|
||||
app_price?: boolean
|
||||
price: number
|
||||
price_profit?: number
|
||||
price_offset_percent?: number
|
||||
|
||||
@@ -12,7 +12,18 @@
|
||||
<span class="setting-card__title">{{ title }}</span>
|
||||
</div>
|
||||
<div class="setting-card__key">کلید: {{ settingKey }}</div>
|
||||
<div v-if="help" class="setting-card__help">{{ help }}</div>
|
||||
<div v-if="switchHelp" class="setting-card__help setting-card__help--switch">
|
||||
<div v-if="switchHelp.note" class="setting-card__help-note">{{ switchHelp.note }}</div>
|
||||
<div v-if="switchHelp.on" class="setting-card__help-state">
|
||||
<span class="setting-card__help-badge setting-card__help-badge--on">روشن</span>
|
||||
<span class="setting-card__help-text">{{ switchHelp.on }}</span>
|
||||
</div>
|
||||
<div v-if="switchHelp.off" class="setting-card__help-state">
|
||||
<span class="setting-card__help-badge setting-card__help-badge--off">خاموش</span>
|
||||
<span class="setting-card__help-text">{{ switchHelp.off }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="help" class="setting-card__help">{{ help }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="setting-card__body"
|
||||
@@ -41,6 +52,7 @@
|
||||
import SettingValueField from './SettingValueField.vue'
|
||||
import {
|
||||
helpText,
|
||||
parseSwitchHelp,
|
||||
resolveSettingTitle,
|
||||
resolveSettingFieldType,
|
||||
type SettingFieldType
|
||||
@@ -70,6 +82,10 @@ const fieldType = computed<SettingFieldType>(() =>
|
||||
)
|
||||
|
||||
const help = computed(() => helpText(props.row))
|
||||
|
||||
const switchHelp = computed(() =>
|
||||
fieldType.value === 'switch' ? parseSwitchHelp(help.value) : null
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@@ -136,6 +152,54 @@ const help = computed(() => helpText(props.row))
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
&__help--switch {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__help-note {
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
color: var(--admin-text-secondary);
|
||||
}
|
||||
|
||||
&__help-state {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
&__help-badge {
|
||||
flex-shrink: 0;
|
||||
min-width: 42px;
|
||||
text-align: center;
|
||||
padding: 1px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1.6;
|
||||
|
||||
&--on {
|
||||
color: #047857;
|
||||
background: #d1fae5;
|
||||
border: 1px solid #a7f3d0;
|
||||
}
|
||||
|
||||
&--off {
|
||||
color: #b91c1c;
|
||||
background: #fee2e2;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
}
|
||||
|
||||
&__help-text {
|
||||
color: var(--admin-text-secondary);
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
&__body {
|
||||
margin-top: 12px;
|
||||
flex: 1;
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div
|
||||
class="setting-toggle"
|
||||
:class="{
|
||||
'setting-toggle--on': checked,
|
||||
'setting-toggle--off': !checked,
|
||||
'setting-toggle--disabled': disabled,
|
||||
}"
|
||||
role="switch"
|
||||
:aria-checked="checked"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="setting-toggle__seg"
|
||||
:class="{ 'setting-toggle__seg--active': !checked }"
|
||||
:disabled="disabled"
|
||||
@click="setChecked(false)"
|
||||
>
|
||||
<BaseIcon icon="vuesax-linear:close-circle" size="15" />
|
||||
<span>{{ labels.off }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="setting-toggle__seg"
|
||||
:class="{ 'setting-toggle__seg--active': checked }"
|
||||
:disabled="disabled"
|
||||
@click="setChecked(true)"
|
||||
>
|
||||
<BaseIcon icon="vuesax-linear:tick-circle" size="15" />
|
||||
<span>{{ labels.on }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BaseIcon from '@core/components/BaseIcon/src/Icon.vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
checked: boolean
|
||||
labels: { on: string; off: string }
|
||||
disabled?: boolean
|
||||
}>(),
|
||||
{ disabled: false },
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [checked: boolean]
|
||||
}>()
|
||||
|
||||
function setChecked(next: boolean) {
|
||||
if (props.disabled || next === props.checked) return
|
||||
emit('change', next)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.setting-toggle {
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border-radius: 12px;
|
||||
background: var(--admin-bg-muted, #f1f5f9);
|
||||
border: 1px solid var(--admin-border, #e2e8f0);
|
||||
user-select: none;
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-toggle__seg {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
min-width: 84px;
|
||||
padding: 6px 14px;
|
||||
border: none;
|
||||
border-radius: 9px;
|
||||
background: transparent;
|
||||
color: var(--admin-text-secondary, #64748b);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: all 0.18s ease;
|
||||
|
||||
:deep(svg) {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
/* حالت روشن → سگمنت فعال سبز */
|
||||
.setting-toggle--on .setting-toggle__seg--active {
|
||||
background: #10b981;
|
||||
color: #fff;
|
||||
|
||||
:deep(svg) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* حالت خاموش → سگمنت فعال خاکستری قرمزفام */
|
||||
.setting-toggle--off .setting-toggle__seg--active {
|
||||
background: #fff;
|
||||
color: #ef4444;
|
||||
|
||||
:deep(svg) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,12 @@
|
||||
<template>
|
||||
<div class="setting-value-field">
|
||||
<a-switch
|
||||
<SettingToggle
|
||||
v-if="fieldType === 'switch'"
|
||||
:checked="switchChecked"
|
||||
:labels="labels"
|
||||
:disabled="disabled"
|
||||
@change="onSwitchChange"
|
||||
>
|
||||
<template #checkedChildren>{{ labels.on }}</template>
|
||||
<template #unCheckedChildren>{{ labels.off }}</template>
|
||||
</a-switch>
|
||||
/>
|
||||
|
||||
<SupportMessengersField
|
||||
v-else-if="isSupportMessengers"
|
||||
@@ -38,6 +36,15 @@
|
||||
@update:value="emit('update:value', $event)"
|
||||
/>
|
||||
|
||||
<TemplateTextField
|
||||
v-else-if="isTemplateField"
|
||||
:model-value="modelValue"
|
||||
:setting-key="settingKey || ''"
|
||||
:disabled="disabled"
|
||||
@update:value="emit('update:value', $event)"
|
||||
@blur="emit('blur')"
|
||||
/>
|
||||
|
||||
<a-select
|
||||
v-else-if="fieldType === 'select'"
|
||||
:value="modelValue || undefined"
|
||||
@@ -124,8 +131,11 @@
|
||||
<script setup lang="ts">
|
||||
import MainWalletProductsField from './MainWalletProductsField.vue'
|
||||
import PartnerServerUrlsField from './PartnerServerUrlsField.vue'
|
||||
import SettingToggle from './SettingToggle.vue'
|
||||
import SupportMessengersField from './SupportMessengersField.vue'
|
||||
import TemplateTextField from './TemplateTextField.vue'
|
||||
import ZaarboxPriceKeyField from './ZaarboxPriceKeyField.vue'
|
||||
import { hasTemplateField } from './templateFields'
|
||||
import {
|
||||
parseSelectOptions,
|
||||
resolveSettingFieldType,
|
||||
@@ -163,6 +173,7 @@ const isZaarboxPriceKey = computed(() => props.settingKey === 'zaarbox_price_key
|
||||
const isMainWalletProducts = computed(() => props.settingKey === 'main_wallet_products')
|
||||
const isSupportMessengers = computed(() => props.settingKey === 'support_messengers')
|
||||
const isPartnerServerUrls = computed(() => props.settingKey === 'zaarbox_api_url')
|
||||
const isTemplateField = computed(() => hasTemplateField(props.settingKey))
|
||||
|
||||
const selectItems = computed(() =>
|
||||
parseSelectOptions({
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
<template>
|
||||
<div class="tpl-field">
|
||||
<div class="tpl-field__grid">
|
||||
<!-- ویرایشگر قالب -->
|
||||
<div class="tpl-field__editor">
|
||||
<div class="tpl-field__section-title">
|
||||
<BaseIcon icon="vuesax-linear:edit-2" size="15" />
|
||||
<span>{{ config.editorTitle }}</span>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
class="tpl-field__textarea"
|
||||
:value="modelValue"
|
||||
:disabled="disabled"
|
||||
:rows="config.rows"
|
||||
spellcheck="false"
|
||||
:placeholder="config.placeholder"
|
||||
@input="onInput"
|
||||
@blur="emit('blur')"
|
||||
/>
|
||||
|
||||
<div class="tpl-field__hint">
|
||||
<BaseIcon icon="vuesax-linear:info-circle" size="13" />
|
||||
<span>
|
||||
{{ config.hint }} متغیرها با <code>%</code> یا <code>٪</code> نوشته میشوند.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- پالت متغیرها -->
|
||||
<div class="tpl-field__vars">
|
||||
<div v-for="group in config.variableGroups" :key="group.title" class="tpl-vargroup">
|
||||
<div class="tpl-vargroup__head">
|
||||
<span class="tpl-vargroup__dot" :style="{ background: group.color }" />
|
||||
<span class="tpl-vargroup__title">{{ group.title }}</span>
|
||||
</div>
|
||||
<div class="tpl-vargroup__chips">
|
||||
<button
|
||||
v-for="v in group.items"
|
||||
:key="v.token"
|
||||
type="button"
|
||||
class="tpl-chip"
|
||||
:class="{ 'tpl-chip--disabled': disabled }"
|
||||
:disabled="disabled"
|
||||
:title="v.help"
|
||||
@click="insertToken(v.token)"
|
||||
>
|
||||
<span class="tpl-chip__token">%{{ v.token }}</span>
|
||||
<span class="tpl-chip__label">{{ v.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- پیشنمایش -->
|
||||
<div class="tpl-field__preview">
|
||||
<div class="tpl-field__section-title">
|
||||
<BaseIcon icon="vuesax-linear:eye" size="15" />
|
||||
<span>{{ config.previewTitle }}</span>
|
||||
<span class="tpl-field__preview-tag">نمونه</span>
|
||||
</div>
|
||||
|
||||
<div class="tpl-preview">
|
||||
<div class="tpl-preview__bubble" dir="auto">{{ previewText }}</div>
|
||||
</div>
|
||||
|
||||
<div class="tpl-field__legend">
|
||||
{{ config.legend }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import BaseIcon from '@core/components/BaseIcon/src/Icon.vue'
|
||||
import { getTemplateFieldConfig, TEMPLATE_FIELD_CONFIGS, type TemplateFieldConfig } from './templateFields'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
settingKey: string
|
||||
disabled?: boolean
|
||||
}>(),
|
||||
{ disabled: false },
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:value': [value: string]
|
||||
blur: []
|
||||
}>()
|
||||
|
||||
const textareaRef = ref<HTMLTextAreaElement>()
|
||||
|
||||
const config = computed<TemplateFieldConfig>(
|
||||
() => getTemplateFieldConfig(props.settingKey) ?? TEMPLATE_FIELD_CONFIGS.price_channel_message_text,
|
||||
)
|
||||
|
||||
function escapeRegExp(str: string): string {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
const previewText = computed(() => {
|
||||
const cfg = config.value
|
||||
let out = (props.modelValue ?? '').trim() || cfg.defaultTemplate
|
||||
const keys = Object.keys(cfg.sampleValues).sort((a, b) => b.length - a.length)
|
||||
for (const key of keys) {
|
||||
const re = new RegExp('[%٪]\\s*' + escapeRegExp(key), 'gu')
|
||||
out = out.replace(re, cfg.sampleValues[key])
|
||||
}
|
||||
return out.replace(/\n{3,}/g, '\n\n').trim()
|
||||
})
|
||||
|
||||
function onInput(event: Event) {
|
||||
emit('update:value', (event.target as HTMLTextAreaElement).value)
|
||||
}
|
||||
|
||||
function insertToken(token: string) {
|
||||
if (props.disabled) return
|
||||
|
||||
const insert = `%${token}`
|
||||
const current = props.modelValue ?? ''
|
||||
const el = textareaRef.value
|
||||
|
||||
if (!el) {
|
||||
emit('update:value', current ? `${current}${insert}` : insert)
|
||||
return
|
||||
}
|
||||
|
||||
const start = el.selectionStart ?? current.length
|
||||
const end = el.selectionEnd ?? current.length
|
||||
const next = current.slice(0, start) + insert + current.slice(end)
|
||||
emit('update:value', next)
|
||||
|
||||
nextTick(() => {
|
||||
el.focus()
|
||||
const pos = start + insert.length
|
||||
el.setSelectionRange(pos, pos)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tpl-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tpl-field__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
|
||||
@media (min-width: 992px) {
|
||||
grid-template-columns: 1.15fr 0.85fr;
|
||||
}
|
||||
}
|
||||
|
||||
.tpl-field__section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--admin-text);
|
||||
margin-bottom: 8px;
|
||||
|
||||
:deep(svg) {
|
||||
color: #d4af37;
|
||||
}
|
||||
}
|
||||
|
||||
.tpl-field__preview-tag {
|
||||
margin-inline-start: auto;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: #b45309;
|
||||
background: #fff7ed;
|
||||
border: 1px solid #fed7aa;
|
||||
border-radius: 6px;
|
||||
padding: 1px 8px;
|
||||
}
|
||||
|
||||
.tpl-field__textarea {
|
||||
width: 100%;
|
||||
min-height: 160px;
|
||||
resize: vertical;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: 10px;
|
||||
background: var(--admin-bg-elevated);
|
||||
color: var(--admin-text);
|
||||
font-size: 13px;
|
||||
line-height: 2;
|
||||
font-family: 'Vazirmatn', 'IRANSans', ui-monospace, monospace;
|
||||
direction: rtl;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #d4af37;
|
||||
box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.15);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: var(--admin-bg-muted);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.tpl-field__hint {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
line-height: 1.7;
|
||||
color: var(--admin-text-secondary);
|
||||
|
||||
:deep(svg) {
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
code {
|
||||
background: var(--admin-bg-muted);
|
||||
border-radius: 4px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.tpl-field__vars {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tpl-vargroup__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.tpl-vargroup__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tpl-vargroup__title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--admin-text-secondary);
|
||||
}
|
||||
|
||||
.tpl-vargroup__chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tpl-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: 8px;
|
||||
background: var(--admin-bg-elevated);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
font-family: inherit;
|
||||
|
||||
&:hover:not(.tpl-chip--disabled) {
|
||||
border-color: #d4af37;
|
||||
background: #fffaf0;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 6px rgba(212, 175, 55, 0.15);
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&__token {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #b45309;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 11px;
|
||||
color: var(--admin-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.tpl-preview {
|
||||
background: linear-gradient(160deg, #e8f5e9 0%, #eef7ff 100%);
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.tpl-preview__bubble {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px 12px 12px 4px;
|
||||
padding: 12px 14px;
|
||||
font-size: 13px;
|
||||
line-height: 2;
|
||||
color: #111827;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.tpl-field__legend {
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
line-height: 1.7;
|
||||
color: var(--admin-text-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -290,6 +290,7 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
|
||||
price_channel_bale_to: 'text',
|
||||
price_channel_telegram_to: 'text',
|
||||
price_channel_rubika_to: 'text',
|
||||
price_channel_message_text: 'longtext',
|
||||
rubika_bot_token: 'text',
|
||||
change_mesghal: 'number'
|
||||
}
|
||||
@@ -429,6 +430,54 @@ export function switchLabels(key: string | undefined): { on: string; off: string
|
||||
return { on: 'فعال', off: 'غیرفعال' }
|
||||
}
|
||||
|
||||
export interface SwitchHelpParts {
|
||||
note: string
|
||||
on: string
|
||||
off: string
|
||||
}
|
||||
|
||||
/**
|
||||
* تفکیک توضیح سوییچ به بخشهای «روشن» و «خاموش» برای نمایش خطبهخط.
|
||||
* از نشانگرهای «روشن:»/«روشن =» و «خاموش:»/«خاموش =» استفاده میکند.
|
||||
* اگر هیچ نشانگری نبود null برمیگرداند تا متن ساده نمایش داده شود.
|
||||
*/
|
||||
export function parseSwitchHelp(help: string | undefined | null): SwitchHelpParts | null {
|
||||
const text = (help ?? '').trim()
|
||||
if (!text) return null
|
||||
|
||||
const onMatch = /روشن\s*[:=]\s*/.exec(text)
|
||||
const offMatch = /خاموش\s*[:=]\s*/.exec(text)
|
||||
if (!onMatch && !offMatch) return null
|
||||
|
||||
const trim = (s: string) => s.replace(/^[\s،؛.:]+|[\s،؛:]+$/g, '').trim()
|
||||
|
||||
let note = ''
|
||||
let on = ''
|
||||
let off = ''
|
||||
|
||||
if (onMatch && offMatch) {
|
||||
const onIdx = onMatch.index
|
||||
const offIdx = offMatch.index
|
||||
if (onIdx < offIdx) {
|
||||
note = text.slice(0, onIdx)
|
||||
on = text.slice(onIdx + onMatch[0].length, offIdx)
|
||||
off = text.slice(offIdx + offMatch[0].length)
|
||||
} else {
|
||||
note = text.slice(0, offIdx)
|
||||
off = text.slice(offIdx + offMatch[0].length, onIdx)
|
||||
on = text.slice(onIdx + onMatch[0].length)
|
||||
}
|
||||
} else if (onMatch) {
|
||||
note = text.slice(0, onMatch.index)
|
||||
on = text.slice(onMatch.index + onMatch[0].length)
|
||||
} else if (offMatch) {
|
||||
note = text.slice(0, offMatch.index)
|
||||
off = text.slice(offMatch.index + offMatch[0].length)
|
||||
}
|
||||
|
||||
return { note: trim(note), on: trim(on), off: trim(off) }
|
||||
}
|
||||
|
||||
/** توضیحات fallback — وقتی در کاتالوگ تعریف شده، بر description دیتابیس اولویت دارد */
|
||||
const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
name_fa:
|
||||
@@ -442,13 +491,13 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
auto_active_customer:
|
||||
'روشن: حساب بلافاصله پس از ثبتنام فعال میشود. خاموش: نیاز به تأیید دستی شما در پنل دارد.',
|
||||
close_price:
|
||||
'فیلتر اصلی فروشگاه. وقتی روشن باشد، خرید و فروش و نمایش قیمت فقط بین «ساعت شروع» و «ساعت پایان» انجام میشود و خارج از این بازه به مشتری «فروشگاه بسته است» نشان داده میشود. خاموش: محدودیت ساعتی سراسری اعمال نمیشود (مگر برای محصولی ساعت اختصاصی تعریف کرده باشید).',
|
||||
'فیلتر اصلی ساعت کاری فروشگاه. روشن: خرید، فروش و نمایش قیمت فقط بین «ساعت شروع» و «ساعت پایان» انجام میشود و خارج از این بازه به مشتری «فروشگاه بسته است» نشان داده میشود. خاموش: محدودیت ساعتی سراسری اعمال نمیشود (مگر برای محصولی ساعت اختصاصی تعریف کرده باشید).',
|
||||
startDay:
|
||||
'ساعت شروع فعالیت روزانه فروشگاه. فقط وقتی «بستن خودکار خارج از ساعت کاری» روشن باشد اعمال میشود. از ساعتشمار انتخاب کنید (مثلاً ۰۹:۰۰).',
|
||||
endDay:
|
||||
'ساعت پایان فعالیت روزانه فروشگاه. فقط وقتی «بستن خودکار خارج از ساعت کاری» روشن باشد اعمال میشود. بعد از این ساعت فروشگاه بسته میشود (مثلاً ۱۷:۰۰ یا ۲۲:۰۰).',
|
||||
show_last_price:
|
||||
'وقتی قیمت زنده در دسترس نباشد: روشن = آخرین قیمت ذخیرهشده نشان داده میشود؛ خاموش = بهجای صفر پیام «مظنه در دسترس نیست» نمایش داده میشود.',
|
||||
'رفتار هنگام در دسترس نبودن قیمت زنده. روشن: آخرین قیمت ذخیرهشده نشان داده میشود. خاموش: بهجای صفر پیام «مظنه در دسترس نیست» نمایش داده میشود.',
|
||||
show_gold_ounce_price:
|
||||
'روشن: اگر آدرس API تابلو (zaarbox_api_url) تنظیم شده باشد، قیمت انس طلا در اپ مشتری نمایش داده میشود. خاموش: نمایش داده نمیشود.',
|
||||
fee_single:
|
||||
@@ -482,9 +531,9 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
gift_for_referral:
|
||||
'مقدار پاداش (معمولاً وزن فلز) که به معرفشونده واریز میشود. صفر = بدون پاداش. زمان واریز بسته به «پاداش معرف پس از اولین سفارش» است.',
|
||||
register_referral_sms_to_referrer:
|
||||
'روشن: پس از ثبتنام کاربر جدید، پیامک اطلاعرسانی برای معرف ارسال میشود.',
|
||||
'روشن: پس از ثبتنام کاربر جدید، پیامک اطلاعرسانی برای معرف ارسال میشود. خاموش: پیامکی برای معرف ارسال نمیشود.',
|
||||
register_referral_bonus_message:
|
||||
'روشن: متن پاداش معرف در پیامک خوشآمد ثبتنام گنجانده میشود.',
|
||||
'روشن: متن پاداش معرف در پیامک خوشآمد ثبتنام گنجانده میشود. خاموش: متن پاداش در پیامک خوشآمد نمیآید.',
|
||||
referral_gift_on_first_order:
|
||||
'روشن: پاداش معرف (gift_for_referrer) و معرفشونده (gift_for_referral) پس از تکمیل اولین سفارش معرفشونده به کیف واریز میشود — فقط اگر مقدار هر پاداش بیشتر از صفر باشد. خاموش: پاداش بلافاصله پس از ثبتنام موفق یا تأیید احراز هویت واریز میشود (باز هم فقط برای مقادیر بیشتر از صفر).',
|
||||
referral_limit:
|
||||
@@ -492,13 +541,13 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
referral_bonus_amount:
|
||||
'مبلغ یا مقدار پاداش معرف برای نمایش در پیامکها و متنهای دعوت.',
|
||||
jibit_verification:
|
||||
'روشن: هنگام ثبت یا ویرایش کارت بانکی، تطبیق با سرویس احراز هویت جیبیت انجام میشود.',
|
||||
'روشن: هنگام ثبت یا ویرایش کارت بانکی، تطبیق با سرویس احراز هویت جیبیت انجام میشود. خاموش: کارت بدون تطبیق جیبیت ثبت میشود.',
|
||||
register_jibit_verify:
|
||||
'روشن: در ثبتنام، تطبیق کد ملی و شماره موبایل از طریق جیبیت انجام میشود.',
|
||||
'روشن: در ثبتنام، تطبیق کد ملی و شماره موبایل از طریق جیبیت انجام میشود. خاموش: ثبتنام بدون تطبیق جیبیت انجام میشود.',
|
||||
has_page_earn_money:
|
||||
'روشن: منوی «کسب درآمد / دعوت دوستان» در اپ مشتری نمایش داده میشود.',
|
||||
'روشن: منوی «کسب درآمد / دعوت دوستان» در اپ مشتری نمایش داده میشود. خاموش: این منو در اپ نمایش داده نمیشود.',
|
||||
invite_link_text:
|
||||
'متن پیام اشتراکگذاری دعوت. میتوانید از ٪نام برای نام کاربر و ٪لینک برای لینک ثبتنام با کد معرف استفاده کنید. خالی = متن پیشفرض اپ.',
|
||||
'متن پیام اشتراکگذاری دعوت. متن را بنویسید و متغیرها (نام کاربر، لینک ثبتنام) را با کلیک اضافه کنید؛ پیشنمایش نتیجه را با مقادیر نمونه نشان میدهد. خالی = متن پیشفرض اپ.',
|
||||
montzar_mobile:
|
||||
'شماره موبایل حساب اتصال به سرویس همکار قیمت.',
|
||||
montzar_password:
|
||||
@@ -510,13 +559,13 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
zaarbox_api_url:
|
||||
'آدرس سرور(های) فید قیمت (/api/free). تا ۵ آدرس میتوانید اضافه کنید؛ اگر آدرس اول پاسخ ندهد بهترتیب سراغ بعدی میرود. آدرس فعلی در ردیف اول حفظ میشود.',
|
||||
montzar_open_buy:
|
||||
'روشن: ثبت سفارش خرید از طریق قیمت همکار مجاز است (وضعیت واقعی از سرویس خارجی خوانده میشود).',
|
||||
'وضعیت واقعی از سرویس خارجی خوانده میشود. روشن: ثبت سفارش خرید از طریق قیمت همکار مجاز است. خاموش: ثبت سفارش خرید از طریق قیمت همکار غیرفعال است.',
|
||||
montzar_open_sell:
|
||||
'روشن: ثبت سفارش فروش از طریق قیمت همکار مجاز است.',
|
||||
'وضعیت واقعی از سرویس خارجی خوانده میشود. روشن: ثبت سفارش فروش از طریق قیمت همکار مجاز است. خاموش: ثبت سفارش فروش از طریق قیمت همکار غیرفعال است.',
|
||||
montzar_closed:
|
||||
'وضعیت گزارششده از سرویس همکار؛ فقط برای نمایش — این سوییچ را دستی تغییر ندهید.',
|
||||
external_order_api_enabled:
|
||||
'روشن: دریافت کیف پول و ثبت سفارش از سرویس فروشگاه خارجی فعال است.',
|
||||
'روشن: دریافت کیف پول و ثبت سفارش از سرویس فروشگاه خارجی فعال است. خاموش: اتصال به سرویس فروشگاه خارجی غیرفعال است.',
|
||||
node_client_id:
|
||||
'شناسه کلاینت برای اتصال امن به سرویس سفارش خارجی.',
|
||||
node_shared_secret:
|
||||
@@ -532,15 +581,9 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
support_messenger_id:
|
||||
'شناسه تماس در پیامرسان انتخابشده: یوزرنیم، آیدی یا لینک کامل. فقط روی آیکون پیامرسان و دکمه شناور اعمال میشود؛ تماس تلفنی از فیلد «شماره پشتیبانی» خوانده میشود.',
|
||||
support_messenger_text:
|
||||
'متن راهنمای واریز در مودال شارژ حساب. متغیرها: ٪نام یا %نام = نام صاحب حساب؛ ٪کارت یا %کارت = شماره کارت؛ ٪شبا یا %شبا = شبا. فاصله بعد از ٪ اختیاری است. اگر خالی باشد از متن پیشفرض اپ استفاده میشود.',
|
||||
'متن راهنمای واریز در مودال شارژ حساب. متن را بنویسید و متغیرها (نام صاحب حساب، شماره کارت، شبا) را با کلیک اضافه کنید؛ پیشنمایش نتیجه را با مقادیر نمونه نشان میدهد. خالی = متن پیشفرض اپ.',
|
||||
deposit_support_message_text:
|
||||
`متن پیام هنگام کلیک «پیام به پشتیبانی» در مودال واریز (SMS و پیامرسان).
|
||||
|
||||
متغیر:
|
||||
• %نام یا ٪نام = نام مشتری واردشده (اگر خالی بود، نام صاحب حساب از تنظیمات)
|
||||
|
||||
پیشفرض اپ:
|
||||
باسلام، %نام هستم. کدپیگیری و مبلغ واریزی من:`,
|
||||
'متن پیام هنگام کلیک «پیام به پشتیبانی» در مودال واریز (SMS و پیامرسان). متن را بنویسید و متغیر «نام مشتری» را با کلیک اضافه کنید؛ پیشنمایش نتیجه را نشان میدهد. خالی = متن پیشفرض اپ.',
|
||||
deposit_account_holder:
|
||||
'نام صاحب حساب واریز — در متن راهنمای واریز با ٪نام یا %نام جایگزین میشود. در پیام پشتیبانی فقط وقتی نام مشتری موجود نباشد استفاده میشود.',
|
||||
deposit_card_number:
|
||||
@@ -550,11 +593,11 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
deposit_sms_support_number:
|
||||
'شماره تماس پشتیبانی؛ با کلیک روی «تماس با پشتیبانی» در اپ، تماس تلفنی برقرار میشود. در صفحه واریز، درباره ما و قوانین هم نمایش داده میشود.',
|
||||
charge_notif:
|
||||
'روشن: پس از شارژ کیف توسط ادمین، پیامک به مشتری ارسال میشود.',
|
||||
'روشن: پس از شارژ کیف توسط ادمین، پیامک به مشتری ارسال میشود. خاموش: پیامکی پس از شارژ ارسال نمیشود.',
|
||||
group_sms:
|
||||
'روشن: امکان ارسال پیامک گروهی از پنل ادمین فعال است.',
|
||||
'روشن: امکان ارسال پیامک گروهی از پنل ادمین فعال است. خاموش: ارسال پیامک گروهی غیرفعال است.',
|
||||
profit_gold_api_enabled:
|
||||
'روشن: محاسبه سود طلا از API خارجی فعال است. در اکثر نصبها خاموش میماند.',
|
||||
'روشن: محاسبه سود طلا از API خارجی فعال است. خاموش: محاسبه سود از API خارجی انجام نمیشود (حالت پیشفرض اکثر نصبها).',
|
||||
main_wallet_products:
|
||||
'محصولاتی که موجودیشان از کیف طلای اصلی خوانده میشود. میتوانید چند محصول انتخاب کنید. اگر خالی بماند، پیشفرض محصول ۱۳ است.',
|
||||
unit:
|
||||
@@ -572,7 +615,7 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
zaarbox_price_key:
|
||||
'شناسه ردیف قیمت در پاسخ سرویس تابلو.',
|
||||
payping_fin_sync_enabled:
|
||||
'روشن: پس از واریز یا برداشت، تراکنش به سامانه حسابداری مالی همگام میشود.',
|
||||
'روشن: پس از واریز یا برداشت، تراکنش به سامانه حسابداری مالی همگام میشود. خاموش: تراکنشها به سامانه حسابداری مالی ارسال نمیشوند.',
|
||||
payping_fin_url:
|
||||
'آدرس ثبت تراکنش در سامانه حسابداری مالی.',
|
||||
payping_fin_token:
|
||||
@@ -590,7 +633,7 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
payping_round_toman:
|
||||
'روشن: مبلغ پرداخت به نزدیکترین تومان گرد میشود. خاموش: مبلغ دقیق ریالی.',
|
||||
payping_strict_failed_withdraw:
|
||||
'روشن: در صورت برداشت ناموفق قبلی، تسویه جدید با احتیاط بیشتر انجام میشود.',
|
||||
'روشن: در صورت برداشت ناموفق قبلی، تسویه جدید با احتیاط بیشتر انجام میشود. خاموش: تسویه بهصورت عادی و بدون احتیاط اضافه انجام میشود.',
|
||||
send_to_tahesab:
|
||||
'تنظیم قدیمی — از «سیستم حسابداری» استفاده کنید.',
|
||||
accounting_provider:
|
||||
@@ -641,6 +684,8 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
'آیدی چنل یا گروه تلگرام که قیمت برایش ارسال میشود. چند مقصد را با ویرگول جدا کنید.',
|
||||
price_channel_rubika_to:
|
||||
'آیدی چنل یا گروه روبیکا که قیمت برایش ارسال میشود. چند مقصد را با ویرگول جدا کنید.',
|
||||
price_channel_message_text:
|
||||
'قالب پیامی که با تغییر قیمت به کانال ارسال میشود. متن را بنویسید و متغیرها را با کلیک اضافه کنید؛ پیشنمایش، پیام نهایی را با مقادیر نمونه نشان میدهد. خالی = قالب پیشفرض.',
|
||||
rubika_bot_token:
|
||||
'توکن ربات روبیکا برای ارسال قیمت به کانال. محرمانه نگه دارید.',
|
||||
change_mesghal:
|
||||
@@ -701,6 +746,7 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
|
||||
price_channel_bale_to: 'مقصد ارسال بله',
|
||||
price_channel_telegram_to: 'مقصد ارسال تلگرام',
|
||||
price_channel_rubika_to: 'مقصد ارسال روبیکا',
|
||||
price_channel_message_text: 'قالب متن پیام کانال',
|
||||
rubika_bot_token: 'توکن بات روبیکا',
|
||||
change_mesghal: 'وزن مثقال (گرم)'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* کاتالوگ فیلدهای «قالب متن با متغیر» — همهٔ تنظیماتی که placeholder با % یا ٪ دارند.
|
||||
* هر کلید یک ویرایشگر متن با چیپهای متغیر و پیشنمایش زنده میگیرد.
|
||||
*/
|
||||
|
||||
export interface TemplateVariableItem {
|
||||
/** توکن بدون % (مثلاً «نام») */
|
||||
token: string
|
||||
/** برچسب کوتاه فارسی روی چیپ */
|
||||
label: string
|
||||
/** توضیح کامل (tooltip) */
|
||||
help: string
|
||||
}
|
||||
|
||||
export interface TemplateVariableGroup {
|
||||
title: string
|
||||
color: string
|
||||
items: TemplateVariableItem[]
|
||||
}
|
||||
|
||||
export interface TemplateFieldConfig {
|
||||
/** گروههای متغیر برای پالت چیپها */
|
||||
variableGroups: TemplateVariableGroup[]
|
||||
/** مقدار نمونه هر توکن برای پیشنمایش */
|
||||
sampleValues: Record<string, string>
|
||||
/** قالب پیشفرض (وقتی فیلد خالی است در پیشنمایش نشان داده میشود) */
|
||||
defaultTemplate: string
|
||||
/** عنوان بالای ویرایشگر */
|
||||
editorTitle: string
|
||||
/** عنوان بالای پیشنمایش */
|
||||
previewTitle: string
|
||||
/** placeholder داخل textarea */
|
||||
placeholder: string
|
||||
/** متن راهنمای زیر textarea */
|
||||
hint: string
|
||||
/** توضیح زیر پیشنمایش */
|
||||
legend: string
|
||||
/** تعداد پیشفرض ردیفهای textarea */
|
||||
rows: number
|
||||
}
|
||||
|
||||
function toFa(input: string): string {
|
||||
const fa = '۰۱۲۳۴۵۶۷۸۹'
|
||||
return input.replace(/[0-9]/g, (d) => fa[Number(d)])
|
||||
}
|
||||
|
||||
const PRICE_CHANNEL_DEFAULT = [
|
||||
'سلام 👋',
|
||||
'%عنوان',
|
||||
'📅 %تاریخ 🕐 %ساعت',
|
||||
'',
|
||||
'🔴 خرید گرم: %گرم_خرید',
|
||||
'🟢 فروش گرم: %گرم_فروش',
|
||||
'🔴 خرید مثقال: %مثقال_خرید',
|
||||
'🟢 فروش مثقال: %مثقال_فروش',
|
||||
'',
|
||||
'📞 %شماره',
|
||||
'%آیدی',
|
||||
'%لینک',
|
||||
].join('\n')
|
||||
|
||||
const INVITE_DEFAULT = [
|
||||
'⭐️الان طلا بخر؛ آخر ماه سودشو ببر!',
|
||||
'با خرید طلای آبشده در مقدارهای کم ولی مستمر، سود بیشتری ببر!',
|
||||
'',
|
||||
'این دعوت نامه از طرف "%نام" عزیز برای شما ارسال شده تا شما هم از بازار طلای ما جا نمونید.',
|
||||
'روی لینک زیر کلیک کنید و ثبت نام خود را تکمیل کنید.',
|
||||
'',
|
||||
'%لینک',
|
||||
].join('\n')
|
||||
|
||||
const DEPOSIT_GUIDE_DEFAULT = [
|
||||
'برای شارژ حساب، مبلغ دلخواه را به کارت زیر واریز کنید:',
|
||||
'',
|
||||
'صاحب حساب: %نام',
|
||||
'شماره کارت: %کارت',
|
||||
'شماره شبا: %شبا',
|
||||
'',
|
||||
'پس از واریز، از دکمه «پیام به پشتیبانی» فیش را ارسال کنید.',
|
||||
].join('\n')
|
||||
|
||||
const DEPOSIT_SUPPORT_DEFAULT = 'باسلام، %نام هستم. کدپیگیری و مبلغ واریزی من:'
|
||||
|
||||
export const TEMPLATE_FIELD_CONFIGS: Record<string, TemplateFieldConfig> = {
|
||||
price_channel_message_text: {
|
||||
editorTitle: 'متن قالب پیام',
|
||||
previewTitle: 'پیشنمایش پیام',
|
||||
placeholder: 'متن پیام کانال را بنویسید و متغیرها را از پایین اضافه کنید…',
|
||||
hint: 'روی هر متغیر کلیک کنید تا در محل نشانگر متن اضافه شود. اگر متن را خالی بگذارید، قالب پیشفرض ارسال میشود.',
|
||||
legend:
|
||||
'مقادیر بالا نمونهاند؛ در پیام واقعی، قیمتها زنده و نام/شماره/آیدی/لینک از تنظیمات همین بخش پر میشوند.',
|
||||
rows: 12,
|
||||
defaultTemplate: PRICE_CHANNEL_DEFAULT,
|
||||
variableGroups: [
|
||||
{
|
||||
title: 'قیمت تابلو',
|
||||
color: '#d4af37',
|
||||
items: [
|
||||
{ token: 'گرم_خرید', label: 'خرید هر گرم', help: 'قیمت خرید هر گرم' },
|
||||
{ token: 'گرم_فروش', label: 'فروش هر گرم', help: 'قیمت فروش هر گرم (عدد بزرگ صفحهٔ اصلی اپ)' },
|
||||
{ token: 'مثقال_خرید', label: 'خرید هر مثقال', help: 'قیمت خرید هر مثقال' },
|
||||
{ token: 'مثقال_فروش', label: 'فروش هر مثقال', help: 'قیمت فروش هر مثقال' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'زمان',
|
||||
color: '#3b82f6',
|
||||
items: [
|
||||
{ token: 'تاریخ', label: 'تاریخ شمسی', help: 'تاریخ شمسی امروز' },
|
||||
{ token: 'ساعت', label: 'ساعت', help: 'ساعت لحظهٔ ارسال' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'نام و عنوان',
|
||||
color: '#8b5cf6',
|
||||
items: [
|
||||
{ token: 'نام', label: 'نام فروشگاه', help: 'فقط نام فروشگاه (از تنظیم «نام فارسی»)' },
|
||||
{ token: 'عنوان', label: 'عنوان آماده', help: 'خط آمادهٔ «📊 قیمت لحظهای + نام فروشگاه»' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'تماس (از فیلدهای همین بخش)',
|
||||
color: '#10b981',
|
||||
items: [
|
||||
{ token: 'شماره', label: 'شماره تماس', help: 'از فیلد «شماره تماس در پیام»' },
|
||||
{ token: 'آیدی', label: 'آیدی کانال', help: 'از فیلد «آیدی کانال در پیام»' },
|
||||
{ token: 'لینک', label: 'لینک', help: 'از فیلد «لینک در پیام»' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'اختیاری',
|
||||
color: '#6b7280',
|
||||
items: [
|
||||
{ token: 'محصولات', label: 'لیست محصولات', help: 'چند خط قیمت محصولات فعال' },
|
||||
{ token: 'فوتر', label: 'فوتر آماده', help: 'بلوک آمادهٔ پایان پیام (آیدی/شماره/لینک)' },
|
||||
],
|
||||
},
|
||||
],
|
||||
sampleValues: {
|
||||
گرم_خرید: '۷۱,۲۵۰,۰۰۰',
|
||||
گرم_فروش: '۷۱,۵۰۰,۰۰۰',
|
||||
مثقال_خرید: '۳۰۸,۷۰۰,۰۰۰',
|
||||
مثقال_فروش: '۳۰۹,۸۰۰,۰۰۰',
|
||||
تاریخ: '۱۴۰۴/۴/۲۲',
|
||||
ساعت: toFa('12:30'),
|
||||
نام: 'طلای نمونه',
|
||||
عنوان: '📊 قیمت لحظهای طلای نمونه',
|
||||
شماره: '۰۹۱۲۳۴۵۶۷۸۹',
|
||||
آیدی: '@gold_channel',
|
||||
لینک: 'https://example.com',
|
||||
محصولات: '• سکه تمام: ۹۲۱,۱۰۰,۰۰۰\n• نیم سکه: ۴۷۵,۰۰۰,۰۰۰',
|
||||
فوتر: '📞 ۰۹۱۲۳۴۵۶۷۸۹\n@gold_channel',
|
||||
},
|
||||
},
|
||||
|
||||
invite_link_text: {
|
||||
editorTitle: 'متن پیام دعوت',
|
||||
previewTitle: 'پیشنمایش پیام دعوت',
|
||||
placeholder: 'متن دعوت از دوستان را بنویسید و متغیرها را از پایین اضافه کنید…',
|
||||
hint: 'روی هر متغیر کلیک کنید تا در محل نشانگر اضافه شود. اگر خالی بگذارید، متن پیشفرض اپ استفاده میشود.',
|
||||
legend: 'مقادیر بالا نمونهاند؛ در پیام واقعی «نام» نام کاربر دعوتکننده و «لینک» آدرس ثبتنام با کد معرف اوست.',
|
||||
rows: 10,
|
||||
defaultTemplate: INVITE_DEFAULT,
|
||||
variableGroups: [
|
||||
{
|
||||
title: 'متغیرها',
|
||||
color: '#8b5cf6',
|
||||
items: [
|
||||
{ token: 'نام', label: 'نام کاربر', help: 'نام کامل کاربر دعوتکننده' },
|
||||
{ token: 'لینک', label: 'لینک ثبتنام', help: 'آدرس کامل ثبتنام همراه کد معرف' },
|
||||
],
|
||||
},
|
||||
],
|
||||
sampleValues: {
|
||||
نام: 'علی رضایی',
|
||||
لینک: 'https://gold.example.com/?ref=AB12CD',
|
||||
},
|
||||
},
|
||||
|
||||
support_messenger_text: {
|
||||
editorTitle: 'متن راهنمای واریز',
|
||||
previewTitle: 'پیشنمایش راهنما',
|
||||
placeholder: 'متن راهنمای واریز در مودال شارژ حساب را بنویسید…',
|
||||
hint: 'روی هر متغیر کلیک کنید تا در محل نشانگر اضافه شود. اگر خالی بگذارید، متن پیشفرض اپ استفاده میشود.',
|
||||
legend: 'مقادیر بالا نمونهاند؛ در اپ، نام/کارت/شبا از فیلدهای «نام صاحب حساب واریز»، «شماره کارت واریز» و «شبا واریز» پر میشوند.',
|
||||
rows: 8,
|
||||
defaultTemplate: DEPOSIT_GUIDE_DEFAULT,
|
||||
variableGroups: [
|
||||
{
|
||||
title: 'اطلاعات حساب واریز',
|
||||
color: '#10b981',
|
||||
items: [
|
||||
{ token: 'نام', label: 'صاحب حساب', help: 'نام صاحب حساب (از فیلد «نام صاحب حساب واریز»)' },
|
||||
{ token: 'کارت', label: 'شماره کارت', help: 'شماره کارت (از فیلد «شماره کارت واریز»)' },
|
||||
{ token: 'شبا', label: 'شماره شبا', help: 'شماره شبا (از فیلد «شبا واریز»)' },
|
||||
],
|
||||
},
|
||||
],
|
||||
sampleValues: {
|
||||
نام: 'علی رضایی',
|
||||
کارت: '۶۰۳۷-۹۹۷۹-۱۲۳۴-۵۶۷۸',
|
||||
شبا: 'IR۸۲۰۵۴۰۱۰۲۶۸۰۰۲۰۸۱۷۹۰۹۰۰۲',
|
||||
},
|
||||
},
|
||||
|
||||
deposit_support_message_text: {
|
||||
editorTitle: 'متن پیام به پشتیبانی',
|
||||
previewTitle: 'پیشنمایش پیام',
|
||||
placeholder: 'متنی که با کلیک «پیام به پشتیبانی» برای مشتری آماده میشود…',
|
||||
hint: 'روی متغیر کلیک کنید تا در محل نشانگر اضافه شود. اگر خالی بگذارید، متن پیشفرض اپ استفاده میشود.',
|
||||
legend: 'مقدار بالا نمونه است؛ در اپ «نام» نام مشتری واردشده است (و اگر خالی بود، نام صاحب حساب از تنظیمات).',
|
||||
rows: 5,
|
||||
defaultTemplate: DEPOSIT_SUPPORT_DEFAULT,
|
||||
variableGroups: [
|
||||
{
|
||||
title: 'متغیر',
|
||||
color: '#3b82f6',
|
||||
items: [{ token: 'نام', label: 'نام مشتری', help: 'نام مشتری واردشده (یا نام صاحب حساب)' }],
|
||||
},
|
||||
],
|
||||
sampleValues: {
|
||||
نام: 'علی رضایی',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export function hasTemplateField(key: string | undefined | null): boolean {
|
||||
return !!key && Object.prototype.hasOwnProperty.call(TEMPLATE_FIELD_CONFIGS, key)
|
||||
}
|
||||
|
||||
export function getTemplateFieldConfig(key: string | undefined | null): TemplateFieldConfig | undefined {
|
||||
if (!key) return undefined
|
||||
return TEMPLATE_FIELD_CONFIGS[key]
|
||||
}
|
||||
@@ -58,12 +58,12 @@
|
||||
<BasicInfoItem
|
||||
is-line
|
||||
:title="metalLabel.goldWalletTitle()"
|
||||
:value="selectedCustomer?.wallet?.[0]?.gold ?? '_'"
|
||||
:value="goldBalanceDisplay"
|
||||
/>
|
||||
<BasicInfoItem
|
||||
is-line
|
||||
title="کیف پول ریالی"
|
||||
:value="selectedCustomer?.wallet?.[0]?.cash ?? '_'"
|
||||
:value="cashBalanceDisplay"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -108,6 +108,7 @@ import type { last } from '@/model/price'
|
||||
import { inputRules, inputRulesNumber } from '@/utils/inputRules'
|
||||
import { usePrice } from '@core/utils/usePrice'
|
||||
import { metalLabel } from '@/utils/adminMetalLabels'
|
||||
import { getCashBalance, getGoldBalance } from '@/components/mobile/utils/wallet'
|
||||
|
||||
const props = defineProps<{
|
||||
form: Order
|
||||
@@ -131,6 +132,14 @@ const marketPriceLabel = computed(() => {
|
||||
return usePrice(price)
|
||||
})
|
||||
|
||||
const goldBalanceDisplay = computed(
|
||||
() => getGoldBalance(props.selectedCustomer?.wallet) || '_'
|
||||
)
|
||||
|
||||
const cashBalanceDisplay = computed(
|
||||
() => getCashBalance(props.selectedCustomer?.wallet) || '_'
|
||||
)
|
||||
|
||||
defineExpose({
|
||||
validateFields: () => formRef.value?.validateFields(),
|
||||
resetFields: () => formRef.value?.resetFields()
|
||||
|
||||
@@ -91,6 +91,7 @@ import { createOrder } from '@/api/order'
|
||||
import { isMobile } from '@core/utils/is'
|
||||
import { metalLabel } from '@/utils/adminMetalLabels'
|
||||
import { formatAmountWithUnit } from '@/utils/weightUnit'
|
||||
import { initWalletSettingsFromApi } from '@/utils/config'
|
||||
|
||||
//loading
|
||||
const loading = ref<boolean>(false)
|
||||
@@ -118,6 +119,7 @@ async function getCustomers() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void initWalletSettingsFromApi()
|
||||
getCustomers()
|
||||
})
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<BasicInfoItem title="قیمت" :value="usePrice(product.price || 0)" />
|
||||
<BasicInfoItem title="هزینه خرید" :value="formatCashFee(product.cash_fee_buy, product.cash_fee_buy_is_percent)" />
|
||||
<BasicInfoItem title="هزینه فروش" :value="formatCashFee(product.cash_fee_sell, product.cash_fee_sell_is_percent)" />
|
||||
<BasicInfoItem title="معامله مقداری" :value="product.trade_by_quantity ? 'بله' : 'خیر'" />
|
||||
<BasicInfoItem title="نوع معامله" :value="resolveProductTradeTypeLabel(product.trade_by_quantity)" />
|
||||
<BasicInfoItem title="تاریخ ثبت" :value="product.created_at ? toJalali(product.created_at) : '—'" />
|
||||
</div>
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
import type { Product } from '@/model/product'
|
||||
import BasicActionSheet from '@/components/BasicActionSheet.vue'
|
||||
import CustomTag from '@/components/CustomTag.vue'
|
||||
import { resolveProductTradeTypeLabel } from '@/utils/productCategories'
|
||||
import { isMobile } from '@core/utils/is'
|
||||
import { toJalali } from '@core/utils/useDataTime'
|
||||
import { usePrice } from '@core/utils/usePrice'
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
<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>
|
||||
<a-radio value="3">قیمت اپ</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div v-if="methodOption === '1'" class="grid gap-4 md:grid-cols-3">
|
||||
@@ -131,6 +132,25 @@
|
||||
<a-switch v-model:checked="form.use_last_price_if_zero" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
<div v-if="methodOption === '3'" class="flex flex-col gap-3">
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<a-form-item name="price_offset_percent" class="!mb-0">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="تعدیل قیمت اپ (٪)" :help="appPriceOffsetHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput type="number" v-model:value="form.price_offset_percent" placeholder="مثلاً 5 یا -3" />
|
||||
</a-form-item>
|
||||
<a-form-item name="use_last_price_if_zero" class="!mb-0">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="قیمت صفر از مرجع" :help="useLastPriceIfZeroHelp" size="sm" />
|
||||
</template>
|
||||
<a-switch v-model:checked="form.use_last_price_if_zero" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
<div 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>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-solid border-blue-200 bg-blue-50 px-3 py-2">
|
||||
@@ -239,6 +259,32 @@
|
||||
</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>
|
||||
@@ -340,23 +386,6 @@
|
||||
</template>
|
||||
<a-switch v-model:checked="form.order_limit_enabled" @change="onOrderLimitChange" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="!isMeltedCategory && !editId"
|
||||
name="trade_by_quantity"
|
||||
>
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="معامله مقداری" :help="tradeByQuantityHelp" size="sm" />
|
||||
</template>
|
||||
<a-switch v-model:checked="form.trade_by_quantity" />
|
||||
</a-form-item>
|
||||
<a-form-item v-else-if="!isMeltedCategory && editId">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="نوع معامله" :help="tradeByQuantityHelp" size="sm" />
|
||||
</template>
|
||||
<div class="text-sm leading-6 pt-1">
|
||||
{{ form.trade_by_quantity ? 'معامله مقداری' : 'معامله عادی' }}
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<template v-if="form.order_limit_enabled">
|
||||
<a-form-item name="min_order_cash">
|
||||
@@ -379,13 +408,13 @@
|
||||
</template>
|
||||
<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_grams">
|
||||
<a-form-item v-if="form.has_physical_delivery" name="min_physical_delivery_quantity">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="حداقل گرم برای تحویل" :help="minPhysicalDeliveryHelp" size="sm" />
|
||||
<DashboardLabelWithHelp :label="minPhysicalDeliveryLabel" :help="minPhysicalDeliveryHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput
|
||||
v-model:value="form.min_physical_delivery_grams"
|
||||
addon-before="گرم"
|
||||
v-model:value="form.min_physical_delivery_quantity"
|
||||
:addon-before="physicalDeliveryUnit"
|
||||
type="number"
|
||||
/>
|
||||
</a-form-item>
|
||||
@@ -420,7 +449,7 @@ 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 { usesMeltedPriceEditor } from '@/utils/productCategories'
|
||||
import { resolveProductTradeTypeLabel, usesMeltedPriceEditor } from '@/utils/productCategories'
|
||||
import { computeProductEditorPricePreview } from '@/utils/productEditorPrice'
|
||||
import { resolveTradeWeightUnit, weightFieldLabelForUnit } from '@/utils/weightUnit'
|
||||
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
|
||||
@@ -429,7 +458,8 @@ import dayjs from 'dayjs'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const priceSourceHelp: DashboardHelp = {
|
||||
description: 'دستی: خودتان قیمت را وارد میکنید. اتوماتیک: قیمت از مرجع بیرونی (zaarbox) خوانده میشود.',
|
||||
description:
|
||||
'دستی: خودتان قیمت را وارد میکنید. اتوماتیک: قیمت از مرجع بیرونی (zaarbox) خوانده میشود. قیمت اپ: از قیمت زندهٔ صفحهٔ اصلی اپ بر اساس وزن محاسبه میشود (هر گرم × وزن محصول).',
|
||||
}
|
||||
|
||||
const meltedPriceSourceHelp: DashboardHelp = {
|
||||
@@ -540,17 +570,17 @@ const orderLimitHelp: DashboardHelp = {
|
||||
|
||||
const physicalDeliveryHelp: DashboardHelp = {
|
||||
description:
|
||||
'روشن: این محصول امکان تحویل فیزیکی دارد. خاموش: تحویل فیزیکی ندارد. برای محصولات تعدادی، مقدار به گرم = تعداد × وزن هر واحد سنجیده میشود.',
|
||||
'روشن: این محصول امکان تحویل فیزیکی دارد. خاموش: تحویل فیزیکی ندارد. حداقل بر حسب واحد خود محصول سنجیده میشود: محصول تعدادی → تعداد (عدد)، محصول وزنی → گرم یا کیلوگرم.',
|
||||
}
|
||||
|
||||
const minPhysicalDeliveryHelp: DashboardHelp = {
|
||||
description:
|
||||
'حداقل موجودی (به گرم) که کاربر باید داشته باشد تا تحویل فیزیکی فعال شود؛ در غیر این صورت پیام «چند گرم مانده تا تحویل» نمایش داده میشود.',
|
||||
'حداقل موجودی که کاربر باید داشته باشد تا تحویل فیزیکی فعال شود؛ واحد آن بر اساس نوع محصول است (تعداد برای تعدادی، گرم/کیلوگرم برای وزنی). در غیر این صورت پیام «چند واحد مانده تا تحویل» نمایش داده میشود.',
|
||||
}
|
||||
|
||||
const tradeByQuantityHelp: DashboardHelp = {
|
||||
description:
|
||||
'فرم خرید/فروش با «ارزش کل» و «مقدار». فقط هنگام ساخت محصول قابل تعیین است و بعداً تغییر نمیکند.',
|
||||
'معامله تعدادی: مشتری تعداد قلم میخرد (مثل سکه). معامله مقداری: فرم با «ارزش کل» و «مقدار» (گرم/کیلوگرم). فقط هنگام ساخت محصول قابل تعیین است و بعداً تغییر نمیکند.',
|
||||
}
|
||||
|
||||
const innerFormRef = ref<FormInstance>()
|
||||
@@ -579,6 +609,13 @@ const isStore = ref(Boolean(props.isStore))
|
||||
const tradeWeightUnit = ref('گرم')
|
||||
const isMeltedCategory = computed(() => usesMeltedPriceEditor(props.form))
|
||||
|
||||
const tradeTypeOption = computed({
|
||||
get: () => (props.form.trade_by_quantity ? 'quantity' : 'count'),
|
||||
set: (value: 'quantity' | 'count') => {
|
||||
props.form.trade_by_quantity = value === 'quantity'
|
||||
},
|
||||
})
|
||||
|
||||
const showDirectPriceField = computed(() => !isMeltedCategory.value && props.methodOption === '1')
|
||||
|
||||
const showUnitField = computed(() => isMeltedCategory.value || props.weightOption === '1')
|
||||
@@ -593,6 +630,23 @@ const livePricePreview = computed(() =>
|
||||
})
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.methodOption,
|
||||
(option) => {
|
||||
if (isMeltedCategory.value) {
|
||||
return
|
||||
}
|
||||
if (option === '3') {
|
||||
// قیمت اپ: مرجع خارجی ندارد، متغیر (هر گرم × وزن)
|
||||
props.form.reading_method = null
|
||||
props.form.direct_price = 0
|
||||
props.form.app_price = true
|
||||
} else {
|
||||
props.form.app_price = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.meltedPriceOption,
|
||||
(option) => {
|
||||
@@ -636,6 +690,24 @@ const weightFieldLabel = computed(() => {
|
||||
return weightFieldLabelForUnit(tradeWeightUnit.value)
|
||||
})
|
||||
|
||||
// واحد حداقل تحویل فیزیکی: محصول وزنی → گرم/کیلوگرم، محصول تعدادی → عدد
|
||||
const physicalDeliveryUnit = computed(() => {
|
||||
const productUnit = props.form.unit?.trim()
|
||||
if (productUnit === 'کیلوگرم' || productUnit === 'گرم') {
|
||||
return productUnit
|
||||
}
|
||||
|
||||
if (isMeltedCategory.value) {
|
||||
return tradeWeightUnit.value
|
||||
}
|
||||
|
||||
return 'عدد'
|
||||
})
|
||||
|
||||
const minPhysicalDeliveryLabel = computed(() =>
|
||||
physicalDeliveryUnit.value === 'عدد' ? 'حداقل تعداد برای تحویل' : 'حداقل مقدار برای تحویل'
|
||||
)
|
||||
|
||||
function onOrderLimitChange(checked: boolean) {
|
||||
if (!checked) {
|
||||
props.form.min_order_cash = undefined
|
||||
@@ -645,7 +717,7 @@ function onOrderLimitChange(checked: boolean) {
|
||||
|
||||
function onPhysicalDeliveryChange(checked: boolean) {
|
||||
if (!checked) {
|
||||
props.form.min_physical_delivery_grams = undefined
|
||||
props.form.min_physical_delivery_quantity = undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
<MobileDetailRow label="دسته بندی محصول" :value="row.group_type || '-'" />
|
||||
<MobileDetailRow label="واحد" :value="row.unit || '-'" />
|
||||
<MobileDetailRow label="وزن" :value="row.amount ?? '-'" />
|
||||
<MobileDetailRow label="معامله مقداری" :value="row.trade_by_quantity ? 'بله' : 'خیر'" />
|
||||
<MobileDetailRow label="نوع معامله" :value="resolveProductTradeTypeLabel(row.trade_by_quantity)" />
|
||||
</MobileInfoGrid>
|
||||
|
||||
<BaseButton
|
||||
@@ -88,6 +88,7 @@ import MobileIconButton from '@/components/mobile/MobileIconButton.vue'
|
||||
import MobileInfoGrid from '@/components/mobile/MobileInfoGrid.vue'
|
||||
import MobileSheetHeader from '@/components/mobile/MobileSheetHeader.vue'
|
||||
import MobileStatCard from '@/components/mobile/MobileStatCard.vue'
|
||||
import { resolveProductTradeTypeLabel } from '@/utils/productCategories'
|
||||
import { isMobile } from '@core/utils/is'
|
||||
import { toJalali } from '@core/utils/useDataTime'
|
||||
import { usePrice } from '@core/utils/usePrice'
|
||||
|
||||
@@ -307,7 +307,9 @@ async function edit(record: Product) {
|
||||
/* use table row data */
|
||||
}
|
||||
|
||||
methodOption.value = productData.reading_method ? '2' : '1'
|
||||
methodOption.value = productData.app_price
|
||||
? '3'
|
||||
: (productData.reading_method ? '2' : '1')
|
||||
meltedPriceOption.value = resolveMeltedPriceSource(productData)
|
||||
if (usesMeltedPriceEditor(productData)) {
|
||||
methodOption.value = '1'
|
||||
@@ -336,9 +338,13 @@ async function edit(record: Product) {
|
||||
...productData,
|
||||
show_in_assets: productData.show_in_assets ?? false,
|
||||
trade_by_quantity: productData.trade_by_quantity ?? false,
|
||||
app_price: Boolean(productData.app_price),
|
||||
override_trading_hours: productData.override_trading_hours ?? false,
|
||||
has_physical_delivery: productData.has_physical_delivery ?? false,
|
||||
min_physical_delivery_grams: productData.min_physical_delivery_grams ?? undefined,
|
||||
min_physical_delivery_quantity:
|
||||
productData.min_physical_delivery_quantity ??
|
||||
(productData as { min_physical_delivery_grams?: number | null }).min_physical_delivery_grams ??
|
||||
undefined,
|
||||
cash_fee_buy: parseCashFeeValue(productData.cash_fee_buy),
|
||||
cash_fee_sell: parseCashFeeValue(productData.cash_fee_sell),
|
||||
cash_fee_buy_is_percent: normalizeCashFeeFlags(productData.cash_fee_buy_is_percent),
|
||||
@@ -385,6 +391,7 @@ function resetData() {
|
||||
price_offset_percent: 0,
|
||||
metal_id: 1,
|
||||
direct_price: 0,
|
||||
app_price: false,
|
||||
file_id: undefined,
|
||||
file: undefined,
|
||||
order_limit_enabled: false,
|
||||
@@ -393,7 +400,7 @@ function resetData() {
|
||||
override_trading_hours: false,
|
||||
use_last_price_if_zero: false,
|
||||
has_physical_delivery: false,
|
||||
min_physical_delivery_grams: undefined,
|
||||
min_physical_delivery_quantity: undefined,
|
||||
inventory: 0,
|
||||
min_order_cash: undefined,
|
||||
max_order_cash: undefined,
|
||||
@@ -467,6 +474,8 @@ async function saveProduct() {
|
||||
payload.cash_fee_sell_is_percent = normalizeCashFeeFlags(payload.cash_fee_sell_is_percent)
|
||||
|
||||
if (usesMeltedPriceEditor(payload)) {
|
||||
// ادیتور آبشده/مقداری قیمت اپ را از روی استنتاج تشخیص میدهد، نه ستون app_price
|
||||
payload.app_price = false
|
||||
if (isMeltedTradeGroup(payload.group_type)) {
|
||||
payload.amount = 1
|
||||
if (!editId) {
|
||||
@@ -489,9 +498,18 @@ async function saveProduct() {
|
||||
} else if (methodOption.value === '1') {
|
||||
payload.reading_method = null
|
||||
payload.price_offset_percent = 0
|
||||
payload.app_price = false
|
||||
} else if (methodOption.value === '3') {
|
||||
// قیمت اپ: بدون مرجع خارجی، متغیر (هر گرم × وزن)، قیمت دستی نگه نمیدارد
|
||||
payload.reading_method = null
|
||||
payload.app_price = true
|
||||
payload.direct_price = 0
|
||||
delete payload.price
|
||||
payload.price_offset_percent = Number(payload.price_offset_percent ?? 0)
|
||||
} else {
|
||||
delete payload.price
|
||||
payload.price_offset_percent = Number(payload.price_offset_percent ?? 0)
|
||||
payload.app_price = false
|
||||
}
|
||||
if (weightOption.value === '2') {
|
||||
payload.amount = 0
|
||||
@@ -529,16 +547,17 @@ async function saveProduct() {
|
||||
|
||||
payload.has_physical_delivery = Boolean(payload.has_physical_delivery)
|
||||
if (!payload.has_physical_delivery) {
|
||||
payload.min_physical_delivery_grams = null
|
||||
payload.min_physical_delivery_quantity = null
|
||||
} else {
|
||||
const minGrams = Number(payload.min_physical_delivery_grams)
|
||||
if (!Number.isFinite(minGrams) || minGrams <= 0) {
|
||||
showSubmitError('برای فعال کردن تحویل فیزیکی باید حداقل گرم معتبر وارد شود')
|
||||
const minQuantity = Number(payload.min_physical_delivery_quantity)
|
||||
if (!Number.isFinite(minQuantity) || minQuantity <= 0) {
|
||||
showSubmitError('برای فعال کردن تحویل فیزیکی باید حداقل مقدار معتبر وارد شود')
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
payload.min_physical_delivery_grams = minGrams
|
||||
payload.min_physical_delivery_quantity = minQuantity
|
||||
}
|
||||
delete payload.min_physical_delivery_grams
|
||||
|
||||
if (editId) {
|
||||
delete payload.trade_by_quantity
|
||||
|
||||
@@ -45,6 +45,13 @@ export function usesMeltedPriceEditor(input?: {
|
||||
|
||||
export type MeltedPriceSource = 'app' | 'fixed' | 'reference'
|
||||
|
||||
/** برچسب نوع معامله — فقط مقداری یا تعدادی */
|
||||
export function resolveProductTradeTypeLabel(tradeByQuantity?: boolean | number | null) {
|
||||
return tradeByQuantity === true || Number(tradeByQuantity) === 1
|
||||
? 'معامله مقداری'
|
||||
: 'معامله تعدادی'
|
||||
}
|
||||
|
||||
export function resolveMeltedPriceSource(product?: {
|
||||
reading_method?: string | null
|
||||
direct_price?: number | null
|
||||
|
||||
@@ -95,6 +95,7 @@ export function computeProductEditorPricePreview(input: {
|
||||
form: {
|
||||
group_type?: string
|
||||
price?: number | null
|
||||
amount?: number | null
|
||||
reading_method?: string | null
|
||||
price_offset_percent?: number | null
|
||||
use_last_price_if_zero?: boolean | null
|
||||
@@ -168,6 +169,32 @@ export function computeProductEditorPricePreview(input: {
|
||||
: { status: 'empty', message: 'قیمت وسط را وارد کنید' }
|
||||
}
|
||||
|
||||
if (input.methodOption === '3') {
|
||||
const weight = Number(input.form.amount) > 0 ? Number(input.form.amount) : 1
|
||||
const base = resolveAppBasePrice(input.appLastPrice)
|
||||
if (base > 0) {
|
||||
const perGram = adjustedProductPrice(base, 0, offsetPercent)
|
||||
return {
|
||||
status: 'ready',
|
||||
amount: Math.round(perGram * weight),
|
||||
source: 'قیمت صفحه اصلی اپ × وزن'
|
||||
}
|
||||
}
|
||||
const fallback = resolveStoredFallbackPrice(
|
||||
input.form.price,
|
||||
input.form.use_last_price_if_zero ?? false,
|
||||
input.appLastPrice?.last_historical
|
||||
)
|
||||
if (fallback > 0) {
|
||||
return {
|
||||
status: 'ready',
|
||||
amount: Math.round(adjustedProductPrice(fallback, 0, offsetPercent) * weight),
|
||||
source: 'آخرین قیمت ذخیرهشده × وزن'
|
||||
}
|
||||
}
|
||||
return { status: 'empty', message: 'قیمت اپ در دسترس نیست' }
|
||||
}
|
||||
|
||||
if (!input.form.reading_method) {
|
||||
return { status: 'empty', message: 'مرجع قیمت را انتخاب کنید' }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user