From 599d67141f0e1dff412f67f012a547f60ac49003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Thu, 31 Jul 2025 16:30:34 +0800 Subject: [PATCH] =?UTF-8?q?FEAT=20=3D>=20=E6=9B=B4=E6=96=B0=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E9=85=8D=E7=BD=AE=E5=92=8C=E7=BB=84=E4=BB=B6=EF=BC=8C?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AE=89=E5=85=A8=E5=8C=BA=E5=9F=9F=E6=94=AF?= =?UTF-8?q?=E6=8C=81=EF=BC=8C=E4=BC=98=E5=8C=96=E5=AF=BC=E8=88=AA=E6=A0=8F?= =?UTF-8?q?=EF=BC=8C=E7=A7=BB=E9=99=A4=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E6=B5=8B=E8=AF=95=E9=A1=B5=E9=9D=A2=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/components/NavCommon/index.tsx | 104 ++- nkebao/src/main.tsx | 5 + nkebao/src/pages/mobile/home/index.tsx | 19 +- nkebao/src/pages/mobile/mine/main/index.tsx | 16 + .../src/pages/mobile/scenarios/list/index.tsx | 25 +- nkebao/src/pages/mobile/test/README.md | 373 ++++++++++ nkebao/src/pages/mobile/test/index.tsx | 72 ++ nkebao/src/pages/mobile/test/postMessage.tsx | 646 ++++++++++++++++++ .../index.tsx => test/select.tsx} | 13 +- .../mobile/workspace/main/index.module.scss | 4 +- .../src/pages/mobile/workspace/main/index.tsx | 59 +- nkebao/src/router/module/component-test.tsx | 11 - nkebao/src/router/module/test.tsx | 27 + nkebao/src/styles/global.scss | 11 + nkebao/src/utils/env.ts | 46 ++ nkebao/src/utils/postMessage.ts | 634 +++++++++++++++++ nkebao/src/utils/safeArea.ts | 176 +++++ nkebao/vite.config.ts | 1 + 18 files changed, 2116 insertions(+), 126 deletions(-) create mode 100644 nkebao/src/pages/mobile/test/README.md create mode 100644 nkebao/src/pages/mobile/test/index.tsx create mode 100644 nkebao/src/pages/mobile/test/postMessage.tsx rename nkebao/src/pages/mobile/{component-test/index.tsx => test/select.tsx} (93%) delete mode 100644 nkebao/src/router/module/component-test.tsx create mode 100644 nkebao/src/router/module/test.tsx create mode 100644 nkebao/src/utils/env.ts create mode 100644 nkebao/src/utils/postMessage.ts create mode 100644 nkebao/src/utils/safeArea.ts diff --git a/nkebao/src/components/NavCommon/index.tsx b/nkebao/src/components/NavCommon/index.tsx index 8002fc25..991de16e 100644 --- a/nkebao/src/components/NavCommon/index.tsx +++ b/nkebao/src/components/NavCommon/index.tsx @@ -1,40 +1,94 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { NavBar } from "antd-mobile"; import { ArrowLeftOutlined } from "@ant-design/icons"; import { useNavigate } from "react-router-dom"; +import { getTopSafeAreaHeight, initSafeArea } from "@/utils/safeArea"; interface NavCommonProps { title: string; backFn?: () => void; right?: React.ReactNode; + left?: React.ReactNode; } -const NavCommon: React.FC = ({ title, backFn, right }) => { +const NavCommon: React.FC = ({ + title, + backFn, + right, + left, +}) => { const navigate = useNavigate(); + const [topBarHeight, setTopBarHeight] = useState(0); + + useEffect(() => { + // 初始化安全区域 + initSafeArea(); + + // 计算顶部安全区域高度 + const calculateTopBarHeight = () => { + const height = getTopSafeAreaHeight(); + setTopBarHeight(height); + }; + + // 立即计算一次 + calculateTopBarHeight(); + + // 监听屏幕方向变化 + const handleOrientationChange = () => { + setTimeout(calculateTopBarHeight, 100); + }; + + // 监听窗口大小变化 + const handleResize = () => { + calculateTopBarHeight(); + }; + + window.addEventListener("orientationchange", handleOrientationChange); + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("orientationchange", handleOrientationChange); + window.removeEventListener("resize", handleResize); + }; + }, []); + return ( - - { - if (backFn) { - backFn(); - } else { - navigate(-1); - } - }} - /> - - } - right={right} - > - - {title} - - + <> +
+ + { + if (backFn) { + backFn(); + } else { + navigate(-1); + } + }} + /> + + ) + } + right={right} + > + + {title} + + + ); }; diff --git a/nkebao/src/main.tsx b/nkebao/src/main.tsx index f4ae337c..1d372bd5 100644 --- a/nkebao/src/main.tsx +++ b/nkebao/src/main.tsx @@ -2,6 +2,11 @@ import React from "react"; import { createRoot } from "react-dom/client"; import App from "./App"; import "./styles/global.scss"; +import { initSafeArea } from "./utils/safeArea"; + +// 初始化安全区域 +initSafeArea(); + // import VConsole from "vconsole"; // new VConsole(); const root = createRoot(document.getElementById("root")!); diff --git a/nkebao/src/pages/mobile/home/index.tsx b/nkebao/src/pages/mobile/home/index.tsx index 25fadb07..3293455c 100644 --- a/nkebao/src/pages/mobile/home/index.tsx +++ b/nkebao/src/pages/mobile/home/index.tsx @@ -1,10 +1,8 @@ import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { NavBar } from "antd-mobile"; +import NavCommon from "@/components/NavCommon"; import { - BellOutlined, MobileOutlined, - UserOutlined, MessageOutlined, TeamOutlined, RiseOutlined, @@ -27,13 +25,6 @@ interface DashboardData { aliveWechatNum?: number; } -interface TodayStatsData { - momentsNum?: number; - groupPushNum?: number; - passRate?: string; - sysActive?: string; -} - interface SevenDayStatsData { date?: string[]; allNum?: number[]; @@ -146,13 +137,7 @@ const Home: React.FC = () => { return ( -
- 存客宝 -
- - } + header={} title="存客宝" />} footer={} loading={isLoading} > diff --git a/nkebao/src/pages/mobile/mine/main/index.tsx b/nkebao/src/pages/mobile/mine/main/index.tsx index 4f98d71e..904ea5c8 100644 --- a/nkebao/src/pages/mobile/mine/main/index.tsx +++ b/nkebao/src/pages/mobile/mine/main/index.tsx @@ -13,6 +13,7 @@ import Layout from "@/components/Layout/Layout"; import style from "./index.module.scss"; import { useUserStore } from "@/store/module/user"; import { getDashboard } from "./api"; +import { DEV_FEATURES } from "@/utils/env"; const Mine: React.FC = () => { const navigate = useNavigate(); @@ -78,6 +79,21 @@ const Mine: React.FC = () => { bgColor: "#fff7e6", iconColor: "#fa8c16", }, + // 只在开发环境显示测试菜单 + ...(DEV_FEATURES.SHOW_TEST_PAGES + ? [ + { + id: "test", + title: "测试", + description: "测试", + icon: , + count: 0, + path: "/test/postMessage", + bgColor: "#fff7e6", + iconColor: "#fa8c16", + }, + ] + : []), ]; // 加载统计数据 diff --git a/nkebao/src/pages/mobile/scenarios/list/index.tsx b/nkebao/src/pages/mobile/scenarios/list/index.tsx index ce66979f..2613bdaf 100644 --- a/nkebao/src/pages/mobile/scenarios/list/index.tsx +++ b/nkebao/src/pages/mobile/scenarios/list/index.tsx @@ -1,8 +1,9 @@ import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { NavBar, Button, Toast } from "antd-mobile"; +import { Button, Toast } from "antd-mobile"; import { PlusOutlined, RiseOutlined } from "@ant-design/icons"; import MeauMobile from "@/components/MeauMobile/MeauMoible"; +import NavCommon from "@/components/NavCommon"; import Layout from "@/components/Layout/Layout"; import { getScenarios } from "./api"; import style from "./index.module.scss"; @@ -68,12 +69,15 @@ const Scene: React.FC = () => { return ( -
场景获客
- - + } + title="场景获客" + right={ + + } + /> } >
@@ -90,10 +94,9 @@ const Scene: React.FC = () => { 场景获客
} + title={""} right={ } - > + /> } footer={} > diff --git a/nkebao/src/pages/mobile/test/README.md b/nkebao/src/pages/mobile/test/README.md new file mode 100644 index 00000000..9c52eb13 --- /dev/null +++ b/nkebao/src/pages/mobile/test/README.md @@ -0,0 +1,373 @@ +# 测试页面使用说明 + +## 概述 + +这些测试页面只在开发环境中显示,用于开发和调试各种功能。所有测试页面都使用了统一的Layout布局组件。 + +## 访问方式 + +1. **开发环境**: 访问 `/test` 或 `/test/postMessage` +2. **生产环境**: 这些页面不会显示,路由也不会注册 + +## 测试页面列表 + +### 1. 测试页面入口 (`/test`) + +- 使用Layout布局 +- 提供测试页面的导航入口 +- 显示开发环境标识 + +### 2. UniApp桥接测试 (`/test/postMessage`) + +测试UniApp WebView桥接功能,包括: + +- ✅ 获取用户信息 +- ✅ 获取设备信息 +- ✅ 显示Toast提示 +- ✅ 显示Alert对话框 +- ✅ 显示Confirm确认框 +- ✅ 分享内容 +- ✅ 支付功能 +- ✅ 页面导航 +- ✅ 自定义消息发送 +- ✅ 桥接状态检查 +- ✅ 通信调试工具 + +**布局特性:** + +- 使用Layout布局组件 +- 响应式设计(移动端适配) +- 实时状态显示 +- 操作日志记录 + +### 3. 安全区域测试 (`/test/safeArea`) + +测试安全区域高度计算功能: + +- ✅ 设备类型检测(iOS/Android) +- ✅ 刘海屏设备检测 +- ✅ 安全区域高度计算 +- ✅ 屏幕方向变化监听 +- ✅ CSS变量动态设置 +- ✅ 实时信息更新 + +**布局特性:** + +- 使用Layout布局组件 +- 实时显示设备信息 +- 动态更新安全区域数据 +- 支持屏幕方向变化 + +### 4. 选择组件测试 (`/test/select`) + +测试各种选择组件: + +- 设备选择 +- 好友选择 +- 群组选择 +- 内容库选择 +- 账号选择 + +**布局特性:** + +- 使用Layout布局组件 +- Tab标签页切换 +- 实时选择状态显示 + +## 通信流程 + +### UniApp ↔ React 通信流程 + +``` +1. UniApp注入桥接代码 + ↓ +2. React检测桥接就绪 + ↓ +3. React发送请求消息 + ↓ +4. UniApp接收并处理 + ↓ +5. UniApp发送响应消息 + ↓ +6. React接收并处理响应 +``` + +### 消息类型映射 + +| React发送 | UniApp接收 | UniApp响应 | React接收 | +| --------------- | --------------- | --------------- | --------------- | +| `getUserInfo` | `getUserInfo` | `userInfo` | `userInfo` | +| `getDeviceInfo` | `getDeviceInfo` | `deviceInfo` | `deviceInfo` | +| `toast` | `toast` | - | - | +| `alert` | `alert` | - | - | +| `confirm` | `confirm` | `confirmResult` | `confirmResult` | +| `share` | `share` | `shareResult` | `shareResult` | +| `payment` | `payment` | `paymentResult` | `paymentResult` | + +### 调试工具 + +#### 1. 桥接状态检查 + +- 检查桥接是否就绪 +- 检查桥接是否存在 +- 显示消息队列状态 +- 显示监听器数量 + +#### 2. 直接消息测试 + +- 绕过桥接工具直接发送消息 +- 测试桥接注入是否成功 +- 验证消息传递机制 + +#### 3. 监听器状态检查 + +- 检查事件监听器注册状态 +- 验证消息处理流程 +- 排查通信问题 + +## 布局组件 + +所有测试页面都使用了以下布局组件: + +### Layout组件 + +- 提供统一的页面布局结构 +- 包含头部导航和内容区域 +- 支持移动端适配 + +### NavCommon组件 + +- 统一的导航头部 +- 支持返回按钮 +- 显示页面标题 + +## 环境配置 + +### 开发环境特性开关 + +在 `src/utils/env.ts` 中配置: + +```typescript +export const DEV_FEATURES = { + SHOW_TEST_PAGES: true, // 显示测试页面 + ENABLE_DEBUG_LOGS: true, // 启用调试日志 + SHOW_DEV_TOOLS: true, // 显示开发工具 + ENABLE_MOCK_DATA: true, // 启用Mock数据 +}; +``` + +### 环境变量 (Vite) + +在Vite项目中,使用 `import.meta.env` 访问环境变量: + +```typescript +// 环境检测 +export const isDevelopment = import.meta.env.DEV; +export const isProduction = import.meta.env.PROD; + +// 环境变量 +const apiUrl = import.meta.env.VITE_API_BASE_URL; +const appTitle = import.meta.env.VITE_APP_TITLE; +``` + +### 环境变量列表 + +- `import.meta.env.MODE`: 环境模式 (development/production) +- `import.meta.env.DEV`: 是否为开发环境 (boolean) +- `import.meta.env.PROD`: 是否为生产环境 (boolean) +- `VITE_APP_TITLE`: 应用标题 +- `VITE_API_BASE_URL`: API基础URL +- `VITE_APP_VERSION`: 应用版本 + +### 环境变量配置 + +在项目根目录创建 `.env` 文件: + +```env +# 开发环境 +VITE_APP_TITLE=存客宝 +VITE_API_BASE_URL=http://localhost:3000/api +VITE_APP_VERSION=1.0.0 +``` + +## 通信问题排查 + +### 常见问题及解决方案 + +#### 1. 桥接初始化超时 + +**症状**: 页面显示"桥接初始化中..." +**原因**: UniApp端桥接代码注入失败或延迟 +**解决**: + +- 检查网络连接 +- 查看控制台错误信息 +- 使用"检查桥接状态"工具 +- 使用"强制重新初始化"按钮 + +#### 2. 消息发送失败 + +**症状**: 点击按钮无响应或显示错误 +**原因**: 桥接未就绪或消息类型不匹配 +**解决**: + +- 确认桥接已就绪 +- 检查消息类型是否正确 +- 使用"测试直接发送消息"工具 + +#### 3. 响应消息未收到 + +**症状**: 发送请求后未收到响应 +**原因**: 监听器未正确注册或消息传递失败 +**解决**: + +- 检查监听器注册状态 +- 查看UniApp端处理逻辑 +- 使用"检查监听器状态"工具 + +#### 4. 桥接注入失败 + +**症状**: window.uniAppBridge 不存在 +**原因**: UniApp端JavaScript注入失败 +**解决**: + +- 检查UniApp端控制台日志 +- 确认web-view组件正常工作 +- 检查网络连接和URL配置 + +#### 5. webview或evalJS方法不存在 + +**症状**: "UniApp: webview或evalJS方法不存在" +**原因**: + +- 不在App环境下运行 +- web-view组件未正确初始化 +- 平台不支持evalJS方法 + **解决**: +- 确认在App环境下运行 +- 检查web-view组件配置 +- 使用备用注入方案 +- 查看平台检测信息 + +### 调试步骤 + +1. **环境检测** + - 点击"环境检测"按钮 + - 确认运行环境和平台信息 + - 检查是否在UniApp WebView中 + +2. **检查桥接状态** + - 点击"检查桥接状态"按钮 + - 确认 `bridgeExists: true` 和 `isReady: true` + +3. **测试直接通信** + - 点击"测试直接发送消息"按钮 + - 查看控制台输出 + +4. **检查监听器** + - 点击"检查监听器状态"按钮 + - 确认监听器数量正确 + +5. **强制重新初始化** + - 点击"强制重新初始化"按钮 + - 观察重新初始化过程 + +6. **查看详细日志** + - 打开浏览器开发者工具 + - 查看Console标签页 + - 搜索相关日志信息 + +7. **检查UniApp端日志** + - 在UniApp开发工具中查看控制台 + - 确认桥接代码注入成功 + - 检查消息处理逻辑 + +### 环境检查清单 + +#### React端检查 + +- [ ] 页面在UniApp WebView中运行 +- [ ] 控制台无JavaScript错误 +- [ ] 桥接状态检查通过 +- [ ] 监听器正确注册 + +#### UniApp端检查 + +- [ ] web-view组件正常加载 +- [ ] 桥接代码注入成功 +- [ ] 消息处理逻辑正确 +- [ ] 响应消息正确发送 + +#### 网络检查 + +- [ ] URL配置正确 +- [ ] 网络连接正常 +- [ ] 无跨域问题 +- [ ] 防火墙未阻止 + +#### 平台检查 + +- [ ] 确认在App环境下运行 +- [ ] web-view组件支持evalJS +- [ ] 平台支持postMessage通信 +- [ ] 无平台限制问题 + +### 常见错误及解决方案 + +#### 错误1: "window.uniAppBridge 不存在" + +**解决方案**: + +1. 检查UniApp端桥接注入是否成功 +2. 确认web-view组件正常工作 +3. 查看UniApp端控制台日志 + +#### 错误2: "桥接初始化超时" + +**解决方案**: + +1. 增加超时时间 +2. 检查网络连接 +3. 使用强制重新初始化 + +#### 错误3: "消息发送失败" + +**解决方案**: + +1. 确认桥接已就绪 +2. 检查消息类型匹配 +3. 验证postMessage方法存在 + +#### 错误4: "响应消息未收到" + +**解决方案**: + +1. 检查监听器注册 +2. 确认UniApp端响应逻辑 +3. 验证消息传递机制 + +#### 错误5: "webview或evalJS方法不存在" + +**解决方案**: + +1. 确认在App环境下运行 +2. 检查web-view组件配置 +3. 使用备用注入方案 +4. 查看平台检测信息 + +## 使用建议 + +1. **开发阶段**: 充分利用测试页面进行功能验证 +2. **调试桥接**: 使用UniApp桥接测试页面验证通信功能 +3. **组件测试**: 使用选择组件测试页面验证UI组件 +4. **生产部署**: 确保测试页面不会出现在生产环境中 + +## 注意事项 + +- 测试页面只在开发环境显示 +- 生产环境会自动隐藏所有测试相关功能 +- 调试日志只在开发环境输出 +- 确保在生产构建前移除所有测试代码 +- 所有测试页面都使用统一的Layout布局,保持UI一致性 +- 使用 `import.meta.env` 而不是 `process.env` 访问环境变量 +- 通信问题优先使用内置调试工具排查 diff --git a/nkebao/src/pages/mobile/test/index.tsx b/nkebao/src/pages/mobile/test/index.tsx new file mode 100644 index 00000000..6fa456d9 --- /dev/null +++ b/nkebao/src/pages/mobile/test/index.tsx @@ -0,0 +1,72 @@ +import React from "react"; +import { Card, Button, Space, Typography, Tag } from "antd"; +import { + MessageOutlined, + SelectOutlined, + SafetyOutlined, +} from "@ant-design/icons"; +import { useNavigate } from "react-router-dom"; +import { isDevelopment } from "@/utils/env"; +import Layout from "@/components/Layout/Layout"; +import NavCommon from "@/components/NavCommon"; + +const { Title, Text } = Typography; + +const TestIndex: React.FC = () => { + const navigate = useNavigate(); + + return ( + }> +
+ + 测试页面 + {isDevelopment && ( + <Tag color="orange" style={{ marginLeft: 8, fontSize: "12px" }}> + 开发环境 + </Tag> + )} + + + + + + + + + + + + + + + 这里提供各种功能的测试页面,方便开发和调试。 + + +
+
+ ); +}; + +export default TestIndex; diff --git a/nkebao/src/pages/mobile/test/postMessage.tsx b/nkebao/src/pages/mobile/test/postMessage.tsx new file mode 100644 index 00000000..7146e8d9 --- /dev/null +++ b/nkebao/src/pages/mobile/test/postMessage.tsx @@ -0,0 +1,646 @@ +import React, { useState, useEffect } from "react"; +import { + Button, + Card, + Space, + Divider, + Typography, + Alert, + Tag, + Avatar, + Row, + Col, +} from "antd"; +import { + UserOutlined, + MobileOutlined, + MessageOutlined, + ExclamationCircleOutlined, + CheckCircleOutlined, + ShareAltOutlined, + PayCircleOutlined, + SendOutlined, + InfoCircleOutlined, + LoadingOutlined, +} from "@ant-design/icons"; +import bridge, { + ResponseType, + type UserInfo, + type DeviceInfo, + type ShareData, + type PaymentData, +} from "@/utils/postMessage"; +import Layout from "@/components/Layout/Layout"; +import NavCommon from "@/components/NavCommon"; + +const { Text, Paragraph } = Typography; + +const PostMessageTest: React.FC = () => { + const [isReady, setIsReady] = useState(false); + const [userInfo, setUserInfo] = useState(null); + const [deviceInfo, setDeviceInfo] = useState(null); + const [loading, setLoading] = useState(false); + const [logs, setLogs] = useState([]); + const [lastResult, setLastResult] = useState(null); + + // 添加日志 + const addLog = (message: string) => { + const timestamp = new Date().toLocaleTimeString(); + setLogs(prev => [`[${timestamp}] ${message}`, ...prev.slice(0, 19)]); + }; + + // 初始化桥接 + useEffect(() => { + const initBridge = async () => { + try { + addLog("正在初始化UniApp桥接..."); + + // 立即检查桥接状态 + const initialStatus = bridge.getBridgeStatus(); + addLog(`初始桥接状态: ${JSON.stringify(initialStatus, null, 2)}`); + + // 设置超时处理 + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error("桥接初始化超时 (30秒)")), 30000); + }); + + // 等待桥接就绪,带超时 + await Promise.race([bridge.waitForReady(), timeoutPromise]); + + setIsReady(true); + addLog("✅ UniApp桥接初始化成功"); + + // 设置监听器 + bridge.on(ResponseType.USER_INFO, (data: UserInfo) => { + setUserInfo(data); + addLog(`收到用户信息: ${data.name}`); + }); + + bridge.on(ResponseType.DEVICE_INFO, (data: DeviceInfo) => { + setDeviceInfo(data); + addLog(`收到设备信息: ${data.platform} ${data.model}`); + }); + + bridge.on(ResponseType.SHARE_RESULT, (result: any) => { + setLastResult(result); + if (result.success) { + addLog("✅ 分享成功"); + } else { + addLog(`❌ 分享失败: ${result.error}`); + } + }); + + bridge.on(ResponseType.PAYMENT_RESULT, (result: any) => { + setLastResult(result); + if (result.success) { + addLog(`✅ 支付成功,订单ID: ${result.orderId}`); + } else { + addLog(`❌ 支付失败: ${result.error}`); + } + }); + + bridge.on(ResponseType.CONFIRM_RESULT, (result: any) => { + setLastResult(result); + addLog(`用户${result.confirmed ? "确认" : "取消"}了操作`); + }); + + // 通知页面准备就绪 + bridge.notifyPageReady({ + version: "1.0.0", + features: ["postMessage", "bridge"], + }); + addLog("已通知页面准备就绪"); + + // 延迟后再次检查状态 + setTimeout(() => { + const finalStatus = bridge.getBridgeStatus(); + addLog(`最终桥接状态: ${JSON.stringify(finalStatus, null, 2)}`); + }, 2000); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + addLog(`❌ 桥接初始化失败: ${errorMessage}`); + + // 提供重试建议 + addLog("💡 建议检查:"); + addLog(" 1. 是否在UniApp WebView环境中运行"); + addLog(" 2. 网络连接是否正常"); + addLog(" 3. 尝试刷新页面重试"); + addLog(" 4. 检查UniApp端控制台日志"); + } + }; + + initBridge(); + + return () => { + bridge.destroy(); + }; + }, []); + + // 获取用户信息 + const handleGetUserInfo = async () => { + setLoading(true); + try { + bridge.getUserInfo(); + addLog("已请求用户信息"); + } catch (error) { + addLog(`获取用户信息失败: ${error}`); + } finally { + setLoading(false); + } + }; + + // 获取设备信息 + const handleGetDeviceInfo = async () => { + setLoading(true); + try { + bridge.getDeviceInfo(); + addLog("已请求设备信息"); + } catch (error) { + addLog(`获取设备信息失败: ${error}`); + } finally { + setLoading(false); + } + }; + + // 显示Toast + const handleShowToast = async () => { + try { + bridge.showToast("这是一个测试Toast消息!", 3000); + addLog("已显示Toast消息"); + } catch (error) { + addLog(`显示Toast失败: ${error}`); + } + }; + + // 显示Alert + const handleShowAlert = async () => { + try { + bridge.showAlert("测试提示", "这是一个测试Alert对话框!"); + addLog("已显示Alert对话框"); + } catch (error) { + addLog(`显示Alert失败: ${error}`); + } + }; + + // 显示Confirm + const handleShowConfirm = async () => { + setLoading(true); + try { + const confirmed = await bridge.showConfirm( + "确认操作", + "确定要执行此测试操作吗?", + ); + setLastResult({ confirmed }); + if (confirmed) { + addLog("用户确认了操作"); + } else { + addLog("用户取消了操作"); + } + } catch (error) { + addLog(`显示Confirm失败: ${error}`); + } finally { + setLoading(false); + } + }; + + // 分享内容 + const handleShare = async () => { + setLoading(true); + try { + const shareData: ShareData = { + title: "UniApp桥接测试", + content: "这是一个UniApp WebView桥接功能的测试页面", + url: window.location.href, + image: "https://via.placeholder.com/300x200/4CAF50/FFFFFF?text=Test", + }; + + const success = await bridge.share(shareData); + setLastResult({ success }); + if (success) { + addLog("分享成功"); + } else { + addLog("分享失败"); + } + } catch (error) { + addLog(`分享失败: ${error}`); + } finally { + setLoading(false); + } + }; + + // 支付测试 + const handlePayment = async () => { + setLoading(true); + try { + const paymentData: PaymentData = { + amount: 100, + orderId: "test_order_" + Date.now(), + description: "测试支付", + currency: "CNY", + }; + + const success = await bridge.payment(paymentData); + setLastResult({ success, orderId: paymentData.orderId }); + if (success) { + addLog(`支付成功,订单ID: ${paymentData.orderId}`); + } else { + addLog("支付失败"); + } + } catch (error) { + addLog(`支付失败: ${error}`); + } finally { + setLoading(false); + } + }; + + // 发送自定义消息 + const handleCustomMessage = async () => { + try { + bridge.sendCustomMessage("testAction", { + action: "test", + timestamp: Date.now(), + data: { message: "Hello from React!" }, + }); + addLog("已发送自定义消息"); + } catch (error) { + addLog(`发送自定义消息失败: ${error}`); + } + }; + + // 页面导航 + const handleNavigate = async () => { + try { + bridge.navigate("https://example.com"); + addLog("已请求页面导航"); + } catch (error) { + addLog(`页面导航失败: ${error}`); + } + }; + + return ( + }> +
+ {/* 状态指示器 */} + + {isReady ? ( + <> + + 桥接已就绪 + + ) : ( + <> + + 桥接初始化中... + + )} + + } + type={isReady ? "success" : "info"} + showIcon={false} + style={{ marginBottom: "20px" }} + /> + + + {/* 左侧:功能测试 */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* 右侧:信息显示 */} + + {/* 用户信息 */} + {userInfo && ( + + + } /> +
+
+ {userInfo.name} +
+
+ ID: {userInfo.id} +
+ {userInfo.phone && ( +
+ 电话: {userInfo.phone} +
+ )} + {userInfo.email && ( +
+ 邮箱: {userInfo.email} +
+ )} +
+
+
+ )} + + {/* 设备信息 */} + {deviceInfo && ( + + +
+ 平台:{" "} + {deviceInfo.platform} +
+
+ 型号: {deviceInfo.model} +
+
+ 版本: {deviceInfo.version} +
+
+ 应用:{" "} + + {deviceInfo.appName} v{deviceInfo.appVersion} + +
+
+ 屏幕:{" "} + + {deviceInfo.screenWidth} × {deviceInfo.screenHeight} + +
+
+
+ )} + + {/* 最后结果 */} + {lastResult && ( + +
+                  {JSON.stringify(lastResult, null, 2)}
+                
+
+ )} + +
+ + {/* 日志区域 */} + +
+ {logs.length === 0 ? ( + 暂无日志 + ) : ( + logs.map((log, index) => ( +
+ {log} +
+ )) + )} +
+
+ + {/* 使用说明 */} + + + 功能说明: + +
    +
  • + 此页面用于测试UniApp WebView桥接功能 +
  • +
  • + 点击按钮可以测试各种桥接方法 +
  • +
  • + 右侧会显示接收到的用户信息和设备信息 +
  • +
  • + 底部日志区域会显示所有操作记录 +
  • +
  • + 确保在UniApp WebView环境中使用以获得最佳效果 +
  • +
+
+
+
+ ); +}; + +export default PostMessageTest; diff --git a/nkebao/src/pages/mobile/component-test/index.tsx b/nkebao/src/pages/mobile/test/select.tsx similarity index 93% rename from nkebao/src/pages/mobile/component-test/index.tsx rename to nkebao/src/pages/mobile/test/select.tsx index 1334cda6..c8127880 100644 --- a/nkebao/src/pages/mobile/component-test/index.tsx +++ b/nkebao/src/pages/mobile/test/select.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { NavBar, Tabs } from "antd-mobile"; +import { NavBar, Tabs, Tag } from "antd-mobile"; import { ArrowLeftOutlined } from "@ant-design/icons"; import { useNavigate } from "react-router-dom"; import Layout from "@/components/Layout/Layout"; @@ -9,6 +9,7 @@ import FriendSelection from "@/components/FriendSelection"; import GroupSelection from "@/components/GroupSelection"; import ContentLibrarySelection from "@/components/ContentLibrarySelection"; import AccountSelection from "@/components/AccountSelection"; +import { isDevelopment } from "@/utils/env"; const ComponentTest: React.FC = () => { const navigate = useNavigate(); @@ -26,11 +27,19 @@ const ComponentTest: React.FC = () => { // 内容库选择状态 const [selectedLibraries, setSelectedLibraries] = useState([]); - const [selectedAccounts, setSelectedAccounts] = useState([]); + const [selectedAccounts, setSelectedAccounts] = useState([]); return ( }>
+ {isDevelopment && ( +
+ + 开发环境 - 组件测试 + +
+ )} +
diff --git a/nkebao/src/pages/mobile/workspace/main/index.module.scss b/nkebao/src/pages/mobile/workspace/main/index.module.scss index 4867a014..e8537ed6 100644 --- a/nkebao/src/pages/mobile/workspace/main/index.module.scss +++ b/nkebao/src/pages/mobile/workspace/main/index.module.scss @@ -1,7 +1,5 @@ .workspace { - padding: 16px; - background-color: #f5f5f5; - min-height: 100vh; + padding: 12px; } .section { diff --git a/nkebao/src/pages/mobile/workspace/main/index.tsx b/nkebao/src/pages/mobile/workspace/main/index.tsx index 2f443115..734a941e 100644 --- a/nkebao/src/pages/mobile/workspace/main/index.tsx +++ b/nkebao/src/pages/mobile/workspace/main/index.tsx @@ -3,19 +3,15 @@ import { Link } from "react-router-dom"; import { Card, NavBar, Badge } from "antd-mobile"; import { LikeOutlined, - MessageOutlined, SendOutlined, TeamOutlined, LinkOutlined, - AppstoreOutlined, - PieChartOutlined, - BarChartOutlined, ClockCircleOutlined, } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import MeauMobile from "@/components/MeauMobile/MeauMoible"; import styles from "./index.module.scss"; - +import NavCommon from "@/components/NavCommon"; const Workspace: React.FC = () => { // 常用功能 const commonFeatures = [ @@ -75,60 +71,9 @@ const Workspace: React.FC = () => { }, ]; - // AI智能助手 - const aiFeatures = [ - { - id: "ai-analyzer", - name: "AI数据分析", - description: "智能分析客户行为特征", - icon: ( - - ), - path: "/workspace/ai-analyzer", - bgColor: "#f0f0ff", - isNew: true, - }, - { - id: "ai-strategy", - name: "AI策略优化", - description: "智能优化获客策略", - icon: ( - - ), - path: "/workspace/ai-strategy", - bgColor: "#e6fffb", - isNew: true, - }, - { - id: "ai-forecast", - name: "AI销售预测", - description: "智能预测销售趋势", - icon: ( - - ), - path: "/workspace/ai-forecast", - bgColor: "#fffbe6", - }, - ]; - return ( -
- 工作台 -
- - } + header={} title="工作台" />} footer={} >
diff --git a/nkebao/src/router/module/component-test.tsx b/nkebao/src/router/module/component-test.tsx deleted file mode 100644 index c2ccd384..00000000 --- a/nkebao/src/router/module/component-test.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import ComponentTest from "@/pages/mobile/component-test"; - -const componentTestRoutes = [ - { - path: "/component-test", - element: , - auth: true, - }, -]; - -export default componentTestRoutes; diff --git a/nkebao/src/router/module/test.tsx b/nkebao/src/router/module/test.tsx new file mode 100644 index 00000000..abd9baf5 --- /dev/null +++ b/nkebao/src/router/module/test.tsx @@ -0,0 +1,27 @@ +import SelectTest from "@/pages/mobile/test/select"; +import PostMessageTest from "@/pages/mobile/test/postMessage"; +import TestIndex from "@/pages/mobile/test/index"; +import { DEV_FEATURES } from "@/utils/env"; + +// 只在开发环境启用测试路由 +const componentTestRoutes = DEV_FEATURES.SHOW_TEST_PAGES + ? [ + { + path: "/test", + element: , + auth: true, + }, + { + path: "/test/select", + element: , + auth: true, + }, + { + path: "/test/postMessage", + element: , + auth: true, + }, + ] + : []; + +export default componentTestRoutes; diff --git a/nkebao/src/styles/global.scss b/nkebao/src/styles/global.scss index 261aaf87..ca4e01d0 100644 --- a/nkebao/src/styles/global.scss +++ b/nkebao/src/styles/global.scss @@ -160,6 +160,17 @@ textarea { } /* 6. 移动端/PC端兼容基础样式 */ + +// 安全区域CSS变量定义 +:root { + --safe-area-top: 0px; + --safe-area-bottom: 0px; + --safe-area-left: 0px; + --safe-area-right: 0px; + --status-bar-height: 0px; + --nav-bar-height: 44px; +} + html, body { height: 100%; diff --git a/nkebao/src/utils/env.ts b/nkebao/src/utils/env.ts new file mode 100644 index 00000000..f0bdd0d5 --- /dev/null +++ b/nkebao/src/utils/env.ts @@ -0,0 +1,46 @@ +// 环境配置 +export const isDevelopment = import.meta.env.DEV; +export const isProduction = import.meta.env.PROD; +export const isTest = import.meta.env.MODE === "test"; + +// 开发环境特性开关 +export const DEV_FEATURES = { + // 是否显示测试页面 + SHOW_TEST_PAGES: isDevelopment, + + // 是否启用调试日志 + ENABLE_DEBUG_LOGS: isDevelopment, + + // 是否显示开发工具 + SHOW_DEV_TOOLS: isDevelopment, + + // 是否启用Mock数据 + ENABLE_MOCK_DATA: isDevelopment, +}; + +// 获取环境变量 +export const getEnvVar = (key: string, defaultValue?: string): string => { + return import.meta.env[key] || defaultValue || ""; +}; + +// 环境信息 +export const ENV_INFO = { + MODE: import.meta.env.MODE, + DEV: import.meta.env.DEV, + PROD: import.meta.env.PROD, + VITE_APP_TITLE: getEnvVar("VITE_APP_TITLE", "存客宝"), + VITE_API_BASE_URL: getEnvVar("VITE_API_BASE_URL", ""), + VITE_APP_VERSION: getEnvVar("VITE_APP_VERSION", "1.0.0"), +}; + +// 开发环境检查 +export const checkDevEnvironment = () => { + if (isDevelopment) { + console.log("🚀 开发环境已启用"); + console.log("📋 环境信息:", ENV_INFO); + console.log("⚙️ 开发特性:", DEV_FEATURES); + } +}; + +// 初始化环境检查 +checkDevEnvironment(); diff --git a/nkebao/src/utils/postMessage.ts b/nkebao/src/utils/postMessage.ts new file mode 100644 index 00000000..8ff281f9 --- /dev/null +++ b/nkebao/src/utils/postMessage.ts @@ -0,0 +1,634 @@ +// UniApp WebView 桥接工具类 +import { DEV_FEATURES } from "./env"; + +export interface BridgeMessage { + type: string; + data: any; + timestamp: number; +} + +export interface UserInfo { + id: string; + name: string; + avatar: string; + phone?: string; + email?: string; +} + +export interface DeviceInfo { + platform: string; + model: string; + version: string; + appVersion: string; + appName: string; + screenWidth: number; + screenHeight: number; + statusBarHeight: number; +} + +export interface ShareData { + title: string; + content: string; + url: string; + image?: string; +} + +export interface PaymentData { + amount: number; + orderId: string; + description?: string; + currency?: string; +} + +export interface ToastData { + message: string; + duration?: number; +} + +export interface AlertData { + title: string; + content: string; +} + +export interface ConfirmData { + title: string; + content: string; +} + +// 消息回调函数类型 +export type MessageCallback = (data: any) => void; + +// 消息类型枚举 +export enum MessageType { + GET_USER_INFO = "getUserInfo", + GET_DEVICE_INFO = "getDeviceInfo", + TOAST = "toast", + ALERT = "alert", + CONFIRM = "confirm", + SHARE = "share", + PAYMENT = "payment", + NAVIGATE = "navigate", + PAGE_LOADED = "pageLoaded", + PAGE_READY = "pageReady", + CUSTOM = "custom", +} + +// 响应消息类型 +export enum ResponseType { + USER_INFO = "userInfo", + DEVICE_INFO = "deviceInfo", + SHARE_RESULT = "shareResult", + PAYMENT_RESULT = "paymentResult", + CONFIRM_RESULT = "confirmResult", + NAVIGATE_RESULT = "navigateResult", +} + +class UniAppBridge { + private messageQueue: BridgeMessage[] = []; + private isReady = false; + private listeners: Map = new Map(); + private bridgeCheckInterval: number | null = null; + + constructor() { + (this as any)._startTime = Date.now(); + this.initBridge(); + } + + /** + * 初始化桥接 + */ + private initBridge(): void { + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log("🔧 开始初始化UniApp桥接"); + console.log("🔧 当前环境:", { + userAgent: navigator.userAgent, + location: window.location.href, + windowParent: !!window.parent, + windowTop: !!window.top, + }); + } + + // 检查桥接是否已存在 + if (window.uniAppBridge) { + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log("✅ 桥接已存在,直接使用"); + } + this.isReady = true; + this.setupMessageListener(); + return; + } + + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log("⏳ 桥接不存在,开始等待注入"); + } + + // 等待桥接注入 + this.waitForBridge(); + } + + /** + * 等待桥接注入 + */ + private waitForBridge(): void { + let retryCount = 0; + const maxRetries = 3; + const checkInterval = 200; // 增加检查间隔到200ms + const timeoutDuration = 30000; // 增加到30秒超时 + + this.bridgeCheckInterval = window.setInterval(() => { + if (window.uniAppBridge) { + this.isReady = true; + this.setupMessageListener(); + if (this.bridgeCheckInterval) { + clearInterval(this.bridgeCheckInterval); + this.bridgeCheckInterval = null; + } + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log("✅ UniApp桥接已就绪"); + } + return; + } + + // 增加重试逻辑 + retryCount++; + if (retryCount % 50 === 0) { + // 每10秒输出一次日志 + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log( + `⏳ 等待UniApp桥接注入... (${(retryCount * checkInterval) / 1000}s)`, + ); + } + } + }, checkInterval); + + // 设置超时 + setTimeout(() => { + if (this.bridgeCheckInterval) { + clearInterval(this.bridgeCheckInterval); + this.bridgeCheckInterval = null; + console.warn("❌ UniApp桥接初始化超时 (30秒)"); + + // 尝试手动触发桥接注入 + this.tryManualBridgeInjection(); + } + }, timeoutDuration); + } + + /** + * 尝试手动触发桥接注入 + */ + private tryManualBridgeInjection(): void { + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log("🔄 尝试手动触发桥接注入..."); + } + + // 发送页面准备就绪消息,可能触发UniApp重新注入桥接 + try { + window.parent.postMessage( + { + type: "pageReady", + data: { + url: window.location.href, + timestamp: Date.now(), + retry: true, + }, + }, + "*", + ); + } catch (error) { + console.error("手动触发桥接注入失败:", error); + } + + // 延迟后重新检查 + setTimeout(() => { + if (window.uniAppBridge) { + this.isReady = true; + this.setupMessageListener(); + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log("✅ 手动触发后桥接已就绪"); + } + } else { + console.error("❌ 手动触发后桥接仍未就绪"); + } + }, 2000); + } + + /** + * 设置消息监听器 + */ + private setupMessageListener(): void { + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log("🔧 设置UniApp消息监听器"); + } + + window.addEventListener("uniAppMessage", (event: any) => { + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log("📨 收到uniAppMessage事件:", event); + } + + try { + const { type, data } = event.detail; + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log("📨 解析消息:", type, data); + } + this.handleMessage(type, data); + } catch (error) { + console.error("❌ 处理uniAppMessage事件失败:", error); + } + }); + + // 同时监听原生message事件作为备用 + window.addEventListener("message", (event: any) => { + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log("📨 收到原生message事件:", event.data); + } + + try { + if (event.data && event.data.type) { + const { type, data } = event.data; + + // 处理注入代码消息 + if (type === "injectCode") { + this.handleInjectCode(data); + return; + } + + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log("📨 解析原生消息:", type, data); + } + this.handleMessage(type, data); + } + } catch (error) { + console.error("❌ 处理原生message事件失败:", error); + } + }); + } + + /** + * 处理注入代码消息 + */ + private handleInjectCode(data: any): void { + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log("🔧 处理注入代码消息:", data); + } + + try { + if (data.code) { + // 执行注入的代码 + const script = document.createElement("script"); + script.textContent = data.code; + document.head.appendChild(script); + document.head.removeChild(script); + + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log("✅ 代码注入成功"); + } + } + } catch (error) { + console.error("❌ 代码注入失败:", error); + } + } + + /** + * 处理接收到的消息 + */ + private handleMessage(type: string, data: any): void { + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log("📨 处理UniApp消息:", type, data); + } + + // 触发对应的事件监听器 + if (this.listeners.has(type)) { + const callbacks = this.listeners.get(type); + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log(`📨 触发${type}事件监听器, 回调数量:`, callbacks?.length); + } + + callbacks?.forEach((callback, index) => { + try { + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log(`📨 执行回调${index + 1}:`, callback); + } + callback(data); + } catch (error) { + console.error(`❌ 处理消息回调失败 (${type}):`, error); + } + }); + } else { + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log(`📨 未找到${type}事件的监听器`); + } + } + } + + /** + * 发送消息到UniApp + */ + private postMessage(type: string, data: any = {}): void { + if (!this.isReady) { + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.warn("⚠️ 桥接未就绪,消息已加入队列:", type, data); + } + this.messageQueue.push({ + type, + data, + timestamp: Date.now(), + }); + return; + } + + try { + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log("📤 发送消息到UniApp:", type, data); + } + + if (!window.uniAppBridge) { + console.error("❌ window.uniAppBridge 不存在"); + return; + } + + if (!window.uniAppBridge.postMessage) { + console.error("❌ window.uniAppBridge.postMessage 方法不存在"); + return; + } + + window.uniAppBridge.postMessage(type, data); + + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log("✅ 消息发送成功"); + } + } catch (error) { + console.error("❌ 发送消息失败:", error); + } + } + + /** + * 处理消息队列 + */ + private processMessageQueue(): void { + while (this.messageQueue.length > 0) { + const message = this.messageQueue.shift(); + if (message) { + this.postMessage(message.type, message.data); + } + } + } + + /** + * 添加消息监听器 + */ + public on(type: string, callback: MessageCallback): void { + if (!this.listeners.has(type)) { + this.listeners.set(type, []); + } + this.listeners.get(type)?.push(callback); + } + + /** + * 移除消息监听器 + */ + public off(type: string, callback?: MessageCallback): void { + if (!callback) { + this.listeners.delete(type); + } else { + const callbacks = this.listeners.get(type); + if (callbacks) { + const index = callbacks.indexOf(callback); + if (index > -1) { + callbacks.splice(index, 1); + } + } + } + } + + /** + * 获取用户信息 + */ + public getUserInfo(): void { + this.postMessage(MessageType.GET_USER_INFO); + } + + /** + * 获取设备信息 + */ + public getDeviceInfo(): void { + this.postMessage(MessageType.GET_DEVICE_INFO); + } + + /** + * 显示Toast提示 + */ + public showToast(message: string, duration: number = 2000): void { + this.postMessage(MessageType.TOAST, { message, duration }); + } + + /** + * 显示Alert对话框 + */ + public showAlert(title: string, content: string): void { + this.postMessage(MessageType.ALERT, { title, content }); + } + + /** + * 显示Confirm确认框 + */ + public showConfirm(title: string, content: string): Promise { + return new Promise(resolve => { + const handleConfirmResult = (data: any) => { + this.off(ResponseType.CONFIRM_RESULT, handleConfirmResult); + resolve(data.confirmed || false); + }; + + this.on(ResponseType.CONFIRM_RESULT, handleConfirmResult); + this.postMessage(MessageType.CONFIRM, { title, content }); + }); + } + + /** + * 分享内容 + */ + public share(data: ShareData): Promise { + return new Promise(resolve => { + const handleShareResult = (result: any) => { + this.off(ResponseType.SHARE_RESULT, handleShareResult); + resolve(result.success || false); + }; + + this.on(ResponseType.SHARE_RESULT, handleShareResult); + this.postMessage(MessageType.SHARE, data); + }); + } + + /** + * 支付 + */ + public payment(data: PaymentData): Promise { + return new Promise(resolve => { + const handlePaymentResult = (result: any) => { + this.off(ResponseType.PAYMENT_RESULT, handlePaymentResult); + resolve(result.success || false); + }; + + this.on(ResponseType.PAYMENT_RESULT, handlePaymentResult); + this.postMessage(MessageType.PAYMENT, data); + }); + } + + /** + * 页面导航 + */ + public navigate(url: string): void { + this.postMessage(MessageType.NAVIGATE, { url }); + } + + /** + * 发送自定义消息 + */ + public sendCustomMessage(type: string, data: any): void { + this.postMessage(type, data); + } + + /** + * 通知页面已准备就绪 + */ + public notifyPageReady(data?: any): void { + this.postMessage(MessageType.PAGE_READY, { + title: document.title, + url: window.location.href, + ...data, + }); + } + + /** + * 检查桥接是否就绪 + */ + public isBridgeReady(): boolean { + return this.isReady; + } + + /** + * 等待桥接就绪 + */ + public waitForReady(): Promise { + return new Promise((resolve, reject) => { + if (this.isReady) { + resolve(); + return; + } + + const maxWaitTime = 30000; // 30秒最大等待时间 + const checkInterval = 200; // 200ms检查间隔 + let elapsedTime = 0; + + const checkReady = () => { + if (this.isReady) { + resolve(); + return; + } + + elapsedTime += checkInterval; + if (elapsedTime >= maxWaitTime) { + reject(new Error(`桥接初始化超时 (${maxWaitTime / 1000}秒)`)); + return; + } + + setTimeout(checkReady, checkInterval); + }; + checkReady(); + }); + } + + /** + * 销毁桥接 + */ + public destroy(): void { + if (this.bridgeCheckInterval) { + clearInterval(this.bridgeCheckInterval); + this.bridgeCheckInterval = null; + } + this.listeners.clear(); + this.messageQueue = []; + this.isReady = false; + } + + /** + * 获取桥接状态信息 + */ + public getBridgeStatus(): { + isReady: boolean; + bridgeExists: boolean; + messageQueueLength: number; + listenersCount: number; + uptime: number; + } { + return { + isReady: this.isReady, + bridgeExists: !!window.uniAppBridge, + messageQueueLength: this.messageQueue.length, + listenersCount: this.listeners.size, + uptime: Date.now() - (this as any)._startTime || 0, + }; + } + + /** + * 手动检查桥接状态 + */ + public checkBridgeStatus(): void { + const status = this.getBridgeStatus(); + if (DEV_FEATURES.ENABLE_DEBUG_LOGS) { + console.log("🔍 桥接状态检查:", status); + } + + if (!status.bridgeExists) { + console.warn("⚠️ window.uniAppBridge 不存在"); + } + + if (!status.isReady && status.bridgeExists) { + console.warn("⚠️ 桥接存在但未就绪"); + } + } +} + +// 创建全局桥接实例 +const bridge = new UniAppBridge(); + +// 导出桥接实例和类型 +export default bridge; +export { UniAppBridge }; + +// 导出便捷方法 +export const { + getUserInfo, + getDeviceInfo, + showToast, + showAlert, + showConfirm, + share, + payment, + navigate, + sendCustomMessage, + notifyPageReady, + isBridgeReady, + waitForReady, + on, + off, + destroy, + getBridgeStatus, + checkBridgeStatus, +} = bridge; + +// 声明全局类型 +declare global { + interface Window { + uniAppBridge?: { + postMessage: (type: string, data: any) => void; + getUserInfo: () => void; + getDeviceInfo: () => void; + showToast: (message: string, duration?: number) => void; + showAlert: (title: string, content: string) => void; + showConfirm: (title: string, content: string) => void; + share: (data: ShareData) => void; + payment: (data: PaymentData) => void; + }; + } +} diff --git a/nkebao/src/utils/safeArea.ts b/nkebao/src/utils/safeArea.ts new file mode 100644 index 00000000..440e02b3 --- /dev/null +++ b/nkebao/src/utils/safeArea.ts @@ -0,0 +1,176 @@ +// 安全区域高度计算工具 + +/** + * 获取设备的安全区域信息 + */ +export interface SafeAreaInfo { + top: number; + bottom: number; + left: number; + right: number; + statusBarHeight: number; + navBarHeight: number; +} + +/** + * 计算安全区域高度 + */ +export function getSafeAreaHeight(): SafeAreaInfo { + // 获取CSS环境变量 + const getCSSValue = (property: string): number => { + const value = getComputedStyle(document.documentElement).getPropertyValue( + property, + ); + return parseInt(value) || 0; + }; + + // 获取状态栏高度 + const statusBarHeight = + getCSSValue("--status-bar-height") || + getCSSValue("--sat") || + getCSSValue("--safe-area-inset-top") || + 0; + + // 获取底部安全区域高度 + const bottomSafeArea = getCSSValue("--safe-area-inset-bottom") || 0; + + // 获取左右安全区域 + const leftSafeArea = getCSSValue("--safe-area-inset-left") || 0; + const rightSafeArea = getCSSValue("--safe-area-inset-right") || 0; + + // 导航栏高度(通常是44px,但可能因设备而异) + const navBarHeight = 44; + + return { + top: statusBarHeight, + bottom: bottomSafeArea, + left: leftSafeArea, + right: rightSafeArea, + statusBarHeight, + navBarHeight, + }; +} + +/** + * 获取顶部安全区域高度(用于mobile-top-bar) + */ +export function getTopSafeAreaHeight(): number { + const safeArea = getSafeAreaHeight(); + + // 如果状态栏高度为0,尝试其他方法 + if (safeArea.statusBarHeight === 0) { + // 尝试从CSS变量获取 + const statusBar = getComputedStyle( + document.documentElement, + ).getPropertyValue("--status-bar-height"); + if (statusBar) { + return parseInt(statusBar) || 0; + } + + // 尝试从CSS变量获取安全区域 + const safeAreaTop = getComputedStyle( + document.documentElement, + ).getPropertyValue("--safe-area-inset-top"); + if (safeAreaTop) { + return parseInt(safeAreaTop) || 0; + } + + // 默认值(iPhone X及以后的设备通常为44px) + return 44; + } + + return safeArea.statusBarHeight; +} + +/** + * 动态设置CSS变量 + */ +export function setSafeAreaCSSVariables(): void { + const safeArea = getSafeAreaHeight(); + + // 设置CSS变量 + document.documentElement.style.setProperty( + "--safe-area-top", + `${safeArea.top}px`, + ); + document.documentElement.style.setProperty( + "--safe-area-bottom", + `${safeArea.bottom}px`, + ); + document.documentElement.style.setProperty( + "--safe-area-left", + `${safeArea.left}px`, + ); + document.documentElement.style.setProperty( + "--safe-area-right", + `${safeArea.right}px`, + ); + document.documentElement.style.setProperty( + "--status-bar-height", + `${safeArea.statusBarHeight}px`, + ); + document.documentElement.style.setProperty( + "--nav-bar-height", + `${safeArea.navBarHeight}px`, + ); +} + +/** + * 检测设备类型 + */ +export function getDeviceType(): "ios" | "android" | "unknown" { + const userAgent = navigator.userAgent.toLowerCase(); + + if (/iphone|ipad|ipod/.test(userAgent)) { + return "ios"; + } else if (/android/.test(userAgent)) { + return "android"; + } + + return "unknown"; +} + +/** + * 检测是否为刘海屏设备 + */ +export function isNotchDevice(): boolean { + const deviceType = getDeviceType(); + + if (deviceType === "ios") { + // iOS设备检测 + const screenHeight = window.screen.height; + const screenWidth = window.screen.width; + + // iPhone X及以后的设备 + return screenHeight >= 812 || screenWidth >= 812; + } else if (deviceType === "android") { + // Android设备检测 + const screenHeight = window.screen.height; + const screenWidth = window.screen.width; + + // 高分辨率设备可能是刘海屏 + return screenHeight >= 800 || screenWidth >= 800; + } + + return false; +} + +/** + * 初始化安全区域 + */ +export function initSafeArea(): void { + // 设置CSS变量 + setSafeAreaCSSVariables(); + + // 监听屏幕方向变化 + window.addEventListener("orientationchange", () => { + setTimeout(() => { + setSafeAreaCSSVariables(); + }, 100); + }); + + // 监听窗口大小变化 + window.addEventListener("resize", () => { + setSafeAreaCSSVariables(); + }); +} diff --git a/nkebao/vite.config.ts b/nkebao/vite.config.ts index 921473b6..eb0ce0a3 100644 --- a/nkebao/vite.config.ts +++ b/nkebao/vite.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ }, server: { open: true, + port: 3000, }, build: { chunkSizeWarningLimit: 2000,