From 3cc2a24be51fb0d4cca4fa9045d791570ec358ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=9D=B0?= Date: Fri, 26 Sep 2025 15:31:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=97=AE=E5=8D=B7=E8=B0=83=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + src/components/Radar/index.tsx | 183 +++++++ src/components/index.ts | 2 + src/other_pages/ntrp-evaluate/index.config.ts | 5 +- .../ntrp-evaluate/index.module.scss | 500 ++++++++++++++++++ src/other_pages/ntrp-evaluate/index.tsx | 402 +++++++++++++- src/services/evaluateService.ts | 146 +++-- src/static/ntrp/ntrp_arrow_back.svg | 4 + src/static/ntrp/ntrp_arrow_right.svg | 4 + src/static/ntrp/ntrp_circle_checked.svg | 4 + src/static/ntrp/ntrp_circle_unchecked.svg | 3 + src/static/ntrp/ntrp_close_icon.svg | 4 + src/static/ntrp/ntrp_doc_copy.svg | 12 + 13 files changed, 1197 insertions(+), 73 deletions(-) create mode 100644 src/components/Radar/index.tsx create mode 100644 src/static/ntrp/ntrp_arrow_back.svg create mode 100644 src/static/ntrp/ntrp_arrow_right.svg create mode 100644 src/static/ntrp/ntrp_circle_checked.svg create mode 100644 src/static/ntrp/ntrp_circle_unchecked.svg create mode 100644 src/static/ntrp/ntrp_close_icon.svg create mode 100644 src/static/ntrp/ntrp_doc_copy.svg 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 @@ + + + + + + + + + + + +