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 { OSS_BASE } 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}`,
});
setQrCodeUrl(qrCodeUrlRes.data.ossPath);
// 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);
const sortOrder = res.data.sort || [];
const abilities = res.data.radar_data.abilities;
const sortedKeys = sortOrder.filter((k) => k in abilities);
const remainingKeys = Object.keys(abilities).filter(
(k) => !sortOrder.includes(k),
);
const allKeys = [...sortedKeys, ...remainingKeys];
let radarData: [string, number][] = allKeys.map((key) => [
key,
Math.min(
100,
Math.floor(
(abilities[key].current_score / abilities[key].max_score) * 100,
),
),
]);
// 直接使用接口 sort 顺序,不经过 adjustRadarLabels 重新排序
setRadarData(radarData);
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}`,
});
finalQrCodeUrl = qrCodeUrlRes.data.ossPath;
// 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);