1405/04/27

-
This commit is contained in:
2026-07-18 11:30:40 +03:30
parent bc80ddc1f1
commit c2a355f2de
22 changed files with 1185 additions and 229 deletions
+60
View File
@@ -2,8 +2,12 @@ export interface AboutSection {
title?: string
paragraphs?: string[]
items?: string[]
/** متن HTML از Setting — در صورت وجود، اولویت با این است */
html?: string
}
const ABOUT_SETTING_KEYS = ['text_about'] as const
export function getAboutSections(projectName: string): AboutSection[] {
return [
{
@@ -11,3 +15,59 @@ export function getAboutSections(projectName: string): AboutSection[] {
},
]
}
function looksLikeHtml(value: string) {
return /<\/?[a-z][\s\S]*>/i.test(value)
}
function parseAboutSettingValue(raw: string): AboutSection[] | null {
const value = raw.trim()
if (!value) return null
try {
const decoded = JSON.parse(value)
if (Array.isArray(decoded) && decoded.length > 0) {
return decoded as AboutSection[]
}
if (decoded && typeof decoded === 'object' && typeof decoded.html === 'string') {
const html = decoded.html.trim()
if (html) return [{ html }]
}
if (decoded && typeof decoded === 'object' && typeof decoded.text === 'string') {
return parseAboutSettingValue(decoded.text)
}
} catch {
// plain text / html
}
if (looksLikeHtml(value)) {
return [{ html: value }]
}
const paragraphs = value
.split(/\r?\n+/)
.map((line) => line.trim())
.filter(Boolean)
return [{ paragraphs: paragraphs.length > 0 ? paragraphs : [value] }]
}
export function getAboutSettingText(
getSettingByKey: (key: string) => unknown,
): string {
for (const key of ABOUT_SETTING_KEYS) {
const value = String(getSettingByKey(key) ?? '').trim()
if (value) return value
}
return ''
}
/** اولویت با Setting؛ در صورت خالی بودن، متن پیش‌فرض */
export function resolveAboutSections(
projectName: string,
settingsText?: string | null,
): AboutSection[] {
const fromSettings = parseAboutSettingValue(String(settingsText ?? ''))
if (fromSettings?.length) return fromSettings
return getAboutSections(projectName)
}
@@ -8,13 +8,41 @@
v-if="sectionIndex > 0"
:class="variant === 'desktop' ? 'dabout-content__divider' : 'w-full h-1px my-3 bg-tertiary'"
/>
<div
v-for="(paragraph, paragraphIndex) in section.paragraphs ?? []"
:key="`p-${sectionIndex}-${paragraphIndex}`"
:class="[variant === 'desktop' ? 'dabout-content__paragraph' : '', paragraphIndex > 0 ? 'mt-3' : '']"
>
{{ paragraph }}
</div>
v-if="section.html"
class="about-sections-html"
:class="variant === 'desktop' ? 'dabout-content__paragraph' : ''"
v-html="section.html"
/>
<template v-else>
<div
v-if="section.title"
:class="variant === 'desktop' ? 'dabout-content__title' : 'font-bold text-sm mb-2'"
>
{{ section.title }}
</div>
<div
v-for="(paragraph, paragraphIndex) in section.paragraphs ?? []"
:key="`p-${sectionIndex}-${paragraphIndex}`"
:class="[variant === 'desktop' ? 'dabout-content__paragraph' : '', paragraphIndex > 0 ? 'mt-3' : '']"
>
{{ paragraph }}
</div>
<ul
v-if="section.items?.length"
:class="variant === 'desktop' ? 'dabout-content__list' : 'mt-2 pr-4 list-disc'"
>
<li
v-for="(item, itemIndex) in section.items"
:key="`i-${sectionIndex}-${itemIndex}`"
:class="variant === 'desktop' ? 'dabout-content__list-item' : 'mt-2'"
>
{{ item }}
</li>
</ul>
</template>
</template>
</div>
</template>
@@ -26,3 +54,21 @@ withDefaults(defineProps<{ variant?: 'mobile' | 'desktop' }>(), { variant: 'mobi
const { aboutSections } = useAboutPageContext()
</script>
<style scoped lang="less">
.about-sections-html {
:deep(p + p) {
margin-top: 0.75rem;
}
:deep(ul) {
margin: 0.5rem 0 0;
padding-right: 1.1rem;
list-style: disc;
}
:deep(li + li) {
margin-top: 0.35rem;
}
}
</style>
@@ -1,23 +1,18 @@
<template>
<div :class="variant === 'desktop' ? 'dabout-support' : 'wrap-fix bottom-0 px-10 py-6 flex gap-2 bg-white items-center'">
<a class="w-full" :href="supportHref" target="_blank" rel="noopener noreferrer">
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
</a>
<a :href="supportHref" target="_blank" rel="noopener noreferrer">
<img
src="@/assets/img/rubika.webp"
alt=""
:class="variant === 'desktop' ? 'dabout-support__bale' : 'w-12'"
/>
</a>
</div>
<SupportContactBar
:variant="variant"
mobile-root-class="wrap-fix mobile-info-page__support flex gap-2"
desktop-root-class="dabout-support"
/>
</template>
<script lang="ts" setup>
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import { useAboutPageContext } from '../composables/useAboutPage'
import SupportContactBar from '@/components/SupportContactBar.vue'
withDefaults(defineProps<{ variant?: 'mobile' | 'desktop' }>(), { variant: 'mobile' })
const { supportHref } = useAboutPageContext()
withDefaults(
defineProps<{
variant?: 'mobile' | 'desktop'
}>(),
{ variant: 'mobile' },
)
</script>
@@ -1,23 +1,34 @@
import { computed, inject, provide, type InjectionKey, type ComputedRef } from 'vue'
import { computed, inject, onMounted, provide, type InjectionKey, type ComputedRef } from 'vue'
import { getConfig } from '@/config/app-config'
import { getAboutSections, type AboutSection } from '../aboutContent'
import { useSettingsStore } from '@/store/settingsStore'
import {
getAboutSettingText,
resolveAboutSections,
type AboutSection,
} from '../aboutContent'
export interface AboutPageContext {
projectName: ComputedRef<string>
aboutSections: ComputedRef<AboutSection[]>
supportHref: ComputedRef<string>
}
export const ABOUT_PAGE_CONTEXT_KEY: InjectionKey<AboutPageContext> = Symbol('aboutPageContext')
const SUPPORT_LINK = 'https://web.rubika.ir/#c=u0I8F1U0a939b0df5ba9f73618d33d7c'
export function useAboutPage() {
const settingsStore = useSettingsStore()
const projectName = computed(() => getConfig('name_fa', 'کشکول گلد'))
const aboutSections = computed(() => getAboutSections(projectName.value))
const supportHref = computed(() => SUPPORT_LINK)
const context: AboutPageContext = { projectName, aboutSections, supportHref }
const aboutSections = computed(() => {
void settingsStore.settings
const fromSettings = getAboutSettingText((key) => settingsStore.getSettingByKey(key))
return resolveAboutSections(projectName.value, fromSettings)
})
onMounted(() => {
void settingsStore.fetchSettings()
})
const context: AboutPageContext = { projectName, aboutSections }
provide(ABOUT_PAGE_CONTEXT_KEY, context)
return context
}
@@ -205,22 +205,7 @@
}
.dabout-support {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
> a:first-child {
flex: 1;
min-width: 0;
}
&__bale {
width: 40px;
height: 40px;
object-fit: contain;
flex-shrink: 0;
}
width: 100%;
}
@media (max-width: 1023.98px) {
@@ -11,13 +11,27 @@
:key="`block-${blockIndex}`"
:class="variant === 'desktop' ? 'drules-content__block' : 'rules-block'"
>
<p
v-for="(rule, index) in block.items"
:key="`rule-${blockIndex}-${index}`"
<div
v-if="block.html"
class="rules-html"
:class="variant === 'desktop' ? 'drules-content__paragraph' : 'rules-paragraph'"
>
{{ rule }}
</p>
v-html="block.html"
/>
<template v-else>
<div
v-if="block.title"
:class="variant === 'desktop' ? 'drules-content__title' : 'font-bold text-sm text-primary'"
>
{{ block.title }}
</div>
<p
v-for="(rule, index) in block.items"
:key="`rule-${blockIndex}-${index}`"
:class="variant === 'desktop' ? 'drules-content__paragraph' : 'rules-paragraph'"
>
{{ rule }}
</p>
</template>
</div>
</div>
</template>
@@ -40,4 +54,18 @@ const { rulesTitle, rulesBlocks } = useRulesPageContext()
text-align: right;
line-height: 1.9;
}
.rules-html :deep(p + p) {
margin-top: 0.75rem;
}
.rules-html :deep(ul) {
margin: 0.5rem 0 0;
padding-right: 1.1rem;
list-style: disc;
}
.rules-html :deep(li + li) {
margin-top: 0.35rem;
}
</style>
@@ -1,19 +1,18 @@
<template>
<div :class="variant === 'desktop' ? 'drules-support' : 'wrap-fix bottom-0 px-10 py-6 flex gap-2 bg-white items-center'">
<a class="w-full" :href="supportHref" target="_blank" rel="noopener noreferrer">
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
</a>
<a :href="supportHref" target="_blank" rel="noopener noreferrer">
<img src="@/assets/img/rubika.webp" alt="" :class="variant === 'desktop' ? 'drules-support__bale' : 'w-12'" />
</a>
</div>
<SupportContactBar
:variant="variant"
mobile-root-class="wrap-fix mobile-info-page__support flex gap-2"
desktop-root-class="drules-support"
/>
</template>
<script lang="ts" setup>
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import { useRulesPageContext } from '../composables/useRulesPage'
import SupportContactBar from '@/components/SupportContactBar.vue'
withDefaults(defineProps<{ variant?: 'mobile' | 'desktop' }>(), { variant: 'mobile' })
const { supportHref } = useRulesPageContext()
withDefaults(
defineProps<{
variant?: 'mobile' | 'desktop'
}>(),
{ variant: 'mobile' },
)
</script>
@@ -1,29 +1,40 @@
import { computed, inject, provide, type ComputedRef, type InjectionKey } from 'vue'
import { computed, inject, onMounted, provide, type ComputedRef, type InjectionKey } from 'vue'
import { getConfig } from '@/config/app-config'
import { getRulesBlocks, getRulesTitle, type RulesBlock } from '../rulesContent'
import { useSettingsStore } from '@/store/settingsStore'
import {
getRulesSettingText,
getRulesTitle,
resolveRulesBlocks,
type RulesBlock,
} from '../rulesContent'
export interface RulesPageContext {
nameProject: ComputedRef<string>
rulesTitle: ComputedRef<string>
rulesBlocks: ComputedRef<RulesBlock[]>
supportHref: ComputedRef<string>
}
export const RULES_PAGE_CONTEXT_KEY: InjectionKey<RulesPageContext> = Symbol('rulesPageContext')
const SUPPORT_LINK = 'https://web.rubika.ir/#c=u0I8F1U0a939b0df5ba9f73618d33d7c'
export function useRulesPage() {
const settingsStore = useSettingsStore()
const nameProject = computed(() => getConfig('name_fa', 'کشکول گلد'))
const rulesTitle = computed(() => getRulesTitle(nameProject.value, false))
const rulesBlocks = computed(() => getRulesBlocks(nameProject.value, false))
const supportHref = computed(() => SUPPORT_LINK)
const rulesBlocks = computed(() => {
void settingsStore.settings
const fromSettings = getRulesSettingText((key) => settingsStore.getSettingByKey(key))
return resolveRulesBlocks(nameProject.value, fromSettings)
})
onMounted(() => {
void settingsStore.fetchSettings()
})
const context: RulesPageContext = {
nameProject,
rulesTitle,
rulesBlocks,
supportHref,
}
provide(RULES_PAGE_CONTEXT_KEY, context)
return context
@@ -127,22 +127,7 @@
}
.drules-support {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
> a:first-child {
flex: 1;
min-width: 0;
}
&__bale {
width: 40px;
height: 40px;
object-fit: contain;
flex-shrink: 0;
}
width: 100%;
}
@media (max-width: 1023.98px) {
@@ -16,14 +16,9 @@
<div class=" font-bold text-sm text-primary text-center">جواز کسب و کار</div>
<!-- <img :src="permission1" alt="" class="w-full">-->
<div class="mt-5 text-xs"></div>
<div class="wrap-fix bottom-0 px-2 py-6 flex gap-2 bg-white">
<a class="w-full" href="tel:09128081392">
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
</a>
<a href="https://t.me/kashkol">
<img src="@/assets/img/telegram.png" alt="" class="h-10 w-10"/>
</a>
</div>
<SupportContactBar
mobile-root-class="wrap-fix bottom-0 px-2 py-6 flex gap-2 bg-white"
/>
</div>
</div>
@@ -39,6 +34,7 @@ import { onMounted, reactive, ref } from 'vue'
import img from '@/assets/img/user_ly1.svg'
// import gr_bottom from './assets/img/gr_bottom_white.svg'
import { loading } from '@/utils'
import SupportContactBar from '@/components/SupportContactBar.vue'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
import { lengthValidation, required } from '@/utils/inputRules'
+60
View File
@@ -1,8 +1,12 @@
export interface RulesBlock {
title?: string
items: string[]
/** متن HTML از Setting — در صورت وجود، اولویت با این است */
html?: string
}
const RULES_SETTING_KEYS = ['text_rule'] as const
function resolveProjectName(nameProject?: string | null) {
return (nameProject ?? '').trim() || 'کشکول گلد'
}
@@ -47,3 +51,59 @@ export function getRulesTitle(nameProject: string, _isBoursecu = false) {
export function getRulesBlocks(nameProject: string, _isBoursecu = false): RulesBlock[] {
return [{ items: getDefaultRulesItems(nameProject) }]
}
function looksLikeHtml(value: string) {
return /<\/?[a-z][\s\S]*>/i.test(value)
}
function parseRulesSettingValue(raw: string): RulesBlock[] | null {
const value = raw.trim()
if (!value) return null
try {
const decoded = JSON.parse(value)
if (Array.isArray(decoded) && decoded.length > 0) {
return decoded as RulesBlock[]
}
if (decoded && typeof decoded === 'object' && typeof decoded.html === 'string') {
const html = decoded.html.trim()
if (html) return [{ items: [], html }]
}
if (decoded && typeof decoded === 'object' && typeof decoded.text === 'string') {
return parseRulesSettingValue(decoded.text)
}
} catch {
// plain text / html
}
if (looksLikeHtml(value)) {
return [{ items: [], html: value }]
}
const items = value
.split(/\r?\n+/)
.map((line) => line.trim())
.filter(Boolean)
return [{ items: items.length > 0 ? items : [value] }]
}
export function getRulesSettingText(
getSettingByKey: (key: string) => unknown,
): string {
for (const key of RULES_SETTING_KEYS) {
const value = String(getSettingByKey(key) ?? '').trim()
if (value) return value
}
return ''
}
/** اولویت با Setting؛ در صورت خالی بودن، متن پیش‌فرض */
export function resolveRulesBlocks(
nameProject: string,
settingsText?: string | null,
): RulesBlock[] {
const fromSettings = parseRulesSettingValue(String(settingsText ?? ''))
if (fromSettings?.length) return fromSettings
return getRulesBlocks(nameProject, false)
}
+26 -5
View File
@@ -65,12 +65,14 @@
<div class="text-center">
<div class="font-bold text-red-600">مشتریان گرامی</div>
<div class="mt-2">
<span>جهت شارژ حساب کاربری خود، کلمه "شماره حساب" را برای پشتیبانی ارسال نمایید.</span>
<span>{{ supportMessengerText }}</span>
</div>
<BasicButton class="mt-4">
<a class="w-full" href="https://web.rubika.ir/#c=u0I8F1U0a939b0df5ba9f73618d33d7c">
پیام به پشتیبانی
</a>
<BasicButton
v-if="hasMessengerLink"
class="mt-4"
@click="onDepositSupportClick"
>
پیام به پشتیبانی
</BasicButton>
</div>
<template #footer>
@@ -90,11 +92,14 @@ import OtpInput from 'vue3-otp-input'
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
import ResponsiveViewPair from '@/layouts/components/ResponsiveViewPair.vue'
import { loading } from '@/utils'
import { useSupportMessenger } from '@/composables/useSupportMessenger'
import { loadSupportMessengerFromSettings } from '@/utils/supportMessenger'
import { useWalletPage } from './composables/useWalletPage'
import DesktopWalletView from './desktop/DesktopWalletView.vue'
import MobileWalletView from './mobile/MobileWalletView.vue'
import SkeletonLoading from './components/SkeletonLoading.vue'
import WalletAddCardForm from './components/WalletAddCardForm.vue'
import { watch } from 'vue'
const isDesktop = useDesktopBreakpoint()
@@ -107,6 +112,22 @@ const {
handleOnComplete,
addCard,
} = useWalletPage()
const {
hasMessengerLink,
supportMessengerText,
openDepositSupportMessenger,
} = useSupportMessenger()
watch(chargeAccountModal, (visible) => {
if (visible) {
void loadSupportMessengerFromSettings(true)
}
})
function onDepositSupportClick() {
void openDepositSupportMessenger()
}
</script>
<style scoped>