refactor code to nuxt and add ssr and edit some text in landing
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -8,11 +8,19 @@ let appConfig: RawAppConfig | null = null
|
||||
|
||||
export function setAppConfig(config: RawAppConfig) {
|
||||
appConfig = config || {}
|
||||
window.__APP_CONFIG__ = appConfig
|
||||
if (import.meta.client) {
|
||||
window.__APP_CONFIG__ = appConfig
|
||||
}
|
||||
}
|
||||
|
||||
export function getRawAppConfig() {
|
||||
return appConfig ?? window.__APP_CONFIG__ ?? null
|
||||
if (appConfig) {
|
||||
return appConfig
|
||||
}
|
||||
if (import.meta.client) {
|
||||
return window.__APP_CONFIG__ ?? null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function getTenantConfig() {
|
||||
|
||||
@@ -24,6 +24,7 @@ type VersionUpdateLock = {
|
||||
}
|
||||
|
||||
function getCookie(name: string) {
|
||||
if (!import.meta.client) return null
|
||||
const cookie = document.cookie.split('; ').find((row) => row.startsWith(`${name}=`))
|
||||
|
||||
if (!cookie) return null
|
||||
@@ -32,6 +33,7 @@ function getCookie(name: string) {
|
||||
}
|
||||
|
||||
function setCookie(name: string, value: string, maxAgeSeconds: number) {
|
||||
if (!import.meta.client) return
|
||||
const secure = window.location.protocol === 'https:' ? '; Secure' : ''
|
||||
|
||||
document.cookie = [
|
||||
@@ -44,6 +46,7 @@ function setCookie(name: string, value: string, maxAgeSeconds: number) {
|
||||
}
|
||||
|
||||
export function readCachedAppConfig(): AppConfig | null {
|
||||
if (!import.meta.client) return null
|
||||
try {
|
||||
const rawCookie = getCookie(CONFIG_COOKIE_NAME)
|
||||
|
||||
@@ -63,6 +66,7 @@ export function readCachedAppConfig(): AppConfig | null {
|
||||
}
|
||||
|
||||
export function writeCachedAppConfig(config: AppConfig) {
|
||||
if (!import.meta.client) return
|
||||
try {
|
||||
const payload: CachedConfigPayload = {
|
||||
cached_at: Date.now(),
|
||||
@@ -83,6 +87,7 @@ export function writeCachedAppConfig(config: AppConfig) {
|
||||
}
|
||||
|
||||
export function clearCachedAppConfig() {
|
||||
if (!import.meta.client) return
|
||||
document.cookie = `${CONFIG_COOKIE_NAME}=; Path=/; Max-Age=0; SameSite=Lax`
|
||||
clearCachedPwaManifest()
|
||||
}
|
||||
|
||||
+21
-10
@@ -5,6 +5,9 @@ const CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL'
|
||||
export const CONFIG_RETRY_ROUNDS = 5
|
||||
|
||||
function getProtocol() {
|
||||
if (import.meta.server) {
|
||||
return 'https:'
|
||||
}
|
||||
return window.location.protocol === 'https:' ? 'https:' : 'http:'
|
||||
}
|
||||
|
||||
@@ -16,11 +19,15 @@ export function getConfigUrls() {
|
||||
`${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`
|
||||
`${protocol}//91.212.174.137:4040/server/get_user`,
|
||||
]
|
||||
}
|
||||
|
||||
export function sortConfigUrls(urls: string[]) {
|
||||
if (!import.meta.client) {
|
||||
return [...urls]
|
||||
}
|
||||
|
||||
const lastSuccessUrl = localStorage.getItem(CONFIG_SUCCESS_URL_KEY)
|
||||
|
||||
return [...urls].sort((a, b) => {
|
||||
@@ -31,6 +38,7 @@ export function sortConfigUrls(urls: string[]) {
|
||||
}
|
||||
|
||||
function clearStaleConfigSuccessUrl(url: string) {
|
||||
if (!import.meta.client) return
|
||||
if (localStorage.getItem(CONFIG_SUCCESS_URL_KEY) === url) {
|
||||
localStorage.removeItem(CONFIG_SUCCESS_URL_KEY)
|
||||
}
|
||||
@@ -38,16 +46,16 @@ function clearStaleConfigSuccessUrl(url: string) {
|
||||
|
||||
async function fetchConfig(url: string, timeout = 3000): Promise<AppConfig> {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = window.setTimeout(() => controller.abort(), timeout)
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
Accept: 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
signal: controller.signal
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -77,8 +85,10 @@ export async function fetchFreshAppConfig(rounds = CONFIG_RETRY_ROUNDS): Promise
|
||||
const timeout = index === urls.length - 1 ? 7000 : 4000
|
||||
const config = await fetchConfig(url, timeout)
|
||||
|
||||
localStorage.setItem(CONFIG_SUCCESS_URL_KEY, url)
|
||||
writeCachedAppConfig(config)
|
||||
if (import.meta.client) {
|
||||
localStorage.setItem(CONFIG_SUCCESS_URL_KEY, url)
|
||||
writeCachedAppConfig(config)
|
||||
}
|
||||
|
||||
return config
|
||||
} catch (error) {
|
||||
@@ -92,10 +102,11 @@ export async function fetchFreshAppConfig(rounds = CONFIG_RETRY_ROUNDS): Promise
|
||||
}
|
||||
|
||||
export async function loadAppConfig(): Promise<AppConfig> {
|
||||
const cachedConfig = readCachedAppConfig()
|
||||
|
||||
if (cachedConfig) {
|
||||
return cachedConfig
|
||||
if (import.meta.client) {
|
||||
const cachedConfig = readCachedAppConfig()
|
||||
if (cachedConfig) {
|
||||
return cachedConfig
|
||||
}
|
||||
}
|
||||
|
||||
return await fetchFreshAppConfig()
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div class="app-shell">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
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'
|
||||
import { initPwaInstallCapture, startPwaManifest } from '@/modules/pwa/manifest'
|
||||
|
||||
/** قبل از bootstrap — تا beforeinstallprompt از دست نرود */
|
||||
initPwaInstallCapture()
|
||||
|
||||
async function bootstrapLanding() {
|
||||
const config = await loadAppConfig()
|
||||
setAppConfig(config)
|
||||
startPwaManifest(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,11 @@
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({
|
||||
layout: false,
|
||||
})
|
||||
|
||||
await navigateTo({ path: '/' }, { redirectCode: 301 })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<div class="about-us-page-root">
|
||||
<DesktopAboutUsView />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import DesktopAboutUsView from '@/views/base/aboutUs/desktop/DesktopAboutUsView.vue'
|
||||
import { useAboutUsPage } from '@/views/base/aboutUs/composables/useAboutUsPage'
|
||||
|
||||
useAboutUsPage()
|
||||
|
||||
const siteName = 'بورسمس'
|
||||
const title = 'خرید آنلاین مس، مستقیم و شفاف | بورسمس'
|
||||
const description =
|
||||
'بورسمس مسیر خرید و فروش آنلاین مس را ساده، سریع و قابل اعتماد کرده است؛ با پشتوانه تولیدکننده واقعی مس'
|
||||
|
||||
useSeoMeta({
|
||||
title,
|
||||
description,
|
||||
ogTitle: title,
|
||||
ogDescription: description,
|
||||
ogType: 'website',
|
||||
ogSiteName: siteName,
|
||||
twitterCard: 'summary_large_image',
|
||||
twitterTitle: title,
|
||||
twitterDescription: description,
|
||||
robots: 'index, follow',
|
||||
})
|
||||
|
||||
useHead({
|
||||
title,
|
||||
htmlAttrs: {
|
||||
lang: 'fa',
|
||||
dir: 'rtl',
|
||||
class: 'font-dana project-boursecu',
|
||||
},
|
||||
style: [
|
||||
{
|
||||
children: `
|
||||
:root {
|
||||
--App-font-family: 'dana', sans-serif;
|
||||
--App-brand-nav: #133050;
|
||||
--App-brand-red: #bc3c3b;
|
||||
--App-brand-red-50: #f8ecec;
|
||||
--App-brand-red-100: #f0d8d8;
|
||||
--App-brand-red-700: #963130;
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
script: [
|
||||
{
|
||||
type: 'application/ld+json',
|
||||
innerHTML: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
name: siteName,
|
||||
url: 'https://boursemes.com/landing',
|
||||
description,
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: 'application/ld+json',
|
||||
innerHTML: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
name: title,
|
||||
description,
|
||||
inLanguage: 'fa-IR',
|
||||
url: 'https://boursemes.com/landing',
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: 'application/ld+json',
|
||||
innerHTML: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: [
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'چگونه خرید انجام میشود؟',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'پس از ثبتنام و ورود، مقدار مفتول مس مورد نظر را انتخاب کرده و سفارش خود را ثبت میکنید. پرداخت بهصورت امن و آنلاین انجام میشود و سفارش شما در پنل کاربری قابل پیگیری است.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'حداقل میزان خرید چقدر است؟',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'حداقل میزان خرید از بورس مس یک کیلوگرم است و میتوانید متناسب با نیاز خود، حجم سفارش را افزایش دهید.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'قیمتها چگونه محاسبه میشوند؟',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'قیمتها بهصورت لحظهای بر اساس عرضه و تقاضای بازار بهروزرسانی میشوند و در زمان ثبت سفارش، قیمت نهایی همان قیمت نمایشدادهشده است.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'ارسال چگونه انجام میشود؟',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'پس از ثبت سفارش، امکان تحویل فیزیکی در زمان و مکان مورد نظر شما فراهم است؛ حتی برای تناژهای بالا.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'آیا پرداخت امن است؟',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'بله. تمام پرداختها از طریق درگاههای امن بانکی انجام میشود و اطلاعات مالی شما با استانداردهای امنیتی محافظت میگردد.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Question',
|
||||
name: 'چگونه فروشنده شوم؟',
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: 'پس از تکمیل ثبتنام و احراز هویت، میتوانید از پنل کاربری درخواست فروش ثبت کنید و مس خود را در بستر بورس مس عرضه نمایید.',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.about-us-page-root {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f4f5f7;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
html:not(.app-layout--desktop) .app-main > .about-us-page-root {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
html.dlanding-menu-open .about-us-page-root {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
import { getLoginAppUrl, getLoginInstallPwaUrl } from '@/utils/config'
|
||||
|
||||
definePageMeta({
|
||||
layout: false,
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const target =
|
||||
route.query.installPwa === 'true' ? getLoginInstallPwaUrl() : getLoginAppUrl()
|
||||
|
||||
await navigateTo(target, { external: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
import { defineNuxtPlugin } from '#app'
|
||||
|
||||
/**
|
||||
* بعضی وابستگیها (مثل ant-design) موقع SSR به window.navigator میزنند.
|
||||
* قبل از رندر، حداقل window/navigator را روی globalThis شبیهسازی میکنیم.
|
||||
*/
|
||||
export default defineNuxtPlugin(() => {
|
||||
const g = globalThis as typeof globalThis & {
|
||||
navigator?: Navigator
|
||||
window?: Window & typeof globalThis
|
||||
document?: Document
|
||||
}
|
||||
|
||||
if (!g.navigator) {
|
||||
g.navigator = {
|
||||
userAgent: 'NuxtSSR',
|
||||
language: 'fa-IR',
|
||||
languages: ['fa-IR', 'fa'],
|
||||
platform: 'SSR',
|
||||
maxTouchPoints: 0,
|
||||
} as Navigator
|
||||
}
|
||||
|
||||
if (!g.window) {
|
||||
g.window = g as unknown as Window & typeof globalThis
|
||||
}
|
||||
|
||||
if (g.window && !g.window.navigator) {
|
||||
;(g.window as Window).navigator = g.navigator
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineNuxtPlugin } from '#app'
|
||||
import { setAppConfig } from '@/config/app-config'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
|
||||
/**
|
||||
* روی سرور config شبکه را بلاک نمیکنیم تا SSR متنها سریع و پایدار باشد.
|
||||
* برند برای HTML اولیه از fallback ثابت میآید؛ config کامل روی کلاینت لود میشود.
|
||||
*/
|
||||
export default defineNuxtPlugin(() => {
|
||||
setAppConfig({
|
||||
user: {
|
||||
name: 'boursemes',
|
||||
name_fa: 'بورسمس',
|
||||
},
|
||||
} as any)
|
||||
|
||||
const configStore = useConfigStore()
|
||||
configStore.hydrateFromAppConfig()
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import { defineNuxtPlugin } from '#app'
|
||||
import { loadAppConfig } from '@/config/load-config'
|
||||
import { setAppConfig } from '@/config/app-config'
|
||||
import { applyProjectTheme } from '@/utils/config'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { initPwaInstallCapture, startPwaManifest } from '@/modules/pwa/manifest'
|
||||
|
||||
/** قبل از هر چیز — تا beforeinstallprompt از دست نرود */
|
||||
initPwaInstallCapture()
|
||||
|
||||
/**
|
||||
* Client bootstrap: تم، PWA، settings — بعد از hydrate.
|
||||
*/
|
||||
export default defineNuxtPlugin(async () => {
|
||||
try {
|
||||
const config = await loadAppConfig()
|
||||
setAppConfig(config)
|
||||
startPwaManifest(config)
|
||||
applyProjectTheme()
|
||||
|
||||
const configStore = useConfigStore()
|
||||
configStore.hydrateFromAppConfig()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
await settingsStore.fetchSettings()
|
||||
} catch (error) {
|
||||
console.warn('[client] landing bootstrap failed', error)
|
||||
applyProjectTheme()
|
||||
}
|
||||
|
||||
;(window as Window & { __APP_BOOTSTRAPPED__?: boolean }).__APP_BOOTSTRAPPED__ = true
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineNuxtPlugin } from '#app'
|
||||
import { Button, Modal, Spin } from 'ant-design-vue'
|
||||
import 'ant-design-vue/dist/antd.less'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.vueApp.use(Button)
|
||||
nuxtApp.vueApp.use(Modal)
|
||||
nuxtApp.vueApp.use(Spin)
|
||||
})
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://boursemes.com/landing/sitemap.xml
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://boursemes.com/landing</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
@@ -1,30 +0,0 @@
|
||||
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
|
||||
+17
-11
@@ -1,16 +1,22 @@
|
||||
import {ref} from 'vue'
|
||||
import {defineStore} from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { getTenantConfig } from '@/config/app-config'
|
||||
|
||||
export const useConfigStore = defineStore('config', () => {
|
||||
const config = ref(window.__APP_CONFIG__?.user)
|
||||
const config = ref<Record<string, any> | null>(null)
|
||||
|
||||
async function getConfigData() {
|
||||
config.value = window.__APP_CONFIG__?.user
|
||||
console.log(config.value)
|
||||
}
|
||||
function hydrateFromAppConfig() {
|
||||
const tenant = getTenantConfig()
|
||||
config.value = tenant && Object.keys(tenant).length > 0 ? tenant : null
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
getConfigData
|
||||
}
|
||||
async function getConfigData() {
|
||||
hydrateFromAppConfig()
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
hydrateFromAppConfig,
|
||||
getConfigData,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ const prefixCls = 'noname'
|
||||
const baseButtonIconSize = '24'
|
||||
|
||||
function readCachedTenantField(key: string): string {
|
||||
if (!import.meta.client) return ''
|
||||
try {
|
||||
const cookie = document.cookie.split('; ').find((row) => row.startsWith('APP_CONFIG_CACHE='))
|
||||
if (!cookie) return ''
|
||||
|
||||
+17
-1
@@ -5,6 +5,10 @@ import {
|
||||
} from '@/themeConfig'
|
||||
|
||||
function legacyTokenKeys(): string[] {
|
||||
if (!import.meta.client) {
|
||||
return []
|
||||
}
|
||||
|
||||
const keys: string[] = []
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
@@ -19,6 +23,10 @@ function legacyTokenKeys(): string[] {
|
||||
|
||||
/** خواندن توکن — اول session ثابت، بعد کلید tenant، بعد هر *_app_token */
|
||||
export function findStoredAuthToken(): string | null {
|
||||
if (!import.meta.client) {
|
||||
return null
|
||||
}
|
||||
|
||||
const session = localStorage.getItem(SESSION_AUTH_KEY)
|
||||
if (session) {
|
||||
return session
|
||||
@@ -42,7 +50,7 @@ export function findStoredAuthToken(): string | null {
|
||||
}
|
||||
|
||||
export function persistAuthToken(token: string | undefined | null) {
|
||||
if (!token) {
|
||||
if (!import.meta.client || !token) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -51,6 +59,10 @@ export function persistAuthToken(token: string | undefined | null) {
|
||||
}
|
||||
|
||||
export function clearStoredAuthTokens() {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.removeItem(SESSION_AUTH_KEY)
|
||||
|
||||
for (const key of legacyTokenKeys()) {
|
||||
@@ -59,6 +71,10 @@ export function clearStoredAuthTokens() {
|
||||
}
|
||||
|
||||
export function collectAuthTokensForPreserve(): Record<string, string> {
|
||||
if (!import.meta.client) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const preserved: Record<string, string> = {}
|
||||
|
||||
for (const key of [SESSION_AUTH_KEY, getTokenName(), ...legacyTokenKeys()]) {
|
||||
|
||||
+20
-8
@@ -10,19 +10,24 @@ import boursecuAssetsIcon from '@/assets/img/boursecu-assets-icon.png'
|
||||
import boursecuHomeIcon from '@/assets/img/boursecu-home-icon.png'
|
||||
import boursecuSettingsIcon from '@/assets/img/boursecu-settings-icon.png'
|
||||
|
||||
export function getConfigApp(key: string) {
|
||||
const config = window.__APP_CONFIG__ ? window.__APP_CONFIG__.user : {}
|
||||
import { getConfig } from '@/config/app-config'
|
||||
|
||||
if (config) {
|
||||
return config[key]
|
||||
}
|
||||
return null
|
||||
export function getConfigApp(key: string) {
|
||||
return getConfig(key)
|
||||
}
|
||||
|
||||
/** Login URL در اپ اصلی */
|
||||
export function getLoginAppUrl() {
|
||||
const configured = import.meta.env.VITE_LOGIN_APP_URL
|
||||
const fallback = 'https://app.boursemes.com/home'
|
||||
let configured: unknown
|
||||
|
||||
try {
|
||||
configured = useRuntimeConfig().public.loginAppUrl
|
||||
} catch {
|
||||
configured =
|
||||
(import.meta as ImportMeta & { env?: Record<string, string> }).env?.NUXT_PUBLIC_LOGIN_APP_URL ||
|
||||
(import.meta as ImportMeta & { env?: Record<string, string> }).env?.VITE_LOGIN_APP_URL
|
||||
}
|
||||
|
||||
if (typeof configured !== 'string' || !configured.trim()) {
|
||||
return fallback
|
||||
@@ -34,7 +39,10 @@ export function getLoginAppUrl() {
|
||||
/** صفحه Login اپ اصلی با فلگ نصب PWA */
|
||||
export function getLoginInstallPwaUrl() {
|
||||
try {
|
||||
const origin = new URL(getLoginAppUrl(), window.location.origin).origin
|
||||
const loginUrl = getLoginAppUrl()
|
||||
const origin = import.meta.client
|
||||
? new URL(loginUrl, window.location.origin).origin
|
||||
: new URL(loginUrl).origin
|
||||
return `${origin}/login?installPwa=true`
|
||||
} catch {
|
||||
return 'https://app.boursemes.com/login?installPwa=true'
|
||||
@@ -202,6 +210,7 @@ export function getProjectSplashBackground() {
|
||||
}
|
||||
|
||||
export function resetAppSurfaceBackground() {
|
||||
if (!import.meta.client) return
|
||||
const background = '#ffffff'
|
||||
document.documentElement.style.backgroundColor = background
|
||||
document.body.style.backgroundColor = background
|
||||
@@ -217,11 +226,13 @@ export function getProjectFontFamily() {
|
||||
}
|
||||
|
||||
export function applyProjectFont() {
|
||||
if (!import.meta.client) return
|
||||
document.documentElement.style.setProperty('--App-font-family', getProjectFontFamily())
|
||||
document.documentElement.classList.add('font-dana')
|
||||
}
|
||||
|
||||
export function isTWA() {
|
||||
if (!import.meta.client) return false
|
||||
if (sessionStorage.getItem('appMode') === 'twa') return true
|
||||
if (document.referrer.startsWith('android-app://')) {
|
||||
sessionStorage.setItem('appMode', 'twa')
|
||||
@@ -231,6 +242,7 @@ export function isTWA() {
|
||||
}
|
||||
|
||||
export function applyProjectTheme() {
|
||||
if (!import.meta.client) return
|
||||
applyProjectFont()
|
||||
document.documentElement.classList.add('project-boursecu')
|
||||
document.documentElement.style.setProperty('--App-brand-nav', BOURSECU_BRAND_NAV)
|
||||
|
||||
+19
-20
@@ -5,13 +5,11 @@ import {
|
||||
} from '@/utils/authToken'
|
||||
|
||||
import { ref } from 'vue'
|
||||
import {getConfigApp} from "@/utils/config";
|
||||
import { getConfigApp } from '@/utils/config'
|
||||
|
||||
export const loading = ref<boolean>(false)
|
||||
export const loadingMore = ref<boolean>(false)
|
||||
const userInfo = ref()
|
||||
const nameProject= getConfigApp('name')
|
||||
|
||||
|
||||
function setToken(token: string | undefined) {
|
||||
if (!token) {
|
||||
@@ -21,7 +19,8 @@ function setToken(token: string | undefined) {
|
||||
}
|
||||
|
||||
function setFingerPrint(val: string | undefined) {
|
||||
return localStorage.setItem('finger', val)
|
||||
if (!import.meta.client) return
|
||||
return localStorage.setItem('finger', val as string)
|
||||
}
|
||||
|
||||
function getToken() {
|
||||
@@ -29,11 +28,14 @@ function getToken() {
|
||||
}
|
||||
|
||||
function getIsActiveFingerprint() {
|
||||
if (!import.meta.client) return false
|
||||
return localStorage.getItem('finger') === 'true'
|
||||
}
|
||||
|
||||
export async function authenticateWithFingerprint() {
|
||||
if (!import.meta.client) return false
|
||||
|
||||
const nameProject = getConfigApp('name') || 'بورسمس'
|
||||
|
||||
const challenge = new Uint8Array(32)
|
||||
window.crypto.getRandomValues(challenge)
|
||||
@@ -42,26 +44,23 @@ export async function authenticateWithFingerprint() {
|
||||
challenge,
|
||||
rp: { name: nameProject },
|
||||
user: {
|
||||
id: Uint8Array.from('user-id', c => c.charCodeAt(0)),
|
||||
id: Uint8Array.from('user-id', (c) => c.charCodeAt(0)),
|
||||
name: nameProject,
|
||||
displayName: nameProject
|
||||
displayName: nameProject,
|
||||
},
|
||||
pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
|
||||
authenticatorSelection: {
|
||||
authenticatorAttachment: 'platform',
|
||||
userVerification: 'required'
|
||||
userVerification: 'required',
|
||||
},
|
||||
timeout: 60000,
|
||||
attestation: 'direct'
|
||||
attestation: 'direct',
|
||||
}
|
||||
|
||||
try {
|
||||
const credential = await navigator.credentials.create({ publicKey: createOptions as CredentialCreationOptions })
|
||||
await navigator.credentials.create({ publicKey: createOptions as CredentialCreationOptions })
|
||||
console.log('Credential با موفقیت ایجاد شد!')
|
||||
|
||||
const credentialId = btoa(String.fromCharCode(...new Uint8Array((credential as PublicKeyCredential).rawId)))
|
||||
|
||||
// بازیابی توکن از localStorage
|
||||
const token = findStoredAuthToken()
|
||||
|
||||
if (!token) {
|
||||
@@ -69,29 +68,28 @@ export async function authenticateWithFingerprint() {
|
||||
return false
|
||||
}
|
||||
|
||||
// Success, proceed with login using token
|
||||
// Perform your login action here
|
||||
return true
|
||||
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.name === 'NotAllowedError'
|
||||
? 'احراز هویت لغو شد یا دسترسی مجاز نبود.'
|
||||
: 'عملیات ناموفق بود. لطفاً دوباره امتحان کنید.'
|
||||
const errorMessage =
|
||||
error.name === 'NotAllowedError'
|
||||
? 'احراز هویت لغو شد یا دسترسی مجاز نبود.'
|
||||
: 'عملیات ناموفق بود. لطفاً دوباره امتحان کنید.'
|
||||
console.error(errorMessage, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function removeToken() {
|
||||
return clearStoredAuthTokens()
|
||||
}
|
||||
|
||||
function removeFingerPrint() {
|
||||
if (!import.meta.client) return
|
||||
return localStorage.removeItem('finger')
|
||||
}
|
||||
|
||||
function URLUserType() {
|
||||
if (!import.meta.client) return 'customer'
|
||||
if (location.host.split('.')[0] === 'app') return 'single'
|
||||
else return 'customer'
|
||||
}
|
||||
@@ -101,6 +99,7 @@ function toFixedDownThreeChar(num: number) {
|
||||
}
|
||||
|
||||
function isIphone() {
|
||||
if (!import.meta.client) return false
|
||||
const userAgent = navigator.userAgent.toLowerCase()
|
||||
|
||||
return /iPhone/.test(userAgent)
|
||||
@@ -115,5 +114,5 @@ export {
|
||||
isIphone,
|
||||
removeFingerPrint,
|
||||
getIsActiveFingerprint,
|
||||
setFingerPrint
|
||||
setFingerPrint,
|
||||
}
|
||||
|
||||
+18
-10
@@ -82,17 +82,25 @@ export function syncProductTradingHoursFromProducts(
|
||||
}
|
||||
|
||||
export function getStoreTradingHours(): StoreTradingHours {
|
||||
const settings = useSettingsStore()
|
||||
const closeRaw = settings.getSettingByKey('close_price')
|
||||
const closePriceEnabled =
|
||||
closeRaw === true ||
|
||||
closeRaw === 1 ||
|
||||
String(closeRaw ?? 'false').trim().toLowerCase() === 'true'
|
||||
try {
|
||||
const settings = useSettingsStore()
|
||||
const closeRaw = settings.getSettingByKey('close_price')
|
||||
const closePriceEnabled =
|
||||
closeRaw === true ||
|
||||
closeRaw === 1 ||
|
||||
String(closeRaw ?? 'false').trim().toLowerCase() === 'true'
|
||||
|
||||
return {
|
||||
start: normalizeTradingTimeSetting(settings.getSettingByKey('startDay'), '00:00'),
|
||||
end: normalizeTradingTimeSetting(settings.getSettingByKey('endDay'), '23:59'),
|
||||
closePriceEnabled,
|
||||
return {
|
||||
start: normalizeTradingTimeSetting(settings.getSettingByKey('startDay'), '00:00'),
|
||||
end: normalizeTradingTimeSetting(settings.getSettingByKey('endDay'), '23:59'),
|
||||
closePriceEnabled,
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
start: '00:00',
|
||||
end: '23:59',
|
||||
closePriceEnabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-7
@@ -4,9 +4,9 @@ import type {AxiosRequestConfig, AxiosResponse} from 'axios'
|
||||
import axios from 'axios'
|
||||
import {getToken, removeToken} from "@/utils";
|
||||
import { findStoredAuthToken } from '@/utils/authToken';
|
||||
import router from "@/router";
|
||||
import {get} from 'lodash'
|
||||
import {message} from "ant-design-vue";
|
||||
import lodash from 'lodash'
|
||||
|
||||
const { get } = lodash
|
||||
|
||||
interface Meta {
|
||||
errorMessage?: 'message' | 'notification' | 'toast' | 'none'
|
||||
@@ -43,12 +43,16 @@ function requestPath(config: { url?: string; baseURL?: string } | undefined): st
|
||||
|
||||
async function logoutToLogin() {
|
||||
removeToken()
|
||||
if (router.currentRoute.value.name !== 'login') {
|
||||
await router.push({ name: 'login' })
|
||||
}
|
||||
if (!import.meta.client) return
|
||||
const { getLoginAppUrl } = await import('@/utils/config')
|
||||
window.location.href = getLoginAppUrl()
|
||||
}
|
||||
|
||||
function scheduleSessionVerify() {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
|
||||
if (sessionVerifyTimer) {
|
||||
return
|
||||
}
|
||||
@@ -193,11 +197,16 @@ function shouldShowApiError(config: MyAxiosRequestConfig | undefined): boolean {
|
||||
}
|
||||
|
||||
function showApiError(msg: string, config: MyAxiosRequestConfig | undefined) {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
if (!shouldShowApiError(config)) {
|
||||
return
|
||||
}
|
||||
|
||||
message.error(msg)
|
||||
void import('ant-design-vue').then(({ message }) => {
|
||||
message.error(msg)
|
||||
})
|
||||
}
|
||||
|
||||
const myAxios = {
|
||||
|
||||
@@ -11,5 +11,8 @@ export const APP_Z_MODAL = 5_500
|
||||
export const APP_Z_SUPPORT_PICKER = 5_600
|
||||
|
||||
export function getModalContainer(): HTMLElement {
|
||||
if (!import.meta.client) {
|
||||
return undefined as unknown as HTMLElement
|
||||
}
|
||||
return document.body
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
</div>
|
||||
<div class="dlanding-feature">
|
||||
<BasicIcon icon="vuesax-linear:building" size="16" />
|
||||
<span>تولید کننده واقعی</span>
|
||||
<span>کالای واقعی</span>
|
||||
</div>
|
||||
<div class="dlanding-feature">
|
||||
<BasicIcon icon="vuesax-linear:convert-3d-cube" size="16" />
|
||||
@@ -680,10 +680,11 @@
|
||||
<div class="dlanding-sunrad__inner">
|
||||
<header class="dlanding-sunrad__header">
|
||||
<h2 id="dlanding-sunrad-title" class="dlanding-sunrad__title">
|
||||
شرکت سانراد، پشتوانه بورس مس
|
||||
کالایی که معامله میکنید، باید واقعا وجود داشته باشد.
|
||||
</h2>
|
||||
<p class="dlanding-sunrad__subtitle">
|
||||
بورس مس با تکیه بر خط تولید ایتالیایی و انبار بزرگ شرکت سانراد، خیال شما را از کیفیت مس و عرضه همیشگی آن، راحت میکند.
|
||||
بورس مس با دسترسی به انبار محصولات مسی، فاصله میان معامله آنلاین و کالای واقعی را کوتاه میکند.
|
||||
<strong>انبار فیزیکی؛ یک قدم نزدیکتر به معاملات مطمئن</strong>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
@@ -772,7 +773,7 @@
|
||||
:id="`faq-panel-${index}`"
|
||||
class="dlanding-faq__panel"
|
||||
role="region"
|
||||
:hidden="openFaqIndex !== index"
|
||||
:aria-hidden="openFaqIndex !== index"
|
||||
>
|
||||
<p class="dlanding-faq__answer">{{ item.answer }}</p>
|
||||
</div>
|
||||
@@ -832,7 +833,7 @@
|
||||
<div class="dlanding-footer__brand">
|
||||
<img :src="brandLogo" :alt="projectName" class="dlanding-footer__logo" />
|
||||
<p class="dlanding-footer__brand-text">
|
||||
بورس مس پلتفرم خرید و فروش آنلاین مس، با پشتوانه تولیدکننده واقعی و قیمت شفاف.
|
||||
بورس مس پلتفرم خرید و فروش آنلاین مس، با پشتوانه تولید کننده واقعی کالای واقعی
|
||||
</p>
|
||||
<div class="dlanding-footer__socials" aria-label="شبکههای اجتماعی">
|
||||
<a href="#" class="dlanding-footer__social" aria-label="اینستاگرام">
|
||||
@@ -897,12 +898,14 @@
|
||||
<section id="rules" class="dlanding-anchor" aria-hidden="true"></section>
|
||||
</div>
|
||||
|
||||
<DesktopPwaGuideModal v-model:show="showPwaGuide" @confirm="onPwaGuideConfirm" />
|
||||
<ClientOnly>
|
||||
<DesktopPwaGuideModal v-model:show="showPwaGuide" @confirm="onPwaGuideConfirm" />
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { computed, defineAsyncComponent, defineComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { getLoginAppUrl } from '@/utils/config'
|
||||
import heroPattern from '@/assets/img/landing-hero-pattern.svg'
|
||||
@@ -938,9 +941,12 @@ import {
|
||||
isPwaManifestReady,
|
||||
promptDeferredPwaInstall,
|
||||
} from '@/modules/pwa/manifest'
|
||||
import DesktopPwaGuideModal from '@/views/login/desktop/components/DesktopPwaGuideModal.vue'
|
||||
import { useAboutUsPageContext } from '../composables/useAboutUsPage'
|
||||
|
||||
const DesktopPwaGuideModal = import.meta.client
|
||||
? defineAsyncComponent(() => import('@/views/login/desktop/components/DesktopPwaGuideModal.vue'))
|
||||
: defineComponent({ name: 'DesktopPwaGuideModalStub', setup: () => () => null })
|
||||
|
||||
const MOBILE_NAV_MQ = '(max-width: 1023.98px)'
|
||||
const PRICE_POLL_MS = 30_000
|
||||
const CHART_WIDTH = 360
|
||||
@@ -1435,6 +1441,7 @@ function closeMobileMenu() {
|
||||
}
|
||||
|
||||
function goToLoginApp() {
|
||||
if (!import.meta.client) return
|
||||
const url = getLoginAppUrl()
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
@@ -1445,6 +1452,7 @@ function goLoginAndCloseMenu() {
|
||||
}
|
||||
|
||||
function syncBodyScrollLock(locked: boolean) {
|
||||
if (!import.meta.client) return
|
||||
document.documentElement.classList.toggle('dlanding-menu-open', locked)
|
||||
document.body.classList.toggle('dlanding-menu-open', locked)
|
||||
}
|
||||
@@ -1617,6 +1625,7 @@ watch(
|
||||
)
|
||||
|
||||
function isStandaloneDisplay() {
|
||||
if (!import.meta.client) return false
|
||||
return (
|
||||
window.matchMedia('(display-mode: standalone)').matches ||
|
||||
window.matchMedia('(display-mode: fullscreen)').matches ||
|
||||
@@ -1625,6 +1634,7 @@ function isStandaloneDisplay() {
|
||||
}
|
||||
|
||||
function isIosDevice() {
|
||||
if (!import.meta.client) return false
|
||||
const ua = window.navigator.userAgent
|
||||
return (
|
||||
/iPad|iPhone|iPod/.test(ua) ||
|
||||
@@ -1633,6 +1643,7 @@ function isIosDevice() {
|
||||
}
|
||||
|
||||
function syncInstallAvailability() {
|
||||
if (!import.meta.client) return
|
||||
isAppInstalledOrStandalone.value = isStandaloneDisplay()
|
||||
// Chrome: فقط وقتی blob manifest + SW آماده و deferred موجود باشد
|
||||
canPromptInstall.value =
|
||||
@@ -1640,6 +1651,7 @@ function syncInstallAvailability() {
|
||||
}
|
||||
|
||||
const canInstallWebApp = computed(() => {
|
||||
if (!import.meta.client) return false
|
||||
if (isAppInstalledOrStandalone.value) return false
|
||||
if (canPromptInstall.value) return true
|
||||
// iOS رویداد beforeinstallprompt ندارد — راهنمای Add to Home Screen
|
||||
@@ -1734,7 +1746,7 @@ const whyFeatures = [
|
||||
},
|
||||
{
|
||||
icon: 'vuesax-linear:building-4',
|
||||
title: 'تولیدکننده رسمی',
|
||||
title: 'معامله کالای فیزیکی',
|
||||
text: 'پشتیبان بورس مس، شرکت سانراد است. یک تولیدکننده واقعی مفتول مس که کیفیت و عرضه پایدار را تضمین میکند.',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1819,6 +1819,11 @@ body.dlanding-menu-open {
|
||||
font-size: clamp(13px, 1.15vw, 15px);
|
||||
font-weight: 500;
|
||||
line-height: 1.9;
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&__layout {
|
||||
@@ -2145,6 +2150,19 @@ body.dlanding-menu-open {
|
||||
padding: 0 20px 18px;
|
||||
}
|
||||
|
||||
&__item:not(.is-open) &__panel {
|
||||
/* Visually collapsed, but answer text stays in SSR HTML for SEO */
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&__answer {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<div class="about-us-page-root">
|
||||
<DesktopAboutUsView />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useAboutUsPage } from './composables/useAboutUsPage'
|
||||
import DesktopAboutUsView from './desktop/DesktopAboutUsView.vue'
|
||||
|
||||
useAboutUsPage()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.about-us-page-root {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f4f5f7;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/*
|
||||
* Mobile shell locks html/body overflow. Scroll the landing inside the page root.
|
||||
* Desktop must NOT use overflow:auto here — a full-height auto container with
|
||||
* overscroll-behavior:contain steals wheel events and blocks document scroll.
|
||||
*/
|
||||
html:not(.app-layout--desktop) .app-main > .about-us-page-root {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
html.dlanding-menu-open .about-us-page-root {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
Vendored
+4
-3
@@ -1,9 +1,10 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="nuxt/schema" />
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__APP_CONFIG__?: Record<string, any>;
|
||||
__APP_CONFIG__?: Record<string, any>
|
||||
__APP_BOOTSTRAPPED__?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
export {}
|
||||
|
||||
Reference in New Issue
Block a user