diff --git a/src/components/GeneralHeader.vue b/src/components/GeneralHeader.vue index 5847dba..8958e72 100644 --- a/src/components/GeneralHeader.vue +++ b/src/components/GeneralHeader.vue @@ -15,7 +15,7 @@ >
- @@ -56,7 +56,7 @@ position: relative; color: rgba(187, 146, 255, 1) !important; " - @click="showLang" + v-touch-tap="showLang" >
{{ currentLangName }}
@@ -64,7 +64,7 @@
{{ lang.value }}
@@ -147,7 +147,7 @@ const props = defineProps({ }) // 定义emits -defineEmits(['help']) +const emit = defineEmits(['help']) const router = useRouter() const route = useRoute() @@ -178,6 +178,10 @@ const handleLanguageChange = (item) => { // 立即更新visibleList状态 } +const emitHelp = () => { + emit('help') +} + // 检测是否在APP环境中 const isInAppEnvironment = ref(false) diff --git a/src/components/MaskLayer.vue b/src/components/MaskLayer.vue index 1ad78d6..1616247 100644 --- a/src/components/MaskLayer.vue +++ b/src/components/MaskLayer.vue @@ -2,7 +2,6 @@
-
+
@@ -31,6 +30,11 @@ const props = defineProps({ default: true, }, }) +const emit = defineEmits(['close']) + +const emitClose = () => { + emit('close') +} let lockCount = 0 let lockedScrollY = 0 diff --git a/src/main.js b/src/main.js index de08db2..fb73f97 100644 --- a/src/main.js +++ b/src/main.js @@ -8,6 +8,7 @@ import { installRuntimeSecurity } from './utils/runtimeSecurity.js' import { applyPerformanceModeClass } from './utils/performanceMode.js' import { versionChecker } from './utils/versionChecker.js' import { smartImage } from './directives/smartImage.js' +import { vTouchTap } from './utils/touchTap.js' import { createPinia } from 'pinia' import { createApp } from 'vue' import { useLangStore } from '@/stores/lang' @@ -28,6 +29,7 @@ pinia.use(piniaPluginPersistedstate) const app = createApp(App) app.directive('smart-img', smartImage) +app.directive('touch-tap', vTouchTap) app.component('LoadingContent', LoadingContent) app.use(router) diff --git a/src/utils/touchTap.js b/src/utils/touchTap.js new file mode 100644 index 0000000..f7984ed --- /dev/null +++ b/src/utils/touchTap.js @@ -0,0 +1,167 @@ +const TAP_MOVE_LIMIT = 10 +const DUPLICATE_EVENT_LIMIT = 700 +const FOCUSABLE_SELECTOR = + 'a,button,input,textarea,select,[tabindex],[role="button"],[contenteditable="true"]' + +const getEventPoint = (event) => { + if (event.changedTouches?.length) return event.changedTouches[0] + if (event.touches?.length) return event.touches[0] + return event +} + +const triggerBinding = (el, event) => { + const binding = el.__touchTapBinding + if (!binding) return + + if (binding.modifiers.self && event.target !== el) { + return + } + + if (binding.modifiers.stop) { + event.stopPropagation() + } + if (binding.modifiers.prevent) { + event.preventDefault() + } + + if (typeof binding.value === 'function') { + binding.value(event) + } +} + +export const vTouchTap = { + mounted(el, binding) { + el.__touchTapBinding = binding + + let startX = 0 + let startY = 0 + let moved = false + let active = false + let lastTouchTime = 0 + let lastActivationTime = 0 + + if (!el.matches(FOCUSABLE_SELECTOR)) { + el.setAttribute('role', 'button') + el.setAttribute('tabindex', '0') + el.__touchTapAddedA11y = true + } + + const activate = (event) => { + lastActivationTime = Date.now() + triggerBinding(el, event) + } + + // Pointer Events 同时覆盖移动端触摸和桌面鼠标;移动阈值用于避免滚动列表时误触。 + const onStart = (event) => { + if (el.__touchTapBinding?.modifiers.self && event.target !== el) { + active = false + return + } + if (event.button !== undefined && event.button !== 0) return + + const point = getEventPoint(event) + startX = point.clientX || 0 + startY = point.clientY || 0 + moved = false + active = true + } + + const onMove = (event) => { + if (!active) return + + const point = getEventPoint(event) + const deltaX = Math.abs((point.clientX || 0) - startX) + const deltaY = Math.abs((point.clientY || 0) - startY) + if (deltaX > TAP_MOVE_LIMIT || deltaY > TAP_MOVE_LIMIT) { + moved = true + } + } + + const onEnd = (event) => { + if (!active) return + + const point = getEventPoint(event) + const deltaX = Math.abs((point.clientX || 0) - startX) + const deltaY = Math.abs((point.clientY || 0) - startY) + active = false + + if (moved || deltaX > TAP_MOVE_LIMIT || deltaY > TAP_MOVE_LIMIT) return + activate(event) + } + + const onTouchEnd = (event) => { + lastTouchTime = Date.now() + onEnd(event) + } + + const onMouseEnd = (event) => { + if (Date.now() - lastTouchTime < DUPLICATE_EVENT_LIMIT) return + onEnd(event) + } + + const onClick = (event) => { + if (event.detail !== 0) return + if (Date.now() - lastActivationTime < DUPLICATE_EVENT_LIMIT) return + activate(event) + } + + const onKeydown = (event) => { + if (event.key !== 'Enter' && event.key !== ' ') return + if (event.key === ' ') { + event.preventDefault() + } + activate(event) + } + + const onCancel = () => { + active = false + moved = false + } + + const hasPointerEvent = typeof window !== 'undefined' && typeof window.PointerEvent === 'function' + const pointerOrTouchEvents = hasPointerEvent + ? [ + ['pointerdown', onStart, { passive: true }], + ['pointermove', onMove, { passive: true }], + ['pointerup', onEnd, { passive: false }], + ['pointercancel', onCancel, { passive: true }], + ] + : [ + ['touchstart', onStart, { passive: true }], + ['touchmove', onMove, { passive: true }], + ['touchend', onTouchEnd, { passive: false }], + ['touchcancel', onCancel, { passive: true }], + ['mousedown', onStart, { passive: true }], + ['mousemove', onMove, { passive: true }], + ['mouseup', onMouseEnd, { passive: false }], + ] + const events = [ + ...pointerOrTouchEvents, + ['click', onClick, { passive: false }], + ['keydown', onKeydown, { passive: false }], + ] + + events.forEach(([name, handler, options]) => { + el.addEventListener(name, handler, options) + }) + + el.__touchTapCleanup = () => { + events.forEach(([name, handler, options]) => { + el.removeEventListener(name, handler, options) + }) + } + }, + updated(el, binding) { + el.__touchTapBinding = binding + }, + beforeUnmount(el) { + el.__touchTapCleanup?.() + delete el.__touchTapBinding + delete el.__touchTapCleanup + if (el.__touchTapAddedA11y) { + el.removeAttribute('role') + el.removeAttribute('tabindex') + delete el.__touchTapAddedA11y + } + }, +} diff --git a/src/views/AdminCenter/ItemDistribution.vue b/src/views/AdminCenter/ItemDistribution.vue index 2fdf921..905ce65 100644 --- a/src/views/AdminCenter/ItemDistribution.vue +++ b/src/views/AdminCenter/ItemDistribution.vue @@ -1,7 +1,6 @@ @@ -208,11 +127,12 @@ const underlineStyle = computed(() => { if (tabItems.value.length === 0) return {} //没有这个元素 const activeTab = tabItems.value[activeIndex.value] + const underlineWidth = 24 console.log('选中元素的左边距', activeTab.offsetLeft) return { position: 'absolute', - left: `${activeTab.offsetLeft + activeTab.offsetWidth / 2 - 15 / 2}px`, + left: `${activeTab.offsetLeft + activeTab.offsetWidth / 2 - underlineWidth / 2}px`, bottom: '0', transition: 'left 0.3s ease', // 平滑过渡 } @@ -341,12 +261,18 @@ onMounted(() => {