- @chakra-ui/icons → lucide-react - react-icons → lucide-react - 涉及 49 个组件文件 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
210 lines
5.6 KiB
JavaScript
210 lines
5.6 KiB
JavaScript
/**
|
||
* 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 { ExternalLink, Copy, Check } from 'lucide-react';
|
||
import { isIOSDevice } from './hooks/useWechatEnvironment';
|
||
|
||
// 小程序 AppID
|
||
const MINIPROGRAM_APPID = 'wx0edeaab76d4fa414';
|
||
|
||
/**
|
||
* 生成明文 URL Scheme
|
||
* 格式:weixin://dl/business/?appid=APPID&path=PATH&query=QUERY
|
||
* 注意:path 不需要 URL encode,否则微信无法识别
|
||
*/
|
||
const generatePlainUrlScheme = (path, query) => {
|
||
let url = `weixin://dl/business/?appid=${MINIPROGRAM_APPID}&path=${path || ''}`;
|
||
if (query) {
|
||
url += `&query=${encodeURIComponent(query)}`;
|
||
}
|
||
return url;
|
||
};
|
||
|
||
/**
|
||
* 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] - 按钮属性
|
||
* @param {Object} [props.buttonStyle] - 按钮内联样式
|
||
*/
|
||
const UrlSchemeLauncher = ({
|
||
path = '',
|
||
query = '',
|
||
children,
|
||
onSuccess,
|
||
onError,
|
||
buttonProps = {},
|
||
buttonStyle = {},
|
||
}) => {
|
||
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(不需要后端 API)
|
||
const scheme = generatePlainUrlScheme(path, query);
|
||
console.log('[UrlSchemeLauncher] 生成的 scheme:', scheme);
|
||
|
||
setOpenlink(scheme);
|
||
|
||
// 尝试直接跳转
|
||
window.location.href = scheme;
|
||
const success = true;
|
||
|
||
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) {
|
||
window.location.href = openlink;
|
||
}
|
||
}, [openlink]);
|
||
|
||
return (
|
||
<>
|
||
<Button
|
||
onClick={handleLaunch}
|
||
isLoading={loading}
|
||
loadingText="正在跳转..."
|
||
colorScheme="green"
|
||
leftIcon={<Icon as={ExternalLink} />}
|
||
style={buttonStyle}
|
||
{...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={ExternalLink} />}
|
||
>
|
||
打开微信
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
width="100%"
|
||
onClick={handleCopy}
|
||
leftIcon={<Icon as={copied ? Check : Copy} />}
|
||
>
|
||
{copied ? '已复制' : '复制链接'}
|
||
</Button>
|
||
</VStack>
|
||
</ModalFooter>
|
||
</ModalContent>
|
||
</Modal>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default UrlSchemeLauncher;
|