diff --git a/.gitignore b/.gitignore
index da0b5cd..d0a811b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ node_modules/
.swc
src/config/env.ts
.vscode
+*.http
diff --git a/src/components/Radar/index.tsx b/src/components/Radar/index.tsx
new file mode 100644
index 0000000..9f86d43
--- /dev/null
+++ b/src/components/Radar/index.tsx
@@ -0,0 +1,183 @@
+import Taro, { useReady } from "@tarojs/taro";
+import { View, Canvas, Button } from "@tarojs/components";
+import { useEffect } from "react";
+
+const RadarChart: React.FC = () => {
+ const labels = [
+ "正手球质",
+ "正手控制",
+ "反手球质",
+ "反手控制",
+ "底线相持",
+ "场地覆盖",
+ "发球接发",
+ "接随机球",
+ "战术设计",
+ ];
+ const values = [50, 75, 60, 20, 40, 70, 65, 35, 75];
+ const maxValue = 100;
+ const levels = 4;
+ const radius = 100;
+ const center = { x: 160, y: 160 };
+
+ useReady(() => {
+ const query = Taro.createSelectorQuery();
+ query
+ .select("#radarCanvas")
+ .fields({ node: true, size: true })
+ .exec((res) => {
+ const canvas = res[0].node as HTMLCanvasElement;
+ const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
+
+ // 设置像素比,保证清晰
+ const dpr = Taro.getSystemInfoSync().pixelRatio;
+ canvas.width = res[0].width * dpr;
+ canvas.height = res[0].height * dpr;
+ ctx.scale(dpr, dpr);
+
+ // 绘制网格
+ ctx.strokeStyle = "#ddd";
+ // for (let i = 1; i <= levels; i++) {
+ // const r = (radius / levels) * i;
+ // ctx.beginPath();
+ // labels.forEach((_, j) => {
+ // const angle = ((Math.PI * 2) / labels.length) * j - Math.PI / 2;
+ // const x = center.x + r * Math.cos(angle);
+ // const y = center.y + r * Math.sin(angle);
+ // if (j === 0) ctx.moveTo(x, y);
+ // else ctx.lineTo(x, y);
+ // });
+ // ctx.closePath();
+ // ctx.stroke();
+ // }
+
+ for (let i = 1; i <= levels; i++) {
+ const r = (radius / levels) * i;
+ ctx.beginPath();
+ labels.forEach((_, j) => {
+ const angle = ((Math.PI * 2) / labels.length) * j - Math.PI / 2;
+ const x = center.x + r * Math.cos(angle);
+ const y = center.y + r * Math.sin(angle);
+ if (j === 0) ctx.moveTo(x, y);
+ else ctx.lineTo(x, y);
+ });
+ ctx.closePath();
+
+ // === 偶数环填充颜色 ===
+ if (i % 2 === 0) {
+ ctx.fillStyle = "rgba(0, 150, 200, 0.1)"; // 浅色填充,透明度可调
+ ctx.fill();
+ }
+
+ // 保留线条
+ ctx.strokeStyle = "#bbb"; // 边框颜色
+ ctx.stroke();
+ }
+
+
+
+ // 坐标轴 & 标签
+ ctx.fillStyle = "#333";
+ ctx.font = "12px sans-serif";
+ // labels.forEach((label, i) => {
+ // const angle = ((Math.PI * 2) / labels.length) * i - Math.PI / 2;
+ // const x = center.x + radius * Math.cos(angle);
+ // const y = center.y + radius * Math.sin(angle);
+ // ctx.beginPath();
+ // ctx.moveTo(center.x, center.y);
+ // ctx.lineTo(x, y);
+ // ctx.strokeStyle = "#bbb";
+ // ctx.stroke();
+ // ctx.fillText(
+ // label,
+ // x + Math.cos(angle) * 20,
+ // y + Math.sin(angle) * 20
+ // );
+ // });
+
+ labels.forEach((label, i) => {
+ const angle = ((Math.PI * 2) / labels.length) * i - Math.PI / 2;
+
+ // 线条终点(半径)
+ const x = center.x + radius * Math.cos(angle);
+ const y = center.y + radius * Math.sin(angle);
+
+ // 画坐标轴线
+ ctx.beginPath();
+ ctx.moveTo(center.x, center.y);
+ ctx.lineTo(x, y);
+ ctx.strokeStyle = "#bbb";
+ ctx.stroke();
+
+ // === 改造后的文字位置 ===
+ const offset = 10; // 标签离图形的距离
+ const textX = center.x + (radius + offset) * Math.cos(angle);
+ const textY = center.y + (radius + offset) * Math.sin(angle);
+
+ ctx.font = "12px sans-serif";
+ ctx.fillStyle = "#333";
+ ctx.textBaseline = "middle";
+
+ console.log(label, angle)
+
+ // 根据角度调整文字对齐方式
+ if (Math.abs(angle) < 0.01 || Math.abs(Math.abs(angle) - Math.PI) < 0.01) {
+ // 顶部或底部
+ ctx.textAlign = "center";
+ } else if (angle > -Math.PI / 2 && angle < Math.PI / 2) {
+ // 右侧
+ ctx.textAlign = "left";
+ } else {
+ // 左侧
+ ctx.textAlign = "right";
+ }
+
+ ctx.fillText(label, textX, textY);
+ });
+
+
+ // 数据区域
+ ctx.beginPath();
+ values.forEach((val, i) => {
+ const angle = ((Math.PI * 2) / labels.length) * i - Math.PI / 2;
+ const r = (val / maxValue) * radius;
+ const x = center.x + r * Math.cos(angle);
+ const y = center.y + r * Math.sin(angle);
+ if (i === 0) ctx.moveTo(x, y);
+ else ctx.lineTo(x, y);
+ });
+ ctx.closePath();
+ ctx.fillStyle = "rgba(0,200,180,0.3)";
+ ctx.fill();
+ ctx.strokeStyle = "#00c8b4";
+ ctx.lineWidth = 4;
+ ctx.stroke();
+ });
+ });
+
+ // 保存为图片
+ const saveImage = () => {
+ Taro.canvasToTempFilePath({
+ canvasId: "radarCanvas",
+ success: (res) => {
+ Taro.saveImageToPhotosAlbum({
+ filePath: res.tempFilePath,
+ success: () => Taro.showToast({ title: "保存成功" }),
+ });
+ },
+ });
+ };
+
+ return (
+
+
+ {/* */}
+
+ );
+};
+
+export default RadarChart;
diff --git a/src/components/index.ts b/src/components/index.ts
index c64e6e9..785769a 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -21,6 +21,7 @@ import GameManagePopup from './GameManagePopup';
import FollowUserCard from './FollowUserCard/index';
import Comments from "./Comments";
import GeneralNavbar from "./GeneralNavbar";
+import RadarChart from './Radar'
export {
ActivityTypeSwitch,
@@ -47,4 +48,5 @@ export {
FollowUserCard,
Comments,
GeneralNavbar,
+ RadarChart,
};
diff --git a/src/other_pages/ntrp-evaluate/index.config.ts b/src/other_pages/ntrp-evaluate/index.config.ts
index 99e9ac1..f001986 100644
--- a/src/other_pages/ntrp-evaluate/index.config.ts
+++ b/src/other_pages/ntrp-evaluate/index.config.ts
@@ -1,5 +1,6 @@
export default definePageConfig({
- navigationBarTitleText: "NTRP 评测",
+ // navigationBarTitleText: "NTRP 评测",
// navigationBarBackgroundColor: '#FAFAFA',
- // navigationStyle: 'custom',
+ navigationStyle: 'custom',
+ enableShareAppMessage: true,
});
diff --git a/src/other_pages/ntrp-evaluate/index.module.scss b/src/other_pages/ntrp-evaluate/index.module.scss
index f66c2da..b9b8ce3 100644
--- a/src/other_pages/ntrp-evaluate/index.module.scss
+++ b/src/other_pages/ntrp-evaluate/index.module.scss
@@ -62,3 +62,503 @@
}
}
}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ height: 44px;
+ padding: 46px 42px 0 10px;
+
+ .closeIcon {
+ width: 32px;
+ height: 32px;
+ margin-right: auto;
+
+ .closeImg {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ .title {
+ flex: 1;
+ margin: auto;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+}
+
+@mixin commonAvatarStyle($multiple: 1) {
+ .avatar {
+ flex: 0 0 auto;
+ width: calc(100px * $multiple);
+ height: calc(100px * $multiple);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: #fff;
+ border-radius: 50%;
+ border: 1px solid #efefef;
+ overflow: hidden;
+ box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.20), 0 8px 20px 0 rgba(0, 0, 0, 0.12);
+
+ .avatarUrl {
+ width: calc(90px * $multiple);
+ height: calc(90px * $multiple);
+ border-radius: 50%;
+ border: 1px solid #efefef;
+ }
+ }
+
+ .addonImage {
+ flex: 0 0 auto;
+ width: calc(88px * $multiple);
+ height: calc(88px * $multiple);
+ transform: rotate(8deg);
+ flex-shrink: 0;
+ aspect-ratio: 1/1;
+ border-radius: calc(20px * $multiple);
+ border: 4px solid #FFF;
+ background: linear-gradient(0deg, rgba(89, 255, 214, 0.20) 0%, rgba(89, 255, 214, 0.20) 100%), #FFF;
+ box-shadow: 0 4px 36px 0 rgba(0, 0, 0, 0.12);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ margin-left: calc(-1 * 20px * $multiple);
+
+ .docImage {
+ width: calc(48px * $multiple);
+ height: calc(48px * $multiple);
+ transform: rotate(-7deg);
+ flex-shrink: 0;
+ }
+ }
+}
+
+.introContainer {
+ width: 100vw;
+ height: 100vh;
+ background: radial-gradient(227.15% 100% at 50% 0%, #BFFFEF 0%, #FFF 36.58%), #FAFAFA;
+
+ .result {
+
+ .avatarWrap {
+ width: 200px;
+ height: 100px;
+ padding: 30px 0 0 30px;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ @include commonAvatarStyle(1);
+ }
+
+ .tip {
+ padding: 0 30px;
+
+ .tipImage {
+ width: 100%;
+ }
+ }
+
+ .lastResult {
+ margin: 40px 22px;
+ display: flex;
+ padding: 16px 20px 20px 20px;
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
+ gap: 8px;
+ border-radius: 16px;
+ border: 1px solid rgba(0, 0, 0, 0.06);
+ background: #FFF;
+ box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.10);
+
+ .tipAndTime {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ color: rgba(0, 0, 0, 0.65);
+ font-feature-settings: 'liga' off, 'clig' off;
+ font-family: "Noto Sans SC";
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 24px;
+ }
+
+ .levelWrap {
+ color: #000;
+ font-feature-settings: 'liga' off, 'clig' off;
+ text-overflow: ellipsis;
+ font-family: "Noto Sans SC";
+ font-size: 32px;
+ font-style: normal;
+ font-weight: 900;
+ line-height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 10px;
+
+ .level {
+ color: #00E5AD;
+ }
+ }
+
+ .slogan {
+ color: #000;
+ font-family: "Noto Sans SC";
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 700;
+ line-height: 22px;
+ }
+ }
+
+ .actions {
+ margin: 0 22px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+
+ .buttonWrap {
+ width: 100%;
+ height: 52px;
+ border-radius: 16px;
+ border: 1px solid rgba(0, 0, 0, 0.06);
+ overflow: hidden;
+
+ .button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ width: 100%;
+ height: 100%;
+ font-feature-settings: 'liga' off, 'clig' off;
+ font-family: "PingFang SC";
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: normal;
+
+ &.primary {
+ color: #fff;
+ background: #000;
+
+ .arrowImage {
+ width: 20px;
+ height: 20px;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ .guide {
+ .tip {
+ padding: 0 30px;
+
+ .tipImage {
+ width: 100%;
+ }
+ }
+
+ .radar {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ .radarImage {
+ width: 320px;
+ transform: scale(1.8);
+ }
+ }
+
+ .desc {
+ padding: 0 30px;
+ color: rgba(0, 0, 0, 0.85);
+ font-family: "PingFang SC";
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 300;
+ line-height: 24px;
+ }
+
+ .actions {
+ margin: 74px 22px 0;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+
+ .buttonWrap {
+ width: 100%;
+ height: 52px;
+ border-radius: 16px;
+ border: 1px solid rgba(0, 0, 0, 0.06);
+ overflow: hidden;
+
+ .button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ width: 100%;
+ height: 100%;
+ font-feature-settings: 'liga' off, 'clig' off;
+ font-family: "PingFang SC";
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: normal;
+
+ &.primary {
+ color: #fff;
+ background: #000;
+
+ .arrowImage {
+ width: 20px;
+ height: 20px;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+.testContainer {
+ width: 100vw;
+ height: 100vh;
+ background: radial-gradient(227.15% 100% at 50% 0%, #BFFFEF 0%, #FFF 36.58%), #FAFAFA;
+
+ .bar {
+ margin: 12px 20px 36px;
+ height: 8px;
+ border-radius: 999px;
+ background: rgba(0, 0, 0, 0.06);
+ position: relative;
+
+ .progressBar {
+ height: 8px;
+ position: absolute;
+ left: 0;
+ top: 0;
+ border-radius: 999px;
+ background-color: #000;
+ }
+ }
+
+ .notice {
+ padding: 0 20px 20px;
+ color: #000;
+ font-family: "PingFang SC";
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 300;
+ line-height: normal;
+ }
+
+ .question {
+ padding: 0 20px 48px;
+ box-sizing: border-box;
+ height: 502px;
+
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: space-between;
+
+ .content {
+ color: #000;
+ font-family: "PingFang SC";
+ font-size: 36px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: normal;
+ }
+
+ .options {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: flex-end;
+ gap: 12px;
+ width: 100%;
+
+ .optionItem {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ display: flex;
+ padding: 14px 20px;
+ align-items: center;
+ gap: 12px;
+ border-radius: 16px;
+ border: 0.5px solid rgba(0, 0, 0, 0.12);
+ background: #fff;
+ width: 100%;
+ box-sizing: border-box;
+
+ .optionText {
+ color: #000;
+ font-feature-settings: 'liga' off, 'clig' off;
+ font-family: "PingFang SC";
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 24px;
+ }
+
+ .optionIcon {
+ display: flex;
+ align-items: center;
+
+ .icon {
+ width: 20px;
+ height: 20px;
+ }
+ }
+ }
+ }
+ }
+
+ .actions {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 0 22px;
+ gap: 24px;
+
+ .next {
+ width: 100%;
+ height: 52px;
+ border-radius: 16px;
+ border: 1px solid rgba(0, 0, 0, 0.06);
+ background: rgba(0, 0, 0, 0.20);
+ box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.10);
+ overflow: hidden;
+
+ .nextBtn {
+ width: 100%;
+ height: 100%;
+ background-color: #000;
+ color: #fff;
+ border-radius: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ &.disabled {
+ border-radius: 16px;
+ border: 1px solid rgba(0, 0, 0, 0.06);
+ background: rgba(0, 0, 0, 0.20);
+ box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.10);
+
+ .nextBtn {
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.20);
+ color: #fff;
+ border-radius: 16px;
+ }
+ }
+ }
+
+ .prev {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ color: #000;
+ font-family: "PingFang SC";
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: normal;
+
+ .backIcon {
+ width: 20px;
+ height: 20px;
+ }
+ }
+ }
+}
+
+.resultContainer {
+ width: 100vw;
+ height: 100vh;
+ background: radial-gradient(227.15% 100% at 50% 0%, #BFFFEF 0%, #FFF 36.58%), #FAFAFA;
+
+ .card {
+ margin: 10px 20px 0;
+
+ padding: 24px 28px 0;
+ position: relative;
+ display: flex;
+ // height: px;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+ align-self: stretch;
+ border-radius: 26px;
+ border: 4px solid #FFF;
+ background: linear-gradient(180deg, #BFFFEF 0%, #F2FFFC 100%), #FFF;
+ box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.10);
+
+ .avatarWrap {
+ padding-bottom: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ @include commonAvatarStyle(0.5);
+ }
+
+ .desc {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+
+ .tip {
+ color: #000;
+ font-family: "PingFang SC";
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 300;
+ line-height: normal;
+ }
+
+ .levelWrap {
+ color: #000;
+ font-feature-settings: 'liga' off, 'clig' off;
+ text-overflow: ellipsis;
+ font-family: "Noto Sans SC";
+ font-size: 36px;
+ font-style: normal;
+ font-weight: 900;
+ line-height: 44px;
+
+ .level {
+ color: #00E5AD;
+ }
+ }
+
+ .slogan {
+ color: #000;
+ font-family: "PingFang SC";
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: normal;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/other_pages/ntrp-evaluate/index.tsx b/src/other_pages/ntrp-evaluate/index.tsx
index fcf5348..df9e8bc 100644
--- a/src/other_pages/ntrp-evaluate/index.tsx
+++ b/src/other_pages/ntrp-evaluate/index.tsx
@@ -1,22 +1,388 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, useRef } from "react";
import { View, Text, Image, Button } from "@tarojs/components";
import Taro, { useRouter } from "@tarojs/taro";
-import { withAuth } from "@/components";
-import evaluateService from "@/services/evaluateService";
-import { useUserActions } from "@/store/userStore";
+import dayjs from "dayjs";
+import classnames from "classnames";
+import { withAuth, RadarChart } from "@/components";
+import evaluateService, {
+ LastTimeTestResult,
+ Question,
+} from "@/services/evaluateService";
+import { useUserInfo, useUserActions } from "@/store/userStore";
import { delay } 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 styles from "./index.module.scss";
+enum StageType {
+ INTRO = "intro",
+ TEST = "test",
+ RESULT = "result",
+}
+
+function CommonGuideBar(props) {
+ const { title, confirm } = props;
+ const { params } = useRouter();
+ const { redirect } = params;
+
+ function handleClose() {
+ //TODO: 二次确认
+ if (confirm) {
+ }
+ Taro.redirectTo({
+ url: redirect ? redirect : "/game_pages/list/index",
+ });
+ }
+
+ return (
+
+
+
+
+
+ {title}
+
+
+ );
+}
+
+function Intro(props) {
+ const { redirect } = props;
+ const [ntrpData, setNtrpData] = useState();
+ const userInfo = useUserInfo();
+ const { fetchUserInfo } = useUserActions();
+ const [ready, setReady] = useState(false);
+
+ 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) {
+ Taro.redirectTo({
+ url: `/other_pages/ntrp-evaluate/index?stage=${type}${
+ type === StageType.RESULT ? `&id=${id}` : ""
+ }${redirect ? `&redirect=${redirect}` : ""}`,
+ });
+ }
+
+ return (
+
+
+ {ntrpData?.has_ntrp_level ? (
+
+
+
+
+
+ {/* avatar side */}
+
+
+
+
+ {/* tip */}
+
+
+
+
+
+ 上次测试结果
+ {lastTestTime}
+
+
+ NTRP
+ {ntrp_level}
+
+
+ 变线+网前,下一步就是赢比赛!
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ {/* tip */}
+
+
+
+ {/* radar */}
+
+
+
+
+
+ NTRP(National Tennis Rating
+ Program)是一种常用的网球水平分级系统,这不是绝对精准的“分数”,而是一个参考标准,能够帮助你更清晰地了解自己的网球水平,从而在训练、比赛或娱乐活动中找到「难度合适」的球友,避免过度碾压或被碾压。
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
+
+function Test(props) {
+ const { redirect } = props;
+ const [disabled, setDisabled] = useState(false);
+ const [index, setIndex] = useState(9);
+ 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
+ }${redirect ? `&redirect=${redirect}` : ""}`,
+ });
+ }
+ } 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 userInfo = useUserInfo();
+ const { fetchUserInfo } = useUserActions();
+
+ useEffect(() => {
+ fetchUserInfo()
+ }, [])
+
+ return (
+
+
+
+
+
+
+
+ {/* avatar side */}
+
+
+
+
+
+
+ 你的 NTRP 测试结果为
+
+
+ NTRP
+ {1.1}
+
+
+ 变线+网前,下一步就是赢比赛!
+
+
+
+
+
+
+
+ );
+}
+
+const ComponentsMap = {
+ [StageType.INTRO]: Intro,
+ [StageType.TEST]: Test,
+ [StageType.RESULT]: Result,
+};
+
function NtrpEvaluate() {
const { updateUserInfo } = useUserActions();
const { params } = useRouter();
const { redirect } = params;
- useEffect(() => {
- evaluateService.getEvaluateQuestions().then((data) => {
- console.log(data);
- });
- }, []);
+ const stage = params.stage as StageType;
async function handleUpdateNtrp() {
await updateUserInfo({
@@ -33,21 +399,9 @@ function NtrpEvaluate() {
}
}
- return (
-
- NTRP评分
-
-
- 您的NTRP评分是 4.0 分。
-
-
-
- );
+ const Component = ComponentsMap[stage];
+
+ return ;
}
export default withAuth(NtrpEvaluate);
diff --git a/src/services/evaluateService.ts b/src/services/evaluateService.ts
index b732212..55e73de 100644
--- a/src/services/evaluateService.ts
+++ b/src/services/evaluateService.ts
@@ -1,71 +1,123 @@
import httpService from "./httpService";
import type { ApiResponse } from "./httpService";
-export interface AnswerResItem {
- question_id: number;
- answer_index: number;
+// 单个选项类型
+interface Option {
+ text: string;
+ score: number;
+}
+
+// 单个问题类型
+export interface Question {
+ id: number;
question_title: string;
+ question_content: string;
+ options: Option[];
+ radar_mapping: string[];
+}
+
+// 单项能力分数
+interface AbilityScore {
+ current_score: number;
+ max_score: number;
+ percentage: number;
+}
+
+// 雷达图数据
+interface RadarData {
+ abilities: Record; // key 是能力名称,如 "正手球质"
+ summary: {
+ total_questions: number;
+ calculation_time: string; // ISO 字符串
+ };
+}
+
+// 单题答案
+interface Answer {
+ question_id: number;
+ question_title: string;
+ answer_index: number;
selected_option: string;
score: number;
}
-export type AnswerItem = Pick;
-
-export interface Answers {
- answers: AnswerItem[];
- test_duration: number;
-}
-
-export interface QuestionItem {
- id: number;
- question_title: string;
- question_content: string;
- options: string[];
- scores: number[];
-}
-
-export interface SubmitAnswerRes {
+// 提交测试结果 对象类型
+export interface TestResultData {
record_id: number;
total_score: number;
ntrp_level: string;
+ is_coverage: boolean;
+ old_ntrp_level: string;
level_description: string;
- answers: AnswerResItem[];
+ radar_data: RadarData;
+ answers: Answer[];
+}
+
+// 单条测试记录
+interface TestRecord {
+ id: number;
+ total_score: number;
+ ntrp_level: string;
+ level_description: string;
+ test_duration: number; // 单位:秒
+ create_time: string; // 时间字符串
+}
+
+// 测试历史对象类型
+export interface TestResultList {
+ count: number;
+ rows: TestRecord[];
+}
+
+// 单次测试结果
+interface TestResult {
+ id: number;
+ total_score: number;
+ ntrp_level: string;
+ level_description: string;
+ radar_data: RadarData;
+ test_duration: number; // 单位秒
+ create_time: string; // 时间字符串
+}
+
+// data 对象
+// 上一次测试结果
+export interface LastTimeTestResult {
+ has_test_record: boolean;
+ has_ntrp_level: boolean;
+ user_ntrp_level: string;
+ last_test_result: TestResult;
}
-// 发布球局类
class EvaluateService {
- async getEvaluateQuestions(): Promise> {
- return httpService.post("/ntrp/questions", {
- showLoading: true,
- });
+ // 获取测试题目
+ async getQuestions(): Promise> {
+ return httpService.post("/ntrp/questions", {}, { showLoading: true });
}
- async submitEvaluateAnswers({
- answers,
- }: Answers): Promise> {
- return httpService.post(
- "/ntrp/submit",
- { answers },
- {
- showLoading: true,
- },
- );
+ // 提交答案
+ async submit(req: { answers: { question_id: number, answer_index: number }[], test_duration: number }): Promise> {
+ return httpService.post("/ntrp/submit", req, { showLoading: true });
}
- async getHistoryNtrp(): Promise> {
- return httpService.post("/ntrp/history", {
- showLoading: true,
- });
+ // 获取测试历史
+ async getResultList(): Promise> {
+ return httpService.post("/ntrp/history", {}, { showLoading: true });
}
- async getNtrpDetail(record_id: number): Promise> {
- return httpService.post(
- "/ntrp/detail",
- { record_id },
- {
- showLoading: true,
- },
- );
+ // 获取测试详情
+ async getTestResult(req: { record_id: number }): Promise> {
+ return httpService.post("/ntrp/detail", req, { showLoading: true });
+ }
+
+ // 获取最后一次(最新)测试结果
+ async getLastResult(): Promise> {
+ return httpService.post("/ntrp/last_result", {}, { showLoading: true });
+ }
+
+ // 更新NTRP等级
+ async updateNtrp(req: { record_id: number, ntrp_level: string, update_type: string }): Promise> {
+ return httpService.post("/ntrp/update_user_level", req, { showLoading: true });
}
}
diff --git a/src/static/ntrp/ntrp_arrow_back.svg b/src/static/ntrp/ntrp_arrow_back.svg
new file mode 100644
index 0000000..56e00d9
--- /dev/null
+++ b/src/static/ntrp/ntrp_arrow_back.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/static/ntrp/ntrp_arrow_right.svg b/src/static/ntrp/ntrp_arrow_right.svg
new file mode 100644
index 0000000..ffb626a
--- /dev/null
+++ b/src/static/ntrp/ntrp_arrow_right.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/static/ntrp/ntrp_circle_checked.svg b/src/static/ntrp/ntrp_circle_checked.svg
new file mode 100644
index 0000000..0f06ec4
--- /dev/null
+++ b/src/static/ntrp/ntrp_circle_checked.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/static/ntrp/ntrp_circle_unchecked.svg b/src/static/ntrp/ntrp_circle_unchecked.svg
new file mode 100644
index 0000000..038c3f1
--- /dev/null
+++ b/src/static/ntrp/ntrp_circle_unchecked.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/static/ntrp/ntrp_close_icon.svg b/src/static/ntrp/ntrp_close_icon.svg
new file mode 100644
index 0000000..496e8dc
--- /dev/null
+++ b/src/static/ntrp/ntrp_close_icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/static/ntrp/ntrp_doc_copy.svg b/src/static/ntrp/ntrp_doc_copy.svg
new file mode 100644
index 0000000..f0cc77d
--- /dev/null
+++ b/src/static/ntrp/ntrp_doc_copy.svg
@@ -0,0 +1,12 @@
+