feat(dealer): 优化分享码页面加载和保存功能
- 修改配置文件环境接口地址为本地调试 - 更新分享二维码页面标题为“账户管理中心”,启用分享按钮 - 新增分享小程序功能,支持转发给朋友和分享到朋友圈 - 改进生成小程序码的加载状态及错误处理 - 增加保存小程序码到相册的权限申请和下载容错机制 - 处理保存失败时授权提示和异常提醒 - 显示加载失败及重试按钮,避免未授权用户界面死循环 - 未成为分销商时增加跳转申请页面引导 - 更新邀请文案和页面UI细节优化 - 在useDealerUser钩子中新增无经销商数据自动跳转申请页逻辑
This commit is contained in:
@@ -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',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
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({
|
Taro.showToast({
|
||||||
title: '保存成功',
|
title: '保存成功',
|
||||||
icon: 'success'
|
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>
|
||||||
|
|
||||||
{/* 推广说明 */}
|
{/* 推广说明 */}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user