feat(user): about software page, enamad badge, and deal sheet UI polish

Add public about-software flow, login-only enamad, and deal buy/sell UI refinements for the store user app.
This commit is contained in:
2026-08-24 19:39:07 +03:30
parent bb4bcdf600
commit e87ba066db
11 changed files with 367 additions and 47 deletions
+22 -1
View File
@@ -89,13 +89,17 @@
<div
v-if="!showSplash && !hideAppChrome && !isDesktop"
class="app-credit-footer app-credit-footer--mobile"
:class="{ 'app-credit-footer--login': isLoginRoute }"
>
<EnamadBadge v-if="isLoginRoute" size="sm" />
<AppCreditLink variant="mobile" />
</div>
<div
v-if="!showSplash && !hideAppChrome && showDesktopFloatingCredit"
class="app-credit-footer app-credit-footer--desktop-float"
:class="{ 'app-credit-footer--login': isLoginRoute }"
>
<EnamadBadge v-if="isLoginRoute" size="sm" />
<AppCreditLink variant="float" />
</div>
@@ -121,6 +125,7 @@ import SupportMessengerPicker from '@/components/SupportMessengerPicker.vue'
import { useDraggableFloatingButton } from '@/utils/useDraggableFloatingButton'
import { useApplyDesktopLayoutClass, useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
import AppCreditLink from '@/components/AppCreditLink.vue'
import EnamadBadge from '@/components/EnamadBadge.vue'
import { usePwaInstall } from '@/composables/usePwaInstall'
import PwaInstallGuideModal from '@/components/pwa/PwaInstallGuideModal.vue'
import GoldDayCalculatorModal from '@/components/GoldDayCalculatorModal.vue'
@@ -159,6 +164,7 @@ const showDesktopFloatingCredit = computed(() => {
if (!isDesktop.value) return false
return route.name === 'login' || !!route.meta.hideMenu
})
const isLoginRoute = computed(() => route.name === 'login')
const {
guideOpen,
@@ -305,19 +311,34 @@ watch(latestIncomingNotification, (payload) => {
margin-left: auto;
margin-right: auto;
min-height: var(--app-credit-bar-inner);
padding-bottom: var(--app-safe-bottom);
padding: 4px 0 var(--app-safe-bottom);
background: #fff;
box-shadow: 0 -2px 4px -2px #0000001a;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 4px;
font-size: 9px;
}
.app-credit-footer--login.app-credit-footer--mobile {
min-height: 72px;
}
:root:has(.app-credit-footer--login) {
--app-credit-bar-inner: 72px;
}
.app-credit-footer--desktop-float {
left: auto;
right: 20px;
bottom: 16px;
width: auto;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.app-credit-link {
+1 -1
View File
@@ -37,7 +37,7 @@ export function setupRouterGuards() {
if (to.meta?.public) {
return true
}
return ['login', '404', 'buyProduct', 'productDetail', 'about', 'cart', 'tvMonitor'].includes(
return ['login', '404', 'buyProduct', 'productDetail', 'about', 'aboutSoftware', 'cart', 'tvMonitor'].includes(
to.name as string,
)
}
+70
View File
@@ -0,0 +1,70 @@
<template>
<a
v-if="href && src"
referrerpolicy="origin"
target="_blank"
rel="noopener noreferrer"
:href="href"
class="enamad-badge"
:class="`enamad-badge--${size}`"
>
<img
referrerpolicy="origin"
:src="src"
alt="enamad"
class="enamad-badge__img"
:code="code || undefined"
/>
</a>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useSettingsStore } from '@/store/settingsStore'
withDefaults(
defineProps<{
size?: 'sm' | 'lg'
}>(),
{ size: 'sm' },
)
const settingsStore = useSettingsStore()
const href = computed(() =>
String(settingsStore.getSettingByKey('enamad_url') ?? '').trim(),
)
const src = computed(() =>
String(settingsStore.getSettingByKey('enamad_logo') ?? '').trim(),
)
const code = computed(() => {
try {
const u = new URL(href.value || src.value)
return u.searchParams.get('Code') || u.searchParams.get('code') || ''
} catch {
return ''
}
})
</script>
<style scoped lang="less">
.enamad-badge {
display: inline-flex;
line-height: 0;
}
.enamad-badge__img {
width: auto;
cursor: pointer;
object-fit: contain;
}
.enamad-badge--sm .enamad-badge__img {
height: 40px;
}
.enamad-badge--lg .enamad-badge__img {
height: 120px;
max-width: 100%;
}
</style>
+6
View File
@@ -197,6 +197,12 @@ const router = createRouter({
name: '404',
component: () => import('../views/base/404.vue')
},
{
path: '/about-software',
name: 'aboutSoftware',
meta: { public: true },
component: () => import('@/views/base/aboutSoftware/index.vue')
},
/* {
path: `/single/login`,
@@ -0,0 +1,27 @@
export function getDefaultAboutSoftwareText(projectName: string) {
const name = projectName.trim() || 'این نرم‌افزار'
return `${name} بستری امن و آنلاین برای خرید و فروش طلا فراهم کرده تا کاربران بتوانند سرمایه‌های خود را به‌سادگی مدیریت کنند.\n\nهدف ما ارائه تجربه‌ای سریع، شفاف و کاربرپسند در معاملات طلا است.`
}
function looksLikeHtml(value: string) {
return /<\/?[a-z][\s\S]*>/i.test(value)
}
export type AboutSoftwareBody =
| { kind: 'html'; html: string }
| { kind: 'text'; text: string }
/** اولویت با تنظیم about_software؛ در صورت خالی بودن، متن پیش‌فرض */
export function resolveAboutSoftwareBody(
raw: unknown,
projectName: string,
): AboutSoftwareBody {
const value = String(raw ?? '').trim()
if (!value) {
return { kind: 'text', text: getDefaultAboutSoftwareText(projectName) }
}
if (looksLikeHtml(value)) {
return { kind: 'html', html: value }
}
return { kind: 'text', text: value }
}
+163
View File
@@ -0,0 +1,163 @@
<template>
<div class="about-software-page-root">
<template v-if="isDesktop">
<div class="dabout">
<div class="dabout__container">
<DesktopPageTopbar eyebrow="اطلاعات" title="درباره نرم‌افزار" show-back />
<div class="dabout-layout">
<section class="dabout-surface dabout-main about-software-card">
<div class="dabout-main__intro">
<div class="dabout-main__logo">
<BasicIcon icon="vuesax-linear:info-circle" size="28" />
</div>
<div>
<h2 class="dabout-main__project">{{ projectName }}</h2>
<p class="dabout-main__tagline">معرفی نرم‌افزار و خدمات</p>
</div>
</div>
<div class="about-software-scroll">
<div
v-if="body.kind === 'html'"
class="about-software-body about-software-html"
v-html="body.html"
/>
<div v-else class="about-software-body whitespace-pre-line">
{{ body.text }}
</div>
<div class="about-software-enamad">
<EnamadBadge size="lg" />
</div>
</div>
</section>
</div>
</div>
</div>
</template>
<template v-else>
<MobilePageHeader title="درباره نرم‌افزار" show-back />
<div class="mpage-content about-software-mobile pb-[calc(var(--app-bottom-chrome-height)+1rem)]">
<div class="about-software-card about-software-mobile__card">
<div class="about-software-scroll">
<div
v-if="body.kind === 'html'"
class="about-software-body about-software-html"
v-html="body.html"
/>
<div v-else class="about-software-body whitespace-pre-line">
{{ body.text }}
</div>
<div class="about-software-enamad">
<EnamadBadge size="lg" />
</div>
</div>
</div>
</div>
</template>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted } from 'vue'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import EnamadBadge from '@/components/EnamadBadge.vue'
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
import DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue'
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
import { getConfig } from '@/config/app-config'
import { useSettingsStore } from '@/store/settingsStore'
import { resolveAboutSoftwareBody } from './aboutSoftwareContent'
const isDesktop = useDesktopBreakpoint()
const settingsStore = useSettingsStore()
const projectName = computed(() => getConfig('name_fa', 'کشکول گلد') || '')
const body = computed(() => {
void settingsStore.settings
return resolveAboutSoftwareBody(
settingsStore.getSettingByKey('about_software'),
String(projectName.value),
)
})
onMounted(() => {
void settingsStore.fetchSettings()
})
</script>
<style scoped>
.about-software-page-root {
min-height: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.about-software-mobile {
display: flex;
flex-direction: column;
min-height: 0;
flex: 1;
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.about-software-mobile__card {
flex: 1 1 0;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid rgba(15, 23, 42, 0.12);
border-radius: 0.75rem;
background: #fff;
}
.about-software-card {
padding: 0.5rem 0.625rem;
}
.about-software-scroll {
flex: 1 1 0;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
overscroll-behavior-y: contain;
}
.about-software-body {
font-size: 0.75rem;
line-height: 1.7;
text-align: justify;
text-justify: inter-word;
}
.about-software-html :deep(p) {
margin: 0;
font-size: 0.75rem;
line-height: 1.7;
text-align: justify;
text-justify: inter-word;
}
.about-software-html :deep(p + p) {
margin-top: 0.65rem;
}
.about-software-html :deep(ul) {
margin: 0.5rem 0 0;
padding-right: 1.1rem;
list-style: disc;
font-size: 0.75rem;
text-align: justify;
}
.about-software-enamad {
display: flex;
justify-content: center;
flex-shrink: 0;
margin-top: 1rem;
padding-top: 0.75rem;
}
</style>
@@ -10,7 +10,7 @@
}
.gold-embed {
max-height: min(70vh, 35rem);
max-height: min(78vh, 40rem);
overflow-x: hidden;
overflow-y: auto;
width: 100%;
+30 -20
View File
@@ -18,35 +18,45 @@
<div :class="panelClass">
<div
:class="[
'!overflow-y-scroll bg-white py-3 text-primary font-bold flex justify-evenly',
'bg-white px-3 pt-3 pb-1',
isStandaloneMobile ? 'mpage-subheader wrap-fix' : '',
]"
>
<!-- <div @click="isBuy = false" class="w-full text-center" :class="!isBuy?'text-primary':'text-gray-400'">خرید طلا</div>
<div @click="isBuy = true" class="w-full text-center" :class="isBuy?'text-primary':'text-gray-400'">فروش طلا</div>-->
<div class="flex justify-evenly w-full px-2 h-11 items-center bg-gray-50 relative rounded ">
<div :class="isBuy?'text-tertiary':'text-gray-400'" class="w-full text-center z-2"
@click.stop="setIsBuy(true) "
:style="!isProductBuyOpen(props?.ProductItem) ? 'pointer-events: none; opacity: 0.5;' : ''">
خرید {{ tradeProductLabel }}
<div class="flex w-full p-1 items-center border border-gray-200 rounded-full bg-white gap-1">
<div
class="w-full h-10 flex items-center justify-center font-bold rounded-full transition-all"
:class="isBuy ? 'bg-green-600 text-white shadow-sm' : 'text-green-600'"
@click.stop="setIsBuy(true)"
:style="!isProductBuyOpen(props?.ProductItem) ? 'pointer-events: none; opacity: 0.35;' : ''"
>
خرید
</div>
<div :class="!isBuy?'text-tertiary':'text-gray-400'" class="w-full text-center z-2"
@click.stop="setIsBuy(false) "
:style="!isProductSellOpen(props?.ProductItem) ? 'pointer-events: none; opacity: 0.5;' : ''">
فروش {{ tradeProductLabel }}
<div
class="w-full h-10 flex items-center justify-center font-bold rounded-full transition-all"
:class="!isBuy ? 'bg-red-600 text-white shadow-sm' : 'text-red-600'"
@click.stop="setIsBuy(false)"
:style="!isProductSellOpen(props?.ProductItem) ? 'pointer-events: none; opacity: 0.35;' : ''"
>
فروش
</div>
<!-- <div ref="bganim" class="absolute bg-white h-8 w-1/2 rounded-lg " :style="`transform: translateX(${isbuy? '0':'-100%'}); left: ${isbuy? '0':'100%'}`"></div>-->
<div ref="bganim" :style="useStyle()"
class="absolute bg-white h-8 w-1/2 rounded-lg animate right"></div>
</div>
</div>
<div v-if="userStore" :class="contentClass">
<a-spin :spinning="!price">
<div v-if="!loading" :class="!isBuy? ' bg-red-100' : ' bg-green-100'"
class="w-full relative rounded-lg h-[88px] flex flex-col justify-center background-color-transition">
class="w-full relative rounded-2xl h-[88px] flex flex-col justify-center background-color-transition">
<img :src="img1" alt="" class="h-[88px] absolute inset-0 w-full object-cover">
<div class="w-full items-center flex flex-col">
<div class="text-gray-700 font-bold">{{ !isBuy ? 'قیمت فروش' : 'قیمت خرید' }}</div>
<div class="flex items-center justify-center gap-2 flex-wrap px-3">
<span class="text-gray-700 font-bold">{{ !isBuy ? 'قیمت فروش' : 'قیمت خرید' }}</span>
<span
v-if="tradeProductLabel"
class="text-sm font-extrabold px-3 py-1 rounded-full max-w-[14rem] truncate shadow-sm"
:class="!isBuy ? 'bg-red-200/90 text-red-900' : 'bg-green-200/90 text-green-900'"
>
{{ tradeProductLabel }}
</span>
</div>
<div v-if="price" class="flex gap-1 mt-3 text-xs items-center">
<div :class="!isBuy? 'text-red-700' : 'text-green-700'" class="font-bold text-2xl color-transition">
{{ usePriceWithoutType(goldPriceValue) }}
@@ -60,7 +70,7 @@
</div>
</a-spin>
<a-form :model="form" class="!mt-5" name="basic" @finish="okConfirm()">
<div class="border-gray-200 border-1 rounded-xl p-5 gold-form-card relative">
<div class="border-gray-200 border-1 rounded-2xl p-5 gold-form-card relative">
<bg_main />
<div class="z-100 relative">
<a-form-item>
@@ -137,7 +147,7 @@
موجودی کیف پول ناکافی می باشد.
</div>
</a-form-item>
<div class="w-full px-4 rounded-xl py-2 border-1 bg-white gold-fee-box">
<div class="w-full px-4 rounded-2xl py-2 border-1 bg-white gold-fee-box">
<div class="flex justify-between items-center">
<div>کارمزد ({{ feeDeductsFromGold ? `از طلا` : `از ریال` }}):</div>
<div class="flex gap-2 items-center font-bold">
@@ -342,7 +352,7 @@ const contentClass = computed(() => {
if (isStandaloneMobile.value) {
return 'px-3 mpage-content mpage-content--with-subheader pb-[calc(var(--app-bottom-chrome-height)+1rem)]'
}
return 'mt-5 px-3 pb-4'
return 'mt-5 px-3 pb-10'
})
const isBuy = ref<boolean>(true)
const inputLock = ref<TradeInputLock>(null)
@@ -1,32 +1,41 @@
<template>
<div class="h-35rem overflow-y-auto">
<div class=" !overflow-y-scroll bg-white py-3 text-primary font-bold flex justify-evenly">
<!-- <div @click="isBuy = false" class="w-full text-center" :class="!isBuy?'text-primary':'text-gray-400'">خرید طلا</div>
<div @click="isBuy = true" class="w-full text-center" :class="isBuy?'text-primary':'text-gray-400'">فروش طلا</div>-->
<div class="flex justify-evenly w-full px-2 h-11 items-center bg-gray-50 relative rounded ">
<div :class="isBuy?'text-primary':'text-gray-400'" class="w-full text-center z-2"
@click.stop="setIsBuy(true) "
:style="!props?.ProductItem?.open_buy ? 'pointer-events: none; opacity: 0.5;' : ''">
خرید {{ props?.ProductItem?.title }}
<div class="h-[40rem] overflow-y-auto">
<div class="bg-white px-3 pt-3 pb-1">
<div class="flex w-full p-1 items-center border border-gray-200 rounded-full bg-white gap-1">
<div
class="w-full h-10 flex items-center justify-center font-bold rounded-full transition-all"
:class="isBuy ? 'bg-green-600 text-white shadow-sm' : 'text-green-600'"
@click.stop="setIsBuy(true)"
:style="!props?.ProductItem?.open_buy ? 'pointer-events: none; opacity: 0.35;' : ''"
>
خرید
</div>
<div
class="w-full h-10 flex items-center justify-center font-bold rounded-full transition-all"
:class="!isBuy ? 'bg-red-600 text-white shadow-sm' : 'text-red-600'"
@click.stop="setIsBuy(false)"
:style="!props?.ProductItem?.open_sell ? 'pointer-events: none; opacity: 0.35;' : ''"
>
فروش
</div>
<!-- <div :class="!isBuy?'text-primary':'text-gray-400'" class="w-full text-center z-2"-->
<!-- @click.stop="setIsBuy(false) "-->
<!-- :style="!props?.ProductItem?.open_sell ? 'pointer-events: none; opacity: 0.5;' : ''"-->
<!-- >-->
<!-- فروش {{ props?.ProductItem?.title }}-->
<!-- </div>-->
<!-- <div ref="bganim" class="absolute bg-white h-8 w-1/2 rounded-lg " :style="`transform: translateX(${isbuy? '0':'-100%'}); left: ${isbuy? '0':'100%'}`"></div>-->
<div ref="bganim" :style="useStyle()"
class="absolute bg-white h-8 w-full rounded-lg animate right"></div>
</div>
</div>
<div v-if="userStore" class=" mt-5 pb-2 mb-19vh">
<div v-if="userStore" class=" mt-5 pb-8 mb-16vh">
<!-- <a-spin :spinning="loading">-->
<div v-if="!loading" :class="!isBuy? ' bg-red-100' : ' bg-green-100'"
class="w-full relative rounded-lg h-[88px] flex flex-col justify-center background-color-transition">
class="w-full relative rounded-2xl h-[88px] flex flex-col justify-center background-color-transition">
<img :src="img1" alt="" class="h-[88px] absolute">
<div class="w-full items-center flex flex-col">
<div class="text-gray-700 font-bold">{{ !isBuy ? 'قیمت فروش' : 'قیمت خرید' }}</div>
<div class="flex items-center justify-center gap-2 flex-wrap px-3">
<span class="text-gray-700 font-bold">{{ !isBuy ? 'قیمت فروش' : 'قیمت خرید' }}</span>
<span
v-if="props?.ProductItem?.title"
class="text-sm font-extrabold px-3 py-1 rounded-full max-w-[14rem] truncate shadow-sm"
:class="!isBuy ? 'bg-red-200/90 text-red-900' : 'bg-green-200/90 text-green-900'"
>
{{ props.ProductItem.title }}
</span>
</div>
<div v-if="price" class="flex gap-1 mt-3 text-xs items-center">
<div :class="!isBuy? 'text-red-700' : 'text-green-700'" class="font-bold text-2xl color-transition">
{{ usePriceWithoutType(isBuy ? ((props?.ProductItem?.price ?? 0) + (props?.ProductItem?.cash_fee_buy ?? 0)) : ((props?.ProductItem?.price ?? 0) - (props?.ProductItem?.cash_fee_sell ?? 0))) }}
@@ -40,7 +49,7 @@
</div>
<!-- </a-spin>-->
<a-form ref="refInput" :model="form" class="!mt-5" name="basic" @finish="okConfirm()" @finishFailed="(errorInfo) => { console.log('Form submit failed:', errorInfo); console.log('Error fields details:', JSON.stringify(errorInfo.errorFields, null, 2)); if (errorInfo.errorFields && errorInfo.errorFields.length > 0) { const firstError = errorInfo.errorFields[0]; console.log('First error field:', firstError.name, 'errors:', firstError.errors); message.error(firstError.errors?.[0] || 'لطفا تمام فیلدها را به درستی پر کنید') } }">
<div class="border-gray-200 border-1 rounded-xl p-5 relative">
<div class="border-gray-200 border-1 rounded-2xl p-5 relative">
<bg_main />
<div class="z-100 relative">
<a-form-item name="method" :rules="[required()]">
@@ -100,7 +109,7 @@
</div>
</div>
<!-- <a-form-item-rest v-if="isBuy && form.method === 'cash'">-->
<!-- <div class="mt-4 w-full px-6 rounded-xl py-1 border-1 bg-white">-->
<!-- <div class="mt-4 w-full px-6 rounded-2xl py-1 border-1 bg-white">-->
<!-- <div class="flex justify-between items-center">-->
<!-- <div>استفاده از درگاه:</div>-->
<!-- <div class="flex gap-2 items-center">-->
@@ -124,7 +133,7 @@
<div v-else>{{ !isBuy ? 'فروش' : 'افزودن به سبد خرید' }}</div>
</div>
</BasicButton>
<!-- <div class=" w-full !z-100 px-6 rounded-xl py-1 border-1 bg-white mt-2">
<!-- <div class=" w-full !z-100 px-6 rounded-2xl py-1 border-1 bg-white mt-2">
&lt;!&ndash; <BasicInfoItem is-line title="کارمزد" :value="parseFloat(((form.goldValue?? 0)*0.005).toFixed(3))" />&ndash;&gt;
&lt;!&ndash; <BasicInfoItem is-line title="کارمزد" :value="parseFloat(((form.goldValue?? 0)*0.005).toFixed(3))" />&ndash;&gt;
<div class="flex justify-between items-center">
+7
View File
@@ -108,6 +108,9 @@
</div>
</BasicButton>
<InstallButton />
<BasicButton class="mt-2 w-full" @click="goToAboutSoftware">
<div>درباره نرم‌افزار</div>
</BasicButton>
<BasicButton
v-if="getToken() && userStore.userInfo?.finger_print"
:disabled="loading"
@@ -204,6 +207,10 @@ async function enterFingerPrint() {
}
}
function goToAboutSoftware() {
router.push({ name: 'aboutSoftware' })
}
function useStyle() {
if (isSMS.value) return 'right: 2%;'
// if (isDeposit.value) return 'animation-direction: normal'
+7
View File
@@ -37,6 +37,9 @@
<div v-else>ثبت نام</div>
</div>
</BasicButton>
<BasicButton class="mt-2 w-full" @click="goToAboutSoftware">
<div>درباره نرم‌افزار</div>
</BasicButton>
<div class="flex gap-1 justify-center mt-5 text-xs" @click="$emit('register',false)">اگر حساب کاربری دارید
<div class="font-bold text-primary">از این قسمت</div>
وارد شوید.
@@ -188,6 +191,10 @@ function registerAccount() {
})
}
function goToAboutSoftware() {
router.push({ name: 'aboutSoftware' })
}
</script>