fix ui
This commit is contained in:
+22
-3
@@ -9,6 +9,10 @@
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ant-tabs-rtl .ant-tabs-nav .ant-tabs-tab{
|
||||
margin: 3px !important;
|
||||
}
|
||||
/*::-webkit-scrollbar-track {
|
||||
background-color: #f2f6ff !important;
|
||||
}
|
||||
@@ -29,9 +33,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-form-item {
|
||||
display: block;
|
||||
margin-bottom: 0px;
|
||||
/* قبلتر display:block انعطاف ant-row را میشکست و لیبل/اینپوت افقی میشدند */
|
||||
.ant-form:not(.ant-form-inline) .ant-form-item.ant-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ant-form:not(.ant-form-inline) .ant-form-item > .ant-form-item-label,
|
||||
.ant-form:not(.ant-form-inline) .ant-form-item > .ant-form-item-control {
|
||||
flex: 0 0 auto !important;
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
padding-inline: 0 !important;
|
||||
}
|
||||
|
||||
.ant-form:not(.ant-form-inline) .ant-form-item > .ant-form-item-label {
|
||||
padding-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.ant-select-selector{
|
||||
|
||||
@@ -1,33 +1,84 @@
|
||||
<template>
|
||||
<div :class="[prefixCls]">
|
||||
<div
|
||||
class="w-[96px] h-[96px] overflow-hidden border border-solid border-blue-600 bg-gray-50 rounded-md p-2"
|
||||
>
|
||||
<div :class="[prefixCls]" class="index-image-uploader">
|
||||
<div class="index-image-uploader__card">
|
||||
<div class="index-image-uploader__header">
|
||||
<div class="index-image-uploader__title">تصویر شاخص محصول</div>
|
||||
<p class="index-image-uploader__desc">
|
||||
این تصویر در لیست محصولات و صفحه جزئیات نمایش داده میشود.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<AUpload
|
||||
class="fix-capture"
|
||||
accept="image/*"
|
||||
v-model:file-list="fileList"
|
||||
name="file"
|
||||
list-type="picture-card"
|
||||
:showUploadList="true"
|
||||
:multiple="false"
|
||||
:max-count="1"
|
||||
:action="uploadURL"
|
||||
:headers="fileHeader"
|
||||
:before-upload="beforeUpload"
|
||||
@change="handleChange"
|
||||
@preview="handlePreview"
|
||||
v-if="!currentImageUrl"
|
||||
class="fix-capture index-image-uploader__zone"
|
||||
accept="image/*"
|
||||
name="file"
|
||||
:show-upload-list="false"
|
||||
:multiple="false"
|
||||
:max-count="1"
|
||||
:action="uploadURL"
|
||||
:headers="fileHeader"
|
||||
:before-upload="beforeUpload"
|
||||
:file-list="fileList"
|
||||
@change="handleChange"
|
||||
@update:file-list="onFileListUpdate"
|
||||
>
|
||||
<div v-if="fileList.length === 0">
|
||||
<loading-outlined v-if="loading"></loading-outlined>
|
||||
<BaseIcon v-else icon="vuesax-linear:add" color="#6B7280" />
|
||||
<div class="text-gray-500 text-center text-xs font-normal">تصویر شاخص</div>
|
||||
<div class="index-image-uploader__empty">
|
||||
<div v-if="loading" class="index-image-uploader__loading">
|
||||
<LoadingOutlined />
|
||||
<span>در حال بارگذاری...</span>
|
||||
</div>
|
||||
<template v-else>
|
||||
<BaseIcon icon="vuesax-linear:gallery-add" size="40" class="index-image-uploader__icon" />
|
||||
<span class="index-image-uploader__cta">افزودن تصویر شاخص</span>
|
||||
<span class="index-image-uploader__hint">JPG، PNG یا WEBP — حداکثر ۲۰۰ کیلوبایت</span>
|
||||
<span class="index-image-uploader__hint">نسبت مربعی برای نمایش بهتر توصیه میشود</span>
|
||||
</template>
|
||||
</div>
|
||||
</AUpload>
|
||||
<AModal :visible="previewVisible" :title="previewTitle" :footer="null" @cancel="handleCancel">
|
||||
<img alt="example" style="width: 100%" :src="previewImage" />
|
||||
</AModal>
|
||||
|
||||
<div v-else class="index-image-uploader__filled">
|
||||
<img :src="currentImageUrl" alt="تصویر شاخص" class="index-image-uploader__img" />
|
||||
<div class="index-image-uploader__bar">
|
||||
<button
|
||||
type="button"
|
||||
class="index-image-uploader__btn"
|
||||
@click="openPreview"
|
||||
>
|
||||
پیشنمایش
|
||||
</button>
|
||||
<AUpload
|
||||
class="fix-capture"
|
||||
accept="image/*"
|
||||
name="file"
|
||||
:show-upload-list="false"
|
||||
:multiple="false"
|
||||
:max-count="1"
|
||||
:action="uploadURL"
|
||||
:headers="fileHeader"
|
||||
:before-upload="beforeUpload"
|
||||
:file-list="fileList"
|
||||
@change="handleChange"
|
||||
@update:file-list="onFileListUpdate"
|
||||
>
|
||||
<button type="button" class="index-image-uploader__btn index-image-uploader__btn--primary">
|
||||
تغییر تصویر
|
||||
</button>
|
||||
</AUpload>
|
||||
<button
|
||||
type="button"
|
||||
class="index-image-uploader__btn index-image-uploader__btn--danger"
|
||||
@click="removeImage"
|
||||
>
|
||||
حذف تصویر
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AModal :visible="previewVisible" :title="previewTitle" :footer="null" @cancel="handleCancel">
|
||||
<img alt="preview" class="w-full" :src="previewImage" />
|
||||
</AModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -45,9 +96,9 @@ import { computed, onMounted, ref } from 'vue'
|
||||
const { prefixCls } = usePrefix('index-image-uploader')
|
||||
const previewVisible = ref(false)
|
||||
const previewImage = ref('')
|
||||
const previewTitle = ref('')
|
||||
const previewTitle = ref('تصویر شاخص')
|
||||
|
||||
const loading = ref<boolean>(false)
|
||||
const loading = ref(false)
|
||||
const fileHeader = {
|
||||
Authorization: `Bearer ${getToken()}`
|
||||
}
|
||||
@@ -73,16 +124,30 @@ const fileList = computed({
|
||||
}
|
||||
})
|
||||
|
||||
const handlePreview = async (file: UploadProps['fileList'][number]) => {
|
||||
previewImage.value = file.url || file.thumbUrl || file.src
|
||||
previewVisible.value = true
|
||||
previewTitle.value =
|
||||
file.name || (file.url ? file.url.substring(file.url.lastIndexOf('/') + 1) : 'image')
|
||||
const currentImageUrl = computed(() => {
|
||||
const file = fileList.value?.[0] as { url?: string; thumbUrl?: string; src?: string } | undefined
|
||||
return file?.url || file?.thumbUrl || file?.src || ''
|
||||
})
|
||||
|
||||
function onFileListUpdate(value: UploadProps['fileList']) {
|
||||
fileList.value = value
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
function openPreview() {
|
||||
previewImage.value = currentImageUrl.value
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
previewVisible.value = false
|
||||
previewTitle.value = ''
|
||||
}
|
||||
|
||||
function removeImage() {
|
||||
const uid = (fileList.value?.[0] as { uid?: string } | undefined)?.uid
|
||||
fileList.value = []
|
||||
if (uid) {
|
||||
emits('delete', uid)
|
||||
}
|
||||
}
|
||||
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
@@ -150,28 +215,160 @@ onMounted(() => {
|
||||
@prefix: ~'@{prefixCls}-index-image-uploader';
|
||||
|
||||
.@{prefix} {
|
||||
.ant-upload-rtl.ant-upload.ant-upload-select-picture-card {
|
||||
margin: 0 !important;
|
||||
border: none;
|
||||
}
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.index-image-uploader__card {
|
||||
border: 1px solid var(--admin-border, var(--ant-color-border-secondary, rgba(0, 0, 0, 0.06)));
|
||||
border-radius: 12px;
|
||||
background: var(--admin-bg-muted, var(--ant-color-fill-alter, rgba(0, 0, 0, 0.02)));
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.index-image-uploader__header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.index-image-uploader__title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: var(--admin-text, inherit);
|
||||
}
|
||||
|
||||
.index-image-uploader__desc {
|
||||
margin: 0.35rem 0 0;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.6;
|
||||
color: var(--admin-text-secondary, #6b7280);
|
||||
}
|
||||
|
||||
.index-image-uploader__zone {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
.ant-upload {
|
||||
width: 78px !important;
|
||||
height: 78px !important;
|
||||
background-color: #eff6ff;
|
||||
border: dashed 1px #93c5fd;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.ant-upload-list-picture-card-container {
|
||||
margin: 0;
|
||||
width: 78px !important;
|
||||
height: 78px !important;
|
||||
|
||||
.ant-upload-select {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
margin: 0 !important;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
.ant-upload-list-picture-card .ant-upload-list-item {
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.index-image-uploader__empty {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 12rem;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
padding: 1.5rem 1.25rem;
|
||||
border: 2px dashed color-mix(in srgb, var(--admin-primary, #f5b800) 45%, var(--admin-border, #d9d9d9));
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--admin-bg-muted, #f5f5f5) 85%, transparent);
|
||||
color: var(--admin-text-secondary, #6b7280);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease, background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--admin-primary, #f5b800);
|
||||
background: color-mix(in srgb, var(--admin-primary, #f5b800) 8%, var(--admin-bg-muted, #f5f5f5));
|
||||
}
|
||||
.ant-upload-list-picture-card .ant-upload-list-item-info {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.index-image-uploader__icon {
|
||||
color: var(--admin-primary, #f5b800);
|
||||
opacity: 0.9;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.index-image-uploader__cta {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: var(--admin-text, inherit);
|
||||
}
|
||||
|
||||
.index-image-uploader__hint {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.index-image-uploader__loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.index-image-uploader__filled {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 22rem;
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--admin-border, #d9d9d9);
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.index-image-uploader__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.index-image-uploader__bar {
|
||||
position: absolute;
|
||||
inset: auto 0 0 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.78), transparent);
|
||||
|
||||
.ant-upload {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ant-upload-select {
|
||||
display: inline-block !important;
|
||||
margin: 0 !important;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.index-image-uploader__btn {
|
||||
padding: 0.4rem 0.85rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
color: #fff;
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&--primary:hover {
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
&--danger {
|
||||
border-color: rgba(255, 77, 79, 0.55);
|
||||
color: #ffb3b3;
|
||||
}
|
||||
|
||||
&--danger:hover {
|
||||
background: rgba(255, 77, 79, 0.35);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<article
|
||||
class="rounded-2xl border-base surface shadow-sm"
|
||||
class="mobile-card rounded-2xl border-base surface shadow-sm"
|
||||
:class="[paddingClass, clickable ? 'cursor-pointer' : '']"
|
||||
@click="clickable ? $emit('click') : undefined"
|
||||
>
|
||||
@@ -26,3 +26,9 @@ defineEmits<{
|
||||
|
||||
const paddingClass = computed(() => (props.compact ? 'px-3 py-3' : 'p-4'))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mobile-card {
|
||||
box-shadow: 0 1px 2px var(--admin-shadow);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<div
|
||||
v-if="showEmpty"
|
||||
class="rounded-2xl border border-dashed border-gray-300 bg-gray-50 px-3 py-6 text-center text-sm text-gray-500"
|
||||
class="rounded-2xl border border-dashed border-[color:var(--admin-border-strong)] bg-[color:var(--admin-bg-muted)] px-3 py-6 text-center text-sm text-[color:var(--admin-text-secondary)]"
|
||||
>
|
||||
{{ emptyText }}
|
||||
</div>
|
||||
|
||||
@@ -94,5 +94,29 @@
|
||||
z-index: 1300 !important;
|
||||
}
|
||||
|
||||
// Ant Design در RTL برای لیبل افقی text-align:left میگذارد؛ روی موبایل/عمودی
|
||||
// عنوان فیلدها سمت چپ میافتد. در پنل فارسی همیشه راستچین باشن.
|
||||
.ant-form-item-label,
|
||||
.ant-form-rtl .ant-form-item-label,
|
||||
.ant-form-vertical .ant-form-item-label,
|
||||
.ant-col-24.ant-form-item-label {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
.ant-form-item-label > label {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
// فرمها ستونی: عنوان بالا، کنترل پایین (inline مستثنی)
|
||||
.ant-form:not(.ant-form-inline) .ant-form-item.ant-row {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
flex-wrap: nowrap !important;
|
||||
}
|
||||
|
||||
.ant-form:not(.ant-form-inline) .ant-form-item > .ant-form-item-label,
|
||||
.ant-form:not(.ant-form-inline) .ant-form-item > .ant-form-item-control {
|
||||
flex: 0 0 auto !important;
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<span
|
||||
class="dashboard-label-with-help inline-flex max-w-full items-baseline gap-1.5"
|
||||
class="dashboard-label-with-help inline-flex max-w-full items-baseline justify-start gap-1.5 text-right"
|
||||
:class="[textClass || sizeTextClass, rootClass]"
|
||||
>
|
||||
<span class="leading-normal" :class="labelClass">{{ label }}</span>
|
||||
|
||||
@@ -115,7 +115,7 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
|
||||
title: 'عمومی و برند',
|
||||
icon: 'vuesax-linear:home-2',
|
||||
match: (k) =>
|
||||
/^(name_fa|name_en|app_url|admin_url|apk_download_link|banner_enabled|bace_url|server_url|logo_|title_pwa|title_fav|metal_type|main_metal|unit$|open_register|auto_active_customer|store_status|key$|is_store|group_sms|has_page_earn_money|sms_panel)/.test(
|
||||
/^(name_fa|name_en|app_url|admin_url|apk_download_link|banner_enabled|permision__enabled|bace_url|server_url|logo_|title_pwa|title_fav|metal_type|main_metal|unit$|open_register|auto_active_customer|store_status|key$|is_store|group_sms|has_page_earn_money|sms_panel)/.test(
|
||||
k
|
||||
)
|
||||
},
|
||||
|
||||
@@ -53,7 +53,16 @@ export type SupportMessengerEntryDraft = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const SUPPORT_MESSENGER_TYPES = ['bale', 'telegram', 'whatsapp', 'rubika', 'eitaa', 'soroush'] as const
|
||||
const SUPPORT_MESSENGER_TYPES = [
|
||||
'bale',
|
||||
'telegram',
|
||||
'whatsapp',
|
||||
'rubika',
|
||||
'eitaa',
|
||||
'soroush',
|
||||
'call',
|
||||
'sms',
|
||||
] as const
|
||||
|
||||
export function isSupportMessengerType(value: string | undefined | null): boolean {
|
||||
const type = (value ?? '').trim().toLowerCase()
|
||||
@@ -158,6 +167,9 @@ export function supportMessengerIdPlaceholder(messenger?: string): string {
|
||||
case 'eitaa':
|
||||
case 'soroush':
|
||||
return 'یوزرنیم یا آیدی'
|
||||
case 'call':
|
||||
case 'sms':
|
||||
return 'شماره موبایل، مثلاً 09123456789'
|
||||
default:
|
||||
return 'شناسه / یوزرنیم / شماره'
|
||||
}
|
||||
@@ -256,6 +268,7 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
|
||||
external_order_api_enabled: 'switch',
|
||||
is_store: 'switch',
|
||||
banner_enabled: 'switch',
|
||||
permision__enabled: 'switch',
|
||||
apk_download_link: 'text',
|
||||
node_client_id: 'text',
|
||||
node_shared_secret: 'text',
|
||||
@@ -307,7 +320,9 @@ const SELECT_LABELS_BY_KEY: Record<string, Record<string, string>> = {
|
||||
whatsapp: 'واتساپ',
|
||||
rubika: 'روبیکا',
|
||||
eitaa: 'ایتا',
|
||||
soroush: 'سروش'
|
||||
soroush: 'سروش',
|
||||
call: 'تماس تلفنی',
|
||||
sms: 'پیامک',
|
||||
},
|
||||
price_channel_messengers: {
|
||||
bale: 'بله',
|
||||
@@ -321,7 +336,7 @@ const SELECT_OPTIONS_BY_KEY: Record<string, string> = {
|
||||
price_provider: 'montzar,zaarbox,main_product',
|
||||
payping_gateway_name: 'sepal,',
|
||||
unit: 'گرم,کیلوگرم',
|
||||
support_messenger: 'bale,telegram,whatsapp,rubika,eitaa,soroush',
|
||||
support_messenger: 'bale,telegram,whatsapp,rubika,eitaa,soroush,call,sms',
|
||||
price_channel_messengers: 'bale,telegram,rubika'
|
||||
}
|
||||
|
||||
@@ -484,6 +499,8 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
'نام فارسی برند که در پیامکها، عنوان صفحات و بخشهای عمومی اپ به مشتری نشان داده میشود.',
|
||||
banner_enabled:
|
||||
'روشن: منوی «بنرها» در تنظیمات فعال میشود و میتوانید اسلایدر بالای صفحهٔ اصلی اپ را مدیریت کنید. خاموش: بنرها در اپ نشان داده نمیشوند.',
|
||||
permision__enabled:
|
||||
'روشن: آیتم «مجوز ها» در منوی تنظیمات اپ خانگی (تم جدید) نمایش داده میشود. خاموش: مخفی است.',
|
||||
open_register:
|
||||
'روشن: کاربران جدید میتوانند ثبتنام کنند. خاموش: فقط ورود کاربران قبلی مجاز است.',
|
||||
sms_panel:
|
||||
@@ -571,7 +588,7 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
node_shared_secret:
|
||||
'کلید مشترک امضای درخواستها به سرویس سفارش خارجی. محرمانه نگه دارید.',
|
||||
support_messengers:
|
||||
'لیست پیامرسانهای پشتیبانی. اگر چند مورد باشد، با کلیک دکمه شناور منوی انتخاب باز میشود؛ اگر یک مورد باشد مستقیم باز میشود. تماس تلفنی جداگانه از «شماره تماس پشتیبانی» است.',
|
||||
'فقط مواردی که در این لیست ثبت شوند در منوی شناور پشتیبانی اپ مشتری نمایش داده میشوند. برای تماس تلفنی یا پیامک، نوع «تماس تلفنی» / «پیامک» را با شماره اضافه کنید.',
|
||||
tracking_systems:
|
||||
'لیست JSON سامانههای ردیابی/آنالیتیکس در اپ مشتری. هر مورد: type (gtag، gtm، clarity، custom)، id (کد اندازهگیری یا شناسه پروژه)، enabled (روشن/خاموش). فقط از صفحه دیپلوی قابل ویرایش است.',
|
||||
tracking_systems_enabled:
|
||||
@@ -583,7 +600,7 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
support_messenger_text:
|
||||
'متن راهنمای واریز در مودال شارژ حساب. متن را بنویسید و متغیرها (نام صاحب حساب، شماره کارت، شبا) را با کلیک اضافه کنید؛ پیشنمایش نتیجه را با مقادیر نمونه نشان میدهد. خالی = متن پیشفرض اپ.',
|
||||
deposit_support_message_text:
|
||||
'متن پیام هنگام کلیک «پیام به پشتیبانی» در مودال واریز (SMS و پیامرسان). متن را بنویسید و متغیر «نام مشتری» را با کلیک اضافه کنید؛ پیشنمایش نتیجه را نشان میدهد. خالی = متن پیشفرض اپ.',
|
||||
'متن پیشنویس فقط وقتی کاربر از مودال راهنمای واریز روی «پیام به پشتیبانی» یا SMS میزند. منوی شناور پشتیبانی این متن را نمیگذارد. خالی = متن پیشفرض اپ.',
|
||||
deposit_account_holder:
|
||||
'نام صاحب حساب واریز — در متن راهنمای واریز با ٪نام یا %نام جایگزین میشود. در پیام پشتیبانی فقط وقتی نام مشتری موجود نباشد استفاده میشود.',
|
||||
deposit_card_number:
|
||||
@@ -591,7 +608,7 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
deposit_sheba:
|
||||
'شماره شبا واریز — در متنها با ٪شبا یا %شبا جایگزین میشود.',
|
||||
deposit_sms_support_number:
|
||||
'شماره تماس پشتیبانی؛ با کلیک روی «تماس با پشتیبانی» در اپ، تماس تلفنی برقرار میشود. در صفحه واریز، درباره ما و قوانین هم نمایش داده میشود.',
|
||||
'شماره تماس پشتیبانی برای راهنمای واریز و دکمه پیامک داخل مودال شارژ. برای نمایش در منوی شناور، باید «تماس تلفنی» یا «پیامک» را جداگانه در لیست پیامرسانهای پشتیبانی اضافه کنید.',
|
||||
charge_notif:
|
||||
'روشن: پس از شارژ کیف توسط ادمین، پیامک به مشتری ارسال میشود. خاموش: پیامکی پس از شارژ ارسال نمیشود.',
|
||||
group_sms:
|
||||
@@ -728,6 +745,7 @@ export const KEY_LABEL_FALLBACK: Record<string, string> = {
|
||||
support_messengers: 'پیامرسانهای پشتیبانی',
|
||||
tracking_systems: 'سامانههای ردیابی',
|
||||
tracking_systems_enabled: 'نمایش سامانههای ردیابی',
|
||||
permision__enabled: 'نمایش منوی مجوزها',
|
||||
support_messenger: 'پیامرسان پشتیبانی',
|
||||
support_messenger_id: 'شناسه پشتیبانی',
|
||||
support_messenger_text: 'متن راهنمای واریز',
|
||||
@@ -773,6 +791,7 @@ export const SETTING_LOCKED_KEYS = new Set([
|
||||
'payping_fin_customer_id', 'payping_fin_sync_enabled', 'payping_fin_token', 'payping_fin_url',
|
||||
'payping_gateway_items', 'payping_gateway_name',
|
||||
'payping_round_toman', 'payping_start_url', 'payping_strict_failed_withdraw', 'payping_verify_url',
|
||||
'permision__enabled',
|
||||
'price_extreme_date', 'price_provider', 'profit_gold_api_enabled',
|
||||
'send_to_tahesab', 'send_to_pargar', 'accounting_provider', 'service_charge_percentage',
|
||||
'sse_path',
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
<template>
|
||||
<a-form
|
||||
<a-form
|
||||
:model="form"
|
||||
ref="innerFormRef"
|
||||
autocomplete="off"
|
||||
scroll-to-first-error
|
||||
layout="vertical"
|
||||
label-align="right"
|
||||
class="product-editor-form"
|
||||
>
|
||||
<Collapse class="grid grid-cols-1 gap-2" :activeKey="activeKey" @update:activeKey="$emit('update:activeKey', $event)">
|
||||
<BaseCollapse title="قیمتگذاری" active-key="0" :actives="activeKey">
|
||||
<template #content>
|
||||
<a-tabs
|
||||
class="product-editor-tabs"
|
||||
:activeKey="activeKey"
|
||||
@update:activeKey="$emit('update:activeKey', $event)"
|
||||
>
|
||||
<a-tab-pane key="0" tab="قیمتگذاری" force-render>
|
||||
<div
|
||||
v-if="isMeltedCategory"
|
||||
v-if="usesMeltedPricing"
|
||||
class="product-editor-panel flex flex-col gap-3"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<DashboardLabelWithHelp label="مرجع قیمت" :help="meltedPriceSourceHelp" size="sm" />
|
||||
<div class="product-spec-switch-row">
|
||||
<div class="product-spec-label">
|
||||
<DashboardLabelWithHelp label="مرجع قیمت" :help="meltedPriceSourceHelp" size="sm" />
|
||||
</div>
|
||||
<a-radio-group :value="meltedPriceOption" name="meltedPriceGroup" @update:value="$emit('update:meltedPriceOption', $event)">
|
||||
<a-radio value="app">قیمت اپ</a-radio>
|
||||
<a-radio value="fixed">ثابت</a-radio>
|
||||
@@ -76,9 +83,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isMeltedCategory" class="product-editor-panel flex flex-col gap-4">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<DashboardLabelWithHelp label="دریافت قیمت از" :help="priceSourceHelp" size="sm" />
|
||||
<div v-if="!usesMeltedPricing" class="product-editor-panel flex flex-col gap-4">
|
||||
<div class="product-spec-switch-row">
|
||||
<div class="product-spec-label">
|
||||
<DashboardLabelWithHelp label="دریافت قیمت از" :help="priceSourceHelp" size="sm" />
|
||||
</div>
|
||||
<a-radio-group :value="methodOption" name="radioGroup" @update:value="$emit('update:methodOption', $event)">
|
||||
<a-radio value="1">دستی</a-radio>
|
||||
<a-radio value="2">اتوماتیک</a-radio>
|
||||
@@ -178,61 +187,94 @@
|
||||
</AButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BaseCollapse>
|
||||
</a-tab-pane>
|
||||
|
||||
<BaseCollapse title="مشخصات محصول" active-key="1" :actives="activeKey">
|
||||
<template #content>
|
||||
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<a-form-item name="title" :rules="inputRules()">
|
||||
<a-tab-pane key="1" tab="مشخصات محصول" force-render>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<a-form-item name="title" :rules="inputRules()" class="!mb-0">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="عنوان" :help="titleHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.title" />
|
||||
</a-form-item>
|
||||
<a-form-item name="group_type" :rules="inputRules()">
|
||||
<a-form-item name="group_type" :rules="inputRules()" class="!mb-0">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="دستهبندی" :help="categoryHelp" size="sm" />
|
||||
</template>
|
||||
<BasicSelect v-model:value="form.group_type" :items="categoryItems" valueName="value" keyName="key" />
|
||||
</a-form-item>
|
||||
<a-form-item name="metal_id" :rules="inputRules()">
|
||||
<a-form-item name="metal_id" :rules="inputRules()" class="!mb-0">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="جنس فلز" :help="metalHelp" size="sm" />
|
||||
</template>
|
||||
<BasicSelect v-model:value="form.metal_id" :items="metalItems" valueName="value" keyName="key" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<template v-if="!isMeltedCategory">
|
||||
<div class="md:col-span-3 product-editor-panel">
|
||||
<div class="flex flex-wrap items-center gap-3 mb-3">
|
||||
<DashboardLabelWithHelp label="وزن محصول" :help="weightHelp" size="sm" />
|
||||
<a-radio-group :value="weightOption" name="weightGroup" @update:value="$emit('update:weightOption', $event)">
|
||||
<a-radio value="1">دارد</a-radio>
|
||||
<a-radio value="2">ندارد</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div v-if="weightOption === '1'" class="grid gap-4 md:grid-cols-3">
|
||||
<a-form-item name="amount" :rules="inputRulesNumber('وزن')" class="!mb-0">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp :label="weightFieldLabel" :help="weightAmountHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.amount" type="number" />
|
||||
</a-form-item>
|
||||
<div
|
||||
v-if="form.amount && form.amount < 1 && 0 < form.amount && tradeWeightUnit === 'گرم'"
|
||||
class="flex items-end pb-1 text-sm text-gray-500"
|
||||
>
|
||||
معادل {{ form.amount * 1000 }} سوت
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isMeltedGroup" class="product-editor-panel flex flex-col gap-4">
|
||||
<div class="product-spec-switch-row">
|
||||
<div class="product-spec-label">
|
||||
<DashboardLabelWithHelp label="نوع معامله" :help="tradeByQuantityHelp" size="sm" />
|
||||
</div>
|
||||
</template>
|
||||
<a-radio-group
|
||||
v-if="!editId"
|
||||
v-model:value="tradeTypeOption"
|
||||
name="tradeTypeGroup"
|
||||
>
|
||||
<a-radio value="count">معامله تعدادی</a-radio>
|
||||
<a-radio value="quantity">معامله مقداری</a-radio>
|
||||
</a-radio-group>
|
||||
<span
|
||||
v-else
|
||||
class="inline-flex items-center rounded-full px-3 py-1 text-sm font-600"
|
||||
:class="form.trade_by_quantity ? 'bg-emerald-100 text-emerald-800' : 'bg-sky-100 text-sky-800'"
|
||||
>
|
||||
{{ resolveProductTradeTypeLabel(form.trade_by_quantity) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isQuantityTrade"
|
||||
class="rounded-lg border border-solid border-amber-200 bg-amber-50 px-3 py-2 text-xs leading-6 text-amber-800"
|
||||
>
|
||||
معامله مقداری: مشتری بر اساس مقدار (گرم/کیلوگرم) خرید میکند. قیمتگذاری این محصول در تب «قیمتگذاری» بهصورت مقداری تنظیم میشود.
|
||||
</div>
|
||||
|
||||
<div class="product-spec-switch-row">
|
||||
<div class="product-spec-label">
|
||||
<DashboardLabelWithHelp label="وزن محصول" :help="weightHelp" size="sm" />
|
||||
</div>
|
||||
<a-radio-group :value="weightOption" name="weightGroup" @update:value="$emit('update:weightOption', $event)">
|
||||
<a-radio value="1">دارد</a-radio>
|
||||
<a-radio value="2">ندارد</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
<div v-if="weightOption === '1'" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<a-form-item name="amount" :rules="inputRulesNumber('وزن')" class="!mb-0">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp :label="weightFieldLabel" :help="weightAmountHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.amount" type="number" />
|
||||
</a-form-item>
|
||||
<div
|
||||
v-if="form.amount && form.amount < 1 && 0 < form.amount && tradeWeightUnit === 'گرم'"
|
||||
class="flex items-end pb-1 text-sm text-gray-500"
|
||||
>
|
||||
معادل {{ form.amount * 1000 }} سوت
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showUnitField || showDirectPriceField || (isStore && !isMeltedGroup)"
|
||||
class="grid gap-4 md:grid-cols-2 xl:grid-cols-3"
|
||||
>
|
||||
<a-form-item
|
||||
v-if="isMeltedCategory || weightOption === '1'"
|
||||
v-if="showUnitField"
|
||||
name="unit"
|
||||
:rules="showUnitField ? inputRules() : []"
|
||||
:rules="inputRules()"
|
||||
class="!mb-0"
|
||||
>
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="واحد" :help="unitHelp" size="sm" />
|
||||
@@ -243,6 +285,7 @@
|
||||
v-if="showDirectPriceField"
|
||||
name="direct_price"
|
||||
:rules="inputRules()"
|
||||
class="!mb-0"
|
||||
>
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="نوع قیمت" :help="directPriceHelp" size="sm" />
|
||||
@@ -250,109 +293,93 @@
|
||||
<BasicSelect v-model:value="form.direct_price" :items="priceItems" valueName="value" keyName="key" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="isStore && !isMeltedCategory"
|
||||
v-if="isStore && !isMeltedGroup"
|
||||
name="inventory"
|
||||
:rules="inputRules()"
|
||||
class="!mb-0"
|
||||
>
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="تعداد موجودی" :help="inventoryHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.inventory" type="number" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
v-if="!isMeltedCategory && !editId"
|
||||
name="trade_by_quantity"
|
||||
>
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="نوع معامله" :help="tradeByQuantityHelp" size="sm" />
|
||||
</template>
|
||||
<a-radio-group v-model:value="tradeTypeOption" name="tradeTypeGroup">
|
||||
<a-radio value="count">معامله تعدادی</a-radio>
|
||||
<a-radio value="quantity">معامله مقداری</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item v-else-if="!isMeltedCategory && editId">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="نوع معامله" :help="tradeByQuantityHelp" size="sm" />
|
||||
</template>
|
||||
<div class="pt-1">
|
||||
<span
|
||||
class="inline-flex items-center rounded-full px-3 py-1 text-sm font-600"
|
||||
:class="form.trade_by_quantity ? 'bg-emerald-100 text-emerald-800' : 'bg-sky-100 text-sky-800'"
|
||||
>
|
||||
{{ resolveProductTradeTypeLabel(form.trade_by_quantity) }}
|
||||
</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</template>
|
||||
</BaseCollapse>
|
||||
|
||||
<BaseCollapse title="کارمزد و محدودیتها" active-key="2" :actives="activeKey">
|
||||
<template #content>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<a-form-item name="cash_fee_buy" :rules="requiredPriceRules('هزینه نقدی موقع خرید')">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="هزینه خرید" :help="cashFeeBuyHelp" size="sm" />
|
||||
</template>
|
||||
<div class="product-cash-fee-field">
|
||||
<a-radio-group
|
||||
v-model:value="form.cash_fee_buy_is_percent"
|
||||
button-style="solid"
|
||||
class="product-cash-fee-mode"
|
||||
>
|
||||
<a-radio-button :value="false">نقدی (ریال)</a-radio-button>
|
||||
<a-radio-button :value="true">درصدی (%)</a-radio-button>
|
||||
</a-radio-group>
|
||||
<BasicInput
|
||||
v-model:value="form.cash_fee_buy"
|
||||
:addon-before="form.cash_fee_buy_is_percent ? '%' : 'ریال'"
|
||||
:type="form.cash_fee_buy_is_percent ? 'number' : 'price'"
|
||||
:step="form.cash_fee_buy_is_percent ? 'any' : undefined"
|
||||
size="large"
|
||||
class="w-full product-cash-fee-input"
|
||||
:placeHolder="form.cash_fee_buy_is_percent ? 'مثلاً 0.5 یا 5' : 'مثلاً 100000'"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item name="cash_fee_sell" :rules="requiredPriceRules('هزینه نقدی موقع فروش')">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="هزینه فروش" :help="cashFeeSellHelp" size="sm" />
|
||||
</template>
|
||||
<div class="product-cash-fee-field">
|
||||
<a-radio-group
|
||||
v-model:value="form.cash_fee_sell_is_percent"
|
||||
button-style="solid"
|
||||
class="product-cash-fee-mode"
|
||||
>
|
||||
<a-radio-button :value="false">نقدی (ریال)</a-radio-button>
|
||||
<a-radio-button :value="true">درصدی (%)</a-radio-button>
|
||||
</a-radio-group>
|
||||
<BasicInput
|
||||
v-model:value="form.cash_fee_sell"
|
||||
:addon-before="form.cash_fee_sell_is_percent ? '%' : 'ریال'"
|
||||
:type="form.cash_fee_sell_is_percent ? 'number' : 'price'"
|
||||
:step="form.cash_fee_sell_is_percent ? 'any' : undefined"
|
||||
size="large"
|
||||
class="w-full product-cash-fee-input"
|
||||
:placeHolder="form.cash_fee_sell_is_percent ? 'مثلاً 0.5 یا 10' : 'مثلاً 200000'"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item name="show_in_assets">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="نمایش در داراییها" :help="showInAssetsHelp" size="sm" />
|
||||
</template>
|
||||
<div class="product-editor-panel product-spec-switch-row">
|
||||
<div class="product-spec-label">
|
||||
<DashboardLabelWithHelp label="نمایش در داراییها" :help="showInAssetsHelp" size="sm" />
|
||||
</div>
|
||||
<a-form-item name="show_in_assets" class="!mb-0">
|
||||
<a-switch v-model:checked="form.show_in_assets" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-form-item name="override_trading_hours">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="ساعت کاری اختصاصی" :help="overrideTradingHoursHelp" size="sm" />
|
||||
</template>
|
||||
<a-tab-pane key="2" tab="اختلاف خرید و فروش" force-render>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<a-form-item name="cash_fee_buy" :rules="requiredPriceRules('هزینه نقدی موقع خرید')">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="هزینه خرید" :help="cashFeeBuyHelp" size="sm" />
|
||||
</template>
|
||||
<div class="product-cash-fee-field">
|
||||
<a-radio-group
|
||||
v-model:value="form.cash_fee_buy_is_percent"
|
||||
button-style="solid"
|
||||
class="product-cash-fee-mode"
|
||||
>
|
||||
<a-radio-button :value="false">نقدی (ریال)</a-radio-button>
|
||||
<a-radio-button :value="true">درصدی (%)</a-radio-button>
|
||||
</a-radio-group>
|
||||
<BasicInput
|
||||
v-model:value="form.cash_fee_buy"
|
||||
:addon-before="form.cash_fee_buy_is_percent ? '%' : 'ریال'"
|
||||
:type="form.cash_fee_buy_is_percent ? 'number' : 'price'"
|
||||
:step="form.cash_fee_buy_is_percent ? 'any' : undefined"
|
||||
size="large"
|
||||
class="w-full product-cash-fee-input"
|
||||
:placeHolder="form.cash_fee_buy_is_percent ? 'مثلاً 0.5 یا 5' : 'مثلاً 100000'"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item name="cash_fee_sell" :rules="requiredPriceRules('هزینه نقدی موقع فروش')">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="هزینه فروش" :help="cashFeeSellHelp" size="sm" />
|
||||
</template>
|
||||
<div class="product-cash-fee-field">
|
||||
<a-radio-group
|
||||
v-model:value="form.cash_fee_sell_is_percent"
|
||||
button-style="solid"
|
||||
class="product-cash-fee-mode"
|
||||
>
|
||||
<a-radio-button :value="false">نقدی (ریال)</a-radio-button>
|
||||
<a-radio-button :value="true">درصدی (%)</a-radio-button>
|
||||
</a-radio-group>
|
||||
<BasicInput
|
||||
v-model:value="form.cash_fee_sell"
|
||||
:addon-before="form.cash_fee_sell_is_percent ? '%' : 'ریال'"
|
||||
:type="form.cash_fee_sell_is_percent ? 'number' : 'price'"
|
||||
:step="form.cash_fee_sell_is_percent ? 'any' : undefined"
|
||||
size="large"
|
||||
class="w-full product-cash-fee-input"
|
||||
:placeHolder="form.cash_fee_sell_is_percent ? 'مثلاً 0.5 یا 10' : 'مثلاً 200000'"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="3" tab="ساعت کاری" force-render>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="product-editor-panel product-spec-switch-row">
|
||||
<div class="product-spec-label">
|
||||
<DashboardLabelWithHelp label="ساعت کاری اختصاصی" :help="overrideTradingHoursHelp" size="sm" />
|
||||
</div>
|
||||
<a-form-item name="override_trading_hours" class="!mb-0">
|
||||
<a-switch v-model:checked="form.override_trading_hours" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<a-form-item name="auto_open_at">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="زمان شروع" :help="autoOpenHelp" size="sm" />
|
||||
@@ -379,36 +406,49 @@
|
||||
class="w-full"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-form-item name="order_limit_enabled">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="محدودیت مبلغ سفارش" :help="orderLimitHelp" size="sm" />
|
||||
</template>
|
||||
<a-tab-pane key="4" tab="محدودیت سفارش" force-render>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="product-editor-panel product-spec-switch-row">
|
||||
<div class="product-spec-label">
|
||||
<DashboardLabelWithHelp label="محدودیت مبلغ سفارش" :help="orderLimitHelp" size="sm" />
|
||||
</div>
|
||||
<a-form-item name="order_limit_enabled" class="!mb-0">
|
||||
<a-switch v-model:checked="form.order_limit_enabled" @change="onOrderLimitChange" />
|
||||
</a-form-item>
|
||||
|
||||
<template v-if="form.order_limit_enabled">
|
||||
<a-form-item name="min_order_cash">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="حداقل مبلغ سفارش" :help="minOrderHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.min_order_cash" addon-before="ریال" type="price" />
|
||||
</a-form-item>
|
||||
<a-form-item name="max_order_cash">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="حداکثر مبلغ سفارش" :help="maxOrderHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.max_order_cash" addon-before="ریال" type="price" />
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<a-form-item name="has_physical_delivery">
|
||||
</div>
|
||||
<div v-if="form.order_limit_enabled" class="grid gap-4 md:grid-cols-2">
|
||||
<a-form-item name="min_order_cash">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="تحویل فیزیکی" :help="physicalDeliveryHelp" size="sm" />
|
||||
<DashboardLabelWithHelp label="حداقل مبلغ سفارش" :help="minOrderHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.min_order_cash" addon-before="ریال" type="price" />
|
||||
</a-form-item>
|
||||
<a-form-item name="max_order_cash">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="حداکثر مبلغ سفارش" :help="maxOrderHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput v-model:value="form.max_order_cash" addon-before="ریال" type="price" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="5" tab="تحویل فیزیکی" force-render>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="product-editor-panel product-spec-switch-row">
|
||||
<div class="product-spec-label">
|
||||
<DashboardLabelWithHelp label="تحویل فیزیکی" :help="physicalDeliveryHelp" size="sm" />
|
||||
</div>
|
||||
<a-form-item name="has_physical_delivery" class="!mb-0">
|
||||
<a-switch v-model:checked="form.has_physical_delivery" @change="onPhysicalDeliveryChange" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="form.has_physical_delivery" name="min_physical_delivery_quantity">
|
||||
</div>
|
||||
<div v-if="form.has_physical_delivery" class="grid gap-4 md:grid-cols-2">
|
||||
<a-form-item name="min_physical_delivery_quantity">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp :label="minPhysicalDeliveryLabel" :help="minPhysicalDeliveryHelp" size="sm" />
|
||||
</template>
|
||||
@@ -419,11 +459,10 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</template>
|
||||
</BaseCollapse>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<BaseCollapse title="تصاویر محصول" active-key="3" :actives="activeKey">
|
||||
<template #content>
|
||||
<a-tab-pane key="6" tab="تصاویر محصول" force-render>
|
||||
<IndexImageUploader
|
||||
:key="editId ?? 'new-product'"
|
||||
:file-list="productIndexImage"
|
||||
@@ -431,25 +470,23 @@
|
||||
@ok="(id, res) => $emit('uploadedIndexFile', id, res)"
|
||||
@delete="$emit('deleteIndexFile')"
|
||||
/>
|
||||
</template>
|
||||
</BaseCollapse>
|
||||
</Collapse>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import { Collapse, TimePicker } from 'ant-design-vue'
|
||||
import { TimePicker } from 'ant-design-vue'
|
||||
import { inputRules, inputRulesNumber, requiredPriceRules } from '@/utils/inputRules'
|
||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||
import BaseCollapse from '@/components/BaseCollapse.vue'
|
||||
import IndexImageUploader from '@/components/ProductUploader/IndexImageUploader.vue'
|
||||
import { getConfiguration } from '@/api/configuration'
|
||||
import { isSwitchOn } from '@/pages/menuPages/configuration/settingTypes'
|
||||
import type { last } from '@/model/price'
|
||||
import type { MethodePrices } from '@/model/product'
|
||||
import { usePrice } from '@core/utils/usePrice'
|
||||
import { resolveProductTradeTypeLabel, usesMeltedPriceEditor } from '@/utils/productCategories'
|
||||
import { isMeltedTradeGroup, resolveProductTradeTypeLabel, usesMeltedPriceEditor } from '@/utils/productCategories'
|
||||
import { computeProductEditorPricePreview } from '@/utils/productEditorPrice'
|
||||
import { resolveTradeWeightUnit, weightFieldLabelForUnit } from '@/utils/weightUnit'
|
||||
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
|
||||
@@ -590,7 +627,7 @@ const props = defineProps<{
|
||||
methodOption: string
|
||||
weightOption: string
|
||||
meltedPriceOption: string
|
||||
activeKey: string[]
|
||||
activeKey: string
|
||||
pricesList: Array<{ id: string; name: string }>
|
||||
pricesData: MethodePrices[]
|
||||
appLastPrice: (last & { price?: number }) | null
|
||||
@@ -607,18 +644,24 @@ const props = defineProps<{
|
||||
const isStore = ref(Boolean(props.isStore))
|
||||
|
||||
const tradeWeightUnit = ref('گرم')
|
||||
const isMeltedCategory = computed(() => usesMeltedPriceEditor(props.form))
|
||||
/** دسته آبشده/کاتد — ساختار فرم ثابت است */
|
||||
const isMeltedGroup = computed(() => isMeltedTradeGroup(props.form.group_type))
|
||||
/** قیمتگذاری مقداری: آبشده/کاتد یا معامله مقداری */
|
||||
const usesMeltedPricing = computed(() => usesMeltedPriceEditor(props.form))
|
||||
const isQuantityTrade = computed(
|
||||
() => props.form.trade_by_quantity === true || Number(props.form.trade_by_quantity) === 1
|
||||
)
|
||||
|
||||
const tradeTypeOption = computed({
|
||||
get: () => (props.form.trade_by_quantity ? 'quantity' : 'count'),
|
||||
get: () => (isQuantityTrade.value ? 'quantity' : 'count'),
|
||||
set: (value: 'quantity' | 'count') => {
|
||||
props.form.trade_by_quantity = value === 'quantity'
|
||||
},
|
||||
})
|
||||
|
||||
const showDirectPriceField = computed(() => !isMeltedCategory.value && props.methodOption === '1')
|
||||
const showDirectPriceField = computed(() => !usesMeltedPricing.value && props.methodOption === '1')
|
||||
|
||||
const showUnitField = computed(() => isMeltedCategory.value || props.weightOption === '1')
|
||||
const showUnitField = computed(() => usesMeltedPricing.value || props.weightOption === '1')
|
||||
|
||||
const livePricePreview = computed(() =>
|
||||
computeProductEditorPricePreview({
|
||||
@@ -633,7 +676,7 @@ const livePricePreview = computed(() =>
|
||||
watch(
|
||||
() => props.methodOption,
|
||||
(option) => {
|
||||
if (isMeltedCategory.value) {
|
||||
if (usesMeltedPricing.value) {
|
||||
return
|
||||
}
|
||||
if (option === '3') {
|
||||
@@ -697,7 +740,7 @@ const physicalDeliveryUnit = computed(() => {
|
||||
return productUnit
|
||||
}
|
||||
|
||||
if (isMeltedCategory.value) {
|
||||
if (usesMeltedPricing.value) {
|
||||
return tradeWeightUnit.value
|
||||
}
|
||||
|
||||
@@ -727,7 +770,7 @@ defineEmits<{
|
||||
(e: 'update:methodOption', value: string): void
|
||||
(e: 'update:weightOption', value: string): void
|
||||
(e: 'update:meltedPriceOption', value: string): void
|
||||
(e: 'update:activeKey', value: string[]): void
|
||||
(e: 'update:activeKey', value: string): void
|
||||
(e: 'update:productIndexImage', value: any[]): void
|
||||
(e: 'uploadedIndexFile', id: number, response?: Record<string, any>): void
|
||||
(e: 'deleteIndexFile'): void
|
||||
@@ -749,11 +792,25 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.product-editor-tabs :deep(.ant-tabs-nav) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.product-editor-tabs :deep(.ant-tabs-tab) {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.product-editor-panel {
|
||||
border: 1px solid var(--ant-color-border-secondary, rgba(0, 0, 0, 0.06));
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.product-spec-switch-row {
|
||||
@apply flex flex-col items-stretch gap-2;
|
||||
}
|
||||
|
||||
.product-spec-label {
|
||||
@apply w-full shrink-0 text-right;
|
||||
}
|
||||
|
||||
.product-editor-form :deep(.ant-form-item-label > label) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<span class="inline-block h-2.5 w-2.5 shrink-0 rounded-full" :class="statusDotClass" />
|
||||
<button
|
||||
type="button"
|
||||
class="truncate border-none bg-transparent p-0 text-right text-sm font-700 text-gray-900 cursor-pointer"
|
||||
class="product-mobile-item__title truncate border-none bg-transparent p-0 text-right text-sm font-700 cursor-pointer"
|
||||
@click="$emit('open', item)"
|
||||
>
|
||||
{{ item.title || '-' }}
|
||||
@@ -15,8 +15,8 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex items-center justify-between gap-2">
|
||||
<span class="text-xs text-gray-500">کد محصول: {{ item.id ?? '-' }}</span>
|
||||
<span class="text-sm font-600 text-gray-700">{{ priceLabel }}</span>
|
||||
<span class="product-mobile-item__meta text-xs">کد محصول: {{ item.id ?? '-' }}</span>
|
||||
<span class="product-mobile-item__price text-sm font-600">{{ priceLabel }}</span>
|
||||
</div>
|
||||
</MobileCard>
|
||||
</template>
|
||||
@@ -39,3 +39,17 @@ defineEmits<{
|
||||
const statusDotClass = computed(() => (props.item.active ? 'bg-green-500' : 'bg-red-400'))
|
||||
const priceLabel = computed(() => (props.item.price ? usePrice(props.item.price) : '-'))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.product-mobile-item__title {
|
||||
color: var(--admin-text);
|
||||
}
|
||||
|
||||
.product-mobile-item__meta {
|
||||
color: var(--admin-text-secondary);
|
||||
}
|
||||
|
||||
.product-mobile-item__price {
|
||||
color: var(--admin-text-muted);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
@cancel="closeProductModal"
|
||||
@close="closeProductModal"
|
||||
>
|
||||
<div v-if="isMobile" class="mb-3 text-base font-700">{{ `${editId ? 'ویرایش' : 'افزودن'} محصول` }}</div>
|
||||
<a-alert
|
||||
v-if="submitError"
|
||||
type="error"
|
||||
@@ -76,7 +75,6 @@ import type { MethodePrices, Product } from '@/model/product'
|
||||
import ProductEditorFormBody from '@/pages/menuPages/products/base/components/ProductEditorFormBody.vue'
|
||||
import { validateProductSubmitInput } from '@/pages/menuPages/products/base/productFormValidation'
|
||||
import tbl from './components/table.vue'
|
||||
import { isMobile } from '@core/utils/is'
|
||||
import { usePrice } from '@core/utils/usePrice'
|
||||
import { referenceProviderTotal } from '@/utils/productEditorPrice'
|
||||
import {
|
||||
@@ -110,7 +108,7 @@ const methodOption = ref('1')
|
||||
const weightOption = ref('1')
|
||||
const meltedPriceOption = ref('app')
|
||||
const tradeWeightUnit = ref('گرم')
|
||||
const activeKey_ = ref(['0'])
|
||||
const activeKey_ = ref('0')
|
||||
const ProductIndexImage = ref<Array<FileList>>([])
|
||||
const addUserVisible = ref(false)
|
||||
let editId: number | undefined
|
||||
@@ -414,6 +412,38 @@ function showSubmitError(text: string) {
|
||||
message.error(text, 4)
|
||||
}
|
||||
|
||||
const PRICING_FIELDS = new Set([
|
||||
'price',
|
||||
'reading_method',
|
||||
'price_offset_percent',
|
||||
'use_last_price_if_zero',
|
||||
])
|
||||
const DETAILS_FIELDS = new Set([
|
||||
'title',
|
||||
'group_type',
|
||||
'metal_id',
|
||||
'amount',
|
||||
'unit',
|
||||
'direct_price',
|
||||
'inventory',
|
||||
'trade_by_quantity',
|
||||
'show_in_assets',
|
||||
])
|
||||
const FEES_FIELDS = new Set(['cash_fee_buy', 'cash_fee_sell'])
|
||||
const HOURS_FIELDS = new Set(['override_trading_hours', 'auto_open_at', 'auto_close_at'])
|
||||
const ORDER_FIELDS = new Set(['order_limit_enabled', 'min_order_cash', 'max_order_cash'])
|
||||
const DELIVERY_FIELDS = new Set(['has_physical_delivery', 'min_physical_delivery_quantity'])
|
||||
|
||||
function tabKeyForField(fieldName: string): string {
|
||||
if (PRICING_FIELDS.has(fieldName)) return '0'
|
||||
if (DETAILS_FIELDS.has(fieldName)) return '1'
|
||||
if (FEES_FIELDS.has(fieldName)) return '2'
|
||||
if (HOURS_FIELDS.has(fieldName)) return '3'
|
||||
if (ORDER_FIELDS.has(fieldName)) return '4'
|
||||
if (DELIVERY_FIELDS.has(fieldName)) return '5'
|
||||
return '6'
|
||||
}
|
||||
|
||||
async function submitProduct() {
|
||||
try {
|
||||
await saveProduct()
|
||||
@@ -425,13 +455,6 @@ async function submitProduct() {
|
||||
}
|
||||
|
||||
async function saveProduct() {
|
||||
const formSectionKeys = ['0', '1', '2']
|
||||
const nextKeys = [...new Set([...formSectionKeys, ...activeKey_.value])]
|
||||
if (nextKeys.length !== activeKey_.value.length) {
|
||||
activeKey_.value = nextKeys
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
const businessError = validateProductSubmitInput({
|
||||
form: form.value as Record<string, unknown>,
|
||||
methodOption: methodOption.value,
|
||||
@@ -452,8 +475,12 @@ async function saveProduct() {
|
||||
try {
|
||||
await editorFormRef.value.validateFields()
|
||||
} catch (err: unknown) {
|
||||
const first = (err as { errorFields?: Array<{ errors?: string[] }> })?.errorFields?.[0]?.errors?.[0]
|
||||
showSubmitError(first || 'لطفاً فیلدهای الزامی را تکمیل کنید')
|
||||
const errorFields = (err as { errorFields?: Array<{ name?: Array<string | number>; errors?: string[] }> })?.errorFields
|
||||
const firstField = errorFields?.[0]
|
||||
const fieldName = String(firstField?.name?.[0] ?? '')
|
||||
activeKey_.value = tabKeyForField(fieldName)
|
||||
await nextTick()
|
||||
showSubmitError(firstField?.errors?.[0] || 'لطفاً فیلدهای الزامی را تکمیل کنید')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -463,6 +490,7 @@ async function saveProduct() {
|
||||
const payload = cloneDeep(form.value)
|
||||
const limitError = validateProductOrderLimitForm(payload)
|
||||
if (limitError) {
|
||||
activeKey_.value = '4'
|
||||
showSubmitError(limitError)
|
||||
loading.value = false
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user