feat: 问卷调查
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ node_modules/
|
||||
.swc
|
||||
src/config/env.ts
|
||||
.vscode
|
||||
*.http
|
||||
|
||||
183
src/components/Radar/index.tsx
Normal file
183
src/components/Radar/index.tsx
Normal file
@@ -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 (
|
||||
<View>
|
||||
<Canvas
|
||||
type="2d"
|
||||
id="radarCanvas"
|
||||
style={{ width: "320px", height: "320px", background: "transparent" }}
|
||||
/>
|
||||
{/* <Button onClick={saveImage}>保存为图片</Button> */}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default RadarChart;
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: "NTRP 评测",
|
||||
// navigationBarTitleText: "NTRP 评测",
|
||||
// navigationBarBackgroundColor: '#FAFAFA',
|
||||
// navigationStyle: 'custom',
|
||||
navigationStyle: 'custom',
|
||||
enableShareAppMessage: true,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<View className={styles.header}>
|
||||
<View className={styles.closeIcon} onClick={handleClose}>
|
||||
<Image className={styles.closeImg} src={CloseIcon} />
|
||||
</View>
|
||||
<View className={styles.title}>
|
||||
<Text>{title}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function Intro(props) {
|
||||
const { redirect } = props;
|
||||
const [ntrpData, setNtrpData] = useState<LastTimeTestResult>();
|
||||
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 (
|
||||
<View className={styles.introContainer}>
|
||||
<CommonGuideBar />
|
||||
{ntrpData?.has_ntrp_level ? (
|
||||
<View className={styles.result}>
|
||||
<View className={styles.avatarWrap}>
|
||||
<View className={styles.avatar}>
|
||||
<Image
|
||||
className={styles.avatarUrl}
|
||||
src={userInfo.avatar_url}
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</View>
|
||||
{/* avatar side */}
|
||||
<View className={styles.addonImage}>
|
||||
<Image
|
||||
className={styles.docImage}
|
||||
src={DocCopy}
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{/* tip */}
|
||||
<View className={styles.tip}>
|
||||
<Image
|
||||
className={styles.tipImage}
|
||||
src="http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/b7cb47aa-b609-4112-899f-3fde02ed2431.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</View>
|
||||
<View className={styles.lastResult}>
|
||||
<View className={styles.tipAndTime}>
|
||||
<Text>上次测试结果</Text>
|
||||
<Text>{lastTestTime}</Text>
|
||||
</View>
|
||||
<View className={styles.levelWrap}>
|
||||
<Text>NTRP</Text>
|
||||
<Text className={styles.level}>{ntrp_level}</Text>
|
||||
</View>
|
||||
<View className={styles.slogan}>
|
||||
<Text>变线+网前,下一步就是赢比赛!</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.actions}>
|
||||
<View className={styles.buttonWrap}>
|
||||
<Button
|
||||
className={classnames(styles.button, styles.primary)}
|
||||
type="primary"
|
||||
onClick={() => handleNext(StageType.TEST)}
|
||||
>
|
||||
<Text>再次测试</Text>
|
||||
<Image className={styles.arrowImage} src={ArrowRight} />
|
||||
</Button>
|
||||
</View>
|
||||
<View className={styles.buttonWrap}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
onClick={() => handleNext(StageType.RESULT)}
|
||||
>
|
||||
<Text>继续使用上次测试结果</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View className={styles.guide}>
|
||||
{/* tip */}
|
||||
<View className={styles.tip}>
|
||||
<Image
|
||||
className={styles.tipImage}
|
||||
src="http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/b7cb47aa-b609-4112-899f-3fde02ed2431.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</View>
|
||||
{/* radar */}
|
||||
<View className={styles.radar}>
|
||||
<Image
|
||||
className={styles.radarImage}
|
||||
src="http://bimwe.oss-cn-shanghai.aliyuncs.com/front/ball/images/a2e1b639-82a9-4ab8-b767-8605556eafcb.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</View>
|
||||
<View className={styles.desc}>
|
||||
<Text>
|
||||
NTRP(National Tennis Rating
|
||||
Program)是一种常用的网球水平分级系统,这不是绝对精准的“分数”,而是一个参考标准,能够帮助你更清晰地了解自己的网球水平,从而在训练、比赛或娱乐活动中找到「难度合适」的球友,避免过度碾压或被碾压。
|
||||
</Text>
|
||||
</View>
|
||||
<View className={styles.actions}>
|
||||
<View className={styles.buttonWrap}>
|
||||
<Button
|
||||
className={classnames(styles.button, styles.primary)}
|
||||
type="primary"
|
||||
onClick={() => handleNext(StageType.TEST)}
|
||||
>
|
||||
<Text>开始测试</Text>
|
||||
<Image className={styles.arrowImage} src={ArrowRight} />
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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<number>(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 (
|
||||
<View className={styles.testContainer}>
|
||||
<CommonGuideBar confirm title={`${index + 1} / ${questions.length}`} />
|
||||
<View className={styles.bar}>
|
||||
<View
|
||||
className={styles.progressBar}
|
||||
style={{ width: `${100 * ((index + 1) / questions.length)}%` }}
|
||||
/>
|
||||
</View>
|
||||
<View className={styles.notice}>
|
||||
<Text>根据近3个月实际表现勾选最符合项</Text>
|
||||
</View>
|
||||
<View className={styles.question}>
|
||||
<View className={styles.content}>{question.question_content}</View>
|
||||
<View className={styles.options}>
|
||||
{question.options.map((item, i) => {
|
||||
const checked = question.choosen === i;
|
||||
return (
|
||||
<View
|
||||
key={i}
|
||||
className={styles.optionItem}
|
||||
onClick={() => handleSelect(i)}
|
||||
>
|
||||
<View className={styles.optionText}>{item.text}</View>
|
||||
<View className={styles.optionIcon}>
|
||||
<Image
|
||||
className={styles.icon}
|
||||
src={checked ? CircleChecked : CircleUnChecked}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.actions}>
|
||||
<View
|
||||
className={classnames(styles.next, disabled ? styles.disabled : "")}
|
||||
onClick={() => handIndexChange(1)}
|
||||
>
|
||||
<Button className={styles.nextBtn} type="primary">
|
||||
{index === questions.length - 1 ? "完成测试" : "继续"}
|
||||
</Button>
|
||||
</View>
|
||||
{index !== 0 && (
|
||||
<View className={styles.prev} onClick={() => handIndexChange(-1)}>
|
||||
<Image className={styles.backIcon} src={ArrowBack} />
|
||||
<Text>返回</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function Result() {
|
||||
const userInfo = useUserInfo();
|
||||
const { fetchUserInfo } = useUserActions();
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserInfo()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View className={styles.resultContainer}>
|
||||
<CommonGuideBar />
|
||||
<View className={styles.card}>
|
||||
<View className={styles.avatarWrap}>
|
||||
<View className={styles.avatar}>
|
||||
<Image
|
||||
className={styles.avatarUrl}
|
||||
src={userInfo.avatar_url}
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</View>
|
||||
{/* avatar side */}
|
||||
<View className={styles.addonImage}>
|
||||
<Image
|
||||
className={styles.docImage}
|
||||
src={DocCopy}
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.desc}>
|
||||
<View className={styles.tip}>
|
||||
<Text>你的 NTRP 测试结果为</Text>
|
||||
</View>
|
||||
<View className={styles.levelWrap}>
|
||||
<Text>NTRP</Text>
|
||||
<Text className={styles.level}>{1.1}</Text>
|
||||
</View>
|
||||
<View className={styles.slogan}>
|
||||
<Text>变线+网前,下一步就是赢比赛!</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View>
|
||||
<RadarChart />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<View className={styles.container}>
|
||||
<View className={styles.title}>NTRP评分</View>
|
||||
<View className={styles.content}>
|
||||
<Image
|
||||
className={styles.image}
|
||||
src="https://img.yzcdn.cn/vant/cat.jpeg"
|
||||
/>
|
||||
<Text className={styles.description}>您的NTRP评分是 4.0 分。</Text>
|
||||
</View>
|
||||
<Button className={styles.button} onClick={handleUpdateNtrp}>
|
||||
评测通过
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
const Component = ComponentsMap[stage];
|
||||
|
||||
return <Component redirect={redirect} />;
|
||||
}
|
||||
|
||||
export default withAuth(NtrpEvaluate);
|
||||
|
||||
@@ -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<string, AbilityScore>; // 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<AnswerResItem, "question_id" | "answer_index">;
|
||||
|
||||
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<ApiResponse<QuestionItem[]>> {
|
||||
return httpService.post("/ntrp/questions", {
|
||||
showLoading: true,
|
||||
});
|
||||
// 获取测试题目
|
||||
async getQuestions(): Promise<ApiResponse<Question[]>> {
|
||||
return httpService.post("/ntrp/questions", {}, { showLoading: true });
|
||||
}
|
||||
|
||||
async submitEvaluateAnswers({
|
||||
answers,
|
||||
}: Answers): Promise<ApiResponse<SubmitAnswerRes>> {
|
||||
return httpService.post(
|
||||
"/ntrp/submit",
|
||||
{ answers },
|
||||
{
|
||||
showLoading: true,
|
||||
},
|
||||
);
|
||||
// 提交答案
|
||||
async submit(req: { answers: { question_id: number, answer_index: number }[], test_duration: number }): Promise<ApiResponse<TestResultData>> {
|
||||
return httpService.post("/ntrp/submit", req, { showLoading: true });
|
||||
}
|
||||
|
||||
async getHistoryNtrp(): Promise<ApiResponse<any>> {
|
||||
return httpService.post("/ntrp/history", {
|
||||
showLoading: true,
|
||||
});
|
||||
// 获取测试历史
|
||||
async getResultList(): Promise<ApiResponse<TestResultList>> {
|
||||
return httpService.post("/ntrp/history", {}, { showLoading: true });
|
||||
}
|
||||
|
||||
async getNtrpDetail(record_id: number): Promise<ApiResponse<any>> {
|
||||
return httpService.post(
|
||||
"/ntrp/detail",
|
||||
{ record_id },
|
||||
{
|
||||
showLoading: true,
|
||||
},
|
||||
);
|
||||
// 获取测试详情
|
||||
async getTestResult(req: { record_id: number }): Promise<ApiResponse<TestResultData>> {
|
||||
return httpService.post("/ntrp/detail", req, { showLoading: true });
|
||||
}
|
||||
|
||||
// 获取最后一次(最新)测试结果
|
||||
async getLastResult(): Promise<ApiResponse<LastTimeTestResult>> {
|
||||
return httpService.post("/ntrp/last_result", {}, { showLoading: true });
|
||||
}
|
||||
|
||||
// 更新NTRP等级
|
||||
async updateNtrp(req: { record_id: number, ntrp_level: string, update_type: string }): Promise<ApiResponse<any>> {
|
||||
return httpService.post("/ntrp/update_user_level", req, { showLoading: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
4
src/static/ntrp/ntrp_arrow_back.svg
Normal file
4
src/static/ntrp/ntrp_arrow_back.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.50002 10L17.5 10" stroke="black" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.5 15L2.5 10L7.5 5" stroke="black" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 338 B |
4
src/static/ntrp/ntrp_arrow_right.svg
Normal file
4
src/static/ntrp/ntrp_arrow_right.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.5 10H2.5" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12.5 5L17.5 10L12.5 15" stroke="white" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 334 B |
4
src/static/ntrp/ntrp_circle_checked.svg
Normal file
4
src/static/ntrp/ntrp_circle_checked.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="10" cy="10" r="10" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.36695 10.343C5.75135 9.94642 6.38444 9.93658 6.78099 10.321L8.84309 12.3199L13.1831 6.17329C13.5016 5.72214 14.1256 5.61464 14.5768 5.93319C15.0279 6.25174 15.1354 6.87571 14.8169 7.32687L9.80293 14.428C9.6344 14.6667 9.3699 14.8197 9.07898 14.8469C8.78805 14.8741 8.4998 14.7726 8.29001 14.5692L5.38894 11.757C4.99239 11.3726 4.98254 10.7395 5.36695 10.343Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 576 B |
3
src/static/ntrp/ntrp_circle_unchecked.svg
Normal file
3
src/static/ntrp/ntrp_circle_unchecked.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="10" cy="10" r="9.25" stroke="#161823" stroke-opacity="0.34" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 196 B |
4
src/static/ntrp/ntrp_close_icon.svg
Normal file
4
src/static/ntrp/ntrp_close_icon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.33325 9.33334L22.6666 22.6667" stroke="black" stroke-width="2.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.33325 22.6667L22.6666 9.33334" stroke="black" stroke-width="2.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 363 B |
12
src/static/ntrp/ntrp_doc_copy.svg
Normal file
12
src/static/ntrp/ntrp_doc_copy.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg width="55" height="55" viewBox="0 0 55 55" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4962_28921)">
|
||||
<path d="M14.462 7.25007C14.6157 6.15625 15.6271 5.39415 16.7209 5.54788L36.5263 8.33134L45.0372 19.6258L41.1404 47.3533C40.9866 48.4471 39.9753 49.2092 38.8815 49.0554L11.154 45.1586C10.0601 45.0049 9.29805 43.9936 9.45178 42.8997L14.462 7.25007Z" stroke="#00E6AD" stroke-width="4" stroke-linejoin="round"/>
|
||||
<path d="M20.4364 22.2275L36.2807 24.4543" stroke="#00E6AD" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19.3226 30.1494L35.1669 32.3762" stroke="#00E6AD" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4962_28921">
|
||||
<rect width="48" height="48" fill="white" transform="translate(7.37561 0.195312) rotate(8)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 862 B |
Reference in New Issue
Block a user