import { useState, useEffect, useRef, useId, useMemo } 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, StageType, } from "@/services/evaluateService"; import { useUserInfo, useUserActions } from "@/store/userStore"; import { useEvaluate, EvaluateScene } from "@/store/evaluateStore"; import { useGlobalState } from "@/store/global"; import { delay, getCurrentFullPath } from "@/utils"; import { formatNtrpDisplay } from "@/utils/helper"; import { waitForAuthInit } from "@/utils/authInit"; 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"; 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 isOnCancelEmpty(onCancelFunc) { if (typeof onCancelFunc !== "function") { console.log("onCancel 不是函数"); return false; } try { const funcString = onCancelFunc.toString().trim(); // 常见空函数模式 const emptyFunctionPatterns = [ "functiononCancel(){}", "function onCancel() {}", "onCancel(){}", "()=>{}", "functiononCancel(){ }", "() => {}", ]; const normalized = funcString.replace(/\s/g, ""); return emptyFunctionPatterns.includes(normalized); } catch (error) { console.error("检查 onCancel 函数时出错:", error); return false; } } function CommonGuideBar(props) { const { title, confirm } = props; const { onCancel } = useEvaluate(); const { statusNavbarHeightInfo } = useGlobalState(); const { statusBarHeight, navBarHeight } = statusNavbarHeightInfo; // const userInfo = useUserInfo() function handleClose() { //TODO: 二次确认 if (confirm) { } try { console.log(onCancel, isOnCancelEmpty(onCancel)); if (isOnCancelEmpty(onCancel)) { Taro.redirectTo({ url: "/main_pages/index" }); } onCancel(); } catch { Taro.redirectTo({ url: "/main_pages/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 = null } = ntrpData || {}; const { ntrp_level, create_time, id } = last_test_result || {}; const lastTestTime = create_time ? dayjs(create_time).format("YYYY年M月D日") : ""; useEffect(() => { const init = async () => { // 先等待静默登录完成 await waitForAuthInit(); // 然后再调用接口 await getLastResult(); }; init(); }, []); async function getLastResult() { const res = await evaluateService.getLastResult(); if (res.code === 0) { setNtrpData(res.data); if (res.data.has_ntrp_level) { // 确保用户信息已加载 if (!userInfo || Object.keys(userInfo).length === 0) { await fetchUserInfo(); } } setReady(true); } } if (!ready) { return ""; } function handleNext(type) { if (!id) { setCallback({ type: EvaluateScene.share, next: () => { Taro.redirectTo({ url: "/main_pages/index" }); }, onCancel: () => { Taro.redirectTo({ url: "/main_pages/index" }); }, }); } Taro.redirectTo({ url: `/other_pages/ntrp-evaluate/index?stage=${type}${type === StageType.RESULT ? `&id=${id}` : "" }`, }); } return ( {ntrpData?.has_test_record ? ( {/* avatar side */} {/* tip */} 上次测试结果 {lastTestTime} NTRP {formatNtrpDisplay(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]); const doneFlag = useMemo(() => { const [q1, q2, q3] = questions; return [q1, q2, q3].every((q) => q?.choosen === 0); }, [questions]); async function getQUestions() { const res = await evaluateService.getQuestions(); if (res.code === 0) { setQuestions(res.data.map((item) => ({ ...item, choosen: -1 }))); } } 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 .filter((item) => item.choosen >= 0) .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) { if (disabled && direction > 0) { return; } if ((index === questions.length - 1 || doneFlag) && 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(() => { const init = async () => { // 先等待静默登录完成 await waitForAuthInit(); // 然后再调用接口 await getResultById(); // 确保用户信息已加载 if (!userInfo || Object.keys(userInfo).length === 0) { await fetchUserInfo(); } }; init(); }, [id]); async function getResultById() { const res = await evaluateService.getTestResult({ record_id: Number(id) }); if (res.code === 0) { setResult(res.data); // delay(1000); setRadarData( adjustRadarLabels( Object.entries(res.data.radar_data.abilities).map(([key, value]) => [ key, Math.min( 100, Math.floor((value.current_score / value.max_score) * 100) ), ]) ) ); 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() { if (!userInfo.phone) { handleAuth(); return false; } Taro.redirectTo({ url: `/other_pages/ntrp-evaluate/index?stage=${StageType.TEST}`, }); } function handleViewGames() { Taro.redirectTo({ url: "/main_pages/index", }); } async function handleGoon() { if (!userInfo?.phone) { Taro.redirectTo({ url: `/login_pages/index/index?redirect=${encodeURIComponent( `/main_pages/index` )}`, }); clear(); return; } if (type) { next({ flag: false, score: result?.ntrp_level as string }); 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.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); }, }); }; }); }); } async function handleSaveImage() { console.log(userInfo); if (!userInfo?.phone) { handleAuth(); return; } Taro.getSetting().then(async (res) => { if (!res.authSetting["scope.writePhotosAlbum"]) { Taro.authorize({ scope: "scope.writePhotosAlbum", success: async () => { try { const url = await genCardImage(); Taro.saveImageToPhotosAlbum({ filePath: url }); Taro.showToast({ title: "保存成功" }); } catch (e) { Taro.showToast({ title: "图片保存失败", icon: "none" }); } }, fail: () => { Taro.showModal({ title: "提示", content: "需要开启相册权限才能保存图片", success: (r) => { if (r.confirm) Taro.openSetting(); }, }); }, }); } else { try { const url = await genCardImage(); Taro.saveImageToPhotosAlbum({ filePath: url }); Taro.showToast({ title: "保存成功" }); } catch (e) { Taro.showToast({ title: "图片保存失败", icon: "none" }); } } }); } useShareAppMessage(async (res) => { // const url = await genCardImage(); console.log(res, "res"); return { title: "来测一测你的NTRP等级吧", // imageUrl: url, path: `/other_pages/ntrp-evaluate/index?stage=${StageType.INTRO}`, }; }); function handleAuth() { if (userInfo?.phone) { return true; } const currentPage = getCurrentFullPath(); Taro.redirectTo({ url: `/login_pages/index/index${currentPage ? `?redirect=${encodeURIComponent(currentPage)}` : "" }`, }); } function handleGo() { } return ( {/* avatar side */} 你的 NTRP 测试结果为 NTRP {formatNtrpDisplay(result?.ntrp_level)} 变线+网前,下一步就是赢比赛! 重新测试 {userInfo?.phone ? ( 你的 NTRP 水平已更新为 {formatNtrpDisplay(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);