diff --git a/src/components/Radar/index.tsx b/src/components/Radar/index.tsx
index 9a325ca..00d2556 100644
--- a/src/components/Radar/index.tsx
+++ b/src/components/Radar/index.tsx
@@ -1,8 +1,8 @@
import Taro, { useReady } from "@tarojs/taro";
import { View, Canvas, Button } from "@tarojs/components";
-import { useEffect, useRef } from "react";
+import { useEffect, useRef, forwardRef, useImperativeHandle } from "react";
-const RadarChart: React.FC = (props) => {
+const RadarChart: React.FC = forwardRef((props, ref) => {
const { data } = props
const renderFnRef = useRef()
@@ -118,6 +118,23 @@ const RadarChart: React.FC = (props) => {
});
}
+ useImperativeHandle(ref, () => ({
+ generateImage: () => new Promise((resolve, reject) => {
+ const query = Taro.createSelectorQuery()
+ query.select("#radarCanvas")
+ .fields({ node: true, size: true })
+ .exec((res) => {
+ const canvas = res[0].node
+ // ⚠️ 关键:传 canvas,而不是 canvasId
+ Taro.canvasToTempFilePath({
+ canvas,
+ success: (res) => resolve(res.tempFilePath),
+ fail: (err) => reject(err),
+ })
+ })
+ })
+ }))
+
// 保存为图片
const saveImage = () => {
@@ -142,6 +159,6 @@ const RadarChart: React.FC = (props) => {
{/* */}
);
-};
+});
export default RadarChart;
diff --git a/src/other_pages/ntrp-evaluate/index.module.scss b/src/other_pages/ntrp-evaluate/index.module.scss
index ccb34f6..122d07a 100644
--- a/src/other_pages/ntrp-evaluate/index.module.scss
+++ b/src/other_pages/ntrp-evaluate/index.module.scss
@@ -564,5 +564,123 @@
line-height: normal;
}
}
+
+ .retest {
+ position: absolute;
+ right: 12px;
+ top: 12px;
+ display: flex;
+ padding: 6px 10px;
+ justify-content: center;
+ align-items: center;
+ gap: 6px;
+ border-radius: 12px;
+ border: 0.5px solid rgba(0, 0, 0, 0.12);
+ background: #FFF;
+ box-shadow: 0 8px 64px 0 rgba(0, 0, 0, 0.10);
+ color: rgba(0, 0, 0, 0.85);
+ font-feature-settings: 'liga' off, 'clig' off;
+ font-family: "PingFang SC";
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 20px;
+
+ .re_actIcon {
+ width: 12px;
+ height: 12px;
+ }
+ }
+ }
+
+ .updateTip {
+ color: #000;
+ font-feature-settings: 'liga' off, 'clig' off;
+ font-family: "PingFang SC";
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 24px;
+
+ text-align: center;
+ padding: 24px 0;
+
+ .grayTip {
+ color: rgba(60, 60, 67, 0.60);
+ }
+ }
+
+ .actions {
+ padding: 0 28px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 10px;
+
+ .viewGame {
+ border-radius: 16px;
+ border: 1px solid rgba(0, 0, 0, 0.06);
+ width: 100%;
+ overflow: hidden;
+
+ .viewGameBtn {
+ width: 100%;
+ height: 50px;
+ background: #000;
+ color: #FFF;
+ font-feature-settings: 'liga' off, 'clig' off;
+ font-family: "PingFang SC";
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ }
+
+ .otherActions {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+
+ .share, .saveImage {
+ width: 50%;
+ height: 50px;
+ border-radius: 16px;
+ border: 1px solid rgba(0, 0, 0, 0.06);
+ overflow: hidden;
+
+ .shareBtn, .saveImageBtn {
+ background: #FFF;
+ width: 100%;
+ height: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ color: #000;
+ font-feature-settings: 'liga' off, 'clig' off;
+ font-family: "PingFang SC";
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: normal;
+
+ .downloadIcon {
+ width: 20px;
+ height: 20px;
+ }
+
+ .wechatIcon {
+ width: 24px;
+ height: 24px;
+ }
+ }
+ }
+ }
}
}
\ 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 a603d9d..42d4396 100644
--- a/src/other_pages/ntrp-evaluate/index.tsx
+++ b/src/other_pages/ntrp-evaluate/index.tsx
@@ -1,6 +1,6 @@
-import { useState, useEffect, useRef } from "react";
-import { View, Text, Image, Button } from "@tarojs/components";
-import Taro, { useRouter } from "@tarojs/taro";
+import { useState, useEffect, useRef, useId } 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";
@@ -10,13 +10,16 @@ import evaluateService, {
TestResultData,
} from "@/services/evaluateService";
import { useUserInfo, useUserActions } from "@/store/userStore";
-import { delay } from "@/utils";
+import { delay, getCurrentFullPath } 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 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";
enum StageType {
@@ -25,6 +28,48 @@ enum StageType {
RESULT = "result",
}
+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 CommonGuideBar(props) {
const { title, confirm } = props;
const { params } = useRouter();
@@ -197,7 +242,7 @@ function Intro(props) {
function Test(props) {
const { redirect } = props;
const [disabled, setDisabled] = useState(false);
- const [index, setIndex] = useState(9);
+ const [index, setIndex] = useState(0);
const [questions, setQuestions] = useState<
(Question & { choosen: number })[]
>([]);
@@ -323,14 +368,18 @@ function Test(props) {
);
}
-function Result() {
+function Result(props) {
+ const { redirect } = props;
const { params } = useRouter();
const { id } = params;
const userInfo = useUserInfo();
const { fetchUserInfo } = useUserActions();
+ const radarRef = useRef();
const [result, setResult] = useState();
- const [radarData, setRadarData] = useState<[propName: string, prop: number][]>([])
+ const [radarData, setRadarData] = useState<
+ [propName: string, prop: number][]
+ >([]);
useEffect(() => {
getResultById();
@@ -341,11 +390,121 @@ function Result() {
const res = await evaluateService.getTestResult({ record_id: Number(id) });
if (res.code === 0) {
setResult(res.data);
- setRadarData(Object.entries(res.data.radar_data.abilities).map(([key, value]) => [key, value.current_score]))
+ setRadarData(
+ adjustRadarLabels(
+ Object.entries(res.data.radar_data.abilities).map(([key, value]) => [
+ key,
+ value.current_score,
+ ])
+ )
+ );
+ updateUserLevel(res.data.record_id, res.data.ntrp_level);
}
}
- console.log(result, "result");
+ 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() {
+ Taro.redirectTo({
+ url: `/other_pages/ntrp-evaluate/index?stage=${StageType.TEST}${
+ redirect ? `&redirect=${redirect}` : ""
+ }`,
+ });
+ }
+
+ function handleViewGames() {
+ Taro.redirectTo({
+ url: "/game_pages/list/index",
+ });
+ }
+
+ 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.getSystemInfoSync().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 ${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() {
+ if (!userInfo.id) {
+ return
+ }
+ const url = await genCardImage();
+ Taro.saveImageToPhotosAlbum({ filePath: url });
+ }
+
+ useShareAppMessage(async (res) => {
+ const url = await genCardImage();
+ console.log(res, "res");
+ return {
+ title: "分享",
+ imageUrl: url,
+ path: `/other_pages/ntrp-evaluate/index?stage=${StageType.INTRO}`,
+ };
+ });
+
+ function handleAuth () {
+ if (userInfo.id) {
+ return true
+ }
+ const currentPage = getCurrentFullPath()
+ Taro.redirectTo({
+ url: `/login_pages/index/index${
+ currentPage ? `?redirect=${encodeURIComponent(currentPage)}` : ""
+ }`,
+ });
+ }
return (
@@ -381,9 +540,52 @@ function Result() {
-
+
+
+
+
+ 重新测试
+ {userInfo.id ? (
+
+ 你的 NTRP 水平已更新为 {result?.ntrp_level}
+ (可在个人信息中修改)
+
+ ) : (
+
+ 登录「有场」小程序,查看匹配你的球局
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/static/ntrp/ntrp_download.svg b/src/static/ntrp/ntrp_download.svg
new file mode 100644
index 0000000..1713295
--- /dev/null
+++ b/src/static/ntrp/ntrp_download.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/static/ntrp/ntrp_re-action.svg b/src/static/ntrp/ntrp_re-action.svg
new file mode 100644
index 0000000..66590de
--- /dev/null
+++ b/src/static/ntrp/ntrp_re-action.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/static/ntrp/ntrp_wechat.svg b/src/static/ntrp/ntrp_wechat.svg
new file mode 100644
index 0000000..217b421
--- /dev/null
+++ b/src/static/ntrp/ntrp_wechat.svg
@@ -0,0 +1,22 @@
+
diff --git a/src/store/userStore.ts b/src/store/userStore.ts
index 7ace708..de00e80 100644
--- a/src/store/userStore.ts
+++ b/src/store/userStore.ts
@@ -14,9 +14,11 @@ export interface UserState {
export const useUser = create()((set) => ({
user: {},
fetchUserInfo: async () => {
- const res = await fetchUserProfile();
- console.log(res);
- set({ user: res.data });
+ try {
+ const res = await fetchUserProfile();
+ console.log(res);
+ set({ user: res.data });
+ } catch {}
},
updateUserInfo: async (userInfo: Partial) => {
const res = await updateUserProfile(userInfo);