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);