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
+1
View File
@@ -64,6 +64,7 @@ declare module 'vue' {
Size: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/size.vue')['default']
StartEnd: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/start-end.vue')['default']
Suffix: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/suffix.vue')['default']
SupportContactBar: typeof import('./src/components/SupportContactBar.vue')['default']
SupportMessengerBrandIcon: typeof import('./src/components/SupportMessengerBrandIcon.vue')['default']
SupportMessengerPicker: typeof import('./src/components/SupportMessengerPicker.vue')['default']
Switchable: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/switchable.vue')['default']
+20 -44
View File
@@ -67,8 +67,8 @@
<SupportMessengerPicker
v-model:open="supportPickerOpen"
:entries="supportMessengerEntries"
:anchor-el="supportButtonEl"
@select="openSupportMessengerEntry"
:anchor-el="pickerAnchorEl"
@select="openMessengerEntry"
/>
<VersionUpdateModal />
@@ -84,13 +84,8 @@ import img_topGradiant from '@/views/login/assets/img/gr_top_white.svg'
import img_pattern from '@/views/login/assets/img/pattern.svg'
import { LoadingOutlined } from '@ant-design/icons-vue'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import {
loadSupportMessengerFromSettings,
openSupportMessengerEntry,
resolveSupportMessengerEntries,
supportMessengerPickerOpen,
supportMessengerSettings,
} from '@/utils/supportMessenger'
import { useSupportMessenger } from '@/composables/useSupportMessenger'
import { loadSupportMessengerFromSettings } from '@/utils/supportMessenger'
import SupportMessengerPicker from '@/components/SupportMessengerPicker.vue'
import { useDraggableFloatingButton } from '@/utils/useDraggableFloatingButton'
import { useApplyDesktopLayoutClass } from '@/layouts/composables/useAppLayout'
@@ -113,42 +108,27 @@ const showSplash = ref(true)
useApplyDesktopLayoutClass()
const {
supportPickerOpen,
supportPickerAnchor,
supportMessengerEntries,
messengerMeta,
hasMessengerLink: showSupportButton,
openMessengerEntry,
openSupportMessenger,
} = useSupportMessenger()
const supportButtonEl = ref<HTMLElement | null>(null)
const supportPickerOpen = supportMessengerPickerOpen
const pickerAnchorEl = computed(() => supportPickerAnchor.value ?? supportButtonEl.value)
const supportMessengerEntries = computed(() =>
resolveSupportMessengerEntries(supportMessengerSettings.value),
)
const showSupportButton = computed(() => supportMessengerEntries.value.length > 0)
const primaryEntry = computed(() => supportMessengerEntries.value[0] ?? null)
const supportIcon = computed(() => primaryEntry.value?.icon ?? 'vuesax-linear:messages-2')
const supportIcon = computed(() => messengerMeta.value.icon)
const supportAccentColor = computed(() =>
(primaryEntry.value?.accent ?? colors.primary) || '#334155',
(messengerMeta.value.accent !== '#334155'
? messengerMeta.value.accent
: colors.primary) || '#334155',
)
let supportPickerOpenedAt = 0
function openSupportLink() {
const entries = supportMessengerEntries.value
if (entries.length === 0) return
if (entries.length === 1) {
void openSupportMessengerEntry(entries[0])
return
}
const now = Date.now()
if (supportPickerOpen.value) {
if (now - supportPickerOpenedAt < 450) return
supportPickerOpen.value = false
return
}
supportPickerOpen.value = true
supportPickerOpenedAt = now
}
const {
dragging: supportDragging,
fabStyle: supportButtonStyle,
@@ -163,15 +143,11 @@ const {
defaultRightOffset: 16,
})
function openSupportLinkHandler() {
openSupportLink()
}
function handleSupportPointerUp(event: PointerEvent) {
const clicked = supportPointerUp(event)
if (!clicked) return
event.stopPropagation()
openSupportLinkHandler()
void openSupportMessenger(supportButtonEl.value)
}
onMounted(async () => {
+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
<circle cx="128" cy="128" r="124" fill="#12B76A"/>
<!-- Bootstrap Icons telephone-fill — گوشی کامل -->
<g transform="translate(48 48) scale(10)">
<path
fill="#fff"
d="M1.885.511a1.745 1.745 0 0 1 2.612.163L6.29 2.98c.329.423.445.967.315 1.49l-.547 2.19a.678.678 0 0 0 .178.643l2.457 2.457a.678.678 0 0 0 .644.178l2.189-.547a1.745 1.745 0 0 1 1.49.315l2.306 1.794c.829.645.905 1.87.163 2.611l-1.034 1.034c-.74.74-1.846 1.065-2.877.702a18.634 18.634 0 0 1-7.01-4.42 18.634 18.634 0 0 1-4.42-7.009c-.362-1.03-.038-2.137.703-2.877L1.885.511z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 680 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" fill="none">
<circle cx="128" cy="128" r="128" fill="#3B82F6"/>
<path
fill="#fff"
d="M78 76h100c11 0 20 9 20 20v64c0 11-9 20-20 20h-52l-30 26v-26H78c-11 0-20-9-20-20V96c0-11 9-20 20-20zm16 40h68v14H94v-14zm0 28h48v14H94v-14z"
/>
</svg>

After

Width:  |  Height:  |  Size: 337 B

+89
View File
@@ -0,0 +1,89 @@
<template>
<div :class="variant === 'desktop' ? desktopRootClass : mobileRootClass">
<button
ref="triggerEl"
type="button"
class="support-contact-bar__btn"
:disabled="!canContactSupport"
@click="onSupportClick"
>
<BasicIcon icon="vuesax-linear:messages-2" size="20" color="#fff" />
<span>ارتباط با پشتیبانی</span>
</button>
</div>
</template>
<script lang="ts" setup>
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import { useSupportMessenger } from '@/composables/useSupportMessenger'
import { computed, ref } from 'vue'
withDefaults(
defineProps<{
variant?: 'mobile' | 'desktop'
mobileRootClass?: string
desktopRootClass?: string
}>(),
{
variant: 'mobile',
mobileRootClass: 'wrap-fix bottom-0 px-10 pb-3rem flex bg-white',
desktopRootClass: 'dpage-support',
},
)
const {
supportPhoneLink,
hasMessengerLink,
hasSupportPhoneLink,
openSupportMessenger,
} = useSupportMessenger()
const triggerEl = ref<HTMLElement | null>(null)
const canContactSupport = computed(() => hasMessengerLink.value || hasSupportPhoneLink.value)
async function onSupportClick() {
// همان رفتار دکمه شناور پشتیبانی — بدون پیش‌نویس واریز
if (hasMessengerLink.value) {
await openSupportMessenger(triggerEl.value)
return
}
if (hasSupportPhoneLink.value && supportPhoneLink.value) {
window.location.href = supportPhoneLink.value
}
}
</script>
<style scoped lang="less">
.support-contact-bar__btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
width: 100%;
min-height: 2.75rem;
padding: 0.65rem 1rem;
border: 0;
border-radius: 0.75rem;
background: var(--App-primary, var(--ant-color-primary, #7f1d1d));
color: #fff;
font-size: 0.9375rem;
font-weight: 600;
cursor: pointer;
transition: opacity 0.15s ease, transform 0.15s ease;
&:active {
transform: scale(0.98);
}
&:disabled {
opacity: 0.55;
cursor: not-allowed;
}
}
.dpage-support {
width: 100%;
}
</style>
@@ -21,8 +21,10 @@
<script lang="ts" setup>
import { computed } from 'vue'
import baleLogo from '@/assets/messengers/bale.png'
import callLogo from '@/assets/messengers/call.svg'
import eitaaLogo from '@/assets/messengers/eitaa.png'
import rubikaLogo from '@/assets/messengers/rubika.png'
import smsLogo from '@/assets/messengers/sms.svg'
import soroushLogo from '@/assets/messengers/soroush.svg'
import telegramLogo from '@/assets/messengers/telegram.svg'
import whatsappLogo from '@/assets/messengers/whatsapp.svg'
@@ -34,6 +36,8 @@ const LOGOS: Record<string, string> = {
rubika: rubikaLogo,
eitaa: eitaaLogo,
soroush: soroushLogo,
call: callLogo,
sms: smsLogo,
}
const props = defineProps<{ messenger: string }>()
+250
View File
@@ -0,0 +1,250 @@
import { computed, ref } from 'vue'
import { message } from 'ant-design-vue'
import {
buildDepositSmsUrl,
buildSupportPhoneUrl,
loadSupportMessengerFromSettings,
messengerNeedsClipboardCopy,
openSupportMessengerEntry,
openSupportMessengerWithMessage,
resolveDepositGuideText,
resolveDepositSupportMessage,
resolveSupportMessengerEntries,
resolveSupportMessengerLink,
resolveSupportMessengerMeta,
supportMessengerPickerAnchor,
supportMessengerPickerOpen,
supportMessengerSettings,
type ResolvedSupportMessengerEntry,
} from '@/utils/supportMessenger'
import {
copyDepositFieldValue,
displayDepositCardNumber,
displayDepositSheba,
getDepositGuideVarsFromSettings,
getDepositSettingLabel,
hasDepositFieldValue,
} from '@/utils/depositGuideText'
import { useSettingsStore } from '@/store/settingsStore'
/** plain: منوی شناور / درباره ما — بدون پیش‌متن | deposit: مودال واریز — با deposit_support_message_text */
type SupportPickerIntent = 'plain' | 'deposit'
export function useSupportMessenger() {
const settingsStore = useSettingsStore()
const supportPickerOpen = supportMessengerPickerOpen
const supportPickerIntent = ref<SupportPickerIntent>('plain')
const legacySupportLink = computed(() => '')
const depositSupportMessage = computed(() => resolveDepositSupportMessage())
/** لینک‌های تمیز بدون پیش‌متن — متن فقط موقع باز کردن (قصد deposit) اضافه می‌شود */
const supportMessengerEntries = computed(() =>
resolveSupportMessengerEntries(supportMessengerSettings.value),
)
const primaryMessengerEntry = computed(() => supportMessengerEntries.value[0] ?? null)
const messengerType = computed(
() => (primaryMessengerEntry.value?.messenger ?? supportMessengerSettings.value.messenger ?? '').trim().toLowerCase(),
)
const messengerMeta = computed(() =>
primaryMessengerEntry.value
? {
label: primaryMessengerEntry.value.label,
icon: primaryMessengerEntry.value.icon,
accent: primaryMessengerEntry.value.accent,
}
: resolveSupportMessengerMeta(messengerType.value),
)
const messengerLink = computed(() =>
primaryMessengerEntry.value?.url
|| resolveSupportMessengerLink(
supportMessengerSettings.value.messenger,
supportMessengerSettings.value.messengerId,
legacySupportLink.value,
),
)
async function openMessengerEntry(entry: ResolvedSupportMessengerEntry, text?: string) {
// فقط وقتی از مودال واریز آمده‌ایم پیش‌نویس می‌گذاریم؛ منوی شناور همیشه خالی
const messageText =
text !== undefined
? text.trim()
: supportPickerIntent.value === 'deposit'
? depositSupportMessage.value.trim()
: ''
const opened = await openSupportMessengerEntry(entry, messageText, legacySupportLink.value)
if (!opened) return false
if (messageText && messengerNeedsClipboardCopy(entry.messenger)) {
message.success('متن پیام کپی شد — در پیام‌رسان paste کنید')
}
return true
}
let supportPickerOpenedAt = 0
async function openSupportPicker(
intent: SupportPickerIntent,
anchorEl?: HTMLElement | null,
) {
supportPickerIntent.value = intent
if (anchorEl !== undefined) {
supportMessengerPickerAnchor.value = anchorEl
}
const draft =
intent === 'deposit' ? depositSupportMessage.value.trim() : ''
const entries = supportMessengerEntries.value
if (entries.length === 0) {
return openSupportMessengerWithMessage(
supportMessengerSettings.value.messenger,
supportMessengerSettings.value.messengerId,
draft,
legacySupportLink.value,
)
}
if (entries.length === 1) {
return openMessengerEntry(entries[0], draft)
}
const now = Date.now()
if (supportPickerOpen.value) {
if (now - supportPickerOpenedAt < 450) return true
supportPickerOpen.value = false
return true
}
supportPickerOpen.value = true
supportPickerOpenedAt = now
return true
}
/** منوی شناور / صفحات درباره ما — بدون پیش‌نویس پیام */
async function openSupportMessenger(anchorEl?: HTMLElement | null) {
return openSupportPicker('plain', anchorEl)
}
/** مودال راهنمای واریز — با متن deposit_support_message_text */
async function openDepositSupportMessenger(anchorEl?: HTMLElement | null) {
return openSupportPicker('deposit', anchorEl)
}
function handleSupportMessengerClick(event?: Event) {
const target = event?.currentTarget
const anchor = target instanceof HTMLElement ? target : null
void openSupportMessenger(anchor)
}
const depositSmsLink = computed(() =>
buildDepositSmsUrl(
supportMessengerSettings.value.depositSmsSupportNumber,
depositSupportMessage.value,
),
)
const supportPhoneLink = computed(() => {
const fromSettings = buildSupportPhoneUrl(
supportMessengerSettings.value.depositSmsSupportNumber,
)
if (fromSettings) return fromSettings
const legacy = legacySupportLink.value
if (legacy.startsWith('tel:') && legacy.length > 4) return legacy
return ''
})
const supportMessengerText = computed(() => resolveDepositGuideText())
const depositCardNumberRaw = computed(() => {
void settingsStore.settings
const fromMessenger = supportMessengerSettings.value.depositCardNumber?.trim()
if (fromMessenger) return fromMessenger
return String(getDepositGuideVarsFromSettings().cardNumber ?? '').trim()
})
const depositShebaRaw = computed(() => {
void settingsStore.settings
const fromMessenger = supportMessengerSettings.value.depositSheba?.trim()
if (fromMessenger) return fromMessenger
return String(getDepositGuideVarsFromSettings().sheba ?? '').trim()
})
const depositCardNumber = computed(() => displayDepositCardNumber(depositCardNumberRaw.value))
const depositSheba = computed(() => displayDepositSheba(depositShebaRaw.value))
const depositCardLabel = computed(() => {
void settingsStore.settings
return getDepositSettingLabel('deposit_card_number', 'شماره کارت واریز')
})
const depositShebaLabel = computed(() => {
void settingsStore.settings
return getDepositSettingLabel('deposit_sheba', 'شبا واریز')
})
const hasDepositCardValue = computed(() => hasDepositFieldValue(depositCardNumberRaw.value))
const hasDepositShebaValue = computed(() => hasDepositFieldValue(depositShebaRaw.value))
async function copyDepositCardNumber() {
await copyDepositFieldValue(
depositCardNumberRaw.value,
'card',
'شماره کارت ثبت نشده است',
)
}
async function copyDepositShebaNumber() {
await copyDepositFieldValue(
depositShebaRaw.value,
'sheba',
'شماره شبا ثبت نشده است',
)
}
const hasMessengerLink = computed(() => supportMessengerEntries.value.length > 0 || !!messengerLink.value)
const hasMultipleMessengers = computed(() => supportMessengerEntries.value.length > 1)
const hasSupportPhoneLink = computed(() => !!supportPhoneLink.value)
const hasSupportActions = computed(() => hasSupportPhoneLink.value || hasMessengerLink.value)
const opensInNewTab = computed(() => /^https?:\/\//i.test(messengerLink.value))
return {
supportPickerOpen,
supportPickerAnchor: supportMessengerPickerAnchor,
supportMessengerEntries,
messengerType,
messengerMeta,
messengerLink,
supportPhoneLink,
depositSmsLink,
supportMessengerText,
depositSupportMessage,
depositCardNumber,
depositSheba,
depositCardNumberRaw,
depositShebaRaw,
depositCardLabel,
depositShebaLabel,
hasDepositCardValue,
hasDepositShebaValue,
copyDepositCardNumber,
copyDepositShebaNumber,
openSupportMessenger,
openDepositSupportMessenger,
openMessengerEntry,
handleSupportMessengerClick,
hasMessengerLink,
hasMultipleMessengers,
hasSupportPhoneLink,
hasSupportActions,
opensInNewTab,
loadSupportMessengerFromSettings,
}
}
+24 -1
View File
@@ -22,6 +22,28 @@ export const useSettingsStore = defineStore('settings', () => {
return item?.value ?? null
}
function mergeSettings(items?: AppSetting[] | null) {
if (!items?.length) {
return
}
const merged = [...settings.value]
items.forEach((item) => {
if (!item?.key) {
return
}
const index = merged.findIndex((setting) => setting.key === item.key)
if (index >= 0) {
merged[index] = { ...merged[index], ...item }
} else {
merged.push(item)
}
})
settings.value = merged
}
async function fetchSettings(force = false) {
if (!force && fetchPromise) {
return fetchPromise
@@ -31,7 +53,7 @@ export const useSettingsStore = defineStore('settings', () => {
try {
const data = await getSettings()
if (Array.isArray(data)) {
settings.value = data
mergeSettings(data)
}
} catch (error) {
console.log(error)
@@ -49,6 +71,7 @@ export const useSettingsStore = defineStore('settings', () => {
settings,
loaded,
fetchSettings,
mergeSettings,
getSettingByKey,
}
})
+202
View File
@@ -0,0 +1,202 @@
import { message } from 'ant-design-vue'
import { useSettingsStore } from '@/store/settingsStore'
import { useUserStore } from '@/store/userStore'
export interface DepositGuideVars {
accountHolder?: string | null
cardNumber?: string | null
sheba?: string | null
/** نام مشتری واردشده — برای پیام پشتیبانی واریز */
customerName?: string | null
}
export const DEFAULT_DEPOSIT_GUIDE_TEXT =
'جهت شارژ حساب کاربری خود، کلمه «شماره حساب» را برای پشتیبانی ارسال نمایید.'
export const DEFAULT_DEPOSIT_SUPPORT_MESSAGE_TEXT =
'باسلام، %نام هستم. کدپیگیری و مبلغ واریزی من:'
/** الگوی متغیر نام — ٪نام / %نام / % نام / %نام% */
const NAME_PLACEHOLDER_RE = /[%٪]\s*نام(?:\s*[%٪])?/gu
const CARD_PLACEHOLDER_RE = /[%٪]\s*کارت(?:\s*[%٪])?/gu
const SHEBA_PLACEHOLDER_RE = /[%٪]\s*شبا(?:\s*[%٪])?/gu
export function getCustomerNameFromUser(): string {
try {
const user = useUserStore().userInfo
const fullName = [user?.fname, user?.lname].filter(Boolean).join(' ').trim()
return fullName
} catch {
return ''
}
}
/** نام نمایشی در پیام پشتیبانی: اول مشتری، بعد نام صاحب حساب تنظیمات */
function resolveSupportMessageName(vars: DepositGuideVars): string {
const customerName = String(vars.customerName ?? '').trim()
if (customerName) return customerName
return String(vars.accountHolder ?? '').trim()
}
/** فقط %نام — برای پیام SMS/پیام‌رسان پشتیبانی (نه کارت/شبا) */
export function renderDepositSupportMessageText(
template: string,
vars: DepositGuideVars,
): string {
const name = resolveSupportMessageName(vars)
return template.replace(NAME_PLACEHOLDER_RE, name)
}
/** جایگزینی متغیرها در متن راهنمای واریز — ٪نام / %نام، ٪کارت / %کارت، ٪شبا / %شبا */
export function renderDepositGuideText(
template: string,
vars: DepositGuideVars,
): string {
const accountHolder = String(vars.accountHolder ?? '').trim()
const cardNumber = String(vars.cardNumber ?? '').trim()
const sheba = String(vars.sheba ?? '').trim()
return template
.replace(SHEBA_PLACEHOLDER_RE, sheba)
.replace(CARD_PLACEHOLDER_RE, cardNumber)
.replace(NAME_PLACEHOLDER_RE, accountHolder)
}
export function getDepositGuideVarsFromSettings(): DepositGuideVars {
const customerName = getCustomerNameFromUser()
try {
const store = useSettingsStore()
return {
accountHolder: store.getSettingByKey('deposit_account_holder') as string | null,
cardNumber: store.getSettingByKey('deposit_card_number') as string | null,
sheba: store.getSettingByKey('deposit_sheba') as string | null,
customerName,
}
} catch {
return { customerName }
}
}
export function getDepositGuideTemplate(): string {
try {
const fromSettings = String(useSettingsStore().getSettingByKey('support_messenger_text') ?? '').trim()
if (fromSettings) return fromSettings
} catch {
// settings store may not be ready yet
}
return DEFAULT_DEPOSIT_GUIDE_TEXT
}
export function getDepositSupportMessageTemplate(): string {
try {
const fromSettings = String(
useSettingsStore().getSettingByKey('deposit_support_message_text') ?? '',
).trim()
if (fromSettings) return fromSettings
} catch {
// settings store may not be ready yet
}
return DEFAULT_DEPOSIT_SUPPORT_MESSAGE_TEXT
}
export function getRenderedDepositGuideText(vars?: DepositGuideVars): string {
return renderDepositGuideText(getDepositGuideTemplate(), vars ?? getDepositGuideVarsFromSettings())
}
export function getRenderedDepositSupportMessage(vars?: DepositGuideVars): string {
return renderDepositSupportMessageText(
getDepositSupportMessageTemplate(),
vars ?? getDepositGuideVarsFromSettings(),
)
}
export function formatDepositCardNumber(value?: string | null) {
const digits = String(value ?? '').replace(/\D/g, '')
if (!digits) return ''
return digits.replace(/(\d{4})(?=\d)/g, '$1 ').trim()
}
export function formatDepositSheba(value?: string | null) {
const normalized = String(value ?? '').replace(/\s/g, '').toUpperCase()
if (!normalized) return ''
const digits = normalized.replace(/^IR/i, '').replace(/\D/g, '')
if (!digits) return normalized
const full = `IR${digits}`
return full.replace(/(.{4})/g, '$1 ').trim()
}
export function hasDepositFieldValue(value?: string | null) {
return Boolean(String(value ?? '').trim())
}
export function displayDepositCardNumber(value?: string | null) {
return formatDepositCardNumber(value) || '**** **** **** ****'
}
export function displayDepositSheba(value?: string | null) {
return formatDepositSheba(value) || 'IR** **** **** **** **** **** **'
}
export function getDepositSettingLabel(key: string, fallback: string) {
try {
const item = useSettingsStore().settings.find(
(setting) => String(setting?.key ?? '') === key,
)
return item?.title_fa?.trim() || fallback
} catch {
return fallback
}
}
function normalizeDepositCopyValue(value: string, kind: 'card' | 'sheba') {
const normalized = value.replace(/\s/g, '')
if (!normalized) return ''
if (kind === 'sheba') {
const upper = normalized.toUpperCase()
if (upper.startsWith('IR')) return upper
const digits = upper.replace(/\D/g, '')
return digits ? `IR${digits}` : upper
}
return normalized.replace(/\D/g, '')
}
export async function copyDepositFieldValue(
value: string | null | undefined,
kind: 'card' | 'sheba',
emptyMessage: string,
) {
const raw = String(value ?? '').trim()
const normalized = normalizeDepositCopyValue(raw, kind)
if (!normalized) {
message.warning(emptyMessage)
return
}
try {
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(normalized)
} else if (typeof document !== 'undefined') {
const textarea = document.createElement('textarea')
textarea.value = normalized
textarea.setAttribute('readonly', 'true')
textarea.style.position = 'fixed'
textarea.style.left = '-9999px'
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
} else {
throw new Error('clipboard unavailable')
}
message.info('کپی شد')
} catch {
message.error('کپی انجام نشد')
}
}
+278 -81
View File
@@ -1,5 +1,17 @@
import { ref } from 'vue'
import { getSettings } from '@/api/configuration'
import type { DepositGuideVars } from '@/utils/depositGuideText'
import {
DEFAULT_DEPOSIT_GUIDE_TEXT,
getCustomerNameFromUser,
getDepositGuideVarsFromSettings,
getDepositSupportMessageTemplate,
getRenderedDepositGuideText,
getRenderedDepositSupportMessage,
renderDepositGuideText,
renderDepositSupportMessageText,
} from '@/utils/depositGuideText'
import { useSettingsStore } from '@/store/settingsStore'
export type SupportMessengerType =
| 'bale'
@@ -8,23 +20,24 @@ export type SupportMessengerType =
| 'rubika'
| 'eitaa'
| 'soroush'
export interface SupportMessengerEntry {
messenger: string
id: string
}
| 'call'
| 'sms'
export interface SupportMessengerSettings {
messengers?: SupportMessengerEntry[]
messenger?: SupportMessengerType | string
messengerId?: string
messengerText?: string
depositSupportMessageText?: string
depositAccountHolder?: string
depositCardNumber?: string
depositSheba?: string
depositSmsSupportNumber?: string
}
export interface SupportMessengerMeta {
label: string
icon: string
accent: string
export interface SupportMessengerEntry {
messenger: string
id: string
}
export interface ResolvedSupportMessengerEntry extends SupportMessengerEntry {
@@ -34,9 +47,6 @@ export interface ResolvedSupportMessengerEntry extends SupportMessengerEntry {
url: string
}
export const supportMessengerSettings = ref<SupportMessengerSettings>({})
export const supportMessengerPickerOpen = ref(false)
const VALID_SUPPORT_MESSENGERS = new Set([
'bale',
'telegram',
@@ -44,27 +54,16 @@ const VALID_SUPPORT_MESSENGERS = new Set([
'rubika',
'eitaa',
'soroush',
'call',
'sms',
])
export const SUPPORT_MESSENGER_META: Record<string, SupportMessengerMeta> = {
bale: { label: 'بله', icon: 'vuesax-linear:messages-2', accent: '#16a34a' },
telegram: { label: 'تلگرام', icon: 'vuesax-linear:send-2', accent: '#0ea5e9' },
whatsapp: { label: 'واتساپ', icon: 'vuesax-linear:whatsapp', accent: '#22c55e' },
rubika: { label: 'روبیکا', icon: 'vuesax-linear:messages-2', accent: '#7c3aed' },
eitaa: { label: 'ایتا', icon: 'vuesax-linear:messages-2', accent: '#f59e0b' },
soroush: { label: 'سروش', icon: 'vuesax-linear:messages-2', accent: '#2563eb' },
}
const PHONE_NATIVE_MESSENGERS = new Set(['call', 'sms'])
function getSupportSettingValue(
settings: Array<{ key?: string; value?: unknown }>,
key: string,
) {
const setting = settings.find((item) => item?.key === key)
if (!setting) return ''
const value = setting.value
if (value === undefined || value === null) return ''
return String(value).trim()
}
export const supportMessengerSettings = ref<SupportMessengerSettings>({})
export const supportMessengerPickerOpen = ref(false)
/** لنگر منوی انتخاب پیام‌رسان — FAB یا دکمه داخل مودال شارژ */
export const supportMessengerPickerAnchor = ref<HTMLElement | null>(null)
export function parseSupportMessengersJson(raw?: string | null): SupportMessengerEntry[] {
const value = (raw ?? '').trim()
@@ -101,54 +100,13 @@ export function resolveSupportMessengersFromSettings(
return [{ messenger, id: legacyId }]
}
export function resolveSupportMessengerMeta(messenger?: string): SupportMessengerMeta {
const type = (messenger ?? '').trim().toLowerCase()
return SUPPORT_MESSENGER_META[type] ?? {
label: 'پشتیبانی',
icon: 'vuesax-linear:headphone',
accent: '#334155',
}
}
export function buildSupportMessengerUrl(
messenger?: string,
messengerId?: string,
) {
const id = (messengerId ?? '').trim()
if (!id) return ''
if (/^https?:\/\//i.test(id)) return id
const type = (messenger ?? '').trim().toLowerCase()
const username = id.replace(/^@/, '')
if (!type) return ''
switch (type) {
case 'bale':
if (id.includes('ble.ir')) {
return id.startsWith('http') ? id : `https://${id.replace(/^\/\//, '')}`
}
return `https://ble.ir/${username}`
case 'telegram':
return `https://t.me/${username}`
case 'whatsapp':
return `https://wa.me/${username.replace(/\D/g, '')}`
case 'rubika':
return `https://rubika.ir/${username}`
case 'eitaa':
return `https://eitaa.com/${username}`
case 'soroush':
return `https://splus.ir/splus/${username}`
default:
return ''
}
}
/** فقط موارد موجود در `support_messengers` — تماس/پیامک خودکار اضافه نمی‌شوند */
export function resolveSupportMessengerEntries(
settings?: SupportMessengerSettings,
messageText?: string,
): ResolvedSupportMessengerEntry[] {
const resolved = settings ?? supportMessengerSettings.value
const text = (messageText ?? '').trim()
const source = resolved.messengers?.length
? resolved.messengers
: resolveLegacySingleMessenger(resolved)
@@ -156,9 +114,13 @@ export function resolveSupportMessengerEntries(
return source
.map((entry) => {
const meta = resolveSupportMessengerMeta(entry.messenger)
const url = buildSupportMessengerUrl(entry.messenger, entry.id)
const url = buildSupportMessengerUrl(entry.messenger, entry.id, text)
if (!url) return null
return { ...entry, ...meta, url }
return {
...entry,
...meta,
url,
}
})
.filter((entry): entry is ResolvedSupportMessengerEntry => entry !== null)
}
@@ -171,25 +133,260 @@ function resolveLegacySingleMessenger(settings: SupportMessengerSettings): Suppo
return [{ messenger, id }]
}
export async function openSupportMessengerEntry(entry: ResolvedSupportMessengerEntry | SupportMessengerEntry) {
const url = 'url' in entry && entry.url
? entry.url
: buildSupportMessengerUrl(entry.messenger, entry.id)
/** @deprecated از depositGuideText استفاده کنید */
export const DEFAULT_SUPPORT_MESSENGER_TEXT = DEFAULT_DEPOSIT_GUIDE_TEXT
export interface SupportMessengerMeta {
label: string
icon: string
accent: string
}
export const SUPPORT_MESSENGER_META: Record<string, SupportMessengerMeta> = {
bale: { label: 'بله', icon: 'vuesax-linear:messages-2', accent: '#16a34a' },
telegram: { label: 'تلگرام', icon: 'vuesax-linear:send-2', accent: '#0ea5e9' },
whatsapp: { label: 'واتساپ', icon: 'vuesax-linear:whatsapp', accent: '#22c55e' },
rubika: { label: 'روبیکا', icon: 'vuesax-linear:messages-2', accent: '#7c3aed' },
eitaa: { label: 'ایتا', icon: 'vuesax-linear:messages-2', accent: '#f59e0b' },
soroush: { label: 'سروش', icon: 'vuesax-linear:messages-2', accent: '#2563eb' },
call: { label: 'تماس', icon: 'vuesax-linear:call-calling', accent: '#10b981' },
sms: { label: 'پیامک', icon: 'vuesax-linear:sms', accent: '#3b82f6' },
}
export const DEFAULT_SUPPORT_MESSENGER_META: SupportMessengerMeta = {
label: 'پشتیبانی',
icon: 'vuesax-linear:headphone',
accent: '#334155',
}
export function resolveSupportMessengerMeta(messenger?: string): SupportMessengerMeta {
const type = (messenger ?? '').trim().toLowerCase()
return SUPPORT_MESSENGER_META[type] ?? DEFAULT_SUPPORT_MESSENGER_META
}
export function resolveSupportMessengerLink(
messenger?: string,
messengerId?: string,
fallbackLink = '',
messageText?: string,
) {
const fromSettings = buildSupportMessengerUrl(messenger, messengerId, messageText)
return fromSettings || fallbackLink
}
export function getDepositGuideVarsFromSupportSettings(
settings: SupportMessengerSettings,
): DepositGuideVars {
return {
accountHolder: settings.depositAccountHolder,
cardNumber: settings.depositCardNumber,
sheba: settings.depositSheba,
customerName: getCustomerNameFromUser(),
}
}
export function resolveDepositGuideText(settings?: SupportMessengerSettings): string {
try {
const fromStore = getRenderedDepositGuideText()
if (fromStore.trim()) return fromStore
} catch {
// settings store may not be ready
}
const resolved = settings ?? supportMessengerSettings.value
const template = resolved.messengerText?.trim() || DEFAULT_DEPOSIT_GUIDE_TEXT
const vars = resolved.depositAccountHolder
? getDepositGuideVarsFromSupportSettings(resolved)
: getDepositGuideVarsFromSettings()
return renderDepositGuideText(template, vars)
}
export function resolveDepositSupportMessage(settings?: SupportMessengerSettings): string {
try {
const fromStore = getRenderedDepositSupportMessage()
if (fromStore.trim()) return fromStore
} catch {
// settings store may not be ready
}
const resolved = settings ?? supportMessengerSettings.value
const rawTemplate = resolved.depositSupportMessageText?.trim() || ''
const template = rawTemplate || getDepositSupportMessageTemplate()
const vars = resolved.depositAccountHolder
? getDepositGuideVarsFromSupportSettings(resolved)
: getDepositGuideVarsFromSettings()
return renderDepositSupportMessageText(template, vars)
}
/** پیام‌رسان‌هایی که پارامتر text در URL دارند */
export function messengerSupportsUrlText(messenger?: string): boolean {
const type = (messenger ?? '').trim().toLowerCase()
return type === 'telegram' || type === 'whatsapp' || type === 'sms'
}
/** تماس/پیام بومی — نیازی به کپی متن در کلیپ‌بورد نیست */
export function isPhoneNativeSupportMessenger(messenger?: string): boolean {
const type = (messenger ?? '').trim().toLowerCase()
return PHONE_NATIVE_MESSENGERS.has(type)
}
export function messengerNeedsClipboardCopy(messenger?: string): boolean {
if (isPhoneNativeSupportMessenger(messenger) || messengerSupportsUrlText(messenger)) {
return false
}
return Boolean((messenger ?? '').trim())
}
export async function openSupportMessengerWithMessage(
messenger?: string,
messengerId?: string,
messageText?: string,
fallbackLink = '',
): Promise<boolean> {
const text = (messageText ?? '').trim()
const url =
buildSupportMessengerUrl(messenger, messengerId, text) ||
(fallbackLink.trim() || '')
return openSupportMessengerUrl(url, messenger, text)
}
export async function openSupportMessengerEntry(
entry: SupportMessengerEntry | ResolvedSupportMessengerEntry,
messageText?: string,
fallbackLink = '',
): Promise<boolean> {
const text = (messageText ?? '').trim()
// همیشه URL را با متن فعلی بساز — url کش‌شدهٔ entry ممکن است text قدیمی داشته باشد
const url =
buildSupportMessengerUrl(entry.messenger, entry.id, text) ||
('url' in entry && !text ? entry.url : '') ||
fallbackLink.trim()
return openSupportMessengerUrl(url, entry.messenger, text)
}
async function openSupportMessengerUrl(
url: string,
messenger?: string,
messageText?: string,
): Promise<boolean> {
if (!url) return false
if (messageText && messengerNeedsClipboardCopy(messenger) && navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(messageText)
} catch {
// clipboard optional
}
}
if (url.startsWith('tel:') || url.startsWith('sms:')) {
window.location.href = url
return true
}
window.open(url, '_blank', 'noopener,noreferrer')
return true
}
export async function loadSupportMessengerFromSettings() {
function getSupportSettingValue(
settings: Array<{ key?: string; is_active?: boolean | number; value?: unknown }>,
key: string
) {
const setting = settings.find((item) => item?.key === key)
if (!setting) return ''
const value = setting.value
if (value === undefined || value === null) return ''
return String(value).trim()
}
export function buildSupportMessengerUrl(
messenger?: string,
messengerId?: string,
messageText?: string,
) {
const id = (messengerId ?? '').trim()
if (!id) return ''
if (/^https?:\/\//i.test(id)) return id
const type = (messenger ?? '').trim().toLowerCase()
const username = id.replace(/^@/, '')
const text = (messageText ?? '').trim()
const textQuery = text ? `?text=${encodeURIComponent(text)}` : ''
if (!type) return ''
switch (type) {
case 'bale':
if (id.includes('ble.ir')) {
return id.startsWith('http') ? id : `https://${id.replace(/^\/\//, '')}`
}
return `https://ble.ir/${username}`
case 'telegram':
return `https://t.me/${username}${textQuery}`
case 'whatsapp':
return `https://wa.me/${username.replace(/\D/g, '')}${textQuery}`
case 'rubika':
return `https://rubika.ir/${username}`
case 'eitaa':
return `https://eitaa.com/${username}`
case 'soroush':
return `https://splus.ir/splus/${username}`
case 'call':
return buildSupportPhoneUrl(id)
case 'sms':
return buildDepositSmsUrl(id, text)
default:
return ''
}
}
export function buildDepositSmsUrl(phoneNumber?: string, messageText?: string) {
const digits = (phoneNumber ?? '').replace(/\D/g, '')
if (!digits) return ''
const body = (messageText ?? '').trim()
// بدون پیش‌نویس مگر صریحاً متن داده شود (مودال واریز)
if (!body) return `sms:${digits}`
return `sms:${digits}?body=${encodeURIComponent(body)}`
}
export function buildSupportPhoneUrl(phoneNumber?: string) {
const digits = (phoneNumber ?? '').replace(/\D/g, '')
if (!digits) return ''
return `tel:${digits}`
}
export async function loadSupportMessengerFromSettings(forceSettingsRefresh = false) {
try {
const settingsStore = useSettingsStore()
if (forceSettingsRefresh) {
await settingsStore.fetchSettings(true)
} else if (!settingsStore.loaded) {
await settingsStore.fetchSettings()
}
const settings = await getSettings()
if (!settings || !Array.isArray(settings)) return
settingsStore.mergeSettings(settings)
supportMessengerSettings.value = {
messengers: resolveSupportMessengersFromSettings(settings),
messenger: getSupportSettingValue(settings, 'support_messenger'),
messengerId: getSupportSettingValue(settings, 'support_messenger_id'),
messengerText: getSupportSettingValue(settings, 'support_messenger_text'),
depositSupportMessageText: getSupportSettingValue(settings, 'deposit_support_message_text'),
depositAccountHolder: getSupportSettingValue(settings, 'deposit_account_holder'),
depositCardNumber: getSupportSettingValue(settings, 'deposit_card_number'),
depositSheba: getSupportSettingValue(settings, 'deposit_sheba'),
depositSmsSupportNumber: getSupportSettingValue(settings, 'deposit_sms_support_number'),
}
} catch (e) {
console.log(e)
+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>