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"; import evaluateService, { LastTimeTestResult, Question, TestResultData, } from "@/services/evaluateService"; import { useUserInfo, useUserActions } from "@/store/userStore"; import { useEvaluate, EvaluateScene } from "@/store/evaluateStore"; 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 { INTRO = "intro", TEST = "test", RESULT = "result", } const sourceTypeToTextMap = new Map([ [EvaluateScene.detail, "继续加入球局"], [EvaluateScene.publish, "继续发布球局"], ]); 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 isEmptyArrowFunction(fn) { return ( typeof fn === "function" && !fn.hasOwnProperty("prototype") && // 排除普通函数 fn.toString().replace(/\s+/g, "") === "()=>{}" ); } function CommonGuideBar(props) { const { title, confirm } = props; const { onCancel } = useEvaluate(); // const userInfo = useUserInfo() function handleClose() { //TODO: 二次确认 if (confirm) { } try { if (isEmptyArrowFunction(onCancel)) { Taro.redirectTo({ url: "/game_pages/list/index" }); } onCancel(); } catch { Taro.redirectTo({ url: "/game_pages/list/index" }); } } return ( {title} ); } function Intro() { const [ntrpData, setNtrpData] = useState(); const userInfo = useUserInfo(); const { fetchUserInfo } = useUserActions(); const [ready, setReady] = useState(false); const { setCallback } = useEvaluate(); const { last_test_result: { ntrp_level, create_time, id } = {} } = ntrpData || {}; const lastTestTime = dayjs(create_time).format("YYYY年M月D日"); useEffect(() => { getLastResult(); }, []); async function getLastResult() { const res = await evaluateService.getLastResult(); if (res.code === 0) { setNtrpData(res.data); if (res.data.has_ntrp_level) { fetchUserInfo(); } setReady(true); } } if (!ready) { return ""; } function handleNext(type) { setCallback({ type: EvaluateScene.share, next: () => { Taro.redirectTo({ url: "/game_pages/list/index" }); }, onCancel: () => { Taro.redirectTo({ url: "/game_pages/list/index" }); // if (userInfo.id) { // Taro.redirectTo({ url: "/game_pages/list/index" }); // } else { // Taro.exitMiniProgram(); // } }, }); Taro.redirectTo({ url: `/other_pages/ntrp-evaluate/index?stage=${type}${ type === StageType.RESULT ? `&id=${id}` : "" }`, }); } return ( {ntrpData?.has_ntrp_level ? ( {/* avatar side */} {/* tip */} 上次测试结果 {lastTestTime} NTRP {ntrp_level} 变线+网前,下一步就是赢比赛! ) : ( {/* tip */} {/* radar */} NTRP(National Tennis Rating Program)是一种常用的网球水平分级系统,这不是绝对精准的“分数”,而是一个参考标准,能够帮助你更清晰地了解自己的网球水平,从而在训练、比赛或娱乐活动中找到「难度合适」的球友,避免过度碾压或被碾压。 )} ); } function Test() { const [disabled, setDisabled] = useState(false); const [index, setIndex] = useState(0); const [questions, setQuestions] = useState< (Question & { choosen: number })[] >([]); const startTimeRef = useRef(0); useEffect(() => { startTimeRef.current = Date.now(); getQUestions(); }, []); useEffect(() => { setDisabled(questions[index]?.choosen === -1); }, [index, questions]); async function getQUestions() { const res = await evaluateService.getQuestions(); if (res.code === 0) { setQuestions(res.data.map((item) => ({ ...item, choosen: 3 }))); } } function handleSelect(i) { setQuestions((prev) => prev.map((item, pIndex) => ({ ...item, ...(pIndex === index ? { choosen: i } : {}), })) ); } async function handleSubmit() { setDisabled(true); try { const res = await evaluateService.submit({ answers: questions.map((item) => ({ question_id: item.id, answer_index: item.choosen, })), test_duration: (Date.now() - startTimeRef.current) / 1000, }); if (res.code === 0) { Taro.redirectTo({ url: `/other_pages/ntrp-evaluate/index?stage=${StageType.RESULT}&id=${res.data.record_id}`, }); } } catch (e) { Taro.showToast({ title: e.message, icon: "error" }); } finally { setDisabled(false); } } function handIndexChange(direction) { console.log(disabled, direction); if (disabled && direction > 0) { return; } if (index === questions.length - 1 && direction > 0) { handleSubmit(); return; } setIndex((prev) => prev + direction); } const question = questions[index]; if (!question) { return ""; } return ( 根据近3个月实际表现勾选最符合项 {question.question_content} {question.options.map((item, i) => { const checked = question.choosen === i; return ( handleSelect(i)} > {item.text} ); })} handIndexChange(1)} > {index !== 0 && ( handIndexChange(-1)}> 返回 )} ); } function Result() { const { params } = useRouter(); const { id } = params; const userInfo = useUserInfo(); const { fetchUserInfo } = useUserActions(); const { type, next, clear } = useEvaluate(); const radarRef = useRef(); const [result, setResult] = useState(); const [radarData, setRadarData] = useState< [propName: string, prop: number][] >([]); useEffect(() => { getResultById(); fetchUserInfo(); }, []); async function getResultById() { const res = await evaluateService.getTestResult({ record_id: Number(id) }); if (res.code === 0) { setResult(res.data); 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); } } 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}`, }); } function handleViewGames() { Taro.redirectTo({ url: "/game_pages/list/index", }); } async function handleGoon() { if (type) { next(); await delay(1500); clear(); } else { handleViewGames(); } } 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 ( {/* avatar side */} 你的 NTRP 测试结果为 NTRP {result?.ntrp_level} 变线+网前,下一步就是赢比赛! 重新测试 {userInfo.id ? ( 你的 NTRP 水平已更新为 {result?.ntrp_level} (可在个人信息中修改) ) : ( 登录「有场」小程序,查看匹配你的球局 )} ); } const ComponentsMap = { [StageType.INTRO]: Intro, [StageType.TEST]: Test, [StageType.RESULT]: Result, }; function NtrpEvaluate() { // const { updateUserInfo } = useUserActions(); const { params } = useRouter(); // const { redirect } = params; const stage = params.stage as StageType; // async function handleUpdateNtrp() { // await updateUserInfo({ // ntrp_level: "4.0", // }); // Taro.showToast({ // title: "更新成功", // icon: "success", // duration: 2000, // }); // await delay(2000); // if (redirect) { // Taro.redirectTo({ url: decodeURIComponent(redirect) }); // } // } const Component = ComponentsMap[stage]; return ; } export default withAuth(NtrpEvaluate);