diff --git a/config/env.ts b/config/env.ts index d49548f..7d83fe0 100644 --- a/config/env.ts +++ b/config/env.ts @@ -2,22 +2,22 @@ export const ENV_CONFIG = { // 开发环境 development: { - API_BASE_URL: 'https://cms-api.websoft.top/api', - // API_BASE_URL: 'http://127.0.0.1:9200/api', + // API_BASE_URL: 'https://mp-api.websoft.top/api', + API_BASE_URL: 'http://127.0.0.1:9200/api', APP_NAME: '开发环境', DEBUG: 'true', }, // 生产环境 production: { - API_BASE_URL: 'https://cms-api.websoft.top/api', - // API_BASE_URL: 'http://127.0.0.1:9200/api', + // API_BASE_URL: 'https://mp-api.websoft.top/api', + API_BASE_URL: 'http://127.0.0.1:9200/api', APP_NAME: '南南佐顿门窗', DEBUG: 'false', }, // 测试环境 test: { - API_BASE_URL: 'https://cms-api.websoft.top/api', - // API_BASE_URL: 'http://127.0.0.1:9200/api', + // API_BASE_URL: 'https://mp-api.websoft.top/api', + API_BASE_URL: 'http://127.0.0.1:9200/api', APP_NAME: '测试环境', DEBUG: 'true', } diff --git a/src/dealer/qrcode/index.config.ts b/src/dealer/qrcode/index.config.ts index b075b21..7abe843 100644 --- a/src/dealer/qrcode/index.config.ts +++ b/src/dealer/qrcode/index.config.ts @@ -1,3 +1,6 @@ export default definePageConfig({ - navigationBarTitleText: '推广二维码' + navigationBarTitleText: '账户管理中心', + // Enable "Share to friends" and "Share to Moments" (timeline) for this page. + enableShareAppMessage: true, + enableShareTimeline: true }) diff --git a/src/dealer/qrcode/index.tsx b/src/dealer/qrcode/index.tsx index 268580a..a721389 100644 --- a/src/dealer/qrcode/index.tsx +++ b/src/dealer/qrcode/index.tsx @@ -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('') - const [loading, setLoading] = useState(false) + const [codeLoading, setCodeLoading] = useState(false) + const [saving, setSaving] = useState(false) // const [inviteStats, setInviteStats] = useState(null) // const [statsLoading, setStatsLoading] = useState(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) + setCodeLoading(true) // 生成邀请小程序码 const codeUrl = await generateInviteCode(dealerUser.userId) @@ -40,7 +74,7 @@ const DealerQrcode: React.FC = () => { // 清空之前的二维码 setMiniProgramCodeUrl('') } finally { - setLoading(false) + setCodeLoading(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 => { + 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 => { + // 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 = {} + 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: '保存失败', - icon: '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 ( @@ -171,6 +290,33 @@ const DealerQrcode: React.FC = () => { ) } + if (error) { + return ( + + 加载失败 + {error} + + + ) + } + + // 未成为分销商时给出明确引导,避免一直停留在“加载中” + if (!dealerUser) { + return ( + + 你还不是分销商 + 申请成为分销商后即可生成分享码 + + + ) + } + return ( {/* 头部卡片 */} @@ -185,9 +331,9 @@ const DealerQrcode: React.FC = () => { }}> - 我的邀请小程序码 + 我的分享码 - 分享小程序码邀请好友,获得丰厚佣金奖励 + 与好友“共享福利 一起省、一起赚” @@ -196,7 +342,7 @@ const DealerQrcode: React.FC = () => { {/* 小程序码展示区 */} - {loading ? ( + {codeLoading ? ( 生成中... @@ -239,10 +385,10 @@ const DealerQrcode: React.FC = () => { )} - 扫码加入我的团队 + 桂乐淘伙伴计划 - 好友扫描小程序码即可直接进入小程序并建立邀请关系 + 自购省 | 分享赚 | 好友惠 @@ -258,34 +404,12 @@ const DealerQrcode: React.FC = () => { block icon={} onClick={saveMiniProgramCode} - disabled={!miniProgramCodeUrl || loading} + disabled={!miniProgramCodeUrl || codeLoading || saving} > 保存小程序码到相册 - {/**/} - {/* }*/} - {/* onClick={copyInviteInfo}*/} - {/* disabled={!dealerUser?.userId || loading}*/} - {/* >*/} - {/* 复制邀请信息*/} - {/* */} - {/**/} - {/**/} - {/* }*/} - {/* onClick={shareMiniProgramCode}*/} - {/* disabled={!dealerUser?.userId || loading}*/} - {/* >*/} - {/* 分享给好友*/} - {/* */} - {/**/} + {/* 推广说明 */} diff --git a/src/hooks/useDealerUser.ts b/src/hooks/useDealerUser.ts index 3a9317d..c61dd63 100644 --- a/src/hooks/useDealerUser.ts +++ b/src/hooks/useDealerUser.ts @@ -46,9 +46,17 @@ export const useDealerUser = (): UseDealerUserReturn => { setDealerUser(dealer) } else { setDealerUser(null) + // 没有经销商记录,跳转到申请加入页面 + Taro.redirectTo({ url: '/dealer/apply/add' }) } } catch (err) { const errorMessage = err instanceof Error ? err.message : '获取经销商信息失败' + // 如果错误消息是"操作成功"(接口返回成功但无数据),也跳转到申请页面 + if (errorMessage === '操作成功' || errorMessage === '查询成功') { + setDealerUser(null) + Taro.redirectTo({ url: '/dealer/apply/add' }) + return + } setError(errorMessage) setDealerUser(null) } finally {