feat(user): 使用微信原生 Picker 替换所在地选择弹层

- 删除 NutUI Address 相关导入、状态及地区数据转换逻辑
- 新增派生 regionValue 用于绑定微信原生 Picker 的 value
- 将 handleRegionChange 调整为处理微信 Picker 的事件
- 将所在地选择 UI 修改为微信原生 Picker 包裹的视图结构
- 地图定位失败使用弹窗提示,引导用户手动选择或重试
- 移除手动打开地区选择器的函数及 NutUI 组件
- 保留 RegionData 用于文本智能识别解析
- 编译通过,提升在无地图或无权限环境下的地区选择稳定性
This commit is contained in:
2026-07-13 08:16:17 +08:00
parent 70ad1d030f
commit d6d6b1f5a7
16 changed files with 546 additions and 25 deletions

View File

@@ -0,0 +1,10 @@
# 2026-07-13 工作日志
## 小程序分享 / 朋友圈能力全量接入
- 新增 `src/hooks/useShare.ts`:统一注册 `useShareAppMessage` + `useShareTimeline`,自动追加 `inviter=${user.id}` 裂变参数;导出 `isTimelineSinglePage()`scene===1154
- 新增 `src/components/SharePoster/index.tsx`Canvas 2D 生成「封面+标题+价格+小程序码」海报,返回临时图路径作 imageUrl。
- 新增 `src/api/share.ts``generateShareCode(page, inviterId)`,复用 `generateMiniProgramCode`scene=uid_${id}page 透传)。
- 扩展 `src/api/shop/shopInvite/index.ts``generateMiniProgramCode`URL 透传 `page` 参数(后端需读取转发)。
- 接入 10 个页面:商品/积分/拼团/秒杀/活动/赛事详情(带 canvas 海报);邀请下级/分销推广/升级VIP/分享赚佣金(仅 enableShare + useShare
- 类型检查:本次改动零错误,仅余 1 个历史既有错误TS2688 @tarojs/taro 类型解析,非本次引入)。
- 待办/注意:朋友圈分享必须真机测试;后端需支持按 page 生成指定落地页小程序码;单页模式降级(避免强制登录跳转)可调用 `isTimelineSinglePage()`

View File

@@ -31,3 +31,11 @@
- 字段:订单号(`character_string1`)、商品名称(`thing4`)、联系人(`thing10`)、联系电话(`phone_number7`)、送货地址(`thing8`)
- 场景说明:下单提醒
- 新订单检测 Hook`src/hooks/useNewOrderDetector.ts`30秒轮询页面隐藏暂停
## 分享 / 朋友圈能力2026-07-13 接入)
- 统一封装:`src/hooks/useShare.ts``useShare({title, path?, query?, imageUrl?})`,内部一次注册 `useShareAppMessage` + `useShareTimeline`,并自动在 path/query 追加 `inviter=${当前用户id}` 做裂变归因;`isTimelineSinglePage()` 判断朋友圈单页模式 scene===1154
- 海报组件:`src/components/SharePoster/index.tsx`Canvas 2D 绘制封面+标题+价格+小程序码,返回临时图路径作 imageUrl
- 小程序码接口:`src/api/share.ts` `generateShareCode(page, inviterId)` 复用 `generateMiniProgramCode`scene 用 `uid_${inviterId}`page 透传但后端需读取转发才生效)
- 裂变约定:分享链接 query 用 `inviter`,与 `src/utils/invite.ts``parseInviteParams` 读取字段一致
- 已接入页面:商品详情、积分商品、拼团、秒杀、活动、赛事(带 canvas 海报邀请下级、分销推广、升级VIP、分享赚佣金仅 enableShare + useShare无海报
- 注意tabBar 4 页(首页/分类/订单/我的)不支持朋友圈分享;朋友圈分享必须真机测试(开发者工具无预览)

23
src/api/share.ts Normal file
View File

@@ -0,0 +1,23 @@
import { generateMiniProgramCode } from '@/api/shop/shopInvite'
/**
* 生成「分享海报」用的小程序码(二维码)图片地址。
*
* - scene 复用现有 uid_${inviterId} 约定,与邀请裂变逻辑一致;
* - page 指向被分享的页面(小程序码扫码后落地页)。
*
* 注意:当前后端 /wx-login/getOrderQRCodeUnlimited/{scene} 仅消费 scene
* 若希望扫码后直达指定页面而非默认页,需后端读取并转发 page 参数。
*
* @param page 被分享页路径,如 pages/shop/product-detail
* @param inviterId 当前登录用户 id用于裂变归因未登录可省略
*/
export function generateShareCode(page: string, inviterId?: number | string) {
const scene = inviterId ? `uid_${inviterId}` : 'share'
return generateMiniProgramCode({
page,
scene,
width: 200,
envVersion: 'release',
})
}

View File

@@ -111,7 +111,12 @@ export interface InviteRecordParam {
*/
export async function generateMiniProgramCode(data: MiniProgramCodeParam) {
try {
const url = '/wx-login/getOrderQRCodeUnlimited/' + data.scene;
let url = '/wx-login/getOrderQRCodeUnlimited/' + data.scene;
// 透传 page便于后端按要求生成指向指定页的小程序码
// (当前后端仅使用 scene若需扫码直达指定页后端需读取并转发 page 到微信 getwxacodeunlimit
if (data.page) {
url += `?page=${encodeURIComponent(data.page)}`;
}
// 由于接口直接返回图片buffer我们直接构建完整的URL
return `${BaseUrl}${url}`;
} catch (error: any) {

View File

@@ -0,0 +1,204 @@
import React, { forwardRef, useImperativeHandle, useRef } from 'react'
import { Canvas, View } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { useUser } from '@/hooks/useUser'
import { generateShareCode } from '@/api/share'
export interface PosterOptions {
/** 封面图(网络地址或本地路径) */
cover?: string
/** 主标题 */
title: string
/** 副标题 / 描述 */
subtitle?: string
/** 价格(仅展示用) */
price?: string | number
/** 小程序码落地页,如 pages/shop/product-detail */
page?: string
}
export interface SharePosterHandle {
/** 生成海报,返回临时图片路径,可直接作为分享 imageUrl */
generate: (opts: PosterOptions) => Promise<string>
}
// 逻辑尺寸CSS px实际位图按 dpr 放大
const W = 300
const H = 420
const CANVAS_ID = 'sharePosterCanvas'
const SharePoster = forwardRef<SharePosterHandle>((_, ref) => {
const { user } = useUser()
const inviterId = user?.id ?? user?.userId
const getCanvasNode = (): Promise<any> => {
return new Promise((resolve, reject) => {
Taro.createSelectorQuery()
.select('#' + CANVAS_ID)
.fields({ node: true, size: true })
.exec((res) => {
const node = res && res[0] && res[0].node
if (node) resolve(node)
else reject(new Error('未找到 canvas 节点'))
})
})
}
const loadImage = (canvas: any, src: string): Promise<any> => {
return new Promise((resolve, reject) => {
if (!src) return reject(new Error('空图片地址'))
const img = canvas.createImage()
img.onload = () => resolve(img)
img.onerror = (e: any) => reject(e)
img.src = src
})
}
const drawRoundRect = (
ctx: any,
x: number,
y: number,
w: number,
h: number,
r: number
) => {
ctx.beginPath()
ctx.moveTo(x + r, y)
ctx.arcTo(x + w, y, x + w, y + h, r)
ctx.arcTo(x + w, y + h, x, y + h, r)
ctx.arcTo(x, y + h, x, y, r)
ctx.arcTo(x, y, x + w, y, r)
ctx.closePath()
}
const drawRoundImage = (
ctx: any,
img: any,
x: number,
y: number,
w: number,
h: number,
r: number
) => {
ctx.save()
drawRoundRect(ctx, x, y, w, h, r)
ctx.clip()
ctx.drawImage(img, x, y, w, h)
ctx.restore()
}
/** 文本按宽度换行,最多 maxLines 行 */
const wrapText = (ctx: any, text: string, maxWidth: number, maxLines: number): string[] => {
const chars = (text || '').split('')
const lines: string[] = []
let line = ''
for (let i = 0; i < chars.length; i++) {
const ch = chars[i]
if (ctx.measureText(line + ch).width > maxWidth && line) {
lines.push(line)
line = ch
if (lines.length === maxLines - 1) break
} else {
line += ch
}
}
if (lines.length < maxLines) lines.push(line)
else if (line) lines[maxLines - 1] = lines[maxLines - 1] + '…'
return lines
}
const generate = async (opts: PosterOptions): Promise<string> => {
const canvas = await getCanvasNode()
const ctx = canvas.getContext('2d')
const dpr = Taro.getSystemInfoSync().pixelRatio || 2
canvas.width = W * dpr
canvas.height = H * dpr
ctx.scale(dpr, dpr)
// 背景
ctx.fillStyle = '#ffffff'
ctx.fillRect(0, 0, W, H)
// 封面图
const coverX = 16
const coverY = 16
const coverW = W - 32
const coverH = 200
if (opts.cover) {
try {
const coverImg = await loadImage(canvas, opts.cover)
drawRoundImage(ctx, coverImg, coverX, coverY, coverW, coverH, 12)
} catch {
ctx.fillStyle = '#f2f2f2'
ctx.fillRect(coverX, coverY, coverW, coverH)
}
} else {
ctx.fillStyle = '#f2f2f2'
ctx.fillRect(coverX, coverY, coverW, coverH)
}
// 主标题
ctx.fillStyle = '#222222'
ctx.font = 'bold 16px sans-serif'
const titleLines = wrapText(ctx, opts.title || '', coverW - 8, 2)
let ty = coverY + coverH + 30
titleLines.forEach((ln) => {
ctx.fillText(ln, coverX, ty)
ty += 22
})
// 价格
let py = ty + 6
if (opts.price !== undefined && opts.price !== '' && opts.price !== null) {
ctx.fillStyle = '#ee0a24'
ctx.font = 'bold 20px sans-serif'
ctx.fillText('¥' + opts.price, coverX, py)
py += 28
}
// 副标题
if (opts.subtitle) {
ctx.fillStyle = '#999999'
ctx.font = '12px sans-serif'
const subLines = wrapText(ctx, opts.subtitle, coverW, 2)
let sy = py
subLines.forEach((ln) => {
ctx.fillText(ln, coverX, sy)
sy += 16
})
}
// 小程序码
const codeSize = 86
const codeX = W - codeSize - 16
const codeY = H - codeSize - 16
try {
const codeUrl = generateShareCode(opts.page || 'pages/index/index', inviterId)
const dl = await Taro.downloadFile({ url: codeUrl })
if (dl.statusCode === 200) {
const codeImg = await loadImage(canvas, dl.tempFilePath)
drawRoundImage(ctx, codeImg, codeX, codeY, codeSize, codeSize, 8)
ctx.fillStyle = '#999999'
ctx.font = '11px sans-serif'
ctx.fillText('长按识别', codeX, codeY - 6)
}
} catch {
// 小程序码生成失败不影响主海报
}
const res = await Taro.canvasToTempFilePath({ canvas })
return res.tempFilePath
}
useImperativeHandle(ref, () => ({ generate }))
return (
<View style={{ position: 'fixed', left: -9999, top: 0, zIndex: -1 }}>
<Canvas type='2d' id={CANVAS_ID} style={{ width: W + 'px', height: H + 'px' }} />
</View>
)
})
SharePoster.displayName = 'SharePoster'
export default SharePoster

69
src/hooks/useShare.ts Normal file
View File

@@ -0,0 +1,69 @@
import { useRef } from 'react'
import Taro, { useShareAppMessage, useShareTimeline } from '@tarojs/taro'
import { useUser } from './useUser'
export interface UseShareOptions {
/** 分享标题 */
title: string
/** 分享给好友的完整 path含或不含 query如 /pages/shop/product-detail?id=123 */
path?: string
/** 朋友圈的 query不含 page 与 ?),如 id=123会被拼到当前页 path 之后 */
query?: string
/** 封面图(海报临时路径或网络图地址),不传则微信截取页面 */
imageUrl?: string
}
/** 当前是否处于朋友圈单页模式scene=1154 */
export function isTimelineSinglePage(): boolean {
try {
const options = Taro.getEnterOptionsSync()
return options?.scene === 1154
} catch {
return false
}
}
function buildPath(path: string | undefined, inviterId?: number | string): string {
if (!path) return ''
if (!inviterId) return path
const sep = path.includes('?') ? '&' : '?'
return `${path}${sep}inviter=${inviterId}`
}
function buildQuery(query: string | undefined, inviterId?: number | string): string {
const parts: string[] = []
if (query) parts.push(query)
if (inviterId) parts.push(`inviter=${inviterId}`)
return parts.join('&')
}
/**
* 统一注册「分享给好友」与「分享到朋友圈」回调。
* 自动在 path/query 中追加 inviter当前登录用户 id用于分销裂变。
* 注意:朋友圈分享仅对非 tabBar 页生效,需在页面 config 中设置 enableShareTimeline: true。
*/
export function useShare(options: UseShareOptions) {
const { user } = useUser()
const inviterId = user?.id ?? user?.userId
// 用 ref 保证分享触发时读取到最新 options含异步生成的海报图
const optionsRef = useRef(options)
optionsRef.current = options
useShareAppMessage(() => {
const o = optionsRef.current
return {
title: o.title,
path: buildPath(o.path, inviterId),
imageUrl: o.imageUrl,
}
})
useShareTimeline(() => {
const o = optionsRef.current
return {
title: o.title,
query: buildQuery(o.query, inviterId),
imageUrl: o.imageUrl,
}
})
}

View File

@@ -1,12 +1,16 @@
import React, { useState, useEffect } from 'react'
import React, { useState, useEffect, useRef } from 'react'
import { View, Text, ScrollView, Image } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { getShopActivity, signUpActivity, cancelSignUpActivity, getSignUpStatus } from '@/api/shop/shopActivity'
import type { ShopActivity, ActivityStatus } from '@/api/shop/shopActivity/model'
import { getCompressedImageUrl } from '@/utils/image'
import { useShare } from '@/hooks/useShare'
import SharePoster, { SharePosterHandle } from '@/components/SharePoster'
definePageConfig({
navigationBarTitleText: '活动详情',
enableShareAppMessage: true,
enableShareTimeline: true,
})
const ActivityDetailPage: React.FC = () => {
@@ -16,6 +20,30 @@ const ActivityDetailPage: React.FC = () => {
const [signedUp, setSignedUp] = useState(false)
const [submitting, setSubmitting] = useState(false)
// 分享 / 朋友圈
const posterRef = useRef<SharePosterHandle>(null)
const [posterPath, setPosterPath] = useState('')
useShare({
title: activity?.name || '精彩活动推荐',
path: `/pages/activity/detail?id=${id}`,
query: `id=${id}`,
imageUrl: posterPath || activity?.image || undefined,
})
// 数据加载完成后异步生成分享海报(带小程序码)
useEffect(() => {
if (activity) {
posterRef.current
?.generate({
cover: activity.image,
title: activity.name || '精彩活动',
page: 'pages/activity/detail',
})
.then(setPosterPath)
.catch(() => {})
}
}, [activity])
useEffect(() => {
if (id) {
fetchDetail(Number(id))
@@ -239,6 +267,8 @@ const ActivityDetailPage: React.FC = () => {
</View>
)}
</View>
{/* 分享海报画布(离屏,用于生成带小程序码的海报图) */}
<SharePoster ref={posterRef} />
</View>
)
}

View File

@@ -1,11 +1,17 @@
import React, { useState, useEffect } from 'react'
import React, { useState, useEffect, useRef } from 'react'
import { View, Text, ScrollView, Image } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { getShopEvent, getRegistrationStatus, getEventStatusLabel } from '@/api/shop/shopEvent'
import type { ShopEvent, RegistrationStatus, EventFormField } from '@/api/shop/shopEvent'
import { getCompressedImageUrl } from '@/utils/image'
import { useShare } from '@/hooks/useShare'
import SharePoster, { SharePosterHandle } from '@/components/SharePoster'
definePageConfig({ navigationBarTitleText: '赛事详情' })
definePageConfig({
navigationBarTitleText: '赛事详情',
enableShareAppMessage: true,
enableShareTimeline: true,
})
const EventDetailPage: React.FC = () => {
const { id } = Taro.getCurrentInstance().router?.params || {}
@@ -13,6 +19,30 @@ const EventDetailPage: React.FC = () => {
const [regStatus, setRegStatus] = useState<RegistrationStatus | null>(null)
const [loading, setLoading] = useState(true)
// 分享 / 朋友圈
const posterRef = useRef<SharePosterHandle>(null)
const [posterPath, setPosterPath] = useState('')
useShare({
title: event?.name || '精彩赛事推荐',
path: `/pages/event/detail?id=${id}`,
query: `id=${id}`,
imageUrl: posterPath || event?.image || undefined,
})
// 数据加载完成后异步生成分享海报(带小程序码)
useEffect(() => {
if (event) {
posterRef.current
?.generate({
cover: event.image,
title: event.name || '精彩赛事',
page: 'pages/event/detail',
})
.then(setPosterPath)
.catch(() => {})
}
}, [event])
useEffect(() => {
if (id) {
fetchDetail(Number(id))
@@ -189,6 +219,8 @@ const EventDetailPage: React.FC = () => {
</View>
)}
</View>
{/* 分享海报画布(离屏,用于生成带小程序码的海报图) */}
<SharePoster ref={posterRef} />
</View>
)
}

View File

@@ -1,13 +1,17 @@
import React, { useState, useEffect } from 'react'
import React, { useState, useEffect, useRef } from 'react'
import { View, Text, Image, ScrollView, RichText } from '@tarojs/components'
import Taro, { useRouter, useShareAppMessage } from '@tarojs/taro'
import Taro, { useRouter } from '@tarojs/taro'
import { getShopPointsProduct } from '@/api/shop/shopPointsProduct'
import type { ShopPointsProduct } from '@/api/shop/shopPointsProduct/model'
import EmptyState from '@/components/common/EmptyState'
import { getCompressedImageUrl } from '@/utils/image'
import { useShare } from '@/hooks/useShare'
import SharePoster, { SharePosterHandle } from '@/components/SharePoster'
definePageConfig({
navigationBarTitleText: '积分商品',
enableShareAppMessage: true,
enableShareTimeline: true,
})
const PointsProductDetail: React.FC = () => {
@@ -16,13 +20,14 @@ const PointsProductDetail: React.FC = () => {
const [product, setProduct] = useState<ShopPointsProduct | null>(null)
const [loading, setLoading] = useState(true)
// 微信分享
useShareAppMessage(() => {
return {
// 分享 / 朋友圈
const posterRef = useRef<SharePosterHandle>(null)
const [posterPath, setPosterPath] = useState('')
useShare({
title: product?.productName || '积分商品推荐',
path: `/pages/points/product-detail?id=${id}`,
imageUrl: product?.productImage || ''
}
query: `id=${id}`,
imageUrl: posterPath || product?.productImage || undefined,
})
useEffect(() => {
@@ -31,6 +36,21 @@ const PointsProductDetail: React.FC = () => {
}
}, [id])
// 数据加载完成后异步生成分享海报(带小程序码)
useEffect(() => {
if (product) {
posterRef.current
?.generate({
cover: product.productImage,
title: product.productName || '积分商品',
subtitle: product.pointsPrice ? `仅需 ${product.pointsPrice} 积分` : undefined,
page: 'pages/points/product-detail',
})
.then(setPosterPath)
.catch(() => {})
}
}, [product])
const loadProduct = async () => {
setLoading(true)
try {
@@ -171,6 +191,9 @@ const PointsProductDetail: React.FC = () => {
<Text className='text-sm'>{isSoldOut ? '已售罄' : '立即兑换'}</Text>
</View>
</View>
{/* 分享海报画布(离屏,用于生成带小程序码的海报图) */}
<SharePoster ref={posterRef} />
</View>
)
}

View File

@@ -1,12 +1,25 @@
import { View, Text, Image } from '@tarojs/components';
import { useState } from 'react';
import Taro from '@tarojs/taro';
import { useShare } from '@/hooks/useShare';
import NavBar from '@/components/NavBar';
definePageConfig({
navigationBarTitleText: '分享赚佣金',
enableShareAppMessage: true,
enableShareTimeline: true,
});
export default function SharePage() {
const [shareLink, setShareLink] = useState('https://paopao.com/ref/abc123');
const [qrCodeUrl, setQrCodeUrl] = useState('');
// 分享 / 朋友圈
useShare({
title: '分享赚佣金,邀请好友一起来',
path: '/pages/share/index',
});
const handleCopyLink = () => {
Taro.setClipboardData({ data: shareLink })
};

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'
import React, { useEffect, useState, useRef } from 'react'
import { View, Text, Image, ScrollView } from '@tarojs/components'
import Taro, { useRouter } from '@tarojs/taro'
import { getShopGroupBuy, listShopGroupBuyRecords, joinGroupBuy, createGroupBuy } from '@/api/shop/shopGroupBuy'
@@ -8,9 +8,13 @@ import type { ShopGoods } from '@/api/shop/shopGoods/model'
import type { ShopGoodsSku } from '@/api/shop/shopGoodsSku/model'
import SkuSelector from '@/components/business/SkuSelector'
import { getCompressedImageUrl } from '@/utils/image'
import { useShare } from '@/hooks/useShare'
import SharePoster, { SharePosterHandle } from '@/components/SharePoster'
definePageConfig({
navigationBarTitleText: '拼团详情',
enableShareAppMessage: true,
enableShareTimeline: true,
})
const GroupBuyDetailPage: React.FC = () => {
@@ -25,6 +29,31 @@ const GroupBuyDetailPage: React.FC = () => {
const [skuVisible, setSkuVisible] = useState(false)
const [pendingRecordId, setPendingRecordId] = useState<number | null>(null)
// 分享 / 朋友圈
const posterRef = useRef<SharePosterHandle>(null)
const [posterPath, setPosterPath] = useState('')
useShare({
title: groupBuy?.goodsName || groupBuy?.product?.name || '拼团商品推荐',
path: `/pages/shop/group-buy-detail?groupBuyId=${groupBuyId}`,
query: `groupBuyId=${groupBuyId}`,
imageUrl: posterPath || groupBuy?.goodsImage || groupBuy?.product?.image || undefined,
})
// 数据加载完成后异步生成分享海报(带小程序码)
useEffect(() => {
if (groupBuy) {
posterRef.current
?.generate({
cover: groupBuy.goodsImage || groupBuy.product?.image,
title: groupBuy.goodsName || groupBuy.product?.name || '拼团商品',
price: groupBuy.groupPrice,
page: 'pages/shop/group-buy-detail',
})
.then(setPosterPath)
.catch(() => {})
}
}, [groupBuy])
useEffect(() => {
if (groupBuyId) {
fetchData()
@@ -317,6 +346,8 @@ const GroupBuyDetailPage: React.FC = () => {
onClose={() => setSkuVisible(false)}
onConfirm={handleSkuConfirm}
/>
{/* 分享海报画布(离屏,用于生成带小程序码的海报图) */}
<SharePoster ref={posterRef} />
</View>
)
}

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'
import React, { useState, useEffect, useRef } from 'react'
import { View, Text, Image, ScrollView, Swiper, SwiperItem, RichText } from '@tarojs/components'
import { Tag } from '@nutui/nutui-react-taro'
import Taro, { useRouter, useShareAppMessage } from '@tarojs/taro'
import Taro, { useRouter } from '@tarojs/taro'
import { getShopGoods } from '@/api/shop/shopGoods'
import {
addShopGoodsFavorite,
@@ -14,12 +14,16 @@ import SkuSelector from '@/components/business/SkuSelector'
import { useCartContext } from '@/contexts/CartContext'
import { useUserContext } from '@/contexts/UserContext'
import { useScrollHeight } from '@/hooks/useScrollHeight'
import { useShare } from '@/hooks/useShare'
import SharePoster, { SharePosterHandle } from '@/components/SharePoster'
import { isGuest } from '@/utils/auth'
import { requireLogin } from '@/utils/login-guard'
import { isVipMember } from '@/utils/vip'
definePageConfig({
navigationBarTitleText: '商品详情',
enableShareAppMessage: true,
enableShareTimeline: true,
})
const ProductDetailPage: React.FC = () => {
@@ -35,13 +39,14 @@ const ProductDetailPage: React.FC = () => {
// 底部操作栏高度约 90px含按钮 + 安全区),动态计算 ScrollView 可用高度
const scrollHeight = useScrollHeight(44)
// 微信分享配置
useShareAppMessage(() => {
return {
// 分享 / 朋友圈
const posterRef = useRef<SharePosterHandle>(null)
const [posterPath, setPosterPath] = useState('')
useShare({
title: product?.name || product?.goodsName || '优质商品推荐',
path: `/pages/shop/product-detail?id=${id}`,
imageUrl: product?.image || ''
}
query: `id=${id}`,
imageUrl: posterPath || product?.image || undefined,
})
useEffect(() => {
@@ -53,10 +58,20 @@ const ProductDetailPage: React.FC = () => {
}
}, [id, isLoggedIn])
// 监听 product 变化后再添加到历史记录
// 监听 product 变化后再添加到历史记录,并异步生成分享海报
useEffect(() => {
if (product) {
addToHistory()
posterRef.current
?.generate({
cover: product.image,
title: product.name || product.goodsName || '优质商品推荐',
price:
product.dealerPrice && isVipMember() ? product.dealerPrice : product.price,
page: 'pages/shop/product-detail',
})
.then(setPosterPath)
.catch(() => {})
}
}, [product])
@@ -444,6 +459,9 @@ const ProductDetailPage: React.FC = () => {
onClose={() => setSkuVisible(false)}
onConfirm={handleSkuConfirm}
/>
{/* 分享海报画布(离屏,用于生成带小程序码的海报图) */}
<SharePoster ref={posterRef} />
</View>
)
}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'
import React, { useEffect, useState, useRef } from 'react'
import { View, Text, Image, ScrollView } from '@tarojs/components'
import Taro, { useRouter } from '@tarojs/taro'
import { getShopSeckill, createSeckillOrder } from '@/api/shop/shopSeckill'
@@ -7,9 +7,13 @@ import type { ShopSeckill } from '@/api/shop/shopSeckill/model'
import type { ShopGoods } from '@/api/shop/shopGoods/model'
import type { ShopGoodsSku } from '@/api/shop/shopGoodsSku/model'
import SkuSelector from '@/components/business/SkuSelector'
import { useShare } from '@/hooks/useShare'
import SharePoster, { SharePosterHandle } from '@/components/SharePoster'
definePageConfig({
navigationBarTitleText: '秒杀详情',
enableShareAppMessage: true,
enableShareTimeline: true,
})
const SeckillDetailPage: React.FC = () => {
@@ -24,6 +28,31 @@ const SeckillDetailPage: React.FC = () => {
const [countdown, setCountdown] = useState('')
const [skuVisible, setSkuVisible] = useState(false)
// 分享 / 朋友圈
const posterRef = useRef<SharePosterHandle>(null)
const [posterPath, setPosterPath] = useState('')
useShare({
title: seckill?.goodsName || seckill?.product?.name || '秒杀商品推荐',
path: `/pages/shop/seckill-detail?seckillId=${seckillId}`,
query: `seckillId=${seckillId}`,
imageUrl: posterPath || seckill?.goodsImage || seckill?.product?.image || undefined,
})
// 数据加载完成后异步生成分享海报(带小程序码)
useEffect(() => {
if (seckill) {
posterRef.current
?.generate({
cover: seckill.goodsImage || seckill.product?.image,
title: seckill.goodsName || seckill.product?.name || '秒杀商品',
price: seckill.seckillPrice,
page: 'pages/shop/seckill-detail',
})
.then(setPosterPath)
.catch(() => {})
}
}, [seckill])
useEffect(() => {
if (seckillId) {
fetchData()
@@ -272,6 +301,8 @@ const SeckillDetailPage: React.FC = () => {
onClose={() => setSkuVisible(false)}
onConfirm={handleSkuConfirm}
/>
{/* 分享海报画布(离屏,用于生成带小程序码的海报图) */}
<SharePoster ref={posterRef} />
</View>
)
}

View File

@@ -3,16 +3,24 @@ import { View, Text, ScrollView, Input, Button, Image } from '@tarojs/components
import Taro, { useRouter } from '@tarojs/taro'
import { useUser } from '@/hooks/useUser'
import { useScrollHeight } from '@/hooks/useScrollHeight'
import { useShare } from '@/hooks/useShare'
import { getMyRegisterList, addSubordinate, updateSubordinate, MemberRegister } from '@/api/shop/shopMemberRegister'
import EmptyState from '@/components/common/EmptyState'
definePageConfig({
navigationBarTitleText: '录入下级',
enableShareAppMessage: true,
enableShareTimeline: true,
})
const InviteSubordinatePage: React.FC = () => {
const router = useRouter()
const { user, isLoggedIn } = useUser()
// 分享 / 朋友圈:带当前用户 id 作为邀请归因
useShare({
title: '邀请下级会员,一起拓客赚钱',
path: '/pages/user/invite-subordinate',
})
const scrollHeight = useScrollHeight()
const [loading, setLoading] = useState(true)
const [submitting, setSubmitting] = useState(false)

View File

@@ -2,14 +2,22 @@ import React, { useState, useEffect } from 'react'
import { View, Text, ScrollView, Button, Image } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { useUser } from '@/hooks/useUser'
import { useShare } from '@/hooks/useShare'
import { useScrollHeight } from '@/hooks/useScrollHeight'
definePageConfig({
navigationBarTitleText: '分销推广',
enableShareAppMessage: true,
enableShareTimeline: true,
})
const PromotionPage: React.FC = () => {
const { user, isLoggedIn } = useUser()
// 分享 / 朋友圈:分销推广页,带当前用户 id 作为邀请归因
useShare({
title: '邀请好友,一起赚佣金',
path: '/pages/user/promotion',
})
const [inviteCode, setInviteCode] = useState('')
const [inviteLink, setInviteLink] = useState('')
const [qrCodeUrl, setQrCodeUrl] = useState('')

View File

@@ -2,15 +2,23 @@ import React, { useState, useEffect } from 'react'
import { View, Text, Input } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { useUser } from '@/hooks/useUser'
import { useShare } from '@/hooks/useShare'
import { addShopDealerApply, listShopDealerApply } from '@/api/shop/shopDealerApply'
import type { ShopDealerApply } from '@/api/shop/shopDealerApply/model'
definePageConfig({
navigationBarTitleText: '升级VIP会员',
enableShareAppMessage: true,
enableShareTimeline: true,
})
const VipUpgradePage: React.FC = () => {
const { user, isLoggedIn } = useUser()
// 分享 / 朋友圈
useShare({
title: '升级VIP会员享专属优惠价',
path: '/pages/user/vip-upgrade',
})
const [realName, setRealName] = useState('')
const [address, setAddress] = useState('')
const [loading, setLoading] = useState(false)