From 9d5c7ce08ed4cc25ac738b564be1b4b47d7550f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=88=90?= Date: Wed, 3 Dec 2025 23:18:48 +0800 Subject: [PATCH] 12 --- src/components/Radar/indexV2.tsx | 247 ++++++++++++++++++++---- src/components/index.ts | 2 + src/other_pages/ntrp-evaluate/index.tsx | 119 +++++++----- src/static/ntrp/ntrp_share_logo.png | Bin 0 -> 2991 bytes src/static/ntrp/ntrp_share_logo.svg | 36 ++++ 5 files changed, 321 insertions(+), 83 deletions(-) create mode 100644 src/static/ntrp/ntrp_share_logo.png create mode 100644 src/static/ntrp/ntrp_share_logo.svg diff --git a/src/components/Radar/indexV2.tsx b/src/components/Radar/indexV2.tsx index 4881b67..6ad5d4f 100644 --- a/src/components/Radar/indexV2.tsx +++ b/src/components/Radar/indexV2.tsx @@ -1,12 +1,15 @@ import Taro from "@tarojs/taro"; import { View, Canvas } from "@tarojs/components"; import { useEffect, forwardRef, useImperativeHandle } from "react"; -import shareLogoSvg from "@/static/ntrp/ntrp_share_logo.svg"; +import shareLogoSvg from "@/static/ntrp/ntrp_share_logo.png"; +import docCopySvg from "@/static/ntrp/ntrp_doc_copy.svg"; interface RadarChartV2Props { data: [string, number][]; title?: string; ntrpLevel?: string; + levelDescription?: string; + avatarUrl?: string; qrCodeUrl?: string; bottomText?: string; } @@ -16,6 +19,8 @@ export interface RadarChartV2Ref { generateFullImage: (options: { title?: string; ntrpLevel?: string; + levelDescription?: string; + avatarUrl?: string; qrCodeUrl?: string; bottomText?: string; width?: number; @@ -24,7 +29,7 @@ export interface RadarChartV2Ref { } const RadarChartV2 = forwardRef((props, ref) => { - const { data, title, ntrpLevel, qrCodeUrl, bottomText } = props; + const { data, title, ntrpLevel, levelDescription, qrCodeUrl, bottomText } = props; const maxValue = 100; const levels = 5; @@ -180,6 +185,21 @@ const RadarChartV2 = forwardRef((props, ref) }); } + // 绘制圆角矩形 + function roundRect(ctx: any, x: number, y: number, width: number, height: number, radius: number) { + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + } + // 格式化 NTRP 显示 function formatNtrpDisplay(level: string): string { if (!level) return ""; @@ -209,6 +229,7 @@ const RadarChartV2 = forwardRef((props, ref) generateFullImage: async (options: { title?: string; ntrpLevel?: string; + levelDescription?: string; qrCodeUrl?: string; bottomText?: string; width?: number; @@ -269,22 +290,130 @@ const RadarChartV2 = forwardRef((props, ref) let currentY = topPadding; - // 绘制标题 - 根据设计稿调整字体大小和间距 - if (options.title) { - ctx.fillStyle = "#000000"; - ctx.font = "400 28px sans-serif"; // 设计稿字体大小 - ctx.textAlign = "left"; - ctx.textBaseline = "top"; - ctx.fillText(options.title, sidePadding, currentY); - currentY += 44; // 标题到 NTRP 等级的间距 + // 绘制用户头像和装饰图片 - 参考 Result 组件布局,居中显示 + if (options.avatarUrl) { + try { + const avatarSize = 50; // Result 组件使用 0.5 倍数,所以是 50px + const avatarImg = await loadImage(canvas, options.avatarUrl); + + // 头像区域总宽度(头像 + 装饰图片重叠部分) + const avatarWrapWidth = 50 + 44 - 10; // 头像宽度 + 装饰宽度 - 重叠部分 + const avatarX = (width - avatarWrapWidth) / 2 + 10; // 居中,考虑装饰图片的位置 + const avatarY = currentY; + + // 绘制头像圆形背景 + ctx.save(); + ctx.beginPath(); + ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2); + ctx.fillStyle = "#FFFFFF"; + ctx.fill(); + ctx.strokeStyle = "#EFEFEF"; + ctx.lineWidth = 1; + ctx.stroke(); + + // 绘制头像(圆形裁剪) + ctx.beginPath(); + ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2 - 1, 0, Math.PI * 2); + ctx.clip(); + const innerAvatarSize = 45; // 头像内部尺寸 90px * 0.5 + ctx.drawImage(avatarImg, avatarX + 2.5, avatarY + 2.5, innerAvatarSize, innerAvatarSize); + ctx.restore(); + + // 绘制装饰图片(DocCopy)- 在头像右侧 + const addonSize = 44; // 88px * 0.5 + const addonX = avatarX + avatarSize - 10; // margin-left: -10px (20px * 0.5) + const addonY = avatarY; + const addonRotation = 8 * (Math.PI / 180); // 旋转 8 度 + + try { + const docCopyImg = await loadImage(canvas, docCopySvg); + ctx.save(); + + // 移动到旋转中心 + const centerX = addonX + addonSize / 2; + const centerY = addonY + addonSize / 2; + ctx.translate(centerX, centerY); + ctx.rotate(addonRotation); + + // 绘制装饰图片背景(圆角矩形,带渐变) + const borderRadius = 10; // 20px * 0.5 + ctx.fillStyle = "#FFFFFF"; + ctx.beginPath(); + roundRect(ctx, -addonSize / 2, -addonSize / 2, addonSize, addonSize, borderRadius); + ctx.fill(); + + // 添加渐变背景色 + ctx.globalAlpha = 0.2; + ctx.fillStyle = "rgba(89, 255, 214, 1)"; // rgba(89, 255, 214, 0.2) + ctx.fill(); + ctx.globalAlpha = 1.0; + + ctx.strokeStyle = "#FFFFFF"; + ctx.lineWidth = 2; + ctx.stroke(); + + // 绘制装饰图片 + const docSize = 24; // 48px * 0.5 + const docRotation = -7 * (Math.PI / 180); // 内部旋转 -7 度 + ctx.rotate(docRotation); + ctx.drawImage(docCopyImg, -docSize / 2, -docSize / 2, docSize, docSize); + ctx.restore(); + } catch (error) { + console.error("Failed to load docCopy image:", error); + } + + currentY += avatarSize + 20; // 头像到底部文字区域的间距 + } catch (error) { + console.error("Failed to load avatar image:", error); + } } - // 绘制 NTRP 等级 - 根据设计稿调整字体大小和颜色 + // 绘制文字区域 - 完全参考 Result 组件样式,居中对齐 + const textCenterX = width / 2; + const textGap = 6; // gap: 6px + + // 绘制标题 - 14px, font-weight: 300 + if (options.title) { + ctx.fillStyle = "#000000"; + ctx.font = "300 14px PingFang SC, sans-serif"; + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + ctx.fillText(options.title, textCenterX, currentY); + currentY += 14 + textGap; // 行高 + gap + } + + // 绘制 NTRP 等级 - 36px, font-weight: 900, "NTRP" 黑色,等级数字 #00e5ad, gap: 8px if (options.ntrpLevel) { + ctx.font = "900 36px Noto Sans SC, sans-serif"; + ctx.textBaseline = "top"; + + const ntrpText = "NTRP"; + const levelText = formatNtrpDisplay(options.ntrpLevel); + const ntrpWidth = ctx.measureText(ntrpText).width; + const levelWidth = ctx.measureText(levelText).width; + const totalWidth = ntrpWidth + 8 + levelWidth; // gap: 8px + const startX = textCenterX - totalWidth / 2; + + // 绘制 "NTRP"(黑色) + ctx.fillStyle = "#000000"; + ctx.textAlign = "left"; + ctx.fillText(ntrpText, startX, currentY); + + // 绘制等级数字(#00e5ad) ctx.fillStyle = "#00E5AD"; - ctx.font = "bold 48px sans-serif"; // 设计稿字体大小 - ctx.fillText(`NTRP ${formatNtrpDisplay(options.ntrpLevel)}`, sidePadding, currentY); - currentY += 72; // NTRP 等级到雷达图的间距 + ctx.fillText(levelText, startX + ntrpWidth + 8, currentY); + + currentY += 44 + textGap; // line-height: 44px + gap + } + + // 绘制描述文本 - 16px, font-weight: 600 + if (options.levelDescription) { + ctx.fillStyle = "#000000"; + ctx.font = "600 16px PingFang SC, sans-serif"; + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + ctx.fillText(options.levelDescription, textCenterX, currentY); + currentY += 16 + 40; // 行高 + 到雷达图的间距 } // 绘制雷达图 - 根据设计稿调整尺寸和位置 @@ -303,14 +432,78 @@ const RadarChartV2 = forwardRef((props, ref) // 绘制底部区域 - 根据设计稿调整 const qrSize = 100; // 设计稿二维码尺寸 + const bottomTextContent = options.bottomText || "长按识别二维码,快来加入,有你就有场!"; // 计算底部区域布局 - 从底部向上计算 const bottomTextHeight = 28; // 底部文字高度 - const bottomSpacing = 30; // 底部间距 + const bottomTextLineHeight = 28; // 行高 - // 绘制二维码 - 右下角 + // 先绘制底部文字,确定文字的实际高度 + ctx.fillStyle = "rgba(0, 0, 0, 0.45)"; + ctx.font = "400 20px sans-serif"; // 设计稿字体大小 + ctx.textAlign = "left"; + ctx.textBaseline = "bottom"; + + const textX = sidePadding; + const textY = height - bottomPadding; + const maxTextWidth = width - sidePadding * 2 - qrSize - 20; // 预留二维码和间距 + + // 计算文字行数 + const bottomWords = bottomTextContent.split(""); + let bottomLine = ""; + let textLines = []; + for (let i = 0; i < bottomWords.length; i++) { + const testBottomLine = bottomLine + bottomWords[i]; + const metrics = ctx.measureText(testBottomLine); + if (metrics.width > maxTextWidth && i > 0) { + textLines.push(bottomLine); + bottomLine = bottomWords[i]; + } else { + bottomLine = testBottomLine; + } + } + if (bottomLine) { + textLines.push(bottomLine); + } + + const actualTextHeight = textLines.length * bottomTextLineHeight; + + // 绘制底部文字 + textLines.forEach((lineText, index) => { + ctx.fillText(lineText, textX, textY - (textLines.length - 1 - index) * bottomTextLineHeight); + }); + + // 绘制底部 SVG logo 图片 - 在文字上方 + const logoSvgY = height - bottomPadding - actualTextHeight - 40; // 在文字上方,间距40px + + try { + const logoSvgPath = shareLogoSvg; + // 先获取图片的实际尺寸,保持宽高比 + const logoInfo = await getImageInfo(logoSvgPath); + const logoSvgImg = await loadImage(canvas, logoSvgPath); + + // 根据设计稿,目标宽度为 235px,按比例计算高度 + const targetWidth = 235; + const aspectRatio = logoInfo.width / logoInfo.height; + const targetHeight = targetWidth / aspectRatio; + + const logoSvgX = sidePadding; // 左边对齐 + + // 按实际宽高比绘制,避免拉伸 + ctx.drawImage( + logoSvgImg, + logoSvgX, + logoSvgY, + targetWidth, + targetHeight + ); + } catch (error) { + console.error("Failed to load logo SVG:", error); + } + + // 绘制二维码 - 右下角,与文字底部对齐 const qrX = width - sidePadding - qrSize; - const qrY = height - bottomPadding - qrSize - bottomTextHeight - 20; // 在文字上方 + const qrY = height - bottomPadding - qrSize; if (options.qrCodeUrl) { try { @@ -321,26 +514,6 @@ const RadarChartV2 = forwardRef((props, ref) } } - // 绘制底部 SVG logo 图片 - 根据设计稿,SVG 包含 logo 和文字 - try { - const logoSvgPath = shareLogoSvg; - const logoSvgImg = await loadImage(canvas, logoSvgPath); - const logoSvgWidth = 235; // SVG 原始宽度 - const logoSvgHeight = 28; // SVG 原始高度 - const logoSvgX = sidePadding; // 左边对齐 - const logoSvgY = height - bottomPadding - logoSvgHeight; // 底部对齐 - - ctx.drawImage( - logoSvgImg, - logoSvgX, - logoSvgY, - logoSvgWidth, - logoSvgHeight - ); - } catch (error) { - console.error("Failed to load logo SVG:", error); - } - // 导出图片 Taro.canvasToTempFilePath({ canvas, diff --git a/src/components/index.ts b/src/components/index.ts index 73f42fe..3720f2e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -23,6 +23,7 @@ import FollowUserCard from './FollowUserCard/index'; import Comments from "./Comments"; import GeneralNavbar from "./GeneralNavbar"; import RadarChart from './Radar' +import RadarChartV2 from './Radar/indexV2' import EmptyState from './EmptyState'; import NTRPTestEntryCard from './NTRPTestEntryCard' @@ -53,6 +54,7 @@ export { Comments, GeneralNavbar, RadarChart, + RadarChartV2, EmptyState, NTRPTestEntryCard, }; diff --git a/src/other_pages/ntrp-evaluate/index.tsx b/src/other_pages/ntrp-evaluate/index.tsx index 97140ec..43800b3 100644 --- a/src/other_pages/ntrp-evaluate/index.tsx +++ b/src/other_pages/ntrp-evaluate/index.tsx @@ -3,7 +3,7 @@ import { View, Text, Image, Button, Canvas } from "@tarojs/components"; import Taro, { useRouter, useShareAppMessage } from "@tarojs/taro"; import dayjs from "dayjs"; import classnames from "classnames"; -import { withAuth, RadarChart } from "@/components"; +import { withAuth, RadarChart, RadarChartV2 } from "@/components"; import evaluateService, { LastTimeTestResult, Question, @@ -16,6 +16,9 @@ import { useGlobalState } from "@/store/global"; import { delay, getCurrentFullPath } from "@/utils"; import { formatNtrpDisplay } from "@/utils/helper"; import { waitForAuthInit } from "@/utils/authInit"; +import httpService from "@/services/httpService"; +import DetailService from "@/services/detailService"; +import { base64ToTempFilePath } from "@/utils/genPoster"; import CloseIcon from "@/static/ntrp/ntrp_close_icon.svg"; import DocCopy from "@/static/ntrp/ntrp_doc_copy.svg"; import ArrowRight from "@/static/ntrp/ntrp_arrow_right.svg"; @@ -452,12 +455,14 @@ function Result() { const userInfo = useUserInfo(); const { fetchUserInfo, updateUserInfo } = useUserActions(); const { type, next, clear } = useEvaluate(); - const radarRef = useRef(); + const radarRef = useRef(); + const radarV2Ref = useRef(); const [result, setResult] = useState(); const [radarData, setRadarData] = useState< [propName: string, prop: number][] >([]); + const [qrCodeUrl, setQrCodeUrl] = useState(""); useEffect(() => { const init = async () => { @@ -469,10 +474,32 @@ function Result() { if (!userInfo || Object.keys(userInfo).length === 0) { await fetchUserInfo(); } + // 获取二维码 + await fetchQRCode(); }; init(); }, [id]); + // 获取二维码 - 调用接口生成分享二维码 + async function fetchQRCode() { + try { + // 调用接口生成二维码,分享当前页面 + const qrCodeUrlRes = await DetailService.getQrCodeUrl({ + page: "other_pages/ntrp-evaluate/index", + scene: `stage=${StageType.INTRO}`, + }); + if (qrCodeUrlRes.code === 0 && qrCodeUrlRes.data?.qr_code_base64) { + // 将 base64 转换为临时文件路径 + const tempFilePath = await base64ToTempFilePath( + qrCodeUrlRes.data.qr_code_base64 + ); + setQrCodeUrl(tempFilePath); + } + } catch (error) { + console.error("获取二维码失败:", error); + } + } + async function getResultById() { const res = await evaluateService.getTestResult({ record_id: Number(id) }); if (res.code === 0) { @@ -542,51 +569,38 @@ function Result() { } async function genCardImage() { - return new Promise(async (resolve, reject) => { - const url = await radarRef.current.generateImage(); - const query = Taro.createSelectorQuery(); - query - .select("#exportCanvas") - .fields({ node: true, size: true }) - .exec((res2) => { - const canvas = res2[0].node; - const ctx = canvas.getContext("2d"); - const dpr = Taro.getWindowInfo().pixelRatio; - const width = 300; - const height = 400; - canvas.width = width * dpr; - canvas.height = height * dpr; - ctx.scale(dpr, dpr); - - // 背景 - ctx.fillStyle = "#e9fdf8"; - ctx.fillRect(0, 0, width, height); - - // 标题文字 - ctx.fillStyle = "#000"; - ctx.font = "16px sans-serif"; - ctx.fillText("你的 NTRP 测试结果为", 20, 40); - ctx.fillStyle = "#00E5AD"; - ctx.font = "bold 22px sans-serif"; - ctx.fillText(`NTRP ${formatNtrpDisplay(result?.ntrp_level)}`, 20, 70); - - // 绘制雷达图 - const img = canvas.createImage(); - img.src = url; - img.onload = () => { - ctx.drawImage(img, 20, 100, 260, 260); - - // 第三步:导出最终卡片 - Taro.canvasToTempFilePath({ - canvas, - success: (res3) => { - console.log("导出成功:", res3.tempFilePath); - resolve(res3.tempFilePath); - }, - }); - }; + try { + // 确保二维码已获取,如果没有则重新获取 + let finalQrCodeUrl = qrCodeUrl; + if (!finalQrCodeUrl) { + // 直接调用接口获取二维码 + const qrCodeUrlRes = await DetailService.getQrCodeUrl({ + page: "other_pages/ntrp-evaluate/index", + scene: `stage=${StageType.INTRO}`, }); - }); + if (qrCodeUrlRes.code === 0 && qrCodeUrlRes.data?.qr_code_base64) { + finalQrCodeUrl = await base64ToTempFilePath( + qrCodeUrlRes.data.qr_code_base64 + ); + } + } + + // 使用 RadarV2 的 generateFullImage 方法生成完整图片 + const imageUrl = await radarV2Ref.current?.generateFullImage({ + title: "你的 NTRP 测试结果为", + ntrpLevel: result?.ntrp_level, + levelDescription: result?.level_description, + avatarUrl: userInfo?.avatar_url, + qrCodeUrl: finalQrCodeUrl, + bottomText: "长按识别二维码,快来加入,有你就有场!", + width: 750, // 设计稿宽度 + height: 1334, // 设计稿高度 + }); + return imageUrl; + } catch (error) { + console.error("生成图片失败:", error); + throw error; + } } async function handleSaveImage() { @@ -746,6 +760,19 @@ function Result() { left: "-9999px", }} /> + {/* 隐藏的 RadarV2 用于生成完整图片,不显示在界面上 */} + + + ); } diff --git a/src/static/ntrp/ntrp_share_logo.png b/src/static/ntrp/ntrp_share_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a1534dac2919425267aa2607e00edf2919866753 GIT binary patch literal 2991 zcmV;g3sCflP)1$00009a7bBm000XU z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yP-plpgFAwZ4u!~;lPOQ=W) zlvG3rBr0#XiA!nhNJ#MHH96{(U%+9XY zeyKm{c+Sk6^Pk&)pS6MwY~VeL7xd5Hm&r69bdt_MB4xjkvbJ3N$i0`!VeOTW=fkXw zu`3;C;tg$%oKT;jL4LyAI-?{b<8SFU=qMetKZ(yGp`ALb2dD7T}n0Y=hNJn5F~6 z=&PhLa_WIyPyJTzo8lAoFD&bu*aX5slRFdfewfn;LoNo5ZjonYFRzmZI)~PI2}Y5} zpFliJ!PSVQSdNCYxysw=2%nb>n#T;MyB?rD$e}wFr8@DC%Ii6}Jf(*8qrIAiD-=QV z*~jbT*=$8HU&sPMdz|ItkzyVXXpgytkzup`$@7Nu_v0Dm4pK>j2@44cvn5WQ2}&I{ z>QI>ZaF80T)c*aOnd@J9@uGNb6y9CC;UozvUbrVp*puO*P@#el=$Lj{k-yD)knF$6HmZS+- zCai?buByt>cp3K$ef^t%yz;`!Up{d2CqyH~7a|X$O7+me1KMc3@32HC2ls#` z`SUzl=@Vl-jr9W^;32FXO7OIvDm^DHSwyfOF8U0kb?=B_-j866NNojlgr8TQ&w<{{ zLP%GI(;9&nXR7er9O^qRtz!nk>!7~-C0c|MUq=z~-E3K4$c;RTaCeNL!BaeRT@k0f z^3LG{4aa^Woy=fU>LFBX&!aC@VZyQu#c|jM*v(C_nMc;J63p5tFs(9b<~-8&?x!x! zTE9BBUoVlcOz}cc;cb$bbQE-{PNp~)un3|@bD4n0Kt!81;wm~9&ecFe``}TI<8tua zG9BT}D?sZJd#1wGv_hL64H;Kh9?@n}>Yqh$(0UfZ+O{awiD7Avb8wkl&=m2205SI* zzZMa`HyS*qEub|XQu#RaTu1j%rTQ`SD2`3^Sx+V#4rBgTuzk~EyuR=NYPD%(`b{P* zHf)E9i!B5t0fkwaWYR{t_9Av}`NYtT`I}#5WX>mibY*{CY+_iMMm*%;1%EQ_ivcwE zwxOl*IiA2u&DsuPnd;^Gcet4WxR z1T2eL5|ib_tR(7n2aSf(bkP>8H?eK#e%97$#-y^ao$K3;2sPvFFIe6=k1=4SYMy0W-?S)`+?`EP1LCFdgp4U+D z%}ZD$(XLYgZwH#Y5>QIg`&+!M*y9_wKleV{uJ3W|DhuTQgLlf4=u2+J-felj_0|A$ z$x^6P6Q~*q7&T^n3@{hdY%ngRLOULi`Pkz z=n%*?&^cXkkVSaR8^uumjGcyXHzf#Dd`voATI)&l9xA+j)X*xF#fUx{pO>6fB3ujU zBQtk_UrWB{0y^3!3yN@gpvY;c0W>HcIS9{Xm*1U)mo6sdsG&RL__g4BJ{K^ifQkrI z)3uCMts$MRF>6u9LiN|!(!U>rnfr0iz$q-;{V5YYr`eWbl{yKFp`cb}@ji=T?k>H? zvSaC{oY0>>>Clm<8#GRzZX?~=n z#$&WSE!7LYcRIg{_%M>GdH|v?CvkcS4W0*4gd6xytW}zOY_;G`Z&dge#yUcy3XkwTWOh4_BMVb>ZTo2-oEq zgQtx(Y3v{r;4!)m&vk7F+S8oW&+k4J;kkHpUb9%~junDQx1@isB7C2&1DTvbUq((ufkKxdQj)Pi{@WbN&7HmxX9Hq3>&3|DGa4QhW_+Un3dfy(|XgY(EEl> z)&(qPNwA>5;WTWknIsW0OMzcCX^cENgN9b0TUZ+1mPBVYpz|TC zc+B_QuY5GP`;kY0cT_da^C`pm2D1IgS(a-TN{g>G+mbt$ zU_Mu)kz}FF5X&TFrp_FH@d~|5pm!i`n)5xQjV5bA8-Eg>J4Oo~N2|}NV|^g3%UX{i zGZ+buirzp)Ybw5oaQB(!aR{wixPbUS>FH!n8oc>X$)WY*=M=n*99_c%P_{-RSow1z zx~C86vZ5Q~a~KUG_}Ih$tv2e`6P4v;qr8-4*)Ijho`-O-n&Fm zkYks%f*ey319ba?#URKoiTLnHsO?AdI&8) z!_!(x>1zCb*!PYg1Sgb-9{cp$Gu2A+@me+0s2dqJlRlVrlLPzWudF|NKT2Jq<1sQ}Ju53F#e` zPQHX|a0N-2|E!NzFSYJ_5kmQhLVK;Ed#LE_oO5VB-?O~0?C#MsQa;4o)4sXN$ns~h z&}hDUd7g8e&MZ2>&o5~^EVs<)77|{-JXv)W4h9p{2AXn@b_8%UPtg; lhY^4ND%_t*t#-|A@qZf<0)=3>5BUH9002ovPDHLkV1h=i$T0u_ literal 0 HcmV?d00001 diff --git a/src/static/ntrp/ntrp_share_logo.svg b/src/static/ntrp/ntrp_share_logo.svg new file mode 100644 index 0000000..2d242d8 --- /dev/null +++ b/src/static/ntrp/ntrp_share_logo.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +