- 将 doctor 目录重命名为 dealer 目录 - 更新页面标题从'会员注册'为'注册会员' - 删除银行卡管理、患者报备和订单消息功能 - 重命名组件 AddDoctor 为 AddUserAddress - 添加用户角色管理和默认角色写入逻辑 - 优化注册成功后跳转至用户中心页面 - 更新应用配置中的页面路径和子包结构 - 添加经销商资金管理、团队管理和二维码推广功能 - 更新租户信息配置,增加租户名称和版权信息 - 优化文章列表组件的类型定义和渲染方式 - 修复广告轮播图数据加载和图片兼容性问题
485 lines
17 KiB
TypeScript
485 lines
17 KiB
TypeScript
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 {useDealerUser} from '@/hooks/useDealerUser'
|
||
import {generateInviteCode} from '@/api/invite'
|
||
// import type {InviteStats} from '@/api/invite'
|
||
import {businessGradients} from '@/styles/gradients'
|
||
|
||
const DealerQrcode: React.FC = () => {
|
||
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
|
||
const [loading, setLoading] = 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 generateMiniProgramCode = async () => {
|
||
if (!dealerUser?.userId) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
setLoading(true)
|
||
|
||
// 生成邀请小程序码
|
||
const codeUrl = await generateInviteCode(dealerUser.userId)
|
||
|
||
if (codeUrl) {
|
||
setMiniProgramCodeUrl(codeUrl)
|
||
} else {
|
||
throw new Error('返回的小程序码URL为空')
|
||
}
|
||
} catch (error: any) {
|
||
Taro.showToast({
|
||
title: error.message || '生成小程序码失败',
|
||
icon: 'error'
|
||
})
|
||
// 清空之前的二维码
|
||
setMiniProgramCodeUrl('')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// 获取邀请统计数据
|
||
// const fetchInviteStats = async () => {
|
||
// if (!dealerUser?.userId) return
|
||
//
|
||
// try {
|
||
// setStatsLoading(true)
|
||
// const stats = await getInviteStats(dealerUser.userId)
|
||
// stats && setInviteStats(stats)
|
||
// } catch (error) {
|
||
// // 静默处理错误,不影响用户体验
|
||
// } finally {
|
||
// setStatsLoading(false)
|
||
// }
|
||
// }
|
||
|
||
// 初始化生成小程序码和获取统计数据
|
||
useEffect(() => {
|
||
if (dealerUser?.userId) {
|
||
generateMiniProgramCode()
|
||
// fetchInviteStats()
|
||
}
|
||
}, [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) {
|
||
Taro.showToast({
|
||
title: '小程序码未生成',
|
||
icon: 'error'
|
||
})
|
||
return
|
||
}
|
||
|
||
try {
|
||
if (saving) return
|
||
setSaving(true)
|
||
Taro.showLoading({title: '保存中...'})
|
||
|
||
const hasPermission = await ensureWriteAlbumPermission()
|
||
if (!hasPermission) return
|
||
|
||
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) {
|
||
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: '需要您授权保存图片到相册',
|
||
confirmText: '去设置'
|
||
})
|
||
if (modal.confirm) {
|
||
await Taro.openSetting()
|
||
}
|
||
} else {
|
||
// Prefer a modal so we can show the real reason (e.g. domain whitelist / network error).
|
||
await Taro.showModal({
|
||
title: '保存失败',
|
||
content: errMsg || '保存失败,请稍后重试',
|
||
showCancel: false
|
||
})
|
||
}
|
||
} finally {
|
||
Taro.hideLoading()
|
||
setSaving(false)
|
||
}
|
||
}
|
||
|
||
// 复制邀请信息
|
||
// const copyInviteInfo = () => {
|
||
// if (!dealerUser?.userId) {
|
||
// Taro.showToast({
|
||
// title: '用户信息未加载',
|
||
// icon: 'error'
|
||
// })
|
||
// return
|
||
// }
|
||
//
|
||
// const inviteText = `🎉 邀请您加入我的团队!
|
||
//
|
||
// 扫描小程序码或搜索"网宿软件"小程序,即可享受优质商品和服务!
|
||
//
|
||
// 💰 成为我的团队成员,一起赚取丰厚佣金
|
||
// 🎁 新用户专享优惠等你来拿
|
||
//
|
||
// 邀请码:${dealerUser.userId}
|
||
// 快来加入我们吧!`
|
||
//
|
||
// Taro.setClipboardData({
|
||
// data: inviteText,
|
||
// success: () => {
|
||
// Taro.showToast({
|
||
// title: '邀请信息已复制',
|
||
// icon: 'success'
|
||
// })
|
||
// }
|
||
// })
|
||
// }
|
||
|
||
// 分享小程序码
|
||
// const shareMiniProgramCode = () => {
|
||
// if (!dealerUser?.userId) {
|
||
// Taro.showToast({
|
||
// title: '用户信息未加载',
|
||
// icon: 'error'
|
||
// })
|
||
// return
|
||
// }
|
||
//
|
||
// // 小程序分享
|
||
// Taro.showShareMenu({
|
||
// withShareTicket: true,
|
||
// showShareItems: ['shareAppMessage']
|
||
// })
|
||
// }
|
||
|
||
if (!dealerUser) {
|
||
return (
|
||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
||
<Loading/>
|
||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<View className="bg-gray-50 min-h-screen">
|
||
{/* 头部卡片 */}
|
||
<View className="rounded-b-3xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
||
background: businessGradients.dealer.header
|
||
}}>
|
||
{/* 装饰背景 */}
|
||
<View className="absolute w-32 h-32 rounded-full" style={{
|
||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||
top: '-16px',
|
||
right: '-16px'
|
||
}}></View>
|
||
|
||
<View className="relative z-10 flex flex-col">
|
||
<Text className="text-2xl font-bold mb-2 text-white">我的邀请小程序码</Text>
|
||
<Text className="text-white text-opacity-80">
|
||
分享小程序码邀请好友,获得丰厚佣金奖励
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
|
||
<View className="px-4">
|
||
{/* 小程序码展示区 */}
|
||
<View className="bg-white rounded-2xl p-6 mb-6 shadow-sm">
|
||
<View className="text-center">
|
||
{loading ? (
|
||
<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>
|
||
</View>
|
||
) : miniProgramCodeUrl ? (
|
||
<View className="w-48 h-48 mx-auto mb-4 bg-white rounded-xl shadow-sm p-4">
|
||
<Image
|
||
src={miniProgramCodeUrl}
|
||
className="w-full h-full"
|
||
mode="aspectFit"
|
||
onError={() => {
|
||
Taro.showModal({
|
||
title: '二维码加载失败',
|
||
content: '请检查网络连接或联系管理员',
|
||
showCancel: true,
|
||
confirmText: '重新生成',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
generateMiniProgramCode();
|
||
}
|
||
}
|
||
});
|
||
}}
|
||
|
||
/>
|
||
</View>
|
||
) : (
|
||
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
|
||
<QrCode size="48" className="text-gray-400 mb-2"/>
|
||
<Text className="text-gray-500">小程序码生成失败</Text>
|
||
<Button
|
||
size="small"
|
||
type="primary"
|
||
className="mt-2"
|
||
onClick={generateMiniProgramCode}
|
||
>
|
||
重新生成
|
||
</Button>
|
||
</View>
|
||
)}
|
||
|
||
<View className="text-lg font-semibold text-gray-800 mb-2">
|
||
扫码加入我的团队
|
||
</View>
|
||
<View className="text-sm text-gray-500 mb-4">
|
||
好友扫描小程序码即可直接进入小程序并建立邀请关系
|
||
</View>
|
||
|
||
|
||
</View>
|
||
</View>
|
||
|
||
{/* 操作按钮 */}
|
||
<View className={'gap-2'}>
|
||
<View className={'my-2'}>
|
||
<Button
|
||
type="primary"
|
||
size="large"
|
||
block
|
||
icon={<Download/>}
|
||
onClick={saveMiniProgramCode}
|
||
disabled={!miniProgramCodeUrl || loading || saving}
|
||
>
|
||
保存小程序码到相册
|
||
</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>
|
||
|
||
{/* 推广说明 */}
|
||
<View className="bg-white rounded-2xl p-4 mt-6 hidden">
|
||
<Text className="font-semibold text-gray-800 mb-3">推广说明</Text>
|
||
<View className="space-y-2">
|
||
<View className="flex items-start">
|
||
<View className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
|
||
<Text className="text-sm text-gray-600">
|
||
好友通过您的二维码或链接注册成为您的团队成员
|
||
</Text>
|
||
</View>
|
||
<View className="flex items-start">
|
||
<View className="w-2 h-2 bg-green-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
|
||
<Text className="text-sm text-gray-600">
|
||
好友购买商品时,您可获得相应层级的分销佣金
|
||
</Text>
|
||
</View>
|
||
<View className="flex items-start">
|
||
<View className="w-2 h-2 bg-purple-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
|
||
<Text className="text-sm text-gray-600">
|
||
支持三级分销,团队越大收益越多
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 邀请统计数据 */}
|
||
{/*<View className="bg-white rounded-2xl p-4 mt-4 mb-6">*/}
|
||
{/* <Text className="font-semibold text-gray-800 mb-3">我的邀请数据</Text>*/}
|
||
{/* {statsLoading ? (*/}
|
||
{/* <View className="flex items-center justify-center py-8">*/}
|
||
{/* <Loading/>*/}
|
||
{/* <Text className="text-gray-500 mt-2">加载中...</Text>*/}
|
||
{/* </View>*/}
|
||
{/* ) : inviteStats ? (*/}
|
||
{/* <View className="space-y-4">*/}
|
||
{/* <View className="grid grid-cols-2 gap-4">*/}
|
||
{/* <View className="text-center">*/}
|
||
{/* <Text className="text-2xl font-bold text-blue-500">*/}
|
||
{/* {inviteStats.totalInvites || 0}*/}
|
||
{/* </Text>*/}
|
||
{/* <Text className="text-sm text-gray-500">总邀请数</Text>*/}
|
||
{/* </View>*/}
|
||
{/* <View className="text-center">*/}
|
||
{/* <Text className="text-2xl font-bold text-green-500">*/}
|
||
{/* {inviteStats.successfulRegistrations || 0}*/}
|
||
{/* </Text>*/}
|
||
{/* <Text className="text-sm text-gray-500">成功注册</Text>*/}
|
||
{/* </View>*/}
|
||
{/* </View>*/}
|
||
|
||
{/* <View className="grid grid-cols-2 gap-4">*/}
|
||
{/* <View className="text-center">*/}
|
||
{/* <Text className="text-2xl font-bold text-purple-500">*/}
|
||
{/* {inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}*/}
|
||
{/* </Text>*/}
|
||
{/* <Text className="text-sm text-gray-500">转化率</Text>*/}
|
||
{/* </View>*/}
|
||
{/* <View className="text-center">*/}
|
||
{/* <Text className="text-2xl font-bold text-orange-500">*/}
|
||
{/* {inviteStats.todayInvites || 0}*/}
|
||
{/* </Text>*/}
|
||
{/* <Text className="text-sm text-gray-500">今日邀请</Text>*/}
|
||
{/* </View>*/}
|
||
{/* </View>*/}
|
||
|
||
{/* /!* 邀请来源统计 *!/*/}
|
||
{/* {inviteStats.sourceStats && inviteStats.sourceStats.length > 0 && (*/}
|
||
{/* <View className="mt-4">*/}
|
||
{/* <Text className="text-sm font-medium text-gray-700 mb-2">邀请来源分布</Text>*/}
|
||
{/* <View className="space-y-2">*/}
|
||
{/* {inviteStats.sourceStats.map((source, index) => (*/}
|
||
{/* <View key={index} className="flex items-center justify-between py-2 px-3 bg-gray-50 rounded-lg">*/}
|
||
{/* <View className="flex items-center">*/}
|
||
{/* <View className="w-3 h-3 rounded-full bg-blue-500 mr-2"></View>*/}
|
||
{/* <Text className="text-sm text-gray-700">{source.source}</Text>*/}
|
||
{/* </View>*/}
|
||
{/* <View className="text-right">*/}
|
||
{/* <Text className="text-sm font-medium text-gray-800">{source.count}</Text>*/}
|
||
{/* <Text className="text-xs text-gray-500">*/}
|
||
{/* {source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}*/}
|
||
{/* </Text>*/}
|
||
{/* </View>*/}
|
||
{/* </View>*/}
|
||
{/* ))}*/}
|
||
{/* </View>*/}
|
||
{/* </View>*/}
|
||
{/* )}*/}
|
||
{/* </View>*/}
|
||
{/* ) : (*/}
|
||
{/* <View className="text-center py-8">*/}
|
||
{/* <View className="text-gray-500">暂无邀请数据</View>*/}
|
||
{/* <Button*/}
|
||
{/* size="small"*/}
|
||
{/* type="primary"*/}
|
||
{/* className="mt-2"*/}
|
||
{/* onClick={fetchInviteStats}*/}
|
||
{/* >*/}
|
||
{/* 刷新数据*/}
|
||
{/* </Button>*/}
|
||
{/* </View>*/}
|
||
{/* )}*/}
|
||
{/*</View>*/}
|
||
</View>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
export default DealerQrcode
|