From d6d6b1f5a70da016d6b3ba184678b712c5d03fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Mon, 13 Jul 2026 08:16:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(user):=20=E4=BD=BF=E7=94=A8=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E5=8E=9F=E7=94=9F=20Picker=20=E6=9B=BF=E6=8D=A2?= =?UTF-8?q?=E6=89=80=E5=9C=A8=E5=9C=B0=E9=80=89=E6=8B=A9=E5=BC=B9=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 NutUI Address 相关导入、状态及地区数据转换逻辑 - 新增派生 regionValue 用于绑定微信原生 Picker 的 value - 将 handleRegionChange 调整为处理微信 Picker 的事件 - 将所在地选择 UI 修改为微信原生 Picker 包裹的视图结构 - 地图定位失败使用弹窗提示,引导用户手动选择或重试 - 移除手动打开地区选择器的函数及 NutUI 组件 - 保留 RegionData 用于文本智能识别解析 - 编译通过,提升在无地图或无权限环境下的地区选择稳定性 --- .workbuddy/memory/2026-07-13.md | 10 + .workbuddy/memory/MEMORY.md | 8 + src/api/share.ts | 23 +++ src/api/shop/shopInvite/index.ts | 7 +- src/components/SharePoster/index.tsx | 204 ++++++++++++++++++++ src/hooks/useShare.ts | 69 +++++++ src/pages/activity/detail/index.tsx | 32 ++- src/pages/event/detail/index.tsx | 36 +++- src/pages/points/product-detail.tsx | 41 +++- src/pages/share/index.tsx | 13 ++ src/pages/shop/group-buy-detail/index.tsx | 33 +++- src/pages/shop/product-detail.tsx | 38 +++- src/pages/shop/seckill-detail/index.tsx | 33 +++- src/pages/user/invite-subordinate/index.tsx | 8 + src/pages/user/promotion/index.tsx | 8 + src/pages/user/vip-upgrade/index.tsx | 8 + 16 files changed, 546 insertions(+), 25 deletions(-) create mode 100644 .workbuddy/memory/2026-07-13.md create mode 100644 src/api/share.ts create mode 100644 src/components/SharePoster/index.tsx create mode 100644 src/hooks/useShare.ts diff --git a/.workbuddy/memory/2026-07-13.md b/.workbuddy/memory/2026-07-13.md new file mode 100644 index 0000000..b204234 --- /dev/null +++ b/.workbuddy/memory/2026-07-13.md @@ -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()`。 diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index bd7afa5..54d8fe0 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -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 页(首页/分类/订单/我的)不支持朋友圈分享;朋友圈分享必须真机测试(开发者工具无预览) diff --git a/src/api/share.ts b/src/api/share.ts new file mode 100644 index 0000000..11ea298 --- /dev/null +++ b/src/api/share.ts @@ -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', + }) +} diff --git a/src/api/shop/shopInvite/index.ts b/src/api/shop/shopInvite/index.ts index ba38f75..aaec8e7 100644 --- a/src/api/shop/shopInvite/index.ts +++ b/src/api/shop/shopInvite/index.ts @@ -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) { diff --git a/src/components/SharePoster/index.tsx b/src/components/SharePoster/index.tsx new file mode 100644 index 0000000..85e47bc --- /dev/null +++ b/src/components/SharePoster/index.tsx @@ -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 +} + +// 逻辑尺寸(CSS px),实际位图按 dpr 放大 +const W = 300 +const H = 420 +const CANVAS_ID = 'sharePosterCanvas' + +const SharePoster = forwardRef((_, ref) => { + const { user } = useUser() + const inviterId = user?.id ?? user?.userId + + const getCanvasNode = (): Promise => { + 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 => { + 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 => { + 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 ( + + + + ) +}) + +SharePoster.displayName = 'SharePoster' + +export default SharePoster diff --git a/src/hooks/useShare.ts b/src/hooks/useShare.ts new file mode 100644 index 0000000..23f5cd9 --- /dev/null +++ b/src/hooks/useShare.ts @@ -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, + } + }) +} diff --git a/src/pages/activity/detail/index.tsx b/src/pages/activity/detail/index.tsx index bc41926..e567775 100644 --- a/src/pages/activity/detail/index.tsx +++ b/src/pages/activity/detail/index.tsx @@ -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(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 = () => { )} + {/* 分享海报画布(离屏,用于生成带小程序码的海报图) */} + ) } diff --git a/src/pages/event/detail/index.tsx b/src/pages/event/detail/index.tsx index 90fa9a2..7bddf1b 100644 --- a/src/pages/event/detail/index.tsx +++ b/src/pages/event/detail/index.tsx @@ -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(null) const [loading, setLoading] = useState(true) + // 分享 / 朋友圈 + const posterRef = useRef(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 = () => { )} + {/* 分享海报画布(离屏,用于生成带小程序码的海报图) */} + ) } diff --git a/src/pages/points/product-detail.tsx b/src/pages/points/product-detail.tsx index 67fa8f0..80efe80 100644 --- a/src/pages/points/product-detail.tsx +++ b/src/pages/points/product-detail.tsx @@ -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(null) const [loading, setLoading] = useState(true) - // 微信分享 - useShareAppMessage(() => { - return { - title: product?.productName || '积分商品推荐', - path: `/pages/points/product-detail?id=${id}`, - imageUrl: product?.productImage || '' - } + // 分享 / 朋友圈 + const posterRef = useRef(null) + const [posterPath, setPosterPath] = useState('') + useShare({ + title: product?.productName || '积分商品推荐', + path: `/pages/points/product-detail?id=${id}`, + 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 = () => { {isSoldOut ? '已售罄' : '立即兑换'} + + {/* 分享海报画布(离屏,用于生成带小程序码的海报图) */} + ) } diff --git a/src/pages/share/index.tsx b/src/pages/share/index.tsx index ec65af4..40a2645 100644 --- a/src/pages/share/index.tsx +++ b/src/pages/share/index.tsx @@ -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 }) }; diff --git a/src/pages/shop/group-buy-detail/index.tsx b/src/pages/shop/group-buy-detail/index.tsx index 1990769..923e202 100644 --- a/src/pages/shop/group-buy-detail/index.tsx +++ b/src/pages/shop/group-buy-detail/index.tsx @@ -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(null) + // 分享 / 朋友圈 + const posterRef = useRef(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} /> + {/* 分享海报画布(离屏,用于生成带小程序码的海报图) */} + ) } diff --git a/src/pages/shop/product-detail.tsx b/src/pages/shop/product-detail.tsx index c737420..5ba9687 100644 --- a/src/pages/shop/product-detail.tsx +++ b/src/pages/shop/product-detail.tsx @@ -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 { - title: product?.name || product?.goodsName || '优质商品推荐', - path: `/pages/shop/product-detail?id=${id}`, - imageUrl: product?.image || '' - } + // 分享 / 朋友圈 + const posterRef = useRef(null) + const [posterPath, setPosterPath] = useState('') + useShare({ + title: product?.name || product?.goodsName || '优质商品推荐', + path: `/pages/shop/product-detail?id=${id}`, + 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} /> + + {/* 分享海报画布(离屏,用于生成带小程序码的海报图) */} + ) } diff --git a/src/pages/shop/seckill-detail/index.tsx b/src/pages/shop/seckill-detail/index.tsx index f3d4a20..4be33c2 100644 --- a/src/pages/shop/seckill-detail/index.tsx +++ b/src/pages/shop/seckill-detail/index.tsx @@ -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(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} /> + {/* 分享海报画布(离屏,用于生成带小程序码的海报图) */} + ) } diff --git a/src/pages/user/invite-subordinate/index.tsx b/src/pages/user/invite-subordinate/index.tsx index d6d44dd..80bd5e2 100644 --- a/src/pages/user/invite-subordinate/index.tsx +++ b/src/pages/user/invite-subordinate/index.tsx @@ -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) diff --git a/src/pages/user/promotion/index.tsx b/src/pages/user/promotion/index.tsx index 611bfa5..df2932f 100644 --- a/src/pages/user/promotion/index.tsx +++ b/src/pages/user/promotion/index.tsx @@ -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('') diff --git a/src/pages/user/vip-upgrade/index.tsx b/src/pages/user/vip-upgrade/index.tsx index 476eb8a..beefbf4 100644 --- a/src/pages/user/vip-upgrade/index.tsx +++ b/src/pages/user/vip-upgrade/index.tsx @@ -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)