admin big changes
This commit is contained in:
@@ -3,24 +3,13 @@
|
||||
class="setting-card"
|
||||
:class="{
|
||||
'setting-card--readonly': readonly,
|
||||
'setting-card--saving': saving,
|
||||
'setting-card--inactive': !isActive && fieldType !== 'switch'
|
||||
'setting-card--saving': saving
|
||||
}"
|
||||
>
|
||||
<div class="setting-card__head">
|
||||
<div class="setting-card__title-row">
|
||||
<span v-if="readonly" class="setting-card__lock" title="فقطخواندنی">🔒</span>
|
||||
<span class="setting-card__title">{{ title }}</span>
|
||||
<a-switch
|
||||
v-if="fieldType !== 'switch'"
|
||||
:checked="isActive"
|
||||
size="small"
|
||||
class="setting-card__active-switch"
|
||||
@change="onActiveChange"
|
||||
>
|
||||
<template #checkedChildren>فعال</template>
|
||||
<template #unCheckedChildren>غیرفعال</template>
|
||||
</a-switch>
|
||||
</div>
|
||||
<div class="setting-card__key">کلید: {{ settingKey }}</div>
|
||||
<div v-if="help" class="setting-card__help">{{ help }}</div>
|
||||
@@ -62,7 +51,6 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:value': [value: string]
|
||||
'update:isActive': [value: boolean]
|
||||
}>()
|
||||
|
||||
const settingKey = computed(() => props.row.key ?? '')
|
||||
@@ -74,12 +62,6 @@ const fieldType = computed<SettingFieldType>(() =>
|
||||
resolveSettingFieldType({ key: props.row.key, type: props.row.type })
|
||||
)
|
||||
|
||||
const isActive = computed(() => props.row.is_active !== false && props.row.is_active !== 0)
|
||||
|
||||
function onActiveChange(checked: boolean) {
|
||||
emit('update:isActive', checked)
|
||||
}
|
||||
|
||||
const help = computed(() => helpText(props.row))
|
||||
</script>
|
||||
|
||||
@@ -108,11 +90,6 @@ const help = computed(() => helpText(props.row))
|
||||
border-color: #91caff;
|
||||
}
|
||||
|
||||
&--inactive {
|
||||
opacity: 0.72;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
&__head {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -123,11 +100,6 @@ const help = computed(() => helpText(props.row))
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__active-switch {
|
||||
margin-right: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__lock {
|
||||
flex-shrink: 0;
|
||||
font-size: 13px;
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
:disabled="disabled"
|
||||
@change="onSwitchChange"
|
||||
>
|
||||
<template #checkedChildren>فعال</template>
|
||||
<template #unCheckedChildren>غیرفعال</template>
|
||||
<template #checkedChildren>{{ labels.on }}</template>
|
||||
<template #unCheckedChildren>{{ labels.off }}</template>
|
||||
</a-switch>
|
||||
|
||||
<ZaarboxPriceKeyField
|
||||
@@ -63,10 +63,11 @@
|
||||
<script setup lang="ts">
|
||||
import ZaarboxPriceKeyField from './ZaarboxPriceKeyField.vue'
|
||||
import {
|
||||
isSwitchOn,
|
||||
parseSelectOptions,
|
||||
resolveSettingFieldType,
|
||||
switchToString,
|
||||
resolveSwitchChecked,
|
||||
switchLabels,
|
||||
switchValueFromChecked,
|
||||
type SettingFieldType
|
||||
} from './settingTypes'
|
||||
|
||||
@@ -101,10 +102,12 @@ const selectItems = computed(() =>
|
||||
})
|
||||
)
|
||||
|
||||
const switchChecked = computed(() => isSwitchOn(props.modelValue))
|
||||
const labels = computed(() => switchLabels(props.settingKey))
|
||||
|
||||
const switchChecked = computed(() => resolveSwitchChecked(props.settingKey, props.modelValue))
|
||||
|
||||
function onSwitchChange(checked: boolean) {
|
||||
emit('update:value', switchToString(checked))
|
||||
emit('update:value', switchValueFromChecked(props.settingKey, checked))
|
||||
}
|
||||
|
||||
function onSelectChange(val: string | undefined) {
|
||||
|
||||
@@ -14,16 +14,6 @@
|
||||
</a-input>
|
||||
|
||||
<div class="settings-toolbar__filters">
|
||||
<label class="settings-filter-field">
|
||||
<span class="settings-filter-field__label">وضعیت</span>
|
||||
<a-select
|
||||
v-model:value="filterActive"
|
||||
allow-clear
|
||||
placeholder="همه"
|
||||
class="settings-filter-field__control"
|
||||
:options="statusFilterOptions"
|
||||
/>
|
||||
</label>
|
||||
<label class="settings-filter-field">
|
||||
<span class="settings-filter-field__label">نوع فیلد</span>
|
||||
<a-select
|
||||
@@ -80,7 +70,6 @@
|
||||
:saving="isSaving(row.id)"
|
||||
:saved-flash="isSavedFlash(row.id)"
|
||||
@update:value="onValueChange(row, $event)"
|
||||
@update:is-active="onActiveChange(row, $event)"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
@@ -187,11 +176,6 @@ import {
|
||||
|
||||
const settingTypeOptions = SETTING_TYPE_OPTIONS
|
||||
|
||||
const statusFilterOptions = [
|
||||
{ value: 'active', label: 'فعال' },
|
||||
{ value: 'inactive', label: 'غیرفعال' }
|
||||
]
|
||||
|
||||
const typeSelectOptions = SETTING_TYPE_OPTIONS.map((t) => ({
|
||||
value: t.key,
|
||||
label: t.value
|
||||
@@ -202,11 +186,6 @@ const drafts = ref<Record<number, string>>({})
|
||||
const addConfigVisible = ref(false)
|
||||
const search = ref('')
|
||||
const filterType = ref<string | undefined>()
|
||||
const filterActive = ref<string | undefined>()
|
||||
|
||||
function isRowActive(row: Const): boolean {
|
||||
return row.is_active !== false && row.is_active !== 0
|
||||
}
|
||||
|
||||
const savingIds = ref<Set<number>>(new Set())
|
||||
const savedFlashIds = ref<Set<number>>(new Set())
|
||||
@@ -249,8 +228,6 @@ function isEditable(row: Const): boolean {
|
||||
|
||||
function matchesFilter(row: Const): boolean {
|
||||
const q = search.value.trim().toLowerCase()
|
||||
if (filterActive.value === 'active' && !isRowActive(row)) return false
|
||||
if (filterActive.value === 'inactive' && isRowActive(row)) return false
|
||||
if (filterType.value) {
|
||||
const t = resolveSettingFieldType(row)
|
||||
if (t !== filterType.value) return false
|
||||
@@ -297,16 +274,29 @@ const SETTING_GROUPS: Array<{
|
||||
id: 'general',
|
||||
title: 'عمومی و اطلاعات اپ',
|
||||
icon: 'vuesax-linear:home-2',
|
||||
match: (k) => /^(name_|app_|admin_|server_|bace_|key$|color_|logo_|title_pwa|title_fav|metal_type|unit$)/.test(k)
|
||||
match: (k) => /^(name_fa|app_url|admin_url|metal_type|unit$|open_register|auto_active_customer|is_store)/.test(k)
|
||||
},
|
||||
{
|
||||
id: 'chart',
|
||||
title: 'نمودار قیمت',
|
||||
icon: 'vuesax-linear:chart-2',
|
||||
match: (k) => /^chart_/.test(k)
|
||||
},
|
||||
{
|
||||
id: 'pricing',
|
||||
title: 'قیمتگذاری و کارمزدها',
|
||||
title: 'قیمتگذاری و تابلو',
|
||||
icon: 'vuesax-linear:dollar-circle',
|
||||
match: (k) =>
|
||||
/^(fee_|buy_fee|sell_fee|min_|referral_|gift_for|price_provider|zaarbox_api_url|chart_id)/.test(k) ||
|
||||
/^(fee_|buy_fee|sell_fee|min_|price_provider|zaarbox_|main_product$|show_last_price|close_price|startDay|endDay)/.test(k) ||
|
||||
k.includes('charge_percentage')
|
||||
},
|
||||
{
|
||||
id: 'referral',
|
||||
title: 'معرف، پاداش و ثبتنام',
|
||||
icon: 'vuesax-linear:people',
|
||||
match: (k) =>
|
||||
/^(referral_|gift_for|register_referral|register_jibit|has_page_earn_money|jibit_verification)/.test(k)
|
||||
},
|
||||
{
|
||||
id: 'montzar',
|
||||
title: 'سرویس همکار (montzar)',
|
||||
@@ -315,22 +305,22 @@ const SETTING_GROUPS: Array<{
|
||||
},
|
||||
{
|
||||
id: 'features',
|
||||
title: 'ویژگیها و صفحات',
|
||||
title: 'ویژگیها و اعلانها',
|
||||
icon: 'vuesax-linear:menu',
|
||||
match: (k) =>
|
||||
/^(has_page_|has_chat|display_montzar|close_price|startDay|endDay|auto_active|open_register|charge_notif|group_sms|show_last_price|product_update|product_auto_close|is_store|store_status)/.test(k)
|
||||
match: (k) => /^(charge_notif|group_sms|main_wallet_products|price_from_nasiryan)/.test(k)
|
||||
},
|
||||
{
|
||||
id: 'integrations',
|
||||
title: 'اتصالات خارجی و درگاهها',
|
||||
icon: 'vuesax-linear:card',
|
||||
match: (k) => /^(tahesab_|payping_|jibit_|telegram_|bale_|liman_|support_messenger|external_order|profit_gold)/.test(k)
|
||||
match: (k) =>
|
||||
/^(tahesab_|payping_|telegram_|bale_|support_messenger|external_order|profit_gold|node_)/.test(k)
|
||||
},
|
||||
{
|
||||
id: 'misc',
|
||||
title: 'سایر تنظیمات (متفرقه)',
|
||||
title: 'سایر تنظیمات',
|
||||
icon: 'vuesax-linear:more-circle',
|
||||
match: () => true // catch-all
|
||||
match: () => true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -434,12 +424,6 @@ function onValueChange(row: Const, value: string) {
|
||||
scheduleSave(row)
|
||||
}
|
||||
|
||||
async function onActiveChange(row: Const, isActive: boolean) {
|
||||
if (row.id == null) return
|
||||
if (isRowActive(row) === isActive) return
|
||||
await persistRow(row, { isActive })
|
||||
}
|
||||
|
||||
function scheduleSave(row: Const) {
|
||||
const id = row.id!
|
||||
if (!debouncedSave.has(id)) {
|
||||
@@ -457,21 +441,18 @@ async function persistRowValue(id: number) {
|
||||
await saveSettingRow(row)
|
||||
}
|
||||
|
||||
async function persistRow(row: Const, overrides?: { isActive?: boolean }) {
|
||||
await saveSettingRow(row, overrides)
|
||||
async function persistRow(row: Const) {
|
||||
await saveSettingRow(row)
|
||||
}
|
||||
|
||||
async function saveSettingRow(row: Const, overrides?: { isActive?: boolean }) {
|
||||
async function saveSettingRow(row: Const) {
|
||||
if (row.id == null) return
|
||||
const id = row.id
|
||||
|
||||
const type = resolveSettingFieldType(row)
|
||||
const raw = drafts.value[id] ?? row.value ?? ''
|
||||
const value = formatValueForSave(raw, type)
|
||||
const isActive =
|
||||
overrides?.isActive !== undefined ? overrides.isActive : isRowActive(row)
|
||||
|
||||
if (overrides?.isActive === undefined && value === (row.value ?? '')) return
|
||||
const value = formatValueForSave(raw, type, row.key)
|
||||
if (value === (row.value ?? '')) return
|
||||
|
||||
savingIds.value.add(id)
|
||||
savedFlashIds.value.delete(id)
|
||||
@@ -482,12 +463,12 @@ async function saveSettingRow(row: Const, overrides?: { isActive?: boolean }) {
|
||||
key: row.key,
|
||||
value,
|
||||
description: row.description ?? '',
|
||||
is_active: isActive,
|
||||
is_active: true,
|
||||
title_fa: row.title_fa ?? '',
|
||||
type
|
||||
})
|
||||
row.value = value
|
||||
row.is_active = isActive
|
||||
row.is_active = true
|
||||
drafts.value[id] = value
|
||||
if (row.key === 'is_store') {
|
||||
setStoreModeFromSettings(isSwitchOn(value))
|
||||
@@ -536,7 +517,7 @@ async function ok() {
|
||||
|
||||
const payload = {
|
||||
key: form.value.key,
|
||||
value: formatValueForSave(form.value.value ?? '', type),
|
||||
value: formatValueForSave(form.value.value ?? '', type, form.value.key),
|
||||
description,
|
||||
is_active: form.value.is_active,
|
||||
title_fa: form.value.title_fa ?? '',
|
||||
|
||||
@@ -3,18 +3,21 @@ import { isStoreApp } from '@/utils/config'
|
||||
|
||||
export type SettingFieldType = 'text' | 'number' | 'switch' | 'select' | 'longtext'
|
||||
|
||||
/** کلیدهای legacy — در پنل نمایش داده نمیشوند (جایگزین: price_provider) */
|
||||
/** کلیدهای legacy — در پنل نمایش داده نمیشوند */
|
||||
export const HIDDEN_LEGACY_SETTING_KEYS = new Set([
|
||||
'main_product_price',
|
||||
'montzar_price',
|
||||
'etehadie_api_url',
|
||||
'goldir_price',
|
||||
'price_primary_source',
|
||||
'price_method'
|
||||
'price_method',
|
||||
'montzar_last_product_log',
|
||||
'montzar_order_response',
|
||||
'service_charge_percentage'
|
||||
])
|
||||
|
||||
/** فقط وقتی is_store فعال است در پنل دیده میشوند */
|
||||
export const STORE_ONLY_SETTING_KEYS = new Set(['store_status'])
|
||||
export const STORE_ONLY_SETTING_KEYS = new Set(['store_status', 'price_from_nasiryan'])
|
||||
|
||||
export function isSettingVisible(key: string | undefined | null): boolean {
|
||||
const k = (key || '').trim()
|
||||
@@ -33,8 +36,9 @@ export const SETTING_TYPE_OPTIONS = [
|
||||
]
|
||||
|
||||
const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
|
||||
chart_use_local_data: 'switch',
|
||||
price_provider: 'select',
|
||||
referral_id: 'select',
|
||||
referral_id: 'switch',
|
||||
payping_gateway_name: 'select',
|
||||
text_rule: 'longtext',
|
||||
text_about: 'longtext',
|
||||
@@ -43,66 +47,43 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
|
||||
buy_fee: 'number',
|
||||
sell_fee: 'number',
|
||||
fee_cash: 'number',
|
||||
fee_free: 'number',
|
||||
min_withdraw: 'number',
|
||||
min_first_buy: 'number',
|
||||
minimum_sell: 'number',
|
||||
gift_for_referrer: 'number',
|
||||
gift_for_referral: 'number',
|
||||
referral_bonus_amount: 'text',
|
||||
referral_count_for_gift: 'number',
|
||||
montzar_product_id: 'number',
|
||||
balance_report_exclude_customer_id: 'number',
|
||||
jibit_verification: 'switch',
|
||||
register_jibit_verify: 'switch',
|
||||
register_referral_sms_to_referrer: 'switch',
|
||||
register_referral_bonus_message: 'switch',
|
||||
open_register: 'switch',
|
||||
auto_active_customer: 'switch',
|
||||
close_price: 'switch',
|
||||
montzar_price: 'switch',
|
||||
main_product_price: 'switch',
|
||||
show_last_price: 'switch',
|
||||
send_to_tahesab: 'switch',
|
||||
profit_gold_api_enabled: 'switch',
|
||||
payping_round_toman: 'switch',
|
||||
payping_strict_failed_withdraw: 'switch',
|
||||
has_page_earn_money: 'switch',
|
||||
has_page_fund: 'switch',
|
||||
has_page_permission: 'switch',
|
||||
has_page_price_trigger: 'switch',
|
||||
has_page_physical_gold: 'switch',
|
||||
has_chat: 'switch',
|
||||
charge_notif: 'switch',
|
||||
group_sms: 'switch',
|
||||
display_montzar: 'switch',
|
||||
credit: 'switch',
|
||||
credit_daily: 'switch',
|
||||
credit_error_in_orders: 'switch',
|
||||
credit_rial_dynamic: 'switch',
|
||||
open_closed_messages: 'switch',
|
||||
new_style_create_order: 'switch',
|
||||
new_style_admin_layout: 'switch',
|
||||
register: 'switch',
|
||||
sharh: 'switch',
|
||||
multi_source: 'switch',
|
||||
get_orders_pdf: 'switch',
|
||||
get_orders_excel: 'switch',
|
||||
auto_order_buy_sell: 'switch',
|
||||
invite_link: 'switch',
|
||||
montzar_closed: 'switch',
|
||||
montzar_open_buy: 'switch',
|
||||
montzar_open_sell: 'switch',
|
||||
product_update_price_on_list: 'switch',
|
||||
product_auto_close_open_on_list: 'switch',
|
||||
payping_fin_sync_enabled: 'switch',
|
||||
external_order_api_enabled: 'switch',
|
||||
is_store: 'switch',
|
||||
store_status: 'select',
|
||||
credit_rial_price_unit: 'select',
|
||||
zaarbox_price_key: 'select',
|
||||
price_from_nasiryan: 'switch',
|
||||
node_client_id: 'text',
|
||||
node_shared_secret: 'text',
|
||||
support_messenger_id: 'text',
|
||||
support_messenger_text: 'text',
|
||||
support_messenger: 'select'
|
||||
support_messenger: 'select',
|
||||
zaarbox_price_key: 'select'
|
||||
}
|
||||
|
||||
const SELECT_LABELS_BY_KEY: Record<string, Record<string, string>> = {
|
||||
@@ -118,14 +99,22 @@ const SELECT_LABELS_BY_KEY: Record<string, Record<string, string>> = {
|
||||
|
||||
const SELECT_OPTIONS_BY_KEY: Record<string, string> = {
|
||||
price_provider: 'montzar,zaarbox,main_product',
|
||||
referral_id: 'required,',
|
||||
payping_gateway_name: 'sepal,',
|
||||
credit_rial_price_unit: 'gram,mesghal,unit',
|
||||
store_status: 'باز,بسته',
|
||||
unit: 'گرم,کیلوگرم',
|
||||
support_messenger: 'bale,telegram,whatsapp,rubika,eitaa,soroush'
|
||||
}
|
||||
|
||||
/** سوییچهایی که مقدار required/خالی دارند (نه true/false) */
|
||||
export const REFERRAL_REQUIRED_KEY = 'referral_id'
|
||||
|
||||
export function isReferralRequired(value: string | undefined): boolean {
|
||||
return (value ?? '').trim().toLowerCase() === 'required'
|
||||
}
|
||||
|
||||
export function referralRequiredToString(checked: boolean): string {
|
||||
return checked ? 'required' : ''
|
||||
}
|
||||
|
||||
export function resolveSettingFieldType(row: Pick<Const, 'key' | 'type'>): SettingFieldType {
|
||||
const raw = (row.type || '').trim().toLowerCase()
|
||||
if (['text', 'number', 'switch', 'select', 'longtext'].includes(raw)) {
|
||||
@@ -169,9 +158,27 @@ export function switchToString(checked: boolean): string {
|
||||
return checked ? 'true' : 'false'
|
||||
}
|
||||
|
||||
export function formatValueForSave(value: string, type: SettingFieldType): string {
|
||||
export function resolveSwitchChecked(key: string | undefined, value: string | undefined): boolean {
|
||||
if (key === REFERRAL_REQUIRED_KEY) {
|
||||
return isReferralRequired(value)
|
||||
}
|
||||
return isSwitchOn(value)
|
||||
}
|
||||
|
||||
export function switchValueFromChecked(key: string | undefined, checked: boolean): string {
|
||||
if (key === REFERRAL_REQUIRED_KEY) {
|
||||
return referralRequiredToString(checked)
|
||||
}
|
||||
return switchToString(checked)
|
||||
}
|
||||
|
||||
export function formatValueForSave(
|
||||
value: string,
|
||||
type: SettingFieldType,
|
||||
key?: string
|
||||
): string {
|
||||
if (type === 'switch') {
|
||||
return switchToString(isSwitchOn(value))
|
||||
return switchValueFromChecked(key, isSwitchOn(value) || (key === REFERRAL_REQUIRED_KEY && isReferralRequired(value)))
|
||||
}
|
||||
if (type === 'number') {
|
||||
return value === '' ? '0' : String(value).replace(/,/g, '')
|
||||
@@ -179,6 +186,16 @@ export function formatValueForSave(value: string, type: SettingFieldType): strin
|
||||
return value ?? ''
|
||||
}
|
||||
|
||||
export function switchLabels(key: string | undefined): { on: string; off: string } {
|
||||
if (key === REFERRAL_REQUIRED_KEY) {
|
||||
return { on: 'اجباری', off: 'اختیاری' }
|
||||
}
|
||||
if (key === 'chart_use_local_data') {
|
||||
return { on: 'داده خود سایت', off: 'Zaarbox' }
|
||||
}
|
||||
return { on: 'فعال', off: 'غیرفعال' }
|
||||
}
|
||||
|
||||
export function helpText(row: Const): string {
|
||||
const desc = (row.description || '').trim()
|
||||
if (!desc) return ''
|
||||
|
||||
@@ -603,7 +603,7 @@ const resetData = () => {
|
||||
form.value = {
|
||||
fname: '',
|
||||
lname: '',
|
||||
active: false,
|
||||
active: true,
|
||||
avatar_id: undefined,
|
||||
sts: undefined,
|
||||
id: undefined,
|
||||
|
||||
@@ -11,29 +11,30 @@
|
||||
@refresh="reload"
|
||||
@load-more="loadMore"
|
||||
>
|
||||
<PriceMobileListItem v-for="item in items" :key="item.id" :item="item" @open="openSheet" />
|
||||
<PriceMobileListItem v-for="item in items" :key="item.id" :item="item" @open="openDetail" />
|
||||
</MobileCardList>
|
||||
|
||||
<PriceMobileDetailSheet v-model:visible="sheetVisible" :item="selectedItem" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getPriceTable } from '@/api/price'
|
||||
import type { Price } from '@/model/price'
|
||||
import { getPriceSyncLogTable } from '@/api/price'
|
||||
import type { PriceSyncLog } from '@/model/priceSyncLog'
|
||||
import MobileCardList from '@/components/mobile/MobileCardList.vue'
|
||||
import { useMobileDetailSheet } from '@/components/mobile/composables/useMobileDetailSheet'
|
||||
import { useMobileTableList } from '@/components/mobile/composables/useMobileTableList'
|
||||
import PriceMobileDetailSheet from './PriceMobileDetailSheet.vue'
|
||||
import PriceMobileListItem from './PriceMobileListItem.vue'
|
||||
|
||||
const { sheetVisible, selectedItem, openSheet, syncSelectedItem } = useMobileDetailSheet<Price>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'detail', item: PriceSyncLog): void
|
||||
}>()
|
||||
|
||||
const { loading, search, items, hasMore, reload, loadMore, fetch } = useMobileTableList<Price>({
|
||||
fetchApi: getPriceTable,
|
||||
onFetched: (rows) => syncSelectedItem(rows)
|
||||
const { loading, search, items, hasMore, reload, loadMore, fetch } = useMobileTableList<PriceSyncLog>({
|
||||
fetchApi: getPriceSyncLogTable
|
||||
})
|
||||
|
||||
function openDetail(item: PriceSyncLog) {
|
||||
emit('detail', item)
|
||||
}
|
||||
|
||||
onMounted(() => reload())
|
||||
|
||||
defineExpose({ fetch, openSheet })
|
||||
defineExpose({ fetch, openDetail })
|
||||
</script>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
<MobileCard clickable @click="$emit('open', item)">
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="truncate text-sm font-700 text-gray-900">{{ userName }}</span>
|
||||
<span class="truncate text-sm font-700 text-gray-900">{{ sourceLabel }}</span>
|
||||
<span class="shrink-0 text-xs text-gray-400">{{ dateLabel }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-2 text-xs">
|
||||
<span class="truncate text-gray-600">#{{ item.id ?? '-' }}</span>
|
||||
<span class="truncate text-gray-600">{{ item.products_updated }} محصول</span>
|
||||
<span class="shrink-0 font-700 text-green-700">{{ priceLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -15,20 +15,27 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Price } from '@/model/price'
|
||||
import type { PriceSyncLog } from '@/model/priceSyncLog'
|
||||
import MobileCard from '@/components/mobile/MobileCard.vue'
|
||||
import { toJalaliNoTime } from '@core/utils/useDataTime'
|
||||
import { usePrice } from '@core/utils/usePrice'
|
||||
|
||||
const SOURCE_LABELS: Record<string, string> = {
|
||||
zaarbox: 'تابلو Zaarbox',
|
||||
legacy: 'فید قدیمی'
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
item: Price
|
||||
item: PriceSyncLog
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'open', item: Price): void
|
||||
(e: 'open', item: PriceSyncLog): void
|
||||
}>()
|
||||
|
||||
const userName = computed(() => props.item.user?.name || '-')
|
||||
const sourceLabel = computed(() => SOURCE_LABELS[props.item.source] ?? props.item.source)
|
||||
const dateLabel = computed(() => (props.item.created_at ? toJalaliNoTime(props.item.created_at) : '-'))
|
||||
const priceLabel = computed(() => usePrice(props.item.price ?? 0, true, ''))
|
||||
const priceLabel = computed(() =>
|
||||
props.item.liquid_gold_price > 0 ? usePrice(props.item.liquid_gold_price, true, '') : '—'
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<template>
|
||||
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
||||
<BasicShowContext :title="isStore ? 'تاریخچه همگامسازی قیمت' : 'تاریخچه قیمت'">
|
||||
<BasicShowContext title="تاریخچه قیمت">
|
||||
<template #extra>
|
||||
<BaseButton
|
||||
v-if="isStore"
|
||||
type="primary"
|
||||
size="small"
|
||||
class="flex shrink-0 md:hidden"
|
||||
@@ -13,23 +12,12 @@
|
||||
<div>بهروزرسانی قیمت</div>
|
||||
<BaseIcon icon="vuesax-linear:refresh" Size="16px" />
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-else
|
||||
type="primary"
|
||||
size="small"
|
||||
class="flex shrink-0 md:hidden"
|
||||
@click="emits('add')"
|
||||
>
|
||||
<div>قیمت گذاری</div>
|
||||
<BaseIcon icon="vuesax-linear:dollar-circle" Size="16px" />
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<div class="flex h-full min-h-0 flex-col">
|
||||
<BaseTable v-if="!isMobile" class="h-full min-h-0 min-w-0" v-bind="tblProps" @register="submit" ref="tbl">
|
||||
<template #extraHeader>
|
||||
<BaseButton
|
||||
v-if="isStore"
|
||||
type="primary"
|
||||
size="small"
|
||||
class="flex"
|
||||
@@ -39,10 +27,6 @@
|
||||
<div>بهروزرسانی قیمت</div>
|
||||
<BaseIcon icon="vuesax-linear:refresh" Size="16px" />
|
||||
</BaseButton>
|
||||
<BaseButton v-else type="primary" size="small" class="flex" @click="emits('add')">
|
||||
<div>قیمت گذاری</div>
|
||||
<BaseIcon icon="vuesax-linear:dollar-circle" Size="16px" />
|
||||
</BaseButton>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<div class="flex justify-center gap-2">
|
||||
@@ -56,8 +40,12 @@
|
||||
</template>
|
||||
</BaseTable>
|
||||
|
||||
<PriceMobileCardList v-else-if="!isStore" ref="mobileListRef" class="min-h-0 flex-1" />
|
||||
<div v-else class="p-4 text-center text-gray-500">در موبایل از نسخه دسکتاپ استفاده کنید</div>
|
||||
<PriceMobileCardList
|
||||
v-else
|
||||
ref="mobileListRef"
|
||||
class="min-h-0 flex-1"
|
||||
@detail="emits('detail', $event)"
|
||||
/>
|
||||
</div>
|
||||
</BasicShowContext>
|
||||
</div>
|
||||
@@ -67,30 +55,26 @@
|
||||
import type { TableInput } from '@core/type/baseTable'
|
||||
import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
|
||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||
import { cols } from '../hooks/priceColumns'
|
||||
import { storePriceColumns } from '../hooks/storePriceColumns'
|
||||
import { usePrefix } from '@core/composable/usePrefix'
|
||||
import { getPriceSyncLogTable, getPriceTable } from '@/api/price'
|
||||
import { isStoreApp } from '@/utils/config'
|
||||
import { getPriceSyncLogTable } from '@/api/price'
|
||||
import PriceMobileCardList from './PriceMobileCardList.vue'
|
||||
import { isMobile } from '@core/utils/is'
|
||||
import { computed } from 'vue'
|
||||
import type { PriceSyncLog } from '@/model/priceSyncLog'
|
||||
|
||||
const props = defineProps<{ syncing?: boolean }>()
|
||||
defineProps<{ syncing?: boolean }>()
|
||||
|
||||
const [submit, reload] = useBaseTable()
|
||||
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
||||
|
||||
const emits = defineEmits(['delete', 'edit', 'add', 'info', 'detail', 'sync'])
|
||||
const emits = defineEmits(['detail', 'sync'])
|
||||
|
||||
const isStore = computed(() => isStoreApp())
|
||||
const { prefixCls } = usePrefix('pophoverstatus')
|
||||
|
||||
const tblProps = computed(() => ({
|
||||
columns: isStore.value ? storePriceColumns : cols,
|
||||
api: (params: TableInput) =>
|
||||
isStore.value ? getPriceSyncLogTable(params) : getPriceTable(params)
|
||||
}))
|
||||
const tblProps = {
|
||||
columns: storePriceColumns,
|
||||
api: (params: TableInput) => getPriceSyncLogTable(params)
|
||||
}
|
||||
|
||||
async function fetch() {
|
||||
await Promise.allSettled([reload(), mobileListRef.value?.fetch?.() as Promise<void>])
|
||||
|
||||
@@ -6,74 +6,28 @@
|
||||
ref="table"
|
||||
class="h-full w-full"
|
||||
:syncing="syncing"
|
||||
@add="add"
|
||||
@detail="openDetail"
|
||||
@sync="handleSync"
|
||||
/>
|
||||
</div>
|
||||
</BasicMainCard>
|
||||
|
||||
<template v-if="!isStore">
|
||||
<BasicModal
|
||||
v-model:visible="priceConfirmVisible"
|
||||
width="25rem"
|
||||
@ok="ok"
|
||||
:confirm-loading="loading"
|
||||
@close="priceConfirmVisible = false"
|
||||
>
|
||||
<div class="flex items-center gap-2 font-bold">
|
||||
<BaseIcon icon="vuesax-linear:info-circle" color="orange" />
|
||||
<div>آیا مطمئن به تغییر قیمت می باشید؟</div>
|
||||
</div>
|
||||
</BasicModal>
|
||||
|
||||
<BasicModal
|
||||
v-model:visible="addPriceVisible"
|
||||
width="25rem"
|
||||
title="قیمت گذاری"
|
||||
@ok="confirmPrice"
|
||||
:loading="loading"
|
||||
@close="addPriceVisible = false"
|
||||
>
|
||||
<a-form :model="form" ref="formRef" autocomplete="off">
|
||||
<div class="gap-4">
|
||||
<a-form-item label="قیمت جدید" name="price" :rules="inputRules()">
|
||||
<BasicInput v-model:value="form.price" type="price" addon-before="ریال" />
|
||||
</a-form-item>
|
||||
<div v-show="form.price > 10">معادل {{ usePrice(form.price / 10, true, 'تومان') }}</div>
|
||||
</div>
|
||||
</a-form>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<PriceSyncDetailModal v-model:visible="detailVisible" :log-id="selectedLogId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inputRules } from '@/utils/inputRules'
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import tbl from './components/table.vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import { message } from 'ant-design-vue/es'
|
||||
import type { Price } from '@/model/price'
|
||||
import { createPrice, runPriceSync } from '@/api/price'
|
||||
import { usePrice } from '@core/utils/usePrice'
|
||||
import { isStoreApp } from '@/utils/config'
|
||||
import { runPriceSync } from '@/api/price'
|
||||
import PriceSyncDetailModal from './components/PriceSyncDetailModal.vue'
|
||||
import type { PriceSyncLog } from '@/model/priceSyncLog'
|
||||
|
||||
const loading = ref(false)
|
||||
const syncing = ref(false)
|
||||
const table = ref<null | { fetch: () => void }>(null)
|
||||
const refreshTable = () => table.value?.fetch()
|
||||
|
||||
const isStore = computed(() => isStoreApp())
|
||||
|
||||
const form = ref<Price>({})
|
||||
const formRef = ref<FormInstance>()
|
||||
const addPriceVisible = ref(false)
|
||||
const priceConfirmVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const selectedLogId = ref<number | null>(null)
|
||||
|
||||
@@ -95,31 +49,6 @@ async function handleSync() {
|
||||
syncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function add() {
|
||||
addPriceVisible.value = true
|
||||
form.value = {}
|
||||
}
|
||||
|
||||
function confirmPrice() {
|
||||
formRef.value?.validateFields().then(async () => {
|
||||
priceConfirmVisible.value = true
|
||||
})
|
||||
}
|
||||
|
||||
async function ok() {
|
||||
loading.value = true
|
||||
try {
|
||||
await createPrice(form.value)
|
||||
refreshTable()
|
||||
priceConfirmVisible.value = false
|
||||
addPriceVisible.value = false
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -80,12 +80,10 @@ import {
|
||||
} from '@/utils/file'
|
||||
import { normalizeProductOrderLimitForm, validateProductOrderLimitForm } from '@/utils/productOrderLimit'
|
||||
import {
|
||||
buildCategorySelectItems,
|
||||
isMeltedTradeGroup,
|
||||
PRODUCT_CATEGORY_ABSHODE,
|
||||
resolveMeltedPriceSource
|
||||
} from '@/utils/productCategories'
|
||||
import { getCategories } from '@/api/category'
|
||||
import { syncAndLoadCategories } from '@/utils/categorySelect'
|
||||
|
||||
dayjs.extend(customParseFormat)
|
||||
|
||||
@@ -128,13 +126,8 @@ const metalItems: Array<Item> = [
|
||||
{ key: 2, value: 'نقره' },
|
||||
{ key: 3, value: 'مس' },
|
||||
]
|
||||
const storeCategorySelectItems = ref<Array<{ key: string; value: string }>>([])
|
||||
const categorySelectItems = computed(() => {
|
||||
if (isStoreApp() && storeCategorySelectItems.value.length) {
|
||||
return storeCategorySelectItems.value
|
||||
}
|
||||
return buildCategorySelectItems(metalItems)
|
||||
})
|
||||
const apiCategorySelectItems = ref<Array<{ key: string; value: string }>>([])
|
||||
const categorySelectItems = computed(() => apiCategorySelectItems.value)
|
||||
const priceItems: Array<Item> = [
|
||||
{ key: 0, value: 'متغیر' },
|
||||
{ key: 1, value: 'ثابت' }
|
||||
@@ -197,25 +190,17 @@ watch(addUserVisible, (visible) => {
|
||||
|
||||
onMounted(() => {
|
||||
void refreshLivePrices()
|
||||
void loadStoreCategories()
|
||||
void loadCategorySelectItems()
|
||||
})
|
||||
|
||||
async function loadStoreCategories() {
|
||||
if (!isStoreApp()) return
|
||||
|
||||
async function loadCategorySelectItems() {
|
||||
try {
|
||||
const rows = await getCategories({ select_only: true })
|
||||
const metalHint = metalItems.map((item) => item.value).join('، ')
|
||||
storeCategorySelectItems.value = rows.map((category) => ({
|
||||
key: category.name,
|
||||
value:
|
||||
category.name === PRODUCT_CATEGORY_ABSHODE
|
||||
? `${category.name} (${metalHint})`
|
||||
: category.name
|
||||
}))
|
||||
apiCategorySelectItems.value = await syncAndLoadCategories({ metalHint })
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
storeCategorySelectItems.value = []
|
||||
apiCategorySelectItems.value = []
|
||||
message.error('خطا در دریافت دستهبندیها')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,6 +270,7 @@ async function setProductIndexImage(record: Record<string, any>, productId?: num
|
||||
}
|
||||
|
||||
async function edit(record: Product) {
|
||||
await loadCategorySelectItems()
|
||||
addUserVisible.value = true
|
||||
editId = record.id
|
||||
let productData = cloneDeep(record) as Record<string, any>
|
||||
@@ -333,6 +319,7 @@ async function add() {
|
||||
editId = undefined
|
||||
addUserVisible.value = true
|
||||
resetData()
|
||||
await loadCategorySelectItems()
|
||||
}
|
||||
|
||||
function resetData() {
|
||||
|
||||
+2
-5
@@ -11,7 +11,7 @@ function applyAppModeMeta(route: RouteRecordRaw, appName: string | null) {
|
||||
const store = isStoreApp()
|
||||
|
||||
if (route.name === 'pricing' && route.meta) {
|
||||
route.meta.title = store ? 'تاریخچه قیمت' : 'قیمت گذاری'
|
||||
route.meta.title = 'تاریخچه قیمت'
|
||||
}
|
||||
|
||||
if (!route.children?.length) return
|
||||
@@ -27,11 +27,8 @@ function applyAppModeMeta(route: RouteRecordRaw, appName: string | null) {
|
||||
if (child.name === 'banners' && child.meta) {
|
||||
child.meta.hideMenu = appName !== 'garoosi'
|
||||
}
|
||||
if (child.name === 'productsMain_categories' && child.meta) {
|
||||
child.meta.hideMenu = !store
|
||||
}
|
||||
if (child.name === 'pricingMain' && child.meta) {
|
||||
child.meta.title = store ? 'تاریخچه قیمت' : 'قیمت گذاری'
|
||||
child.meta.title = 'تاریخچه قیمت'
|
||||
}
|
||||
applyAppModeMeta(child, appName)
|
||||
}
|
||||
|
||||
@@ -54,8 +54,7 @@ const Products: RouteRecordRaw = {
|
||||
component: () => import('@/pages/menuPages/products/categories/index.vue'),
|
||||
meta: {
|
||||
icon: 'vuesax-linear:category',
|
||||
title: 'دستهبندیها',
|
||||
hideMenu: true
|
||||
title: 'دستهبندیها'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { getCategories, syncCategoriesFromProducts } from '@/api/category'
|
||||
import type { Category } from '@/model/category'
|
||||
import { PRODUCT_CATEGORY_ABSHODE } from '@/utils/productCategories'
|
||||
|
||||
export type CategorySelectItem = { key: string; value: string }
|
||||
|
||||
export function mapCategoriesToSelectItems(
|
||||
rows: Category[],
|
||||
metalHint = 'طلا، نقره، مس'
|
||||
): CategorySelectItem[] {
|
||||
return rows.map((category) => ({
|
||||
key: category.name,
|
||||
value:
|
||||
category.name === PRODUCT_CATEGORY_ABSHODE
|
||||
? `${category.name} (${metalHint})`
|
||||
: category.name
|
||||
}))
|
||||
}
|
||||
|
||||
/** همان منبع جدول دستهبندیها — قبل از خواندن، همگامسازی میکند */
|
||||
export async function syncAndLoadCategories(options?: {
|
||||
selectOnly?: boolean
|
||||
metalHint?: string
|
||||
}): Promise<CategorySelectItem[]> {
|
||||
await syncCategoriesFromProducts()
|
||||
|
||||
const rows = await getCategories(
|
||||
options?.selectOnly ? { select_only: true } : undefined
|
||||
)
|
||||
|
||||
return mapCategoriesToSelectItems(rows, options?.metalHint)
|
||||
}
|
||||
@@ -7,6 +7,9 @@ export const UNIFIED_PRODUCT_CATEGORIES = [
|
||||
'گوی',
|
||||
'سکه',
|
||||
'سکه پارسیان',
|
||||
'شمش',
|
||||
'شمش ونوس',
|
||||
'شمش برنا',
|
||||
'مس',
|
||||
'نقره',
|
||||
] as const
|
||||
|
||||
Reference in New Issue
Block a user