index.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import { Canvas, Image, View } from "@tarojs/components";
  2. import Taro, { FC } from "@tarojs/taro";
  3. import { useEffect, useRef, useState } from "react";
  4. import { AtFloatLayout } from "taro-ui";
  5. import { AtFloatLayoutProps } from "taro-ui/types/float-layout";
  6. import CloseIcon from "../../../images/icon_back@2x-min.png";
  7. import "./index.scss";
  8. export interface CertLayoutProps extends AtFloatLayoutProps {
  9. name: string;
  10. date: string;
  11. }
  12. const system = Taro.getSystemInfoSync();
  13. export const CertLayout: FC<CertLayoutProps> = ({ name, date, ...props }) => {
  14. const loaded = useRef(false);
  15. const [imgPath, setImgPath] = useState("");
  16. useEffect(() => {
  17. if (props.isOpened && !loaded.current) init();
  18. }, [props.isOpened]);
  19. const init = async () => {
  20. try {
  21. Taro.showLoading({
  22. title: "绘制中",
  23. });
  24. await new Promise((resolve, reject) => {
  25. Taro.createSelectorQuery()
  26. .select("#certCanvas")
  27. .fields({ node: true, size: true }, async (res) => {
  28. const canvas = res.node;
  29. const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
  30. canvas.width = res.width * system.pixelRatio;
  31. canvas.height = res.height * system.pixelRatio;
  32. try {
  33. const bgInfo = await getTempImgPath(
  34. "https://wuxicharitymuseum.cn/wx-csbwg-public/images/cert-min.png"
  35. );
  36. const ratio = bgInfo.width / canvas.width;
  37. const bgSource = await loadImg(canvas, bgInfo.path);
  38. ctx.drawImage(bgSource, 0, 0, canvas.width, canvas.height);
  39. ctxText(
  40. ctx,
  41. "特此授予锡善云城·热心市民".split("").join(" "),
  42. `${7 * system.pixelRatio}px SourceHanSansSCRegular`,
  43. "center",
  44. "#424A4A",
  45. canvas.width / 2,
  46. 800 / ratio
  47. );
  48. ctxText(
  49. ctx,
  50. name.split("").join(" "),
  51. `${15 * system.pixelRatio}px SourceHanSansCN-Bold`,
  52. "center",
  53. "#CFC49E",
  54. canvas.width / 2,
  55. 1020 / ratio
  56. );
  57. ctxTextWrap(
  58. ctx,
  59. "特此证明,感谢您的热情参与与慷慨支持。您的善举为社区带来了无限的温暖与希望。因您的无私奉献,特授予您热心市民称号,以表彰您对慈善事业的贡献。您的善行将永远激励着我们,为构建更美好的社会贡献一份力量。",
  60. `${7 * system.pixelRatio}px SourceHanSansSCRegular`,
  61. "center",
  62. "#424A4A",
  63. canvas.width / 2,
  64. 1200 / ratio,
  65. 460 * system.pixelRatio
  66. );
  67. ctxText(
  68. ctx,
  69. date,
  70. `${6 * system.pixelRatio}px SourceHanSerifCN-Bold`,
  71. "left",
  72. "#424A4A",
  73. 740 / ratio,
  74. 1660 / ratio
  75. );
  76. ctxText(
  77. ctx,
  78. "锡善云城",
  79. `${6 * system.pixelRatio}px SourceHanSerifCN-Bold`,
  80. "left",
  81. "#424A4A",
  82. 1880 / ratio,
  83. 1660 / ratio
  84. );
  85. await new Promise((resolve, reject) => {
  86. Taro.canvasToTempFilePath({
  87. canvas,
  88. fileType: "jpg",
  89. success(res2) {
  90. setImgPath(res2.tempFilePath);
  91. loaded.current = true;
  92. resolve(true);
  93. },
  94. fail(err) {
  95. Taro.showToast({
  96. title: err.errMsg,
  97. icon: "none",
  98. duration: 4000,
  99. });
  100. reject(err);
  101. },
  102. });
  103. });
  104. resolve(true);
  105. } catch (err) {
  106. reject(false);
  107. }
  108. })
  109. .exec();
  110. });
  111. } finally {
  112. Taro.hideLoading();
  113. }
  114. };
  115. const loadImg = (canvas, src) => {
  116. return new Promise((res, rej) => {
  117. const source = canvas.createImage();
  118. source.src = src;
  119. source.onload = () => {
  120. res(source);
  121. };
  122. source.onerror = rej;
  123. }) as Promise<CanvasImageSource>;
  124. };
  125. const ctxTextWrap = (ctx, text, font, align, color, x, y, maxWidth) => {
  126. let line = "";
  127. const lines: string[] = [];
  128. const words = text.split("");
  129. for (let i = 0; i < words.length; i++) {
  130. const word = words[i];
  131. const testLine = line + word;
  132. const metrics = ctx.measureText(testLine);
  133. if (metrics.width > maxWidth && i > 0) {
  134. lines.push(line);
  135. line = word;
  136. } else {
  137. line = testLine;
  138. }
  139. }
  140. lines.push(line);
  141. lines.forEach((t, idx) =>
  142. ctxText(ctx, t, font, align, color, x, y + idx * 28)
  143. );
  144. };
  145. const ctxText = (ctx, text, font, align, color, x, y) => {
  146. ctx.beginPath();
  147. ctx.font = font;
  148. ctx.textAlign = align;
  149. ctx.fillStyle = color;
  150. ctx.fillText(text, x, y);
  151. ctx.save();
  152. };
  153. const getTempImgPath = async (src: string) => {
  154. return new Promise((resolve, reject) => {
  155. Taro.getImageInfo({
  156. src,
  157. success(res) {
  158. resolve(res);
  159. },
  160. fail(err) {
  161. reject(err);
  162. },
  163. });
  164. }) as Promise<Taro.getImageInfo.SuccessCallbackResult>;
  165. };
  166. return (
  167. <AtFloatLayout className="cert" {...props}>
  168. <View className="cert-wrap">
  169. {imgPath ? (
  170. <>
  171. <Image
  172. className="cert__img"
  173. src={imgPath}
  174. showMenuByLongpress
  175. onClick={() => {
  176. Taro.previewImage({
  177. current: imgPath,
  178. urls: [imgPath],
  179. });
  180. }}
  181. />
  182. <View className="cert__tips">长按图片,保存证书</View>
  183. </>
  184. ) : (
  185. <Canvas
  186. id="certCanvas"
  187. type="2d"
  188. className="cert__img"
  189. style="position:absolute; top: -200%; left: -200%"
  190. />
  191. )}
  192. </View>
  193. <Image className="cert__close" src={CloseIcon} onClick={props.onClose} />
  194. </AtFloatLayout>
  195. );
  196. };