This commit is contained in:
2026-07-01 16:15:51 +03:30
parent 6c393ecb94
commit d2c5c20729
5 changed files with 166 additions and 10 deletions
@@ -0,0 +1,110 @@
<template>
<a-select
mode="multiple"
:value="selectedIds"
:disabled="disabled"
:loading="loading"
allow-clear
show-search
placeholder="انتخاب محصولات متصل به کیف اصلی"
class="w-full"
option-filter-prop="label"
@update:value="onChange"
>
<a-select-option
v-for="item in allOptions"
:key="String(item.id)"
:value="String(item.id)"
:label="`${item.name} ${item.id}`"
>
<div class="leading-snug">
<div>{{ item.name }}</div>
<div class="text-xs text-gray-400">شناسه: {{ item.id }}</div>
</div>
</a-select-option>
</a-select>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { getCustomersList } from '@/api/product'
import type { Product } from '@/model/product'
import {
MAIN_WALLET_PRODUCTS_DEFAULT,
formatMainWalletProductIds,
parseMainWalletProductIds
} from './settingTypes'
type ProductOption = { id: number; name: string }
function productLabel(row: Pick<Product, 'id' | 'title'> & { name?: string }): string {
const label = (row.title || row.name || '').trim()
return label || `محصول #${row.id}`
}
function normalizeProductList(response: unknown): Product[] {
if (Array.isArray(response)) {
return response as Product[]
}
if (response && typeof response === 'object' && Array.isArray((response as { data?: unknown }).data)) {
return (response as { data: Product[] }).data
}
return []
}
const props = withDefaults(
defineProps<{
modelValue: string
disabled?: boolean
}>(),
{ disabled: false }
)
const emit = defineEmits<{ 'update:value': [value: string] }>()
const loading = ref(false)
const products = ref<ProductOption[]>([])
const selectedIds = computed(() => {
const parsed = parseMainWalletProductIds(props.modelValue)
return parsed.length > 0 ? parsed : [MAIN_WALLET_PRODUCTS_DEFAULT]
})
const allOptions = computed(() => {
const known = new Set(products.value.map((p) => p.id))
const extras: ProductOption[] = []
for (const idStr of selectedIds.value) {
const id = Number(idStr)
if (!Number.isFinite(id) || id <= 0 || known.has(id)) continue
extras.push({ id, name: `محصول #${id}` })
}
return [...extras, ...products.value]
})
onMounted(() => {
void loadProducts()
})
async function loadProducts() {
loading.value = true
try {
const response = await getCustomersList()
const rows = normalizeProductList(response)
products.value = rows
.map((row) => ({ id: row.id, name: productLabel(row) }))
.filter((row) => row.id > 0)
.sort((a, b) => a.name.localeCompare(b.name, 'fa'))
} catch (e) {
console.error(e)
products.value = []
} finally {
loading.value = false
}
}
function onChange(val: string[] | undefined) {
emit('update:value', formatMainWalletProductIds(val ?? []))
}
</script>
@@ -10,6 +10,13 @@
<template #unCheckedChildren>{{ labels.off }}</template>
</a-switch>
<MainWalletProductsField
v-else-if="isMainWalletProducts"
:model-value="modelValue"
:disabled="disabled"
@update:value="emit('update:value', $event)"
/>
<ZaarboxPriceKeyField
v-else-if="isZaarboxPriceKey"
:model-value="modelValue"
@@ -86,6 +93,7 @@
</template>
<script setup lang="ts">
import MainWalletProductsField from './MainWalletProductsField.vue'
import ZaarboxPriceKeyField from './ZaarboxPriceKeyField.vue'
import {
parseSelectOptions,
@@ -121,6 +129,7 @@ const fieldType = computed<SettingFieldType>(() =>
)
const isZaarboxPriceKey = computed(() => props.settingKey === 'zaarbox_price_key')
const isMainWalletProducts = computed(() => props.settingKey === 'main_wallet_products')
const selectItems = computed(() =>
parseSelectOptions({
@@ -114,7 +114,7 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
title: 'منبع قیمت و تابلو',
icon: 'vuesax-linear:dollar-circle',
match: (k) =>
/^(price_provider|zaarbox_|main_product$|default_vc_number)/.test(k) ||
/^(price_provider|zaarbox_|main_product$)/.test(k) ||
k.includes('charge_percentage')
},
{
@@ -145,7 +145,7 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
id: 'infra',
title: 'زیرساخت',
icon: 'vuesax-linear:cpu',
match: (k) => /^(wss_enabled|sse_path|wss_path|wss_link|wss_trigger_path|profit_gold_api_enabled)/.test(k)
match: (k) => /^(wss_enabled|sse_path|profit_gold_api_enabled)/.test(k)
},
{
id: 'misc',
@@ -3,6 +3,43 @@ import { isStoreApp } from '@/utils/config'
export type SettingFieldType = 'text' | 'number' | 'price' | 'switch' | 'select' | 'longtext' | 'time'
export const MAIN_WALLET_PRODUCTS_DEFAULT = '13'
/** پارس setting `main_wallet_products` — id با کاما یا JSON */
export function parseMainWalletProductIds(value: string | undefined | null): string[] {
const raw = (value ?? '').trim()
if (!raw) return []
try {
const decoded = JSON.parse(raw)
if (Array.isArray(decoded)) {
return decoded
.map((part) => String(part).trim())
.filter((part) => /^\d+$/.test(part))
}
} catch {
// not JSON — comma-separated
}
return raw
.split(',')
.map((part) => part.trim())
.filter((part) => /^\d+$/.test(part))
}
export function formatMainWalletProductIds(ids: Array<string | number>): string {
const unique = [...new Set(ids.map((id) => String(id).trim()).filter((id) => /^\d+$/.test(id)))]
if (unique.length === 0) {
return MAIN_WALLET_PRODUCTS_DEFAULT
}
return unique.join(',')
}
export function normalizeMainWalletProductsValue(value: string | undefined | null): string {
const formatted = formatMainWalletProductIds(parseMainWalletProductIds(value))
return formatted || MAIN_WALLET_PRODUCTS_DEFAULT
}
/** کلیدهای legacy — در پنل نمایش داده نمی‌شوند */
export const HIDDEN_LEGACY_SETTING_KEYS = new Set([
'main_product_price',
@@ -56,7 +93,6 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
fee_single: 'price',
buy_fee: 'price',
sell_fee: 'price',
fee_cash: 'price',
fee_free: 'number',
min_withdraw: 'price',
min_first_buy: 'price',
@@ -209,6 +245,9 @@ export function formatValueForSave(
if (type === 'time') {
return normalizeTimeValue(value)
}
if (key === 'main_wallet_products') {
return normalizeMainWalletProductsValue(value)
}
return value ?? ''
}
@@ -316,8 +355,8 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
'روشن: امکان ارسال پیامک گروهی از پنل ادمین فعال است.',
profit_gold_api_enabled:
'روشن: محاسبه سود طلا از API خارجی فعال است. در اکثر نصب‌ها خاموش می‌ماند.',
default_vc_number:
'پیشوند اختیاری برای شماره‌گذاری حواله‌ها در حسابداری خارجی. خالی = بدون پیشوند.',
main_wallet_products:
'محصولاتی که موجودی‌شان از کیف طلای اصلی خوانده می‌شود. می‌توانید چند محصول انتخاب کنید. اگر خالی بماند، پیش‌فرض محصول ۱۳ است.',
unit:
'واحد نمایش وزن در دارایی‌ها، کیف پول و جزئیات معاملات (گرم یا کیلوگرم).',
chart_use_local_data:
@@ -342,10 +381,8 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
'شناسه مشتری در سامانه حسابداری که تراکنش‌ها به نام آن ثبت می‌شوند.',
payping_gateway_name:
'نام درگاه پرداخت فعال که به واسط درگاه ارسال می‌شود.',
payping_gateway_item:
'کد درگاه (تکی). اگر لیست چرخشی درگاه پر باشد، اولویت با آن است.',
payping_gateway_items:
'لیست کدهای درگاه با ویرگول برای چرخش بین چند پذیرنده. خالی = استفاده از کد تکی.',
'لیست کدهای درگاه با ویرگول برای چرخش بین چند پذیرنده.',
payping_start_url:
'آدرس API شروع پرداخت (هدایت مشتری به درگاه).',
payping_verify_url: