kashkool ver 1

This commit is contained in:
2026-06-13 14:03:52 +03:30
parent f7ba6963ef
commit 548866160c
10 changed files with 551 additions and 29 deletions
+158
View File
@@ -0,0 +1,158 @@
import {
buildSupportMessengerUrl,
loadSupportMessengerFromSettings,
supportMessengerSettings,
} from '@/utils/supportMessenger'
const STORAGE_KEY = 'ghatreh_whatsapp_button_position'
const DRAG_THRESHOLD = 6
const WIDTH = 60
const HEIGHT = 60
const DEFAULT_BOTTOM_OFFSET = 90
const DEFAULT_RIGHT_OFFSET = 20
type FabPosition = { x: number; y: number }
function getViewportWidth() {
return Math.min(window.innerWidth, 426)
}
function getHorizontalOffset() {
const vw = window.innerWidth
const contentWidth = getViewportWidth()
return Math.max(0, (vw - contentWidth) / 2)
}
function getDefaultPosition(): FabPosition {
const vh = window.innerHeight
const offsetX = getHorizontalOffset()
const contentWidth = getViewportWidth()
return {
x: offsetX + contentWidth - WIDTH - DEFAULT_RIGHT_OFFSET,
y: vh - DEFAULT_BOTTOM_OFFSET - HEIGHT,
}
}
function loadPosition(): FabPosition | null {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return null
const parsed = JSON.parse(raw) as FabPosition
if (typeof parsed?.x === 'number' && typeof parsed?.y === 'number') {
return parsed
}
} catch {
// ignore
}
return null
}
function savePosition(pos: FabPosition) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(pos))
}
function clampPosition(pos: FabPosition): FabPosition {
const vw = window.innerWidth
const vh = window.innerHeight
return {
x: Math.max(0, Math.min(vw - WIDTH, pos.x)),
y: Math.max(0, Math.min(vh - HEIGHT, pos.y)),
}
}
function applyPosition(button: HTMLElement, pos: FabPosition) {
button.style.position = 'fixed'
button.style.bottom = 'auto'
button.style.right = 'auto'
button.style.left = `${pos.x}px`
button.style.top = `${pos.y}px`
button.style.touchAction = 'none'
}
function updateWhatsappHref(button: HTMLAnchorElement) {
const link = buildSupportMessengerUrl(
supportMessengerSettings.value.messenger,
supportMessengerSettings.value.messengerId
)
button.href = link || '#'
button.style.display = 'flex'
}
export function initWhatsappButtonDrag() {
const button = document.getElementById('whatsapp-button') as HTMLAnchorElement | null
if (!button) return
let pos = clampPosition(loadPosition() ?? getDefaultPosition())
applyPosition(button, pos)
let pointerId: number | null = null
let startX = 0
let startY = 0
let originX = 0
let originY = 0
let hasMoved = false
let dragging = false
function onResize() {
pos = clampPosition(pos)
applyPosition(button!, pos)
savePosition(pos)
}
function onPointerDown(event: PointerEvent) {
dragging = true
hasMoved = false
pointerId = event.pointerId
startX = event.clientX
startY = event.clientY
originX = pos.x
originY = pos.y
button!.classList.add('is-dragging')
button!.setPointerCapture(event.pointerId)
event.preventDefault()
}
function onPointerMove(event: PointerEvent) {
if (!dragging || pointerId !== event.pointerId) return
const dx = event.clientX - startX
const dy = event.clientY - startY
if (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD) {
hasMoved = true
}
pos = clampPosition({ x: originX + dx, y: originY + dy })
applyPosition(button!, pos)
}
function finishPointer(event: PointerEvent) {
if (!dragging || pointerId !== event.pointerId) return
dragging = false
pointerId = null
button!.classList.remove('is-dragging')
savePosition(pos)
const clicked = !hasMoved
hasMoved = false
if (clicked && button!.href && button!.href !== '#') {
window.open(button!.href, '_blank', 'noopener,noreferrer')
}
}
button.addEventListener('pointerdown', onPointerDown)
button.addEventListener('pointermove', onPointerMove)
button.addEventListener('pointerup', finishPointer)
button.addEventListener('pointercancel', finishPointer)
button.addEventListener('click', (event) => event.preventDefault())
window.addEventListener('resize', onResize)
void loadSupportMessengerFromSettings().then(() => {
updateWhatsappHref(button)
})
}
+70
View File
@@ -0,0 +1,70 @@
import { ref } from 'vue'
import { getSettings } from '@/api/configuration'
export type SupportMessengerType =
| 'bale'
| 'telegram'
| 'whatsapp'
| 'rubika'
| 'eitaa'
| 'soroush'
export interface SupportMessengerSettings {
messenger?: SupportMessengerType | string
messengerId?: string
messengerText?: string
}
export const supportMessengerSettings = ref<SupportMessengerSettings>({})
function getActiveSettingValue(settings: Array<{ key?: string; is_active?: boolean; value?: unknown }>, key: string) {
const setting = settings.find((item) => item?.key === key && item?.is_active === true)
const value = setting?.value
if (value === undefined || value === null) return ''
return String(value).trim()
}
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(/^@/, '')
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 ''
}
}
export async function loadSupportMessengerFromSettings() {
try {
const settings = await getSettings()
if (!settings || !Array.isArray(settings)) return
supportMessengerSettings.value = {
messenger: getActiveSettingValue(settings, 'support_messenger'),
messengerId: getActiveSettingValue(settings, 'support_messenger_id'),
messengerText: getActiveSettingValue(settings, 'support_messenger_text'),
}
} catch (e) {
console.log(e)
}
}
+165
View File
@@ -0,0 +1,165 @@
import { computed, onMounted, onUnmounted, ref } from 'vue'
const DRAG_THRESHOLD = 6
type FabPosition = { x: number; y: number }
type DraggableFloatingOptions = {
width?: number
height?: number
defaultBottomOffset?: number
defaultRightOffset?: number
}
export function useDraggableFloatingButton(
storageKey: string,
options: DraggableFloatingOptions = {}
) {
const width = options.width ?? 96
const height = options.height ?? 44
const defaultBottomOffset = options.defaultBottomOffset ?? 92
const defaultRightOffset = options.defaultRightOffset ?? 20
const pos = ref<FabPosition>({ x: 0, y: 0 })
const dragging = ref(false)
let pointerId: number | null = null
let startX = 0
let startY = 0
let originX = 0
let originY = 0
let hasMoved = false
const fabStyle = computed(() => ({
left: `${pos.value.x}px`,
top: `${pos.value.y}px`,
width: `${width}px`,
touchAction: 'none'
}))
function getViewportWidth() {
return Math.min(window.innerWidth, 426)
}
function getHorizontalOffset() {
const vw = window.innerWidth
const contentWidth = getViewportWidth()
return Math.max(0, (vw - contentWidth) / 2)
}
function resetToDefault() {
const vh = window.innerHeight
const offsetX = getHorizontalOffset()
const contentWidth = getViewportWidth()
pos.value = {
x: offsetX + contentWidth - width - defaultRightOffset,
y: vh - defaultBottomOffset - height
}
}
function clampToViewport() {
const vw = window.innerWidth
const vh = window.innerHeight
pos.value = {
x: Math.max(0, Math.min(vw - width, pos.value.x)),
y: Math.max(0, Math.min(vh - height, pos.value.y))
}
}
function loadPosition() {
try {
const raw = localStorage.getItem(storageKey)
if (!raw) return false
const parsed = JSON.parse(raw) as FabPosition
if (typeof parsed?.x === 'number' && typeof parsed?.y === 'number') {
pos.value = parsed
clampToViewport()
return true
}
} catch {
// ignore
}
return false
}
function savePosition() {
localStorage.setItem(storageKey, JSON.stringify(pos.value))
}
function handleResize() {
clampToViewport()
savePosition()
}
function handlePointerDown(event: PointerEvent) {
dragging.value = true
hasMoved = false
pointerId = event.pointerId
startX = event.clientX
startY = event.clientY
originX = pos.value.x
originY = pos.value.y
;(event.currentTarget as HTMLElement)?.setPointerCapture?.(event.pointerId)
}
function handlePointerMove(event: PointerEvent) {
if (!dragging.value || pointerId !== event.pointerId) return
const dx = event.clientX - startX
const dy = event.clientY - startY
if (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD) {
hasMoved = true
}
pos.value = {
x: originX + dx,
y: originY + dy
}
clampToViewport()
}
function finishPointer(event: PointerEvent) {
if (!dragging.value || pointerId !== event.pointerId) return false
dragging.value = false
pointerId = null
savePosition()
const clicked = !hasMoved
hasMoved = false
return clicked
}
function handlePointerUp(event: PointerEvent) {
return finishPointer(event)
}
function handlePointerCancel(event: PointerEvent) {
finishPointer(event)
return false
}
onMounted(() => {
if (!loadPosition()) {
resetToDefault()
}
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
return {
dragging,
fabStyle,
handlePointerDown,
handlePointerMove,
handlePointerUp,
handlePointerCancel,
resetToDefault
}
}