diff --git a/src/components/Radar/index.tsx b/src/components/Radar/index.tsx index 9a325ca..00d2556 100644 --- a/src/components/Radar/index.tsx +++ b/src/components/Radar/index.tsx @@ -1,8 +1,8 @@ import Taro, { useReady } from "@tarojs/taro"; import { View, Canvas, Button } from "@tarojs/components"; -import { useEffect, useRef } from "react"; +import { useEffect, useRef, forwardRef, useImperativeHandle } from "react"; -const RadarChart: React.FC = (props) => { +const RadarChart: React.FC = forwardRef((props, ref) => { const { data } = props const renderFnRef = useRef() @@ -118,6 +118,23 @@ const RadarChart: React.FC = (props) => { }); } + useImperativeHandle(ref, () => ({ + generateImage: () => new Promise((resolve, reject) => { + const query = Taro.createSelectorQuery() + query.select("#radarCanvas") + .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 saveImage = () => { @@ -142,6 +159,6 @@ const RadarChart: React.FC = (props) => { {/* */} ); -}; +}); export default RadarChart; diff --git a/src/other_pages/ntrp-evaluate/index.module.scss b/src/other_pages/ntrp-evaluate/index.module.scss index ccb34f6..122d07a 100644 --- a/src/other_pages/ntrp-evaluate/index.module.scss +++ b/src/other_pages/ntrp-evaluate/index.module.scss @@ -564,5 +564,123 @@ line-height: normal; } } + + .retest { + position: absolute; + right: 12px; + top: 12px; + display: flex; + padding: 6px 10px; + justify-content: center; + align-items: center; + gap: 6px; + border-radius: 12px; + border: 0.5px solid rgba(0, 0, 0, 0.12); + background: #FFF; + box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.10); + color: rgba(0, 0, 0, 0.85); + font-feature-settings: 'liga' off, 'clig' off; + font-family: "PingFang SC"; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 20px; + + .re_actIcon { + width: 12px; + height: 12px; + } + } + } + + .updateTip { + color: #000; + font-feature-settings: 'liga' off, 'clig' off; + font-family: "PingFang SC"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 24px; + + text-align: center; + padding: 24px 0; + + .grayTip { + color: rgba(60, 60, 67, 0.60); + } + } + + .actions { + padding: 0 28px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + gap: 10px; + + .viewGame { + border-radius: 16px; + border: 1px solid rgba(0, 0, 0, 0.06); + width: 100%; + overflow: hidden; + + .viewGameBtn { + width: 100%; + height: 50px; + background: #000; + color: #FFF; + font-feature-settings: 'liga' off, 'clig' off; + font-family: "PingFang SC"; + font-size: 16px; + font-style: normal; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + } + } + + .otherActions { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + + .share, .saveImage { + width: 50%; + height: 50px; + border-radius: 16px; + border: 1px solid rgba(0, 0, 0, 0.06); + overflow: hidden; + + .shareBtn, .saveImageBtn { + background: #FFF; + width: 100%; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + color: #000; + font-feature-settings: 'liga' off, 'clig' off; + font-family: "PingFang SC"; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: normal; + + .downloadIcon { + width: 20px; + height: 20px; + } + + .wechatIcon { + width: 24px; + height: 24px; + } + } + } + } } } \ No newline at end of file diff --git a/src/other_pages/ntrp-evaluate/index.tsx b/src/other_pages/ntrp-evaluate/index.tsx index a603d9d..42d4396 100644 --- a/src/other_pages/ntrp-evaluate/index.tsx +++ b/src/other_pages/ntrp-evaluate/index.tsx @@ -1,6 +1,6 @@ -import { useState, useEffect, useRef } from "react"; -import { View, Text, Image, Button } from "@tarojs/components"; -import Taro, { useRouter } from "@tarojs/taro"; +import { useState, useEffect, useRef, useId } from "react"; +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"; @@ -10,13 +10,16 @@ import evaluateService, { TestResultData, } from "@/services/evaluateService"; import { useUserInfo, useUserActions } from "@/store/userStore"; -import { delay } from "@/utils"; +import { delay, getCurrentFullPath } from "@/utils"; 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"; import ArrowBack from "@/static/ntrp/ntrp_arrow_back.svg"; import CircleChecked from "@/static/ntrp/ntrp_circle_checked.svg"; import CircleUnChecked from "@/static/ntrp/ntrp_circle_unchecked.svg"; +import WechatIcon from "@/static/ntrp/ntrp_wechat.svg"; +import DownloadIcon from "@/static/ntrp/ntrp_download.svg"; +import ReTestIcon from "@/static/ntrp/ntrp_re-action.svg"; import styles from "./index.module.scss"; enum StageType { @@ -25,6 +28,48 @@ enum StageType { RESULT = "result", } +function adjustRadarLabels( + source: [string, number][], + topK: number = 4 // 默认挑前4个最长的标签保护 +): [string, number][] { + if (source.length === 0) return source; + + // 复制并按长度排序(降序) + let sorted = [...source].sort((a, b) => b[0].length - a[0].length); + + // 取出前 K 个最长标签 + let protectedLabels = sorted.slice(0, topK); + // 其他标签(保持原始顺序,但排除掉 protected) + let protectedSet = new Set(protectedLabels.map(([l]) => l)); + let others = source.filter(([l]) => !protectedSet.has(l)); + + let n = source.length; + let result: ([string, number] | undefined)[] = new Array(n); + + // 放首尾 + result[0] = protectedLabels.shift() || others.shift(); + result[n - 1] = protectedLabels.shift() || others.shift(); + + // 放中间(支持偶数两个位置) + if (n % 2 === 0) { + let mid1 = n / 2 - 1; + let mid2 = n / 2; + result[mid1] = protectedLabels.shift() || others.shift(); + result[mid2] = protectedLabels.shift() || others.shift(); + } else { + let mid = Math.floor(n / 2); + result[mid] = protectedLabels.shift() || others.shift(); + } + + // 把剩余标签按顺序塞进空位 + let pool = [...protectedLabels, ...others]; + for (let i = 0; i < n; i++) { + if (!result[i]) result[i] = pool.shift(); + } + + return result as [string, number][]; +} + function CommonGuideBar(props) { const { title, confirm } = props; const { params } = useRouter(); @@ -197,7 +242,7 @@ function Intro(props) { function Test(props) { const { redirect } = props; const [disabled, setDisabled] = useState(false); - const [index, setIndex] = useState(9); + const [index, setIndex] = useState(0); const [questions, setQuestions] = useState< (Question & { choosen: number })[] >([]); @@ -323,14 +368,18 @@ function Test(props) { ); } -function Result() { +function Result(props) { + const { redirect } = props; const { params } = useRouter(); const { id } = params; const userInfo = useUserInfo(); const { fetchUserInfo } = useUserActions(); + const radarRef = useRef(); const [result, setResult] = useState(); - const [radarData, setRadarData] = useState<[propName: string, prop: number][]>([]) + const [radarData, setRadarData] = useState< + [propName: string, prop: number][] + >([]); useEffect(() => { getResultById(); @@ -341,11 +390,121 @@ function Result() { const res = await evaluateService.getTestResult({ record_id: Number(id) }); if (res.code === 0) { setResult(res.data); - setRadarData(Object.entries(res.data.radar_data.abilities).map(([key, value]) => [key, value.current_score])) + setRadarData( + adjustRadarLabels( + Object.entries(res.data.radar_data.abilities).map(([key, value]) => [ + key, + value.current_score, + ]) + ) + ); + updateUserLevel(res.data.record_id, res.data.ntrp_level); } } - console.log(result, "result"); + function updateUserLevel(record_id, ntrp_level) { + try { + evaluateService.updateNtrp({ + record_id, + ntrp_level, + update_type: "test_result", + }); + } catch (e) { + Taro.showToast({ title: e.message, icon: "none" }); + } + } + + function handleReTest() { + Taro.redirectTo({ + url: `/other_pages/ntrp-evaluate/index?stage=${StageType.TEST}${ + redirect ? `&redirect=${redirect}` : "" + }`, + }); + } + + function handleViewGames() { + Taro.redirectTo({ + url: "/game_pages/list/index", + }); + } + + 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.getSystemInfoSync().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 ${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); + }, + }); + }; + }); + }); + } + + async function handleSaveImage() { + if (!userInfo.id) { + return + } + const url = await genCardImage(); + Taro.saveImageToPhotosAlbum({ filePath: url }); + } + + useShareAppMessage(async (res) => { + const url = await genCardImage(); + console.log(res, "res"); + return { + title: "分享", + imageUrl: url, + path: `/other_pages/ntrp-evaluate/index?stage=${StageType.INTRO}`, + }; + }); + + function handleAuth () { + if (userInfo.id) { + return true + } + const currentPage = getCurrentFullPath() + Taro.redirectTo({ + url: `/login_pages/index/index${ + currentPage ? `?redirect=${encodeURIComponent(currentPage)}` : "" + }`, + }); + } return ( @@ -381,9 +540,52 @@ function Result() { - + + + + + 重新测试 + {userInfo.id ? ( + + 你的 NTRP 水平已更新为 {result?.ntrp_level} + (可在个人信息中修改) + + ) : ( + + 登录「有场」小程序,查看匹配你的球局 + + )} + + + + + + + + + + + + + + ); } diff --git a/src/static/ntrp/ntrp_download.svg b/src/static/ntrp/ntrp_download.svg new file mode 100644 index 0000000..1713295 --- /dev/null +++ b/src/static/ntrp/ntrp_download.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/static/ntrp/ntrp_re-action.svg b/src/static/ntrp/ntrp_re-action.svg new file mode 100644 index 0000000..66590de --- /dev/null +++ b/src/static/ntrp/ntrp_re-action.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/static/ntrp/ntrp_wechat.svg b/src/static/ntrp/ntrp_wechat.svg new file mode 100644 index 0000000..217b421 --- /dev/null +++ b/src/static/ntrp/ntrp_wechat.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/store/userStore.ts b/src/store/userStore.ts index 7ace708..de00e80 100644 --- a/src/store/userStore.ts +++ b/src/store/userStore.ts @@ -14,9 +14,11 @@ export interface UserState { export const useUser = create()((set) => ({ user: {}, fetchUserInfo: async () => { - const res = await fetchUserProfile(); - console.log(res); - set({ user: res.data }); + try { + const res = await fetchUserProfile(); + console.log(res); + set({ user: res.data }); + } catch {} }, updateUserInfo: async (userInfo: Partial) => { const res = await updateUserProfile(userInfo);