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, RadarChartV2 } 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 httpService from "@/services/httpService"; import DetailService from "@/services/detailService"; import { base64ToTempFilePath } from "@/utils/genPoster"; import { OSS_BASE_URL } from "@/config/api"; 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() // 判断是否从分享链接进入:页面栈只有1个页面时,说明是从分享直接进入的 const isFromShare = () => { try { const pages = (Taro as any).getCurrentPages(); return pages && pages.length === 1; } catch { return false; } }; function handleClose() { // 如果是从分享链接进入,直接跳转首页 if (isFromShare()) { (Taro as any).redirectTo({ url: "/main_pages/index" }); return; } // 否则执行原来的逻辑(跳转球局或执行 onCancel) //TODO: 二次确认 if (confirm) { } try { console.log(onCancel, isOnCancelEmpty(onCancel)); if (isOnCancelEmpty(onCancel)) { (Taro as any).redirectTo({ url: "/main_pages/index" }); } onCancel(); } catch { (Taro as any).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, level_description } = 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 */} handleNext(StageType.RESULT)} > 上次测试结果 {lastTestTime} NTRP {formatNtrpDisplay(ntrp_level || "")} {level_description} 再次测试 继续使用上次测试结果 ) : ( {/* 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, updateUserInfo } = useUserActions(); const { type, next, clear } = useEvaluate(); 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 () => { // 先等待静默登录完成 await waitForAuthInit(); // 然后再调用接口 await getResultById(); // 确保用户信息已加载 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) { 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", }); updateUserInfo({ ntrp_level: ntrp_level }); } 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() { 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 userNickname = (userInfo as any)?.nickname; const titleText = userNickname ? `${userNickname}的 NTRP 测试结果为` : "你的 NTRP 测试结果为"; const imageUrl = await radarV2Ref.current?.generateFullImage({ title: titleText, ntrpLevel: result?.ntrp_level, levelDescription: result?.level_description, avatarUrl: (userInfo as any)?.avatar_url, qrCodeUrl: finalQrCodeUrl, bottomText: "长按识别二维码,快来加入,有你就有场!", width: 750, // 设计稿宽度 height: 1334, // 设计稿高度 }); return imageUrl; } catch (error) { console.error("生成图片失败:", error); throw error; } } 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) => { console.log("res", result); return { title: "来测一测你的NTRP等级吧", imageUrl: result?.level_img || undefined, 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 */} {(userInfo as any)?.nickname ? `${(userInfo as any).nickname}的 NTRP 测试结果为` : "你的 NTRP 测试结果为"} NTRP {formatNtrpDisplay(result?.ntrp_level)} {/* 变线+网前,下一步就是赢比赛! */} {result?.level_description} 重新测试 {userInfo?.phone ? ( 你的 NTRP 水平已更新为 {formatNtrpDisplay(result?.ntrp_level || "")}{" "} (可在个人信息中修改) ) : ( 登录「有场」小程序,查看匹配你的球局 )} 邀请好友测试 保存图片 {/* 隐藏的 RadarV2 用于生成完整图片,不显示在界面上 */} ); } 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);