From 548866160c832fa0ad3e5ea3703109c6a4246e54 Mon Sep 17 00:00:00 2001 From: adonis Date: Sat, 13 Jun 2026 14:03:52 +0330 Subject: [PATCH] kashkool ver 1 --- components.d.ts | 75 +++++++++++ index.html | 15 ++- src/App.vue | 1 - src/api/configuration.ts | 5 + src/main.ts | 2 + src/utils/initWhatsappButtonDrag.ts | 158 +++++++++++++++++++++++ src/utils/supportMessenger.ts | 70 ++++++++++ src/utils/useDraggableFloatingButton.ts | 165 ++++++++++++++++++++++++ src/views/base/cart/index.vue | 69 ++++++++-- src/views/base/index.vue | 20 ++- 10 files changed, 551 insertions(+), 29 deletions(-) create mode 100644 src/api/configuration.ts create mode 100644 src/utils/initWhatsappButtonDrag.ts create mode 100644 src/utils/supportMessenger.ts create mode 100644 src/utils/useDraggableFloatingButton.ts diff --git a/components.d.ts b/components.d.ts index e69de29..0dacc28 100644 --- a/components.d.ts +++ b/components.d.ts @@ -0,0 +1,75 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +export {} + +declare module 'vue' { + export interface GlobalComponents { + ABadge: typeof import('ant-design-vue/es')['Badge'] + AButton: typeof import('ant-design-vue/es')['Button'] + AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider'] + ADivider: typeof import('ant-design-vue/es')['Divider'] + AForm: typeof import('ant-design-vue/es')['Form'] + AFormItem: typeof import('ant-design-vue/es')['FormItem'] + AInput: typeof import('ant-design-vue/es')['Input'] + AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] + AModal: typeof import('ant-design-vue/es')['Modal'] + ARadio: typeof import('ant-design-vue/es')['Radio'] + ARadioButton: typeof import('ant-design-vue/es')['RadioButton'] + ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] + ASelect: typeof import('ant-design-vue/es')['Select'] + ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] + ASlider: typeof import('ant-design-vue/es')['Slider'] + ASpin: typeof import('ant-design-vue/es')['Spin'] + ASwitch: typeof import('ant-design-vue/es')['Switch'] + ATag: typeof import('ant-design-vue/es')['Tag'] + ATextarea: typeof import('ant-design-vue/es')['Textarea'] + ATooltip: typeof import('ant-design-vue/es')['Tooltip'] + BaseDatePicker: typeof import('./src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue')['default'] + Basic: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/basic.vue')['default'] + BasicActionSheet: typeof import('./src/components/BasicPacks/BasicActionSheet.vue')['default'] + BasicButton: typeof import('./src/components/BasicPacks/BasicButton.vue')['default'] + BasicDraggableFileUpload: typeof import('./src/components/BasicPacks/BasicUploads/BasicDraggableFileUpload.vue')['default'] + BasicIcon: typeof import('./src/components/BasicPacks/BasicIcon.vue')['default'] + BasicInfoItem: typeof import('./src/components/BasicPacks/BasicInfoItem.vue')['default'] + BasicInput: typeof import('./src/components/BasicPacks/BasicInput.vue')['default'] + BasicModal: typeof import('./src/components/BasicPacks/BasicModal.vue')['default'] + BasicPriceInput: typeof import('./src/components/BasicPacks/BasicPriceInput/src/BasicPriceInput.vue')['default'] + BasicSelect: typeof import('./src/components/BasicPacks/BasicSelect.vue')['default'] + BasicTags: typeof import('./src/components/BasicPacks/BasicTags/index.vue')['default'] + BasicTextArea: typeof import('./src/components/BasicPacks/BasicTextArea.vue')['default'] + BasicToggleTwoItem: typeof import('./src/components/BasicPacks/BasicToggleTwoItem.vue')['default'] + Bg_main: typeof import('./src/components/bg_main.vue')['default'] + Bordered: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/bordered.vue')['default'] + DateRender: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/date-render.vue')['default'] + Demo: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/index.vue')['default'] + DialogVant: typeof import('./src/components/DialogVant.vue')['default'] + DialogVantOneButton: typeof import('./src/components/DialogVantOneButton.vue')['default'] + Disabled: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/disabled.vue')['default'] + DisabledDate: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/disabled-date.vue')['default'] + Empty: typeof import('./src/components/empty.vue')['default'] + ExtraFooter: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/extra-footer.vue')['default'] + Format: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/format.vue')['default'] + GoldLoading: typeof import('./src/components/GoldLoading.vue')['default'] + ListAnim: typeof import('./src/components/ListAnim.vue')['default'] + Mode: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/mode.vue')['default'] + PresettedRanges: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/presetted-ranges.vue')['default'] + ProductImage: typeof import('./src/components/ProductImage.vue')['default'] + RangePicker: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/range-picker.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + SelectInRange: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/select-in-range.vue')['default'] + 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'] + Switchable: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/switchable.vue')['default'] + Text: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/text.vue')['default'] + Time: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/time.vue')['default'] + TransactionSkeleton: typeof import('./src/components/TransactionSkeleton.vue')['default'] + VanActionSheet: typeof import('vant/es')['ActionSheet'] + VanDialog: typeof import('vant/es')['Dialog'] + VanField: typeof import('vant/es')['Field'] + } +} diff --git a/index.html b/index.html index 6170ce5..ec8ff93 100644 --- a/index.html +++ b/index.html @@ -23,16 +23,27 @@ justify-content: center; align-items: center; box-shadow: 0 4px 10px rgba(0,0,0,0.3); - cursor: pointer; + cursor: grab; + user-select: none; transition: transform 0.2s ease, box-shadow 0.2s ease; z-index: 9999; } - #whatsapp-button:hover { + #whatsapp-button:hover:not(.is-dragging) { transform: scale(1.1); box-shadow: 0 6px 14px rgba(0,0,0,0.3); } + #whatsapp-button.is-dragging { + cursor: grabbing; + transform: none; + box-shadow: 0 8px 18px rgba(0,0,0,0.22); + } + + #whatsapp-button img { + pointer-events: none; + } + #whatsapp-button:hover + #whatsapp-tooltip { opacity: 1; transform: translateY(0); diff --git a/src/App.vue b/src/App.vue index fe346de..e0568e2 100644 --- a/src/App.vue +++ b/src/App.vue @@ -100,7 +100,6 @@ const indicator = h(LoadingOutlined, { }) const showSplash = ref(true) - onMounted(() => { checkVersion() setTimeout(() => { diff --git a/src/api/configuration.ts b/src/api/configuration.ts new file mode 100644 index 0000000..b1de9d5 --- /dev/null +++ b/src/api/configuration.ts @@ -0,0 +1,5 @@ +import useAxios from '@/utils/useAxios' + +export async function getSettings() { + return await useAxios.get(`api/get_settings`) +} diff --git a/src/main.ts b/src/main.ts index 7fc31a0..901c568 100644 --- a/src/main.ts +++ b/src/main.ts @@ -15,6 +15,7 @@ import VueLazyLoad from 'vue3-lazyload' import { getMe } from '@/api/me' import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router' import { useAuthStore } from '@/store/useAuthStore' +import { initWhatsappButtonDrag } from '@/utils/initWhatsappButtonDrag' const app = createApp(App) app.use(createPinia()) @@ -181,6 +182,7 @@ router.beforeEach(async (to, from, next) => { }) app.mount('#app') +initWhatsappButtonDrag() // if (currentRoute.value !== '/') { // await start() // } diff --git a/src/utils/initWhatsappButtonDrag.ts b/src/utils/initWhatsappButtonDrag.ts new file mode 100644 index 0000000..18aeaac --- /dev/null +++ b/src/utils/initWhatsappButtonDrag.ts @@ -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) + }) +} diff --git a/src/utils/supportMessenger.ts b/src/utils/supportMessenger.ts new file mode 100644 index 0000000..35c4cd2 --- /dev/null +++ b/src/utils/supportMessenger.ts @@ -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({}) + +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) + } +} diff --git a/src/utils/useDraggableFloatingButton.ts b/src/utils/useDraggableFloatingButton.ts new file mode 100644 index 0000000..af3feb7 --- /dev/null +++ b/src/utils/useDraggableFloatingButton.ts @@ -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({ 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 + } +} diff --git a/src/views/base/cart/index.vue b/src/views/base/cart/index.vue index c66d1c9..06fe89a 100644 --- a/src/views/base/cart/index.vue +++ b/src/views/base/cart/index.vue @@ -80,27 +80,56 @@
+
+
+
+ کد تخفیف: + {{ discountInfo.code }} +
+
+ {{ discountPercentLabel }} تخفیف اعمال شد +
+
+ +
+
مجموع:
-
+
{{ usePrice(displaytotalPrice) }}
- +
+ {{ usePrice(totalPrice) }} +
- -
- {{ usePrice(totalPrice) }} -
-
- کد تخفیف دارم -
-
+ +
+ + کد تخفیف دارم + +
+ { return totalPrice.value }) +const discountPercentLabel = computed(() => { + const amount = Number(discountInfo.value?.amount ?? 0) + if (amount <= 0) return '' + return `${Math.round(amount * 100)}٪` +}) + +function removeDiscount() { + discountInfo.value = undefined + discountCode.value = '' + message.success('کد تخفیف حذف شد') +} + async function submitDiscount() { try { loading.value = true diff --git a/src/views/base/index.vue b/src/views/base/index.vue index c638750..60b495c 100644 --- a/src/views/base/index.vue +++ b/src/views/base/index.vue @@ -127,8 +127,6 @@ src="@/assets/img/logo.png" />
-
- +
+
+ +
+