mobeicanyue 8 месяцев назад
Родитель
Сommit
d67996f75f
2 измененных файлов с 215 добавлено и 0 удалено
  1. 214 0
      modules/web/src/utils/xboxGamepad.ts
  2. 1 0
      modules/web/src/views/BookChapter.vue

+ 214 - 0
modules/web/src/utils/xboxGamepad.ts

@@ -0,0 +1,214 @@
+// =====================================================
+// 类型定义
+// =====================================================
+
+interface GamepadConfig {
+  DEBUG: boolean;
+  AXIS_THRESHOLD: number;
+  AXIS_COOLDOWN: number;
+  DPAD_INDEX: {
+    UP: number;
+    DOWN: number;
+    LEFT: number;
+    RIGHT: number;
+  };
+}
+
+interface DPadState {
+  up: boolean;
+  down: boolean;
+  left: boolean;
+  right: boolean;
+}
+
+interface GlobalState {
+  lastAxisTime: number;
+  lastAxisDirection: number;
+  dpadPressed: DPadState;
+}
+
+// =====================================================
+// 配置区域
+// =====================================================
+
+const CONFIG: GamepadConfig = {
+  DEBUG: true,              // 日志总开关
+  AXIS_THRESHOLD: 0.7,      // 摇杆触发阈值
+  AXIS_COOLDOWN: 200,       // 摇杆触发冷却时间 (ms)
+  DPAD_INDEX: {             // Xbox 标准映射
+    UP: 12,
+    DOWN: 13,
+    LEFT: 14,
+    RIGHT: 15
+  }
+};
+
+// =====================================================
+// 日志工具函数
+// =====================================================
+
+function log(...args: any[]): void {
+  if (!CONFIG.DEBUG) return;
+  console.log(...args);
+}
+
+// =====================================================
+// 状态记录
+// =====================================================
+
+const state: GlobalState = {
+  lastAxisTime: 0,
+  lastAxisDirection: 0,
+  dpadPressed: {
+    up: false,
+    down: false,
+    left: false,
+    right: false
+  }
+};
+
+// =====================================================
+// 通用工具函数
+// =====================================================
+
+/**
+ * 平滑翻页函数
+ * @param direction 1 = 向下, -1 = 向上
+ */
+function scrollPage(direction: number): void {
+  const offset = window.innerHeight - 80;
+  const distance = direction === 1 ? offset : -offset;
+
+  log(direction === 1 ? "🎮 翻页:向下" : "🎮 翻页:向上");
+
+  window.scrollBy({
+    top: distance,
+    behavior: "smooth"
+  });
+}
+
+/**
+ * 章节切换函数
+ * @param direction 1 = 下一章, -1 = 上一章
+ */
+function switchChapter(direction: number): void {
+  const buttons = document.querySelectorAll<HTMLElement>(".read-bar .tool-icon");
+
+  if (buttons.length < 2) return;
+
+  if (direction === 1) {
+    log("🎮 切换章节:下一章");
+    buttons[1].click();
+  } else {
+    log("🎮 切换章节:上一章");
+    buttons[0].click();
+  }
+}
+
+/**
+ * 边沿检测函数
+ */
+function isPressedOnce(current: boolean, previous: boolean): boolean {
+  return current && !previous;
+}
+
+// =====================================================
+// 摇杆处理
+// =====================================================
+
+function handleAxis(gp: Gamepad, now: number): void {
+  const axisY = gp.axes[1] || 0;
+
+  if (Math.abs(axisY) <= CONFIG.AXIS_THRESHOLD) {
+    state.lastAxisDirection = 0;
+    return;
+  }
+
+  const direction = axisY > 0 ? 1 : -1;
+  const cooldownPassed = now - state.lastAxisTime > CONFIG.AXIS_COOLDOWN;
+  const directionChanged = direction !== state.lastAxisDirection;
+
+  if (cooldownPassed || directionChanged) {
+    state.lastAxisTime = now;
+    state.lastAxisDirection = direction;
+    log(`🎮 摇杆触发 | axisY=${axisY.toFixed(2)}`);
+    scrollPage(direction);
+  }
+}
+
+// =====================================================
+// DPad 处理
+// =====================================================
+
+function handleDPad(gp: Gamepad): void {
+  const indexes = CONFIG.DPAD_INDEX;
+
+  const current: DPadState = {
+    up: gp.buttons[indexes.UP]?.pressed || false,
+    down: gp.buttons[indexes.DOWN]?.pressed || false,
+    left: gp.buttons[indexes.LEFT]?.pressed || false,
+    right: gp.buttons[indexes.RIGHT]?.pressed || false
+  };
+
+  if (isPressedOnce(current.up, state.dpadPressed.up)) {
+    log("🎮 DPad 上");
+    scrollPage(-1);
+  }
+  if (isPressedOnce(current.down, state.dpadPressed.down)) {
+    log("🎮 DPad 下");
+    scrollPage(1);
+  }
+  if (isPressedOnce(current.left, state.dpadPressed.left)) {
+    log("🎮 DPad 左");
+    switchChapter(-1);
+  }
+  if (isPressedOnce(current.right, state.dpadPressed.right)) {
+    log("🎮 DPad 右");
+    switchChapter(1);
+  }
+
+  state.dpadPressed = current;
+}
+
+// =====================================================
+// 主手柄处理入口
+// =====================================================
+
+function handleGamepad(gp: Gamepad | null): void {
+  if (!gp) return;
+  const now = performance.now();
+  handleAxis(gp, now);
+  handleDPad(gp);
+}
+
+// =====================================================
+// 主循环
+// =====================================================
+
+let running = false;
+
+function gamepadLoop(): void {
+  const gamepads = navigator.getGamepads?.() || [];
+
+  for (const gp of gamepads) {
+    if (gp) handleGamepad(gp);
+  }
+
+  requestAnimationFrame(gamepadLoop);
+}
+
+// =====================================================
+// 连接 / 断开 事件
+// =====================================================
+
+window.addEventListener("gamepadconnected", (e: GamepadEvent) => {
+  log("🎮 手柄已连接:", e.gamepad.id);
+  if (!running) {
+    running = true;
+    requestAnimationFrame(gamepadLoop);
+  }
+});
+
+window.addEventListener("gamepaddisconnected", (e: GamepadEvent) => {
+  log("🎮 手柄已断开:", e.gamepad.id);
+});

+ 1 - 0
modules/web/src/views/BookChapter.vue

@@ -113,6 +113,7 @@ import API from '@api'
 import { useLoading } from '@/hooks/loading'
 import { useLoading } from '@/hooks/loading'
 import { useThrottleFn } from '@vueuse/shared'
 import { useThrottleFn } from '@vueuse/shared'
 import { isNullOrBlank } from '@/utils/utils'
 import { isNullOrBlank } from '@/utils/utils'
+import '@/utils/xboxGamepad'
 
 
 const content = ref()
 const content = ref()
 // loading spinner
 // loading spinner