@@ -2,7 +2,7 @@ import React, {useState, useEffect} from 'react'
import { View , Text , Image } from '@tarojs/components'
import { Button , Loading } from '@nutui/nutui-react-taro'
import { Download , QrCode } from '@nutui/icons-react-taro'
import Taro from '@tarojs/taro'
import Taro , { useShareAppMessage } from '@tarojs/taro'
import { useDealerUser } from '@/hooks/useDealerUser'
import { generateInviteCode } from '@/api/invite'
// import type {InviteStats} from '@/api/invite'
@@ -10,10 +10,44 @@ import {businessGradients} from '@/styles/gradients'
const DealerQrcode : React.FC = ( ) = > {
const [ miniProgramCodeUrl , setMiniProgramCodeUrl ] = useState < string > ( '' )
const [ l oading, setLoading ] = useState < boolean > ( false )
const [ codeL oading, setCode Loading ] = useState < boolean > ( false )
const [ saving , setSaving ] = useState < boolean > ( false )
// const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
// const [statsLoading, setStatsLoading] = useState<boolean>(false)
const { dealerUser } = useDealerUser ( )
const { dealerUser , loading : dealerLoading , error , refresh } = useDealerUser ( )
// Enable "转发给朋友" + "分享到朋友圈" items in the share panel/menu.
useEffect ( ( ) = > {
// Some clients require explicit call to show both share entries.
Taro . showShareMenu ( {
withShareTicket : true ,
showShareItems : [ 'shareAppMessage' , 'shareTimeline' ]
} ) . catch ( ( ) = > { } )
} , [ ] )
// 转发给朋友(分享小程序链接)
useShareAppMessage ( ( ) = > {
const inviterRaw = dealerUser ? . userId ? ? Taro . getStorageSync ( 'UserId' )
const inviter = Number ( inviterRaw )
const hasInviter = Number . isFinite ( inviter ) && inviter > 0
const user = Taro . getStorageSync ( 'User' ) || { }
const nickname = ( user && ( user . nickname || user . realName || user . username ) ) || ''
const title = hasInviter ? ` ${ nickname || '我' } 邀请你加入桂乐淘伙伴计划 ` : '桂乐淘伙伴计划'
return {
title ,
path : hasInviter
? ` /pages/index/index?inviter= ${ inviter } &source=dealer_qrcode&t= ${ Date . now ( ) } `
: ` /pages/index/index ` ,
success : function ( ) {
Taro . showToast ( { title : '分享成功' , icon : 'success' , duration : 2000 } )
} ,
fail : function ( ) {
Taro . showToast ( { title : '分享失败' , icon : 'none' , duration : 2000 } )
}
}
} )
// 生成小程序码
const generateMiniProgramCode = async ( ) = > {
@@ -22,7 +56,7 @@ const DealerQrcode: React.FC = () => {
}
try {
setLoading ( true )
setCode Loading ( true )
// 生成邀请小程序码
const codeUrl = await generateInviteCode ( dealerUser . userId )
@@ -40,7 +74,7 @@ const DealerQrcode: React.FC = () => {
// 清空之前的二维码
setMiniProgramCodeUrl ( '' )
} finally {
setLoading ( false )
setCode Loading ( false )
}
}
@@ -67,6 +101,66 @@ const DealerQrcode: React.FC = () => {
}
} , [ dealerUser ? . userId ] )
const isAlbumAuthError = ( errMsg? : string ) = > {
if ( ! errMsg ) return false
// WeChat uses variants like: "saveImageToPhotosAlbum:fail auth deny",
// "saveImageToPhotosAlbum:fail auth denied", "authorize:fail auth deny"
return (
errMsg . includes ( 'auth deny' ) ||
errMsg . includes ( 'auth denied' ) ||
errMsg . includes ( 'authorize' ) ||
errMsg . includes ( 'scope.writePhotosAlbum' )
)
}
const ensureWriteAlbumPermission = async ( ) : Promise < boolean > = > {
try {
const setting = await Taro . getSetting ( )
if ( setting ? . authSetting ? . [ 'scope.writePhotosAlbum' ] ) return true
await Taro . authorize ( { scope : 'scope.writePhotosAlbum' } )
return true
} catch ( error : any ) {
const modal = await Taro . showModal ( {
title : '提示' ,
content : '需要您授权保存图片到相册,请在设置中开启相册权限' ,
confirmText : '去设置'
} )
if ( modal . confirm ) {
await Taro . openSetting ( )
}
return false
}
}
const downloadImageToLocalPath = async ( url : string ) : Promise < string > = > {
// saveImageToPhotosAlbum must receive a local temp path (e.g. `http://tmp/...` or `wxfile://...`).
// Some environments may return a non-existing temp path from getImageInfo, so we verify.
if ( url . startsWith ( 'http://tmp/' ) || url . startsWith ( 'wxfile://' ) ) {
return url
}
const token = Taro . getStorageSync ( 'access_token' )
const tenantId = Taro . getStorageSync ( 'TenantId' )
const header : Record < string , string > = { }
if ( token ) header . Authorization = token
if ( tenantId ) header . TenantId = tenantId
// 先下载到本地临时文件再保存到相册
const res = await Taro . downloadFile ( { url , header } )
if ( res . statusCode !== 200 || ! res . tempFilePath ) {
throw new Error ( ` 图片下载失败( ${ res . statusCode || 'unknown' } ) ` )
}
// Double-check file exists to avoid: saveImageToPhotosAlbum:fail no such file or directory
try {
await Taro . getFileInfo ( { filePath : res.tempFilePath } )
} catch ( _ ) {
throw new Error ( '图片临时文件不存在,请重试' )
}
return res . tempFilePath
}
// 保存小程序码到相册
const saveMiniProgramCode = async ( ) = > {
if ( ! miniProgramCodeUrl ) {
@@ -78,39 +172,64 @@ const DealerQrcode: React.FC = () => {
}
try {
// 先下载图片到本地
const res = await Taro . downloadFile ( {
url : miniProgramCodeUrl
} )
if ( saving ) return
setSaving ( true )
Taro . showLoading ( { title : '保存中...' } )
if ( res . statusCode === 200 ) {
// 保存到相册
await Taro . saveImageToPhotosAlbum ( {
filePath : res.tempFilePath
} )
const hasPermission = await ensureWriteAlbumPermission ( )
if ( ! hasPermission ) return
Taro . showToast ( {
title : '保存成功' ,
icon : 'success'
} )
let filePath = await downloadImageToLocalPath ( miniProgramCodeUrl )
try {
await Taro . saveImageToPhotosAlbum ( { filePath } )
} catch ( e : any ) {
const msg = e ? . errMsg || e ? . message || ''
// Fallback: some devices/clients may fail to save directly from a temp path.
if (
msg . includes ( 'no such file or directory' ) &&
( filePath . startsWith ( 'http://tmp/' ) || filePath . startsWith ( 'wxfile://' ) )
) {
const saved = ( await Taro . saveFile ( { tempFilePath : filePath } ) ) as unknown as { savedFilePath? : string }
if ( saved ? . savedFilePath ) {
filePath = saved . savedFilePath
}
await Taro . saveImageToPhotosAlbum ( { filePath } )
} else {
throw e
}
}
Taro . showToast ( {
title : '保存成功' ,
icon : 'success'
} )
} catch ( error : any ) {
if ( error . errMsg ? . includes ( 'auth deny' ) ) {
Taro . showModal ( {
const errMsg = error ? . errMsg || error ? . message
if ( errMsg ? . includes ( 'cancel' ) ) {
Taro . showToast ( { title : '已取消' , icon : 'none' } )
return
}
if ( isAlbumAuthError ( errMsg ) ) {
const modal = await Taro . showModal ( {
title : '提示' ,
content : '需要您授权保存图片到相册' ,
success : ( res ) = > {
if ( res . confirm ) {
Taro . openSetting ( )
}
}
confirmText : '去设置'
} )
if ( modal . confirm ) {
await Taro . openSetting ( )
}
} else {
Taro . showToast ( {
// Prefer a modal so we can show the real reason (e.g. domain whitelist / network error).
await Taro . showModal ( {
title : '保存失败' ,
i con: 'error'
content : errMsg || '保存失败,请稍后重试' ,
showCancel : false
} )
}
} finally {
Taro . hideLoading ( )
setSaving ( false )
}
}
@@ -126,7 +245,7 @@ const DealerQrcode: React.FC = () => {
//
// const inviteText = `🎉 邀请您加入我的团队!
//
// 扫描小程序码或搜索"九云售电云 "小程序,即可享受优质商品和服务!
// 扫描小程序码或搜索"桂乐淘 "小程序,即可享受优质商品和服务!
//
// 💰 成为我的团队成员,一起赚取丰厚佣金
// 🎁 新用户专享优惠等你来拿
@@ -162,7 +281,7 @@ const DealerQrcode: React.FC = () => {
// })
// }
if ( ! dealerUser ) {
if ( dealerLoading ) {
return (
< View className = "bg-gray-50 min-h-screen flex items-center justify-center" >
< Loading / >
@@ -171,6 +290,33 @@ const DealerQrcode: React.FC = () => {
)
}
if ( error ) {
return (
< View className = "bg-gray-50 min-h-screen flex flex-col items-center justify-center p-6" >
< Text className = "text-gray-800 font-semibold" > 加 载 失 败 < / Text >
< Text className = "text-gray-500 text-sm mt-2" > { error } < / Text >
< Button className = "mt-6" type = "primary" onClick = { refresh } > 重 试 < / Button >
< / View >
)
}
// 未成为分销商时给出明确引导,避免一直停留在“加载中”
if ( ! dealerUser ) {
return (
< View className = "bg-gray-50 min-h-screen flex flex-col items-center justify-center p-6" >
< Text className = "text-gray-800 font-semibold" > 你 还 不 是 分 销 商 < / Text >
< Text className = "text-gray-500 text-sm mt-2 text-center" > 申 请 成 为 分 销 商 后 即 可 生 成 分 享 码 < / Text >
< Button
className = "mt-6"
type = "primary"
onClick = { ( ) = > Taro . navigateTo ( { url : '/dealer/apply/add' } ) }
>
去 申 请
< / Button >
< / View >
)
}
return (
< View className = "bg-gray-50 min-h-screen" >
{ /* 头部卡片 */ }
@@ -185,9 +331,9 @@ const DealerQrcode: React.FC = () => {
} } > < / View >
< View className = "relative z-10 flex flex-col" >
< Text className = "text-2xl font-bold mb-2 text-white" > 我 的 邀 请 小 程 序 码 < / Text >
< Text className = "text-2xl font-bold mb-2 text-white" > 我 的 分 享 码 < / Text >
< Text className = "text-white text-opacity-80" >
分 享 小 程 序 码 邀 请 好 友 , 获 得 丰 厚 佣 金 奖 励
与 好 友 “ 共 享 福 利 一 起 省 、 一 起 赚 ”
< / Text >
< / View >
< / View >
@@ -196,7 +342,7 @@ const DealerQrcode: React.FC = () => {
{ /* 小程序码展示区 */ }
< View className = "bg-white rounded-2xl p-6 mb-6 shadow-sm" >
< View className = "text-center" >
{ l oading ? (
{ codeL oading ? (
< View className = "w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl" >
< Loading / >
< Text className = "text-gray-500 mt-2" > 生 成 中 . . . < / Text >
@@ -239,10 +385,10 @@ const DealerQrcode: React.FC = () => {
) }
< View className = "text-lg font-semibold text-gray-800 mb-2" >
扫 码 加 入 我 的 团 队
桂 乐 淘 伙 伴 计 划
< / View >
< View className = "text-sm text-gray-500 mb-4" >
好 友 扫 描 小 程 序 码 即 可 直 接 进 入 小 程 序 并 建 立 邀 请 关 系
自 购 省 | 分 享 赚 | 好 友 惠
< / View >
@@ -258,34 +404,12 @@ const DealerQrcode: React.FC = () => {
block
icon = { < Download / > }
onClick = { saveMiniProgramCode }
disabled = { ! miniProgramCodeUrl || load ing}
disabled = { ! miniProgramCodeUrl || codeLoading || sav ing}
>
保 存 小 程 序 码 到 相 册
< / Button >
< / View >
{ /*<View className={'my-2 bg-white'}>*/ }
{ /* <Button*/ }
{ /* size="large"*/ }
{ /* block*/ }
{ /* icon={<Copy/>}*/ }
{ /* onClick={copyInviteInfo}*/ }
{ /* disabled={!dealerUser?.userId || loading}*/ }
{ /* >*/ }
{ /* 复制邀请信息*/ }
{ /* </Button>*/ }
{ /*</View>*/ }
{ /*<View className={'my-2 bg-white'}>*/ }
{ /* <Button*/ }
{ /* size="large"*/ }
{ /* block*/ }
{ /* fill="outline"*/ }
{ /* icon={<Share/>}*/ }
{ /* onClick={shareMiniProgramCode}*/ }
{ /* disabled={!dealerUser?.userId || loading}*/ }
{ /* >*/ }
{ /* 分享给好友*/ }
{ /* </Button>*/ }
{ /*</View>*/ }
< / View >
{ /* 推广说明 */ }