feat(dealer): 优化分享码页面加载和保存功能

- 修改配置文件环境接口地址为本地调试
- 更新分享二维码页面标题为“账户管理中心”,启用分享按钮
- 新增分享小程序功能,支持转发给朋友和分享到朋友圈
- 改进生成小程序码的加载状态及错误处理
- 增加保存小程序码到相册的权限申请和下载容错机制
- 处理保存失败时授权提示和异常提醒
- 显示加载失败及重试按钮,避免未授权用户界面死循环
- 未成为分销商时增加跳转申请页面引导
- 更新邀请文案和页面UI细节优化
- 在useDealerUser钩子中新增无经销商数据自动跳转申请页逻辑
This commit is contained in:
2026-04-16 14:22:23 +08:00
parent d2cdd42846
commit 238a652afc
4 changed files with 200 additions and 65 deletions

View File

@@ -2,22 +2,22 @@
export const ENV_CONFIG = { export const ENV_CONFIG = {
// 开发环境 // 开发环境
development: { development: {
API_BASE_URL: 'https://cms-api.websoft.top/api', // API_BASE_URL: 'https://mp-api.websoft.top/api',
// API_BASE_URL: 'http://127.0.0.1:9200/api', API_BASE_URL: 'http://127.0.0.1:9200/api',
APP_NAME: '开发环境', APP_NAME: '开发环境',
DEBUG: 'true', DEBUG: 'true',
}, },
// 生产环境 // 生产环境
production: { production: {
API_BASE_URL: 'https://cms-api.websoft.top/api', // API_BASE_URL: 'https://mp-api.websoft.top/api',
// API_BASE_URL: 'http://127.0.0.1:9200/api', API_BASE_URL: 'http://127.0.0.1:9200/api',
APP_NAME: '南南佐顿门窗', APP_NAME: '南南佐顿门窗',
DEBUG: 'false', DEBUG: 'false',
}, },
// 测试环境 // 测试环境
test: { test: {
API_BASE_URL: 'https://cms-api.websoft.top/api', // API_BASE_URL: 'https://mp-api.websoft.top/api',
// API_BASE_URL: 'http://127.0.0.1:9200/api', API_BASE_URL: 'http://127.0.0.1:9200/api',
APP_NAME: '测试环境', APP_NAME: '测试环境',
DEBUG: 'true', DEBUG: 'true',
} }

View File

@@ -1,3 +1,6 @@
export default definePageConfig({ export default definePageConfig({
navigationBarTitleText: '推广二维码' navigationBarTitleText: '账户管理中心',
// Enable "Share to friends" and "Share to Moments" (timeline) for this page.
enableShareAppMessage: true,
enableShareTimeline: true
}) })

View File

@@ -2,7 +2,7 @@ import React, {useState, useEffect} from 'react'
import {View, Text, Image} from '@tarojs/components' import {View, Text, Image} from '@tarojs/components'
import {Button, Loading} from '@nutui/nutui-react-taro' import {Button, Loading} from '@nutui/nutui-react-taro'
import {Download, QrCode} from '@nutui/icons-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 {useDealerUser} from '@/hooks/useDealerUser'
import {generateInviteCode} from '@/api/invite' import {generateInviteCode} from '@/api/invite'
// import type {InviteStats} from '@/api/invite' // import type {InviteStats} from '@/api/invite'
@@ -10,10 +10,44 @@ import {businessGradients} from '@/styles/gradients'
const DealerQrcode: React.FC = () => { const DealerQrcode: React.FC = () => {
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('') const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
const [loading, setLoading] = useState<boolean>(false) const [codeLoading, setCodeLoading] = useState<boolean>(false)
const [saving, setSaving] = useState<boolean>(false)
// const [inviteStats, setInviteStats] = useState<InviteStats | null>(null) // const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
// const [statsLoading, setStatsLoading] = useState<boolean>(false) // 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 () => { const generateMiniProgramCode = async () => {
@@ -22,7 +56,7 @@ const DealerQrcode: React.FC = () => {
} }
try { try {
setLoading(true) setCodeLoading(true)
// 生成邀请小程序码 // 生成邀请小程序码
const codeUrl = await generateInviteCode(dealerUser.userId) const codeUrl = await generateInviteCode(dealerUser.userId)
@@ -40,7 +74,7 @@ const DealerQrcode: React.FC = () => {
// 清空之前的二维码 // 清空之前的二维码
setMiniProgramCodeUrl('') setMiniProgramCodeUrl('')
} finally { } finally {
setLoading(false) setCodeLoading(false)
} }
} }
@@ -67,6 +101,66 @@ const DealerQrcode: React.FC = () => {
} }
}, [dealerUser?.userId]) }, [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 () => { const saveMiniProgramCode = async () => {
if (!miniProgramCodeUrl) { if (!miniProgramCodeUrl) {
@@ -78,39 +172,64 @@ const DealerQrcode: React.FC = () => {
} }
try { try {
// 先下载图片到本地 if (saving) return
const res = await Taro.downloadFile({ setSaving(true)
url: miniProgramCodeUrl Taro.showLoading({title: '保存中...'})
})
if (res.statusCode === 200) { const hasPermission = await ensureWriteAlbumPermission()
// 保存到相册 if (!hasPermission) return
await Taro.saveImageToPhotosAlbum({
filePath: res.tempFilePath
})
Taro.showToast({ let filePath = await downloadImageToLocalPath(miniProgramCodeUrl)
title: '保存成功', try {
icon: 'success' 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) { } catch (error: any) {
if (error.errMsg?.includes('auth deny')) { const errMsg = error?.errMsg || error?.message
Taro.showModal({ if (errMsg?.includes('cancel')) {
Taro.showToast({title: '已取消', icon: 'none'})
return
}
if (isAlbumAuthError(errMsg)) {
const modal = await Taro.showModal({
title: '提示', title: '提示',
content: '需要您授权保存图片到相册', content: '需要您授权保存图片到相册',
success: (res) => { confirmText: '去设置'
if (res.confirm) {
Taro.openSetting()
}
}
}) })
if (modal.confirm) {
await Taro.openSetting()
}
} else { } else {
Taro.showToast({ // Prefer a modal so we can show the real reason (e.g. domain whitelist / network error).
await Taro.showModal({
title: '保存失败', title: '保存失败',
icon: 'error' content: errMsg || '保存失败,请稍后重试',
showCancel: false
}) })
} }
} finally {
Taro.hideLoading()
setSaving(false)
} }
} }
@@ -126,7 +245,7 @@ const DealerQrcode: React.FC = () => {
// //
// const inviteText = `🎉 邀请您加入我的团队! // const inviteText = `🎉 邀请您加入我的团队!
// //
// 扫描小程序码或搜索"九云售电云"小程序,即可享受优质商品和服务! // 扫描小程序码或搜索"桂乐淘"小程序,即可享受优质商品和服务!
// //
// 💰 成为我的团队成员,一起赚取丰厚佣金 // 💰 成为我的团队成员,一起赚取丰厚佣金
// 🎁 新用户专享优惠等你来拿 // 🎁 新用户专享优惠等你来拿
@@ -162,7 +281,7 @@ const DealerQrcode: React.FC = () => {
// }) // })
// } // }
if (!dealerUser) { if (dealerLoading) {
return ( return (
<View className="bg-gray-50 min-h-screen flex items-center justify-center"> <View className="bg-gray-50 min-h-screen flex items-center justify-center">
<Loading/> <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 ( return (
<View className="bg-gray-50 min-h-screen"> <View className="bg-gray-50 min-h-screen">
{/* 头部卡片 */} {/* 头部卡片 */}
@@ -185,9 +331,9 @@ const DealerQrcode: React.FC = () => {
}}></View> }}></View>
<View className="relative z-10 flex flex-col"> <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 className="text-white text-opacity-80">
</Text> </Text>
</View> </View>
</View> </View>
@@ -196,7 +342,7 @@ const DealerQrcode: React.FC = () => {
{/* 小程序码展示区 */} {/* 小程序码展示区 */}
<View className="bg-white rounded-2xl p-6 mb-6 shadow-sm"> <View className="bg-white rounded-2xl p-6 mb-6 shadow-sm">
<View className="text-center"> <View className="text-center">
{loading ? ( {codeLoading ? (
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl"> <View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
<Loading/> <Loading/>
<Text className="text-gray-500 mt-2">...</Text> <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 className="text-lg font-semibold text-gray-800 mb-2">
</View> </View>
<View className="text-sm text-gray-500 mb-4"> <View className="text-sm text-gray-500 mb-4">
| |
</View> </View>
@@ -258,34 +404,12 @@ const DealerQrcode: React.FC = () => {
block block
icon={<Download/>} icon={<Download/>}
onClick={saveMiniProgramCode} onClick={saveMiniProgramCode}
disabled={!miniProgramCodeUrl || loading} disabled={!miniProgramCodeUrl || codeLoading || saving}
> >
</Button> </Button>
</View> </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> </View>
{/* 推广说明 */} {/* 推广说明 */}

View File

@@ -46,9 +46,17 @@ export const useDealerUser = (): UseDealerUserReturn => {
setDealerUser(dealer) setDealerUser(dealer)
} else { } else {
setDealerUser(null) setDealerUser(null)
// 没有经销商记录,跳转到申请加入页面
Taro.redirectTo({ url: '/dealer/apply/add' })
} }
} catch (err) { } catch (err) {
const errorMessage = err instanceof Error ? err.message : '获取经销商信息失败' const errorMessage = err instanceof Error ? err.message : '获取经销商信息失败'
// 如果错误消息是"操作成功"(接口返回成功但无数据),也跳转到申请页面
if (errorMessage === '操作成功' || errorMessage === '查询成功') {
setDealerUser(null)
Taro.redirectTo({ url: '/dealer/apply/add' })
return
}
setError(errorMessage) setError(errorMessage)
setDealerUser(null) setDealerUser(null)
} finally { } finally {