feat: 添加 H5 跳转小程序功能
- 后端: 新增 JS-SDK 签名接口和 URL Scheme 生成接口 - 前端: 创建 MiniProgramLauncher 组件,支持环境自适应 - 微信内 H5: 使用 wx-open-launch-weapp 开放标签 - 外部浏览器: 使用 URL Scheme 拉起微信 - PC 端: 显示小程序码引导扫码 - 引入微信 JS-SDK (jweixin-1.6.0.js) - 新增 miniprogramService 服务层封装 API 调用 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
143
src/components/MiniProgramLauncher/QRCodeDisplay.js
Normal file
143
src/components/MiniProgramLauncher/QRCodeDisplay.js
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 小程序码显示组件
|
||||
* 用于 PC 端显示小程序码供用户扫描
|
||||
*/
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
Text,
|
||||
Image,
|
||||
Button,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
useDisclosure,
|
||||
Icon,
|
||||
} from '@chakra-ui/react';
|
||||
import { FiSmartphone } from 'react-icons/fi';
|
||||
|
||||
// 默认小程序码图片(可替换为实际的小程序码)
|
||||
// 注意:需要在微信公众平台生成小程序码图片
|
||||
const DEFAULT_QR_CODE = 'https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=weixin://dl/business/?t=placeholder';
|
||||
|
||||
/**
|
||||
* PC 端小程序码显示组件
|
||||
* @param {Object} props
|
||||
* @param {string} [props.path] - 小程序页面路径(用于显示提示)
|
||||
* @param {string} [props.qrCodeUrl] - 小程序码图片 URL
|
||||
* @param {React.ReactNode} props.children - 按钮内容
|
||||
* @param {string} [props.title] - 弹窗标题
|
||||
* @param {string} [props.description] - 描述文案
|
||||
* @param {Object} [props.buttonProps] - 按钮属性
|
||||
*/
|
||||
const QRCodeDisplay = ({
|
||||
path = '',
|
||||
qrCodeUrl,
|
||||
children,
|
||||
title = '扫码打开小程序',
|
||||
description = '请使用微信扫描下方二维码',
|
||||
buttonProps = {},
|
||||
}) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
// 使用传入的二维码或默认二维码
|
||||
const qrCode = qrCodeUrl || DEFAULT_QR_CODE;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={onOpen}
|
||||
colorScheme="green"
|
||||
leftIcon={<Icon as={FiSmartphone} />}
|
||||
{...buttonProps}
|
||||
>
|
||||
{children || '打开小程序'}
|
||||
</Button>
|
||||
|
||||
<Modal isOpen={isOpen} onClose={onClose} isCentered size="sm">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader textAlign="center">{title}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody pb={6}>
|
||||
<VStack spacing={4}>
|
||||
{/* 小程序码图片 */}
|
||||
<Box
|
||||
p={4}
|
||||
bg="white"
|
||||
borderRadius="xl"
|
||||
boxShadow="lg"
|
||||
border="1px solid"
|
||||
borderColor="gray.100"
|
||||
>
|
||||
<Image
|
||||
src={qrCode}
|
||||
alt="小程序码"
|
||||
boxSize="200px"
|
||||
objectFit="contain"
|
||||
fallback={
|
||||
<Box
|
||||
boxSize="200px"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
bg="gray.100"
|
||||
borderRadius="md"
|
||||
>
|
||||
<Text color="gray.500" fontSize="sm">
|
||||
加载中...
|
||||
</Text>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 说明文案 */}
|
||||
<VStack spacing={1}>
|
||||
<Text color="gray.700" fontWeight="medium">
|
||||
{description}
|
||||
</Text>
|
||||
<Text color="gray.500" fontSize="sm">
|
||||
打开微信,扫一扫即可访问
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
{/* 提示信息 */}
|
||||
<Box
|
||||
w="100%"
|
||||
p={3}
|
||||
bg="green.50"
|
||||
borderRadius="md"
|
||||
border="1px solid"
|
||||
borderColor="green.100"
|
||||
>
|
||||
<VStack spacing={1} align="start">
|
||||
<Text fontSize="xs" color="green.700" fontWeight="medium">
|
||||
温馨提示
|
||||
</Text>
|
||||
<Text fontSize="xs" color="green.600">
|
||||
• 请确保手机已安装微信 App
|
||||
</Text>
|
||||
<Text fontSize="xs" color="green.600">
|
||||
• 扫码后点击"打开小程序"按钮
|
||||
</Text>
|
||||
{path && (
|
||||
<Text fontSize="xs" color="green.600">
|
||||
• 将跳转到: {path}
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
</VStack>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default QRCodeDisplay;
|
||||
193
src/components/MiniProgramLauncher/UrlSchemeLauncher.js
Normal file
193
src/components/MiniProgramLauncher/UrlSchemeLauncher.js
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* URL Scheme 跳转组件
|
||||
* 用于外部浏览器跳转小程序
|
||||
*/
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalCloseButton,
|
||||
Text,
|
||||
VStack,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
Icon,
|
||||
} from '@chakra-ui/react';
|
||||
import { FiExternalLink, FiCopy, FiCheck } from 'react-icons/fi';
|
||||
import { generateUrlScheme, openUrlScheme } from '@services/miniprogramService';
|
||||
import { isIOSDevice } from './hooks/useWechatEnvironment';
|
||||
|
||||
/**
|
||||
* URL Scheme 跳转组件
|
||||
* @param {Object} props
|
||||
* @param {string} [props.path] - 小程序页面路径
|
||||
* @param {string} [props.query] - 页面参数
|
||||
* @param {React.ReactNode} props.children - 按钮内容
|
||||
* @param {Function} [props.onSuccess] - 跳转成功回调
|
||||
* @param {Function} [props.onError] - 跳转失败回调
|
||||
* @param {Object} [props.buttonProps] - 按钮属性
|
||||
*/
|
||||
const UrlSchemeLauncher = ({
|
||||
path = '',
|
||||
query = '',
|
||||
children,
|
||||
onSuccess,
|
||||
onError,
|
||||
buttonProps = {},
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [openlink, setOpenlink] = useState(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const toast = useToast();
|
||||
|
||||
const isIOS = isIOSDevice();
|
||||
|
||||
// 处理点击跳转
|
||||
const handleLaunch = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// 生成 URL Scheme
|
||||
const result = await generateUrlScheme({ path, query });
|
||||
|
||||
if (!result?.openlink) {
|
||||
throw new Error('生成跳转链接失败');
|
||||
}
|
||||
|
||||
setOpenlink(result.openlink);
|
||||
|
||||
// 尝试直接跳转
|
||||
const success = openUrlScheme(result.openlink);
|
||||
|
||||
if (success) {
|
||||
onSuccess?.();
|
||||
// iOS 上可能会弹出确认框,显示引导弹窗
|
||||
if (isIOS) {
|
||||
setTimeout(() => {
|
||||
onOpen();
|
||||
}, 500);
|
||||
}
|
||||
} else {
|
||||
// 跳转失败,显示引导弹窗
|
||||
onOpen();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[UrlSchemeLauncher] error:', error);
|
||||
toast({
|
||||
title: '跳转失败',
|
||||
description: error.message || '请稍后重试',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
onError?.(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [path, query, onSuccess, onError, onOpen, toast, isIOS]);
|
||||
|
||||
// 复制链接
|
||||
const handleCopy = useCallback(async () => {
|
||||
if (!openlink) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(openlink);
|
||||
setCopied(true);
|
||||
toast({
|
||||
title: '已复制',
|
||||
description: '请在微信中打开',
|
||||
status: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '复制失败',
|
||||
description: '请手动复制',
|
||||
status: 'error',
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
}, [openlink, toast]);
|
||||
|
||||
// 再次尝试跳转
|
||||
const handleRetry = useCallback(() => {
|
||||
if (openlink) {
|
||||
openUrlScheme(openlink);
|
||||
}
|
||||
}, [openlink]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={handleLaunch}
|
||||
isLoading={loading}
|
||||
loadingText="正在跳转..."
|
||||
colorScheme="green"
|
||||
leftIcon={<Icon as={FiExternalLink} />}
|
||||
{...buttonProps}
|
||||
>
|
||||
{children || '打开小程序'}
|
||||
</Button>
|
||||
|
||||
{/* 引导弹窗 */}
|
||||
<Modal isOpen={isOpen} onClose={onClose} isCentered size="sm">
|
||||
<ModalOverlay />
|
||||
<ModalContent mx={4}>
|
||||
<ModalHeader>打开小程序</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<VStack spacing={4} align="stretch">
|
||||
<Text color="gray.600" fontSize="sm">
|
||||
{isIOS
|
||||
? '如果没有自动跳转,请点击下方按钮重试'
|
||||
: '请在弹出的对话框中选择"打开微信"'}
|
||||
</Text>
|
||||
|
||||
<Box
|
||||
p={3}
|
||||
bg="gray.50"
|
||||
borderRadius="md"
|
||||
fontSize="xs"
|
||||
color="gray.500"
|
||||
>
|
||||
<Text fontWeight="medium" mb={1}>提示:</Text>
|
||||
<Text>1. 确保已安装微信</Text>
|
||||
<Text>2. 点击"打开微信"按钮</Text>
|
||||
<Text>3. 如果没有反应,请复制链接到微信打开</Text>
|
||||
</Box>
|
||||
</VStack>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<VStack spacing={2} width="100%">
|
||||
<Button
|
||||
colorScheme="green"
|
||||
width="100%"
|
||||
onClick={handleRetry}
|
||||
leftIcon={<Icon as={FiExternalLink} />}
|
||||
>
|
||||
打开微信
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
width="100%"
|
||||
onClick={handleCopy}
|
||||
leftIcon={<Icon as={copied ? FiCheck : FiCopy} />}
|
||||
>
|
||||
{copied ? '已复制' : '复制链接'}
|
||||
</Button>
|
||||
</VStack>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UrlSchemeLauncher;
|
||||
203
src/components/MiniProgramLauncher/WxOpenLaunchWeapp.js
Normal file
203
src/components/MiniProgramLauncher/WxOpenLaunchWeapp.js
Normal file
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* 微信开放标签封装组件
|
||||
* 用于在微信内 H5 跳转小程序
|
||||
*/
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Box, Spinner, Text, Button as ChakraButton } from '@chakra-ui/react';
|
||||
import { getJsSdkConfig } from '@services/miniprogramService';
|
||||
|
||||
// 小程序原始 ID
|
||||
const MINIPROGRAM_ORIGINAL_ID = 'gh_fd2fd8dd2fb5';
|
||||
|
||||
/**
|
||||
* 微信开放标签组件
|
||||
* @param {Object} props
|
||||
* @param {string} [props.path] - 小程序页面路径
|
||||
* @param {string} [props.query] - 页面参数
|
||||
* @param {React.ReactNode} props.children - 按钮内容
|
||||
* @param {Function} [props.onLaunch] - 跳转成功回调
|
||||
* @param {Function} [props.onError] - 跳转失败回调
|
||||
* @param {Object} [props.buttonStyle] - 按钮样式
|
||||
*/
|
||||
const WxOpenLaunchWeapp = ({
|
||||
path = '',
|
||||
query = '',
|
||||
children,
|
||||
onLaunch,
|
||||
onError,
|
||||
buttonStyle = {},
|
||||
}) => {
|
||||
const [ready, setReady] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const launchBtnRef = useRef(null);
|
||||
|
||||
// 初始化微信 JS-SDK
|
||||
const initWxSdk = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 获取当前页面 URL(不含 hash)
|
||||
const currentUrl = window.location.href.split('#')[0];
|
||||
|
||||
// 获取签名配置
|
||||
const config = await getJsSdkConfig(currentUrl);
|
||||
|
||||
if (!config) {
|
||||
throw new Error('获取签名配置失败');
|
||||
}
|
||||
|
||||
// 检查 wx 对象是否存在
|
||||
if (typeof wx === 'undefined') {
|
||||
throw new Error('微信 JS-SDK 未加载');
|
||||
}
|
||||
|
||||
// 配置 wx
|
||||
wx.config({
|
||||
debug: false,
|
||||
appId: config.appId,
|
||||
timestamp: config.timestamp,
|
||||
nonceStr: config.nonceStr,
|
||||
signature: config.signature,
|
||||
jsApiList: config.jsApiList || [],
|
||||
openTagList: config.openTagList || ['wx-open-launch-weapp'],
|
||||
});
|
||||
|
||||
// 监听 ready 事件
|
||||
wx.ready(() => {
|
||||
console.log('[WxOpenLaunchWeapp] wx.ready');
|
||||
setReady(true);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
// 监听 error 事件
|
||||
wx.error((err) => {
|
||||
console.error('[WxOpenLaunchWeapp] wx.error:', err);
|
||||
setError(err.errMsg || '初始化失败');
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('[WxOpenLaunchWeapp] initWxSdk error:', err);
|
||||
setError(err.message || '初始化失败');
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
initWxSdk();
|
||||
}, [initWxSdk]);
|
||||
|
||||
// 监听开放标签事件
|
||||
useEffect(() => {
|
||||
const btn = launchBtnRef.current;
|
||||
if (!btn) return;
|
||||
|
||||
const handleLaunch = (e) => {
|
||||
console.log('[WxOpenLaunchWeapp] launch success:', e.detail);
|
||||
onLaunch?.(e.detail);
|
||||
};
|
||||
|
||||
const handleError = (e) => {
|
||||
console.error('[WxOpenLaunchWeapp] launch error:', e.detail);
|
||||
onError?.(e.detail);
|
||||
};
|
||||
|
||||
btn.addEventListener('launch', handleLaunch);
|
||||
btn.addEventListener('error', handleError);
|
||||
|
||||
return () => {
|
||||
btn.removeEventListener('launch', handleLaunch);
|
||||
btn.removeEventListener('error', handleError);
|
||||
};
|
||||
}, [ready, onLaunch, onError]);
|
||||
|
||||
// 构建小程序路径
|
||||
const mpPath = query ? `${path}?${query}` : path;
|
||||
|
||||
// 默认按钮样式
|
||||
const defaultButtonStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '12px 24px',
|
||||
backgroundColor: '#07c160',
|
||||
color: '#ffffff',
|
||||
fontSize: '16px',
|
||||
fontWeight: '500',
|
||||
borderRadius: '8px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
width: '100%',
|
||||
...buttonStyle,
|
||||
};
|
||||
|
||||
// 加载中状态
|
||||
if (loading) {
|
||||
return (
|
||||
<Box display="flex" alignItems="center" justifyContent="center" py={3}>
|
||||
<Spinner size="sm" mr={2} />
|
||||
<Text fontSize="sm" color="gray.500">正在初始化...</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// 错误状态
|
||||
if (error) {
|
||||
return (
|
||||
<ChakraButton
|
||||
colorScheme="gray"
|
||||
isDisabled
|
||||
width="100%"
|
||||
py={3}
|
||||
>
|
||||
{error}
|
||||
</ChakraButton>
|
||||
);
|
||||
}
|
||||
|
||||
// 渲染开放标签
|
||||
if (ready) {
|
||||
// 微信开放标签需要使用纯 HTML 字符串,不支持 JSX
|
||||
const buttonText = typeof children === 'string' ? children : '打开小程序';
|
||||
const htmlContent = `
|
||||
<style>
|
||||
.wx-launch-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: ${defaultButtonStyle.padding};
|
||||
background-color: ${defaultButtonStyle.backgroundColor};
|
||||
color: ${defaultButtonStyle.color};
|
||||
font-size: ${defaultButtonStyle.fontSize};
|
||||
font-weight: ${defaultButtonStyle.fontWeight};
|
||||
border-radius: ${defaultButtonStyle.borderRadius};
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
<button class="wx-launch-btn">${buttonText}</button>
|
||||
`;
|
||||
|
||||
return (
|
||||
<Box position="relative">
|
||||
<wx-open-launch-weapp
|
||||
ref={launchBtnRef}
|
||||
id="launch-btn"
|
||||
username={MINIPROGRAM_ORIGINAL_ID}
|
||||
path={mpPath}
|
||||
style={{ display: 'block', width: '100%' }}
|
||||
>
|
||||
<script type="text/wxtag-template" dangerouslySetInnerHTML={{ __html: htmlContent }} />
|
||||
</wx-open-launch-weapp>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default WxOpenLaunchWeapp;
|
||||
118
src/components/MiniProgramLauncher/hooks/useWechatEnvironment.js
Normal file
118
src/components/MiniProgramLauncher/hooks/useWechatEnvironment.js
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* 微信环境检测 Hook
|
||||
* 用于判断当前运行环境,选择合适的小程序跳转方式
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
|
||||
/**
|
||||
* 跳转方式枚举
|
||||
*/
|
||||
export const LAUNCH_METHOD = {
|
||||
OPEN_TAG: 'openTag', // 微信内使用开放标签
|
||||
URL_SCHEME: 'urlScheme', // 外部浏览器使用 URL Scheme
|
||||
QR_CODE: 'qrCode', // PC 端显示小程序码
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测是否在微信浏览器内
|
||||
*/
|
||||
export const isWeChatBrowser = () => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
return ua.includes('micromessenger');
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测是否为移动端设备
|
||||
*/
|
||||
export const isMobileDevice = () => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
const ua = navigator.userAgent;
|
||||
return /iPhone|iPad|iPod|Android|webOS|BlackBerry|IEMobile|Opera Mini/i.test(ua);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测是否为 iOS 设备
|
||||
*/
|
||||
export const isIOSDevice = () => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
const ua = navigator.userAgent;
|
||||
return /iPhone|iPad|iPod/i.test(ua);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测是否为 Android 设备
|
||||
*/
|
||||
export const isAndroidDevice = () => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
const ua = navigator.userAgent;
|
||||
return /Android/i.test(ua);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取微信版本号
|
||||
* @returns {string|null} 版本号字符串,如 "7.0.12"
|
||||
*/
|
||||
export const getWeChatVersion = () => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
const match = ua.match(/micromessenger\/(\d+\.\d+\.\d+)/i);
|
||||
return match ? match[1] : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测微信版本是否支持开放标签
|
||||
* 开放标签需要微信 7.0.12 及以上版本
|
||||
*/
|
||||
export const isOpenTagSupported = () => {
|
||||
const version = getWeChatVersion();
|
||||
if (!version) return false;
|
||||
|
||||
const [major, minor, patch] = version.split('.').map(Number);
|
||||
// 7.0.12 及以上支持
|
||||
if (major > 7) return true;
|
||||
if (major === 7) {
|
||||
if (minor > 0) return true;
|
||||
if (minor === 0 && patch >= 12) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 微信环境检测 Hook
|
||||
* @returns {Object} 环境信息对象
|
||||
*/
|
||||
export const useWechatEnvironment = () => {
|
||||
const environment = useMemo(() => {
|
||||
const isWechat = isWeChatBrowser();
|
||||
const isMobile = isMobileDevice();
|
||||
const isIOS = isIOSDevice();
|
||||
const isAndroid = isAndroidDevice();
|
||||
const wechatVersion = getWeChatVersion();
|
||||
const openTagSupported = isOpenTagSupported();
|
||||
|
||||
// 确定跳转方式
|
||||
let launchMethod;
|
||||
if (isWechat && openTagSupported) {
|
||||
launchMethod = LAUNCH_METHOD.OPEN_TAG;
|
||||
} else if (isMobile) {
|
||||
launchMethod = LAUNCH_METHOD.URL_SCHEME;
|
||||
} else {
|
||||
launchMethod = LAUNCH_METHOD.QR_CODE;
|
||||
}
|
||||
|
||||
return {
|
||||
isWechat, // 是否在微信内
|
||||
isMobile, // 是否移动端
|
||||
isIOS, // 是否 iOS
|
||||
isAndroid, // 是否 Android
|
||||
wechatVersion, // 微信版本
|
||||
openTagSupported, // 是否支持开放标签
|
||||
launchMethod, // 推荐的跳转方式
|
||||
};
|
||||
}, []);
|
||||
|
||||
return environment;
|
||||
};
|
||||
|
||||
export default useWechatEnvironment;
|
||||
113
src/components/MiniProgramLauncher/index.js
Normal file
113
src/components/MiniProgramLauncher/index.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 小程序跳转组件
|
||||
* 自动根据环境选择合适的跳转方式:
|
||||
* - 微信内:使用开放标签 wx-open-launch-weapp
|
||||
* - 外部浏览器(移动端):使用 URL Scheme
|
||||
* - PC 浏览器:显示小程序码
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import useWechatEnvironment, { LAUNCH_METHOD } from './hooks/useWechatEnvironment';
|
||||
import WxOpenLaunchWeapp from './WxOpenLaunchWeapp';
|
||||
import UrlSchemeLauncher from './UrlSchemeLauncher';
|
||||
import QRCodeDisplay from './QRCodeDisplay';
|
||||
|
||||
/**
|
||||
* 小程序跳转统一入口组件
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {string} [props.path] - 小程序页面路径,如 "/pages/index/index"
|
||||
* @param {string} [props.query] - 页面参数,如 "id=123&from=h5"
|
||||
* @param {React.ReactNode} props.children - 按钮内容
|
||||
* @param {string} [props.qrCodeUrl] - PC 端显示的小程序码图片 URL
|
||||
* @param {Function} [props.onSuccess] - 跳转成功回调
|
||||
* @param {Function} [props.onError] - 跳转失败回调
|
||||
* @param {Object} [props.buttonProps] - 传递给按钮的属性
|
||||
* @param {Object} [props.buttonStyle] - 微信开放标签按钮样式
|
||||
* @param {string} [props.forceLaunchMethod] - 强制使用指定的跳转方式(调试用)
|
||||
*
|
||||
* @example
|
||||
* // 基础用法
|
||||
* <MiniProgramLauncher path="/pages/pay/index" query="order_id=123">
|
||||
* 去小程序支付
|
||||
* </MiniProgramLauncher>
|
||||
*
|
||||
* @example
|
||||
* // 带回调
|
||||
* <MiniProgramLauncher
|
||||
* path="/pages/index/index"
|
||||
* onSuccess={() => console.log('跳转成功')}
|
||||
* onError={(err) => console.error('跳转失败', err)}
|
||||
* >
|
||||
* 打开小程序
|
||||
* </MiniProgramLauncher>
|
||||
*/
|
||||
const MiniProgramLauncher = ({
|
||||
path = '',
|
||||
query = '',
|
||||
children = '打开小程序',
|
||||
qrCodeUrl,
|
||||
onSuccess,
|
||||
onError,
|
||||
buttonProps = {},
|
||||
buttonStyle = {},
|
||||
forceLaunchMethod,
|
||||
}) => {
|
||||
const { launchMethod } = useWechatEnvironment();
|
||||
|
||||
// 使用强制指定的方式或自动检测的方式
|
||||
const method = forceLaunchMethod || launchMethod;
|
||||
|
||||
// 根据环境选择组件
|
||||
const renderLauncher = () => {
|
||||
switch (method) {
|
||||
case LAUNCH_METHOD.OPEN_TAG:
|
||||
return (
|
||||
<WxOpenLaunchWeapp
|
||||
path={path}
|
||||
query={query}
|
||||
onLaunch={onSuccess}
|
||||
onError={onError}
|
||||
buttonStyle={buttonStyle}
|
||||
>
|
||||
{children}
|
||||
</WxOpenLaunchWeapp>
|
||||
);
|
||||
|
||||
case LAUNCH_METHOD.URL_SCHEME:
|
||||
return (
|
||||
<UrlSchemeLauncher
|
||||
path={path}
|
||||
query={query}
|
||||
onSuccess={onSuccess}
|
||||
onError={onError}
|
||||
buttonProps={buttonProps}
|
||||
>
|
||||
{children}
|
||||
</UrlSchemeLauncher>
|
||||
);
|
||||
|
||||
case LAUNCH_METHOD.QR_CODE:
|
||||
default:
|
||||
return (
|
||||
<QRCodeDisplay
|
||||
path={path}
|
||||
qrCodeUrl={qrCodeUrl}
|
||||
buttonProps={buttonProps}
|
||||
>
|
||||
{children}
|
||||
</QRCodeDisplay>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return <Box>{renderLauncher()}</Box>;
|
||||
};
|
||||
|
||||
// 导出子组件和工具函数,方便单独使用
|
||||
export { default as WxOpenLaunchWeapp } from './WxOpenLaunchWeapp';
|
||||
export { default as UrlSchemeLauncher } from './UrlSchemeLauncher';
|
||||
export { default as QRCodeDisplay } from './QRCodeDisplay';
|
||||
export { default as useWechatEnvironment, LAUNCH_METHOD } from './hooks/useWechatEnvironment';
|
||||
|
||||
export default MiniProgramLauncher;
|
||||
Reference in New Issue
Block a user