added charge for all products 2

This commit is contained in:
2026-07-11 22:31:45 +03:30
parent 654002f1d6
commit 6107a2cbf3
13 changed files with 1010 additions and 213 deletions
+3
View File
@@ -10,6 +10,7 @@ export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
_index: typeof import('./src/core/components/_sample/_index.vue')['default']
AAlert: typeof import('ant-design-vue/es')['Alert']
AAvatar: typeof import('ant-design-vue/es')['Avatar']
ABadge: typeof import('ant-design-vue/es')['Badge']
AButton: typeof import('ant-design-vue/es')['Button']
@@ -42,6 +43,7 @@ declare module '@vue/runtime-core' {
APagination: typeof import('ant-design-vue/es')['Pagination']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
APopover: typeof import('ant-design-vue/es')['Popover']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARow: typeof import('ant-design-vue/es')['Row']
@@ -56,6 +58,7 @@ declare module '@vue/runtime-core' {
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
AUpload: typeof import('ant-design-vue/es')['Upload']
BaseButton: typeof import('./src/core/components/BaseButton/index.ts')['default']
BaseCard: typeof import('./src/core/components/BaseCard/index.ts')['default']
BaseCollapse: typeof import('./src/components/BaseCollapse.vue')['default']
+23 -2
View File
@@ -60,6 +60,9 @@
<div class="flex cursor-pointer justify-center pt-2" @click="visibleProxy = false">
<span class="w-12 rounded-sm border-t-2 border-t-solid border-t-gray-500"></span>
</div>
<div v-if="title" class="basic-action-sheet__header">
<h3 class="basic-action-sheet__title">{{ title }}</h3>
</div>
<div class="basic-action-sheet__content">
<slot />
</div>
@@ -167,7 +170,8 @@ function handleOk() {
.basic-action-sheet {
.ant-drawer-content {
border-radius: 16px 16px 0 0;
overflow: hidden;
overflow-x: visible;
overflow-y: hidden;
}
.ant-drawer-body {
@@ -185,10 +189,27 @@ function handleOk() {
.basic-action-sheet__content {
flex: 1;
min-width: 0;
overflow-y: auto;
padding: 0 16px 12px;
}
.basic-action-sheet__header {
flex: 0 0 auto;
padding: 8px 16px 12px;
border-bottom: 1px solid var(--admin-border);
background: var(--admin-bg-elevated);
}
.basic-action-sheet__title {
margin: 0;
color: var(--admin-text);
font-size: 1rem;
font-weight: 700;
line-height: 1.6;
text-align: right;
}
.basic-action-sheet--compact {
.ant-drawer-content-wrapper {
height: auto !important;
@@ -209,7 +230,7 @@ function handleOk() {
.basic-action-sheet__container--compact .basic-action-sheet__content {
flex: 0 1 auto;
max-height: calc(88vh - 40px);
max-height: calc(88vh - 92px);
}
.basic-action-sheet__footer {
@@ -1,8 +1,14 @@
<template>
<div class="customer-wallet-balance" :class="compact ? 'space-y-1.5' : 'space-y-3'">
<div
class="customer-wallet-balance"
:class="[
compact ? 'customer-wallet-balance--compact space-y-1.5' : 'space-y-3',
layout === 'sidebar' ? 'customer-wallet-balance--sidebar' : '',
]"
>
<div
v-if="loading"
class="rounded-xl border border-gray-200 bg-gray-50 text-center text-sm text-gray-500"
class="customer-wallet-balance__placeholder rounded-xl text-center text-sm"
:class="compact ? 'px-3 py-3' : 'px-4 py-6'"
>
در حال بارگذاری موجودی…
@@ -16,24 +22,30 @@
{{ error }}
</div>
<div v-else-if="summary && compact" class="customer-wallet-balance__minimal">
<div
v-else-if="summary && compact"
ref="minimalTrackRef"
class="customer-wallet-balance__minimal-track"
>
<div class="customer-wallet-balance__minimal">
<template v-if="chargeTargets.length">
<button
<component
:is="interactive ? 'button' : 'div'"
v-for="target in chargeTargets"
:key="target.key"
type="button"
:type="interactive ? 'button' : undefined"
class="customer-wallet-balance__minimal-item"
:class="[
targetToneClass(target),
{ 'customer-wallet-balance__minimal-item--inactive': target.active === false },
{ 'customer-wallet-balance__minimal-item--selected': target.key === selectedChargeKey },
{ 'customer-wallet-balance__minimal-item--selected': interactive && target.key === selectedChargeKey },
]"
:title="target.label"
@click="emit('selectChargeTarget', target.key)"
@click="interactive ? emit('selectChargeTarget', target.key) : undefined"
>
<span class="customer-wallet-balance__minimal-label">{{ minimalTargetLabel(target) }}</span>
<span class="customer-wallet-balance__minimal-value">{{ balanceForTarget(target) }}</span>
</button>
</component>
</template>
<template v-else>
<div
@@ -50,68 +62,83 @@
<span class="customer-wallet-balance__minimal-value">{{ usePrice(summary.cash_wallet ?? 0, true, '') }}</span>
</div>
</template>
</div>
</div>
<template v-else-if="summary">
<div
v-for="metal in summary.metal_totals"
:key="metal.metal_id"
class="overflow-hidden rounded-xl border border-gray-200"
>
<div class="flex items-center justify-between gap-3 bg-emerald-50 px-4 py-3">
<div class="flex min-w-0 items-center gap-2">
<BaseIcon icon="vuesax-linear:coin" size="18" color="#15803d" />
<span class="truncate font-700 text-emerald-900">{{ metal.label }}</span>
</div>
<span class="shrink-0 font-800 text-emerald-800">{{ formatMetalTotal(metal) }}</span>
</div>
<div v-else-if="summary" class="customer-wallet-balance__full">
<p v-if="layout === 'sidebar'" class="customer-wallet-balance__sidebar-title">موجودی دارایی‌ها</p>
<div class="customer-wallet-balance__overview">
<article
v-for="metal in summary.metal_totals"
:key="metal.metal_id"
class="customer-wallet-balance__summary-card"
:class="groupToneClass(metal.metal_id)"
>
<span class="customer-wallet-balance__summary-label">{{ minimalMetalLabel(metal) }}</span>
<span class="customer-wallet-balance__summary-value">{{ formatMetalTotal(metal) }}</span>
</article>
<div v-if="linesForMetal(metal.metal_id).length" class="divide-y divide-gray-100 bg-white">
<div
v-for="line in linesForMetal(metal.metal_id)"
:key="line.key"
class="flex items-center justify-between gap-3 px-4 py-2.5 text-sm"
>
<span class="min-w-0 truncate text-gray-600">{{ line.label }}</span>
<span class="shrink-0 font-600 text-gray-900">{{ line.value }}</span>
</div>
</div>
<article class="customer-wallet-balance__summary-card customer-wallet-balance__group--cash">
<span class="customer-wallet-balance__summary-label">ریالی</span>
<span class="customer-wallet-balance__summary-value">{{ usePrice(summary.total_rial_value ?? 0, true, '') }}</span>
</article>
</div>
<div class="overflow-hidden rounded-xl border border-gray-200">
<div class="flex items-center justify-between gap-3 bg-sky-50 px-4 py-3">
<div class="flex items-center gap-2">
<BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#0369a1" />
<span class="font-700 text-sky-900">کل دارایی ریالی</span>
</div>
<span class="font-800 text-sky-800">{{ usePrice(summary.total_rial_value ?? 0, true, '') }}</span>
</div>
<div class="customer-wallet-balance__details">
<div class="customer-wallet-balance__details-title">جزئیات موجودی</div>
<div class="divide-y divide-gray-100 bg-white">
<div class="flex items-center justify-between gap-3 px-4 py-2.5 text-sm">
<span class="text-gray-600">کیف پول ریالی</span>
<span class="font-600">{{ usePrice(summary.cash_wallet ?? 0, true, '') }}</span>
<template v-for="metal in summary.metal_totals" :key="`detail-${metal.metal_id}`">
<section
v-if="linesForMetal(metal.metal_id).length"
class="customer-wallet-balance__detail-group"
:class="groupToneClass(metal.metal_id)"
>
<div class="customer-wallet-balance__detail-group-title">
<span class="customer-wallet-balance__group-dot" aria-hidden="true" />
<span>{{ metal.label }}</span>
</div>
<div
v-for="line in linesForMetal(metal.metal_id)"
:key="line.key"
class="customer-wallet-balance__detail-row"
:class="{ 'customer-wallet-balance__detail-row--strong': line.key === 'total' }"
>
<span class="customer-wallet-balance__detail-label">{{ line.label }}</span>
<span class="customer-wallet-balance__detail-value">{{ line.value }}</span>
</div>
</section>
</template>
<section class="customer-wallet-balance__detail-group customer-wallet-balance__group--cash">
<div class="customer-wallet-balance__detail-group-title">
<span class="customer-wallet-balance__group-dot" aria-hidden="true" />
<span>دارایی ریالی</span>
</div>
<div class="customer-wallet-balance__detail-row">
<span class="customer-wallet-balance__detail-label">کیف پول ریالی</span>
<span class="customer-wallet-balance__detail-value">{{ usePrice(summary.cash_wallet ?? 0, true, '') }}</span>
</div>
<div
v-if="Number(summary.assets_rial_value ?? 0) > 0"
class="flex items-center justify-between gap-3 px-4 py-2.5 text-sm"
class="customer-wallet-balance__detail-row"
>
<span class="text-gray-600">ارزش ریالی فلزات</span>
<span class="font-600">{{ usePrice(summary.assets_rial_value ?? 0, true, '') }}</span>
<span class="customer-wallet-balance__detail-label">ارزش ریالی فلزات</span>
<span class="customer-wallet-balance__detail-value">{{ usePrice(summary.assets_rial_value ?? 0, true, '') }}</span>
</div>
<div class="flex items-center justify-between gap-3 px-4 py-2.5 text-sm font-700">
<span>مجموع کل</span>
<span>{{ usePrice(summary.total_rial_value ?? 0, true, '') }}</span>
<div class="customer-wallet-balance__detail-row customer-wallet-balance__detail-row--strong">
<span class="customer-wallet-balance__detail-label">مجموع کل</span>
<span class="customer-wallet-balance__detail-value">{{ usePrice(summary.total_rial_value ?? 0, true, '') }}</span>
</div>
</div>
</section>
</div>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { getCustomerAssetsSummary } from '@/api/customer'
import { useHorizontalTouchScroll } from '@/components/mobile/composables/useHorizontalTouchScroll'
import type { ChargeTarget } from '@/model/wallet'
import type { WalletAssetsSummary, WalletInventoryCard, WalletMetalTotal } from '@/model/walletAssets'
import { usePrice } from '@core/utils/usePrice'
@@ -122,14 +149,20 @@ const props = withDefaults(
customerId?: number | null
/** نمایش مینیمال برای مودال‌ها */
compact?: boolean
/** چیدمان ستون کناری در اطلاعات کاربر */
layout?: 'default' | 'sidebar'
/** گزینه‌های شارژ — برای نمایش موجودی هر محصول */
chargeTargets?: ChargeTarget[]
selectedChargeKey?: string
/** false = فقط نمایش، بدون کلیک */
interactive?: boolean
}>(),
{
compact: false,
layout: 'default',
chargeTargets: () => [],
selectedChargeKey: '',
interactive: true,
},
)
@@ -167,9 +200,23 @@ function minimalMetalToneClass(metal: WalletMetalTotal) {
function minimalTargetLabel(target: ChargeTarget) {
if (target.kind === 'money') return 'نقدی'
const label = target.label.replace(/\s*\(کیف اصلی\)\s*$/u, '').trim()
if (label.length <= 14) return label
return `${label.slice(0, 13)}…`
return target.label
.replace(/\s*\(کیف اصلی\)\s*$/u, '')
.replace(/\s*\(غیرفعال\)\s*$/u, '')
.trim()
}
function groupToneClass(metalId: number | string | undefined) {
switch (Number(metalId)) {
case 1:
return 'customer-wallet-balance__group--gold'
case 2:
return 'customer-wallet-balance__group--silver'
case 3:
return 'customer-wallet-balance__group--copper'
default:
return 'customer-wallet-balance__group--product'
}
}
function targetToneClass(target: ChargeTarget) {
@@ -210,6 +257,9 @@ function balanceForTarget(target: ChargeTarget) {
const loading = ref(false)
const error = ref<string | null>(null)
const summary = ref<WalletAssetsSummary | null>(null)
const minimalTrackRef = ref<HTMLElement | null>(null)
useHorizontalTouchScroll(minimalTrackRef)
watch(
() => props.customerId,
@@ -287,36 +337,262 @@ defineExpose({ reload: () => props.customerId && load(props.customerId) })
</script>
<style scoped>
.customer-wallet-balance__minimal {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(4.75rem, 1fr));
gap: 0.35rem;
padding: 0.45rem;
border-radius: 0.6rem;
border: 1px solid #e2e8f0;
background: #f8fafc;
max-height: 14rem;
.customer-wallet-balance--compact {
width: 100%;
min-width: 0;
max-width: 100%;
}
.customer-wallet-balance__placeholder {
border: 1px solid var(--admin-border, #e2e8f0);
background: var(--admin-bg-muted, #f8fafc);
color: var(--admin-text-secondary, #64748b);
}
/* ---------- نمای کامل (خلاصه + جزئیات) ---------- */
.customer-wallet-balance__full {
width: 100%;
min-width: 0;
}
.customer-wallet-balance--sidebar .customer-wallet-balance__full {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.customer-wallet-balance__sidebar-title {
margin: 0;
font-size: 0.8125rem;
font-weight: 700;
color: var(--admin-text-secondary, #64748b);
}
.customer-wallet-balance--sidebar .customer-wallet-balance__overview {
grid-template-columns: 1fr;
}
.customer-wallet-balance--sidebar .customer-wallet-balance__details {
max-height: min(52vh, 26rem);
overflow-y: auto;
}
.customer-wallet-balance__minimal-item {
.customer-wallet-balance__overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
gap: 0.5rem;
}
.customer-wallet-balance__summary-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.1rem;
min-width: 0;
padding: 0.35rem 0.25rem;
align-items: center;
justify-content: space-between;
gap: 0.6rem;
border-radius: 0.65rem;
border: 1px solid color-mix(in srgb, var(--wallet-accent, #94a3b8) 28%, transparent);
background: color-mix(in srgb, var(--wallet-accent, #94a3b8) 10%, var(--admin-bg-elevated, #fff));
padding: 0.55rem 0.7rem;
}
.customer-wallet-balance__summary-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.75rem;
font-weight: 700;
color: var(--admin-text-secondary, #64748b);
}
.customer-wallet-balance__summary-value {
flex: 0 0 auto;
font-size: 0.8125rem;
font-weight: 800;
color: var(--wallet-accent-strong, var(--admin-text, #0f172a));
white-space: nowrap;
}
.customer-wallet-balance__details {
margin-top: 0.6rem;
overflow: hidden;
border-radius: 0.75rem;
border: 1px solid var(--admin-border, #e2e8f0);
background: var(--admin-bg-elevated, #fff);
}
.customer-wallet-balance__details-title {
padding: 0.55rem 0.8rem;
border-bottom: 1px solid var(--admin-border, #e2e8f0);
color: var(--admin-text-secondary, #64748b);
font-size: 0.75rem;
font-weight: 700;
}
.customer-wallet-balance__detail-group + .customer-wallet-balance__detail-group {
border-top: 1px solid var(--admin-border, #e2e8f0);
}
.customer-wallet-balance__detail-group-title {
display: flex;
align-items: center;
gap: 0.45rem;
padding: 0.55rem 0.8rem 0.25rem;
color: var(--admin-text, #0f172a);
font-size: 0.75rem;
font-weight: 800;
}
.customer-wallet-balance__group-dot {
width: 0.45rem;
height: 0.45rem;
flex: 0 0 auto;
border-radius: 9999px;
background: var(--wallet-accent, var(--admin-text-secondary, #94a3b8));
}
.customer-wallet-balance__detail-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 0.75rem;
padding: 0.45rem 0.8rem;
font-size: 0.8125rem;
}
.customer-wallet-balance__detail-row + .customer-wallet-balance__detail-row {
border-top: 1px solid color-mix(in srgb, var(--admin-border, #e2e8f0) 70%, transparent);
}
.customer-wallet-balance__detail-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--admin-text-secondary, #64748b);
}
.customer-wallet-balance__detail-value {
font-weight: 600;
color: var(--admin-text, #0f172a);
white-space: nowrap;
}
.customer-wallet-balance__detail-row--strong {
background: color-mix(in srgb, var(--wallet-accent, #94a3b8) 8%, transparent);
}
.customer-wallet-balance__detail-row--strong .customer-wallet-balance__detail-label,
.customer-wallet-balance__detail-row--strong .customer-wallet-balance__detail-value {
font-weight: 800;
color: var(--admin-text, #0f172a);
}
.customer-wallet-balance__group--gold {
--wallet-accent: #d97706;
--wallet-accent-strong: #b45309;
}
.customer-wallet-balance__group--silver {
--wallet-accent: #64748b;
--wallet-accent-strong: #475569;
}
.customer-wallet-balance__group--copper {
--wallet-accent: #ea580c;
--wallet-accent-strong: #c2410c;
}
.customer-wallet-balance__group--cash {
--wallet-accent: #0284c7;
--wallet-accent-strong: #0369a1;
}
.customer-wallet-balance__group--product {
--wallet-accent: var(--admin-text-secondary, #64748b);
--wallet-accent-strong: var(--admin-text, #0f172a);
}
:global(html.is-dark) .customer-wallet-balance__group--gold {
--wallet-accent: #fbbf24;
--wallet-accent-strong: #fcd34d;
}
:global(html.is-dark) .customer-wallet-balance__group--silver {
--wallet-accent: #94a3b8;
--wallet-accent-strong: #cbd5e1;
}
:global(html.is-dark) .customer-wallet-balance__group--copper {
--wallet-accent: #fb923c;
--wallet-accent-strong: #fdba74;
}
:global(html.is-dark) .customer-wallet-balance__group--cash {
--wallet-accent: #60a5fa;
--wallet-accent-strong: #93c5fd;
}
.customer-wallet-balance__minimal-track {
display: block;
width: 100%;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
overscroll-behavior-x: contain;
scrollbar-width: thin;
scrollbar-color: var(--admin-border-strong, #cbd5e1) transparent;
padding: 0.45rem 0.35rem;
border-radius: 0.6rem;
border: 1px solid var(--admin-border, #e2e8f0);
background: var(--admin-bg-muted, #f8fafc);
direction: ltr;
}
.customer-wallet-balance__minimal-track::-webkit-scrollbar {
height: 0.35rem;
}
.customer-wallet-balance__minimal-track::-webkit-scrollbar-thumb {
border-radius: 9999px;
background: var(--admin-border-strong, #cbd5e1);
}
.customer-wallet-balance__minimal {
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: stretch;
gap: 0.35rem;
width: max-content;
min-width: 100%;
direction: rtl;
}
.customer-wallet-balance__minimal-item {
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
flex: 0 0 6.25rem;
width: 6.25rem;
min-width: 6.25rem;
min-height: 4rem;
gap: 0.25rem;
padding: 0.45rem 0.35rem;
border-radius: 0.45rem;
background: #fff;
background: var(--wallet-tile-bg, var(--admin-bg-elevated, #fff));
text-align: center;
border: 1px solid transparent;
border: 1px solid var(--wallet-tile-border, var(--admin-border, #e2e8f0));
cursor: default;
box-sizing: border-box;
}
button.customer-wallet-balance__minimal-item {
cursor: pointer;
transition: box-shadow 0.15s ease, transform 0.15s ease;
transition: box-shadow 0.15s ease, transform 0.15s ease, border-color 0.15s ease;
}
button.customer-wallet-balance__minimal-item:hover {
@@ -324,7 +600,9 @@ button.customer-wallet-balance__minimal-item:hover {
}
.customer-wallet-balance__minimal-item--selected {
box-shadow: 0 0 0 2px #2563eb;
border-width: 2px;
padding: calc(0.45rem - 1px) calc(0.35rem - 1px);
box-shadow: none;
}
.customer-wallet-balance__minimal-item--inactive {
@@ -332,84 +610,98 @@ button.customer-wallet-balance__minimal-item:hover {
}
.customer-wallet-balance__minimal-item--gold {
background: #fffbeb;
border-color: #fde68a;
--wallet-tile-bg: #fffbeb;
--wallet-tile-border: #fde68a;
--wallet-tile-label: #92400e;
--wallet-tile-value: #b45309;
}
.customer-wallet-balance__minimal-item--silver {
background: #f8fafc;
border-color: #cbd5e1;
--wallet-tile-bg: #f8fafc;
--wallet-tile-border: #cbd5e1;
--wallet-tile-label: #64748b;
--wallet-tile-value: #334155;
}
.customer-wallet-balance__minimal-item--copper {
background: #fff7ed;
border-color: #fdba74;
--wallet-tile-bg: #fff7ed;
--wallet-tile-border: #fdba74;
--wallet-tile-label: #c2410c;
--wallet-tile-value: #9a3412;
}
.customer-wallet-balance__minimal-item--cash {
background: #eff6ff;
border-color: #93c5fd;
--wallet-tile-bg: #eff6ff;
--wallet-tile-border: #93c5fd;
--wallet-tile-label: #0369a1;
--wallet-tile-value: #0284c7;
}
.customer-wallet-balance__minimal-item--product {
background: #f8fafc;
border-color: #e2e8f0;
--wallet-tile-bg: var(--admin-bg-elevated, #fff);
--wallet-tile-border: var(--admin-border, #e2e8f0);
--wallet-tile-label: #475569;
--wallet-tile-value: #1e293b;
}
.customer-wallet-balance__minimal-label {
font-size: 0.6875rem;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
align-self: start;
min-height: 0;
max-height: 2.45em;
font-size: 0.625rem;
font-weight: 600;
color: #64748b;
max-width: 100%;
line-height: 1.25;
color: var(--wallet-tile-label, var(--admin-text-secondary, #64748b));
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
white-space: normal;
word-break: break-word;
}
.customer-wallet-balance__minimal-value {
font-size: 0.75rem;
align-self: end;
font-size: 0.6875rem;
font-weight: 700;
color: #0f172a;
line-height: 1.25;
color: var(--wallet-tile-value, var(--admin-text, #0f172a));
white-space: nowrap;
}
.customer-wallet-balance__minimal-item--gold .customer-wallet-balance__minimal-label {
color: #92400e;
:global(html.is-dark) .customer-wallet-balance__minimal-item--gold {
--wallet-tile-bg: rgba(245, 158, 11, 0.14);
--wallet-tile-border: rgba(251, 191, 36, 0.28);
--wallet-tile-label: #fbbf24;
--wallet-tile-value: #fcd34d;
}
.customer-wallet-balance__minimal-item--gold .customer-wallet-balance__minimal-value {
color: #b45309;
:global(html.is-dark) .customer-wallet-balance__minimal-item--silver {
--wallet-tile-bg: rgba(148, 163, 184, 0.12);
--wallet-tile-border: rgba(148, 163, 184, 0.28);
--wallet-tile-label: #94a3b8;
--wallet-tile-value: #cbd5e1;
}
.customer-wallet-balance__minimal-item--silver .customer-wallet-balance__minimal-label {
color: #64748b;
:global(html.is-dark) .customer-wallet-balance__minimal-item--copper {
--wallet-tile-bg: rgba(249, 115, 22, 0.14);
--wallet-tile-border: rgba(251, 146, 60, 0.28);
--wallet-tile-label: #fb923c;
--wallet-tile-value: #fdba74;
}
.customer-wallet-balance__minimal-item--silver .customer-wallet-balance__minimal-value {
color: #334155;
:global(html.is-dark) .customer-wallet-balance__minimal-item--cash {
--wallet-tile-bg: rgba(59, 130, 246, 0.14);
--wallet-tile-border: rgba(96, 165, 250, 0.32);
--wallet-tile-label: #60a5fa;
--wallet-tile-value: #93c5fd;
}
.customer-wallet-balance__minimal-item--copper .customer-wallet-balance__minimal-label {
color: #c2410c;
}
.customer-wallet-balance__minimal-item--copper .customer-wallet-balance__minimal-value {
color: #9a3412;
}
.customer-wallet-balance__minimal-item--cash .customer-wallet-balance__minimal-label {
color: #0369a1;
}
.customer-wallet-balance__minimal-item--cash .customer-wallet-balance__minimal-value {
color: #0284c7;
}
.customer-wallet-balance__minimal-item--product .customer-wallet-balance__minimal-label {
color: #475569;
}
.customer-wallet-balance__minimal-item--product .customer-wallet-balance__minimal-value {
color: #1e293b;
:global(html.is-dark) .customer-wallet-balance__minimal-item--product {
--wallet-tile-bg: rgba(255, 255, 255, 0.04);
--wallet-tile-border: var(--admin-border);
--wallet-tile-label: var(--admin-text-secondary);
--wallet-tile-value: var(--admin-text);
}
</style>
+10 -2
View File
@@ -1,12 +1,12 @@
<template>
<BasicActionSheet
v-model:visible="visibleModel"
:title="''"
:title="title"
:showButton="false"
:compact="true"
:drawerHeight="drawerHeight"
>
<div v-if="item" class="space-y-3 pb-2">
<div v-if="item" class="mobile-detail-sheet__body space-y-3 pb-2">
<slot :item="item" />
</div>
</BasicActionSheet>
@@ -36,3 +36,11 @@ const visibleModel = computed({
set: (value: boolean) => emit('update:visible', value)
})
</script>
<style scoped>
.mobile-detail-sheet__body {
width: 100%;
min-width: 0;
max-width: 100%;
}
</style>
@@ -0,0 +1,69 @@
import { onBeforeUnmount, watch, type Ref } from 'vue'
function bindHorizontalTouchScroll(el: HTMLElement) {
let startX = 0
let startY = 0
let scrollStart = 0
let axis: 'x' | 'y' | null = null
const onTouchStart = (event: TouchEvent) => {
if (event.touches.length !== 1) return
startX = event.touches[0].clientX
startY = event.touches[0].clientY
scrollStart = el.scrollLeft
axis = null
}
const onTouchMove = (event: TouchEvent) => {
if (event.touches.length !== 1) return
const dx = event.touches[0].clientX - startX
const dy = event.touches[0].clientY - startY
if (!axis) {
if (Math.abs(dx) < 4 && Math.abs(dy) < 4) return
axis = Math.abs(dx) > Math.abs(dy) ? 'x' : 'y'
}
if (axis !== 'x') return
event.preventDefault()
event.stopPropagation()
el.scrollLeft = scrollStart - dx
}
el.addEventListener('touchstart', onTouchStart, { passive: true })
el.addEventListener('touchmove', onTouchMove, { passive: false })
requestAnimationFrame(() => {
const max = el.scrollWidth - el.clientWidth
if (max > 0) el.scrollLeft = max
})
return () => {
el.removeEventListener('touchstart', onTouchStart)
el.removeEventListener('touchmove', onTouchMove)
}
}
/**
* Reliable horizontal swipe inside RTL pages and nested vertical scroll areas.
*/
export function useHorizontalTouchScroll(trackRef: Ref<HTMLElement | null>) {
let cleanup: (() => void) | undefined
watch(
trackRef,
(el) => {
cleanup?.()
cleanup = undefined
if (!el) return
cleanup = bindHorizontalTouchScroll(el)
},
{ flush: 'post' },
)
onBeforeUnmount(() => {
cleanup?.()
})
}
+2 -1
View File
@@ -49,7 +49,8 @@
.ant-drawer-bottom .ant-drawer-content {
border-radius: 16px 16px 0 0;
overflow: hidden;
overflow-x: visible;
overflow-y: hidden;
}
.ant-card-body{
+160
View File
@@ -428,4 +428,164 @@ html.is-dark {
border-color: var(--admin-gold-light) !important;
box-shadow: 0 0 0 2px rgba(212, 175, 55, 0.18) !important;
}
.wallet-charge-form__direction.ant-radio-group-solid {
.ant-radio-button-wrapper {
background: var(--admin-bg-muted) !important;
border-color: var(--admin-border-strong) !important;
color: var(--admin-text) !important;
}
.ant-radio-button-wrapper:hover {
color: var(--admin-text) !important;
}
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge:hover,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge:active {
background: #16a34a !important;
border-color: #16a34a !important;
color: #fff !important;
}
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge::before,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge:hover::before,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge:active::before {
background-color: #16a34a !important;
}
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge:hover,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge:active {
background: #dc2626 !important;
border-color: #dc2626 !important;
color: #fff !important;
}
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge::before,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge:hover::before,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge:active::before {
background-color: #dc2626 !important;
}
}
}
.wallet-charge-form__direction.ant-radio-group-solid {
.ant-radio-button-wrapper {
background: var(--admin-bg-muted) !important;
border-color: var(--admin-border-strong) !important;
color: var(--admin-text) !important;
}
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge:hover,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge:active {
background: #15803d !important;
border-color: #15803d !important;
color: #fff !important;
}
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge::before,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge:hover::before,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge:active::before {
background-color: #15803d !important;
}
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge:hover,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge:active {
background: #b91c1c !important;
border-color: #b91c1c !important;
color: #fff !important;
}
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge::before,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge:hover::before,
.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge:active::before {
background-color: #b91c1c !important;
}
}
html.is-dark {
.customer-wallet-balance__minimal {
background: var(--admin-bg-muted);
border-color: var(--admin-border);
}
.customer-wallet-balance__minimal-item--gold {
background: rgba(245, 158, 11, 0.14);
border-color: rgba(251, 191, 36, 0.28);
}
.customer-wallet-balance__minimal-item--gold .customer-wallet-balance__minimal-label {
color: #fbbf24;
}
.customer-wallet-balance__minimal-item--gold .customer-wallet-balance__minimal-value {
color: #fcd34d;
}
.customer-wallet-balance__minimal-item--silver {
background: rgba(148, 163, 184, 0.12);
border-color: rgba(148, 163, 184, 0.28);
}
.customer-wallet-balance__minimal-item--silver .customer-wallet-balance__minimal-label {
color: #94a3b8;
}
.customer-wallet-balance__minimal-item--silver .customer-wallet-balance__minimal-value {
color: #cbd5e1;
}
.customer-wallet-balance__minimal-item--copper {
background: rgba(249, 115, 22, 0.14);
border-color: rgba(251, 146, 60, 0.28);
}
.customer-wallet-balance__minimal-item--copper .customer-wallet-balance__minimal-label {
color: #fb923c;
}
.customer-wallet-balance__minimal-item--copper .customer-wallet-balance__minimal-value {
color: #fdba74;
}
.customer-wallet-balance__minimal-item--cash {
background: rgba(59, 130, 246, 0.14);
border-color: rgba(96, 165, 250, 0.32);
}
.customer-wallet-balance__minimal-item--cash .customer-wallet-balance__minimal-label {
color: #60a5fa;
}
.customer-wallet-balance__minimal-item--cash .customer-wallet-balance__minimal-value {
color: #93c5fd;
}
.customer-wallet-balance__minimal-item--product {
background: rgba(255, 255, 255, 0.04);
border-color: var(--admin-border);
}
.customer-wallet-balance__minimal-item--product .customer-wallet-balance__minimal-label {
color: var(--admin-text-secondary);
}
.customer-wallet-balance__minimal-item--product .customer-wallet-balance__minimal-value {
color: var(--admin-text);
}
.wallet-charge-form__preview--charge {
background: rgba(34, 197, 94, 0.14);
border-color: rgba(34, 197, 94, 0.32);
color: #86efac;
}
.wallet-charge-form__preview--discharge {
background: rgba(239, 68, 68, 0.14);
border-color: rgba(239, 68, 68, 0.32);
color: #fca5a5;
}
}
+8
View File
@@ -58,6 +58,14 @@ export interface DashboardFinance {
profitGoldGrams?: number
/** سود ریالی: مجموع کارمزدهای کسرشده از ریال */
profitRial?: number
/** سود ریالی قبل از کسر سندهای تبدیل */
profitRialGross?: number
/** سود طلایی قبل از افزودن سندهای تبدیل */
profitGoldGramsGross?: number
/** جمع ریالی تبدیل‌شده به طلا در بازه */
profitRialConverted?: number
/** جمع گرم تبدیل‌شده از سود ریالی در بازه */
profitGoldConvertedGram?: number
/** ارزش ریالی سود طلایی با قیمت روز */
profitGoldValueRial?: number
/** تفکیک روزانهٔ سود طلایی/ریالی */
@@ -55,6 +55,9 @@
<span class="dashboard-profit-split__unit">گرم</span>
</p>
<p class="dashboard-profit-split__sub">≈ {{ formatDashboardAmount(profitGoldValueRial) }} ریال</p>
<p v-if="profitGoldConvertedGram > 0" class="dashboard-profit-split__note">
شامل {{ formatGram(profitGoldConvertedGram) }} گرم از تبدیل سود ریالی
</p>
</article>
<article class="dashboard-profit-split__card dashboard-profit-split__card--rial">
@@ -69,7 +72,14 @@
<span class="dashboard-profit-split__amount">{{ formatDashboardAmount(profitRial) }}</span>
<span class="dashboard-profit-split__unit">ریال</span>
</p>
<p class="dashboard-profit-split__sub">{{ dailyRows.length }} روز دارای معامله</p>
<p class="dashboard-profit-split__sub">
<template v-if="profitRialConverted > 0">
{{ formatGram(profitGoldConvertedGram) }} گرم تبدیل‌شده به طلا
</template>
<template v-else>
{{ dailyRows.length }} روز دارای معامله
</template>
</p>
</article>
</div>
@@ -291,15 +301,15 @@ const activeEnd = ref(rangeEnd.value)
const sectionHelp: DashboardHelp = {
description:
'کارمزد معاملات بر اساس فیلدی که مشتری وارد می‌کند تفکیک می‌شود: وقتی مشتری «مبلغ ریالی» ثابت می‌زند کارمزد از طلا کسر می‌شود (سود طلایی)، و وقتی «وزن» ثابت می‌زند کارمزد از ریال کسر می‌شود (سود ریالی). اعداد بر اساس بازهٔ انتخابی همین بخش محاسبه می‌شوند.',
formula: 'سود طلایی = جمع کارمزدِ کسرشده از طلا (گرم) | سود ریالی = جمع کارمزدِ کسرشده از ریال (ریال)',
formula: 'سود طلایی = جمع کارمزدِ کسرشده از طلا (گرم) + گرم سندهای تبدیل سود ریالی | سود ریالی = جمع کارمزدِ کسرشده از ریال − ریال تبدیل‌شده به طلا',
}
const goldHelp: DashboardHelp = {
description: 'مجموع کارمزدهایی که در معامله‌های «مبلغ‌محور» (مشتری مبلغ ریالی ثابت زده) از طلا کسر شده است.',
formula: 'مجموع کارمزد طلا (گرم) × قیمت روز = ارزش ریالی تقریبی',
description: 'مجموع کارمزدهای کسرشده از طلا به‌علاوهٔ گرم سندهای «تبدیل سود ریالی به طلا» در همین بازه.',
formula: 'کارمزد طلا (گرم) + گرم تبدیل‌شده از سود ریالی',
}
const rialHelp: DashboardHelp = {
description: 'مجموع کارمزدهایی که در معامله‌های «وزن‌محور» (مشتری وزن ثابت زده) از ریال کسر شده است.',
formula: 'مجموع کارمزد ریالی معامله‌های وزن‌محور',
description: 'سود ریالی باقی‌مانده پس از کسر سندهای تبدیل به طلا در همین بازه.',
formula: 'کارمزد ریالی − ریال تبدیل‌شده به طلا',
}
const loading = ref(false)
@@ -308,6 +318,8 @@ const finance = ref<DashboardFinance | null>(null)
const profitGoldGrams = computed(() => Number(finance.value?.profitGoldGrams ?? 0))
const profitRial = computed(() => Number(finance.value?.profitRial ?? 0))
const profitGoldValueRial = computed(() => Number(finance.value?.profitGoldValueRial ?? 0))
const profitRialConverted = computed(() => Number(finance.value?.profitRialConverted ?? 0))
const profitGoldConvertedGram = computed(() => Number(finance.value?.profitGoldConvertedGram ?? 0))
const dailyRows = computed(() => finance.value?.daily ?? [])
// صفحه‌بندی جدول روزانه
@@ -467,7 +479,7 @@ async function submitSanad() {
})
message.success('سند تبدیل سود ثبت شد')
closeSanad()
await loadConversions()
reloadAll()
} catch (e) {
console.error(e)
message.error('خطا در ثبت سند')
@@ -480,7 +492,7 @@ async function removeConversion(id: number) {
try {
await deleteProfitConversion(id)
message.success('سند حذف شد')
await loadConversions()
reloadAll()
} catch (e) {
console.error(e)
message.error('خطا در حذف سند')
@@ -595,6 +607,12 @@ reloadAll()
color: var(--admin-text-secondary);
}
.dashboard-profit-split__note {
margin: 0.2rem 0 0;
font-size: 0.6875rem;
color: var(--admin-text-secondary);
}
.dashboard-profit-split__sanad {
border-top: 1px solid var(--admin-border);
padding-top: 0.75rem;
@@ -1,78 +1,96 @@
<template>
<MobileDetailSheet v-model:visible="visibleProxy" :item="item" title="">
<MobileDetailSheet v-model:visible="visibleProxy" :item="item" :title="sheetTitle">
<template #default="{ item: row }">
<h2 class="text-base font-700 leading-relaxed text-gray-900">{{ row.name || '-' }}</h2>
<p v-if="row.mobile" class="text-sm text-gray-500" dir="ltr">{{ row.mobile }}</p>
<div class="customer-mobile-sheet">
<p v-if="row.mobile" class="customer-mobile-sheet__mobile" dir="ltr">{{ row.mobile }}</p>
<div class="customer-mobile-sheet__chips">
<span class="customer-mobile-sheet__chip" :class="row.active ? 'is-success' : 'is-danger'">
{{ row.active ? 'فعال' : 'غیرفعال' }}
</span>
<span
class="customer-mobile-sheet__chip"
:class="row.trade_active === 1 ? 'is-success' : 'is-danger'"
>
{{ row.trade_active === 1 ? 'سفارش مجاز' : 'سفارش ممنوع' }}
</span>
</div>
<section class="customer-mobile-sheet__section">
<h4 class="customer-mobile-sheet__section-title">موجودی</h4>
<CustomerWalletBalancePanel
:customer-id="row.id"
:charge-targets="chargeTargets"
:interactive="false"
compact
/>
</section>
<div class="grid grid-cols-2 gap-2">
<BaseButton class="w-full col-span-2" size="middle" type="primary" icon="vuesax-linear:wallet-add" @click="closeAndEmit('chargeWallet', row)">
شارژ کیف پول
</BaseButton>
<BaseButton
v-if="!row.is_legal"
class="w-full"
class="customer-mobile-sheet__charge-btn"
size="middle"
type="default"
icon="vuesax-linear:user-search"
@click="closeAndEmit('confirmUser', row)"
type="primary"
icon="vuesax-linear:wallet-add"
@click="closeAndEmit('chargeWallet', row)"
>
تایید کاربر
</BaseButton>
<BaseButton class="w-full" size="middle" type="default" icon="vuesax-linear:stickynote" @click="closeAndEmit('info', row)">
مشخصات کامل
</BaseButton>
<BaseButton class="w-full" size="middle" type="default" icon="vuesax-linear:edit" @click="closeAndEmit('edit', row)">
ویرایش
</BaseButton>
<BaseButton class="w-full" size="middle" type="default" icon="vuesax-linear:archive-tick" @click="closeAndEmit('archive', row)">
بایگانی
</BaseButton>
<BaseButton
class="w-full"
size="middle"
:type="row.active ? 'default' : 'primary'"
:icon="row.active ? 'vuesax-linear:user-remove' : 'vuesax-linear:user-tick'"
@click="confirmToggleActive(row)"
>
{{ row.active ? 'غیرفعال کردن' : 'فعال کردن' }}
</BaseButton>
<BaseButton
class="w-full"
size="middle"
:type="row.trade_active === 1 ? 'default' : 'primary'"
:icon="row.trade_active === 1 ? 'vuesax-linear:box-remove' : 'vuesax-linear:box-tick'"
@click="confirmToggleTrade(row)"
>
{{ row.trade_active === 1 ? 'ممنوعیت سفارش' : 'مجاز کردن سفارش' }}
شارژ / دشارژ کیف پول
</BaseButton>
<div class="customer-mobile-sheet__actions">
<BaseButton size="small" type="default" icon="vuesax-linear:stickynote" @click="closeAndEmit('info', row)">
مشخصات
</BaseButton>
<BaseButton size="small" type="default" icon="vuesax-linear:edit" @click="closeAndEmit('edit', row)">
ویرایش
</BaseButton>
<BaseButton size="small" type="default" icon="vuesax-linear:archive-tick" @click="closeAndEmit('archive', row)">
بایگانی
</BaseButton>
<BaseButton
v-if="!row.is_legal"
size="small"
type="default"
icon="vuesax-linear:user-search"
@click="closeAndEmit('confirmUser', row)"
>
تایید
</BaseButton>
<BaseButton
size="small"
:type="row.active ? 'default' : 'primary'"
:icon="row.active ? 'vuesax-linear:user-remove' : 'vuesax-linear:user-tick'"
@click="confirmToggleActive(row)"
>
{{ row.active ? 'غیرفعال' : 'فعال' }}
</BaseButton>
<BaseButton
size="small"
:type="row.trade_active === 1 ? 'default' : 'primary'"
:icon="row.trade_active === 1 ? 'vuesax-linear:box-remove' : 'vuesax-linear:box-tick'"
@click="confirmToggleTrade(row)"
>
{{ row.trade_active === 1 ? 'ممنوعیت' : 'مجاز سفارش' }}
</BaseButton>
</div>
<MobileInfoGrid>
<MobileDetailRow label="کد کاربری" :value="row.id ?? '-'" />
<MobileDetailRow label="نوع کاربر" :value="row.type_str || '-'" />
<MobileDetailRow label="کد معرف" :value="row.referral_id ?? '-'" />
<MobileDetailRow label="نام معرف" :value="row.referral?.name ?? '-'" />
<MobileDetailRow label="آخرین بازدید" :value="lastSeen(row)" value-class="text-xs" />
</MobileInfoGrid>
</div>
<CustomerWalletBalancePanel :customer-id="row?.id" />
<MobileInfoGrid>
<MobileDetailRow label="وضعیت کاربر" full-width :value-class="row.active ? 'text-green-700' : 'text-red-600'">
{{ row.active ? 'فعال' : 'غیرفعال' }}
</MobileDetailRow>
<MobileDetailRow
label="وضعیت سفارش دهی"
full-width
:value-class="row.trade_active === 1 ? 'text-green-700' : 'text-red-600'"
>
{{ row.trade_active === 1 ? 'مجاز' : 'غیر مجاز' }}
</MobileDetailRow>
<MobileDetailRow label="کد کاربری" :value="row.id ?? '-'" />
<MobileDetailRow label="نوع کاربر" :value="row.type_str || '-'" />
<MobileDetailRow label="کد معرف" :value="row.referral_id ?? '-'" />
<MobileDetailRow label="نام معرف کاربر" :value="row.referral?.name ?? '-'" />
<MobileDetailRow label="آخرین بازدید" :value="lastSeen(row)" value-class="text-xs" />
</MobileInfoGrid>
</template>
</MobileDetailSheet>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { Modal } from 'ant-design-vue'
import type { Customer } from '@/model/customer'
import type { ChargeTarget } from '@/model/wallet'
import { getChargeTargets } from '@/api/wallet'
import { MODAL_ABOVE_ACTION_SHEET_Z_INDEX } from '@/constants/overlayZIndex'
import MobileDetailRow from '@/components/mobile/MobileDetailRow.vue'
import MobileDetailSheet from '@/components/mobile/MobileDetailSheet.vue'
@@ -96,11 +114,15 @@ const emit = defineEmits<{
(e: 'confirmUser', item: Customer): void
}>()
const chargeTargets = ref<ChargeTarget[]>([])
const visibleProxy = computed({
get: () => props.visible,
set: (value: boolean) => emit('update:visible', value)
set: (value: boolean) => emit('update:visible', value),
})
const sheetTitle = computed(() => props.item?.name?.trim() || 'مشتری')
type SheetEmit = 'archive' | 'chargeWallet' | 'info' | 'confirmUser' | 'edit'
function closeAndEmit(event: SheetEmit, row: Customer) {
@@ -115,7 +137,7 @@ function confirmToggleActive(row: Customer) {
cancelText: 'رد',
centered: true,
zIndex: MODAL_ABOVE_ACTION_SHEET_Z_INDEX,
onOk: () => emit('toggleActive', row)
onOk: () => emit('toggleActive', row),
})
}
@@ -126,11 +148,98 @@ function confirmToggleTrade(row: Customer) {
cancelText: 'رد',
centered: true,
zIndex: MODAL_ABOVE_ACTION_SHEET_Z_INDEX,
onOk: () => emit('toggleTrade', row)
onOk: () => emit('toggleTrade', row),
})
}
function lastSeen(row: Customer) {
return row.last_seen_at ? toJalali(row.last_seen_at) : '-'
}
async function loadChargeTargets() {
try {
const res = await getChargeTargets()
chargeTargets.value = res?.targets ?? []
} catch {
chargeTargets.value = []
}
}
watch(
() => [props.visible, props.item?.id] as const,
([open]) => {
if (open) void loadChargeTargets()
},
)
</script>
<style scoped>
.customer-mobile-sheet {
display: flex;
flex-direction: column;
gap: 0.75rem;
width: 100%;
min-width: 0;
max-width: 100%;
}
.customer-mobile-sheet__mobile {
margin: 0;
font-size: 0.8125rem;
color: var(--admin-text-secondary);
}
.customer-mobile-sheet__chips {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.customer-mobile-sheet__chip {
padding: 0.2rem 0.55rem;
border-radius: 9999px;
font-size: 0.6875rem;
font-weight: 700;
}
.customer-mobile-sheet__chip.is-success {
background: rgba(34, 197, 94, 0.14);
color: #15803d;
}
.customer-mobile-sheet__chip.is-danger {
background: rgba(239, 68, 68, 0.14);
color: #b91c1c;
}
.customer-mobile-sheet__section {
width: 100%;
min-width: 0;
max-width: 100%;
}
.customer-mobile-sheet__section-title {
margin: 0 0 0.35rem;
font-size: 0.75rem;
font-weight: 700;
color: var(--admin-text-secondary);
}
.customer-mobile-sheet__charge-btn {
width: 100%;
}
.customer-mobile-sheet__actions {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.35rem;
}
:global(html.is-dark) .customer-mobile-sheet__chip.is-success {
color: #4ade80;
}
:global(html.is-dark) .customer-mobile-sheet__chip.is-danger {
color: #f87171;
}
</style>
@@ -138,12 +138,27 @@ defineExpose({
margin-bottom: 0.65rem;
}
.wallet-charge-form :deep(.ant-form-item-label > label) {
color: var(--admin-text, rgba(0, 0, 0, 0.88));
}
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper) {
width: 50%;
text-align: center;
height: 2.35rem;
line-height: 2.25rem;
font-weight: 700;
background: var(--admin-bg-muted, #fafafa) !important;
border-color: var(--admin-border-strong, #d9d9d9) !important;
color: var(--admin-text, rgba(0, 0, 0, 0.88)) !important;
}
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper:not(:first-child)::before) {
background-color: var(--admin-border-strong, #d9d9d9) !important;
}
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper:hover) {
color: var(--admin-text, rgba(0, 0, 0, 0.88)) !important;
}
.wallet-charge-form__radio-icon {
@@ -180,29 +195,29 @@ defineExpose({
.wallet-charge-form__preview--charge {
background: rgba(34, 197, 94, 0.12);
border-color: rgba(34, 197, 94, 0.35);
color: #86efac;
color: #15803d;
}
.wallet-charge-form__preview--charge .wallet-charge-form__preview-sign {
background: rgba(34, 197, 94, 0.2);
color: #4ade80;
color: #16a34a;
}
.wallet-charge-form__preview--discharge {
background: rgba(239, 68, 68, 0.12);
border-color: rgba(239, 68, 68, 0.35);
color: #fca5a5;
color: #b91c1c;
}
.wallet-charge-form__preview--discharge .wallet-charge-form__preview-sign {
background: rgba(239, 68, 68, 0.2);
color: #f87171;
color: #dc2626;
}
.wallet-charge-form__hint {
margin-top: 0.35rem;
font-size: 0.75rem;
opacity: 0.75;
color: var(--admin-text-secondary, rgba(0, 0, 0, 0.45));
}
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge) {
@@ -211,9 +226,59 @@ defineExpose({
color: #fff !important;
}
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge::before) {
background-color: #15803d !important;
}
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge) {
background: #b91c1c !important;
border-color: #b91c1c !important;
color: #fff !important;
}
.wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge::before) {
background-color: #b91c1c !important;
}
:global(html.is-dark) .wallet-charge-form__preview--charge {
background: rgba(34, 197, 94, 0.14);
border-color: rgba(34, 197, 94, 0.32);
color: #86efac;
}
:global(html.is-dark) .wallet-charge-form__preview--charge .wallet-charge-form__preview-sign {
background: rgba(34, 197, 94, 0.22);
color: #4ade80;
}
:global(html.is-dark) .wallet-charge-form__preview--discharge {
background: rgba(239, 68, 68, 0.14);
border-color: rgba(239, 68, 68, 0.32);
color: #fca5a5;
}
:global(html.is-dark) .wallet-charge-form__preview--discharge .wallet-charge-form__preview-sign {
background: rgba(239, 68, 68, 0.22);
color: #f87171;
}
:global(html.is-dark) .wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge) {
background: #16a34a !important;
border-color: #16a34a !important;
color: #fff !important;
}
:global(html.is-dark) .wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--charge::before) {
background-color: #16a34a !important;
}
:global(html.is-dark) .wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge) {
background: #dc2626 !important;
border-color: #dc2626 !important;
color: #fff !important;
}
:global(html.is-dark) .wallet-charge-form__direction :deep(.ant-radio-button-wrapper-checked.wallet-charge-form__radio--discharge::before) {
background-color: #dc2626 !important;
}
</style>
@@ -15,14 +15,15 @@
/>
</div>
<CustomerWalletBalancePanel
v-if="resolvedCustomerId && !targetsLoading"
:customer-id="resolvedCustomerId"
:charge-targets="targets"
:selected-charge-key="selectedTargetKey"
compact
@select-charge-target="selectedTargetKeyProxy = $event"
/>
<div v-if="resolvedCustomerId && !targetsLoading" class="wallet-charge-modal-body__assets">
<CustomerWalletBalancePanel
:customer-id="resolvedCustomerId"
:charge-targets="targets"
:selected-charge-key="selectedTargetKey"
compact
@select-charge-target="selectedTargetKeyProxy = $event"
/>
</div>
<div class="wallet-charge-modal-body__section">
<label class="wallet-charge-modal-body__label">محصول / کیف پول</label>
@@ -151,6 +152,13 @@ defineExpose({
padding: 0 0.75rem 0.25rem;
}
.wallet-charge-modal-body__assets {
margin-bottom: 0.75rem;
width: 100%;
min-width: 0;
max-width: 100%;
}
.wallet-charge-modal-body__section {
display: flex;
flex-direction: column;
+39 -4
View File
@@ -132,10 +132,12 @@
:showButton="false"
@close="userInfoVisible = false"
>
<div v-if="isMobile" class="mb-3 text-base font-700">اطلاعات کاربر</div>
<a-tabs v-if="userInfo" v-model:activeKey="activeKey">
<a-tab-pane key="اطلاعات جامع" tab="اطلاعات جامع">
<div class="space-y-2">
<div
class="grid min-w-0 grid-cols-1 items-start gap-3 lg:grid-cols-[minmax(17rem,21rem)_minmax(0,1fr)] xl:grid-cols-[minmax(18rem,23rem)_minmax(0,1fr)]"
>
<div class="min-w-0 space-y-2 lg:order-2">
<div class="admin-stat-muted rounded-lg w-full flex flex-col gap-3 p-3 md:flex-row md:justify-between">
<div>
<div class="font-bold">{{ userInfo.name }}</div>
@@ -172,7 +174,7 @@
</div>
</div>
<div class="grid grid-cols-2 gap-2 md:grid-cols-4">
<div class="grid min-w-0 grid-cols-2 gap-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
<BasicInfoItem title="کد کاربری" :value="userInfo?.id" />
<BasicInfoItem title="نوع کاربر" :value="userInfo?.type_str" />
<BasicInfoItem
@@ -215,7 +217,26 @@
</BasicInfoItem>
</div>
<CustomerWalletBalancePanel v-if="userInfo?.id" :customer-id="userInfo.id" class="mt-2" />
<CustomerWalletBalancePanel
v-if="userInfo?.id && isMobile"
:customer-id="userInfo.id"
compact
:charge-targets="infoChargeTargets"
:interactive="false"
class="mt-2"
/>
</div>
<aside
v-if="userInfo?.id && !isMobile"
class="surface-muted min-w-0 rounded-xl border border-base p-2.5 lg:order-1 lg:sticky lg:top-0"
>
<CustomerWalletBalancePanel
:customer-id="userInfo.id"
layout="sidebar"
:interactive="false"
/>
</aside>
</div>
</a-tab-pane>
<a-tab-pane key="لیست سفارشات" tab="لیست سفارشات">
@@ -283,6 +304,8 @@ import Icon from '@core/components/BaseIcon/src/Icon.vue'
import { isMobile } from '@core/utils/is'
import WalletChargeModal from './components/WalletChargeModal.vue'
import CustomerWalletBalancePanel from '@/components/customer/CustomerWalletBalancePanel.vue'
import { getChargeTargets } from '@/api/wallet'
import type { ChargeTarget } from '@/model/wallet'
import { metalLabel } from '@/utils/adminMetalLabels'
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
@@ -482,6 +505,7 @@ async function info(val: Customer) {
activeKey.value = 'اطلاعات جامع'
userInfo.value = cloneDeep(val)
userInfoVisible.value = true
void loadInfoChargeTargets()
await nextTick(() => {
loading.value = true
refreshFundTable()
@@ -551,6 +575,17 @@ onMounted(async () => {
//add project modal
const addUserVisible = ref<boolean>(false)
const userInfoVisible = ref<boolean>(false)
const infoChargeTargets = ref<ChargeTarget[]>([])
async function loadInfoChargeTargets() {
if (!isMobile.value) return
try {
const res = await getChargeTargets()
infoChargeTargets.value = res?.targets ?? []
} catch {
infoChargeTargets.value = []
}
}
const userInfo = ref<Customer>()
//submit user