create landing page for boursemes

This commit is contained in:
2026-08-01 10:11:28 +03:30
commit 4cc5ca6b5c
150 changed files with 18714 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
<template>
<router-view />
</template>
+5
View File
@@ -0,0 +1,5 @@
import useAxios from "@/utils/useAxios";
export async function getSettings() {
return await useAxios.get(`api/get_settings`)
}
+11
View File
@@ -0,0 +1,11 @@
import useAxios from "@/utils/useAxios";
import type {Price} from "@/models/home";
export const getChart = async () => await useAxios.get<Array<Array<any>>>(`/api/candle`)
/** @deprecated قیمت up داخل /api/price/last در فیلد up برمی‌گردد */
export const getPrice = async () => await useAxios.get<Price>(`/api/price/up`)
export const getLastPrice = async () => await useAxios.get<Price>(`/api/price/last`)
export const getAllPrices = async () => await useAxios.post('/customer/prices/all')
+14
View File
@@ -0,0 +1,14 @@
import useAxios from "@/utils/useAxios";
import type {PInventory, Product} from "@/models/products";
import type { PWallet, Wallet } from '@/models/wallet'
export const getProducts = async (): Promise<any> => {
return await useAxios.get<Product[]>(`/customer/product/list`)
}
export const getProductInventory = async (): Promise<any> => {
return await useAxios.post<PInventory[]>(`/customer/wallet/product/table`)
}
export const getOneProductWallet = async (id: number): Promise<any> => {
return await useAxios.get<PWallet>(`/customer/wallet/product/${id}`)
}
+110
View File
@@ -0,0 +1,110 @@
@import (less) './var.less';
/**
* Dana Pro (FaNum) — اعداد فارسی
* منبع: DanaPro Webfonts / Farsi numerals
*/
@font-face {
font-family: dana;
font-style: normal;
font-weight: 10;
src: url('../fonts/Dana/woff2/DanaFaNum-Hairline.woff2') format('woff2'),
url('../fonts/Dana/woff/DanaFaNum-Hairline.woff') format('woff');
}
@font-face {
font-family: dana;
font-style: normal;
font-weight: 100;
src: url('../fonts/Dana/woff2/DanaFaNum-Thin.woff2') format('woff2'),
url('../fonts/Dana/woff/DanaFaNum-Thin.woff') format('woff');
}
@font-face {
font-family: dana;
font-style: normal;
font-weight: 200;
src: url('../fonts/Dana/woff2/DanaFaNum-UltraLight.woff2') format('woff2'),
url('../fonts/Dana/woff/DanaFaNum-UltraLight.woff') format('woff');
}
@font-face {
font-family: dana;
font-style: normal;
font-weight: 300;
src: url('../fonts/Dana/woff2/DanaFaNum-Light.woff2') format('woff2'),
url('../fonts/Dana/woff/DanaFaNum-Light.woff') format('woff');
}
@font-face {
font-family: dana;
font-style: normal;
font-weight: 400;
src: url('../fonts/Dana/woff2/DanaFaNum-Regular.woff2') format('woff2'),
url('../fonts/Dana/woff/DanaFaNum-Regular.woff') format('woff');
}
@font-face {
font-family: dana;
font-style: normal;
font-weight: 500;
src: url('../fonts/Dana/woff2/DanaFaNum-Medium.woff2') format('woff2'),
url('../fonts/Dana/woff/DanaFaNum-Medium.woff') format('woff');
}
@font-face {
font-family: dana;
font-style: normal;
font-weight: 600;
src: url('../fonts/Dana/woff2/DanaFaNum-DemiBold.woff2') format('woff2'),
url('../fonts/Dana/woff/DanaFaNum-DemiBold.woff') format('woff');
}
@font-face {
font-family: dana;
font-style: normal;
font-weight: 700;
src: url('../fonts/Dana/woff2/DanaFaNum-Bold.woff2') format('woff2'),
url('../fonts/Dana/woff/DanaFaNum-Bold.woff') format('woff');
}
@font-face {
font-family: dana;
font-style: normal;
font-weight: 800;
src: url('../fonts/Dana/woff2/DanaFaNum-ExtraBold.woff2') format('woff2'),
url('../fonts/Dana/woff/DanaFaNum-ExtraBold.woff') format('woff');
}
@font-face {
font-family: dana;
font-style: normal;
font-weight: 900;
src: url('../fonts/Dana/woff2/DanaFaNum-Black.woff2') format('woff2'),
url('../fonts/Dana/woff/DanaFaNum-Black.woff') format('woff');
}
@font-face {
font-family: dana;
font-style: normal;
font-weight: 930;
src: url('../fonts/Dana/woff2/DanaFaNum-ExtraBlack.woff2') format('woff2'),
url('../fonts/Dana/woff/DanaFaNum-ExtraBlack.woff') format('woff');
}
@font-face {
font-family: dana;
font-style: normal;
font-weight: 960;
src: url('../fonts/Dana/woff2/DanaFaNum-Heavy.woff2') format('woff2'),
url('../fonts/Dana/woff/DanaFaNum-Heavy.woff') format('woff');
}
@font-face {
font-family: dana;
font-style: normal;
font-weight: 990;
src: url('../fonts/Dana/woff2/DanaFaNum-fat.woff2') format('woff2'),
url('../fonts/Dana/woff/DanaFaNum-fat.woff') format('woff');
}
+13
View File
@@ -0,0 +1,13 @@
@import (less) './var.less';
@import (less) './fonts.less';
@import (less) '../../views/base/aboutUs/desktop/styles/desktop-about-us.less';
@import (less) '../../views/login/desktop/styles/desktop-auth.less';
html {
scroll-behavior: smooth;
}
body {
font-family: var(--App-font-family) !important;
margin: 0;
}
+14
View File
@@ -0,0 +1,14 @@
@prefixCls: 'noname';
@App-primary-color: var(--App-primary-color);
@secondary-color: var(--App-secondary-color);
@prefixCls-font-family: 'dana';
@prefixCls-background-color: var(--App-background-color);
@border-radius-base: 8px;
:root {
--App-primary-color: #0A1B37;
--App-secondary-color: #e0be7f;
--App-background-color: #ffffff;
--App-font-family: 'dana', sans-serif;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 3.5V14.5" stroke="#1a2b47" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.25 11L12 14.75L15.75 11" stroke="#1a2b47" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 16.5V18.25C5 19.22 5.78 20 6.75 20H17.25C18.22 20 19 19.22 19 18.25V16.5" stroke="#1a2b47" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 510 B

@@ -0,0 +1,3 @@
<svg width="19" height="10" viewBox="0 0 19 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.5002 0.833496L10.4168 7.91683L6.25016 3.75016L0.833496 9.16683" stroke="#0E9F6E" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

+47
View File
@@ -0,0 +1,47 @@
{
"primary":"#81d8d0",
"primary-rgba":"#81d8d0",
"secondary": "#135953",
"secondary-rgba": "#135953",
"tertiary": "#feb701",
"tertiary-rgba": "#fec94114",
"gray": {
"50": "#f9fafd",
"70": "#f9f9f9",
"100": "#e8eaee",
"200": "#caced8",
"300": "#D1D5DB",
"400": "#9CA3AF",
"500": "#949db0",
"700": "#374151",
"900": "#656f87"
},
"green": {
"100": "#ecf5ef",
"900": "#0f7f36"
},
"green-rgba": "#ecf5ef",
"primary": "#1e2229",
"white": "#ffffff",
"red": {
"100": "#fee2e2",
"500": "#f4474d",
"600": "#dc2626"
},
"red-rgba": "#fce6e6",
"yellow": {
"100": "#FFF6E1",
"900": "#FEB701"
},
"blue": {
"100": "#ebf4fd",
"600": "#0891B2",
"900": "#0891B2"
},
"cyan": {
"50": "#ECFEFF",
"300": "#67E8F9",
"600": "#0891B2",
"800": "#155E75"
}
}
+66
View File
@@ -0,0 +1,66 @@
<template>
<AButton :disabled="props.disabledB" :type="props.type" :size="props.size" :class="[prefixCls, _class]" :loading="props.loading">
<!-- py-1 px-2-->
<div
class="flex items-center gap-1 justify-center flex-row-reverse gap-2 py-1 px-2"
:class="[{ 'flex-row-reverse': suffixIcon }]"
>
<slot v-if="$slots.default"></slot>
<Icon :size="iconSize" :icon="icon" v-if="icon" :class="iconClass" />
</div>
</AButton>
</template>
<script lang="ts" setup>
// import { usePrefix } from '@core/composable/usePrefix'
import {usePrefix} from "@/utils/usePrefix";
import {computed, useSlots} from 'vue'
import {baseButtonIconSize} from "@/themeConfig";
import Icon from "@/components/BasicPacks/BasicIcon.vue";
interface Props {
icon?: string
iconSize?: string
iconClass?: string
suffixIcon?: boolean
type?: string
size?: string
loading?: boolean
disabledB?:boolean
// prefixIcon?: string
}
const slot = useSlots()
const _class = computed(() => {
if (!slot.default) {
return '!px-1 !py-1'
}
return ''
})
const _loading = computed(() => {
if (props.loading){
return 'flex'
} else return 'unset'
})
const { prefixCls } = usePrefix('base-button')
const props = withDefaults(defineProps<Props>(), {
iconSize: baseButtonIconSize,
// iconLeft: false,
iconClass: ''
})
</script>
<style lang="less">
@prefix:~'@{prefixCls}-base-button';
.@{prefix}{
//width: fit-content;
height: auto;
/*padding: 8px;*/
display: v-bind(_loading);
}
</style>
+54
View File
@@ -0,0 +1,54 @@
<template>
<span
ref="refIcon"
:style="{ color }"
:class="[prefixCls]"
class="!flex"
v-html="tempIcon"
></span>
</template>
<script lang="ts" setup>
import icons from '@/components/BasicPacks/tools/icons/icons.json'
import {usePrefix} from "@/utils/usePrefix";
import {resolveProjectColor} from "@/utils/config";
import {computed, onMounted, ref, withDefaults, defineProps} from "vue";
interface Props {
icon: string
color?: string | null
size?: string | number
strokeWidth?:string | number
}
const props = withDefaults(defineProps<Props>(), {
color: null,
})
const { prefixCls } = usePrefix('icons')
const tempIcon = ref<string>()
const refIcon = ref<HTMLDivElement>()
tempIcon.value = icons[`${props.icon}`] as string
const color = computed(() => {
return resolveProjectColor(props.color)
})
onMounted(() => {
if (props.size) {
const svg: SVGElement = refIcon.value?.children[0] as SVGElement
svg.setAttribute('width', props.size as string)
svg.setAttribute('height', props.size as string)
}
})
</script>
<style lang="less" scoped>
@prefix: ~'@{prefixCls}-icons';
.@{prefix} {
& :deep(svg path) {
stroke: currentColor;
stroke-width: v-bind("props.strokeWidth");
}
}
</style>
+79
View File
@@ -0,0 +1,79 @@
<template>
<div>
<a-modal
v-model:visible="localValue"
:title="props.title"
centered
:z-index="props.zIndex"
:get-container="getModalContainer"
@ok="emits('ok')"
@cancel="emits('close')"
@close="emits('close')"
:maskClosable="false"
:width="props.width"
:ok-text="props.okText"
:cancel-text="props.cancelText"
:wrapClassName="props.wrapClassName"
:closable="props.closable"
:destroy-on-close="props.destroyOnClose"
>
<template v-if="props.customFooter" #footer> <slot name="footer" /></template>
<a-spin :spinning="props.loading">
<div class="overflow-y-auto py-2 px-1 relative max-h-[83vh]" id="body">
<slot />
</div>
</a-spin>
</a-modal>
</div>
</template>
<script setup lang="ts">
//define emit
import { computed } from 'vue'
import { APP_Z_MODAL, getModalContainer } from '@/utils/zIndex'
const emits = defineEmits(['ok', 'close', 'update:visible'])
//define props
interface Prop {
visible: boolean
title?: string
okText?: string
width?: string
cancelText?: string
customFooter?: boolean
loading?: boolean
wrapClassName?: string
closable?: boolean
destroyOnClose?:boolean
zIndex?: number
}
const props = withDefaults(defineProps<Prop>(), {
visible: false,
cancelText: 'انصراف',
okText: 'ثبت',
customFooter: false,
loading: false,
wrapClassName: '',
closable: true,
zIndex: APP_Z_MODAL,
})
const localValue = computed({
get() {
return props.visible
},
set(value) {
emits('update:visible', value)
}
})
</script>
<style>
.ant-modal-content {
border-radius: 0.6rem;
padding-top: 0.5rem;
}
</style>
File diff suppressed because one or more lines are too long
+44
View File
@@ -0,0 +1,44 @@
/**
* Deduplicate async work across remounts (e.g. accidental v-if layout switches).
* Prefer fixing layout with v-show + parent-level fetch; use this in stores/shared loaders.
*/
const cache = new Map<string, unknown>()
const inflight = new Map<string, Promise<unknown>>()
export async function runOnceAsync<T>(key: string, fn: () => Promise<T>, force = false): Promise<T> {
if (!force && cache.has(key)) {
return cache.get(key) as T
}
if (!force && inflight.has(key)) {
return inflight.get(key) as Promise<T>
}
const promise = fn()
.then((result) => {
cache.set(key, result)
return result
})
.finally(() => {
if (inflight.get(key) === promise) {
inflight.delete(key)
}
})
inflight.set(key, promise)
return promise
}
export function resetOnceAsync(key: string) {
cache.delete(key)
inflight.delete(key)
}
export function resetOnceAsyncByPrefix(prefix: string) {
for (const key of [...cache.keys(), ...inflight.keys()]) {
if (key.startsWith(prefix)) {
resetOnceAsync(key)
}
}
}
+42
View File
@@ -0,0 +1,42 @@
import { ref } from 'vue'
import { getProducts } from '@/api/products'
import type { Product } from '@/models/products'
import { syncProductTradingHoursFromProducts } from '@/utils/storeHours'
/** Shared product catalog — dedupes getProducts() across wallet assets and chart offset. */
const catalogProducts = ref<Product[]>([])
let catalogPromise: Promise<Product[]> | null = null
export function getProductsCatalogRef() {
return catalogProducts
}
export function getProductsCatalog(force = false): Promise<Product[]> {
if (!force && catalogProducts.value.length > 0) {
return Promise.resolve(catalogProducts.value)
}
if (!force && catalogPromise) {
return catalogPromise
}
catalogPromise = getProducts()
.then((products) => {
// وقتی فروشگاه بسته است API گاهی [] برمی‌گرداند — کاتالوگ قبلی را پاک نکن
if (Array.isArray(products) && products.length > 0) {
catalogProducts.value = products
syncProductTradingHoursFromProducts(catalogProducts.value)
}
return catalogProducts.value
})
.finally(() => {
catalogPromise = null
})
return catalogPromise
}
export function clearProductsCatalog() {
catalogProducts.value = []
catalogPromise = null
}
+37
View File
@@ -0,0 +1,37 @@
import type { AppConfig } from './types'
type RawAppConfig = AppConfig & {
user?: Record<string, any>
}
let appConfig: RawAppConfig | null = null
export function setAppConfig(config: RawAppConfig) {
appConfig = config || {}
window.__APP_CONFIG__ = appConfig
}
export function getRawAppConfig() {
return appConfig ?? window.__APP_CONFIG__ ?? null
}
export function getTenantConfig() {
const rawConfig = getRawAppConfig()
if (rawConfig?.user && typeof rawConfig.user === 'object') {
return rawConfig.user
}
if (rawConfig && Object.keys(rawConfig).length > 0) {
return rawConfig
}
return {}
}
export function getConfig<T = any>(key: string, fallback?: T): T {
const config = getTenantConfig()
const value = config?.[key]
return (value ?? fallback ?? null) as T
}
+281
View File
@@ -0,0 +1,281 @@
import type { AppConfig } from '@/config/types'
import { resetVersionCheckCache } from '@/config/load-version'
import { clearCachedPwaManifest } from '@/modules/pwa/manifest-cache'
import { collectAuthTokensForPreserve } from '@/utils/authToken'
const CONFIG_COOKIE_NAME = 'APP_CONFIG_CACHE'
const CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL'
const VERSION_SUCCESS_URL_KEY = 'VERSION_Success_URL'
const CONFIG_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 7
const MAX_COOKIE_VALUE_LENGTH = 3500
const VERSION_STORAGE_KEY = 'version'
const VERSION_UPDATE_LOCK_KEY = 'version_update_lock'
const VERSION_LAST_RELOAD_KEY = 'version_last_reload'
type CachedConfigPayload = {
cached_at: number
config: AppConfig
}
type VersionUpdateLock = {
version: string
started_at: number
}
function getCookie(name: string) {
const cookie = document.cookie.split('; ').find((row) => row.startsWith(`${name}=`))
if (!cookie) return null
return cookie.substring(name.length + 1)
}
function setCookie(name: string, value: string, maxAgeSeconds: number) {
const secure = window.location.protocol === 'https:' ? '; Secure' : ''
document.cookie = [
`${name}=${value}`,
'Path=/',
`Max-Age=${maxAgeSeconds}`,
'SameSite=Lax',
secure
].join('; ')
}
export function readCachedAppConfig(): AppConfig | null {
try {
const rawCookie = getCookie(CONFIG_COOKIE_NAME)
if (!rawCookie) return null
const decoded = decodeURIComponent(rawCookie)
const payload = JSON.parse(decoded) as CachedConfigPayload
if (!payload?.config) return null
return payload.config
} catch (error) {
console.warn('Failed to read cached app config:', error)
clearCachedAppConfig()
return null
}
}
export function writeCachedAppConfig(config: AppConfig) {
try {
const payload: CachedConfigPayload = {
cached_at: Date.now(),
config
}
const encoded = encodeURIComponent(JSON.stringify(payload))
if (encoded.length > MAX_COOKIE_VALUE_LENGTH) {
console.warn(`App config is too large for cookie cache. Size: ${encoded.length}`)
return
}
setCookie(CONFIG_COOKIE_NAME, encoded, CONFIG_COOKIE_MAX_AGE_SECONDS)
} catch (error) {
console.warn('Failed to write app config cache:', error)
}
}
export function clearCachedAppConfig() {
document.cookie = `${CONFIG_COOKIE_NAME}=; Path=/; Max-Age=0; SameSite=Lax`
clearCachedPwaManifest()
}
export function getStoredAppVersion() {
return localStorage.getItem(VERSION_STORAGE_KEY)
}
export function getDisplayAppVersion(fallbackVersion: string) {
return getStoredAppVersion() || fallbackVersion
}
export function writeStoredAppVersion(version: string) {
localStorage.setItem(VERSION_STORAGE_KEY, String(version).trim())
}
export function getNormalizedVersion(version: unknown) {
if (version === null || version === undefined) return ''
return String(version).trim()
}
export function shouldUpdateForVersion(serverVersion: unknown) {
const normalizedServerVersion = getNormalizedVersion(serverVersion)
if (!normalizedServerVersion) return false
const storedVersion = getStoredAppVersion()
if (!storedVersion) {
writeStoredAppVersion(normalizedServerVersion)
return false
}
return storedVersion !== normalizedServerVersion
}
export function canStartVersionUpdate(serverVersion: string) {
const version = getNormalizedVersion(serverVersion)
if (!version) return false
const lastReloadVersion = sessionStorage.getItem(VERSION_LAST_RELOAD_KEY)
if (lastReloadVersion === version) {
return false
}
const rawLock = sessionStorage.getItem(VERSION_UPDATE_LOCK_KEY)
if (rawLock) {
try {
const lock = JSON.parse(rawLock) as VersionUpdateLock
const isSameVersion = lock.version === version
const isFreshLock = Date.now() - lock.started_at < 60_000
if (isSameVersion && isFreshLock) {
return false
}
} catch {
sessionStorage.removeItem(VERSION_UPDATE_LOCK_KEY)
}
}
return true
}
export function startVersionUpdateLock(serverVersion: string) {
const version = getNormalizedVersion(serverVersion)
const lock: VersionUpdateLock = {
version,
started_at: Date.now()
}
sessionStorage.setItem(VERSION_UPDATE_LOCK_KEY, JSON.stringify(lock))
}
export function finishVersionUpdateBeforeReload(serverVersion: string) {
const version = getNormalizedVersion(serverVersion)
writeStoredAppVersion(version)
sessionStorage.setItem(VERSION_LAST_RELOAD_KEY, version)
sessionStorage.removeItem(VERSION_UPDATE_LOCK_KEY)
}
export function cancelVersionUpdateLock() {
sessionStorage.removeItem(VERSION_UPDATE_LOCK_KEY)
}
function clearReadableCookiesExceptConfigAlreadyHandled() {
document.cookie.split(';').forEach((cookie) => {
const name = cookie.split('=')[0]?.trim()
if (!name) return
document.cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax`
})
}
export async function clearBrowserRuntimeCaches() {
try {
if ('serviceWorker' in navigator) {
const registrations = await navigator.serviceWorker.getRegistrations()
await Promise.all(registrations.map((registration) => registration.unregister()))
}
} catch (error) {
console.warn('Could not unregister service workers:', error)
}
try {
if (!('caches' in window)) return
const cacheKeys = await caches.keys()
await Promise.all(cacheKeys.map((key) => caches.delete(key)))
} catch (error) {
console.warn('Could not clear browser caches:', error)
}
}
function clearLocalStorageExceptToken() {
const preserved = collectAuthTokensForPreserve()
localStorage.clear()
for (const [key, value] of Object.entries(preserved)) {
localStorage.setItem(key, value)
}
}
export function clearAppStorageExceptToken() {
clearReadableCookiesExceptConfigAlreadyHandled()
clearLocalStorageExceptToken()
sessionStorage.clear()
}
export function clearAllBootstrapFetchCaches() {
localStorage.removeItem(CONFIG_SUCCESS_URL_KEY)
localStorage.removeItem(VERSION_SUCCESS_URL_KEY)
clearCachedPwaManifest()
resetVersionCheckCache()
window.__APP_CONFIG__ = undefined
}
export function hardReloadApp() {
const refreshUrl = `${window.location.origin}${window.location.pathname}?__app_refresh=${Date.now()}`
window.location.href = refreshUrl
}
export async function refreshConfigCacheForVersion(
newVersion: string,
fetchConfig: () => Promise<AppConfig>
) {
const normalizedVersion = getNormalizedVersion(newVersion)
if (!normalizedVersion) {
throw new Error('Version is empty')
}
startVersionUpdateLock(normalizedVersion)
const freshConfig = await fetchConfig()
clearCachedAppConfig()
clearAppStorageExceptToken()
clearAllBootstrapFetchCaches()
await clearBrowserRuntimeCaches()
writeCachedAppConfig(freshConfig)
finishVersionUpdateBeforeReload(normalizedVersion)
return freshConfig
}
export async function resetAppAndRefreshConfig(
fetchConfig: () => Promise<AppConfig>,
version?: string | null
) {
const normalizedVersion = getNormalizedVersion(version)
const freshConfig = await fetchConfig()
clearCachedAppConfig()
clearAppStorageExceptToken()
clearAllBootstrapFetchCaches()
await clearBrowserRuntimeCaches()
writeCachedAppConfig(freshConfig)
if (normalizedVersion) {
writeStoredAppVersion(normalizedVersion)
}
return freshConfig
}
+102
View File
@@ -0,0 +1,102 @@
import type { AppConfig } from '@/config/types'
import { readCachedAppConfig, writeCachedAppConfig } from '@/config/config-cache'
const CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL'
export const CONFIG_RETRY_ROUNDS = 5
function getProtocol() {
return window.location.protocol === 'https:' ? 'https:' : 'http:'
}
export function getConfigUrls() {
const protocol = getProtocol()
return [
`${protocol}//nerkh.gold/get_user`,
`${protocol}//tablo.talasevin.ir/get_user`,
`${protocol}//abshode-service.montgit.ir/server/get_user`,
`${protocol}//212.23.201.173:8381/get_user`,
`${protocol}//91.212.174.137:4040/server/get_user`
]
}
export function sortConfigUrls(urls: string[]) {
const lastSuccessUrl = localStorage.getItem(CONFIG_SUCCESS_URL_KEY)
return [...urls].sort((a, b) => {
if (a === lastSuccessUrl) return -1
if (b === lastSuccessUrl) return 1
return 0
})
}
function clearStaleConfigSuccessUrl(url: string) {
if (localStorage.getItem(CONFIG_SUCCESS_URL_KEY) === url) {
localStorage.removeItem(CONFIG_SUCCESS_URL_KEY)
}
}
async function fetchConfig(url: string, timeout = 3000): Promise<AppConfig> {
const controller = new AbortController()
const timeoutId = window.setTimeout(() => controller.abort(), timeout)
try {
const response = await fetch(url, {
method: 'GET',
headers: {
Accept: 'application/json'
},
cache: 'no-store',
signal: controller.signal
})
if (!response.ok) {
throw new Error(`Config request failed with status ${response.status}`)
}
const contentType = response.headers.get('content-type') ?? ''
if (!contentType.includes('application/json')) {
throw new Error('Config response is not JSON')
}
return await response.json()
} finally {
clearTimeout(timeoutId)
}
}
export async function fetchFreshAppConfig(rounds = CONFIG_RETRY_ROUNDS): Promise<AppConfig> {
const urls = sortConfigUrls(getConfigUrls())
for (let round = 1; round <= rounds; round++) {
for (let index = 0; index < urls.length; index++) {
const url = urls[index]
try {
const timeout = index === urls.length - 1 ? 7000 : 4000
const config = await fetchConfig(url, timeout)
localStorage.setItem(CONFIG_SUCCESS_URL_KEY, url)
writeCachedAppConfig(config)
return config
} catch (error) {
clearStaleConfigSuccessUrl(url)
console.warn(`Config failed from ${url}. Round: ${round}`, error)
}
}
}
throw new Error('Cannot load app config from any server')
}
export async function loadAppConfig(): Promise<AppConfig> {
const cachedConfig = readCachedAppConfig()
if (cachedConfig) {
return cachedConfig
}
return await fetchFreshAppConfig()
}
+179
View File
@@ -0,0 +1,179 @@
type VersionResponse =
| string
| number
| {
version?: string | number
data?: string | number | { version?: string | number; value?: string | number }
value?: string | number
user?: unknown
}
const VERSION_SUCCESS_URL_KEY = 'VERSION_Success_URL'
export const VERSION_RETRY_ROUNDS = 5
let versionFetchPromise: Promise<string | null> | null = null
let cachedVersionResult: string | null | undefined
function getProtocol() {
return window.location.protocol === 'https:' ? 'https:' : 'http:'
}
function getVersionUrls() {
const protocol = getProtocol()
return [
'https://nerkh.gold/get_version',
'https://tablo.talasevin.ir/get_version',
'https://abshode-service.montgit.ir/server/get_version',
`${protocol}//212.23.201.173:8381/get_version`,
`${protocol}//91.212.174.137:4040/server/get_version`
]
}
function sortVersionUrls(urls: string[]) {
const lastSuccessUrl = localStorage.getItem(VERSION_SUCCESS_URL_KEY)
if (!lastSuccessUrl) {
return urls
}
const others = urls.filter((url) => url !== lastSuccessUrl)
return [lastSuccessUrl, ...others]
}
function clearStaleVersionSuccessUrl(url: string) {
if (localStorage.getItem(VERSION_SUCCESS_URL_KEY) === url) {
localStorage.removeItem(VERSION_SUCCESS_URL_KEY)
}
}
function readUserVersion(user: unknown) {
if (!user || typeof user !== 'object' || Array.isArray(user)) {
return ''
}
const record = user as { version?: string | number; value?: string | number }
const version = record.version ?? record.value
return version === undefined || version === null ? '' : String(version).trim()
}
function normalizeVersionResponse(response: VersionResponse) {
if (typeof response === 'string' || typeof response === 'number') {
return String(response).trim()
}
if (!response) return ''
if (typeof response.data === 'string' || typeof response.data === 'number') {
return String(response.data).trim()
}
const directVersion =
response.version ??
response.value ??
(typeof response.data === 'object' ? response.data?.version ?? response.data?.value : undefined)
if (directVersion !== undefined && directVersion !== null) {
return String(directVersion).trim()
}
return readUserVersion(response.user)
}
async function fetchVersion(url: string, timeout = 3500): Promise<string | null> {
const controller = new AbortController()
const timeoutId = window.setTimeout(() => controller.abort(), timeout)
try {
const response = await fetch(url, {
method: 'GET',
headers: {
Accept: 'application/json'
},
cache: 'no-store',
signal: controller.signal
})
if (!response.ok) {
throw new Error(`Version request failed with status ${response.status}`)
}
const contentType = response.headers.get('content-type') ?? ''
const data = contentType.includes('application/json')
? ((await response.json()) as VersionResponse)
: ((await response.text()) as VersionResponse)
const version = normalizeVersionResponse(data)
if (!version) {
throw new Error('Version response is empty')
}
localStorage.setItem(VERSION_SUCCESS_URL_KEY, url)
return version
} finally {
window.clearTimeout(timeoutId)
}
}
async function fetchVersionWithRetry(rounds = VERSION_RETRY_ROUNDS): Promise<string | null> {
const urls = sortVersionUrls(getVersionUrls())
for (let round = 1; round <= rounds; round++) {
for (let index = 0; index < urls.length; index++) {
const url = urls[index]
const timeout = index === urls.length - 1 ? 6000 : 3500
try {
const version = await fetchVersion(url, timeout)
if (version) {
return version
}
} catch (error) {
clearStaleVersionSuccessUrl(url)
console.warn(`Version failed from ${url}. Round: ${round}`, error)
}
}
}
return null
}
export function resetVersionCheckCache() {
cachedVersionResult = undefined
versionFetchPromise = null
}
export async function loadRemoteAppVersion(options?: {
rounds?: number
force?: boolean
}): Promise<string | null> {
const rounds = options?.rounds ?? VERSION_RETRY_ROUNDS
const force = options?.force ?? false
if (force) {
resetVersionCheckCache()
}
if (cachedVersionResult !== undefined) {
return cachedVersionResult
}
if (versionFetchPromise) {
return versionFetchPromise
}
versionFetchPromise = fetchVersionWithRetry(rounds)
.then((version) => {
cachedVersionResult = version
return version
})
.finally(() => {
versionFetchPromise = null
})
return versionFetchPromise
}
+20
View File
@@ -0,0 +1,20 @@
export interface AppConfig {
mode?: string
theme?: string
name?: string
name_fa?: string
avatar?: string
color?: string
background_color?: string
secondary_color?: string
node_server?: string
node_ws?: string
manifest_url?: string
user?: Record<string, any>
[key: string]: any
}
+32
View File
@@ -0,0 +1,32 @@
import 'ant-design-vue/dist/antd.less'
import './assets/css/landing-entry.less'
import 'virtual:uno.css'
import '@unocss/reset/tailwind-compat.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from '@/App.vue'
import router from '@/router'
import { loadAppConfig } from '@/config/load-config'
import { setAppConfig } from '@/config/app-config'
import { applyProjectTheme } from '@/utils/config'
import { useSettingsStore } from '@/store/settingsStore'
async function bootstrapLanding() {
const config = await loadAppConfig()
setAppConfig(config)
applyProjectTheme()
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
const settingsStore = useSettingsStore()
await settingsStore.fetchSettings()
app.mount('#app')
;(window as Window & { __APP_BOOTSTRAPPED__?: boolean }).__APP_BOOTSTRAPPED__ = true
}
void bootstrapLanding()
+34
View File
@@ -0,0 +1,34 @@
/*export interface Series {
x: string
y: Array<number>
}*/
export interface Series {
}
/*
export interface Chart {
Array<Series>
}
*/
export interface PriceUp {
buy: number
sell: number
}
export type PriceStatus = 'open' | 'closed' | 'no_quote'
export interface Price {
sell: number
buy: number
price: number
price_last: number
goldir?: number
up?: PriceUp
closed?: boolean
status?: PriceStatus
time?: string
main_product?: number
montzar?: number
}
+68
View File
@@ -0,0 +1,68 @@
/*
export interface UserMobile {
mobile: string
}
export interface UserPassword {
password: string
}
export interface Token {
access_token: string
}
*/
import type {PWallet, Wallet, Wallets} from '@/models/wallet'
export interface User {
id: number;
fname: string;
lname: string;
name: string;
active: boolean;
mobile: string;
avatar_id?: number | null;
avatar_url?: string | null;
full_src?: string | null;
avatar?: { id?: number; src?: string; full_src?: string; url?: string };
file?: { id?: number; src?: string; full_src?: string; url?: string };
last_seen_at: string;
default_address_id?: any;
sts?: any;
gold_sum?: number
type: number;
total_transaction: number;
orders_count: number;
birth_date: string;
shop_name: string;
phone_number?: any;
city?: any;
province?: any;
n_code: string;
is_archive: boolean;
trade_active: number;
referral_id: string;
gram: number;
is_legal: number;
created_at: string;
updated_at: string;
access_token?: string;
deleted_at?: any;
wallet: Wallet;
pwallet:PWallet;
wallets: Wallets[]
finger_print: boolean
gold: number;
}
export interface Login {
id?: number
mobile?: string
password?: string
token?: string
provider: string
}
export interface Password{
password?: string
}
+80
View File
@@ -0,0 +1,80 @@
import type {Wallet} from "@/models/wallet";
import type { ProductFile } from '@/utils/file'
export interface Product {
id: number
img: string | undefined
title: string
amount: any
metal_id: any
file_id: any
file?: ProductFile | null
unit: string
active: boolean
show: boolean
show_in_assets?: boolean
open_sell: boolean
open_buy: boolean
group_type:string
percent_fee_buy: number | undefined
percent_fee_sell: number | undefined
cash_fee_buy: number | undefined
cash_fee_sell: number | undefined
cash_fee_buy_is_percent?: boolean
cash_fee_sell_is_percent?: boolean
order_limit_enabled?: boolean
min_order_cash?: number | null
max_order_cash?: number | null
has_physical_delivery?: boolean
min_physical_delivery_quantity?: number | null
direct_price: number | undefined
price_offset_percent?: number | null
use_last_price_if_zero?: boolean
reference_provider_active?: boolean
trade_by_quantity?: boolean
reading_method: any
created_at: any
updated_at: any
deleted_at: any
price?:number | undefined
sellPrice?:number | undefined
wallet: Wallet
auto_close_at: string
auto_open_at: string
price_profit: number
}
export interface PInventory {
id: number
title: string
amount: any
metal_id: any
file_id: any
file?: ProductFile | null
active: boolean
show: boolean
open_sell: boolean
open_buy: boolean
group_type:string
percent_fee_buy: number
percent_fee_sell: number
cash_fee_buy: number
cash_fee_sell: number
cash_fee_buy_is_percent?: boolean
cash_fee_sell_is_percent?: boolean
has_physical_delivery?: boolean
min_physical_delivery_quantity?: number | null
reading_method: any
created_at: any
updated_at: any
deleted_at: any
price?:number
unit?: string
direct_price?: number
wallet:Wallet
}
export interface CountPInventory {
count: number
product: PInventory
}
+97
View File
@@ -0,0 +1,97 @@
/*
export interface deposit {
price?: number
}
export interface endurance {
price?: number
percentWallet?: number
}*/
/*export interface Wallet {
price?: number
percentWallet?: number
}*/
import type {Product} from "@/models/products";
export interface Wallet {
owner: number,
treasure: number,
treasure_sum: number,
id: number,
gold: number,
gold_block: number,
cash: number
gold_sum: number
product: Product
cash_block: number
count_cash: number
count_cash_wallet: number
count_cash_wallets: number
count_gold: number
count_gold_wallet: number
count_gold_wallets: number
count_silver: number
count_copper: number
count_product: number
count_cash_total: number
/** boursecu: موجودی دارایی کاتد مس (کیلوگرم) — API field: count_katod_mes */
count_katod_mes?: number
/** boursecu: موجودی دارایی مفتول مس (کیلوگرم) */
count_maftol_mes?: number
}
export interface Wallets {
cash : number
created_at : string
fund_id : null | number
gold : number
hash : string
id : number
number : number
owner : number
owner_type : number
product : Product | null
product_id : null | number
updated_at : string
}
export type UserWalletLine = Wallets
export interface PWallet {
id: number
owner_type: number
created_at: string
updated_at: string
owner: number
cash: number
hash: string
gold: number
fund_id: any
product_id: number
number: number
product: Product
}
export interface Test {
price?: number,
percentWallet: number
}
export interface WithdrawConfirm {
account_number?: string
bank_name?: string
birth_date?: string
cart_number?: string
shaba_number?: string
}
export interface Card {
id?: number
birth_date: number
cart_number?: string
shaba_number?: string
bank_name?: string
account_number?: string
sts?: number
sts_str?: string
}
+72
View File
@@ -0,0 +1,72 @@
export type PwaManifestIcon = {
src: string
sizes?: string
type?: string
purpose?: string
}
export type PwaManifest = {
id?: string
name?: string
short_name?: string
lang?: string
dir?: string
start_url?: string
scope?: string
display?: string
background_color?: string
theme_color?: string
description?: string
icons?: PwaManifestIcon[]
}
const MANIFEST_CACHE_KEY = 'PWA_MANIFEST_CACHE_V5'
const MANIFEST_CACHE_MAX_AGE_MS = 60 * 60 * 24 * 7
export type CachedPwaManifest = {
cached_at: number
user_id: string | number
config_base: string
manifest_url: string
manifest: PwaManifest
}
export function readCachedPwaManifest(
userId: string | number | null | undefined,
configBase: string
): CachedPwaManifest | null {
if (userId == null || userId === '' || !configBase) return null
try {
const raw = localStorage.getItem(MANIFEST_CACHE_KEY)
if (!raw) return null
const payload = JSON.parse(raw) as CachedPwaManifest
if (!payload?.manifest || !payload.manifest_url) return null
if (String(payload.user_id) !== String(userId)) return null
if (payload.config_base !== configBase) return null
if (Date.now() - Number(payload.cached_at || 0) > MANIFEST_CACHE_MAX_AGE_MS) {
localStorage.removeItem(MANIFEST_CACHE_KEY)
return null
}
return payload
} catch {
localStorage.removeItem(MANIFEST_CACHE_KEY)
return null
}
}
export function writeCachedPwaManifest(entry: CachedPwaManifest) {
try {
localStorage.setItem(MANIFEST_CACHE_KEY, JSON.stringify(entry))
} catch (error) {
console.warn('Failed to cache PWA manifest:', error)
}
}
export function clearCachedPwaManifest() {
localStorage.removeItem(MANIFEST_CACHE_KEY)
}
+30
View File
@@ -0,0 +1,30 @@
import {createRouter, createWebHistory} from 'vue-router'
import { getLoginAppUrl, getLoginInstallPwaUrl } from '@/utils/config'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'landing',
component: () => import('@/views/base/aboutUs/index.vue')
},
{
path: '/aboutUs',
name: 'aboutUs',
redirect: { name: 'landing' },
},
{
path: '/login',
name: 'login',
beforeEnter(to) {
window.location.href =
to.query.installPwa === 'true' ? getLoginInstallPwaUrl() : getLoginAppUrl()
return false
},
component: { render: () => null },
},
]
})
export default router
+16
View File
@@ -0,0 +1,16 @@
import {ref} from 'vue'
import {defineStore} from 'pinia'
export const useConfigStore = defineStore('config', () => {
const config = ref(window.__APP_CONFIG__?.user)
async function getConfigData() {
config.value = window.__APP_CONFIG__?.user
console.log(config.value)
}
return {
config,
getConfigData
}
})
+99
View File
@@ -0,0 +1,99 @@
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
import { getSettings } from '@/api/configuration'
import { resolveMeltedGoldLabel, resolveMetalTypeLabel } from '@/utils/metalLabels'
import { resolveTradeWeightUnit } from '@/utils/weightUnit'
import { runOnceAsync } from '@/composables/useOnceAsync'
export interface AppSetting {
id?: number
key?: string | number
title_fa?: string
value?: string | number
description?: string
is_active?: boolean
type?: string
}
export const useSettingsStore = defineStore('settings', () => {
const settings = ref<AppSetting[]>([])
const loaded = ref(false)
function getSettingByKey(key: string) {
const item = settings.value.find((setting) => setting?.key === key)
return item?.value ?? null
}
function isSettingTruthy(key: string) {
const value = getSettingByKey(key)
if (value == null) {
return false
}
if (value === true) {
return true
}
const normalized = String(value).trim().toLowerCase()
return normalized === 'true' || normalized === '1' || normalized === 'yes'
}
function mergeSettings(items?: AppSetting[] | null) {
if (!items?.length) {
return
}
const merged = [...settings.value]
items.forEach((item) => {
if (!item?.key) {
return
}
const index = merged.findIndex((setting) => setting.key === item.key)
if (index >= 0) {
merged[index] = { ...merged[index], ...item }
} else {
merged.push(item)
}
})
settings.value = merged
}
async function fetchSettings(force = false) {
return runOnceAsync('settings:all', async () => {
try {
const data = await getSettings()
if (Array.isArray(data)) {
mergeSettings(data)
}
} catch (e) {
console.log(e)
} finally {
loaded.value = true
}
return settings.value
}, force)
}
const metalTypeSettingValue = computed(() => getSettingByKey('metal_type'))
const metalTypeLabel = computed(() => resolveMetalTypeLabel(metalTypeSettingValue.value))
const meltedGoldLabel = computed(() => resolveMeltedGoldLabel(metalTypeSettingValue.value))
const tradeWeightUnit = computed(() => resolveTradeWeightUnit(getSettingByKey('unit')))
const isReferralRequired = computed(() => {
const value = getSettingByKey('referral_id')
return String(value ?? '').trim().toLowerCase() === 'required'
})
return {
settings,
loaded,
fetchSettings,
mergeSettings,
getSettingByKey,
isSettingTruthy,
isReferralRequired,
metalTypeSettingValue,
metalTypeLabel,
meltedGoldLabel,
tradeWeightUnit,
}
})
+51
View File
@@ -0,0 +1,51 @@
import colors from './baseColor.json'
import { getConfigApp } from '@/utils/config'
const prefixCls = 'noname'
const baseButtonIconSize = '24'
function readCachedTenantField(key: string): string {
try {
const cookie = document.cookie.split('; ').find((row) => row.startsWith('APP_CONFIG_CACHE='))
if (!cookie) return ''
const payload = JSON.parse(decodeURIComponent(cookie.slice('APP_CONFIG_CACHE='.length)))
const user = payload?.config?.user
const value = user?.[key]
return value != null ? String(value) : ''
} catch {
return ''
}
}
export function resolveProjectBase(): string {
return getConfigApp('node_server') || readCachedTenantField('node_server') || ''
}
/** fallback خالی — useAxios در هر درخواست node_server را از config می‌گیرد */
export const baseURL = ''
export function getAvatarUploadURL(): string {
const base = resolveProjectBase().replace(/\/$/, '')
return base ? `${base}/customer/avatar` : '/customer/avatar'
}
/** کلید ثابت — وقتی config پاک می‌شود (به‌روزرسانی نسخه) توکن گم نشود */
export const SESSION_AUTH_KEY = 'customer_session_token'
export const LEGACY_TOKEN_SUFFIX = '_app_token'
export function getTokenName(): string {
const name = readCachedTenantField('name') || getConfigApp('name') || 'app'
return `${name}${LEGACY_TOKEN_SUFFIX}`
}
/** @deprecated از getTokenName() استفاده کن */
export const tokenName = 'app_app_token'
export {
prefixCls,
colors,
baseButtonIconSize,
}
+72
View File
@@ -0,0 +1,72 @@
import {
getTokenName,
LEGACY_TOKEN_SUFFIX,
SESSION_AUTH_KEY,
} from '@/themeConfig'
function legacyTokenKeys(): string[] {
const keys: string[] = []
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if (key?.endsWith(LEGACY_TOKEN_SUFFIX)) {
keys.push(key)
}
}
return keys
}
/** خواندن توکن — اول session ثابت، بعد کلید tenant، بعد هر *_app_token */
export function findStoredAuthToken(): string | null {
const session = localStorage.getItem(SESSION_AUTH_KEY)
if (session) {
return session
}
const named = localStorage.getItem(getTokenName())
if (named) {
localStorage.setItem(SESSION_AUTH_KEY, named)
return named
}
for (const key of legacyTokenKeys()) {
const value = localStorage.getItem(key)
if (value) {
localStorage.setItem(SESSION_AUTH_KEY, value)
return value
}
}
return null
}
export function persistAuthToken(token: string | undefined | null) {
if (!token) {
return
}
localStorage.setItem(SESSION_AUTH_KEY, token)
localStorage.setItem(getTokenName(), token)
}
export function clearStoredAuthTokens() {
localStorage.removeItem(SESSION_AUTH_KEY)
for (const key of legacyTokenKeys()) {
localStorage.removeItem(key)
}
}
export function collectAuthTokensForPreserve(): Record<string, string> {
const preserved: Record<string, string> = {}
for (const key of [SESSION_AUTH_KEY, getTokenName(), ...legacyTokenKeys()]) {
const value = localStorage.getItem(key)
if (value) {
preserved[key] = value
}
}
return preserved
}

Some files were not shown because too many files have changed in this diff Show More