create landing page for boursemes
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
import useAxios from "@/utils/useAxios";
|
||||
|
||||
export async function getSettings() {
|
||||
return await useAxios.get(`api/get_settings`)
|
||||
}
|
||||
@@ -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')
|
||||
@@ -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}`)
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 607 B |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 64 KiB |
@@ -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 |
|
After Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 508 KiB |
|
After Width: | Height: | Size: 37 KiB |
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||