fix chart
This commit is contained in:
@@ -10,3 +10,10 @@ export const getConfiguration = async () =>
|
|||||||
/** فقط تنظیمات قابل ویرایش — برای صفحه تنظیمات ادمین */
|
/** فقط تنظیمات قابل ویرایش — برای صفحه تنظیمات ادمین */
|
||||||
export const getEditableConfiguration = async () =>
|
export const getEditableConfiguration = async () =>
|
||||||
await useAxios.get<Array<Const>>(`/admin/get_settings`, { params: { editable_only: 1 } })
|
await useAxios.get<Array<Const>>(`/admin/get_settings`, { params: { editable_only: 1 } })
|
||||||
|
|
||||||
|
/** تنظیمات سیستمی (can_edit=0, is_active=1) — فقط ادمین دیپلوی */
|
||||||
|
export const getSystemConfiguration = async () =>
|
||||||
|
await useAxios.get<Array<Const>>(`/admin/get_settings`, { params: { system_only: 1 } })
|
||||||
|
|
||||||
|
export const setSystemConfiguration = async (obj: Const & { system_deploy?: boolean }) =>
|
||||||
|
await useAxios.post<Const>(`/admin/get_settings`, { ...obj, system_deploy: true })
|
||||||
+39
-6
@@ -1,13 +1,46 @@
|
|||||||
import useAxios from '@core/utils/useAxios'
|
import useAxios from '@core/utils/useAxios'
|
||||||
import type { AddId, User } from '@/model/user'
|
import type { User } from '@/model/user'
|
||||||
|
import type {
|
||||||
|
DashboardFlowPoint,
|
||||||
|
DashboardFinance,
|
||||||
|
DashboardMaster,
|
||||||
|
PriceChartPoint,
|
||||||
|
} from '@/model/dashboard'
|
||||||
|
import type { Order } from '@/model/order'
|
||||||
|
import type { Balance } from '@/model/product'
|
||||||
import type { TableInput } from '@core/type/baseTable'
|
import type { TableInput } from '@core/type/baseTable'
|
||||||
import type {Dashboard} from '@/model/dashboard'
|
|
||||||
import type {PriceChart} from "@/model/priceChart";
|
|
||||||
|
|
||||||
export const getDashboard = async () => await useAxios.get<Dashboard>('/admin/dashboard')
|
export const getDashboard = async () => await useAxios.get<DashboardMaster>('/admin/dashboard')
|
||||||
|
|
||||||
export const updateMyInformation = async (val: User) =>
|
export const updateMyInformation = async (val: User) =>
|
||||||
await useAxios.post<User>('/admin/me/update', val)
|
await useAxios.post<User>('/admin/me/update', val)
|
||||||
|
|
||||||
//priceChart
|
export const getDashboardFinance = async (startDate: string, endDate: string) =>
|
||||||
export const getAllPrices = async () => await useAxios.post('/admin/prices')
|
await useAxios.post<DashboardFinance>('/admin/dashboard/finance', { startDate, endDate })
|
||||||
|
|
||||||
|
export const getDashboardBalance = async (startDate: string, endDate: string) =>
|
||||||
|
await useAxios.post<Balance>('/admin/getBalance', { startDate, endDate })
|
||||||
|
|
||||||
|
export const getDashboardFlow = async (type: 'week' | 'month') =>
|
||||||
|
await useAxios.post<DashboardFlowPoint[]>('/admin/dashboard/customer/flow', { type })
|
||||||
|
|
||||||
|
export const getPriceChartData = async () => {
|
||||||
|
const res = await useAxios.post<{
|
||||||
|
code?: number
|
||||||
|
data?: { prices?: Record<'DAILY' | 'WEEKLY' | 'MONTHLY', PriceChartPoint[]> }
|
||||||
|
}>('/admin/prices/chart')
|
||||||
|
|
||||||
|
return res?.data?.prices ?? (res as unknown as { prices?: Record<string, PriceChartPoint[]> })?.prices
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRecentOrders = async (limit = 10) => {
|
||||||
|
const res = await useAxios.post<{ rows?: Order[] }>('/admin/order/table', {
|
||||||
|
limit,
|
||||||
|
page: 1,
|
||||||
|
sort: { field: 'id', order: 'desc' },
|
||||||
|
} as TableInput)
|
||||||
|
|
||||||
|
return res?.rows ?? (Array.isArray(res) ? res : [])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toggleGateway = async () => await useAxios.post('/admin/gateway')
|
||||||
|
|||||||
+2
-2
@@ -29,9 +29,9 @@ const init = async () => {
|
|||||||
if (token) {
|
if (token) {
|
||||||
await userStore.getMyInfo()
|
await userStore.getMyInfo()
|
||||||
await initStoreModeFromSettings()
|
await initStoreModeFromSettings()
|
||||||
setupRoutes(['*'])
|
setupRoutes(['*'], undefined, userStore.userInfo?.mobile)
|
||||||
const { refreshAppModeMenu } = await import('@/router')
|
const { refreshAppModeMenu } = await import('@/router')
|
||||||
refreshAppModeMenu()
|
refreshAppModeMenu(userStore.userInfo?.mobile)
|
||||||
//setupRoutes(['project'])
|
//setupRoutes(['project'])
|
||||||
/*router.removeRoute('usersMain')
|
/*router.removeRoute('usersMain')
|
||||||
router.removeRoute('users')*/
|
router.removeRoute('users')*/
|
||||||
|
|||||||
+50
-27
@@ -1,4 +1,11 @@
|
|||||||
export interface Dashboard {
|
import type { Order } from '@/model/order'
|
||||||
|
|
||||||
|
export interface DashboardMaster {
|
||||||
|
price?: number
|
||||||
|
pricem?: number
|
||||||
|
update_date?: string
|
||||||
|
is_gateway_open?: boolean
|
||||||
|
prices?: Array<{ id?: string | number; name?: string; price?: number; profit?: number }>
|
||||||
new_customer_major?: number
|
new_customer_major?: number
|
||||||
new_customer_single?: number
|
new_customer_single?: number
|
||||||
all_customer_major?: number
|
all_customer_major?: number
|
||||||
@@ -7,35 +14,51 @@ export interface Dashboard {
|
|||||||
archive_customer_single?: number
|
archive_customer_single?: number
|
||||||
all_gold?: number
|
all_gold?: number
|
||||||
all_gold_single?: number
|
all_gold_single?: number
|
||||||
price?: Price
|
await_order?: Order[]
|
||||||
}
|
best_customer?: Array<{ customer_id?: number; amount?: number; customer?: { name?: string; mobile?: string } }>
|
||||||
export interface Price {
|
best_customer_single?: Array<{ gold?: number; customer?: { name?: string; mobile?: string } }>
|
||||||
id?: number
|
GT_all_customers?: number
|
||||||
title_fa?: string
|
GT_no_login_customers?: number
|
||||||
value?: number
|
GT_no_valid_customers?: number
|
||||||
created_at?: string
|
GT_active_customers?: number
|
||||||
updated_at?: string
|
GT_deActive_customers?: number
|
||||||
update_date?: string
|
GT_products_report?: Array<{
|
||||||
value_json?: ValuePrice[]
|
id?: number
|
||||||
|
title?: string
|
||||||
|
transaction_sum_gold_buy?: number
|
||||||
|
total_negative_gold_sell?: number
|
||||||
|
total_gold?: number
|
||||||
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValuePrice {
|
export interface DashboardFinance {
|
||||||
key?: number
|
fullPriceM1?: number
|
||||||
low?: string
|
fullPriceM2?: number
|
||||||
high?: string
|
fullPriceSood?: number
|
||||||
price?: string
|
fullPriceKarmozd?: number
|
||||||
numbertitle?: string
|
fullPriceKaermozd?: number
|
||||||
change?: string
|
orderCount?: number
|
||||||
category?: string
|
Gifts?: string
|
||||||
created_at?: string
|
GiftsPrise?: number
|
||||||
|
sumAll?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Charts {
|
export interface DashboardFlowPoint {
|
||||||
buySellChart?: Chart
|
date?: string
|
||||||
customerSellChart?: Chart
|
'1'?: number
|
||||||
singleSellChart?: Chart
|
'2'?: number
|
||||||
|
1?: number
|
||||||
|
2?: number
|
||||||
}
|
}
|
||||||
export interface Chart {
|
|
||||||
price: number
|
export interface PriceChartPoint {
|
||||||
obj: any[]
|
timestamp: number
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardWalletSums {
|
||||||
|
gold?: number
|
||||||
|
goldToCash?: number
|
||||||
|
cash?: number
|
||||||
|
cashToCash?: number
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<a-tooltip placement="top" :overlay-inner-style="{ maxWidth: '300px', direction: 'rtl' }">
|
||||||
|
<template #title>
|
||||||
|
<div class="space-y-1.5 text-xs leading-5">
|
||||||
|
<p>{{ help.description }}</p>
|
||||||
|
<p v-if="help.formula" class="border-t border-white/20 pt-1.5 text-white/90">
|
||||||
|
<span class="font-semibold">فرمول:</span> {{ help.formula }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span
|
||||||
|
class="dashboard-help-trigger inline-flex shrink-0 items-center justify-center"
|
||||||
|
role="button"
|
||||||
|
tabindex="-1"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<IosInfoBadge :size="badgeSize" />
|
||||||
|
</span>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
|
||||||
|
import IosInfoBadge from '@/pages/dashboard/components/IosInfoBadge.vue'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
help: DashboardHelp
|
||||||
|
size?: 'xs' | 'sm' | 'md'
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 'sm',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const badgeSize = computed(() => props.size)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard-help-trigger {
|
||||||
|
line-height: 0;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<span
|
||||||
|
class="dashboard-label-with-help inline-flex max-w-full items-center gap-1"
|
||||||
|
:class="[textClass || sizeTextClass, rootClass]"
|
||||||
|
>
|
||||||
|
<span class="truncate leading-none" :class="labelClass">{{ label }}</span>
|
||||||
|
<DashboardHelpTip v-if="help" :help="help" :size="size" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
|
||||||
|
import DashboardHelpTip from '@/pages/dashboard/components/DashboardHelpTip.vue'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
label: string
|
||||||
|
help?: DashboardHelp
|
||||||
|
size?: 'xs' | 'sm' | 'md'
|
||||||
|
textClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
rootClass?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 'xs',
|
||||||
|
textClass: '',
|
||||||
|
labelClass: '',
|
||||||
|
rootClass: '',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const sizeTextClass = computed(() => {
|
||||||
|
if (props.size === 'md') return 'text-base'
|
||||||
|
if (props.size === 'sm') return 'text-sm'
|
||||||
|
return 'text-[11px]'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard-label-with-help :deep(.dashboard-help-trigger) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mb-2">
|
||||||
|
<DashboardLabelWithHelp
|
||||||
|
:label="title"
|
||||||
|
:help="help"
|
||||||
|
size="sm"
|
||||||
|
label-class="font-bold text-slate-900 dark:text-slate-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
|
||||||
|
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
title: string
|
||||||
|
help?: DashboardHelp
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex h-full min-h-[4.25rem] items-center gap-2 rounded-xl border border-slate-200/90 bg-white px-2.5 py-2 dark:border-slate-600 dark:bg-slate-900"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg"
|
||||||
|
:class="iconBgClass"
|
||||||
|
>
|
||||||
|
<BaseIcon :icon="icon" size="17" :class="iconTextClass" />
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<DashboardLabelWithHelp
|
||||||
|
:label="title"
|
||||||
|
:help="help"
|
||||||
|
size="xs"
|
||||||
|
label-class="font-medium text-slate-600 dark:text-slate-300"
|
||||||
|
/>
|
||||||
|
<p class="truncate text-[15px] font-bold leading-snug text-slate-900 dark:text-slate-50">
|
||||||
|
{{ value }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DashboardHelp } from '@/pages/dashboard/dashboardSectionCopy'
|
||||||
|
import DashboardLabelWithHelp from '@/pages/dashboard/components/DashboardLabelWithHelp.vue'
|
||||||
|
|
||||||
|
type Tone = 'gold' | 'green' | 'blue' | 'rose' | 'violet' | 'amber'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
title: string
|
||||||
|
value: string
|
||||||
|
help?: DashboardHelp
|
||||||
|
icon?: string
|
||||||
|
tone?: Tone
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
icon: 'vuesax-linear:chart-2',
|
||||||
|
tone: 'blue',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const toneMap: Record<Tone, { iconBg: string; iconText: string }> = {
|
||||||
|
gold: {
|
||||||
|
iconBg: 'bg-amber-100 dark:bg-amber-500/25',
|
||||||
|
iconText: 'text-amber-700 dark:text-amber-300',
|
||||||
|
},
|
||||||
|
green: {
|
||||||
|
iconBg: 'bg-emerald-100 dark:bg-emerald-500/25',
|
||||||
|
iconText: 'text-emerald-700 dark:text-emerald-300',
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
iconBg: 'bg-sky-100 dark:bg-sky-500/25',
|
||||||
|
iconText: 'text-sky-700 dark:text-sky-300',
|
||||||
|
},
|
||||||
|
rose: {
|
||||||
|
iconBg: 'bg-rose-100 dark:bg-rose-500/25',
|
||||||
|
iconText: 'text-rose-700 dark:text-rose-300',
|
||||||
|
},
|
||||||
|
violet: {
|
||||||
|
iconBg: 'bg-violet-100 dark:bg-violet-500/25',
|
||||||
|
iconText: 'text-violet-700 dark:text-violet-300',
|
||||||
|
},
|
||||||
|
amber: {
|
||||||
|
iconBg: 'bg-orange-100 dark:bg-orange-500/25',
|
||||||
|
iconText: 'text-orange-700 dark:text-orange-300',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconBgClass = computed(() => toneMap[props.tone].iconBg)
|
||||||
|
const iconTextClass = computed(() => toneMap[props.tone].iconText)
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<span
|
||||||
|
class="ios-info-badge inline-flex shrink-0 items-center justify-center rounded-full border-0 bg-[#F5B800] text-white outline-none ring-0"
|
||||||
|
:class="sizeClass"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" class="block h-[72%] w-[72%]">
|
||||||
|
<rect x="6.75" y="6.75" width="2.5" height="5.5" rx="1.25" fill="currentColor" />
|
||||||
|
<circle cx="8" cy="4.35" r="1.35" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
size?: 'xs' | 'sm' | 'md'
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 'sm',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const sizeClass = computed(() => {
|
||||||
|
if (props.size === 'xs') return 'h-[1em] w-[1em] min-h-[0.85rem] min-w-[0.85rem]'
|
||||||
|
if (props.size === 'md') return 'h-[1.15em] w-[1.15em] min-h-[1.1rem] min-w-[1.1rem]'
|
||||||
|
return 'h-[1em] w-[1em] min-h-[0.95rem] min-w-[0.95rem]'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ios-info-badge {
|
||||||
|
box-shadow: none;
|
||||||
|
transform: translateY(-0.5px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
//@ts-ignore
|
||||||
|
import moment from 'moment-jalaali'
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import {
|
||||||
|
getDashboard,
|
||||||
|
getDashboardBalance,
|
||||||
|
getDashboardFinance,
|
||||||
|
getDashboardFlow,
|
||||||
|
getPriceChartData,
|
||||||
|
getRecentOrders,
|
||||||
|
toggleGateway,
|
||||||
|
} from '@/api/dashboard'
|
||||||
|
import { getWalletTable } from '@/api/wallet'
|
||||||
|
import type {
|
||||||
|
DashboardFlowPoint,
|
||||||
|
DashboardFinance,
|
||||||
|
DashboardMaster,
|
||||||
|
PriceChartPoint,
|
||||||
|
} from '@/model/dashboard'
|
||||||
|
import type { Order } from '@/model/order'
|
||||||
|
import type { Balance } from '@/model/product'
|
||||||
|
import { usePrice } from '@core/utils/usePrice'
|
||||||
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
|
import { isStoreApp } from '@/utils/config'
|
||||||
|
|
||||||
|
const MESGHAL = 4.331802
|
||||||
|
|
||||||
|
export function useDashboardPage() {
|
||||||
|
const loading = ref(true)
|
||||||
|
const flowLoading = ref(false)
|
||||||
|
const priceRange = ref<'DAILY' | 'WEEKLY' | 'MONTHLY'>('DAILY')
|
||||||
|
const flowRange = ref<'week' | 'month'>('week')
|
||||||
|
|
||||||
|
const startDate = ref(moment().subtract(30, 'days').startOf('day').format('YYYY-MM-DD HH:mm:ss'))
|
||||||
|
const endDate = ref(moment().endOf('day').format('YYYY-MM-DD HH:mm:ss'))
|
||||||
|
|
||||||
|
const master = ref<DashboardMaster>()
|
||||||
|
const finance = ref<DashboardFinance>()
|
||||||
|
const balance = ref<Balance>()
|
||||||
|
const walletSums = ref<{ gold?: number; cash?: number; goldToCash?: number; cashToCash?: number }>()
|
||||||
|
const recentOrders = ref<Order[]>([])
|
||||||
|
const flowData = ref<DashboardFlowPoint[]>([])
|
||||||
|
const priceSeries = ref<PriceChartPoint[]>([])
|
||||||
|
|
||||||
|
const isStore = computed(() => isStoreApp())
|
||||||
|
|
||||||
|
const pendingOrders = computed(() => master.value?.await_order ?? [])
|
||||||
|
|
||||||
|
const customerStats = computed(() => ({
|
||||||
|
newCount: isStore.value
|
||||||
|
? master.value?.new_customer_major ?? 0
|
||||||
|
: master.value?.new_customer_single ?? 0,
|
||||||
|
total: isStore.value
|
||||||
|
? master.value?.all_customer_major ?? 0
|
||||||
|
: master.value?.all_customer_single ?? 0,
|
||||||
|
active: master.value?.GT_active_customers ?? 0,
|
||||||
|
inactive: master.value?.GT_deActive_customers ?? 0,
|
||||||
|
notVerified: master.value?.GT_no_valid_customers ?? 0,
|
||||||
|
}))
|
||||||
|
|
||||||
|
async function loadCore() {
|
||||||
|
const [dash, fin, bal, wallet, orders, prices] = await Promise.allSettled([
|
||||||
|
getDashboard(),
|
||||||
|
getDashboardFinance(startDate.value, endDate.value),
|
||||||
|
getDashboardBalance(startDate.value, endDate.value),
|
||||||
|
getWalletTable({ limit: 1, page: 1 } as never),
|
||||||
|
getRecentOrders(12),
|
||||||
|
getPriceChartData(),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (dash.status === 'fulfilled') master.value = dash.value
|
||||||
|
if (fin.status === 'fulfilled') finance.value = fin.value
|
||||||
|
if (bal.status === 'fulfilled') balance.value = bal.value
|
||||||
|
if (wallet.status === 'fulfilled') walletSums.value = wallet.value?.sums
|
||||||
|
if (orders.status === 'fulfilled') recentOrders.value = orders.value
|
||||||
|
if (prices.status === 'fulfilled') {
|
||||||
|
priceSeries.value = prices.value?.DAILY ?? []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadFlow() {
|
||||||
|
flowLoading.value = true
|
||||||
|
try {
|
||||||
|
flowData.value = (await getDashboardFlow(flowRange.value)) ?? []
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
flowData.value = []
|
||||||
|
} finally {
|
||||||
|
flowLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshAll() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await Promise.all([loadCore(), loadFlow()])
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyDateRange() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const [fin, bal] = await Promise.all([
|
||||||
|
getDashboardFinance(startDate.value, endDate.value),
|
||||||
|
getDashboardBalance(startDate.value, endDate.value),
|
||||||
|
])
|
||||||
|
finance.value = fin
|
||||||
|
balance.value = bal
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function switchPriceRange(range?: 'DAILY' | 'WEEKLY' | 'MONTHLY') {
|
||||||
|
const key = range ?? priceRange.value
|
||||||
|
priceRange.value = key
|
||||||
|
const prices = await getPriceChartData()
|
||||||
|
priceSeries.value = prices?.[key] ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onToggleGateway() {
|
||||||
|
await toggleGateway()
|
||||||
|
master.value = await getDashboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatGram(value?: number) {
|
||||||
|
return `${Number(value ?? 0).toLocaleString('fa-IR', { maximumFractionDigits: 3 })} گرم`
|
||||||
|
}
|
||||||
|
|
||||||
|
function directionLabel(direction?: number) {
|
||||||
|
return direction === 1 ? 'خرید' : direction === 2 ? 'فروش' : '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
function directionClass(direction?: number) {
|
||||||
|
return direction === 1
|
||||||
|
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300'
|
||||||
|
: 'bg-rose-100 text-rose-700 dark:bg-rose-500/20 dark:text-rose-300'
|
||||||
|
}
|
||||||
|
|
||||||
|
function stsClass(sts?: number) {
|
||||||
|
if (sts === 2) {
|
||||||
|
return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300'
|
||||||
|
}
|
||||||
|
if (sts === 1) {
|
||||||
|
return 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-300'
|
||||||
|
}
|
||||||
|
return 'bg-rose-100 text-rose-700 dark:bg-rose-500/20 dark:text-rose-300'
|
||||||
|
}
|
||||||
|
|
||||||
|
function stsLabel(order: Order) {
|
||||||
|
return (order as Order & { sts_str?: string }).sts_str
|
||||||
|
?? (order.sts === 2 ? 'موفق' : order.sts === 1 ? 'باز' : 'ناموفق')
|
||||||
|
}
|
||||||
|
|
||||||
|
const financeChartOptions = computed(() => {
|
||||||
|
const sood = finance.value?.fullPriceSood ?? 0
|
||||||
|
const karmozd = finance.value?.fullPriceKarmozd ?? finance.value?.fullPriceKaermozd ?? 0
|
||||||
|
const gifts = finance.value?.GiftsPrise ?? 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
chart: { type: 'donut', fontFamily: 'inherit' },
|
||||||
|
labels: ['سود معامله', 'کارمزد طلایی', 'هدایای طلا'],
|
||||||
|
colors: ['#10b981', '#f59e0b', '#8b5cf6'],
|
||||||
|
legend: { position: 'bottom', fontSize: '12px' },
|
||||||
|
dataLabels: { enabled: false },
|
||||||
|
plotOptions: {
|
||||||
|
pie: {
|
||||||
|
donut: {
|
||||||
|
size: '68%',
|
||||||
|
labels: {
|
||||||
|
show: true,
|
||||||
|
total: {
|
||||||
|
show: true,
|
||||||
|
label: 'درآمد خالص',
|
||||||
|
formatter: () => usePrice(finance.value?.sumAll ?? 0, true, ''),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
y: { formatter: (v: number) => usePrice(v, true, '') },
|
||||||
|
},
|
||||||
|
series: [sood, karmozd, gifts],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const flowChartOptions = computed(() => {
|
||||||
|
const points = [...flowData.value].sort((a, b) =>
|
||||||
|
String(a.date ?? '').localeCompare(String(b.date ?? '')),
|
||||||
|
)
|
||||||
|
const categories = points.map((p) => {
|
||||||
|
const d = p.date ?? ''
|
||||||
|
return d ? toJalali(`${d} 12:00:00`, 'jMM/jDD') : d
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
chart: { type: 'bar', stacked: true, toolbar: { show: false }, fontFamily: 'inherit' },
|
||||||
|
plotOptions: { bar: { borderRadius: 4, columnWidth: '55%' } },
|
||||||
|
colors: ['#10b981', '#f43f5e'],
|
||||||
|
xaxis: { categories, labels: { rotate: -35, style: { fontSize: '11px' } } },
|
||||||
|
yaxis: {
|
||||||
|
labels: {
|
||||||
|
formatter: (v: number) => `${Number(v).toLocaleString('fa-IR')} گرم`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: { position: 'top' },
|
||||||
|
dataLabels: { enabled: false },
|
||||||
|
series: [
|
||||||
|
{ name: 'خرید مشتری', data: points.map((p) => Number(p[1] ?? p['1'] ?? 0)) },
|
||||||
|
{ name: 'فروش مشتری', data: points.map((p) => Number(p[2] ?? p['2'] ?? 0)) },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const priceChartOptions = computed(() => ({
|
||||||
|
chart: { type: 'area', toolbar: { show: false }, fontFamily: 'inherit', zoom: { enabled: false } },
|
||||||
|
stroke: { curve: 'smooth', width: 2 },
|
||||||
|
fill: {
|
||||||
|
type: 'gradient',
|
||||||
|
gradient: { shadeIntensity: 1, opacityFrom: 0.45, opacityTo: 0.05 },
|
||||||
|
},
|
||||||
|
colors: ['#2563eb'],
|
||||||
|
dataLabels: { enabled: false },
|
||||||
|
xaxis: {
|
||||||
|
type: 'datetime',
|
||||||
|
labels: { datetimeUTC: false, style: { fontSize: '11px' } },
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
labels: {
|
||||||
|
formatter: (v: number) => `${Math.round(v / 1_000_000).toLocaleString('fa-IR')}M`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
x: { format: 'yyyy/MM/dd HH:mm' },
|
||||||
|
y: { formatter: (v: number) => usePrice(v, true, '') },
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'مظنه',
|
||||||
|
data: priceSeries.value.map((p) => [p.timestamp, p.value]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
|
||||||
|
const productChartOptions = computed(() => {
|
||||||
|
const items = (master.value?.GT_products_report ?? []).slice(0, 8)
|
||||||
|
return {
|
||||||
|
chart: { type: 'bar', toolbar: { show: false }, fontFamily: 'inherit' },
|
||||||
|
plotOptions: { bar: { horizontal: true, borderRadius: 4 } },
|
||||||
|
colors: ['#0ea5e9', '#f97316'],
|
||||||
|
dataLabels: { enabled: false },
|
||||||
|
xaxis: {
|
||||||
|
categories: items.map((p) => p.title ?? `#${p.id}`),
|
||||||
|
labels: { style: { fontSize: '11px' } },
|
||||||
|
},
|
||||||
|
legend: { position: 'top' },
|
||||||
|
series: [
|
||||||
|
{ name: 'خرید (گرم)', data: items.map((p) => p.transaction_sum_gold_buy ?? 0) },
|
||||||
|
{
|
||||||
|
name: 'فروش (گرم)',
|
||||||
|
data: items.map((p) => Math.abs(p.total_negative_gold_sell ?? 0)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void refreshAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
flowLoading,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
master,
|
||||||
|
finance,
|
||||||
|
balance,
|
||||||
|
walletSums,
|
||||||
|
recentOrders,
|
||||||
|
pendingOrders,
|
||||||
|
customerStats,
|
||||||
|
priceRange,
|
||||||
|
flowRange,
|
||||||
|
flowData,
|
||||||
|
priceSeries,
|
||||||
|
isStore,
|
||||||
|
MESGHAL,
|
||||||
|
refreshAll,
|
||||||
|
applyDateRange,
|
||||||
|
switchPriceRange,
|
||||||
|
loadFlow,
|
||||||
|
onToggleGateway,
|
||||||
|
formatGram,
|
||||||
|
directionLabel,
|
||||||
|
directionClass,
|
||||||
|
stsClass,
|
||||||
|
stsLabel,
|
||||||
|
financeChartOptions,
|
||||||
|
flowChartOptions,
|
||||||
|
priceChartOptions,
|
||||||
|
productChartOptions,
|
||||||
|
usePrice,
|
||||||
|
toJalali,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/** توضیح و فرمول فارسی — برای tooltip آیکون راهنما */
|
||||||
|
|
||||||
|
export type DashboardHelp = {
|
||||||
|
description: string
|
||||||
|
formula: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MESGHAL_FACTOR = '۴.۳۳۱۸۰۲'
|
||||||
|
|
||||||
|
export const dashboardHero: DashboardHelp = {
|
||||||
|
description: 'آخرین قیمت پایه هر گرم طلا در سیستم.',
|
||||||
|
formula: `قیمت هر مثقال = قیمت هر گرم × ${MESGHAL_FACTOR}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dashboardGateway: DashboardHelp = {
|
||||||
|
description: 'وقتی فعال باشد مشتری میتواند از درگاه بانکی واریز کند؛ در حالت غیرفعال فقط واریز دستی ممکن است.',
|
||||||
|
formula: 'وضعیت: فعال یا غیرفعال',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dashboardKpi = {
|
||||||
|
profit: {
|
||||||
|
description: 'مجموع سود اسپرد (تفاوت قیمت خرید/فروش با مشتری) روی همه سفارشهای ثبتشده در بازه.',
|
||||||
|
formula: 'سود کل = جمع «سود هر سفارش»',
|
||||||
|
},
|
||||||
|
commission: {
|
||||||
|
description: 'مجموع کارمزد طلایی محصول که روی همان سفارشها محاسبه شده.',
|
||||||
|
formula: 'کارمزد کل = جمع «کارمزد هر سفارش»',
|
||||||
|
},
|
||||||
|
orderCount: {
|
||||||
|
description: 'تعداد همه سفارشهایی که در بازه انتخابی ثبت شدهاند (هر وضعیتی).',
|
||||||
|
formula: 'تعداد = شمارش سفارشها بین دو تاریخ',
|
||||||
|
},
|
||||||
|
goldBalance: {
|
||||||
|
description: 'تراز وزنی طلا از سفارشات موفق؛ مثبت یعنی به مشتری بیشتر فروختهاید.',
|
||||||
|
formula: 'تراز طلا = گرم فروش − گرم خرید (سفارش موفق)',
|
||||||
|
},
|
||||||
|
cashBalance: {
|
||||||
|
description: 'تراز ریالی همان سفارشات موفق؛ ورود و خروج نقد از معاملات.',
|
||||||
|
formula: 'تراز ریال = مبلغ فروش − مبلغ خرید',
|
||||||
|
},
|
||||||
|
mazanehBalance: {
|
||||||
|
description: 'میانگین موزون قیمت معاملات، تبدیلشده به مثقال.',
|
||||||
|
formula: `(جمع مبلغ معاملات ÷ جمع گرم) × ${MESGHAL_FACTOR}`,
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, DashboardHelp>
|
||||||
|
|
||||||
|
export const dashboardDateRange: DashboardHelp = {
|
||||||
|
description: 'تاریخ شروع و پایان برای محاسبه کارتها، تراز و نمودارها.',
|
||||||
|
formula: 'گزارش = از تاریخ … تا تاریخ …',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dashboardDateMetrics = {
|
||||||
|
bankDeposit: {
|
||||||
|
description: 'کل واریزهای موفق مشتریان از درگاه آنلاین در این بازه.',
|
||||||
|
formula: 'جمع مبلغ واریزهای تأییدشده از درگاه',
|
||||||
|
},
|
||||||
|
netIncome: {
|
||||||
|
description: 'درآمد پس از کسر ارزش طلای هدیهدادهشده.',
|
||||||
|
formula: 'درآمد خالص = (اختلاف قیمت اصلی و پرداختی) − (گرم هدیه × مظنه)',
|
||||||
|
},
|
||||||
|
gifts: {
|
||||||
|
description: 'طلایی که ادمین به مشتریان هدیه داده در همین بازه.',
|
||||||
|
formula: 'گرم هدیه = جمع طلای ثبتشده در تراکنشهای ادمین',
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, DashboardHelp>
|
||||||
|
|
||||||
|
export const dashboardCharts = {
|
||||||
|
incomeMix: {
|
||||||
|
description: 'نشان میدهد سود معامله، کارمزد و هدایا چقدر از درآمد کل را تشکیل میدهند.',
|
||||||
|
formula: 'درآمد = سود معامله + کارمزد + (گرم هدیه × مظنه)',
|
||||||
|
},
|
||||||
|
buySellVolume: {
|
||||||
|
description: 'مقدار طلای خرید و فروش مشتری در هر روز (فقط سفارش موفق).',
|
||||||
|
formula: 'هر روز: گرم خرید مشتری و گرم فروش به مشتری',
|
||||||
|
},
|
||||||
|
priceChart: {
|
||||||
|
description: 'روند تغییر قیمت پایه هر گرم در ۲۴ ساعت، هفته یا ماه.',
|
||||||
|
formula: 'نمودار = قیمت پایه در هر لحظه',
|
||||||
|
},
|
||||||
|
productActivity: {
|
||||||
|
description: 'مقایسه حجم خرید و فروش هر محصول.',
|
||||||
|
formula: 'خرید = جمع گرم مثبت | فروش = جمع گرم منفی',
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, DashboardHelp>
|
||||||
|
|
||||||
|
export const dashboardOrders = {
|
||||||
|
pending: {
|
||||||
|
description: 'سفارشهایی که هنوز تأیید نشدهاند.',
|
||||||
|
formula: 'سود هر گرم × مقدار = سود کل سفارش',
|
||||||
|
},
|
||||||
|
recent: {
|
||||||
|
description: '۱۲ سفارش آخر با جزئیات مظنه و سود.',
|
||||||
|
formula: 'مظنه = قیمت پایه | قیمت مشتری = مظنه ± اسپرد | سود/گرم = تفاوت اسپرد',
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, DashboardHelp>
|
||||||
|
|
||||||
|
export const dashboardCustomers: DashboardHelp = {
|
||||||
|
description: 'آمار مشتریان خانگی ثبتشده در سیستم.',
|
||||||
|
formula: 'فعال = دارای کیف پول | بدون احراز = هویت تأیید نشده',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dashboardCustomerMetrics = {
|
||||||
|
total: {
|
||||||
|
description: 'همه مشتریان خانگی ثبتشده.',
|
||||||
|
formula: 'شمارش کل مشتریان',
|
||||||
|
},
|
||||||
|
new: {
|
||||||
|
description: 'مشتریانی که در ۳ ماه اخیر ثبتنام کردهاند.',
|
||||||
|
formula: 'ثبتنام در ۹۰ روز گذشته',
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
description: 'مشتریانی که حداقل یک کیف پول دارند.',
|
||||||
|
formula: 'دارای موجودی در کیف',
|
||||||
|
},
|
||||||
|
notVerified: {
|
||||||
|
description: 'مشتریانی که احراز هویت را تکمیل نکردهاند.',
|
||||||
|
formula: 'وضعیت احراز: ناقص',
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, DashboardHelp>
|
||||||
|
|
||||||
|
export const dashboardWallet: DashboardHelp = {
|
||||||
|
description: 'مجموع موجودی طلا و ریال همه مشتریان.',
|
||||||
|
formula: 'ارزش طلا به ریال = جمع گرم × مظنه لحظهای',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dashboardWalletMetrics = {
|
||||||
|
gold: {
|
||||||
|
description: 'مجموع طلای موجود در کیف همه مشتریان.',
|
||||||
|
formula: 'جمع گرم طلا در کیفها',
|
||||||
|
},
|
||||||
|
cash: {
|
||||||
|
description: 'مجموع ریال موجود در کیف همه مشتریان.',
|
||||||
|
formula: 'جمع ریال در کیفها',
|
||||||
|
},
|
||||||
|
goldToCash: {
|
||||||
|
description: 'ارزش ریالی کل طلای مشتریان با مظنه فعلی.',
|
||||||
|
formula: 'جمع گرم × قیمت هر گرم',
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, DashboardHelp>
|
||||||
+488
-871
File diff suppressed because it is too large
Load Diff
@@ -161,9 +161,9 @@ const onUserPassFormFinish = async () => {
|
|||||||
}
|
}
|
||||||
await userStore.getMyInfo()
|
await userStore.getMyInfo()
|
||||||
await initStoreModeFromSettings()
|
await initStoreModeFromSettings()
|
||||||
setupRoutes(['*'], router)
|
setupRoutes(['*'], router, userStore.userInfo?.mobile)
|
||||||
const { refreshAppModeMenu } = await import('@/router')
|
const { refreshAppModeMenu } = await import('@/router')
|
||||||
refreshAppModeMenu()
|
refreshAppModeMenu(userStore.userInfo?.mobile)
|
||||||
// const info = await getMyInformation()
|
// const info = await getMyInformation()
|
||||||
// await setMyInfo(JSON.stringify(info))
|
// await setMyInfo(JSON.stringify(info))
|
||||||
// emits('enter')
|
// emits('enter')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full min-h-0">
|
<div class="h-full min-h-0">
|
||||||
<BasicShowContent title="تنظیمات" class="h-full">
|
<BasicShowContent :title="pageTitle" class="h-full">
|
||||||
<div class="settings-toolbar">
|
<div class="settings-toolbar">
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="search"
|
v-model:value="search"
|
||||||
@@ -27,6 +27,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <a-button type="primary" :loading="loading" @click="add">افزودن تنظیم</a-button>-->
|
<!-- <a-button type="primary" :loading="loading" @click="add">افزودن تنظیم</a-button>-->
|
||||||
|
<a-button
|
||||||
|
v-if="isSystemMode"
|
||||||
|
type="primary"
|
||||||
|
class="settings-toolbar__action"
|
||||||
|
:loading="loading"
|
||||||
|
@click="add"
|
||||||
|
>
|
||||||
|
افزودن تنظیم
|
||||||
|
</a-button>
|
||||||
<a-button class="settings-toolbar__action" :loading="loading" @click="initConfig">
|
<a-button class="settings-toolbar__action" :loading="loading" @click="initConfig">
|
||||||
بروزرسانی
|
بروزرسانی
|
||||||
</a-button>
|
</a-button>
|
||||||
@@ -34,21 +43,20 @@
|
|||||||
|
|
||||||
<a-spin :spinning="loading && !baseData.length">
|
<a-spin :spinning="loading && !baseData.length">
|
||||||
<a-collapse
|
<a-collapse
|
||||||
v-model:activeKey="activeKeys"
|
v-model:activeKey="activeKey"
|
||||||
|
accordion
|
||||||
class="settings-collapse"
|
class="settings-collapse"
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
>
|
>
|
||||||
<a-collapse-panel
|
<a-collapse-panel
|
||||||
v-for="group in SETTING_GROUPS"
|
v-for="group in visibleSettingGroups"
|
||||||
:key="group.id"
|
:key="group.id"
|
||||||
:collapsible="groupedFiltered[group.id]?.length ? undefined : 'disabled'"
|
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="settings-collapse-header">
|
<div class="settings-collapse-header">
|
||||||
<BaseIcon :icon="group.icon" size="18" class="settings-collapse-icon" />
|
<BaseIcon :icon="group.icon" size="18" class="settings-collapse-icon" />
|
||||||
<span class="settings-collapse-title">{{ group.title }}</span>
|
<span class="settings-collapse-title">{{ group.title }}</span>
|
||||||
<a-badge
|
<a-badge
|
||||||
v-if="groupedFiltered[group.id]"
|
|
||||||
:count="groupedFiltered[group.id].length"
|
:count="groupedFiltered[group.id].length"
|
||||||
:number-style="{ backgroundColor: '#d4af37', color: '#fff', fontSize: '11px' }"
|
:number-style="{ backgroundColor: '#d4af37', color: '#fff', fontSize: '11px' }"
|
||||||
/>
|
/>
|
||||||
@@ -57,7 +65,7 @@
|
|||||||
|
|
||||||
<a-row :gutter="[16, 16]" class="settings-grid">
|
<a-row :gutter="[16, 16]" class="settings-grid">
|
||||||
<a-col
|
<a-col
|
||||||
v-for="row in groupedFiltered[group.id] || []"
|
v-for="row in groupedFiltered[group.id]"
|
||||||
:key="row.id"
|
:key="row.id"
|
||||||
:xs="24"
|
:xs="24"
|
||||||
:sm="12"
|
:sm="12"
|
||||||
@@ -74,16 +82,16 @@
|
|||||||
/>
|
/>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
|
||||||
<div v-if="!groupedFiltered[group.id]?.length" class="settings-empty-group">
|
|
||||||
موردی در این گروه یافت نشد
|
|
||||||
</div>
|
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
|
|
||||||
<a-empty
|
<a-empty
|
||||||
v-if="!loading && Object.keys(groupedFiltered).length === 0"
|
v-if="!loading && visibleSettingGroups.length === 0"
|
||||||
description="تنظیمی یافت نشد"
|
:description="
|
||||||
|
isSystemMode
|
||||||
|
? 'تنظیم سیستمی فعالی (can_edit=0) یافت نشد — deploy_settings_const.sql را اجرا کنید'
|
||||||
|
: 'تنظیمی یافت نشد'
|
||||||
|
"
|
||||||
class="mt-8"
|
class="mt-8"
|
||||||
/>
|
/>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
@@ -161,20 +169,43 @@ import type { FormInstance } from 'ant-design-vue'
|
|||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import type { Const } from '@/model/configuration'
|
import type { Const } from '@/model/configuration'
|
||||||
import { loading } from '@/utils'
|
import { loading } from '@/utils'
|
||||||
import { getEditableConfiguration, setConfiguration } from '@/api/configuration'
|
import { getEditableConfiguration, getSystemConfiguration, setConfiguration, setSystemConfiguration } from '@/api/configuration'
|
||||||
import SettingValueField from './SettingValueField.vue'
|
import SettingValueField from './SettingValueField.vue'
|
||||||
import SettingCard from './SettingCard.vue'
|
import SettingCard from './SettingCard.vue'
|
||||||
import { refreshAppModeMenu } from '@/router'
|
import { refreshAppModeMenu } from '@/router'
|
||||||
|
import { useUserStore } from '@/store/userStore'
|
||||||
import { setStoreModeFromSettings } from '@/utils/config'
|
import { setStoreModeFromSettings } from '@/utils/config'
|
||||||
|
import {
|
||||||
|
EDITABLE_SETTING_GROUPS,
|
||||||
|
SYSTEM_SETTING_GROUPS,
|
||||||
|
getSettingGroupId
|
||||||
|
} from './settingGroups'
|
||||||
import {
|
import {
|
||||||
SETTING_TYPE_OPTIONS,
|
SETTING_TYPE_OPTIONS,
|
||||||
formatValueForSave,
|
formatValueForSave,
|
||||||
helpText,
|
helpText,
|
||||||
isSettingVisible,
|
isSettingVisible,
|
||||||
|
isSettingLocked,
|
||||||
|
isSettingActive,
|
||||||
isSwitchOn,
|
isSwitchOn,
|
||||||
resolveSettingFieldType
|
resolveSettingFieldType
|
||||||
} from './settingTypes'
|
} from './settingTypes'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ mode?: 'editable' | 'system' }>(), {
|
||||||
|
mode: 'editable'
|
||||||
|
})
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
function refreshMenuAfterStoreChange() {
|
||||||
|
refreshAppModeMenu(userStore.userInfo?.mobile)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSystemMode = computed(() => props.mode === 'system')
|
||||||
|
const pageTitle = computed(() => (isSystemMode.value ? 'تنظیمات پیشرفته' : 'تنظیمات'))
|
||||||
|
const settingGroups = computed(() =>
|
||||||
|
isSystemMode.value ? SYSTEM_SETTING_GROUPS : EDITABLE_SETTING_GROUPS
|
||||||
|
)
|
||||||
|
|
||||||
const settingTypeOptions = SETTING_TYPE_OPTIONS
|
const settingTypeOptions = SETTING_TYPE_OPTIONS
|
||||||
|
|
||||||
const typeSelectOptions = SETTING_TYPE_OPTIONS.map((t) => ({
|
const typeSelectOptions = SETTING_TYPE_OPTIONS.map((t) => ({
|
||||||
@@ -224,7 +255,7 @@ function sortRows(rows: Const[]): Const[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isEditable(row: Const): boolean {
|
function isEditable(row: Const): boolean {
|
||||||
return row.can_edit !== 0 && row.can_edit !== false
|
return !isSettingLocked(row.can_edit)
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchesFilter(row: Const): boolean {
|
function matchesFilter(row: Const): boolean {
|
||||||
@@ -244,13 +275,14 @@ function matchesFilter(row: Const): boolean {
|
|||||||
|
|
||||||
const filteredRows = computed(() =>
|
const filteredRows = computed(() =>
|
||||||
sortRows(
|
sortRows(
|
||||||
(baseData.value || []).filter(
|
(baseData.value || []).filter((row) => {
|
||||||
(row) =>
|
if (row.id == null || !matchesFilter(row)) return false
|
||||||
row.id != null &&
|
if (isSystemMode.value) {
|
||||||
isEditable(row) &&
|
// API با system_only=1 فیلتر کرده؛ فقط جستجو اعمال میشود
|
||||||
isSettingVisible(row.key) &&
|
return true
|
||||||
matchesFilter(row)
|
}
|
||||||
)
|
return isEditable(row) && isSettingVisible(row.key)
|
||||||
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -265,148 +297,75 @@ const otherRows = computed(() =>
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
// گروهبندی تنظیمات — اولین match برنده است؛ misc فقط برای باقیمانده
|
// گروهبندی تنظیمات — اولین match برنده است؛ misc فقط برای باقیمانده
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
const SETTING_GROUPS: Array<{
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
icon: string
|
|
||||||
match: (key: string) => boolean
|
|
||||||
}> = [
|
|
||||||
{
|
|
||||||
id: 'general',
|
|
||||||
title: 'عمومی و برند اپ',
|
|
||||||
icon: 'vuesax-linear:home-2',
|
|
||||||
match: (k) =>
|
|
||||||
/^(name_fa|name_en|app_url|admin_url|bace_url|server_url|logo_|title_pwa|title_fav|metal_type|unit$|open_register|auto_active_customer|store_status|key$)/.test(
|
|
||||||
k
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'chart',
|
|
||||||
title: 'نمودار قیمت',
|
|
||||||
icon: 'vuesax-linear:chart-2',
|
|
||||||
match: (k) => /^chart_/.test(k)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'pricing',
|
|
||||||
title: 'قیمتگذاری، کارمزد و تابلو',
|
|
||||||
icon: 'vuesax-linear:dollar-circle',
|
|
||||||
match: (k) =>
|
|
||||||
/^(fee_|buy_fee|sell_fee|min_|minimum_|price_provider|zaarbox_|main_product$|show_last_price|close_price|startDay|endDay|up_price|down_price|price_extreme_date|balance_report_exclude)/.test(
|
|
||||||
k
|
|
||||||
) || k.includes('charge_percentage')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'content',
|
|
||||||
title: 'متنهای اپ (قوانین و درباره)',
|
|
||||||
icon: 'vuesax-linear:document-text',
|
|
||||||
match: (k) => /^text_/.test(k)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'referral',
|
|
||||||
title: 'معرف، پاداش و ثبتنام',
|
|
||||||
icon: 'vuesax-linear:people',
|
|
||||||
match: (k) =>
|
|
||||||
/^(referral_|gift_for|register_referral|register_jibit|has_page_earn_money|jibit_verification|referral_bonus|invite_link_text)/.test(
|
|
||||||
k
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'montzar',
|
|
||||||
title: 'همکار قیمت (Montzar)',
|
|
||||||
icon: 'vuesax-linear:link-2',
|
|
||||||
match: (k) => k.startsWith('montzar_') || k === 'display_montzar'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'store',
|
|
||||||
title: 'اتصال به فروشگاه (Montify)',
|
|
||||||
icon: 'vuesax-linear:shop',
|
|
||||||
match: (k) => /^(external_order|node_|montify_name)/.test(k)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'payping',
|
|
||||||
title: 'درگاه Payping / Zify',
|
|
||||||
icon: 'vuesax-linear:card',
|
|
||||||
match: (k) => k.startsWith('payping_')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tahesab',
|
|
||||||
title: 'تهحساب',
|
|
||||||
icon: 'vuesax-linear:receipt-2',
|
|
||||||
match: (k) => k.startsWith('tahesab_') || k === 'send_to_tahesab'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'support',
|
|
||||||
title: 'پشتیبانی اپ کاربر',
|
|
||||||
icon: 'vuesax-linear:headphone',
|
|
||||||
match: (k) => k.startsWith('support_messenger') || k === 'deposit_sms_support_number'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'bots',
|
|
||||||
title: 'بات تلگرام و بله',
|
|
||||||
icon: 'vuesax-linear:messages-2',
|
|
||||||
match: (k) => /^(telegram_bot|bale_bot)/.test(k)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'notifications',
|
|
||||||
title: 'پیامک و اعلان',
|
|
||||||
icon: 'vuesax-linear:notification',
|
|
||||||
match: (k) => /^(charge_notif|group_sms)/.test(k)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'features',
|
|
||||||
title: 'محصولات و قابلیتهای سیستم',
|
|
||||||
icon: 'vuesax-linear:setting-2',
|
|
||||||
match: (k) => /^(product_|main_wallet_products|price_from_nasiryan|profit_gold)/.test(k)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'misc',
|
|
||||||
title: 'سایر تنظیمات',
|
|
||||||
icon: 'vuesax-linear:more-circle',
|
|
||||||
match: () => true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
function getGroupId(key: string): string {
|
function getGroupId(key: string): string {
|
||||||
for (const g of SETTING_GROUPS) {
|
return getSettingGroupId(key, settingGroups.value)
|
||||||
if (g.id === 'misc') continue
|
|
||||||
if (g.match(key)) return g.id
|
|
||||||
}
|
|
||||||
return 'misc'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupedFiltered = computed(() => {
|
const groupedFiltered = computed(() => {
|
||||||
const map: Record<string, Const[]> = {}
|
const map: Record<string, Const[]> = {}
|
||||||
for (const g of SETTING_GROUPS) map[g.id] = []
|
for (const g of settingGroups.value) map[g.id] = []
|
||||||
for (const row of filteredRows.value) {
|
for (const row of filteredRows.value) {
|
||||||
const gid = getGroupId(row.key || '')
|
const gid = getGroupId(row.key || '')
|
||||||
map[gid].push(row)
|
map[gid].push(row)
|
||||||
}
|
}
|
||||||
// حذف گروههای خالی
|
|
||||||
Object.keys(map).forEach((k) => {
|
Object.keys(map).forEach((k) => {
|
||||||
if (map[k].length === 0) delete map[k]
|
if (map[k].length === 0) delete map[k]
|
||||||
})
|
})
|
||||||
return map
|
return map
|
||||||
})
|
})
|
||||||
|
|
||||||
// کالپسهای باز — وقتی سرچ فعال است فقط گروههای دارای نتیجه باز میشوند
|
const visibleSettingGroups = computed(() =>
|
||||||
const defaultOpenGroups = ['general', 'pricing', 'store', 'payping', 'tahesab']
|
settingGroups.value.filter((g) => (groupedFiltered.value[g.id]?.length ?? 0) > 0)
|
||||||
const activeKeys = ref<string[]>(defaultOpenGroups)
|
)
|
||||||
|
|
||||||
|
const defaultOpenGroupId = computed(() => {
|
||||||
|
const visibleIds = visibleSettingGroups.value.map((g) => g.id)
|
||||||
|
const preferred = isSystemMode.value
|
||||||
|
? ['chart', 'pricing', 'payping', 'tahesab', 'store', 'general']
|
||||||
|
: ['general', 'content', 'pricing', 'referral']
|
||||||
|
const fromPreferred = preferred.find((id) => visibleIds.includes(id))
|
||||||
|
return fromPreferred ?? visibleIds[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeKey = ref<string | undefined>(undefined)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
visibleSettingGroups,
|
||||||
|
(groups) => {
|
||||||
|
const visibleIds = new Set(groups.map((g) => g.id))
|
||||||
|
if (activeKey.value && !visibleIds.has(activeKey.value)) {
|
||||||
|
activeKey.value = groups[0]?.id
|
||||||
|
}
|
||||||
|
if (!activeKey.value && groups.length) {
|
||||||
|
activeKey.value = defaultOpenGroupId.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ flush: 'post' }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
defaultOpenGroupId,
|
||||||
|
(id) => {
|
||||||
|
if (!activeKey.value && id) {
|
||||||
|
activeKey.value = id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
watch(search, (q) => {
|
watch(search, (q) => {
|
||||||
const query = q.trim().toLowerCase()
|
const query = q.trim().toLowerCase()
|
||||||
if (!query) {
|
if (!query) {
|
||||||
// برگشت به حالت پیشفرض
|
if (!activeKey.value || !visibleSettingGroups.value.some((g) => g.id === activeKey.value)) {
|
||||||
if (activeKeys.value.length === 0 || activeKeys.value.every(k => !defaultOpenGroups.includes(k))) {
|
activeKey.value = defaultOpenGroupId.value
|
||||||
activeKeys.value = [...defaultOpenGroups]
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// فقط گروههایی که حداقل یک آیتم matching دارند
|
const matching = Object.keys(groupedFiltered.value).find(
|
||||||
const matching: string[] = []
|
(gid) => (groupedFiltered.value[gid]?.length ?? 0) > 0
|
||||||
for (const gid of Object.keys(groupedFiltered.value)) {
|
)
|
||||||
if (groupedFiltered.value[gid].length > 0) matching.push(gid)
|
activeKey.value = matching ?? defaultOpenGroupId.value
|
||||||
}
|
|
||||||
activeKeys.value = matching.length ? matching : defaultOpenGroups
|
|
||||||
}, { flush: 'post' })
|
}, { flush: 'post' })
|
||||||
|
|
||||||
function rowColSpan(row: Const): number {
|
function rowColSpan(row: Const): number {
|
||||||
@@ -414,7 +373,8 @@ function rowColSpan(row: Const): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isReadonly(row: Const): boolean {
|
function isReadonly(row: Const): boolean {
|
||||||
return row.can_edit === 0 || row.can_edit === false
|
if (isSystemMode.value) return false
|
||||||
|
return isSettingLocked(row.can_edit)
|
||||||
}
|
}
|
||||||
|
|
||||||
function draftValue(row: Const): string {
|
function draftValue(row: Const): string {
|
||||||
@@ -443,14 +403,29 @@ onMounted(() => {
|
|||||||
initConfig()
|
initConfig()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.mode,
|
||||||
|
() => {
|
||||||
|
search.value = ''
|
||||||
|
filterType.value = undefined
|
||||||
|
activeKey.value = undefined
|
||||||
|
initConfig()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
async function initConfig() {
|
async function initConfig() {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
baseData.value = (await getEditableConfiguration()) ?? []
|
baseData.value =
|
||||||
const storeRow = baseData.value.find((row) => row.key === 'is_store')
|
(isSystemMode.value
|
||||||
if (storeRow) {
|
? await getSystemConfiguration()
|
||||||
setStoreModeFromSettings(isSwitchOn(storeRow.value))
|
: await getEditableConfiguration()) ?? []
|
||||||
refreshAppModeMenu()
|
if (!isSystemMode.value) {
|
||||||
|
const storeRow = baseData.value.find((row) => row.key === 'is_store')
|
||||||
|
if (storeRow) {
|
||||||
|
setStoreModeFromSettings(isSwitchOn(storeRow.value))
|
||||||
|
refreshMenuAfterStoreChange()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
syncDraftsFromBase()
|
syncDraftsFromBase()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -534,7 +509,8 @@ async function saveSettingRow(row: Const) {
|
|||||||
savedFlashIds.value.delete(id)
|
savedFlashIds.value.delete(id)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await setConfiguration({
|
const saveFn = isSystemMode.value ? setSystemConfiguration : setConfiguration
|
||||||
|
await saveFn({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
key: row.key,
|
key: row.key,
|
||||||
value,
|
value,
|
||||||
@@ -551,9 +527,9 @@ async function saveSettingRow(row: Const) {
|
|||||||
} else {
|
} else {
|
||||||
scheduleSave(row)
|
scheduleSave(row)
|
||||||
}
|
}
|
||||||
if (row.key === 'is_store') {
|
if (row.key === 'is_store' && !isSystemMode.value) {
|
||||||
setStoreModeFromSettings(isSwitchOn(value))
|
setStoreModeFromSettings(isSwitchOn(value))
|
||||||
refreshAppModeMenu()
|
refreshMenuAfterStoreChange()
|
||||||
}
|
}
|
||||||
savedFlashIds.value.add(id)
|
savedFlashIds.value.add(id)
|
||||||
if (saveTimers.has(id)) clearTimeout(saveTimers.get(id))
|
if (saveTimers.has(id)) clearTimeout(saveTimers.get(id))
|
||||||
@@ -610,7 +586,8 @@ async function ok() {
|
|||||||
try {
|
try {
|
||||||
await formRef.value?.validateFields()
|
await formRef.value?.validateFields()
|
||||||
loading.value = true
|
loading.value = true
|
||||||
await setConfiguration(payload)
|
const saveFn = isSystemMode.value ? setSystemConfiguration : setConfiguration
|
||||||
|
await saveFn(payload)
|
||||||
message.success('تنظیم جدید اضافه شد')
|
message.success('تنظیم جدید اضافه شد')
|
||||||
addConfigVisible.value = false
|
addConfigVisible.value = false
|
||||||
await initConfig()
|
await initConfig()
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
export type SettingGroupDef = {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
icon: string
|
||||||
|
match: (key: string) => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** گروههای صفحه تنظیمات عادی (فقط can_edit=1) */
|
||||||
|
export const EDITABLE_SETTING_GROUPS: SettingGroupDef[] = [
|
||||||
|
{
|
||||||
|
id: 'general',
|
||||||
|
title: 'عمومی و برند',
|
||||||
|
icon: 'vuesax-linear:home-2',
|
||||||
|
match: (k) =>
|
||||||
|
/^(name_fa|name_en|bace_url|server_url|logo_|title_pwa|title_fav|open_register|auto_active_customer|store_status|key$)/.test(
|
||||||
|
k
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pricing',
|
||||||
|
title: 'کارمزد و محدودیتها',
|
||||||
|
icon: 'vuesax-linear:dollar-circle',
|
||||||
|
match: (k) =>
|
||||||
|
/^(fee_|buy_fee|sell_fee|min_|minimum_|show_last_price|close_price|startDay|endDay|up_price|down_price|price_extreme_date|balance_report_exclude)/.test(
|
||||||
|
k
|
||||||
|
) || k.includes('charge_percentage')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'content',
|
||||||
|
title: 'متنهای اپ',
|
||||||
|
icon: 'vuesax-linear:document-text',
|
||||||
|
match: (k) => /^text_/.test(k)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'referral',
|
||||||
|
title: 'معرف و پاداش',
|
||||||
|
icon: 'vuesax-linear:people',
|
||||||
|
match: (k) =>
|
||||||
|
/^(referral_|gift_for|register_referral|register_jibit|jibit_verification|referral_bonus|invite_link_text)/.test(
|
||||||
|
k
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'montzar',
|
||||||
|
title: 'همکار Montzar',
|
||||||
|
icon: 'vuesax-linear:link-2',
|
||||||
|
match: (k) =>
|
||||||
|
(k.startsWith('montzar_') && !['montzar_base_url', 'montzar_closed'].includes(k)) ||
|
||||||
|
k === 'display_montzar'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'store',
|
||||||
|
title: 'سفارش خارجی',
|
||||||
|
icon: 'vuesax-linear:shop',
|
||||||
|
match: (k) => /^external_order/.test(k)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'support',
|
||||||
|
title: 'پشتیبانی',
|
||||||
|
icon: 'vuesax-linear:headphone',
|
||||||
|
match: (k) => k.startsWith('support_messenger') || k === 'deposit_sms_support_number'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bots',
|
||||||
|
title: 'باتها',
|
||||||
|
icon: 'vuesax-linear:messages-2',
|
||||||
|
match: (k) => /^(telegram_bot|bale_bot)/.test(k)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'notifications',
|
||||||
|
title: 'پیامک و اعلان',
|
||||||
|
icon: 'vuesax-linear:notification',
|
||||||
|
match: (k) => /^charge_notif/.test(k)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'features',
|
||||||
|
title: 'قابلیتهای سیستم',
|
||||||
|
icon: 'vuesax-linear:setting-2',
|
||||||
|
match: (k) => /^(product_|main_wallet_products|price_from_nasiryan|profit_gold)/.test(k)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'misc',
|
||||||
|
title: 'سایر',
|
||||||
|
icon: 'vuesax-linear:more-circle',
|
||||||
|
match: () => true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/** گروههای صفحه دیپلوی — شامل تنظیمات can_edit=0 */
|
||||||
|
export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
|
||||||
|
{
|
||||||
|
id: 'general',
|
||||||
|
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|unit$|open_register|auto_active_customer|store_status|key$|is_store|group_sms|has_page_earn_money)/.test(
|
||||||
|
k
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'chart',
|
||||||
|
title: 'نمودار قیمت',
|
||||||
|
icon: 'vuesax-linear:chart-2',
|
||||||
|
match: (k) => /^chart_/.test(k)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pricing',
|
||||||
|
title: 'منبع قیمت و تابلو',
|
||||||
|
icon: 'vuesax-linear:dollar-circle',
|
||||||
|
match: (k) =>
|
||||||
|
/^(price_provider|zaarbox_|main_product$|default_vc_number)/.test(k) ||
|
||||||
|
k.includes('charge_percentage')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'montzar',
|
||||||
|
title: 'Montzar',
|
||||||
|
icon: 'vuesax-linear:link-2',
|
||||||
|
match: (k) => /^montzar_/.test(k) || k === 'display_montzar'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'store',
|
||||||
|
title: 'اتصال فروشگاه',
|
||||||
|
icon: 'vuesax-linear:shop',
|
||||||
|
match: (k) => /^node_/.test(k) || k === 'montify_name' || k === 'external_order_api_enabled'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'payping',
|
||||||
|
title: 'درگاه Payping',
|
||||||
|
icon: 'vuesax-linear:card',
|
||||||
|
match: (k) => k.startsWith('payping_')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tahesab',
|
||||||
|
title: 'تهحساب',
|
||||||
|
icon: 'vuesax-linear:receipt-2',
|
||||||
|
match: (k) => k.startsWith('tahesab_') || k === 'send_to_tahesab'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'misc',
|
||||||
|
title: 'سایر',
|
||||||
|
icon: 'vuesax-linear:more-circle',
|
||||||
|
match: () => true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export function getSettingGroupId(key: string, groups: SettingGroupDef[]): string {
|
||||||
|
for (const g of groups) {
|
||||||
|
if (g.id === 'misc') continue
|
||||||
|
if (g.match(key)) return g.id
|
||||||
|
}
|
||||||
|
return 'misc'
|
||||||
|
}
|
||||||
@@ -19,6 +19,15 @@ export const HIDDEN_LEGACY_SETTING_KEYS = new Set([
|
|||||||
/** فقط وقتی is_store فعال است در پنل دیده میشوند */
|
/** فقط وقتی is_store فعال است در پنل دیده میشوند */
|
||||||
export const STORE_ONLY_SETTING_KEYS = new Set(['store_status', 'price_from_nasiryan'])
|
export const STORE_ONLY_SETTING_KEYS = new Set(['store_status', 'price_from_nasiryan'])
|
||||||
|
|
||||||
|
export function isSettingLocked(canEdit: Const['can_edit']): boolean {
|
||||||
|
return canEdit === 0 || canEdit === false || canEdit === '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSettingActive(isActive: Const['is_active']): boolean {
|
||||||
|
if (isActive === 0 || isActive === false || isActive === '0') return false
|
||||||
|
return isActive !== null && isActive !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
export function isSettingVisible(key: string | undefined | null): boolean {
|
export function isSettingVisible(key: string | undefined | null): boolean {
|
||||||
const k = (key || '').trim()
|
const k = (key || '').trim()
|
||||||
if (!k) return false
|
if (!k) return false
|
||||||
@@ -78,6 +87,8 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
|
|||||||
external_order_api_enabled: 'switch',
|
external_order_api_enabled: 'switch',
|
||||||
is_store: 'switch',
|
is_store: 'switch',
|
||||||
price_from_nasiryan: 'switch',
|
price_from_nasiryan: 'switch',
|
||||||
|
banner_enabled: 'switch',
|
||||||
|
apk_download_link: 'text',
|
||||||
node_client_id: 'text',
|
node_client_id: 'text',
|
||||||
node_shared_secret: 'text',
|
node_shared_secret: 'text',
|
||||||
tahesab_host: 'text',
|
tahesab_host: 'text',
|
||||||
|
|||||||
@@ -11,7 +11,10 @@
|
|||||||
class="hidden md:inline overflow-auto min-w-[13.5rem]"
|
class="hidden md:inline overflow-auto min-w-[13.5rem]"
|
||||||
/>
|
/>
|
||||||
<!-- <component :is="selectedComponent" class="mr-5" />-->
|
<!-- <component :is="selectedComponent" class="mr-5" />-->
|
||||||
<RouterView class="mt-5 min-h-0 w-full flex-1 overflow-hidden md:mr-5 md:mt-0" />
|
<RouterView
|
||||||
|
:key="route.name"
|
||||||
|
class="mt-5 min-h-0 w-full flex-1 overflow-hidden md:mr-5 md:mt-0"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</BasicMainCard>
|
</BasicMainCard>
|
||||||
</div>
|
</div>
|
||||||
@@ -20,14 +23,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import BasicMenu from '@/components/BasicMenu/index.vue'
|
import BasicMenu from '@/components/BasicMenu/index.vue'
|
||||||
import { useRouterStore } from '@core/stores/router'
|
import { useRouterStore } from '@core/stores/router'
|
||||||
|
import { useUserStore } from '@/store/userStore'
|
||||||
|
import { isDeployAdmin } from '@/utils/deployAdmin'
|
||||||
import { get } from 'lodash'
|
import { get } from 'lodash'
|
||||||
import type { RouteRecordRaw } from 'vue-router'
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
const routerStore = useRouterStore()
|
const routerStore = useRouterStore()
|
||||||
|
const route = useRoute()
|
||||||
|
const userStore = useUserStore()
|
||||||
const menu = computed(() => {
|
const menu = computed(() => {
|
||||||
const item = routerStore.routes.find((item) => item.name === 'settings')
|
const item = routerStore.routes.find((item) => item.name === 'settings')
|
||||||
const children = get(item, 'children[0].children', []) as RouteRecordRaw[]
|
const children = get(item, 'children[0].children', []) as RouteRecordRaw[]
|
||||||
return children.filter((child) => !child.meta?.hideMenu)
|
return children
|
||||||
|
.filter((child) => !child.meta?.hideMenu)
|
||||||
|
.filter(
|
||||||
|
(child) =>
|
||||||
|
!child.meta?.deployAdminOnly || isDeployAdmin(userStore.userInfo?.mobile)
|
||||||
|
)
|
||||||
|
.sort((a, b) => (Number(a.meta?.order) || 99) - (Number(b.meta?.order) || 99))
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
+79
-50
@@ -3,6 +3,7 @@
|
|||||||
import { useRouterStore } from '@core/stores/router'
|
import { useRouterStore } from '@core/stores/router'
|
||||||
import type { Router, RouteRecordRaw } from 'vue-router'
|
import type { Router, RouteRecordRaw } from 'vue-router'
|
||||||
import { getConfigApp, isStoreApp } from '@/utils/config'
|
import { getConfigApp, isStoreApp } from '@/utils/config'
|
||||||
|
import { isDeployAdmin } from '@/utils/deployAdmin'
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash'
|
||||||
//const _router = useRouter()
|
//const _router = useRouter()
|
||||||
//import { cloneDeep } from 'lodash'
|
//import { cloneDeep } from 'lodash'
|
||||||
@@ -45,38 +46,84 @@ function filterRoutesByApp(routes: RouteRecordRaw, appName: string | null) {
|
|||||||
routes.children.forEach((child) => filterRoutesByApp(child, appName))
|
routes.children.forEach((child) => filterRoutesByApp(child, appName))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** بعد از تغییر is_store — منو را از روتهای تازه (clone) دوباره میسازد */
|
/** زیرمنوی تنظیمات پیشرفته — فقط برای ادمین دیپلoyi */
|
||||||
export function refreshAppModeMenu() {
|
function filterDeployAdminRoutes(route: RouteRecordRaw, mobile?: string | number | null) {
|
||||||
const routerStore = useRouterStore()
|
if (!route.children?.length) return
|
||||||
|
|
||||||
|
route.children = route.children.filter((child) => {
|
||||||
|
if (!child.meta?.deployAdminOnly) return true
|
||||||
|
return isDeployAdmin(mobile)
|
||||||
|
})
|
||||||
|
|
||||||
|
route.children.forEach((child) => filterDeployAdminRoutes(child, mobile))
|
||||||
|
}
|
||||||
|
|
||||||
|
const notFoundRoute: RouteRecordRaw = {
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
name: 'notFound',
|
||||||
|
component: () => import('@/core/pages/404.vue'),
|
||||||
|
meta: {
|
||||||
|
hideMenu: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function basePreservedRoutes(): RouteRecordRaw[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'factor',
|
||||||
|
path: '/withdraw/factor/:id',
|
||||||
|
component: () => import('@/pages/menuPages/withdraw/factor/index.vue'),
|
||||||
|
meta: {
|
||||||
|
permissionsKey: 'factor',
|
||||||
|
hideMenu: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** notFound باید آخر لیست باشد وگرنه /dashboard و بقیه مسیرها 404 میشوند */
|
||||||
|
function composeRoutes(dynamicRoutes: RouteRecordRaw[]): RouteRecordRaw[] {
|
||||||
|
return [...basePreservedRoutes(), ...dynamicRoutes, notFoundRoute]
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareRoute(route: RouteRecordRaw, appName: string | null, mobile?: string | number | null) {
|
||||||
|
applyAppModeMeta(route, appName)
|
||||||
|
filterRoutesByApp(route, appName)
|
||||||
|
filterDeployAdminRoutes(route, mobile)
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDynamicRoutes(appName: string | null, mobile?: string | number | null): RouteRecordRaw[] {
|
||||||
const modules = import.meta.glob<any>('./routes/*.ts', { eager: true })
|
const modules = import.meta.glob<any>('./routes/*.ts', { eager: true })
|
||||||
const appName = getConfigApp('name')
|
|
||||||
|
|
||||||
const preserved = routerStore.routes.filter(
|
|
||||||
(route) => route.name === 'notFound' || route.name === 'factor'
|
|
||||||
)
|
|
||||||
|
|
||||||
const dynamicRoutes: RouteRecordRaw[] = []
|
const dynamicRoutes: RouteRecordRaw[] = []
|
||||||
|
|
||||||
for (const modulesKey in modules) {
|
for (const modulesKey in modules) {
|
||||||
const sourceRoute: RouteRecordRaw | undefined = modules[modulesKey].default ?? undefined
|
const sourceRoute: RouteRecordRaw | undefined = modules[modulesKey].default ?? undefined
|
||||||
if (!sourceRoute) continue
|
if (!sourceRoute) continue
|
||||||
|
if (sourceRoute.meta?.deployAdminOnly) continue
|
||||||
|
|
||||||
const route = cloneDeep(sourceRoute)
|
const route = cloneDeep(sourceRoute)
|
||||||
applyAppModeMeta(route, appName)
|
prepareRoute(route, appName, mobile)
|
||||||
filterRoutesByApp(route, appName)
|
|
||||||
dynamicRoutes.push(route)
|
dynamicRoutes.push(route)
|
||||||
}
|
}
|
||||||
|
|
||||||
routerStore.routes = [...preserved, ...dynamicRoutes]
|
return dynamicRoutes
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupRoutes(permissionsKeys: string[], router?: Router) {
|
/** بعد از تغییر is_store — منو را از روتهای تازه (clone) دوباره میسازد */
|
||||||
//console.log('this is permission', permissionsKeys)
|
export function refreshAppModeMenu(mobile?: string | number | null) {
|
||||||
//console.log('this is use router', _router)
|
|
||||||
console.log('salammm')
|
|
||||||
const routerStore = useRouterStore()
|
const routerStore = useRouterStore()
|
||||||
const modules = import.meta.glob<any>('./routes/*.ts', { eager: true })
|
|
||||||
console.log(modules)
|
|
||||||
const appName = getConfigApp('name')
|
const appName = getConfigApp('name')
|
||||||
|
routerStore.routes = composeRoutes(loadDynamicRoutes(appName, mobile))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupRoutes(
|
||||||
|
permissionsKeys: string[],
|
||||||
|
router?: Router,
|
||||||
|
userMobile?: string | number | null
|
||||||
|
) {
|
||||||
|
const routerStore = useRouterStore()
|
||||||
|
const appName = getConfigApp('name')
|
||||||
|
const modules = import.meta.glob<any>('./routes/*.ts', { eager: true })
|
||||||
|
|
||||||
function walkInRoutes(routes: RouteRecordRaw) {
|
function walkInRoutes(routes: RouteRecordRaw) {
|
||||||
if (!routes.children) return
|
if (!routes.children) return
|
||||||
@@ -88,54 +135,36 @@ export function setupRoutes(permissionsKeys: string[], router?: Router) {
|
|||||||
for (const route of routes.children) {
|
for (const route of routes.children) {
|
||||||
walkInRoutes(route)
|
walkInRoutes(route)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
routerStore.routes = [
|
const dynamicRoutes: RouteRecordRaw[] = []
|
||||||
{
|
|
||||||
path: '/:pathMatch(.*)*',
|
|
||||||
name: 'notFound',
|
|
||||||
component: () => import('@/core/pages/404.vue'),
|
|
||||||
meta: {
|
|
||||||
hideMenu: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'factor',
|
|
||||||
path: '/withdraw/factor/:id',
|
|
||||||
component: () => import('@/pages/menuPages/withdraw/factor/index.vue'),
|
|
||||||
meta: {
|
|
||||||
permissionsKey: 'factor',
|
|
||||||
hideMenu: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
for (const modulesKey in modules) {
|
for (const modulesKey in modules) {
|
||||||
const sourceRoute: RouteRecordRaw | undefined = modules[modulesKey].default ?? undefined
|
const sourceRoute: RouteRecordRaw | undefined = modules[modulesKey].default ?? undefined
|
||||||
if (!sourceRoute) continue
|
if (!sourceRoute) continue
|
||||||
|
if (sourceRoute.meta?.deployAdminOnly) continue
|
||||||
|
|
||||||
const route = cloneDeep(sourceRoute)
|
const route = cloneDeep(sourceRoute)
|
||||||
|
|
||||||
if (permissionsKeys.length && permissionsKeys[0] == '*') {
|
if (permissionsKeys.length && permissionsKeys[0] == '*') {
|
||||||
applyAppModeMeta(route, appName)
|
prepareRoute(route, appName, userMobile)
|
||||||
filterRoutesByApp(route, appName)
|
dynamicRoutes.push(route)
|
||||||
routerStore.routes.push(route)
|
|
||||||
if (router) {
|
|
||||||
router.addRoute(route)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (
|
if (
|
||||||
!route?.meta?.permissionsKey ||
|
!route?.meta?.permissionsKey ||
|
||||||
(route.meta.permissionsKey && permissionsKeys.includes(route.meta.permissionsKey))
|
(route.meta.permissionsKey && permissionsKeys.includes(route.meta.permissionsKey))
|
||||||
) {
|
) {
|
||||||
walkInRoutes(route)
|
walkInRoutes(route)
|
||||||
applyAppModeMeta(route, appName)
|
prepareRoute(route, appName, userMobile)
|
||||||
filterRoutesByApp(route, appName)
|
dynamicRoutes.push(route)
|
||||||
routerStore.routes.push(route)
|
|
||||||
if (router) {
|
|
||||||
router.addRoute(route)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
routerStore.routes = composeRoutes(dynamicRoutes)
|
||||||
|
if (router) {
|
||||||
|
for (const route of dynamicRoutes) {
|
||||||
|
router.addRoute(route)
|
||||||
|
}
|
||||||
|
router.addRoute(notFoundRoute)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import type { RouteRecordRaw } from 'vue-router'
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
import { isDeployAdmin } from '@/utils/deployAdmin'
|
||||||
|
import { useUserStore } from '@/store/userStore'
|
||||||
|
|
||||||
const Setting: RouteRecordRaw = {
|
const Setting: RouteRecordRaw = {
|
||||||
name: 'settings',
|
name: 'settings',
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
@@ -26,9 +29,46 @@ const Setting: RouteRecordRaw = {
|
|||||||
title: 'پیکربندی',
|
title: 'پیکربندی',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
hideMenu: true
|
hideMenu: true
|
||||||
//hideChildren: false
|
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
name: 'configuration',
|
||||||
|
path: 'configuration',
|
||||||
|
component: () => import('@/pages/menuPages/configuration/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'vuesax-linear:setting-2',
|
||||||
|
title: 'تنظیمات',
|
||||||
|
order: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'advancedConfiguration',
|
||||||
|
path: 'advanced-configuration',
|
||||||
|
component: () => import('@/pages/menuPages/configuration/index.vue'),
|
||||||
|
props: { mode: 'system' },
|
||||||
|
meta: {
|
||||||
|
deployAdminOnly: true,
|
||||||
|
icon: 'vuesax-linear:code-circle',
|
||||||
|
title: 'تنظیمات پیشرفته',
|
||||||
|
order: 2
|
||||||
|
},
|
||||||
|
beforeEnter() {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
if (!isDeployAdmin(userStore.userInfo?.mobile)) {
|
||||||
|
return { name: 'configuration' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'banners',
|
||||||
|
path: 'banners',
|
||||||
|
component: () => import('@/pages/menuPages/banners/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'vuesax-linear:image',
|
||||||
|
title: 'بنرها',
|
||||||
|
order: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'settingMain_limitedOrder',
|
name: 'settingMain_limitedOrder',
|
||||||
path: 'limitedOrder',
|
path: 'limitedOrder',
|
||||||
@@ -36,19 +76,8 @@ const Setting: RouteRecordRaw = {
|
|||||||
meta: {
|
meta: {
|
||||||
permissionsKey: 'config.limitedOrder',
|
permissionsKey: 'config.limitedOrder',
|
||||||
icon: 'vuesax-linear:document-text',
|
icon: 'vuesax-linear:document-text',
|
||||||
title: '' +
|
title: 'محدودیت سفارش',
|
||||||
'' +
|
order: 4
|
||||||
'محدودیت سفارش'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'usersMain',
|
|
||||||
path: 'admins',
|
|
||||||
component: () => import('@/pages/menuPages/users/index.vue'),
|
|
||||||
meta: {
|
|
||||||
permissionsKey: 'user',
|
|
||||||
icon: 'vuesax-linear:profile',
|
|
||||||
title: 'لیست مدیران'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -58,7 +87,8 @@ const Setting: RouteRecordRaw = {
|
|||||||
meta: {
|
meta: {
|
||||||
permissionsKey: 'priceTriggers',
|
permissionsKey: 'priceTriggers',
|
||||||
icon: 'vuesax-linear:notification-bing',
|
icon: 'vuesax-linear:notification-bing',
|
||||||
title: 'هشدار قیمت'
|
title: 'هشدار قیمت',
|
||||||
|
order: 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -68,7 +98,8 @@ const Setting: RouteRecordRaw = {
|
|||||||
meta: {
|
meta: {
|
||||||
permissionsKey: 'sms',
|
permissionsKey: 'sms',
|
||||||
icon: 'vuesax-linear:sms',
|
icon: 'vuesax-linear:sms',
|
||||||
title: 'پیامکها'
|
title: 'پیامکها',
|
||||||
|
order: 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -79,7 +110,8 @@ const Setting: RouteRecordRaw = {
|
|||||||
permissionsKey: 'project',
|
permissionsKey: 'project',
|
||||||
icon: 'vuesax-linear:gift',
|
icon: 'vuesax-linear:gift',
|
||||||
title: 'کارت هدیه',
|
title: 'کارت هدیه',
|
||||||
hideMenu: true
|
hideMenu: true,
|
||||||
|
order: 7
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -90,28 +122,21 @@ const Setting: RouteRecordRaw = {
|
|||||||
permissionsKey: 'discounts',
|
permissionsKey: 'discounts',
|
||||||
icon: 'vuesax-linear:discount-shape',
|
icon: 'vuesax-linear:discount-shape',
|
||||||
title: 'تخفیفات',
|
title: 'تخفیفات',
|
||||||
hideMenu: true
|
hideMenu: true,
|
||||||
|
order: 8
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'configuration',
|
name: 'usersMain',
|
||||||
path: 'configuration',
|
path: 'admins',
|
||||||
component: () => import('@/pages/menuPages/configuration/index.vue'),
|
component: () => import('@/pages/menuPages/users/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'vuesax-linear:setting-2',
|
permissionsKey: 'user',
|
||||||
title: 'تنظیمات'
|
icon: 'vuesax-linear:profile',
|
||||||
//hideChildren: false
|
title: 'مدیران',
|
||||||
|
order: 9
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
{
|
|
||||||
name: 'banners',
|
|
||||||
path: 'banners',
|
|
||||||
component: () => import('@/pages/menuPages/banners/index.vue'),
|
|
||||||
meta: {
|
|
||||||
icon: 'vuesax-linear:image',
|
|
||||||
title: 'بنرها'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { type ComputedRef, ref } from 'vue'
|
|||||||
import type { User } from '@/model/user'
|
import type { User } from '@/model/user'
|
||||||
import { getMyInformation } from '@/api/user'
|
import { getMyInformation } from '@/api/user'
|
||||||
import { get } from 'lodash'
|
import { get } from 'lodash'
|
||||||
|
import { isDeployAdmin } from '@/utils/deployAdmin'
|
||||||
export const useUserStore = defineStore('user', () => {
|
export const useUserStore = defineStore('user', () => {
|
||||||
const userInfo = ref<User | undefined>(undefined)
|
const userInfo = ref<User | undefined>(undefined)
|
||||||
|
|
||||||
@@ -29,5 +30,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
return userInfo.value?.is_admin
|
return userInfo.value?.is_admin
|
||||||
})
|
})
|
||||||
|
|
||||||
return { userInfo, getMyInfo, permissions, canAccess, isAdmin }
|
const isDeployAdminUser = computed(() => isDeployAdmin(userInfo.value?.mobile))
|
||||||
|
|
||||||
|
return { userInfo, getMyInfo, permissions, canAccess, isAdmin, isDeployAdminUser }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export const DEPLOY_ADMIN_MOBILE = '09128081392'
|
||||||
|
const DEPLOY_ADMIN_MOBILE_CORE = '9128081392'
|
||||||
|
|
||||||
|
export function normalizeMobile(mobile?: string | number | null): string {
|
||||||
|
let digits = String(mobile ?? '').replace(/\D/g, '')
|
||||||
|
if (digits.startsWith('98') && digits.length >= 12) {
|
||||||
|
digits = digits.slice(2)
|
||||||
|
}
|
||||||
|
if (digits.length === 11 && digits.startsWith('0')) {
|
||||||
|
digits = digits.slice(1)
|
||||||
|
}
|
||||||
|
return digits
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDeployAdmin(mobile?: string | number | null): boolean {
|
||||||
|
return normalizeMobile(mobile) === DEPLOY_ADMIN_MOBILE_CORE
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user