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
+75
View File
@@ -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']
}
}
+13 -2
View File
@@ -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);
-1
View File
@@ -100,7 +100,6 @@ const indicator = h(LoadingOutlined, {
})
const showSplash = ref(true)
onMounted(() => {
checkVersion()
setTimeout(() => {
+5
View File
@@ -0,0 +1,5 @@
import useAxios from '@/utils/useAxios'
export async function getSettings() {
return await useAxios.get(`api/get_settings`)
}
+2
View File
@@ -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()
// }
+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
}
}
+55 -14
View File
@@ -80,27 +80,56 @@
<!-- Total and Checkout -->
<div class=" flex justify-center fixed bottom-20 left-0 right-0 z-1">
<div class="md:w-[426px] w-full bg-white border-t border-gray-200 p-4 shadow-lg">
<div
v-if="discountInfo?.amount > 0"
class="mb-3 flex items-center justify-between rounded-lg bg-purple-50 px-3 py-2"
>
<div class="flex flex-col gap-0.5 text-sm">
<div class="flex items-center gap-2">
<span class="text-gray-600">کد تخفیف:</span>
<span class="font-semibold">{{ discountInfo.code }}</span>
</div>
<div class="text-xs text-green-600">
{{ discountPercentLabel }} تخفیف اعمال شد
</div>
</div>
<button
type="button"
class="flex items-center gap-1 text-xs text-red-500"
@click="removeDiscount"
>
<BasicIcon icon="vuesax-linear:close-circle" size="18" />
<span>حذف</span>
</button>
</div>
<div class="flex justify-between items-center mb-4">
<div class="text-lg font-semibold">مجموع:</div>
<div class="flex flex-col gap-2 ">
<div class="flex flex-col items-end gap-1">
<div class="text-primary font-bold text-xl">
{{ usePrice(displaytotalPrice) }}
</div>
<div
v-if="discountInfo?.amount > 0"
class="text-xs text-gray-400 line-through"
>
{{ usePrice(totalPrice) }}
</div>
</div>
<div class="!text-primary font-bold text-xs line-through cursor-pointer "
v-if="discountInfo?.amount>0"
@click="showDiscountModal=true">
{{ usePrice(totalPrice) }}
</div>
<div v-else
class="!text-primary font-bold text-xs underline-solid underline cursor-pointer"
@click="showDiscountModal=true">
کد تخفیف دارم
</div>
</div>
<div
v-if="!discountInfo?.amount"
class="mb-3 text-center"
>
<span
class="text-xs text-primary underline cursor-pointer"
@click="showDiscountModal = true"
>
کد تخفیف دارم
</span>
</div>
<BasicButton
class="w-full py-3"
type="primary"
@@ -186,6 +215,18 @@ const displaytotalPrice = computed(() => {
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
+8 -12
View File
@@ -127,8 +127,6 @@
src="@/assets/img/logo.png"
/>
</div>
<!-- <div class="flex justify-center">خرید </div>
<div class="flex justify-center">محصول </div>-->
</div>
</div>
<!-- <div :class="setNavColor('buyProduct')" class="tab" @click="setRoute('buyProduct')">
@@ -166,16 +164,14 @@
<div class="text-xs mt-1 font-bold">درباره ما</div>
</div>
<div class="main_in">
<div class="text-white circle_in">
<div class="circle_into " @click="popVisible = !popVisible">
<img
class="p-2"
src="@/assets/img/logo.png"
/>
</div>
<!-- <div class="flex justify-center">خرید </div>
<div class="flex justify-center">محصول </div>-->
</div>
<div class="text-white circle_in">
<div class="circle_into " @click="popVisible = !popVisible">
<img
class="p-2"
src="@/assets/img/logo.png"
/>
</div>
</div>
</div>
<!-- <div :class="setNavColor('buyProduct')" class="tab" @click="setRoute('buyProduct')">
<Icon icon="vuesax-linear:coin" />