diff --git a/src/components/Poster/index.tsx b/src/components/Poster/index.tsx index 110e536..be0db94 100644 --- a/src/components/Poster/index.tsx +++ b/src/components/Poster/index.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useImperativeHandle, forwardRef } from "react"; import { Canvas } from "@tarojs/components"; import Taro from "@tarojs/taro"; @@ -11,19 +11,59 @@ function getImageWh(src): Promise<{ width: number; height: number }> { }); } +const qrCodeUrl = + "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/5e013195-fc79-4082-bf06-9aa79aea65ae.png"; + const ringUrl = - "http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/b635164f-ecec-434a-a00b-69614a918f2f.png"; + "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/b635164f-ecec-434a-a00b-69614a918f2f.png"; -const Poster = (props) => { - const carouselUrl = - "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/63f62c80-ac44-4f3b-bb6c-d7f6e8ebf76d.jpg"; +const dateIcon = + "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/1b49476e-0eda-42ff-b08c-002ce510df82.jpg"; - const avatarUrl = - "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/aac792b0-6f81-4192-ae55-04bee417167c.png"; +const mapIcon = + "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/06b994fa-9227-4708-8555-8a07af8d0c3b.jpg"; + +// const logo = "http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/fb732da6-11b9-4022-a524-a377b17635eb.jpg" + +const logoText = + "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/9d8cbc9d-9601-4e2d-ab23-76420a4537d6.png"; + +const Poster = (props, ref) => { + const { data } = props; + const { + playType, + ntrp, + mainCoursal, + nickname, + avatarUrl, + title, + locationName, + date, + time, + } = data; useEffect(() => { drawCard(); }, []); + useImperativeHandle(ref, () => ({ + generateImage: () => + new Promise((resolve, reject) => { + const query = Taro.createSelectorQuery(); + query + .select("#cardCanvas") + .fields({ node: true, size: true }) + .exec((res) => { + const canvas = res[0].node; + // ⚠️ 关键:传 canvas,而不是 canvasId + Taro.canvasToTempFilePath({ + canvas, + success: (res) => resolve(res.tempFilePath), + fail: (err) => reject(err), + }); + }); + }), + })); + const drawCard = async () => { const query = Taro.createSelectorQuery(); query @@ -47,11 +87,11 @@ const Poster = (props) => { roundRectGradient(ctx, 0, 0, width, height, 24, "#BFFFEF", "#F2FFFC"); // 顶部图片 - const img = await loadImage(canvas, carouselUrl); + const img = await loadImage(canvas, mainCoursal); // roundRect(ctx, 20, 20, width - 40, width - 40, 20, "#fff"); await drawCoverImage( ctx, - carouselUrl, + mainCoursal, img, 10, 10, @@ -61,8 +101,8 @@ const Poster = (props) => { ); // 标签 - let left = drawTag(ctx, "单打", 18, 18); - drawTag(ctx, "NTRP 2.5 - 3.0", left + 4, 18); + let left = drawTag(ctx, playType, 18, 18); + drawTag(ctx, ntrp, left + 4, 18); let top = width - 10; left = 16; @@ -87,7 +127,7 @@ const Poster = (props) => { ctx.fillStyle = "#333"; ctx.font = "bold 28px sans-serif"; // ctx.fillText("华巴轮卡 邀你加入球局", 100, 370); - const nickNameText = "华巴轮卡 邀你加入"; + const nickNameText = `${nickname} 邀你加入`; ctx.fillText(nickNameText, left, top); let textW = ctx.measureText(nickNameText).width; left += textW; @@ -102,56 +142,77 @@ const Poster = (props) => { // 活动标题 ctx.fillStyle = "#333"; ctx.font = "bold 34px sans-serif"; - const r = drawTextWrap( - ctx, - "周一晚上浦东新区单打约球", - left, - top, - width - 32, - 40 - ); + let r = drawTextWrap(ctx, title, left, top, width - 32, 40); - top = r.top + 50; + top = r.top + 40; + left = 16; + const dateImg = await loadImage(canvas, dateIcon); + await drawCoverImage(ctx, dateIcon, dateImg, left, top, 40, 40, 8); + + left += 40 + 8; + + top += 30; // 时间 ctx.font = "26px sans-serif"; ctx.fillStyle = "#00B578"; - const day = "6月20日 (周五)"; - ctx.fillText(day, 80, top); - textW = ctx.measureText(day).width; - left = 80 + 8 + textW; + ctx.fillText(date, left, top); + textW = ctx.measureText(date).width; + left += 8 + textW; ctx.fillStyle = "#333"; - ctx.fillText("下午5点 2小时", left, top); + ctx.fillText(time, left, top); + + left = 16; + top += 24; + const mapImg = await loadImage(canvas, mapIcon); + await drawCoverImage(ctx, dateIcon, mapImg, left, top, 40, 40, 8); + + left += 40 + 8; + top += 30; // 地址 ctx.fillStyle = "#666"; ctx.font = "26px sans-serif"; - drawTextWrap( - ctx, - "因乐驰网球俱乐部 (嘉定江桥万达店)", - 80, - 560, - 480, - 34 + r = drawTextWrap(ctx, locationName, left, top, width - 32 - left, 34); + + left = 16; + top = r.top + 60; + + const logoWh = await getImageWh(logoText); + console.log(logoWh); + const logoTextImg = await loadImage(canvas, logoText); + ctx.drawImage( + logoTextImg, + left, + top, + 400, + // 56 + 400 / (logoWh.width / logoWh.height) ); + const qrImg = await loadImage(canvas, qrCodeUrl); + ctx.drawImage(qrImg, width - 12 - 150, top - 50, 160, 160); + + left = 16; + top += 400 / (logoWh.width / logoWh.height) + 30; + // 底部文字 ctx.fillStyle = "#333"; - ctx.font = "24px sans-serif"; - ctx.fillText("有场 · 网球", 40, 960); + ctx.font = "20px sans-serif"; + ctx.fillText("长按识别二维码,快来加入,有你就有场!", left, top); // 小程序码 // const qrcode = await loadImage(canvas, "小程序码路径"); // ctx.drawImage(qrcode, 480, 880, 100, 100); // 导出图片 - Taro.canvasToTempFilePath({ - canvas, - success: (res) => { - console.log("导出路径", res.tempFilePath); - }, - fail: (err) => console.error(err), - }); + // Taro.canvasToTempFilePath({ + // canvas, + // success: (res) => { + // console.log("导出路径", res.tempFilePath); + // }, + // fail: (err) => console.error(err), + // }); }); }; @@ -291,4 +352,4 @@ const Poster = (props) => { ); }; -export default Poster; +export default forwardRef(Poster); diff --git a/src/components/UploadCover/upload-source-popup.scss b/src/components/UploadCover/upload-source-popup.scss index bb8fbcd..6e455e3 100644 --- a/src/components/UploadCover/upload-source-popup.scss +++ b/src/components/UploadCover/upload-source-popup.scss @@ -1,4 +1,4 @@ -@use '~@/scss/themeColor.scss' as theme; +@use "~@/scss/themeColor.scss" as theme; .upload-source-popup-text { width: 100%; @@ -131,14 +131,28 @@ align-items: flex-start; gap: 8px; - .upload-popup-footer-cancel, .upload-popup-footer-confirm { - font-feature-settings: 'liga' off, 'clig' off; + .upload-popup-footer-cancel, + .upload-popup-footer-confirm { + font-feature-settings: "liga" off, "clig" off; font-family: "PingFang SC"; box-sizing: border-box; height: 44px; border-radius: 12px; border: 1px solid rgba(0, 0, 0, 0.12); flex: 1; + + display: flex; + justify-content: center; + align-items: center; + + color: #000; + font-feature-settings: "liga" off, "clig" off; + font-family: "PingFang SC"; + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: 20px; + letter-spacing: -0.23px; } .upload-popup-footer-cancel { @@ -154,4 +168,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/config/images.js b/src/config/images.js index 920240f..ff26b3b 100644 --- a/src/config/images.js +++ b/src/config/images.js @@ -37,7 +37,6 @@ export default { ICON_CIRCLE_SELECT_ARROW: require('@/static/publishBall/icon-circle-select-arrow.svg'), ICON_LOGO: require('@/static/logo.svg'), ICON_CHANGE: require('@/static/list/icon-change.svg'), - ICON_WEATHER_SUN: require('@/static/detail/icon-weather-sun.svg'), ICON_DETAIL_MAP: require('@/static/detail/icon-map.svg'), ICON_DETAIL_ARROW_RIGHT: require('@/static/detail/icon-arrow-right.svg'), ICON_DETAIL_NOTICE: require('@/static/detail/icon-notice.svg'), diff --git a/src/game_pages/detail/config.ts b/src/game_pages/detail/config.ts new file mode 100644 index 0000000..396f1b1 --- /dev/null +++ b/src/game_pages/detail/config.ts @@ -0,0 +1,9 @@ +export const DayOfWeekMap = new Map([ + [0, "周日"], + [1, "周一"], + [2, "周二"], + [3, "周三"], + [4, "周四"], + [5, "周五"], + [6, "周六"], +]); \ No newline at end of file diff --git a/src/game_pages/detail/index.config.ts b/src/game_pages/detail/index.config.ts index 3275f5e..6b4a8bb 100644 --- a/src/game_pages/detail/index.config.ts +++ b/src/game_pages/detail/index.config.ts @@ -2,4 +2,5 @@ export default definePageConfig({ navigationBarTitleText: '球局详情', navigationStyle: 'custom', enableShareAppMessage: true, + enableShareTimeline: true, }) diff --git a/src/game_pages/detail/index.tsx b/src/game_pages/detail/index.tsx index 85889cb..c4866cb 100644 --- a/src/game_pages/detail/index.tsx +++ b/src/game_pages/detail/index.tsx @@ -6,7 +6,6 @@ import React, { forwardRef, } from "react"; import { View, Text, Image, Map, ScrollView, Button } from "@tarojs/components"; -// import { Avatar } from "@nutui/nutui-react-taro"; import Taro, { useRouter, useShareAppMessage, @@ -25,6 +24,7 @@ import { Comments, Poster, } from "@/components"; +import { generateShareImage, generatePosterImage } from "@/utils"; import DetailService, { MATCH_STATUS, IsSubstituteSupported, @@ -35,6 +35,12 @@ import { getCurrentLocation, calculateDistance } from "@/utils/locationUtils"; import { useUserInfo, useUserActions } from "@/store/userStore"; import { EvaluateCallback, EvaluateScene } from "@/store/evaluateStore"; import img from "@/config/images"; +import DownloadIcon from "@/static/detail/download_icon.svg"; +import WechatLogo from "@/static/detail/wechat_icon.svg"; +import WechatTimeline from "@/static/detail/wechat_timeline.svg"; +import LinkIcon from "@/static/detail/link.svg"; +import CrossIcon from "@/static/detail/cross.svg"; +import { DayOfWeekMap } from "./config"; import styles from "./style.module.scss"; import "./index.scss"; @@ -173,100 +179,281 @@ function Coursel(props) { ); } -// 分享弹窗 -const SharePopup = forwardRef( - ({ id, from }: { id: string; from: string }, ref) => { - const [visible, setVisible] = useState(true); - - useImperativeHandle(ref, () => ({ - show: () => { - setVisible(true); - }, - })); - - useShareAppMessage((res) => { - console.log(res, "res"); - return { - title: "分享", - imageUrl: "https://img.yzcdn.cn/vant/cat.jpeg", - path: `/game_pages/detail/index?id=${id}&from=share`, - }; - }); - - // function handleShareToWechatMoments() { - // useShareTimeline(() => { - // return { - // title: '分享', - // path: `/game_pages/detail/index?id=${id}&from=share`, - // } - // }) - // } - - function handleSaveToLocal() { - Taro.showToast({ title: "not yet", icon: "error" }); - return; - Taro.saveImageToPhotosAlbum({ - filePath: "", - success: () => { - Taro.showToast({ title: "保存成功", icon: "success" }); - }, - fail: () => { - Taro.showToast({ title: "保存失败", icon: "none" }); - }, +const PosterPopup = forwardRef((props, ref) => { + const [visible, setVisible] = useState(false); + const [posterData, setPosterData] = useState(); + const posterRef = useRef(); + useImperativeHandle(ref, () => ({ + show: (detail, user) => { + setVisible(true); + const { + play_type, + skill_level_max, + skill_level_min, + image_list, + title, + start_time, + end_time, + location_name, + } = detail; + const { avatar_url, nickname } = user; + const startTime = dayjs(start_time); + const endTime = dayjs(end_time); + const dayofWeek = DayOfWeekMap.get(startTime.day()); + const gameLength = `${endTime.diff(startTime, "hour")}小时`; + setPosterData({ + playType: play_type, + ntrp: `NTRP ${genNTRPRequirementText( + skill_level_min, + skill_level_max + )}`, + mainCoursal: + image_list[0] || + "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png", + nickname, + avatarUrl: avatar_url, + title, + locationName: location_name, + date: `${startTime.format("M月D日")} (${dayofWeek})`, + time: `${startTime.format("ah")}点 ${gameLength}`, }); - } + }, + })); - return ( - <> - {/* { - setVisible(false); - }} - hideFooter - style={{ minHeight: "100px" }} - > - - - 分享至 - - - - - - - */} - { - setVisible(false); - }} - showHeader={false} - position="center" - hideFooter - enableDragToClose={false} - style={{ minHeight: "100px" }} - > - - - - - - ); + useShareAppMessage(async () => { + const tempFilePath = await posterRef.current.generateImage(); + return { + // title: detail.title, + imageUrl: tempFilePath, + path: `/game_pages/detail/index?id=${props.id}&from=share`, + }; + }); + + useShareTimeline(async () => { + const tempFilePath = await posterRef.current.generateImage(); + return { + title: "分享", + imageUrl: tempFilePath, + path: `/game_pages/detail/index?id=${props.id}&from=share`, + }; + }); + + function onClose() { + setVisible(false); + setPosterData(undefined); + Taro.updateShareMenu({ + isUpdatableMessage: true, // 是否是动态消息(需要服务端配置过模版) + }); } -); + + async function handleShare() { + const tempFilePath = await posterRef.current.generateImage(); + Taro.showShareImageMenu({ + path: tempFilePath, + }); + } + return ( + visible && ( + + + + {posterData && } + + + + + + + + + ) + ); +}); + +// 分享弹窗 +const SharePopup = forwardRef(({ id, from, detail, userInfo }, ref) => { + const [visible, setVisible] = useState(false); + const posterRef = useRef(); + + useEffect(() => { + changeMessageType(); + }, []); + + async function changeMessageType() { + try { + const res = await DetailService.getActivityId({ + business_id: id, + business_type: "game", + is_private: false, + }); + if (res.code === 0) { + Taro.updateShareMenu({ + withShareTicket: false, // 是否需要返回 shareTicket + isUpdatableMessage: true, // 是否是动态消息(需要服务端配置过模版) + activityId: res.data.activity_id, // 动态消息的活动 id + }); + } + } catch (e) { + Taro.showToast({ title: e.message, icon: "none" }); + } + } + + useImperativeHandle(ref, () => ({ + show: () => { + setVisible(true); + }, + })); + + useShareAppMessage(async (res) => { + const { + play_type, + skill_level_max, + skill_level_min, + start_time, + end_time, + location_name, + venue_image_list, + } = detail || {}; + const startTime = dayjs(start_time); + const endTime = dayjs(end_time); + const dayofWeek = DayOfWeekMap.get(startTime.day()); + const gameLength = `${endTime.diff(startTime, "hour")}小时`; + const url = await generateShareImage({ + userAvatar: userInfo.avatar_url, + userNickname: userInfo.nickname, + gameType: play_type, + skillLevel: `NTRP ${genNTRPRequirementText( + skill_level_min, + skill_level_max + )}`, + gameDate: `${startTime.format("M月D日")} (${dayofWeek})`, + gameTime: `${startTime.format("ah")}点 ${gameLength}`, + venueName: location_name, + venueImages: venue_image_list ? venue_image_list.map((c) => c.url) : [], + }); + // console.log(res, "res"); + return { + title: detail.title, + imageUrl: url || "https://img.yzcdn.cn/vant/cat.jpeg", + path: `/game_pages/detail/index?id=${id}&from=share`, + }; + }); + + async function handlePost() { + const { + play_type, + skill_level_max, + skill_level_min, + start_time, + end_time, + location_name, + image_list, + title, + } = detail || {}; + const { avatar_url, nickname } = userInfo; + const startTime = dayjs(start_time); + const endTime = dayjs(end_time); + const dayofWeek = DayOfWeekMap.get(startTime.day()); + const gameLength = `${endTime.diff(startTime, "hour")}小时`; + Taro.showLoading({ title: "生成中..." }); + const url = await generatePosterImage({ + playType: play_type, + ntrp: `NTRP ${genNTRPRequirementText(skill_level_min, skill_level_max)}`, + mainCoursal: + image_list[0] || + "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/0621b8cf-f7d6-43ad-b852-7dc39f29a782.png", + nickname, + avatarUrl: avatar_url, + title, + locationName: location_name, + date: `${startTime.format("M月D日")} (${dayofWeek})`, + time: `${startTime.format("ah")}点 ${gameLength}`, + }); + Taro.hideLoading(); + setVisible(false); + Taro.showShareImageMenu({ + path: url, + }); + } + + function onClose() { + setVisible(false); + } + + return ( + <> + + + + 分享至 + + + + + + + + + + + + {/* */} + + ); +}); function navto(url) { Taro.navigateTo({ @@ -1346,6 +1533,8 @@ function Index() { ref={sharePopupRef} id={id as string} from={from as string} + detail={detail} + userInfo={userInfo} /> diff --git a/src/game_pages/detail/style.module.scss b/src/game_pages/detail/style.module.scss index 2df4699..7772d09 100644 --- a/src/game_pages/detail/style.module.scss +++ b/src/game_pages/detail/style.module.scss @@ -2,26 +2,173 @@ .title { padding: 20px; color: #000; - text-align: center; + // text-align: center; font-family: "PingFang SC"; font-size: 18px; font-style: normal; font-weight: 600; line-height: 28px; + display: flex; + align-items: center; + justify-content: space-between; + + .closeIconWrap { + display: flex; + width: 40px; + height: 40px; + justify-content: center; + align-items: center; + gap: 6px; + flex-shrink: 0; + border-radius: 999px; + border: 1px solid rgba(0, 0, 0, 0.06); + background: #fff; + box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.06); + + .closeIcon { + width: 24px; + height: 24px; + } + } } .shareItems { display: flex; align-items: center; - justify-content: space-between; + justify-content: space-around; padding-bottom: 60px; .button { - width: 140px; - height: 40px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + gap: 12px; + color: rgba(0, 0, 0, 0.85); + font-feature-settings: "liga" off, "clig" off; + font-family: "PingFang SC"; + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: normal; + + background-color: #fff; + border: none; + padding: 0; + margin: 0; + line-height: normal; + font-size: inherit; + color: inherit; + + &:after { + border: none; + background: transparent; + } + + .icon { + width: 64px; + height: 64px; + border-radius: 50%; + background-color: #fff; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid rgba(0, 0, 0, 0.06); + box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.1); + + &.wechatIcon { + background-color: #07c160; + } + .download { + width: 28px; + height: 28px; + } + .wechat { + width: 36px; + height: 30px; + } + .linkIcon { + width: 28px; + height: 28px; + } + } } } } -.posterWrap { +.posterContainer { + background: linear-gradient(180deg, #fff 0%, #fafafa 100%), #fff; + padding: 20px; +} + +.posterWrap { + border-radius: 19.067px; + border: 1px solid rgba(0, 0, 0, 0.06); + background: linear-gradient(180deg, #bfffef 0%, #f2fffc 100%), #fff; + box-shadow: 0 6.933px 55.467px 0 rgba(0, 0, 0, 0.1); + overflow: hidden; + box-sizing: border-box; +} + +.sharePoster { + margin-top: 40px; + display: flex; + align-items: center; + justify-content: space-around; + + .shareItem { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + gap: 12px; + color: rgba(0, 0, 0, 0.85); + font-feature-settings: "liga" off, "clig" off; + font-family: "PingFang SC"; + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: normal; + + background-color: #fff; + border: none; + padding: 0; + margin: 0; + line-height: normal; + font-size: inherit; + color: inherit; + + &:after { + border: none; + background: transparent; + } + + .icon { + width: 64px; + height: 64px; + border-radius: 50%; + background-color: #fff; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid rgba(0, 0, 0, 0.06); + box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.1); + + &.wechatIcon { + background-color: #07c160; + } + .download { + width: 28px; + height: 28px; + } + .wechat { + width: 36px; + height: 30px; + } + .timeline { + width: 32px; + height: 32px; + } + } + } } diff --git a/src/scss/images.scss b/src/scss/images.scss index 28b8db1..25bc579 100644 --- a/src/scss/images.scss +++ b/src/scss/images.scss @@ -1,53 +1,51 @@ // src/scss/images.scss // 暴露公共API (可选) -@forward 'sass:map'; -@forward 'sass:meta'; -@use 'sass:map'; +@forward "sass:map"; +@forward "sass:meta"; +@use "sass:map"; // 使用私有变量命名 (前缀加 -) -$-static-path: '~@/static/' !default; +$-static-path: "~@/static/" !default; // 图片映射表 $-images: ( - 'icon-upload': '/publishBall/icon-upload.svg', - 'icon-add': '/publishBall/icon-add.svg', - 'icon-location': '/publishBall/icon-location.svg', - 'icon-gameplay': '/publishBall/icon-gameplay.svg', - 'icon-personal': '/publishBall/icon-personal.svg', - 'icon-changda': '/publishBall/icon-changda.svg', - 'icon-cost': '/publishBall/icon-cost.svg', - 'icon-remove': '/publishBall/icon-remove.svg', - 'icon-arrow-left': '/detail/icon-arrow-left.svg', - 'icon-logo-go': '/detail/icon-logo-go.svg', - 'icon-search': '/publishBall/icon-search.svg', - 'icon-map': '/publishBall/icon-map.svg', - 'icon-stadium': '/publishBall/icon-stadium.svg', - 'icon-arrow-small': '/publishBall/icon-arrow-small.svg', - 'icon-map-search': '/publishBall/icon-map-search.svg', - 'icon-heartcircle': '/publishBall/icon-heartcircle.png', - 'icon-copy': '/publishBall/icon-arrow-right.svg', - 'icon-delete': '/publishBall/icon-delete.svg', - 'icon-circle-unselect': '/publishBall/icon-circle-unselect.svg', - 'icon-circle-select-ring': '/publishBall/icon-circle-select-ring.svg', - 'icon-circle-select-arrow': '/publishBall/icon-circle-select-arrow.svg', - 'icon-weather-sun': '/detail/icon-weather-sun.svg', - 'icon-detail-map': '/detail/icon-map.svg', - 'icon-detail-arrow-right': '/detail/icon-arrow-right.svg', - 'icon-detail-notice': '/detail/icon-notice.svg', - 'icon-detail-application-add': '/detail/icon-application-add.svg', - 'icon-detail-comment': '/detail/icon-comment.svg', - 'icon-detail-comment-dark': '/detail/icon-comment-dark.svg', - 'icon-detail-share': '/detail/icon-share-dark.svg', - 'icon-guide-bar-publish': '/common/guide-bar-publish.svg', - 'icon-navigator-back': '/common/navigator-back.svg', + "icon-upload": "/publishBall/icon-upload.svg", + "icon-add": "/publishBall/icon-add.svg", + "icon-location": "/publishBall/icon-location.svg", + "icon-gameplay": "/publishBall/icon-gameplay.svg", + "icon-personal": "/publishBall/icon-personal.svg", + "icon-changda": "/publishBall/icon-changda.svg", + "icon-cost": "/publishBall/icon-cost.svg", + "icon-remove": "/publishBall/icon-remove.svg", + "icon-arrow-left": "/detail/icon-arrow-left.svg", + "icon-logo-go": "/detail/icon-logo-go.svg", + "icon-search": "/publishBall/icon-search.svg", + "icon-map": "/publishBall/icon-map.svg", + "icon-stadium": "/publishBall/icon-stadium.svg", + "icon-arrow-small": "/publishBall/icon-arrow-small.svg", + "icon-map-search": "/publishBall/icon-map-search.svg", + "icon-heartcircle": "/publishBall/icon-heartcircle.png", + "icon-copy": "/publishBall/icon-arrow-right.svg", + "icon-delete": "/publishBall/icon-delete.svg", + "icon-circle-unselect": "/publishBall/icon-circle-unselect.svg", + "icon-circle-select-ring": "/publishBall/icon-circle-select-ring.svg", + "icon-circle-select-arrow": "/publishBall/icon-circle-select-arrow.svg", + "icon-detail-map": "/detail/icon-map.svg", + "icon-detail-arrow-right": "/detail/icon-arrow-right.svg", + "icon-detail-notice": "/detail/icon-notice.svg", + "icon-detail-application-add": "/detail/icon-application-add.svg", + "icon-detail-comment": "/detail/icon-comment.svg", + "icon-detail-comment-dark": "/detail/icon-comment-dark.svg", + "icon-detail-share": "/detail/icon-share-dark.svg", + "icon-guide-bar-publish": "/common/guide-bar-publish.svg", + "icon-navigator-back": "/common/navigator-back.svg", ) !default; // 图片获取函数 @function taro-image($name) { @if not map.has-key($-images, $name) { @warn "Image `#{$name}` not found in $images map"; - @return url($-static-path + 'default.png'); + @return url($-static-path + "default.png"); } @return url($-static-path + map.get($-images, $name)); } - diff --git a/src/services/detailService.ts b/src/services/detailService.ts index 9b3d221..cb89880 100644 --- a/src/services/detailService.ts +++ b/src/services/detailService.ts @@ -140,6 +140,17 @@ class GameDetailService { showLoading: true, }); } + + async getActivityId(req: { business_type: 'game' | 'order' | 'participant', business_id: number, is_private: boolean }): Promise> { + return httpService.post('/user/create_activity_id', req, { + showLoading: false + }) + } } // 导出认证服务实例 diff --git a/src/static/detail/cross.svg b/src/static/detail/cross.svg new file mode 100644 index 0000000..9e8c64b --- /dev/null +++ b/src/static/detail/cross.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/static/detail/download_icon.svg b/src/static/detail/download_icon.svg new file mode 100644 index 0000000..1e0704a --- /dev/null +++ b/src/static/detail/download_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/static/detail/icon-weather-sun.svg b/src/static/detail/icon-weather-sun.svg deleted file mode 100644 index 3851486..0000000 --- a/src/static/detail/icon-weather-sun.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/static/detail/link.svg b/src/static/detail/link.svg new file mode 100644 index 0000000..2e774f6 --- /dev/null +++ b/src/static/detail/link.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/static/detail/wechat_icon.svg b/src/static/detail/wechat_icon.svg new file mode 100644 index 0000000..e92b1cd --- /dev/null +++ b/src/static/detail/wechat_icon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/static/detail/wechat_timeline.svg b/src/static/detail/wechat_timeline.svg new file mode 100644 index 0000000..7d5c79b --- /dev/null +++ b/src/static/detail/wechat_timeline.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/utils/genPoster.ts b/src/utils/genPoster.ts new file mode 100644 index 0000000..b25fb0e --- /dev/null +++ b/src/utils/genPoster.ts @@ -0,0 +1,312 @@ +import Taro from "@tarojs/taro"; + +const qrCodeUrl = + "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/5e013195-fc79-4082-bf06-9aa79aea65ae.png"; + +const ringUrl = + "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/b635164f-ecec-434a-a00b-69614a918f2f.png"; + +const dateIcon = + "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/1b49476e-0eda-42ff-b08c-002ce510df82.jpg"; + +const mapIcon = + "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/06b994fa-9227-4708-8555-8a07af8d0c3b.jpg"; + +// const logo = "http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/fb732da6-11b9-4022-a524-a377b17635eb.jpg" + +const logoText = + "https://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/9d8cbc9d-9601-4e2d-ab23-76420a4537d6.png"; + +/** 获取图片宽高 */ +function getImageWh(src: string): Promise<{ width: number; height: number }> { + return new Promise((resolve) => { + Taro.getImageInfo({ + src, + success: ({ width, height }) => resolve({ width, height }), + }); + }); +} + +/** 加载图片 */ +function loadImage(canvas: any, src: string): Promise { + return new Promise((resolve) => { + const img = canvas.createImage(); + img.onload = () => resolve(img); + img.src = src; + }); +} + +/** 圆角矩形渐变 */ +function roundRectGradient( + ctx: any, + x: number, + y: number, + w: number, + h: number, + r: number, + color1: string, + color2: string +) { + const gradient = ctx.createLinearGradient(x, y, x, y + h); + gradient.addColorStop(0, color1); + gradient.addColorStop(1, color2); + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.lineTo(x + w - r, y); + ctx.arcTo(x + w, y, x + w, y + r, r); + ctx.lineTo(x + w, y + h - r); + ctx.arcTo(x + w, y + h, x + w - r, y + h, r); + ctx.lineTo(x + r, y + h); + ctx.arcTo(x, y + h, x, y + h - r, r); + ctx.lineTo(x, y + r); + ctx.arcTo(x, y, x + r, y, r); + ctx.closePath(); + ctx.fillStyle = gradient; + ctx.fill(); +} + +/** 绘制 cover 图片(支持圆角) */ +async function drawCoverImage( + ctx: any, + canvas: any, + src: string, + img: any, + x: number, + y: number, + w: number, + h: number, + r = 0 +) { + const { width, height } = await getImageWh(src); + const scale = Math.max(w / width, h / height); + const newW = width * scale; + const newH = height * scale; + const offsetX = x + (w - newW) / 2; + const offsetY = y + (h - newH) / 2; + + ctx.save(); + if (r > 0) { + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.lineTo(x + w - r, y); + ctx.arcTo(x + w, y, x + w, y + r, r); + ctx.lineTo(x + w, y + h - r); + ctx.arcTo(x + w, y + h, x + w - r, y + h, r); + ctx.lineTo(x + r, y + h); + ctx.arcTo(x, y + h, x, y + h - r, r); + ctx.lineTo(x, y + r); + ctx.arcTo(x, y, x + r, y, r); + ctx.closePath(); + ctx.clip(); + } + ctx.drawImage(img, offsetX, offsetY, newW, newH); + ctx.restore(); +} + +/** 圆角矩形 */ +function roundRect( + ctx: any, + x: number, + y: number, + w: number, + h: number, + r: number, + fillStyle: string +) { + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.lineTo(x + w - r, y); + ctx.arcTo(x + w, y, x + w, y + r, r); + ctx.lineTo(x + w, y + h - r); + ctx.arcTo(x + w, y + h, x + w - r, y + h, r); + ctx.lineTo(x + r, y + h); + ctx.arcTo(x, y + h, x, y + h - r, r); + ctx.lineTo(x, y + r); + ctx.arcTo(x, y, x + r, y, r); + ctx.closePath(); + ctx.fillStyle = fillStyle; + ctx.fill(); +} + +/** 绘制标签 */ +function drawTag(ctx: any, text: string, x: number, y: number) { + ctx.font = "22px sans-serif"; + const padding = 12; + const textWidth = ctx.measureText(text).width; + roundRect(ctx, x, y, textWidth + padding * 2, 40, 20, "#fff"); + ctx.fillStyle = "#333"; + ctx.fillText(text, x + padding, y + 28); + return x + textWidth + padding * 2; +} + +/** 文本换行 */ +function drawTextWrap( + ctx: any, + text: string, + x: number, + y: number, + maxWidth: number, + lineHeight: number +) { + let line = ""; + const lines = []; + for (let char of text) { + const testLine = line + char; + if (ctx.measureText(testLine).width > maxWidth) { + lines.push(line); + line = char; + } else { + line = testLine; + } + } + if (line) lines.push(line); + lines.forEach((l, i) => { + ctx.fillText(l, x, y + i * lineHeight); + }); + const lastLineText = lines.at(-1); + return { + left: x + ctx.measureText(lastLineText).width, + top: y + (lines.length - 1) * lineHeight, + }; +} + +/** 核心纯函数:生成海报图片 */ +export async function generatePosterImage(data: any): Promise { + console.log("start !!!!"); + const dpr = Taro.getSystemInfoSync().pixelRatio; + const width = 600; + const height = 1000; + + const canvas = Taro.createOffscreenCanvas({ type: "2d", width: width * dpr, height: height * dpr }); + const ctx = canvas.getContext("2d"); + ctx.scale(dpr, dpr); + + // 背景渐变 + roundRectGradient(ctx, 0, 0, width, height, 24, "#BFFFEF", "#F2FFFC"); + + // 顶部图片 + const mainImg = await loadImage(canvas, data.mainCoursal); + console.log(222); + await drawCoverImage( + ctx, + canvas, + data.mainCoursal, + mainImg, + 10, + 10, + width - 20, + width - 20, + 20 + ); + + // 标签 + let left = drawTag(ctx, data.playType, 18, 18); + drawTag(ctx, data.ntrp, left + 4, 18); + + let top = width - 10 + 16; + left = 16; + + // 用户头像 + const avatarImg = await loadImage(canvas, data.avatarUrl); + ctx.save(); + ctx.beginPath(); + ctx.arc(left + 30, top + 30, 30, 0, Math.PI * 2); + ctx.clip(); + ctx.drawImage(avatarImg, left, top, 60, 60); + ctx.restore(); + + left += 66; + top += 40; + + // 用户名 + 邀请 + ctx.fillStyle = "#333"; + ctx.font = "bold 28px sans-serif"; + const nickNameText = `${data.nickname} 邀你加入`; + ctx.fillText(nickNameText, left, top); + let textW = ctx.measureText(nickNameText).width; + left += textW; + ctx.fillStyle = "#00B578"; + ctx.fillText("球局", left, top); + + const ringImg = await loadImage(canvas, ringUrl); + ctx.drawImage(ringImg, left - 10, top - 30, 80, 36); + + left = 16; + top += 60; + + // 活动标题 + ctx.fillStyle = "#333"; + ctx.font = "bold 34px sans-serif"; + let r = drawTextWrap(ctx, data.title, left, top, width - 32, 40); + + top = r.top + 40; + left = 16; + + const dateImg = await loadImage(canvas, dateIcon); + await drawCoverImage( + ctx, + canvas, + dateIcon, + dateImg, + left, + top, + 40, + 40, + 8 + ); + + left += 40 + 8; + top += 30; + + ctx.font = "26px sans-serif"; + ctx.fillStyle = "#00B578"; + ctx.fillText(data.date, left, top); + textW = ctx.measureText(data.date).width; + left += 8 + textW; + ctx.fillStyle = "#333"; + ctx.fillText(data.time, left, top); + + left = 16; + top += 24; + + const mapImg = await loadImage(canvas, mapIcon); + await drawCoverImage(ctx, canvas, mapIcon, mapImg, left, top, 40, 40, 8); + + left += 40 + 8; + top += 30; + + ctx.fillStyle = "#666"; + ctx.font = "26px sans-serif"; + r = drawTextWrap(ctx, data.locationName, left, top, width - 32 - left, 34); + + left = 16; + top = r.top + 60; + + const logoWh = await getImageWh(logoText); + const logoTextImg = await loadImage(canvas, logoText); + ctx.drawImage( + logoTextImg, + left, + top, + 400, + 400 / (logoWh.width / logoWh.height) + ); + + const qrImg = await loadImage(canvas, qrCodeUrl); + ctx.drawImage(qrImg, width - 12 - 150, top - 50, 160, 160); + + left = 16; + top += 400 / (logoWh.width / logoWh.height) + 30; + + ctx.fillStyle = "#333"; + ctx.font = "20px sans-serif"; + ctx.fillText("长按识别二维码,快来加入,有你就有场!", left, top); + + // 导出图片 + const { tempFilePath } = await Taro.canvasToTempFilePath({ + canvas, + fileType: 'png', + quality: 1, + }); + return tempFilePath; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 08ea333..198e242 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -7,3 +7,5 @@ export * from "./tokenManager"; export * from "./order.pay"; export * from './orderActions'; export * from './routeUtil'; +export * from './share' +export * from './genPoster' diff --git a/types/taro-canvas.d.ts b/types/taro-canvas.d.ts new file mode 100644 index 0000000..67055a0 --- /dev/null +++ b/types/taro-canvas.d.ts @@ -0,0 +1,71 @@ +declare module "@tarojs/taro" { + interface RenderingContext { + // 变换 + scale(x: number, y: number): void; + rotate(angle: number): void; + translate(x: number, y: number): void; + save(): void; + restore(): void; + + // 路径 + beginPath(): void; + closePath(): void; + moveTo(x: number, y: number): void; + lineTo(x: number, y: number): void; + arc( + x: number, + y: number, + radius: number, + startAngle: number, + endAngle: number, + anticlockwise?: boolean + ): void; + arcTo( + x1: number, + y1: number, + x2: number, + y2: number, + radius: number + ): void; + clip(): void; + + // 绘制 + drawImage( + image: any, + dx: number, + dy: number, + dWidth?: number, + dHeight?: number + ): void; + fill(): void; + stroke(): void; + + // 样式 + fillStyle: string | CanvasGradient | CanvasPattern; + strokeStyle: string | CanvasGradient | CanvasPattern; + font: string; + textAlign: CanvasTextAlign; + textBaseline: CanvasTextBaseline; + + // 文本 + fillText(text: string, x: number, y: number, maxWidth?: number): void; + strokeText(text: string, x: number, y: number, maxWidth?: number): void; + measureText(text: string): TextMetrics; + + // 渐变 + createLinearGradient( + x0: number, + y0: number, + x1: number, + y1: number + ): CanvasGradient; + createRadialGradient( + x0: number, + y0: number, + r0: number, + x1: number, + y1: number, + r1: number + ): CanvasGradient; + } +}