FEAT => 更新项目配置和组件,添加安全区域支持,优化导航栏,移除不必要的组件测试页面。
This commit is contained in:
@@ -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<NavCommonProps> = ({ title, backFn, right }) => {
|
||||
const NavCommon: React.FC<NavCommonProps> = ({
|
||||
title,
|
||||
backFn,
|
||||
right,
|
||||
left,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const [topBarHeight, setTopBarHeight] = useState<number>(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 (
|
||||
<NavBar
|
||||
back={null}
|
||||
style={{ background: "#fff" }}
|
||||
left={
|
||||
<div className="nav-title">
|
||||
<ArrowLeftOutlined
|
||||
twoToneColor="#1677ff"
|
||||
onClick={() => {
|
||||
if (backFn) {
|
||||
backFn();
|
||||
} else {
|
||||
navigate(-1);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
right={right}
|
||||
>
|
||||
<span style={{ color: "var(--primary-color)", fontWeight: 600 }}>
|
||||
{title}
|
||||
</span>
|
||||
</NavBar>
|
||||
<>
|
||||
<div
|
||||
className="mobile-top-bar"
|
||||
style={{
|
||||
height: `${topBarHeight}px`,
|
||||
backgroundColor: "#fff",
|
||||
}}
|
||||
></div>
|
||||
<NavBar
|
||||
back={null}
|
||||
style={{ background: "#fff" }}
|
||||
left={
|
||||
left ? (
|
||||
left
|
||||
) : (
|
||||
<div className="nav-title">
|
||||
<ArrowLeftOutlined
|
||||
twoToneColor="#1677ff"
|
||||
onClick={() => {
|
||||
if (backFn) {
|
||||
backFn();
|
||||
} else {
|
||||
navigate(-1);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
right={right}
|
||||
>
|
||||
<span style={{ color: "var(--primary-color)", fontWeight: 600 }}>
|
||||
{title}
|
||||
</span>
|
||||
</NavBar>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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")!);
|
||||
|
||||
@@ -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 (
|
||||
<Layout
|
||||
header={
|
||||
<NavBar back={null} style={{ background: "#fff" }}>
|
||||
<div className={style["nav-title"]}>
|
||||
<span className={style["nav-text"]}>存客宝</span>
|
||||
</div>
|
||||
</NavBar>
|
||||
}
|
||||
header={<NavCommon left={<></>} title="存客宝" />}
|
||||
footer={<MeauMobile activeKey="home" />}
|
||||
loading={isLoading}
|
||||
>
|
||||
|
||||
@@ -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: <MessageOutlined />,
|
||||
count: 0,
|
||||
path: "/test/postMessage",
|
||||
bgColor: "#fff7e6",
|
||||
iconColor: "#fa8c16",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
// 加载统计数据
|
||||
|
||||
@@ -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 (
|
||||
<Layout
|
||||
header={
|
||||
<NavBar back={null} style={{ background: "#fff" }}>
|
||||
<div className="nav-title">场景获客</div>
|
||||
<Button size="small" color="primary" onClick={handleNewPlan}>
|
||||
<PlusOutlined /> 新建计划
|
||||
</Button>
|
||||
</NavBar>
|
||||
<NavCommon
|
||||
left={<></>}
|
||||
title="场景获客"
|
||||
right={
|
||||
<Button size="small" color="primary" onClick={handleNewPlan}>
|
||||
<PlusOutlined /> 新建计划
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={style["error"]}>
|
||||
@@ -90,10 +94,9 @@ const Scene: React.FC = () => {
|
||||
<Layout
|
||||
loading={loading}
|
||||
header={
|
||||
<NavBar
|
||||
back={null}
|
||||
style={{ background: "#fff" }}
|
||||
<NavCommon
|
||||
left={<div className="nav-title">场景获客</div>}
|
||||
title={""}
|
||||
right={
|
||||
<Button
|
||||
size="small"
|
||||
@@ -104,7 +107,7 @@ const Scene: React.FC = () => {
|
||||
<PlusOutlined /> 新建计划
|
||||
</Button>
|
||||
}
|
||||
></NavBar>
|
||||
/>
|
||||
}
|
||||
footer={<MeauMobile activeKey="scenarios" />}
|
||||
>
|
||||
|
||||
373
nkebao/src/pages/mobile/test/README.md
Normal file
373
nkebao/src/pages/mobile/test/README.md
Normal file
@@ -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` 访问环境变量
|
||||
- 通信问题优先使用内置调试工具排查
|
||||
72
nkebao/src/pages/mobile/test/index.tsx
Normal file
72
nkebao/src/pages/mobile/test/index.tsx
Normal file
@@ -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 (
|
||||
<Layout header={<NavCommon title="测试页面" />}>
|
||||
<div style={{ padding: "20px", maxWidth: "800px", margin: "0 auto" }}>
|
||||
<Title level={2}>
|
||||
测试页面
|
||||
{isDevelopment && (
|
||||
<Tag color="orange" style={{ marginLeft: 8, fontSize: "12px" }}>
|
||||
开发环境
|
||||
</Tag>
|
||||
)}
|
||||
</Title>
|
||||
|
||||
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||
<Card title="组件测试" size="small">
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<MessageOutlined />}
|
||||
size="large"
|
||||
block
|
||||
onClick={() => navigate("/test/postMessage")}
|
||||
>
|
||||
UniApp桥接测试
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon={<SelectOutlined />}
|
||||
size="large"
|
||||
block
|
||||
onClick={() => navigate("/test/select")}
|
||||
>
|
||||
选择组件测试
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon={<SafetyOutlined />}
|
||||
size="large"
|
||||
block
|
||||
onClick={() => navigate("/test/safeArea")}
|
||||
>
|
||||
安全区域测试
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card title="说明" size="small">
|
||||
<Text>这里提供各种功能的测试页面,方便开发和调试。</Text>
|
||||
</Card>
|
||||
</Space>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestIndex;
|
||||
646
nkebao/src/pages/mobile/test/postMessage.tsx
Normal file
646
nkebao/src/pages/mobile/test/postMessage.tsx
Normal file
@@ -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<UserInfo | null>(null);
|
||||
const [deviceInfo, setDeviceInfo] = useState<DeviceInfo | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
const [lastResult, setLastResult] = useState<any>(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 (
|
||||
<Layout header={<NavCommon title="UniApp桥接测试" />}>
|
||||
<div style={{ padding: "20px" }}>
|
||||
{/* 状态指示器 */}
|
||||
<Alert
|
||||
message={
|
||||
<Space>
|
||||
{isReady ? (
|
||||
<>
|
||||
<CheckCircleOutlined style={{ color: "#52c41a" }} />
|
||||
<Text strong>桥接已就绪</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LoadingOutlined />
|
||||
<Text>桥接初始化中...</Text>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
type={isReady ? "success" : "info"}
|
||||
showIcon={false}
|
||||
style={{ marginBottom: "20px" }}
|
||||
/>
|
||||
|
||||
<Row gutter={[20, 20]}>
|
||||
{/* 左侧:功能测试 */}
|
||||
<Col xs={24} lg={12}>
|
||||
<Card title="功能测试" size="small">
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<UserOutlined />}
|
||||
onClick={handleGetUserInfo}
|
||||
loading={loading}
|
||||
block
|
||||
>
|
||||
获取用户信息
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<MobileOutlined />}
|
||||
onClick={handleGetDeviceInfo}
|
||||
loading={loading}
|
||||
block
|
||||
>
|
||||
获取设备信息
|
||||
</Button>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Button
|
||||
icon={<MessageOutlined />}
|
||||
onClick={handleShowToast}
|
||||
block
|
||||
>
|
||||
显示Toast
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon={<ExclamationCircleOutlined />}
|
||||
onClick={handleShowAlert}
|
||||
block
|
||||
>
|
||||
显示Alert
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon={<CheckCircleOutlined />}
|
||||
onClick={handleShowConfirm}
|
||||
loading={loading}
|
||||
block
|
||||
>
|
||||
显示Confirm
|
||||
</Button>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Button
|
||||
icon={<ShareAltOutlined />}
|
||||
onClick={handleShare}
|
||||
loading={loading}
|
||||
block
|
||||
>
|
||||
分享内容
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon={<PayCircleOutlined />}
|
||||
onClick={handlePayment}
|
||||
loading={loading}
|
||||
block
|
||||
>
|
||||
支付测试
|
||||
</Button>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Button
|
||||
icon={<SendOutlined />}
|
||||
onClick={handleCustomMessage}
|
||||
block
|
||||
>
|
||||
发送自定义消息
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon={<InfoCircleOutlined />}
|
||||
onClick={handleNavigate}
|
||||
block
|
||||
>
|
||||
页面导航
|
||||
</Button>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Button
|
||||
icon={<InfoCircleOutlined />}
|
||||
onClick={() => {
|
||||
bridge.checkBridgeStatus();
|
||||
const status = bridge.getBridgeStatus();
|
||||
addLog(`桥接状态: ${JSON.stringify(status, null, 2)}`);
|
||||
}}
|
||||
block
|
||||
>
|
||||
检查桥接状态
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon={<SendOutlined />}
|
||||
onClick={() => {
|
||||
// 测试直接发送消息
|
||||
try {
|
||||
if (window.uniAppBridge) {
|
||||
window.uniAppBridge.postMessage("test", {
|
||||
message: "测试消息",
|
||||
});
|
||||
addLog("✅ 直接发送测试消息成功");
|
||||
} else {
|
||||
addLog("❌ window.uniAppBridge 不存在");
|
||||
}
|
||||
} catch (error) {
|
||||
addLog(`❌ 直接发送消息失败: ${error}`);
|
||||
}
|
||||
}}
|
||||
block
|
||||
>
|
||||
测试直接发送消息
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon={<InfoCircleOutlined />}
|
||||
onClick={() => {
|
||||
// 测试监听器状态
|
||||
const status = bridge.getBridgeStatus();
|
||||
addLog(`监听器数量: ${status.listenersCount}`);
|
||||
addLog(`消息队列长度: ${status.messageQueueLength}`);
|
||||
addLog(`桥接就绪: ${status.isReady}`);
|
||||
addLog(`桥接存在: ${status.bridgeExists}`);
|
||||
}}
|
||||
block
|
||||
>
|
||||
检查监听器状态
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon={<LoadingOutlined />}
|
||||
onClick={() => {
|
||||
// 强制重新初始化桥接
|
||||
addLog("🔄 强制重新初始化桥接...");
|
||||
bridge.destroy();
|
||||
|
||||
// 重新创建桥接实例
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// 重新初始化
|
||||
bridge
|
||||
.waitForReady()
|
||||
.then(() => {
|
||||
setIsReady(true);
|
||||
addLog("✅ 强制重新初始化成功");
|
||||
})
|
||||
.catch(error => {
|
||||
addLog(`❌ 强制重新初始化失败: ${error}`);
|
||||
});
|
||||
} catch (error) {
|
||||
addLog(`❌ 强制重新初始化异常: ${error}`);
|
||||
}
|
||||
}, 1000);
|
||||
}}
|
||||
block
|
||||
>
|
||||
强制重新初始化
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon={<InfoCircleOutlined />}
|
||||
onClick={() => {
|
||||
// 环境检测
|
||||
addLog("🔍 环境检测开始...");
|
||||
addLog(`用户代理: ${navigator.userAgent}`);
|
||||
addLog(`当前URL: ${window.location.href}`);
|
||||
addLog(`父窗口: ${!!window.parent}`);
|
||||
addLog(`顶层窗口: ${!!window.top}`);
|
||||
addLog(`是否在iframe中: ${window !== window.top}`);
|
||||
addLog(`window.uniAppBridge: ${!!window.uniAppBridge}`);
|
||||
|
||||
// 检查是否在UniApp环境中
|
||||
const isInUniApp =
|
||||
navigator.userAgent.includes("uni-app") ||
|
||||
window.location.href.includes("uni-app") ||
|
||||
!!window.uniAppBridge;
|
||||
addLog(`是否在UniApp环境: ${isInUniApp}`);
|
||||
|
||||
// 检查平台信息
|
||||
if (window.uniAppBridge) {
|
||||
addLog("✅ 桥接已存在,可以正常通信");
|
||||
} else {
|
||||
addLog("❌ 桥接不存在,可能不在UniApp环境中");
|
||||
}
|
||||
}}
|
||||
block
|
||||
>
|
||||
环境检测
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 右侧:信息显示 */}
|
||||
<Col xs={24} lg={12}>
|
||||
{/* 用户信息 */}
|
||||
{userInfo && (
|
||||
<Card
|
||||
title="用户信息"
|
||||
size="small"
|
||||
style={{ marginBottom: "20px" }}
|
||||
>
|
||||
<Space>
|
||||
<Avatar src={userInfo.avatar} icon={<UserOutlined />} />
|
||||
<div>
|
||||
<div>
|
||||
<Text strong>{userInfo.name}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text type="secondary">ID: {userInfo.id}</Text>
|
||||
</div>
|
||||
{userInfo.phone && (
|
||||
<div>
|
||||
<Text type="secondary">电话: {userInfo.phone}</Text>
|
||||
</div>
|
||||
)}
|
||||
{userInfo.email && (
|
||||
<div>
|
||||
<Text type="secondary">邮箱: {userInfo.email}</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 设备信息 */}
|
||||
{deviceInfo && (
|
||||
<Card
|
||||
title="设备信息"
|
||||
size="small"
|
||||
style={{ marginBottom: "20px" }}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<div>
|
||||
<Text strong>平台:</Text>{" "}
|
||||
<Tag color="blue">{deviceInfo.platform}</Tag>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>型号:</Text> <Text>{deviceInfo.model}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>版本:</Text> <Text>{deviceInfo.version}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>应用:</Text>{" "}
|
||||
<Text>
|
||||
{deviceInfo.appName} v{deviceInfo.appVersion}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>屏幕:</Text>{" "}
|
||||
<Text>
|
||||
{deviceInfo.screenWidth} × {deviceInfo.screenHeight}
|
||||
</Text>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 最后结果 */}
|
||||
{lastResult && (
|
||||
<Card
|
||||
title="最后操作结果"
|
||||
size="small"
|
||||
style={{ marginBottom: "20px" }}
|
||||
>
|
||||
<pre style={{ fontSize: "12px", margin: 0 }}>
|
||||
{JSON.stringify(lastResult, null, 2)}
|
||||
</pre>
|
||||
</Card>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 日志区域 */}
|
||||
<Card title="操作日志" size="small" style={{ marginTop: "20px" }}>
|
||||
<div
|
||||
style={{
|
||||
height: "200px",
|
||||
overflowY: "auto",
|
||||
backgroundColor: "#f5f5f5",
|
||||
padding: "10px",
|
||||
borderRadius: "4px",
|
||||
fontFamily: "monospace",
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
{logs.length === 0 ? (
|
||||
<Text type="secondary">暂无日志</Text>
|
||||
) : (
|
||||
logs.map((log, index) => (
|
||||
<div key={index} style={{ marginBottom: "4px" }}>
|
||||
{log}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 使用说明 */}
|
||||
<Card title="使用说明" size="small" style={{ marginTop: "20px" }}>
|
||||
<Paragraph>
|
||||
<Text strong>功能说明:</Text>
|
||||
</Paragraph>
|
||||
<ul>
|
||||
<li>
|
||||
<Text>此页面用于测试UniApp WebView桥接功能</Text>
|
||||
</li>
|
||||
<li>
|
||||
<Text>点击按钮可以测试各种桥接方法</Text>
|
||||
</li>
|
||||
<li>
|
||||
<Text>右侧会显示接收到的用户信息和设备信息</Text>
|
||||
</li>
|
||||
<li>
|
||||
<Text>底部日志区域会显示所有操作记录</Text>
|
||||
</li>
|
||||
<li>
|
||||
<Text>确保在UniApp WebView环境中使用以获得最佳效果</Text>
|
||||
</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostMessageTest;
|
||||
@@ -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<string[]>([]);
|
||||
|
||||
const [selectedAccounts, setSelectedAccounts] = useState<string[]>([]);
|
||||
const [selectedAccounts, setSelectedAccounts] = useState<number[]>([]);
|
||||
|
||||
return (
|
||||
<Layout header={<NavCommon title="组件调试" />}>
|
||||
<div style={{ padding: 16 }}>
|
||||
{isDevelopment && (
|
||||
<div style={{ marginBottom: 16, textAlign: "center" }}>
|
||||
<Tag color="orange" style={{ fontSize: "12px" }}>
|
||||
开发环境 - 组件测试
|
||||
</Tag>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||
<Tabs.Tab title="设备选择" key="devices">
|
||||
<div style={{ padding: "16px 0" }}>
|
||||
@@ -1,7 +1,5 @@
|
||||
.workspace {
|
||||
padding: 16px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.section {
|
||||
|
||||
@@ -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: (
|
||||
<BarChartOutlined
|
||||
className={styles.icon}
|
||||
style={{ color: "#531dab" }}
|
||||
/>
|
||||
),
|
||||
path: "/workspace/ai-analyzer",
|
||||
bgColor: "#f0f0ff",
|
||||
isNew: true,
|
||||
},
|
||||
{
|
||||
id: "ai-strategy",
|
||||
name: "AI策略优化",
|
||||
description: "智能优化获客策略",
|
||||
icon: (
|
||||
<AppstoreOutlined
|
||||
className={styles.icon}
|
||||
style={{ color: "#13c2c2" }}
|
||||
/>
|
||||
),
|
||||
path: "/workspace/ai-strategy",
|
||||
bgColor: "#e6fffb",
|
||||
isNew: true,
|
||||
},
|
||||
{
|
||||
id: "ai-forecast",
|
||||
name: "AI销售预测",
|
||||
description: "智能预测销售趋势",
|
||||
icon: (
|
||||
<PieChartOutlined
|
||||
className={styles.icon}
|
||||
style={{ color: "#d48806" }}
|
||||
/>
|
||||
),
|
||||
path: "/workspace/ai-forecast",
|
||||
bgColor: "#fffbe6",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavBar back={null} style={{ background: "#fff" }}>
|
||||
<div style={{ color: "var(--primary-color)", fontWeight: 600 }}>
|
||||
工作台
|
||||
</div>
|
||||
</NavBar>
|
||||
}
|
||||
header={<NavCommon left={<></>} title="工作台" />}
|
||||
footer={<MeauMobile activeKey="workspace" />}
|
||||
>
|
||||
<div className={styles.workspace}>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import ComponentTest from "@/pages/mobile/component-test";
|
||||
|
||||
const componentTestRoutes = [
|
||||
{
|
||||
path: "/component-test",
|
||||
element: <ComponentTest />,
|
||||
auth: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default componentTestRoutes;
|
||||
27
nkebao/src/router/module/test.tsx
Normal file
27
nkebao/src/router/module/test.tsx
Normal file
@@ -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: <TestIndex />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/test/select",
|
||||
element: <SelectTest />,
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/test/postMessage",
|
||||
element: <PostMessageTest />,
|
||||
auth: true,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
export default componentTestRoutes;
|
||||
@@ -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%;
|
||||
|
||||
46
nkebao/src/utils/env.ts
Normal file
46
nkebao/src/utils/env.ts
Normal file
@@ -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();
|
||||
634
nkebao/src/utils/postMessage.ts
Normal file
634
nkebao/src/utils/postMessage.ts
Normal file
@@ -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<string, MessageCallback[]> = 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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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;
|
||||
};
|
||||
}
|
||||
}
|
||||
176
nkebao/src/utils/safeArea.ts
Normal file
176
nkebao/src/utils/safeArea.ts
Normal file
@@ -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();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user