Files
vf_react/src/components/MiniProgramLauncher/WxOpenLaunchWeapp.js
zdl 154bb76212 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>
2025-12-12 16:56:04 +08:00

204 lines
5.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 微信开放标签封装组件
* 用于在微信内 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;