feat: 生成海报

This commit is contained in:
2025-10-03 09:19:05 +08:00
parent 40a043d2a0
commit 5fec10b342
18 changed files with 1032 additions and 200 deletions

View File

@@ -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);

View File

@@ -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 @@
}
}
}
}
}

View File

@@ -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'),

View File

@@ -0,0 +1,9 @@
export const DayOfWeekMap = new Map([
[0, "周日"],
[1, "周一"],
[2, "周二"],
[3, "周三"],
[4, "周四"],
[5, "周五"],
[6, "周六"],
]);

View File

@@ -2,4 +2,5 @@ export default definePageConfig({
navigationBarTitleText: '球局详情',
navigationStyle: 'custom',
enableShareAppMessage: true,
enableShareTimeline: true,
})

View File

@@ -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 (
<>
{/* <CommonPopup
title="分享至"
visible={visible}
onClose={() => {
setVisible(false);
}}
hideFooter
style={{ minHeight: "100px" }}
>
<View className={styles.shareContainer}>
<View catchMove className={styles.title}>
分享至
</View>
<View className={styles.shareItems}>
<Button
className={classnames(styles.button, styles.share)}
openType="share"
>
微信好友
</Button>
<Button
className={classnames(styles.button, styles.save)}
onClick={handleSaveToLocal}
>
生成分享图
</Button>
</View>
</View>
</CommonPopup> */}
<CommonPopup
title="分享至"
visible={visible}
onClose={() => {
setVisible(false);
}}
showHeader={false}
position="center"
hideFooter
enableDragToClose={false}
style={{ minHeight: "100px" }}
>
<View className={styles.posterWrap}>
<Poster />
</View>
</CommonPopup>
</>
);
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 && (
<CommonPopup
title="分享至"
visible={visible}
onClose={onClose}
showHeader={false}
position="center"
hideFooter
enableDragToClose={false}
style={{ minHeight: "100px" }}
zIndex={2001}
>
<View className={styles.posterContainer}>
<View className={styles.posterWrap}>
{posterData && <Poster ref={posterRef} data={posterData} />}
</View>
<View className={styles.sharePoster}>
<Button className={styles.shareItem} plain={true}>
<View className={styles.icon}>
<Image className={styles.download} src={DownloadIcon} />
</View>
<Text></Text>
</Button>
<Button
className={styles.shareItem}
plain={true}
onClick={handleShare}
>
<View className={classnames(styles.icon, styles.wechatIcon)}>
<Image className={styles.wechat} src={WechatLogo} />
</View>
<Text></Text>
</Button>
<Button className={styles.shareItem} plain={true} openType="share">
<View className={styles.icon}>
<Image className={styles.timeline} src={WechatTimeline} />
</View>
<Text></Text>
</Button>
</View>
</View>
</CommonPopup>
)
);
});
// 分享弹窗
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 (
<>
<CommonPopup
title="分享至"
visible={visible}
onClose={onClose}
showHeader={false}
hideFooter
enableDragToClose={false}
style={{ minHeight: "100px" }}
zIndex={1000}
>
<View className={styles.shareContainer}>
<View catchMove className={styles.title}>
<Text></Text>
<View className={styles.closeIconWrap} onClick={onClose}>
<Image className={styles.closeIcon} src={CrossIcon} />
</View>
</View>
<View className={styles.shareItems}>
<Button className={styles.button} openType="share">
<View className={classnames(styles.icon, styles.wechatIcon)}>
<Image className={styles.wechat} src={WechatLogo} />
</View>
<Text></Text>
</Button>
<Button className={styles.button} onClick={handlePost}>
<View className={styles.icon}>
<Image className={styles.download} src={DownloadIcon} />
</View>
<Text></Text>
</Button>
<Button className={styles.button}>
<View className={styles.icon}>
<Image className={styles.linkIcon} src={LinkIcon} />
</View>
<Text></Text>
</Button>
</View>
</View>
</CommonPopup>
{/* <PosterPopup ref={posterRef} id={detail.id} /> */}
</>
);
});
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}
/>
</View>
</View>

View File

@@ -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;
}
}
}
}

View File

@@ -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));
}

View File

@@ -140,6 +140,17 @@ class GameDetailService {
showLoading: true,
});
}
async getActivityId(req: { business_type: 'game' | 'order' | 'participant', business_id: number, is_private: boolean }): Promise<ApiResponse<{
activity_id: string,
expiration_time: number,
business_type: 'game' | 'order' | 'participant',
business_id: number
}>> {
return httpService.post('/user/create_activity_id', req, {
showLoading: false
})
}
}
// 导出认证服务实例

View File

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 7L17 17" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 17L17 7" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 307 B

View File

@@ -0,0 +1,5 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.5 14.0048V24.5H24.5V14" stroke="black" stroke-width="2.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.25 13.417L14 18.667L8.75 13.417" stroke="black" stroke-width="2.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.9954 3.5V18.6667" stroke="black" stroke-width="2.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 477 B

View File

@@ -1,18 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3061_9153)">
<path d="M3.81265 3.81287L4.74074 4.74096" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.25 10H2.5625" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.81265 16.1873L4.74074 15.2592" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.1873 16.1873L15.2592 15.2592" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.7498 10H17.4373" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.1873 3.81287L15.2592 4.74096" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 1.25V2.5625" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 15C12.7614 15 15 12.7614 15 10C15 7.23858 12.7614 5 10 5C7.23858 5 5 7.23858 5 10C5 12.7614 7.23858 15 10 15Z" fill="white" stroke="white" stroke-width="1.66667" stroke-linejoin="round"/>
<path d="M10 18.7501V17.4376" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_3061_9153">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,6 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.3067 9.55113L9.975 4.21948C8.42258 2.66703 5.87137 2.70122 4.2767 4.29586C2.68206 5.89048 2.64786 8.44175 4.20032 9.99417L8.82922 14.6231" stroke="black" stroke-width="2.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.1933 13.4189L23.8222 18.0479C25.3747 19.6004 25.3404 22.1516 23.7458 23.7462C22.1512 25.3409 19.6 25.3751 18.0475 23.8226L12.7158 18.4909" stroke="black" stroke-width="2.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.2305 15.2491C16.8251 13.6545 16.8593 11.1033 15.3069 9.55078" stroke="black" stroke-width="2.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.716 12.7158C11.1214 14.3105 11.0872 16.8617 12.6396 18.4141" stroke="black" stroke-width="2.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 904 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="30" viewBox="0 0 36 30" fill="none">
<path d="M17.3505 9.31185C16.5014 9.31185 15.8113 8.6188 15.8113 7.76072C15.8113 6.90265 16.5014 6.20959 17.3505 6.20959C18.1995 6.20959 18.8896 6.90265 18.8896 7.76072C18.8896 8.6188 18.1995 9.31185 17.3505 9.31185ZM8.67673 9.31185C7.82766 9.31185 7.1376 8.6188 7.1376 7.76072C7.1376 6.90265 7.82766 6.20959 8.67673 6.20959C9.5258 6.20959 10.2159 6.90265 10.2159 7.76072C10.2159 8.6188 9.5258 9.31185 8.67673 9.31185ZM13.0121 0.329102C5.82649 0.329102 0 5.22251 0 11.259C0 14.5533 1.75215 17.5175 4.49738 19.5217C4.71639 19.6807 4.86041 19.9387 4.86041 20.2358C4.86041 20.3318 4.83941 20.4218 4.8154 20.5148C4.59639 21.3399 4.24536 22.657 4.23035 22.72C4.20335 22.825 4.16135 22.93 4.16135 23.041C4.16135 23.281 4.35636 23.479 4.59639 23.479C4.68939 23.479 4.7674 23.443 4.84541 23.398L7.69564 21.7389C7.90866 21.6159 8.13668 21.5379 8.3857 21.5379C8.51771 21.5379 8.64672 21.5589 8.76973 21.5949C10.0988 21.9789 11.533 22.1949 13.0181 22.1949C13.2581 22.1949 13.4951 22.1889 13.7322 22.1769C13.4501 21.3249 13.2941 20.4278 13.2941 19.4977C13.2941 13.9922 18.6076 9.52787 25.1631 9.52787C25.4001 9.52787 25.4601 9.53687 25.6942 9.54587C24.7101 4.32244 19.5046 0.329102 13.0121 0.329102Z" fill="white"/>
<path d="M28.7725 17.9434C28.0254 17.9434 27.4194 17.3314 27.4194 16.5783C27.4194 15.8253 28.0254 15.2132 28.7725 15.2132C29.5196 15.2132 30.1256 15.8253 30.1256 16.5783C30.1256 17.3314 29.5196 17.9434 28.7725 17.9434ZM21.5419 17.9434C20.7948 17.9434 20.1888 17.3314 20.1888 16.5783C20.1888 15.8253 20.7948 15.2132 21.5419 15.2132C22.2889 15.2132 22.895 15.8253 22.895 16.5783C22.895 17.3314 22.2889 17.9434 21.5419 17.9434ZM32.2528 26.3772C34.539 24.706 36.0001 22.2368 36.0001 19.4916C36.0001 14.4602 31.1457 10.3828 25.1572 10.3828C19.1687 10.3828 14.3113 14.4602 14.3113 19.4916C14.3113 24.523 19.1657 28.6003 25.1572 28.6003C26.3933 28.6003 27.5904 28.4233 28.6975 28.1023C28.7995 28.0723 28.9045 28.0543 29.0155 28.0543C29.2225 28.0543 29.4115 28.1173 29.5916 28.2223L31.9648 29.6024C32.0308 29.6414 32.0968 29.6714 32.1748 29.6714C32.3758 29.6714 32.5378 29.5094 32.5378 29.3084C32.5378 29.2184 32.5018 29.1284 32.4808 29.0414C32.4658 28.9904 32.1748 27.8893 31.9918 27.2052C31.9708 27.1272 31.9528 27.0522 31.9528 26.9742C31.9528 26.7282 32.0728 26.5122 32.2558 26.3802" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,17 @@
<svg width="33" height="32" viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5089_10854)">
<path d="M12.6821 0.59089C14.0145 0.237434 15.3823 0.0595703 16.7511 0.0595703C18.891 0.0595703 20.9721 0.480501 22.9385 1.31274L22.6772 12.8663L12.6821 0.59089Z" fill="#F57659"/>
<path d="M2.9751 7.98069C3.6713 6.78428 4.51319 5.68963 5.47929 4.72785C6.99157 3.21547 8.75994 2.04164 10.7382 1.23828L18.7262 9.59159L2.9751 7.98069Z" fill="#F0B360"/>
<path d="M1.34058 20.071C0.988181 18.744 0.809326 17.3762 0.809326 15.9999C0.809326 13.8577 1.23128 11.7767 2.06243 9.81445L13.6171 10.0736L1.34058 20.071Z" fill="#EFDE58"/>
<path d="M8.73089 29.7752C7.54305 29.0832 6.45061 28.2414 5.48023 27.271C3.96682 25.7608 2.79293 23.9925 1.99072 22.0121L10.3439 14.0273L8.73089 29.7752Z" fill="#BBEF5C"/>
<path d="M16.751 31.9403C14.6109 31.9403 12.53 31.5193 10.5635 30.6893L10.8248 19.1357L20.8199 31.4112C19.4854 31.7625 18.1187 31.9403 16.751 31.9403Z" fill="#61F15E"/>
<path d="M14.7759 22.4092L30.527 24.0201C29.8362 25.2057 28.9965 26.2982 28.0218 27.2707C26.5115 28.7852 24.7411 29.9591 22.7629 30.7624L14.7759 22.4092Z" fill="#5BE4EE"/>
<path d="M19.885 21.928L32.1604 11.9307C32.5128 13.2588 32.6906 14.6276 32.6906 15.9996C32.6906 18.1482 32.2687 20.2272 31.4386 22.1872L19.885 21.928Z" fill="#5C9BF6"/>
<path d="M24.7691 2.22656C25.9559 2.91528 27.0505 3.75602 28.0219 4.72858C29.5364 6.23879 30.7103 8.00923 31.5114 9.98748L23.1582 17.9744L24.7691 2.22656Z" fill="#6D58F1"/>
</g>
<defs>
<clipPath id="clip0_5089_10854">
<rect width="32" height="32" fill="white" transform="translate(0.75)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

312
src/utils/genPoster.ts Normal file
View File

@@ -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<any> {
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<string> {
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;
}

View File

@@ -7,3 +7,5 @@ export * from "./tokenManager";
export * from "./order.pay";
export * from './orderActions';
export * from './routeUtil';
export * from './share'
export * from './genPoster'

71
types/taro-canvas.d.ts vendored Normal file
View File

@@ -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;
}
}