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
+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)