refactor code to nuxt and add ssr and edit some text in landing
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
|||||||
VITE_LOGIN_APP_URL=https://app.boursemes.com/home
|
NUXT_PUBLIC_LOGIN_APP_URL=https://app.boursemes.com/home
|
||||||
|
|||||||
+4
-1
@@ -1,7 +1,10 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
.nuxt
|
||||||
|
.output
|
||||||
dist
|
dist
|
||||||
dist.zip
|
dist.zip
|
||||||
|
.data
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.local
|
*.local
|
||||||
.env
|
.env
|
||||||
.idea
|
.idea
|
||||||
|
|||||||
Vendored
-19
@@ -1,19 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/* prettier-ignore */
|
|
||||||
// @ts-nocheck
|
|
||||||
// Generated by unplugin-vue-components
|
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
|
||||||
export {}
|
|
||||||
|
|
||||||
declare module 'vue' {
|
|
||||||
export interface GlobalComponents {
|
|
||||||
AButton: typeof import('ant-design-vue/es')['Button']
|
|
||||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
|
||||||
ASpin: typeof import('ant-design-vue/es')['Spin']
|
|
||||||
BasicButton: typeof import('./src/components/BasicPacks/BasicButton.vue')['default']
|
|
||||||
BasicIcon: typeof import('./src/components/BasicPacks/BasicIcon.vue')['default']
|
|
||||||
BasicModal: typeof import('./src/components/BasicPacks/BasicModal.vue')['default']
|
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
/// <reference types="vite/client" />
|
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
|
||||||
readonly VITE_LOGIN_APP_URL: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ImportMeta {
|
|
||||||
readonly env: ImportMetaEnv
|
|
||||||
}
|
|
||||||
-751
@@ -1,751 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="fa" dir="rtl">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="enamad" content="44889195" />
|
|
||||||
|
|
||||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
|
||||||
<meta http-equiv="Pragma" content="no-cache" />
|
|
||||||
<meta http-equiv="Expires" content="0" />
|
|
||||||
|
|
||||||
<meta
|
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content"
|
|
||||||
/>
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
||||||
|
|
||||||
<title>در حال بارگذاری...</title>
|
|
||||||
<!---->
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--boot-bg: #133050;
|
|
||||||
--boot-bg-soft: #133050;
|
|
||||||
--boot-card: rgba(255, 255, 255, 0.88);
|
|
||||||
--boot-border: rgba(15, 23, 42, 0.08);
|
|
||||||
--boot-text: #111827;
|
|
||||||
--boot-muted: rgba(15, 23, 42, 0.62);
|
|
||||||
--boot-accent: #2563eb;
|
|
||||||
--boot-orb: rgba(37, 99, 235, 0.10);
|
|
||||||
--boot-spinner-track: rgba(15, 23, 42, 0.10);
|
|
||||||
--boot-shadow: rgba(15, 23, 42, 0.10);
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
min-height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
background: var(--boot-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
min-height: 100vh;
|
|
||||||
width: 100%;
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.boot-loader {
|
|
||||||
min-height: 100vh;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
direction: rtl;
|
|
||||||
font-family:
|
|
||||||
system-ui,
|
|
||||||
-apple-system,
|
|
||||||
BlinkMacSystemFont,
|
|
||||||
"Segoe UI",
|
|
||||||
sans-serif;
|
|
||||||
background: var(--boot-bg);
|
|
||||||
color: var(--boot-text);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.boot-loader__logo {
|
|
||||||
width: min(72vw, 280px);
|
|
||||||
height: auto;
|
|
||||||
object-fit: contain;
|
|
||||||
animation: boot-logo-fade-in 0.45s ease-out both;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes boot-logo-fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.96);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Critical: full-viewport splash before Vue/CSS hydrate */
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
html, body, #app {
|
|
||||||
width: 100%;
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#app,
|
|
||||||
.boot-loader {
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: none !important;
|
|
||||||
}
|
|
||||||
.app-splash-screen {
|
|
||||||
position: fixed !important;
|
|
||||||
inset: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
min-width: 100vw !important;
|
|
||||||
max-width: none !important;
|
|
||||||
min-height: 100vh !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
z-index: 2147483646;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
if (window.matchMedia('(min-width: 768px)').matches) {
|
|
||||||
document.documentElement.classList.add('app-layout--desktop')
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
;(function () {
|
|
||||||
var CONFIG_COOKIE_NAME = 'APP_CONFIG_CACHE'
|
|
||||||
|
|
||||||
function getCookie(name) {
|
|
||||||
var cookie = document.cookie.split('; ').find(function (row) {
|
|
||||||
return row.indexOf(name + '=') === 0
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!cookie) return null
|
|
||||||
|
|
||||||
return cookie.substring(name.length + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function readCachedConfig() {
|
|
||||||
try {
|
|
||||||
var rawCookie = getCookie(CONFIG_COOKIE_NAME)
|
|
||||||
|
|
||||||
if (!rawCookie) return null
|
|
||||||
|
|
||||||
var decoded = decodeURIComponent(rawCookie)
|
|
||||||
var payload = JSON.parse(decoded)
|
|
||||||
|
|
||||||
return payload && payload.config ? payload.config : null
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Boot theme: failed to read cached config', error)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeHex(hex) {
|
|
||||||
if (!hex || typeof hex !== 'string') return null
|
|
||||||
|
|
||||||
var value = hex.trim()
|
|
||||||
|
|
||||||
if (!value) return null
|
|
||||||
|
|
||||||
if (value[0] !== '#') {
|
|
||||||
value = '#' + value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/^#[0-9a-fA-F]{3}$/.test(value)) {
|
|
||||||
value =
|
|
||||||
'#' +
|
|
||||||
value[1] +
|
|
||||||
value[1] +
|
|
||||||
value[2] +
|
|
||||||
value[2] +
|
|
||||||
value[3] +
|
|
||||||
value[3]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!/^#[0-9a-fA-F]{6}$/.test(value)) return null
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
function hexToRgb(hex) {
|
|
||||||
var normalized = normalizeHex(hex)
|
|
||||||
|
|
||||||
if (!normalized) return null
|
|
||||||
|
|
||||||
var num = parseInt(normalized.slice(1), 16)
|
|
||||||
|
|
||||||
return {
|
|
||||||
r: (num >> 16) & 255,
|
|
||||||
g: (num >> 8) & 255,
|
|
||||||
b: num & 255
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function rgbToHex(r, g, b) {
|
|
||||||
function toHex(value) {
|
|
||||||
var hex = Math.max(0, Math.min(255, Math.round(value))).toString(16)
|
|
||||||
return hex.length === 1 ? '0' + hex : hex
|
|
||||||
}
|
|
||||||
|
|
||||||
return '#' + toHex(r) + toHex(g) + toHex(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
function mix(hexA, hexB, weight) {
|
|
||||||
var a = hexToRgb(hexA)
|
|
||||||
var b = hexToRgb(hexB)
|
|
||||||
|
|
||||||
if (!a || !b) return hexA
|
|
||||||
|
|
||||||
return rgbToHex(
|
|
||||||
a.r * (1 - weight) + b.r * weight,
|
|
||||||
a.g * (1 - weight) + b.g * weight,
|
|
||||||
a.b * (1 - weight) + b.b * weight
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function brightness(hex) {
|
|
||||||
var rgb = hexToRgb(hex)
|
|
||||||
|
|
||||||
if (!rgb) return 0
|
|
||||||
|
|
||||||
return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
function hexToRgba(hex, alpha) {
|
|
||||||
var rgb = hexToRgb(hex)
|
|
||||||
|
|
||||||
if (!rgb) return 'rgba(96, 165, 250, ' + alpha + ')'
|
|
||||||
|
|
||||||
return 'rgba(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ', ' + alpha + ')'
|
|
||||||
}
|
|
||||||
|
|
||||||
function setVar(name, value) {
|
|
||||||
document.documentElement.style.setProperty(name, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyBootTheme() {
|
|
||||||
var config = readCachedConfig()
|
|
||||||
var user = config && config.user ? config.user : null
|
|
||||||
|
|
||||||
if (!user) return
|
|
||||||
|
|
||||||
var tenantColor = normalizeHex(user.color)
|
|
||||||
var tenantSecondary = normalizeHex(user.secondary_color)
|
|
||||||
var tenantBackground = normalizeHex(user.background_color)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* مهم:
|
|
||||||
* background_color بکگراند اپ است.
|
|
||||||
* اگر نبود، لودینگ باید روشن بماند، نه اینکه color را بکگراند کنیم.
|
|
||||||
*/
|
|
||||||
var bg = tenantBackground || '#ffffff'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* color و secondary_color فقط برای accent استفاده میشوند.
|
|
||||||
*/
|
|
||||||
var accent = tenantSecondary || tenantColor || '#2563eb'
|
|
||||||
|
|
||||||
var isLight = brightness(bg) > 155
|
|
||||||
|
|
||||||
var bgSoft = isLight ? mix(bg, accent, 0.045) : mix(bg, '#ffffff', 0.055)
|
|
||||||
|
|
||||||
var text = isLight ? '#111827' : '#ffffff'
|
|
||||||
var muted = isLight ? 'rgba(15, 23, 42, 0.62)' : 'rgba(255, 255, 255, 0.70)'
|
|
||||||
|
|
||||||
var card = isLight ? 'rgba(255, 255, 255, 0.88)' : 'rgba(255, 255, 255, 0.07)'
|
|
||||||
var border = isLight ? 'rgba(15, 23, 42, 0.08)' : 'rgba(255, 255, 255, 0.12)'
|
|
||||||
|
|
||||||
var orb = hexToRgba(accent, isLight ? 0.10 : 0.18)
|
|
||||||
var spinnerTrack = isLight ? 'rgba(15, 23, 42, 0.10)' : 'rgba(255, 255, 255, 0.14)'
|
|
||||||
var shadow = isLight ? 'rgba(15, 23, 42, 0.10)' : 'rgba(0, 0, 0, 0.24)'
|
|
||||||
|
|
||||||
setVar('--boot-bg', bg)
|
|
||||||
setVar('--boot-bg-soft', bgSoft)
|
|
||||||
setVar('--boot-accent', accent)
|
|
||||||
setVar('--boot-text', text)
|
|
||||||
setVar('--boot-muted', muted)
|
|
||||||
setVar('--boot-card', card)
|
|
||||||
setVar('--boot-border', border)
|
|
||||||
setVar('--boot-orb', orb)
|
|
||||||
setVar('--boot-spinner-track', spinnerTrack)
|
|
||||||
setVar('--boot-shadow', shadow)
|
|
||||||
|
|
||||||
if (tenantColor) {
|
|
||||||
setVar('--App-primary-color', tenantColor)
|
|
||||||
}
|
|
||||||
if (tenantSecondary) {
|
|
||||||
setVar('--App-secondary-color', tenantSecondary)
|
|
||||||
}
|
|
||||||
if (tenantBackground) {
|
|
||||||
setVar('--App-background-color', tenantBackground)
|
|
||||||
}
|
|
||||||
|
|
||||||
var logoEl = document.getElementById('boot-logo')
|
|
||||||
if (logoEl) {
|
|
||||||
if (user.name === 'boursecu') {
|
|
||||||
logoEl.src = '/boursecu-splash.png'
|
|
||||||
} else if (user.avatar) {
|
|
||||||
logoEl.src = user.avatar
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.name === 'boursecu') {
|
|
||||||
setVar('--boot-bg', '#133050')
|
|
||||||
setVar('--boot-bg-soft', '#133050')
|
|
||||||
}
|
|
||||||
|
|
||||||
document.title = user.name_fa || user.name || 'در حال بارگذاری...'
|
|
||||||
}
|
|
||||||
|
|
||||||
applyBootTheme()
|
|
||||||
})()
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="app">
|
|
||||||
<div class="boot-loader">
|
|
||||||
<img
|
|
||||||
id="boot-logo"
|
|
||||||
class="boot-loader__logo"
|
|
||||||
src="/boursecu-splash.png"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
;(function () {
|
|
||||||
var BOOT_TIMEOUT_MS = 90000
|
|
||||||
|
|
||||||
var RETRY_KEY = '__APP_BOOT_RETRY_CLICKED__'
|
|
||||||
var RECOVERY_KEY = '__APP_BOOT_RECOVERY_DONE__'
|
|
||||||
|
|
||||||
var CONFIG_COOKIE_NAME = 'APP_CONFIG_CACHE'
|
|
||||||
var CONFIG_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 7
|
|
||||||
|
|
||||||
var TOKEN_STORAGE_KEY = 'customer_session_token'
|
|
||||||
var LEGACY_TOKEN_SUFFIX = '_app_token'
|
|
||||||
var VERSION_SUCCESS_URL_KEY = 'VERSION_Success_URL'
|
|
||||||
var VERSION_STORAGE_KEY = 'version'
|
|
||||||
var CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL'
|
|
||||||
|
|
||||||
function getProtocol() {
|
|
||||||
return window.location.protocol === 'https:' ? 'https:' : 'http:'
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVersionUrls() {
|
|
||||||
var 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 getConfigUrls() {
|
|
||||||
var 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'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortVersionUrls(urls) {
|
|
||||||
var lastSuccessUrl = localStorage.getItem(VERSION_SUCCESS_URL_KEY)
|
|
||||||
|
|
||||||
if (!lastSuccessUrl) {
|
|
||||||
return urls.slice()
|
|
||||||
}
|
|
||||||
|
|
||||||
return [lastSuccessUrl].concat(urls.filter(function (url) {
|
|
||||||
return url !== lastSuccessUrl
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortByLastSuccess(urls) {
|
|
||||||
var lastSuccessUrl = localStorage.getItem(CONFIG_SUCCESS_URL_KEY)
|
|
||||||
|
|
||||||
return urls.slice().sort(function (a, b) {
|
|
||||||
if (a === lastSuccessUrl) return -1
|
|
||||||
if (b === lastSuccessUrl) return 1
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeVersionResponse(data) {
|
|
||||||
if (typeof data === 'string' || typeof data === 'number') {
|
|
||||||
return String(data).trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) return ''
|
|
||||||
|
|
||||||
if (typeof data.data === 'string' || typeof data.data === 'number') {
|
|
||||||
return String(data.data).trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
var version =
|
|
||||||
data.version ||
|
|
||||||
data.value ||
|
|
||||||
(data.data && typeof data.data === 'object' ? data.data.version : '')
|
|
||||||
|
|
||||||
return version ? String(version).trim() : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchJsonOrText(url, timeout) {
|
|
||||||
var controller = new AbortController()
|
|
||||||
|
|
||||||
var timeoutId = window.setTimeout(function () {
|
|
||||||
controller.abort()
|
|
||||||
}, timeout)
|
|
||||||
|
|
||||||
try {
|
|
||||||
var response = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json'
|
|
||||||
},
|
|
||||||
cache: 'no-store',
|
|
||||||
signal: controller.signal
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Request failed: ' + response.status)
|
|
||||||
}
|
|
||||||
|
|
||||||
var contentType = response.headers.get('content-type') || ''
|
|
||||||
|
|
||||||
if (contentType.indexOf('application/json') !== -1) {
|
|
||||||
return await response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.text()
|
|
||||||
} finally {
|
|
||||||
window.clearTimeout(timeoutId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchVersionFromUrl(url, timeout) {
|
|
||||||
var controller = new AbortController()
|
|
||||||
var timeoutId = window.setTimeout(function () {
|
|
||||||
controller.abort()
|
|
||||||
}, timeout)
|
|
||||||
|
|
||||||
try {
|
|
||||||
var 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: ' + response.status)
|
|
||||||
}
|
|
||||||
|
|
||||||
var contentType = response.headers.get('content-type') || ''
|
|
||||||
var data
|
|
||||||
|
|
||||||
if (contentType.indexOf('application/json') !== -1) {
|
|
||||||
data = await response.json()
|
|
||||||
} else {
|
|
||||||
data = await response.text()
|
|
||||||
}
|
|
||||||
|
|
||||||
var version = normalizeVersionResponse(data)
|
|
||||||
|
|
||||||
if (!version) {
|
|
||||||
throw new Error('Version response is empty')
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem(VERSION_SUCCESS_URL_KEY, url)
|
|
||||||
return version
|
|
||||||
} catch (error) {
|
|
||||||
if (localStorage.getItem(VERSION_SUCCESS_URL_KEY) === url) {
|
|
||||||
localStorage.removeItem(VERSION_SUCCESS_URL_KEY)
|
|
||||||
}
|
|
||||||
console.warn('Version failed:', url, error)
|
|
||||||
return undefined
|
|
||||||
} finally {
|
|
||||||
window.clearTimeout(timeoutId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadRemoteVersion(rounds) {
|
|
||||||
var urls = sortVersionUrls(getVersionUrls())
|
|
||||||
|
|
||||||
for (var round = 1; round <= rounds; round++) {
|
|
||||||
for (var i = 0; i < urls.length; i++) {
|
|
||||||
var timeout = i === urls.length - 1 ? 6000 : 3500
|
|
||||||
var result = await fetchVersionFromUrl(urls[i], timeout)
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchFreshConfig(rounds) {
|
|
||||||
var urls = sortByLastSuccess(getConfigUrls())
|
|
||||||
|
|
||||||
for (var round = 1; round <= rounds; round++) {
|
|
||||||
for (var i = 0; i < urls.length; i++) {
|
|
||||||
var url = urls[i]
|
|
||||||
|
|
||||||
try {
|
|
||||||
var config = await fetchJsonOrText(url, i === urls.length - 1 ? 7000 : 4000)
|
|
||||||
|
|
||||||
if (!config || typeof config !== 'object') {
|
|
||||||
throw new Error('Config response is invalid')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
config: config,
|
|
||||||
url: url
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (localStorage.getItem(CONFIG_SUCCESS_URL_KEY) === url) {
|
|
||||||
localStorage.removeItem(CONFIG_SUCCESS_URL_KEY)
|
|
||||||
}
|
|
||||||
console.warn('Config failed:', url, 'round:', round, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Cannot fetch fresh config')
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeConfigCookie(config) {
|
|
||||||
try {
|
|
||||||
var payload = {
|
|
||||||
cached_at: Date.now(),
|
|
||||||
config: config
|
|
||||||
}
|
|
||||||
|
|
||||||
var encoded = encodeURIComponent(JSON.stringify(payload))
|
|
||||||
var secure = window.location.protocol === 'https:' ? '; Secure' : ''
|
|
||||||
|
|
||||||
document.cookie =
|
|
||||||
CONFIG_COOKIE_NAME +
|
|
||||||
'=' +
|
|
||||||
encoded +
|
|
||||||
'; Path=/; Max-Age=' +
|
|
||||||
CONFIG_COOKIE_MAX_AGE_SECONDS +
|
|
||||||
'; SameSite=Lax' +
|
|
||||||
secure
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to write config cookie:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearReadableCookies() {
|
|
||||||
document.cookie.split(';').forEach(function (cookie) {
|
|
||||||
var name = cookie.split('=')[0] && cookie.split('=')[0].trim()
|
|
||||||
|
|
||||||
if (!name) return
|
|
||||||
|
|
||||||
document.cookie = name + '=; Path=/; Max-Age=0; SameSite=Lax'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectAuthTokensForPreserve() {
|
|
||||||
var preserved = {}
|
|
||||||
|
|
||||||
var session = localStorage.getItem(TOKEN_STORAGE_KEY)
|
|
||||||
if (session) {
|
|
||||||
preserved[TOKEN_STORAGE_KEY] = session
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < localStorage.length; i++) {
|
|
||||||
var key = localStorage.key(i)
|
|
||||||
if (!key) continue
|
|
||||||
if (key.length >= LEGACY_TOKEN_SUFFIX.length &&
|
|
||||||
key.slice(-LEGACY_TOKEN_SUFFIX.length) === LEGACY_TOKEN_SUFFIX) {
|
|
||||||
var value = localStorage.getItem(key)
|
|
||||||
if (value) {
|
|
||||||
preserved[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return preserved
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearLocalStorageExceptToken() {
|
|
||||||
var preserved = collectAuthTokensForPreserve()
|
|
||||||
|
|
||||||
localStorage.clear()
|
|
||||||
|
|
||||||
Object.keys(preserved).forEach(function (key) {
|
|
||||||
localStorage.setItem(key, preserved[key])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clearServiceWorkersAndCaches() {
|
|
||||||
try {
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
var registrations = await navigator.serviceWorker.getRegistrations()
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
registrations.map(function (registration) {
|
|
||||||
return registration.unregister()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to unregister service workers:', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ('caches' in window) {
|
|
||||||
var keys = await caches.keys()
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
keys.map(function (key) {
|
|
||||||
return caches.delete(key)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to clear caches:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reloadWithCacheBust() {
|
|
||||||
var url = new URL(window.location.href)
|
|
||||||
url.searchParams.set('__boot_recover', String(Date.now()))
|
|
||||||
window.location.replace(url.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderRecovering(text) {
|
|
||||||
var app = document.getElementById('app')
|
|
||||||
if (!app) return
|
|
||||||
|
|
||||||
app.innerHTML =
|
|
||||||
'<div style="min-height: 100vh; display: flex; align-items: center; justify-content: center; direction: rtl; font-family: sans-serif; padding: 24px; background: var(--boot-bg); color: var(--boot-text);">' +
|
|
||||||
'<div style="width: min(420px, 100%); text-align: center; background: var(--boot-card); border: 1px solid var(--boot-border); border-radius: 24px; padding: 28px 22px;">' +
|
|
||||||
'<div style="width: 54px; height: 54px; margin: 0 auto 18px; border-radius: 999px; border: 4px solid rgba(255,255,255,0.18); border-top-color: var(--boot-accent); animation: bootSpin 0.9s linear infinite;"></div>' +
|
|
||||||
'<style>@keyframes bootSpin { to { transform: rotate(360deg); } }</style>' +
|
|
||||||
'<h2 style="margin: 0 0 12px; font-size: 20px;">در حال بازیابی برنامه</h2>' +
|
|
||||||
'<p style="margin: 0; color: var(--boot-muted); line-height: 2; font-size: 14px;">' +
|
|
||||||
(text || 'لطفاً چند لحظه صبر کنید.') +
|
|
||||||
'</p>' +
|
|
||||||
'</div>' +
|
|
||||||
'</div>'
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderBootError() {
|
|
||||||
var app = document.getElementById('app')
|
|
||||||
if (!app) return
|
|
||||||
|
|
||||||
app.innerHTML =
|
|
||||||
'<div style="min-height: 100vh; display: flex; align-items: center; justify-content: center; direction: rtl; font-family: sans-serif; padding: 24px; background: var(--boot-bg); color: var(--boot-text);">' +
|
|
||||||
'<div style="width: min(420px, 100%); text-align: center; background: var(--boot-card); border: 1px solid var(--boot-border); border-radius: 24px; padding: 28px 22px;">' +
|
|
||||||
'<div style="font-size: 42px; margin-bottom: 12px;">⚠️</div>' +
|
|
||||||
'<h2 style="margin: 0 0 12px; font-size: 20px;">خطا در بارگذاری برنامه</h2>' +
|
|
||||||
'<p style="margin: 0 0 20px; color: var(--boot-muted); line-height: 2; font-size: 14px;">' +
|
|
||||||
'ارتباط با سرور برقرار نشد یا فایلهای برنامه بهدرستی دریافت نشدند.' +
|
|
||||||
'</p>' +
|
|
||||||
'<button onclick="window.__APP_BOOT_SIMPLE_RETRY__()" style="padding: 11px 22px; border: 0; border-radius: 12px; background: var(--boot-accent); color: white; cursor: pointer; font-weight: 700;">' +
|
|
||||||
'تلاش مجدد' +
|
|
||||||
'</button>' +
|
|
||||||
'<button onclick="window.__APP_BOOT_CLEAN_RELOAD__()" style="margin-right: 8px; padding: 11px 18px; border: 1px solid var(--boot-border); border-radius: 12px; background: transparent; color: var(--boot-text); cursor: pointer; font-weight: 700;">' +
|
|
||||||
'پاکسازی و تلاش مجدد' +
|
|
||||||
'</button>' +
|
|
||||||
'</div>' +
|
|
||||||
'</div>'
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cleanFetchAndReload() {
|
|
||||||
renderRecovering('در حال دریافت تنظیمات جدید برنامه...')
|
|
||||||
|
|
||||||
var version = await loadRemoteVersion(5)
|
|
||||||
var freshConfigResult = await fetchFreshConfig(5)
|
|
||||||
|
|
||||||
clearReadableCookies()
|
|
||||||
// clearLocalStorageExceptToken()
|
|
||||||
await clearServiceWorkersAndCaches()
|
|
||||||
|
|
||||||
writeConfigCookie(freshConfigResult.config)
|
|
||||||
localStorage.setItem(CONFIG_SUCCESS_URL_KEY, freshConfigResult.url)
|
|
||||||
|
|
||||||
if (version) {
|
|
||||||
localStorage.setItem(VERSION_STORAGE_KEY, version)
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionStorage.removeItem(RETRY_KEY)
|
|
||||||
sessionStorage.removeItem(RECOVERY_KEY)
|
|
||||||
|
|
||||||
reloadWithCacheBust()
|
|
||||||
}
|
|
||||||
|
|
||||||
window.__APP_BOOT_SIMPLE_RETRY__ = function () {
|
|
||||||
sessionStorage.setItem(RETRY_KEY, '1')
|
|
||||||
reloadWithCacheBust()
|
|
||||||
}
|
|
||||||
|
|
||||||
window.__APP_BOOT_CLEAN_RELOAD__ = async function () {
|
|
||||||
try {
|
|
||||||
sessionStorage.setItem(RECOVERY_KEY, '1')
|
|
||||||
await cleanFetchAndReload()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Manual boot recovery failed:', error)
|
|
||||||
renderBootError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.__BOOT_TIMEOUT__ = window.setTimeout(async function () {
|
|
||||||
if (window.__APP_BOOTSTRAPPED__) return
|
|
||||||
|
|
||||||
var app = document.getElementById('app')
|
|
||||||
var stillBooting = app && app.querySelector('.boot-loader')
|
|
||||||
|
|
||||||
if (!stillBooting) return
|
|
||||||
|
|
||||||
if (!sessionStorage.getItem(RETRY_KEY)) {
|
|
||||||
renderBootError()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sessionStorage.getItem(RECOVERY_KEY)) {
|
|
||||||
sessionStorage.setItem(RECOVERY_KEY, '1')
|
|
||||||
|
|
||||||
try {
|
|
||||||
await cleanFetchAndReload()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Auto boot recovery after retry failed:', error)
|
|
||||||
renderBootError()
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
renderBootError()
|
|
||||||
}, BOOT_TIMEOUT_MS)
|
|
||||||
})()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
import { createRequire } from 'node:module'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { resolve } from 'node:path'
|
||||||
|
import { defineNuxtConfig } from 'nuxt/config'
|
||||||
|
|
||||||
|
const srcDir = fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
const require = createRequire(import.meta.url)
|
||||||
|
/** Vite در SSR بهاشتباه browser entry را میگیرد و به window.FormData میزند */
|
||||||
|
const formDataNodeEntry = require.resolve('form-data')
|
||||||
|
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
compatibilityDate: '2024-11-01',
|
||||||
|
srcDir: 'src/',
|
||||||
|
ssr: true,
|
||||||
|
|
||||||
|
app: {
|
||||||
|
baseURL: '/landing/',
|
||||||
|
head: {
|
||||||
|
htmlAttrs: {
|
||||||
|
lang: 'fa',
|
||||||
|
dir: 'rtl',
|
||||||
|
},
|
||||||
|
meta: [
|
||||||
|
{ charset: 'utf-8' },
|
||||||
|
{ name: 'enamad', content: '44889195' },
|
||||||
|
{
|
||||||
|
name: 'viewport',
|
||||||
|
content:
|
||||||
|
'width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content',
|
||||||
|
},
|
||||||
|
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
|
||||||
|
{ name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' },
|
||||||
|
],
|
||||||
|
link: [
|
||||||
|
{ rel: 'icon', type: 'image/png', href: '/landing/favicon.png' },
|
||||||
|
{ rel: 'shortcut icon', type: 'image/png', href: '/landing/favicon.png' },
|
||||||
|
{ rel: 'apple-touch-icon', href: '/landing/favicon.png' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
loginAppUrl:
|
||||||
|
process.env.NUXT_PUBLIC_LOGIN_APP_URL ||
|
||||||
|
process.env.VITE_LOGIN_APP_URL ||
|
||||||
|
'https://app.boursemes.com/home',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
css: [
|
||||||
|
'@/assets/css/landing-entry.less',
|
||||||
|
'@unocss/reset/tailwind-compat.css',
|
||||||
|
],
|
||||||
|
|
||||||
|
modules: ['@pinia/nuxt', '@unocss/nuxt'],
|
||||||
|
|
||||||
|
vite: {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'form-data': formDataNodeEntry,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ssr: {
|
||||||
|
external: ['form-data'],
|
||||||
|
resolve: {
|
||||||
|
conditions: ['node', 'import', 'module', 'default'],
|
||||||
|
externalConditions: ['node', 'import', 'module', 'default'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
less: {
|
||||||
|
modifyVars: {
|
||||||
|
hack: `; @import (reference) "${resolve(srcDir, 'assets/css/var.less').replace(/\\/g, '/')}";`,
|
||||||
|
},
|
||||||
|
javascriptEnabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
typescript: {
|
||||||
|
typeCheck: false,
|
||||||
|
strict: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
nitro: {
|
||||||
|
compressPublicAssets: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
experimental: {
|
||||||
|
payloadExtraction: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
devServer: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 3200,
|
||||||
|
},
|
||||||
|
})
|
||||||
Generated
+13812
-957
File diff suppressed because it is too large
Load Diff
+19
-14
@@ -2,39 +2,44 @@
|
|||||||
"name": "landing",
|
"name": "landing",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "nuxt dev --port 3200",
|
||||||
"build": "vite build",
|
"build": "nuxt build",
|
||||||
"preview": "vite preview",
|
"preview": "nuxt preview --port 3200",
|
||||||
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
"generate": "nuxt generate",
|
||||||
|
"postinstall": "nuxt prepare",
|
||||||
|
"type-check": "nuxt typecheck",
|
||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@pinia/nuxt": "^0.5.5",
|
||||||
"ant-design-vue": "^3.2.20",
|
"ant-design-vue": "^3.2.20",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"nuxt": "^3.15.4",
|
||||||
"pinia": "^2.1.4",
|
"pinia": "^2.1.4",
|
||||||
"unocss": "^0.55.1",
|
"vue": "^3.5.13",
|
||||||
"unplugin-vue-components": "^0.25.1",
|
"vue-router": "^4.5.0"
|
||||||
"vue": "^3.3.4",
|
|
||||||
"vue-router": "^4.2.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rushstack/eslint-patch": "^1.3.2",
|
"@rushstack/eslint-patch": "^1.3.2",
|
||||||
"@tsconfig/node18": "^18.2.0",
|
|
||||||
"@types/lodash": "^4.17.7",
|
"@types/lodash": "^4.17.7",
|
||||||
"@types/node": "^18.17.0",
|
"@types/node": "^20.17.0",
|
||||||
"@vitejs/plugin-vue": "^4.2.3",
|
"@unocss/nuxt": "^0.65.4",
|
||||||
|
"@unocss/reset": "^0.65.4",
|
||||||
"@vue/eslint-config-prettier": "^8.0.0",
|
"@vue/eslint-config-prettier": "^8.0.0",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"@vue/tsconfig": "^0.4.0",
|
|
||||||
"eslint": "^8.45.0",
|
"eslint": "^8.45.0",
|
||||||
"eslint-plugin-vue": "^9.15.1",
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
"less": "^4.2.0",
|
"less": "^4.2.0",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"typescript": "~5.1.6",
|
"typescript": "~5.6.3",
|
||||||
"vite": "^4.4.6",
|
"unocss": "^0.65.4",
|
||||||
"vue-tsc": "^2.1.10"
|
"vue-tsc": "^2.1.10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB 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>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { existsSync, readFileSync } from 'node:fs'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import { defineEventHandler, getRequestURL, send, setResponseHeader } from 'h3'
|
||||||
|
|
||||||
|
const FAVICON_PATHS = [
|
||||||
|
'/favicon.ico',
|
||||||
|
'/favicon.png',
|
||||||
|
'/landing/favicon.ico',
|
||||||
|
'/landing/favicon.png',
|
||||||
|
]
|
||||||
|
|
||||||
|
function resolveFaviconFile() {
|
||||||
|
const candidates = [
|
||||||
|
join(process.cwd(), 'src', 'public', 'favicon.png'),
|
||||||
|
join(process.cwd(), 'public', 'favicon.png'),
|
||||||
|
]
|
||||||
|
for (const path of candidates) {
|
||||||
|
if (existsSync(path)) return path
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* سرو مستقیم favicon تا درخواستهای روت و /landing
|
||||||
|
* وارد Vue Router نشوند و هشدار No match ندهند.
|
||||||
|
*/
|
||||||
|
export default defineEventHandler((event) => {
|
||||||
|
const { pathname } = getRequestURL(event)
|
||||||
|
if (!FAVICON_PATHS.includes(pathname)) return
|
||||||
|
|
||||||
|
const file = resolveFaviconFile()
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
setResponseHeader(event, 'Content-Type', 'image/png')
|
||||||
|
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400')
|
||||||
|
return send(event, readFileSync(file))
|
||||||
|
})
|
||||||
@@ -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) {
|
export function setAppConfig(config: RawAppConfig) {
|
||||||
appConfig = config || {}
|
appConfig = config || {}
|
||||||
window.__APP_CONFIG__ = appConfig
|
if (import.meta.client) {
|
||||||
|
window.__APP_CONFIG__ = appConfig
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRawAppConfig() {
|
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() {
|
export function getTenantConfig() {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type VersionUpdateLock = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCookie(name: string) {
|
function getCookie(name: string) {
|
||||||
|
if (!import.meta.client) return null
|
||||||
const cookie = document.cookie.split('; ').find((row) => row.startsWith(`${name}=`))
|
const cookie = document.cookie.split('; ').find((row) => row.startsWith(`${name}=`))
|
||||||
|
|
||||||
if (!cookie) return null
|
if (!cookie) return null
|
||||||
@@ -32,6 +33,7 @@ function getCookie(name: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setCookie(name: string, value: string, maxAgeSeconds: number) {
|
function setCookie(name: string, value: string, maxAgeSeconds: number) {
|
||||||
|
if (!import.meta.client) return
|
||||||
const secure = window.location.protocol === 'https:' ? '; Secure' : ''
|
const secure = window.location.protocol === 'https:' ? '; Secure' : ''
|
||||||
|
|
||||||
document.cookie = [
|
document.cookie = [
|
||||||
@@ -44,6 +46,7 @@ function setCookie(name: string, value: string, maxAgeSeconds: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function readCachedAppConfig(): AppConfig | null {
|
export function readCachedAppConfig(): AppConfig | null {
|
||||||
|
if (!import.meta.client) return null
|
||||||
try {
|
try {
|
||||||
const rawCookie = getCookie(CONFIG_COOKIE_NAME)
|
const rawCookie = getCookie(CONFIG_COOKIE_NAME)
|
||||||
|
|
||||||
@@ -63,6 +66,7 @@ export function readCachedAppConfig(): AppConfig | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function writeCachedAppConfig(config: AppConfig) {
|
export function writeCachedAppConfig(config: AppConfig) {
|
||||||
|
if (!import.meta.client) return
|
||||||
try {
|
try {
|
||||||
const payload: CachedConfigPayload = {
|
const payload: CachedConfigPayload = {
|
||||||
cached_at: Date.now(),
|
cached_at: Date.now(),
|
||||||
@@ -83,6 +87,7 @@ export function writeCachedAppConfig(config: AppConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function clearCachedAppConfig() {
|
export function clearCachedAppConfig() {
|
||||||
|
if (!import.meta.client) return
|
||||||
document.cookie = `${CONFIG_COOKIE_NAME}=; Path=/; Max-Age=0; SameSite=Lax`
|
document.cookie = `${CONFIG_COOKIE_NAME}=; Path=/; Max-Age=0; SameSite=Lax`
|
||||||
clearCachedPwaManifest()
|
clearCachedPwaManifest()
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-10
@@ -5,6 +5,9 @@ const CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL'
|
|||||||
export const CONFIG_RETRY_ROUNDS = 5
|
export const CONFIG_RETRY_ROUNDS = 5
|
||||||
|
|
||||||
function getProtocol() {
|
function getProtocol() {
|
||||||
|
if (import.meta.server) {
|
||||||
|
return 'https:'
|
||||||
|
}
|
||||||
return window.location.protocol === 'https:' ? 'https:' : 'http:'
|
return window.location.protocol === 'https:' ? 'https:' : 'http:'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,11 +19,15 @@ export function getConfigUrls() {
|
|||||||
`${protocol}//tablo.talasevin.ir/get_user`,
|
`${protocol}//tablo.talasevin.ir/get_user`,
|
||||||
`${protocol}//abshode-service.montgit.ir/server/get_user`,
|
`${protocol}//abshode-service.montgit.ir/server/get_user`,
|
||||||
`${protocol}//212.23.201.173:8381/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[]) {
|
export function sortConfigUrls(urls: string[]) {
|
||||||
|
if (!import.meta.client) {
|
||||||
|
return [...urls]
|
||||||
|
}
|
||||||
|
|
||||||
const lastSuccessUrl = localStorage.getItem(CONFIG_SUCCESS_URL_KEY)
|
const lastSuccessUrl = localStorage.getItem(CONFIG_SUCCESS_URL_KEY)
|
||||||
|
|
||||||
return [...urls].sort((a, b) => {
|
return [...urls].sort((a, b) => {
|
||||||
@@ -31,6 +38,7 @@ export function sortConfigUrls(urls: string[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearStaleConfigSuccessUrl(url: string) {
|
function clearStaleConfigSuccessUrl(url: string) {
|
||||||
|
if (!import.meta.client) return
|
||||||
if (localStorage.getItem(CONFIG_SUCCESS_URL_KEY) === url) {
|
if (localStorage.getItem(CONFIG_SUCCESS_URL_KEY) === url) {
|
||||||
localStorage.removeItem(CONFIG_SUCCESS_URL_KEY)
|
localStorage.removeItem(CONFIG_SUCCESS_URL_KEY)
|
||||||
}
|
}
|
||||||
@@ -38,16 +46,16 @@ function clearStaleConfigSuccessUrl(url: string) {
|
|||||||
|
|
||||||
async function fetchConfig(url: string, timeout = 3000): Promise<AppConfig> {
|
async function fetchConfig(url: string, timeout = 3000): Promise<AppConfig> {
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const timeoutId = window.setTimeout(() => controller.abort(), timeout)
|
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json'
|
Accept: 'application/json',
|
||||||
},
|
},
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
signal: controller.signal
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
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 timeout = index === urls.length - 1 ? 7000 : 4000
|
||||||
const config = await fetchConfig(url, timeout)
|
const config = await fetchConfig(url, timeout)
|
||||||
|
|
||||||
localStorage.setItem(CONFIG_SUCCESS_URL_KEY, url)
|
if (import.meta.client) {
|
||||||
writeCachedAppConfig(config)
|
localStorage.setItem(CONFIG_SUCCESS_URL_KEY, url)
|
||||||
|
writeCachedAppConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -92,10 +102,11 @@ export async function fetchFreshAppConfig(rounds = CONFIG_RETRY_ROUNDS): Promise
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function loadAppConfig(): Promise<AppConfig> {
|
export async function loadAppConfig(): Promise<AppConfig> {
|
||||||
const cachedConfig = readCachedAppConfig()
|
if (import.meta.client) {
|
||||||
|
const cachedConfig = readCachedAppConfig()
|
||||||
if (cachedConfig) {
|
if (cachedConfig) {
|
||||||
return cachedConfig
|
return cachedConfig
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await fetchFreshAppConfig()
|
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 { ref } from 'vue'
|
||||||
import {defineStore} from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import { getTenantConfig } from '@/config/app-config'
|
||||||
|
|
||||||
export const useConfigStore = defineStore('config', () => {
|
export const useConfigStore = defineStore('config', () => {
|
||||||
const config = ref(window.__APP_CONFIG__?.user)
|
const config = ref<Record<string, any> | null>(null)
|
||||||
|
|
||||||
async function getConfigData() {
|
function hydrateFromAppConfig() {
|
||||||
config.value = window.__APP_CONFIG__?.user
|
const tenant = getTenantConfig()
|
||||||
console.log(config.value)
|
config.value = tenant && Object.keys(tenant).length > 0 ? tenant : null
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
async function getConfigData() {
|
||||||
config,
|
hydrateFromAppConfig()
|
||||||
getConfigData
|
}
|
||||||
}
|
|
||||||
|
return {
|
||||||
|
config,
|
||||||
|
hydrateFromAppConfig,
|
||||||
|
getConfigData,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const prefixCls = 'noname'
|
|||||||
const baseButtonIconSize = '24'
|
const baseButtonIconSize = '24'
|
||||||
|
|
||||||
function readCachedTenantField(key: string): string {
|
function readCachedTenantField(key: string): string {
|
||||||
|
if (!import.meta.client) return ''
|
||||||
try {
|
try {
|
||||||
const cookie = document.cookie.split('; ').find((row) => row.startsWith('APP_CONFIG_CACHE='))
|
const cookie = document.cookie.split('; ').find((row) => row.startsWith('APP_CONFIG_CACHE='))
|
||||||
if (!cookie) return ''
|
if (!cookie) return ''
|
||||||
|
|||||||
+17
-1
@@ -5,6 +5,10 @@ import {
|
|||||||
} from '@/themeConfig'
|
} from '@/themeConfig'
|
||||||
|
|
||||||
function legacyTokenKeys(): string[] {
|
function legacyTokenKeys(): string[] {
|
||||||
|
if (!import.meta.client) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const keys: string[] = []
|
const keys: string[] = []
|
||||||
|
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
@@ -19,6 +23,10 @@ function legacyTokenKeys(): string[] {
|
|||||||
|
|
||||||
/** خواندن توکن — اول session ثابت، بعد کلید tenant، بعد هر *_app_token */
|
/** خواندن توکن — اول session ثابت، بعد کلید tenant، بعد هر *_app_token */
|
||||||
export function findStoredAuthToken(): string | null {
|
export function findStoredAuthToken(): string | null {
|
||||||
|
if (!import.meta.client) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const session = localStorage.getItem(SESSION_AUTH_KEY)
|
const session = localStorage.getItem(SESSION_AUTH_KEY)
|
||||||
if (session) {
|
if (session) {
|
||||||
return session
|
return session
|
||||||
@@ -42,7 +50,7 @@ export function findStoredAuthToken(): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function persistAuthToken(token: string | undefined | null) {
|
export function persistAuthToken(token: string | undefined | null) {
|
||||||
if (!token) {
|
if (!import.meta.client || !token) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +59,10 @@ export function persistAuthToken(token: string | undefined | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function clearStoredAuthTokens() {
|
export function clearStoredAuthTokens() {
|
||||||
|
if (!import.meta.client) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
localStorage.removeItem(SESSION_AUTH_KEY)
|
localStorage.removeItem(SESSION_AUTH_KEY)
|
||||||
|
|
||||||
for (const key of legacyTokenKeys()) {
|
for (const key of legacyTokenKeys()) {
|
||||||
@@ -59,6 +71,10 @@ export function clearStoredAuthTokens() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function collectAuthTokensForPreserve(): Record<string, string> {
|
export function collectAuthTokensForPreserve(): Record<string, string> {
|
||||||
|
if (!import.meta.client) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
const preserved: Record<string, string> = {}
|
const preserved: Record<string, string> = {}
|
||||||
|
|
||||||
for (const key of [SESSION_AUTH_KEY, getTokenName(), ...legacyTokenKeys()]) {
|
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 boursecuHomeIcon from '@/assets/img/boursecu-home-icon.png'
|
||||||
import boursecuSettingsIcon from '@/assets/img/boursecu-settings-icon.png'
|
import boursecuSettingsIcon from '@/assets/img/boursecu-settings-icon.png'
|
||||||
|
|
||||||
export function getConfigApp(key: string) {
|
import { getConfig } from '@/config/app-config'
|
||||||
const config = window.__APP_CONFIG__ ? window.__APP_CONFIG__.user : {}
|
|
||||||
|
|
||||||
if (config) {
|
export function getConfigApp(key: string) {
|
||||||
return config[key]
|
return getConfig(key)
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Login URL در اپ اصلی */
|
/** Login URL در اپ اصلی */
|
||||||
export function getLoginAppUrl() {
|
export function getLoginAppUrl() {
|
||||||
const configured = import.meta.env.VITE_LOGIN_APP_URL
|
|
||||||
const fallback = 'https://app.boursemes.com/home'
|
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()) {
|
if (typeof configured !== 'string' || !configured.trim()) {
|
||||||
return fallback
|
return fallback
|
||||||
@@ -34,7 +39,10 @@ export function getLoginAppUrl() {
|
|||||||
/** صفحه Login اپ اصلی با فلگ نصب PWA */
|
/** صفحه Login اپ اصلی با فلگ نصب PWA */
|
||||||
export function getLoginInstallPwaUrl() {
|
export function getLoginInstallPwaUrl() {
|
||||||
try {
|
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`
|
return `${origin}/login?installPwa=true`
|
||||||
} catch {
|
} catch {
|
||||||
return 'https://app.boursemes.com/login?installPwa=true'
|
return 'https://app.boursemes.com/login?installPwa=true'
|
||||||
@@ -202,6 +210,7 @@ export function getProjectSplashBackground() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resetAppSurfaceBackground() {
|
export function resetAppSurfaceBackground() {
|
||||||
|
if (!import.meta.client) return
|
||||||
const background = '#ffffff'
|
const background = '#ffffff'
|
||||||
document.documentElement.style.backgroundColor = background
|
document.documentElement.style.backgroundColor = background
|
||||||
document.body.style.backgroundColor = background
|
document.body.style.backgroundColor = background
|
||||||
@@ -217,11 +226,13 @@ export function getProjectFontFamily() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function applyProjectFont() {
|
export function applyProjectFont() {
|
||||||
|
if (!import.meta.client) return
|
||||||
document.documentElement.style.setProperty('--App-font-family', getProjectFontFamily())
|
document.documentElement.style.setProperty('--App-font-family', getProjectFontFamily())
|
||||||
document.documentElement.classList.add('font-dana')
|
document.documentElement.classList.add('font-dana')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isTWA() {
|
export function isTWA() {
|
||||||
|
if (!import.meta.client) return false
|
||||||
if (sessionStorage.getItem('appMode') === 'twa') return true
|
if (sessionStorage.getItem('appMode') === 'twa') return true
|
||||||
if (document.referrer.startsWith('android-app://')) {
|
if (document.referrer.startsWith('android-app://')) {
|
||||||
sessionStorage.setItem('appMode', 'twa')
|
sessionStorage.setItem('appMode', 'twa')
|
||||||
@@ -231,6 +242,7 @@ export function isTWA() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function applyProjectTheme() {
|
export function applyProjectTheme() {
|
||||||
|
if (!import.meta.client) return
|
||||||
applyProjectFont()
|
applyProjectFont()
|
||||||
document.documentElement.classList.add('project-boursecu')
|
document.documentElement.classList.add('project-boursecu')
|
||||||
document.documentElement.style.setProperty('--App-brand-nav', BOURSECU_BRAND_NAV)
|
document.documentElement.style.setProperty('--App-brand-nav', BOURSECU_BRAND_NAV)
|
||||||
|
|||||||
+19
-20
@@ -5,13 +5,11 @@ import {
|
|||||||
} from '@/utils/authToken'
|
} from '@/utils/authToken'
|
||||||
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import {getConfigApp} from "@/utils/config";
|
import { getConfigApp } from '@/utils/config'
|
||||||
|
|
||||||
export const loading = ref<boolean>(false)
|
export const loading = ref<boolean>(false)
|
||||||
export const loadingMore = ref<boolean>(false)
|
export const loadingMore = ref<boolean>(false)
|
||||||
const userInfo = ref()
|
const userInfo = ref()
|
||||||
const nameProject= getConfigApp('name')
|
|
||||||
|
|
||||||
|
|
||||||
function setToken(token: string | undefined) {
|
function setToken(token: string | undefined) {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@@ -21,7 +19,8 @@ function setToken(token: string | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setFingerPrint(val: 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() {
|
function getToken() {
|
||||||
@@ -29,11 +28,14 @@ function getToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getIsActiveFingerprint() {
|
function getIsActiveFingerprint() {
|
||||||
|
if (!import.meta.client) return false
|
||||||
return localStorage.getItem('finger') === 'true'
|
return localStorage.getItem('finger') === 'true'
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function authenticateWithFingerprint() {
|
export async function authenticateWithFingerprint() {
|
||||||
|
if (!import.meta.client) return false
|
||||||
|
|
||||||
|
const nameProject = getConfigApp('name') || 'بورسمس'
|
||||||
|
|
||||||
const challenge = new Uint8Array(32)
|
const challenge = new Uint8Array(32)
|
||||||
window.crypto.getRandomValues(challenge)
|
window.crypto.getRandomValues(challenge)
|
||||||
@@ -42,26 +44,23 @@ export async function authenticateWithFingerprint() {
|
|||||||
challenge,
|
challenge,
|
||||||
rp: { name: nameProject },
|
rp: { name: nameProject },
|
||||||
user: {
|
user: {
|
||||||
id: Uint8Array.from('user-id', c => c.charCodeAt(0)),
|
id: Uint8Array.from('user-id', (c) => c.charCodeAt(0)),
|
||||||
name: nameProject,
|
name: nameProject,
|
||||||
displayName: nameProject
|
displayName: nameProject,
|
||||||
},
|
},
|
||||||
pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
|
pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
|
||||||
authenticatorSelection: {
|
authenticatorSelection: {
|
||||||
authenticatorAttachment: 'platform',
|
authenticatorAttachment: 'platform',
|
||||||
userVerification: 'required'
|
userVerification: 'required',
|
||||||
},
|
},
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
attestation: 'direct'
|
attestation: 'direct',
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const credential = await navigator.credentials.create({ publicKey: createOptions as CredentialCreationOptions })
|
await navigator.credentials.create({ publicKey: createOptions as CredentialCreationOptions })
|
||||||
console.log('Credential با موفقیت ایجاد شد!')
|
console.log('Credential با موفقیت ایجاد شد!')
|
||||||
|
|
||||||
const credentialId = btoa(String.fromCharCode(...new Uint8Array((credential as PublicKeyCredential).rawId)))
|
|
||||||
|
|
||||||
// بازیابی توکن از localStorage
|
|
||||||
const token = findStoredAuthToken()
|
const token = findStoredAuthToken()
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@@ -69,29 +68,28 @@ export async function authenticateWithFingerprint() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success, proceed with login using token
|
|
||||||
// Perform your login action here
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const errorMessage = error.name === 'NotAllowedError'
|
const errorMessage =
|
||||||
? 'احراز هویت لغو شد یا دسترسی مجاز نبود.'
|
error.name === 'NotAllowedError'
|
||||||
: 'عملیات ناموفق بود. لطفاً دوباره امتحان کنید.'
|
? 'احراز هویت لغو شد یا دسترسی مجاز نبود.'
|
||||||
|
: 'عملیات ناموفق بود. لطفاً دوباره امتحان کنید.'
|
||||||
console.error(errorMessage, error)
|
console.error(errorMessage, error)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function removeToken() {
|
function removeToken() {
|
||||||
return clearStoredAuthTokens()
|
return clearStoredAuthTokens()
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFingerPrint() {
|
function removeFingerPrint() {
|
||||||
|
if (!import.meta.client) return
|
||||||
return localStorage.removeItem('finger')
|
return localStorage.removeItem('finger')
|
||||||
}
|
}
|
||||||
|
|
||||||
function URLUserType() {
|
function URLUserType() {
|
||||||
|
if (!import.meta.client) return 'customer'
|
||||||
if (location.host.split('.')[0] === 'app') return 'single'
|
if (location.host.split('.')[0] === 'app') return 'single'
|
||||||
else return 'customer'
|
else return 'customer'
|
||||||
}
|
}
|
||||||
@@ -101,6 +99,7 @@ function toFixedDownThreeChar(num: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isIphone() {
|
function isIphone() {
|
||||||
|
if (!import.meta.client) return false
|
||||||
const userAgent = navigator.userAgent.toLowerCase()
|
const userAgent = navigator.userAgent.toLowerCase()
|
||||||
|
|
||||||
return /iPhone/.test(userAgent)
|
return /iPhone/.test(userAgent)
|
||||||
@@ -115,5 +114,5 @@ export {
|
|||||||
isIphone,
|
isIphone,
|
||||||
removeFingerPrint,
|
removeFingerPrint,
|
||||||
getIsActiveFingerprint,
|
getIsActiveFingerprint,
|
||||||
setFingerPrint
|
setFingerPrint,
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-10
@@ -82,17 +82,25 @@ export function syncProductTradingHoursFromProducts(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getStoreTradingHours(): StoreTradingHours {
|
export function getStoreTradingHours(): StoreTradingHours {
|
||||||
const settings = useSettingsStore()
|
try {
|
||||||
const closeRaw = settings.getSettingByKey('close_price')
|
const settings = useSettingsStore()
|
||||||
const closePriceEnabled =
|
const closeRaw = settings.getSettingByKey('close_price')
|
||||||
closeRaw === true ||
|
const closePriceEnabled =
|
||||||
closeRaw === 1 ||
|
closeRaw === true ||
|
||||||
String(closeRaw ?? 'false').trim().toLowerCase() === 'true'
|
closeRaw === 1 ||
|
||||||
|
String(closeRaw ?? 'false').trim().toLowerCase() === 'true'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
start: normalizeTradingTimeSetting(settings.getSettingByKey('startDay'), '00:00'),
|
start: normalizeTradingTimeSetting(settings.getSettingByKey('startDay'), '00:00'),
|
||||||
end: normalizeTradingTimeSetting(settings.getSettingByKey('endDay'), '23:59'),
|
end: normalizeTradingTimeSetting(settings.getSettingByKey('endDay'), '23:59'),
|
||||||
closePriceEnabled,
|
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 axios from 'axios'
|
||||||
import {getToken, removeToken} from "@/utils";
|
import {getToken, removeToken} from "@/utils";
|
||||||
import { findStoredAuthToken } from '@/utils/authToken';
|
import { findStoredAuthToken } from '@/utils/authToken';
|
||||||
import router from "@/router";
|
import lodash from 'lodash'
|
||||||
import {get} from 'lodash'
|
|
||||||
import {message} from "ant-design-vue";
|
const { get } = lodash
|
||||||
|
|
||||||
interface Meta {
|
interface Meta {
|
||||||
errorMessage?: 'message' | 'notification' | 'toast' | 'none'
|
errorMessage?: 'message' | 'notification' | 'toast' | 'none'
|
||||||
@@ -43,12 +43,16 @@ function requestPath(config: { url?: string; baseURL?: string } | undefined): st
|
|||||||
|
|
||||||
async function logoutToLogin() {
|
async function logoutToLogin() {
|
||||||
removeToken()
|
removeToken()
|
||||||
if (router.currentRoute.value.name !== 'login') {
|
if (!import.meta.client) return
|
||||||
await router.push({ name: 'login' })
|
const { getLoginAppUrl } = await import('@/utils/config')
|
||||||
}
|
window.location.href = getLoginAppUrl()
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleSessionVerify() {
|
function scheduleSessionVerify() {
|
||||||
|
if (!import.meta.client) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (sessionVerifyTimer) {
|
if (sessionVerifyTimer) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -193,11 +197,16 @@ function shouldShowApiError(config: MyAxiosRequestConfig | undefined): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showApiError(msg: string, config: MyAxiosRequestConfig | undefined) {
|
function showApiError(msg: string, config: MyAxiosRequestConfig | undefined) {
|
||||||
|
if (!import.meta.client) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!shouldShowApiError(config)) {
|
if (!shouldShowApiError(config)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
message.error(msg)
|
void import('ant-design-vue').then(({ message }) => {
|
||||||
|
message.error(msg)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const myAxios = {
|
const myAxios = {
|
||||||
|
|||||||
@@ -11,5 +11,8 @@ export const APP_Z_MODAL = 5_500
|
|||||||
export const APP_Z_SUPPORT_PICKER = 5_600
|
export const APP_Z_SUPPORT_PICKER = 5_600
|
||||||
|
|
||||||
export function getModalContainer(): HTMLElement {
|
export function getModalContainer(): HTMLElement {
|
||||||
|
if (!import.meta.client) {
|
||||||
|
return undefined as unknown as HTMLElement
|
||||||
|
}
|
||||||
return document.body
|
return document.body
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="dlanding-feature">
|
<div class="dlanding-feature">
|
||||||
<BasicIcon icon="vuesax-linear:building" size="16" />
|
<BasicIcon icon="vuesax-linear:building" size="16" />
|
||||||
<span>تولید کننده واقعی</span>
|
<span>کالای واقعی</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="dlanding-feature">
|
<div class="dlanding-feature">
|
||||||
<BasicIcon icon="vuesax-linear:convert-3d-cube" size="16" />
|
<BasicIcon icon="vuesax-linear:convert-3d-cube" size="16" />
|
||||||
@@ -680,10 +680,11 @@
|
|||||||
<div class="dlanding-sunrad__inner">
|
<div class="dlanding-sunrad__inner">
|
||||||
<header class="dlanding-sunrad__header">
|
<header class="dlanding-sunrad__header">
|
||||||
<h2 id="dlanding-sunrad-title" class="dlanding-sunrad__title">
|
<h2 id="dlanding-sunrad-title" class="dlanding-sunrad__title">
|
||||||
شرکت سانراد، پشتوانه بورس مس
|
کالایی که معامله میکنید، باید واقعا وجود داشته باشد.
|
||||||
</h2>
|
</h2>
|
||||||
<p class="dlanding-sunrad__subtitle">
|
<p class="dlanding-sunrad__subtitle">
|
||||||
بورس مس با تکیه بر خط تولید ایتالیایی و انبار بزرگ شرکت سانراد، خیال شما را از کیفیت مس و عرضه همیشگی آن، راحت میکند.
|
بورس مس با دسترسی به انبار محصولات مسی، فاصله میان معامله آنلاین و کالای واقعی را کوتاه میکند.
|
||||||
|
<strong>انبار فیزیکی؛ یک قدم نزدیکتر به معاملات مطمئن</strong>
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -772,7 +773,7 @@
|
|||||||
:id="`faq-panel-${index}`"
|
:id="`faq-panel-${index}`"
|
||||||
class="dlanding-faq__panel"
|
class="dlanding-faq__panel"
|
||||||
role="region"
|
role="region"
|
||||||
:hidden="openFaqIndex !== index"
|
:aria-hidden="openFaqIndex !== index"
|
||||||
>
|
>
|
||||||
<p class="dlanding-faq__answer">{{ item.answer }}</p>
|
<p class="dlanding-faq__answer">{{ item.answer }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -832,7 +833,7 @@
|
|||||||
<div class="dlanding-footer__brand">
|
<div class="dlanding-footer__brand">
|
||||||
<img :src="brandLogo" :alt="projectName" class="dlanding-footer__logo" />
|
<img :src="brandLogo" :alt="projectName" class="dlanding-footer__logo" />
|
||||||
<p class="dlanding-footer__brand-text">
|
<p class="dlanding-footer__brand-text">
|
||||||
بورس مس پلتفرم خرید و فروش آنلاین مس، با پشتوانه تولیدکننده واقعی و قیمت شفاف.
|
بورس مس پلتفرم خرید و فروش آنلاین مس، با پشتوانه تولید کننده واقعی کالای واقعی
|
||||||
</p>
|
</p>
|
||||||
<div class="dlanding-footer__socials" aria-label="شبکههای اجتماعی">
|
<div class="dlanding-footer__socials" aria-label="شبکههای اجتماعی">
|
||||||
<a href="#" class="dlanding-footer__social" aria-label="اینستاگرام">
|
<a href="#" class="dlanding-footer__social" aria-label="اینستاگرام">
|
||||||
@@ -897,12 +898,14 @@
|
|||||||
<section id="rules" class="dlanding-anchor" aria-hidden="true"></section>
|
<section id="rules" class="dlanding-anchor" aria-hidden="true"></section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DesktopPwaGuideModal v-model:show="showPwaGuide" @confirm="onPwaGuideConfirm" />
|
<ClientOnly>
|
||||||
|
<DesktopPwaGuideModal v-model:show="showPwaGuide" @confirm="onPwaGuideConfirm" />
|
||||||
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
import { getLoginAppUrl } from '@/utils/config'
|
import { getLoginAppUrl } from '@/utils/config'
|
||||||
import heroPattern from '@/assets/img/landing-hero-pattern.svg'
|
import heroPattern from '@/assets/img/landing-hero-pattern.svg'
|
||||||
@@ -938,9 +941,12 @@ import {
|
|||||||
isPwaManifestReady,
|
isPwaManifestReady,
|
||||||
promptDeferredPwaInstall,
|
promptDeferredPwaInstall,
|
||||||
} from '@/modules/pwa/manifest'
|
} from '@/modules/pwa/manifest'
|
||||||
import DesktopPwaGuideModal from '@/views/login/desktop/components/DesktopPwaGuideModal.vue'
|
|
||||||
import { useAboutUsPageContext } from '../composables/useAboutUsPage'
|
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 MOBILE_NAV_MQ = '(max-width: 1023.98px)'
|
||||||
const PRICE_POLL_MS = 30_000
|
const PRICE_POLL_MS = 30_000
|
||||||
const CHART_WIDTH = 360
|
const CHART_WIDTH = 360
|
||||||
@@ -1435,6 +1441,7 @@ function closeMobileMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function goToLoginApp() {
|
function goToLoginApp() {
|
||||||
|
if (!import.meta.client) return
|
||||||
const url = getLoginAppUrl()
|
const url = getLoginAppUrl()
|
||||||
window.open(url, '_blank', 'noopener,noreferrer')
|
window.open(url, '_blank', 'noopener,noreferrer')
|
||||||
}
|
}
|
||||||
@@ -1445,6 +1452,7 @@ function goLoginAndCloseMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function syncBodyScrollLock(locked: boolean) {
|
function syncBodyScrollLock(locked: boolean) {
|
||||||
|
if (!import.meta.client) return
|
||||||
document.documentElement.classList.toggle('dlanding-menu-open', locked)
|
document.documentElement.classList.toggle('dlanding-menu-open', locked)
|
||||||
document.body.classList.toggle('dlanding-menu-open', locked)
|
document.body.classList.toggle('dlanding-menu-open', locked)
|
||||||
}
|
}
|
||||||
@@ -1617,6 +1625,7 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
function isStandaloneDisplay() {
|
function isStandaloneDisplay() {
|
||||||
|
if (!import.meta.client) return false
|
||||||
return (
|
return (
|
||||||
window.matchMedia('(display-mode: standalone)').matches ||
|
window.matchMedia('(display-mode: standalone)').matches ||
|
||||||
window.matchMedia('(display-mode: fullscreen)').matches ||
|
window.matchMedia('(display-mode: fullscreen)').matches ||
|
||||||
@@ -1625,6 +1634,7 @@ function isStandaloneDisplay() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isIosDevice() {
|
function isIosDevice() {
|
||||||
|
if (!import.meta.client) return false
|
||||||
const ua = window.navigator.userAgent
|
const ua = window.navigator.userAgent
|
||||||
return (
|
return (
|
||||||
/iPad|iPhone|iPod/.test(ua) ||
|
/iPad|iPhone|iPod/.test(ua) ||
|
||||||
@@ -1633,6 +1643,7 @@ function isIosDevice() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function syncInstallAvailability() {
|
function syncInstallAvailability() {
|
||||||
|
if (!import.meta.client) return
|
||||||
isAppInstalledOrStandalone.value = isStandaloneDisplay()
|
isAppInstalledOrStandalone.value = isStandaloneDisplay()
|
||||||
// Chrome: فقط وقتی blob manifest + SW آماده و deferred موجود باشد
|
// Chrome: فقط وقتی blob manifest + SW آماده و deferred موجود باشد
|
||||||
canPromptInstall.value =
|
canPromptInstall.value =
|
||||||
@@ -1640,6 +1651,7 @@ function syncInstallAvailability() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const canInstallWebApp = computed(() => {
|
const canInstallWebApp = computed(() => {
|
||||||
|
if (!import.meta.client) return false
|
||||||
if (isAppInstalledOrStandalone.value) return false
|
if (isAppInstalledOrStandalone.value) return false
|
||||||
if (canPromptInstall.value) return true
|
if (canPromptInstall.value) return true
|
||||||
// iOS رویداد beforeinstallprompt ندارد — راهنمای Add to Home Screen
|
// iOS رویداد beforeinstallprompt ندارد — راهنمای Add to Home Screen
|
||||||
@@ -1734,7 +1746,7 @@ const whyFeatures = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'vuesax-linear:building-4',
|
icon: 'vuesax-linear:building-4',
|
||||||
title: 'تولیدکننده رسمی',
|
title: 'معامله کالای فیزیکی',
|
||||||
text: 'پشتیبان بورس مس، شرکت سانراد است. یک تولیدکننده واقعی مفتول مس که کیفیت و عرضه پایدار را تضمین میکند.',
|
text: 'پشتیبان بورس مس، شرکت سانراد است. یک تولیدکننده واقعی مفتول مس که کیفیت و عرضه پایدار را تضمین میکند.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1819,6 +1819,11 @@ body.dlanding-menu-open {
|
|||||||
font-size: clamp(13px, 1.15vw, 15px);
|
font-size: clamp(13px, 1.15vw, 15px);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.9;
|
line-height: 1.9;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: 700;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__layout {
|
&__layout {
|
||||||
@@ -2145,6 +2150,19 @@ body.dlanding-menu-open {
|
|||||||
padding: 0 20px 18px;
|
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 {
|
&__answer {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #64748b;
|
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 {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
__APP_CONFIG__?: Record<string, any>;
|
__APP_CONFIG__?: Record<string, any>
|
||||||
|
__APP_BOOTSTRAPPED__?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
|
||||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
|
||||||
"exclude": ["src/**/__tests__/*"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-9
@@ -1,11 +1,3 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"extends": "./.nuxt/tsconfig.json"
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.node.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.app.json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@tsconfig/node18/tsconfig.json",
|
|
||||||
"include": [
|
|
||||||
"vite.config.*"
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "Bundler",
|
|
||||||
"types": ["node"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import {fileURLToPath, URL} from 'node:url'
|
|
||||||
|
|
||||||
import {defineConfig} from 'vite'
|
|
||||||
import vue from '@vitejs/plugin-vue'
|
|
||||||
import UnoCSS from 'unocss/vite'
|
|
||||||
import {resolve} from 'path'
|
|
||||||
import {AntDesignVueResolver} from 'unplugin-vue-components/resolvers'
|
|
||||||
import Components from 'unplugin-vue-components/vite'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
base: '/',
|
|
||||||
plugins: [
|
|
||||||
vue(),
|
|
||||||
UnoCSS({
|
|
||||||
configFile: './uno.config.ts',
|
|
||||||
}),
|
|
||||||
Components({
|
|
||||||
resolvers: [AntDesignVueResolver({importStyle: 'less'})],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
host: true,
|
|
||||||
port: 3200,
|
|
||||||
strictPort: true,
|
|
||||||
},
|
|
||||||
css: {
|
|
||||||
preprocessorOptions: {
|
|
||||||
less: {
|
|
||||||
modifyVars: {
|
|
||||||
hack: `; @import (reference) "${resolve('src/assets/css/var.less')}"`,
|
|
||||||
},
|
|
||||||
javascriptEnabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user