From 325ca2b796b8f5ce5a597f7364067ce95d2a426a Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Sun, 28 Dec 2025 22:53:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=8B=E4=BB=B6=E7=9A=84?= =?UTF-8?q?=E5=88=86=E4=BA=AB=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EventDetailPanel/CompactMetaBar.js | 13 +- .../EventDetailPanel/EventHeaderInfo.js | 33 ++- .../RelatedConceptsSection/TradingDateInfo.js | 3 +- .../EventDetailPanel/StockListItem.js | 3 +- src/components/ShareButton/index.js | 252 ++++++++++++++++++ src/hooks/useWechatShare.js | 232 ++++++++++++++++ .../EventCard/atoms/EventEngagement.js | 3 +- 7 files changed, 524 insertions(+), 15 deletions(-) create mode 100644 src/components/ShareButton/index.js create mode 100644 src/hooks/useWechatShare.js diff --git a/src/components/EventDetailPanel/CompactMetaBar.js b/src/components/EventDetailPanel/CompactMetaBar.js index 3148ebf6..fa6f0f98 100644 --- a/src/components/EventDetailPanel/CompactMetaBar.js +++ b/src/components/EventDetailPanel/CompactMetaBar.js @@ -11,6 +11,7 @@ import { } from '@chakra-ui/react'; import { EventFollowButton } from '@views/Community/components/EventCard/atoms'; import { Eye } from 'lucide-react'; +import ShareButton from '@components/ShareButton'; /** * 精简信息栏组件 @@ -79,7 +80,7 @@ const CompactMetaBar = ({ event, importance, isFollowing, followerCount, onToggl borderRadius="md" boxShadow="sm" > - + {(event.view_count || 0).toLocaleString()} @@ -93,6 +94,16 @@ const CompactMetaBar = ({ event, importance, isFollowing, followerCount, onToggl size="sm" showCount={true} /> + + {/* 分享按钮 */} + ); }; diff --git a/src/components/EventDetailPanel/EventHeaderInfo.js b/src/components/EventDetailPanel/EventHeaderInfo.js index 3f787ef0..d3f9528c 100644 --- a/src/components/EventDetailPanel/EventHeaderInfo.js +++ b/src/components/EventDetailPanel/EventHeaderInfo.js @@ -16,6 +16,7 @@ import dayjs from 'dayjs'; import { Eye } from 'lucide-react'; import StockChangeIndicators from '../StockChangeIndicators'; import { EventFollowButton } from '@views/Community/components/EventCard/atoms'; +import ShareButton from '@components/ShareButton'; /** * 事件头部信息区组件 @@ -70,27 +71,39 @@ const EventHeaderInfo = ({ event, importance, isFollowing, followerCount, onTogg )} - {/* 第一行:标题 + 关注按钮 */} + {/* 第一行:标题 + 关注按钮 + 分享按钮 */} {/* 标题 */} {event.title} - {/* 关注按钮 */} - + + {/* 关注按钮 */} + + + {/* 分享按钮 */} + + {/* 第二行:浏览数 + 日期 */} {/* 浏览数 */} - + {(event.view_count || 0).toLocaleString()}次浏览 diff --git a/src/components/EventDetailPanel/RelatedConceptsSection/TradingDateInfo.js b/src/components/EventDetailPanel/RelatedConceptsSection/TradingDateInfo.js index 32d07641..d89cd6ed 100644 --- a/src/components/EventDetailPanel/RelatedConceptsSection/TradingDateInfo.js +++ b/src/components/EventDetailPanel/RelatedConceptsSection/TradingDateInfo.js @@ -5,6 +5,7 @@ import React from 'react'; import { HStack, Text, + Icon, useColorModeValue, } from '@chakra-ui/react'; import { Calendar } from 'lucide-react'; @@ -25,7 +26,7 @@ const TradingDateInfo = ({ effectiveTradingDate, eventTime }) => { return ( - + 涨跌幅数据:{effectiveTradingDate} {eventTime && effectiveTradingDate !== dayjs(eventTime).format('YYYY-MM-DD') && ( diff --git a/src/components/EventDetailPanel/StockListItem.js b/src/components/EventDetailPanel/StockListItem.js index acb863d1..7a38c882 100644 --- a/src/components/EventDetailPanel/StockListItem.js +++ b/src/components/EventDetailPanel/StockListItem.js @@ -14,6 +14,7 @@ import { Collapse, Tooltip, Badge, + Icon, useColorModeValue, } from '@chakra-ui/react'; import { Tag } from 'antd'; @@ -197,7 +198,7 @@ const StockListItem = ({ size="xs" variant={isInWatchlist ? 'solid' : 'outline'} colorScheme={isInWatchlist ? 'yellow' : 'gray'} - icon={} + icon={} onClick={handleWatchlistClick} aria-label={isInWatchlist ? '已关注' : '加自选'} borderRadius="full" diff --git a/src/components/ShareButton/index.js b/src/components/ShareButton/index.js new file mode 100644 index 00000000..19ca7b54 --- /dev/null +++ b/src/components/ShareButton/index.js @@ -0,0 +1,252 @@ +// src/components/ShareButton/index.js +// 分享按钮组件 - 支持微信分享引导和其他分享方式 + +import React, { useState, useCallback } from 'react'; +import { + Button, + IconButton, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalCloseButton, + VStack, + HStack, + Text, + Box, + Icon, + useDisclosure, + useToast, + useColorModeValue, + Tooltip, +} from '@chakra-ui/react'; +import { Share2, Copy, MessageCircle, Check } from 'lucide-react'; +import useWechatShare from '@hooks/useWechatShare'; + +/** + * 分享按钮组件 + * + * @param {Object} props + * @param {string} props.title - 分享标题 + * @param {string} props.desc - 分享描述 + * @param {string} props.link - 分享链接 + * @param {string} props.imgUrl - 分享图片 + * @param {string} props.size - 按钮大小 ('sm' | 'md' | 'lg') + * @param {string} props.variant - 按钮样式 ('solid' | 'outline' | 'ghost' | 'icon') + * @param {string} props.colorScheme - 颜色主题 + * @param {React.ReactNode} props.children - 自定义按钮内容 + */ +const ShareButton = ({ + title = '', + desc = '', + link = '', + imgUrl = '', + size = 'sm', + variant = 'ghost', + colorScheme = 'gray', + children, + ...rest +}) => { + const { isOpen, onOpen, onClose } = useDisclosure(); + const toast = useToast(); + const [copied, setCopied] = useState(false); + + // 主题颜色 + const modalBg = useColorModeValue('white', 'gray.800'); + const borderColor = useColorModeValue('gray.200', 'gray.600'); + const hoverBg = useColorModeValue('gray.100', 'gray.700'); + + // 使用微信分享 Hook + const { isInWechat, triggerShare } = useWechatShare({ + title, + desc, + link, + imgUrl, + autoSetup: true, + }); + + // 复制链接 + const handleCopyLink = useCallback(async () => { + const shareLink = link || window.location.href; + try { + await navigator.clipboard.writeText(shareLink); + setCopied(true); + toast({ + title: '链接已复制', + status: 'success', + duration: 2000, + isClosable: true, + }); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + // 降级方案:使用 execCommand + const textArea = document.createElement('textarea'); + textArea.value = shareLink; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + setCopied(true); + toast({ + title: '链接已复制', + status: 'success', + duration: 2000, + isClosable: true, + }); + setTimeout(() => setCopied(false), 2000); + } + }, [link, toast]); + + // 点击分享按钮 + const handleShareClick = useCallback(() => { + if (isInWechat) { + // 在微信中,打开分享引导弹窗 + onOpen(); + } else if (navigator.share) { + // 支持 Web Share API + navigator.share({ + title: title || document.title, + text: desc, + url: link || window.location.href, + }).catch(() => { + // 用户取消或失败,打开弹窗作为降级 + onOpen(); + }); + } else { + // 不支持 Web Share API,打开弹窗 + onOpen(); + } + }, [isInWechat, title, desc, link, onOpen]); + + // 渲染按钮 + const renderButton = () => { + if (children) { + return ( + + {children} + + ); + } + + if (variant === 'icon') { + return ( + + } + size={size} + variant="ghost" + colorScheme={colorScheme} + aria-label="分享" + onClick={handleShareClick} + {...rest} + /> + + ); + } + + return ( + + ); + }; + + return ( + <> + {renderButton()} + + {/* 分享引导弹窗 */} + + + + 分享给好友 + + + + {/* 分享预览 */} + + + {title || '分享内容'} + + {desc && ( + + {desc} + + )} + + + {/* 分享选项 */} + + {/* 微信分享提示(仅在微信环境显示) */} + {isInWechat && ( + + + + + 微信分享 + + + 请点击右上角「...」选择分享方式 + + + + )} + + {/* 复制链接 */} + + + + + {copied ? '已复制' : '复制链接'} + + + {link || window.location.href} + + + + + + + + + + ); +}; + +export default ShareButton; diff --git a/src/hooks/useWechatShare.js b/src/hooks/useWechatShare.js new file mode 100644 index 00000000..160cc2e6 --- /dev/null +++ b/src/hooks/useWechatShare.js @@ -0,0 +1,232 @@ +// src/hooks/useWechatShare.js +// 微信分享 Hook - 支持自动配置右上角分享菜单和显式分享引导 + +import { useState, useEffect, useCallback, useRef } from 'react'; +import { getJsSdkConfig } from '@services/miniprogramService'; + +/** + * 检测是否在微信浏览器中 + */ +const isWechatBrowser = () => { + const ua = navigator.userAgent.toLowerCase(); + return ua.includes('micromessenger'); +}; + +/** + * 微信分享 Hook + * + * @param {Object} options - 分享配置 + * @param {string} options.title - 分享标题 + * @param {string} options.desc - 分享描述 + * @param {string} options.link - 分享链接(默认当前页面) + * @param {string} options.imgUrl - 分享图标 URL + * @param {boolean} options.autoSetup - 是否自动配置分享(默认 true) + * + * @returns {Object} 分享状态和方法 + */ +const useWechatShare = (options = {}) => { + const { + title = '', + desc = '', + link = '', + imgUrl = '', + autoSetup = true, + } = options; + + const [isReady, setIsReady] = useState(false); + const [isInWechat, setIsInWechat] = useState(false); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + // 使用 ref 存储最新的分享配置,避免重复初始化 + const shareConfigRef = useRef({ title, desc, link, imgUrl }); + const isInitializedRef = useRef(false); + + // 更新 ref 中的配置 + useEffect(() => { + shareConfigRef.current = { title, desc, link, imgUrl }; + + // 如果已初始化且配置变化,更新分享内容 + if (isReady && isInWechat && window.wx) { + updateShareData(); + } + }, [title, desc, link, imgUrl, isReady, isInWechat]); + + /** + * 更新分享数据 + */ + const updateShareData = useCallback(() => { + if (!window.wx || !isReady) return; + + const config = shareConfigRef.current; + const shareLink = config.link || window.location.href.split('#')[0]; + + // 分享给朋友 + window.wx.updateAppMessageShareData({ + title: config.title || document.title, + desc: config.desc || '', + link: shareLink, + imgUrl: config.imgUrl || `${window.location.origin}/logo192.png`, + success: () => { + console.log('[WechatShare] updateAppMessageShareData 设置成功'); + }, + fail: (err) => { + console.error('[WechatShare] updateAppMessageShareData 失败:', err); + } + }); + + // 分享到朋友圈 + window.wx.updateTimelineShareData({ + title: config.title || document.title, + link: shareLink, + imgUrl: config.imgUrl || `${window.location.origin}/logo192.png`, + success: () => { + console.log('[WechatShare] updateTimelineShareData 设置成功'); + }, + fail: (err) => { + console.error('[WechatShare] updateTimelineShareData 失败:', err); + } + }); + }, [isReady]); + + /** + * 初始化微信 JS-SDK + */ + const initWxSdk = useCallback(async () => { + if (isInitializedRef.current || !isInWechat) return; + + setIsLoading(true); + setError(null); + + try { + // 获取当前页面 URL(不含 hash) + const currentUrl = window.location.href.split('#')[0]; + + // 获取 JS-SDK 签名配置 + const config = await getJsSdkConfig(currentUrl); + + if (!config) { + throw new Error('获取签名配置失败'); + } + + // 配置微信 JS-SDK + window.wx.config({ + debug: false, // 生产环境关闭 + appId: config.appId, + timestamp: config.timestamp, + nonceStr: config.nonceStr, + signature: config.signature, + jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData'], + }); + + // 监听 ready 事件 + window.wx.ready(() => { + console.log('[WechatShare] wx.ready 触发'); + isInitializedRef.current = true; + setIsReady(true); + setIsLoading(false); + + // 自动设置分享内容 + if (autoSetup) { + updateShareData(); + } + }); + + // 监听 error 事件 + window.wx.error((err) => { + console.error('[WechatShare] wx.error:', err); + setError(err.errMsg || '微信 JS-SDK 初始化失败'); + setIsLoading(false); + }); + + } catch (err) { + console.error('[WechatShare] 初始化失败:', err); + setError(err.message || '初始化失败'); + setIsLoading(false); + } + }, [isInWechat, autoSetup, updateShareData]); + + // 检测微信环境并初始化 + useEffect(() => { + const inWechat = isWechatBrowser(); + setIsInWechat(inWechat); + + if (inWechat && autoSetup) { + // 确保 wx 对象已加载 + if (window.wx) { + initWxSdk(); + } else { + // 等待 wx 对象加载 + const checkWx = setInterval(() => { + if (window.wx) { + clearInterval(checkWx); + initWxSdk(); + } + }, 100); + + // 3秒后停止检查 + setTimeout(() => clearInterval(checkWx), 3000); + } + } + }, [autoSetup, initWxSdk]); + + /** + * 手动触发分享引导 + * 在微信中会提示用户点击右上角分享 + * 在非微信环境中可以展示其他分享方式 + */ + const triggerShare = useCallback(() => { + if (!isInWechat) { + // 非微信环境,可以使用 Web Share API 或复制链接 + if (navigator.share) { + navigator.share({ + title: shareConfigRef.current.title || document.title, + text: shareConfigRef.current.desc, + url: shareConfigRef.current.link || window.location.href, + }).catch((err) => { + console.log('[WechatShare] Web Share 取消或失败:', err); + }); + } else { + // 复制链接到剪贴板 + const link = shareConfigRef.current.link || window.location.href; + navigator.clipboard?.writeText(link).then(() => { + console.log('[WechatShare] 链接已复制'); + }); + } + return { type: 'web', success: true }; + } + + // 微信环境:无法直接触发分享,返回提示信息 + return { + type: 'wechat', + success: true, + message: '请点击右上角「...」进行分享', + }; + }, [isInWechat]); + + /** + * 手动更新分享配置 + */ + const setShareConfig = useCallback((newConfig) => { + shareConfigRef.current = { + ...shareConfigRef.current, + ...newConfig, + }; + + if (isReady && isInWechat) { + updateShareData(); + } + }, [isReady, isInWechat, updateShareData]); + + return { + isReady, // JS-SDK 是否就绪 + isInWechat, // 是否在微信浏览器中 + isLoading, // 是否正在加载 + error, // 错误信息 + triggerShare, // 触发分享引导 + setShareConfig, // 手动更新分享配置 + updateShareData, // 手动更新分享数据到微信 + }; +}; + +export default useWechatShare; diff --git a/src/views/Community/components/EventCard/atoms/EventEngagement.js b/src/views/Community/components/EventCard/atoms/EventEngagement.js index 29726c5e..bcc2ddfb 100644 --- a/src/views/Community/components/EventCard/atoms/EventEngagement.js +++ b/src/views/Community/components/EventCard/atoms/EventEngagement.js @@ -4,7 +4,6 @@ import React, { useState, useCallback } from 'react'; import { HStack, - VStack, Box, Text, Tooltip, @@ -41,7 +40,7 @@ const EventEngagement = ({ onVoteChange, }) => { const toast = useToast(); - const { isLoggedIn } = useAuth(); + const { isAuthenticated: isLoggedIn } = useAuth(); const [localVote, setLocalVote] = useState(userVote); const [localBullish, setLocalBullish] = useState(bullishCount);