- 后端: 新增 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>
204 lines
5.3 KiB
JavaScript
204 lines
5.3 KiB
JavaScript
/**
|
||
* 微信开放标签封装组件
|
||
* 用于在微信内 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;
|