乐于分享
好东西不私藏

游标卡尺读数练习微信小程序源码

游标卡尺读数练习微信小程序源码

去年沉默AI编程研究了以下游标卡尺微信小程序,想上架但有一些漏洞没解决,保存在本地又怕到时候想继续研究的时候找不到,所以决定分享出来,后续有时间再折腾。
  • .js
    // 游标卡尺练习小程序 - 核心逻辑页(误差容错版)Page({    data: {      __route__: "",      canvasContext: null,      canvasWidth: 0,      canvasHeight: 240,      pixelsPerMM: 0,      localRange: 50,      caliperOffsetX: 0,      caliperScale: 1.0,      touchState: {        isTouching: false,        touchType: '',        startX: 0,        startOffsetX: 0,        startDistance: 0,        startScale: 0      },      currentQuestion: {        mainRuler: 0,        alignIndex: 0,        mainScalePosition: 0,        vernierScalePosition: 0,        vernierLengthMM: 0,        divisions: 10,        specialType: false      },      currentReading: 0,      currentReadingStr: "",      currentUnit: "mm",      currentPrecision: 1,      requiredPrecisionValue: 0.1,      unitHint: "请输入读数(单位:mm,仅输入数字)",      userInput: "",      feedback: "",      feedbackType: "",      isChallengeMode: false,      timeLeft: 100,      score: 0,      questionCount: 0,      maxQuestions: 5,      timer: null,      currentDivisions: 10,      currentSpecialType: false,      correctStreak: 0,      maxStreak: 0,      errorCount: 0,      divisionOptions: [        { name: "10分度", divisions: 10, specialType: false, step: 0.9, accuracy: 0.1 },        { name: "20分度", divisions: 20, specialType: false, step: 0.95, accuracy: 0.05 },        { name: "特殊20分度", divisions: 20, specialType: true, step: 0.95, accuracy: 0.05 },        { name: "50分度", divisions: 50, specialType: false, step: 0.98, accuracy: 0.02 }      ],      selectedDivisionIndex: 0,      showAlignLine: true    },    toggleAlignLine() {      if (this.data.isChallengeMode) {        wx.showToast({          title: "挑战模式下隐藏对齐线",          icon: "none",          duration: 1500        });        return;      }      this.setData({        showAlignLine: !this.data.showAlignLine      }, () => {        this.redrawCaliper(true);      });    },    confirmExitChallenge() {      return new Promise((resolve) => {        wx.showModal({          title: "退出挑战确认",          content: "挑战模式中途退出将清空当前进度,是否确定退出?",          confirmText: "确定退出",          cancelText: "继续挑战",          success: (res) => {            if (res.confirm) {              if (this.data.timer) {                clearInterval(this.data.timer);                this.setData({ timer: null });              }              this.setData({                isChallengeMode: false,                timeLeft: 100,                score: 0,                questionCount: 0,                correctStreak: 0,                maxStreak: 0,                errorCount: 0,                showAlignLine: true              });              resolve(true);            } else {              resolve(false);            }          }        });      });    },    onDivisionChange(e) {      if (this.data.isChallengeMode) {        this.confirmExitChallenge().then((isExit) => {          if (!isExit) {            return;          }          const index = e.detail.value;          const selectedOption = this.data.divisionOptions[index];          this.setData({            selectedDivisionIndex: index,            currentDivisions: selectedOption.divisions,            currentSpecialType: selectedOption.specialType,            errorCount: 0,            isChallengeMode: false          }, () => {            this.startExercise(selectedOption.divisions, selectedOption.specialType);          });        });      } else {        const index = e.detail.value;        const selectedOption = this.data.divisionOptions[index];        this.setData({          selectedDivisionIndex: index,          currentDivisions: selectedOption.divisions,          currentSpecialType: selectedOption.specialType,          errorCount: 0,          isChallengeMode: false        }, () => {          this.startExercise(selectedOption.divisions, selectedOption.specialType);        });      }    },    onBackPress(options) {      if (this.data.isChallengeMode && options.from === 'backbutton') {        this.confirmExitChallenge();        return true;      }      return false;    },    enforceAccuracy(num, accuracy) {      return Math.round(num / accuracy) * accuracy;    },    formatNumber(num, decimalPlaces) {      return parseFloat(num).toFixed(decimalPlaces);    },    initCanvas() {      wx.showLoading({ title: "初始化卡尺..." });      try {        const systemInfo = wx.getSystemInfoSync();        const screenWidth = systemInfo.screenWidth;        const canvasDisplayWidth = screenWidth * 2;        const canvasDisplayHeight = 240;        const pixelsPerMM = canvasDisplayWidth / (this.data.localRange * 2);        const ctx = wx.createCanvasContext('caliperCanvas'this);        if (!ctx) throw new Error("创建Canvas上下文失败");        this.setData({          canvasContext: ctx,          canvasWidth: canvasDisplayWidth,          canvasHeight: canvasDisplayHeight,          pixelsPerMM        });        wx.hideLoading();        this.start10();      } catch (e) {        wx.hideLoading();        console.error("Canvas初始化失败:", e);        wx.showToast({ title: "卡尺初始化成功(兼容模式)", icon: "none" });        this.start10();      }    },    getTouchDistance(touches) {      if (touches.length < 2return 0;      const x1 = touches[0].clientX, y1 = touches[0].clientY;      const x2 = touches[1].clientX, y2 = touches[1].clientY;      return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));    },    handleTouchStart(e) {      const touches = e.touches;      const touchState = { ...this.data.touchState };      if (touches.length === 1) {        touchState.isTouching = true;        touchState.touchType = 'single';        touchState.startX = touches[0].clientX;        touchState.startOffsetX = this.data.caliperOffsetX;      } else if (touches.length === 2) {        touchState.isTouching = true;        touchState.touchType = 'double';        touchState.startDistance = this.getTouchDistance(touches);        touchState.startScale = this.data.caliperScale;      }      this.setData({ touchState });    },    handleTouchMove(e) {      const { touchState, caliperScale, canvasWidth, localRange, pixelsPerMM } = this.data;      if (!touchState.isTouching || !this.data.canvasContext) return;      if (this.lastMoveTime && Date.now() - this.lastMoveTime < 5return;      this.lastMoveTime = Date.now();      const touches = e.touches;      if (touchState.touchType === 'single' && touches.length === 1) {        const moveX = touches[0].clientX - touchState.startX;        const maxOffset = ((200 - localRange * 2) * pixelsPerMM * caliperScale) * 2;        const newOffsetX = Math.max(-maxOffset, Math.min(maxOffset, touchState.startOffsetX + moveX));        this.setData({ caliperOffsetX: newOffsetX }, () => this.redrawCaliper(true));      } else if (touchState.touchType === 'double' && touches.length === 2) {        const currentDistance = this.getTouchDistance(touches);        const scaleRatio = currentDistance / touchState.startDistance;        let newScale = touchState.startScale * scaleRatio;        newScale = Math.max(0.8, Math.min(3.0, newScale));        this.setData({ caliperScale: newScale }, () => this.redrawCaliper(true));      }    },    handleTouchEnd(e) {      const touchState = { ...this.data.touchState };      touchState.isTouching = false;      touchState.touchType = '';      this.setData({ touchState });      this.lastMoveTime = 0;    },    zoomIn() {      let newScale = this.data.caliperScale + 0.1;      newScale = Math.min(3.0, newScale);      this.setData({ caliperScale: newScale }, () => this.redrawCaliper(true));    },    zoomOut() {      let newScale = this.data.caliperScale - 0.1;      newScale = Math.max(0.8, newScale);      this.setData({ caliperScale: newScale }, () => this.redrawCaliper(true));    },    moveLeft() {      const { caliperOffsetX, caliperScale, localRange, pixelsPerMM } = this.data;      const maxOffset = ((200 - localRange * 2) * pixelsPerMM * caliperScale) * 2;      const newOffsetX = Math.max(-maxOffset, caliperOffsetX - 50);      this.setData({ caliperOffsetX: newOffsetX }, () => this.redrawCaliper(true));    },    moveRight() {      const { caliperOffsetX, caliperScale, localRange, pixelsPerMM } = this.data;      const maxOffset = ((200 - localRange * 2) * pixelsPerMM * caliperScale) * 2;      const newOffsetX = Math.min(maxOffset, caliperOffsetX + 50);      this.setData({ caliperOffsetX: newOffsetX }, () => this.redrawCaliper(true));    },    resetCaliper() {      this.setData({ caliperOffsetX: 0, caliperScale: 1.0 }, () => this.redrawCaliper(true));    },    calculateAlignPosition(mainScalePosition, vernierScalePosition, divisions, specialType) {      const selectedOption = this.data.divisionOptions.find(item => item.divisions === divisions && item.specialType === specialType);      const vernierStep = selectedOption ? selectedOption.step : 0.9;      const { pixelsPerMM, caliperOffsetX, caliperScale, canvasWidth } = this.data;      const mainScaleCenterY = this.data.canvasHeight / 2;      const canvasLeftQuarterX = canvasWidth * 0.05;      const mmToScreenX = (mm) => {        const rawPX = mm * pixelsPerMM;        const scaledPX = rawPX * caliperScale;        return canvasLeftQuarterX + caliperOffsetX - (mainScalePosition * pixelsPerMM * caliperScale) + scaledPX;      };      let minDistance = Infinity;      let alignMainScale = mainScalePosition;      const vernierMaxIndex = specialType && divisions === 20 ? 20 : divisions;      const mainScaleStart = Math.max(0, Math.floor(vernierScalePosition - 50));      const mainScaleEnd = Math.min(200, Math.ceil(vernierScalePosition + 50));      for (let mainMM = mainScaleStart; mainMM <= mainScaleEnd; mainMM++) {        const mainX = mmToScreenX(mainMM);        for (let vernierIndex = 0; vernierIndex <= vernierMaxIndex; vernierIndex++) {          const vernierMM = vernierScalePosition + vernierIndex * vernierStep;          const vernierX = mmToScreenX(vernierMM);          const distance = Math.abs(mainX - vernierX);          if (distance < minDistance) {            minDistance = distance;            alignMainScale = mainMM;          }        }      }      return alignMainScale;    },    redrawCaliper(forceUpdate = false) {      const { currentQuestion } = this.data;      const divisions = currentQuestion.divisions;      const specialType = currentQuestion.specialType;      const alignScalePosition = forceUpdate        ? this.calculateAlignPosition(currentQuestion.mainScalePosition, currentQuestion.vernierScalePosition, divisions, specialType)        : currentQuestion.alignScalePosition;      this.drawCaliper(        currentQuestion.mainScalePosition,        currentQuestion.vernierScalePosition,        divisions,        specialType,        alignScalePosition,        currentQuestion.vernierLengthMM      );    },    drawCaliper(mainScalePosition, vernierScalePosition, divisions, specialType = false, alignScalePosition, vernierLengthMM) {      const {        canvasContext, canvasWidth, canvasHeight,        pixelsPerMM, localRange, caliperOffsetX, caliperScale,        showAlignLine, isChallengeMode      } = this.data;      if (!canvasContext) return;      canvasContext.clearRect(00, canvasWidth, canvasHeight);      canvasContext.save();      const longTickHeight = canvasHeight / 6;      const shortTickHeight = canvasHeight / 8;      const vernierTickHeight = canvasHeight / 5;      const mainScaleCenterY = canvasHeight / 2;      const canvasLeftQuarterX = canvasWidth * 0.05;      const mmToScreenX = (mm) => {        const rawPX = mm * pixelsPerMM;        const scaledPX = rawPX * caliperScale;        return canvasLeftQuarterX + caliperOffsetX - (mainScalePosition * pixelsPerMM * caliperScale) + scaledPX;      };      canvasContext.setLineWidth(1);      canvasContext.beginPath();      canvasContext.moveTo(mmToScreenX(0), mainScaleCenterY);      canvasContext.lineTo(mmToScreenX(200), mainScaleCenterY);      canvasContext.setStrokeStyle("#000");      canvasContext.stroke();      for (let i = Math.ceil(0); i <= Math.floor(200); i++) {        const x = mmToScreenX(i);        canvasContext.beginPath();        canvasContext.moveTo(x, mainScaleCenterY);        if (i % 10 === 0) {          canvasContext.lineTo(x, mainScaleCenterY - longTickHeight);          canvasContext.setFontSize(14);          canvasContext.fillText((i / 10).toString(), x, mainScaleCenterY - longTickHeight - 5);        } else if (i % 5 === 0) {          canvasContext.lineTo(x, mainScaleCenterY - shortTickHeight);        } else {          canvasContext.lineTo(x, mainScaleCenterY - shortTickHeight / 2);        }        canvasContext.stroke();      }      const selectedOption = this.data.divisionOptions.find(item => item.divisions === divisions && item.specialType === specialType);      const vernierStep = selectedOption ? selectedOption.step : 0.9;      const vernierMaxIndex = specialType && divisions === 20 ? 20 : divisions;      const actualVernierLengthMM = vernierMaxIndex * vernierStep;      const vernierStartMM = vernierScalePosition;      const vernierEndMM = vernierStartMM + actualVernierLengthMM;      canvasContext.setLineWidth(0.8);      canvasContext.beginPath();      canvasContext.moveTo(mmToScreenX(vernierStartMM), mainScaleCenterY);      canvasContext.lineTo(mmToScreenX(vernierEndMM), mainScaleCenterY);      canvasContext.stroke();      const vernierTopY = mainScaleCenterY;      const vernierBottomY = mainScaleCenterY + vernierTickHeight;      const drawLimitOffset = divisions === 50 ? 100 : 50;      for (let i = 0; i <= vernierMaxIndex; i++) {        const mm = vernierStartMM + i * vernierStep;        const x = mmToScreenX(mm);        if (x < mmToScreenX(0) - drawLimitOffset || x > mmToScreenX(200) + drawLimitOffset) continue;        canvasContext.beginPath();        canvasContext.moveTo(x, vernierTopY);        if (specialType && divisions === 20) {          if (i % 2 === 0) {            canvasContext.setLineWidth(1.0);            canvasContext.lineTo(x, vernierTopY + vernierTickHeight);            canvasContext.fillText((i / 2).toString(), x, vernierTopY + vernierTickHeight + 15);          } else {            canvasContext.setLineWidth(1.0);            canvasContext.lineTo(x, vernierTopY + vernierTickHeight / 1.5);          }        } else if (divisions === 50) {          if (i % 5 === 0) {            canvasContext.setLineWidth(1.0);            canvasContext.lineTo(x, vernierTopY + vernierTickHeight);            canvasContext.fillText((i / 5).toString(), x, vernierTopY + vernierTickHeight + 15);          } else {            canvasContext.setLineWidth(1.0);            canvasContext.lineTo(x, vernierTopY + vernierTickHeight / 2);          }        } else {          if (i % 5 === 0) {            canvasContext.setLineWidth(1.0);            canvasContext.lineTo(x, vernierTopY + vernierTickHeight);            canvasContext.fillText(i.toString(), x, vernierTopY + vernierTickHeight + 15);          } else {            canvasContext.setLineWidth(1.0);            canvasContext.lineTo(x, vernierTopY + vernierTickHeight / 1.5);          }        }        canvasContext.stroke();      }      if (!isChallengeMode && showAlignLine) {        const alignX = mmToScreenX(alignScalePosition);        canvasContext.setLineWidth(1);        canvasContext.setStrokeStyle("#ff5722");        canvasContext.beginPath();        canvasContext.moveTo(alignX, mainScaleCenterY - longTickHeight * 1.5);        canvasContext.lineTo(alignX, mainScaleCenterY + vernierTickHeight * 1.5);        canvasContext.stroke();        canvasContext.setFontSize(16);        canvasContext.setFillStyle("#ff5722");        canvasContext.fillText(``, alignX + 10, mainScaleCenterY - longTickHeight * 1.8);      }      canvasContext.restore();      canvasContext.draw();    },    generateRandomPositionAndUnit(divisions, specialType = false) {      const caliperMaxMM = 200;      const selectedOption = this.data.divisionOptions.find(item => item.divisions === divisions && item.specialType === specialType);      const { step: vernierStep, accuracy } = selectedOption;      const vernierMaxIndex = specialType && divisions === 20 ? 20 : divisions;      const vernierLengthMM = vernierMaxIndex * vernierStep;      const mainRuler = Math.floor(Math.random() * 190) + 5;      const alignIndex = Math.floor(Math.random() * divisions);      let readingInMM = mainRuler + alignIndex * accuracy;      readingInMM = this.enforceAccuracy(readingInMM, accuracy);      const mainScalePosition = (mainRuler + alignIndex) - alignIndex * vernierStep;      const vernierScalePosition = mainScalePosition;      const alignScalePosition = mainRuler + alignIndex;      const units = ["mm""cm"];      const randomUnit = units[Math.floor(Math.random() * units.length)];      let finalReading = readingInMM;      let decimalPlaces = 1;      let unitAccuracy = accuracy;      if (divisions === 10) {        if (randomUnit === "mm") {          decimalPlaces = 1;          unitAccuracy = 0.1;        } else {          decimalPlaces = 2;          unitAccuracy = 0.01;        }      } else if (divisions === 20 || divisions === 50) {        if (randomUnit === "mm") {          decimalPlaces = 2;        } else {          decimalPlaces = 3;        }        unitAccuracy = randomUnit === "mm" ? accuracy : accuracy / 10;      }      if (randomUnit === "cm") {        finalReading = readingInMM / 10;      }      finalReading = this.enforceAccuracy(finalReading, unitAccuracy);      const correctReadingStr = this.formatNumber(finalReading, decimalPlaces);      return {        mainRuler,        alignIndex,        mainScalePosition,        vernierScalePosition,        alignScalePosition,        vernierLengthMM,        divisions,        specialType,        correctReading: finalReading,        correctReadingStr,        unit: randomUnit,        decimalPlaces,        accuracy: unitAccuracy      };    },    startExercise(divisions, specialType = false) {      if (!this.data.canvasContext) return;      this.setData({ errorCount: 0, feedback: "", feedbackType: "", userInput: "" });      const position = this.generateRandomPositionAndUnit(divisions, specialType);      const currentReading = position.correctReading;      const currentReadingStr = position.correctReadingStr;      const currentUnit = position.unit;      const currentPrecision = position.decimalPlaces;      const requiredPrecisionValue = position.accuracy;      this.setData({        currentQuestion: {          mainRuler: position.mainRuler,          alignIndex: position.alignIndex,          mainScalePosition: position.mainScalePosition,          vernierScalePosition: position.vernierScalePosition,          alignScalePosition: position.alignScalePosition,          vernierLengthMM: position.vernierLengthMM,          divisions: position.divisions,          specialType: position.specialType        },        currentReading,        currentReadingStr,        currentUnit,        currentPrecision,        requiredPrecisionValue,        unitHint: `请输入读数(单位:${currentUnit})`,        caliperOffsetX: 0,        caliperScale: 1.0      }, () => {        this.drawCaliper(          position.mainScalePosition,          position.vernierScalePosition,          divisions,          specialType,          position.alignScalePosition,          position.vernierLengthMM        );      });    },    nextQuestion() {      const { currentQuestion } = this.data;      this.setData({ errorCount: 0, feedback: "", feedbackType: "", userInput: "" });      this.startExercise(currentQuestion.divisions, currentQuestion.specialType);    },    // ========== 核心修改:提交答案逻辑(添加误差容错判定) ==========    submitAnswer() {      const {        userInput, currentReadingStr, currentPrecision,        currentUnit, isChallengeMode, score, questionCount, maxQuestions,        correctStreak, maxStreak, currentQuestion      } = this.data;      if (!userInput.trim()) {        this.setData({ feedback: "请输入读数!", feedbackType: "error" });        return;      }      let feedback, newScore = score, newQuestionCount = questionCount;      let newCorrectStreak = correctStreak, newMaxStreak = maxStreak;      this.setData({ feedbackType: "error" });      // 1. 校验小数位数      const inputDecimalPart = userInput.split(".")[1] || "";      if (inputDecimalPart.length !== currentPrecision) {        feedback = `错误:请输入${currentPrecision}位小数!正确答案是 ${currentReadingStr}`;        this.setData({ feedback });        if (isChallengeMode) {          this.handleChallengeNextQuestion(newQuestionCount, maxQuestions);        }        return;      }      // 2. 数值转换      const userValue = parseFloat(userInput);      const correctValue = parseFloat(currentReadingStr);      const divisions = currentQuestion.divisions;      // 3. 定义各分度的允许误差(mm单位)      let allowErrorMM = 0;      switch(divisions) {        case 20:          allowErrorMM = 0.5// 20分度允许±0.5mm          break;        case 50:          allowErrorMM = 0.04// 50分度允许±0.04mm          break;        default:          allowErrorMM = 0// 10分度精确匹配      }      // 4. 单位转换:cm单位误差 = mm误差 / 10      const allowError = currentUnit === "cm" ? allowErrorMM / 10 : allowErrorMM;      // 5. 误差判定(加1e-6解决浮点精度问题)      const diff = Math.abs(userValue - correctValue);      const isCorrect = diff <= allowError + 1e-6;      if (isCorrect) {        // 答对逻辑        feedback = "正确!";        this.setData({ feedbackType: "success" });        newCorrectStreak += 1;        newMaxStreak = Math.max(newMaxStreak, newCorrectStreak);        newScore += 5;        if (newCorrectStreak % 5 === 0) {          newScore += 5;          feedback = `正确!连续答对${newCorrectStreak}题,额外加5分🎉`;        }      } else {        // 答错逻辑        feedback = `错误,正确答案是 ${currentReadingStr}`;        newCorrectStreak = 0;      }      this.setData({        feedback,        score: newScore,        correctStreak: newCorrectStreak,        maxStreak: newMaxStreak      });      // 挑战模式自动下一题      if (isChallengeMode) {        newQuestionCount++;        this.setData({ questionCount: newQuestionCount });        if (newQuestionCount < maxQuestions) {          setTimeout(() => {            const divisionTypes = [              { divisions: 10, specialType: false },              { divisions: 20, specialType: false },              { divisions: 20, specialType: true },              { divisions: 50, specialType: false }            ];            const randomType = divisionTypes[Math.floor(Math.random() * divisionTypes.length)];            this.startExercise(randomType.divisions, randomType.specialType);            this.setData({ userInput: "", feedback: "", feedbackType: "" });          }, 1000);        } else {          this.endGame();        }      } else {        // 普通模式答对自动下一题        if (isCorrect) {          setTimeout(() => {            this.startExercise(currentQuestion.divisions, currentQuestion.specialType);            this.setData({ userInput: "", feedback: "", feedbackType: "" });          }, 1000);        } else {          const newErrorCount = this.data.errorCount + 1;          this.setData({ errorCount: newErrorCount });          if (newErrorCount >= 3) {            feedback = `错误次数已用完!正确答案是:${currentReadingStr}`;            this.setData({ feedback });            setTimeout(() => {              this.startExercise(currentQuestion.divisions, currentQuestion.specialType);              this.setData({ userInput: "", feedback: "", feedbackType: "", errorCount: 0 });            }, 3000);          }        }      }    },    handleChallengeNextQuestion(questionCount, maxQuestions) {      let newQuestionCount = questionCount + 1;      this.setData({ questionCount: newQuestionCount });      if (newQuestionCount < maxQuestions) {        setTimeout(() => {          const divisionTypes = [            { divisions: 10, specialType: false },            { divisions: 20, specialType: false },            { divisions: 20, specialType: true },            { divisions: 50, specialType: false }          ];          const randomType = divisionTypes[Math.floor(Math.random() * divisionTypes.length)];          this.startExercise(randomType.divisions, randomType.specialType);          this.setData({ userInput: "", feedback: "", feedbackType: "" });        }, 1000);      } else {        this.endGame();      }    },    startChallengeMode() {      if (!this.data.canvasContext) {        wx.showToast({ title: "卡尺已就绪,开始挑战!", icon: "none" });        return;      }      this.setData({        isChallengeMode: true,        timeLeft: 100,        score: 0,        questionCount: 0,        correctStreak: 0,        maxStreak: 0,        errorCount: 0,        showAlignLine: false      }, () => {        if (this.data.timer) clearInterval(this.data.timer);        const timer = setInterval(() => {          this.setData({ timeLeft: this.data.timeLeft - 1 }, () => {            if (this.data.timeLeft <= 0) {              clearInterval(timer);              this.endGame();            }          });        }, 1000);        this.setData({ timer });      });      const divisionTypes = [        { divisions: 10, specialType: false },        { divisions: 20, specialType: false },        { divisions: 20, specialType: true },        { divisions: 50, specialType: false }      ];      const randomType = divisionTypes[Math.floor(Math.random() * divisionTypes.length)];      this.startExercise(randomType.divisions, randomType.specialType);    },    endGame() {      if (this.data.timer) {        clearInterval(this.data.timer);        this.setData({ timer: null });      }      const { score, timeLeft, maxQuestions, questionCount, maxStreak } = this.data;      let feedback, resultDesc;      if (timeLeft <= 0) {        feedback = `时间到!挑战结束,最终得分:${score}分`;        resultDesc = "超时结束";      } else if (questionCount >= maxQuestions) {        feedback = score === 25 ? `挑战成功!满分${score}分` : `挑战完成!得分:${score}分`;        resultDesc = "完成所有题目";      } else {        feedback = `挑战结束!得分:${score}分`;        resultDesc = "提前结束";      }      const record = {        id: Date.now(),        score,        maxStreak,        totalQuestions: questionCount,        correctQuestions: Math.floor(score / 5),        timeUsed: 100 - timeLeft,        resultDesc,        createTime: new Date().toLocaleString()      };      const historyRecords = wx.getStorageSync('caliperChallengeRecords') || [];      historyRecords.unshift(record);      wx.setStorageSync('caliperChallengeRecords', historyRecords);      this.setData({        feedback,        isChallengeMode: false,        userInput: "",        correctStreak: 0,        maxStreak: 0,        errorCount: 0,        showAlignLine: true      });      wx.showModal({        title: "挑战结束",        content: feedback,        cancelText: "返回",        confirmText: "查看战绩",        success: (res) => {          if (res.confirm) wx.navigateTo({ url: '/pages/records/records' });        }      });      setTimeout(() => {        this.setData({ feedback: "", feedbackType: "", timeLeft: 100, score: 0, questionCount: 0 });      }, 3000);    },    onInputChange(e) {      this.setData({ userInput: e.detail.value });    },    goToRecords() {      wx.navigateTo({ url: '/pages/records/records' });    },    start10() {      this.setData({ isChallengeMode: false, selectedDivisionIndex: 0 });      this.startExercise(10false);    },    start20() {      this.setData({ isChallengeMode: false, selectedDivisionIndex: 1 });      this.startExercise(20false);    },    start20Special() {      this.setData({ isChallengeMode: false, selectedDivisionIndex: 2 });      this.startExercise(20true);    },    start50() {      this.setData({ isChallengeMode: false, selectedDivisionIndex: 3 });      this.startExercise(50false);    },    onLoad(options) {      this.setData({ __route__: this.route || "" });      this.initCanvas();    },    onHide() {      if (this.data.timer && this.data.isChallengeMode) {        clearInterval(this.data.timer);        this.setData({ timer: null });      }    },    onShow() {      if (this.data.isChallengeMode && this.data.timeLeft > 0 && !this.data.timer) {        const timer = setInterval(() => {          this.setData({ timeLeft: this.data.timeLeft - 1 }, () => {            if (this.data.timeLeft <= 0) {              clearInterval(timer);              this.endGame();            }          });        }, 1000);        this.setData({ timer });      }      if (this.data.canvasContext) this.redrawCaliper(true);    },    onUnload() {      if (this.data.timer) clearInterval(this.data.timer);    }  });
  • .json
    {    "usingComponents": {      "navigation-bar": "/components/navigation-bar/navigation-bar"    }  }
  • .wxss
    /* 游标卡尺练习小程序 - 样式文件   核心修改:1. 画布容器支持横向滚动 2. 画布宽度自适应 3. 适配不同屏幕 *//* 页面根容器 */.container {    padding15rpx;    box-sizing: border-box;    background-color#f8f9fa;    min-height60vh;  }  /* 标题样式 */  .title {    font-size40rpx;    text-align: center;    margin-bottom40rpx;    font-weight600;    color#1e88e5;    letter-spacing2rpx;  }  /* 按钮组样式 - 分度选择/挑战模式/查看战绩 */  .btn-group {    display: flex;    flex-wrap: wrap; /* 自动换行,适配小屏幕 */    gap20rpx; /* 按钮间距 */    margin-bottom16rpx;    padding0 10rpx;    align-items: center; /* 垂直居中 */  }  /* 新增:分度下拉选择器样式 */  .division-picker {    flex1/* 占满剩余宽度 */    min-width200rpx;    height80rpx;    line-height80rpx;    background-color#ffffff;    border1px solid #e0e0e0;    border-radius16rpx;    padding0 20rpx;    font-size28rpx;    color#333;  }  .picker-text {    white-space: nowrap;    overflow: hidden;    text-overflow: ellipsis;  }  .btn-group button {    flex: none; /* 取消均分,固定宽度 */    min-width160rpx; /* 最小宽度,避免按钮过窄 */    height80rpx;    line-height80rpx;    border-radius16rpx; /* 圆角 */    font-size28rpx;    padding0 20rpx;    transition: all 0.2s ease; /* 过渡动画 */  }  /* 普通按钮样式 */  .btn-group button:not([type="primary"]) {    background-color#ffffff;    border1px solid #e0e0e0;    color#333;  }  .btn-group button:not([type="primary"]):hover {    background-color#f5f5f5/* hover效果 */  }  /* 主按钮样式(挑战模式) */  .btn-group button[type="primary"] {    background-color#1e88e5;    border: none;    color#fff;    box-shadow0 4rpx 8rpx rgba(301362290.2); /* 阴影 */  }  .btn-group button[type="primary"]:hover {    background-color#1976d2/* 深色hover */  }  /* 挑战模式状态栏 - 倒计时+得分 */  .status-bar {    display: flex;    justify-content: space-between; /* 左右分布 */    align-items: center;    margin-bottom16rpx;    padding20rpx 30rpx;    background-color#ffffff;    border-radius20rpx;    box-shadow0 4rpx 12rpx rgba(0000.05); /* 轻微阴影 */  }  .timer.score {    font-size30rpx;    font-weight500;  }  .timer { color#e53935; } /* 倒计时红色 */  .score { color#1e88e5; } /* 得分蓝色 */  /* ========== 核心修改:卡尺画布容器 ========== */  .caliper-container {    width100%;    margin10rpx auto 40rpx;    overflow-x: auto; /* 允许横向滚动,查看全部刻度 */    overflow-y: hidden; /* 禁止纵向滚动 */    background-color#ffffff;    border-radius24rpx;    box-shadow0 8rpx 24rpx rgba(0000.08); /* 立体阴影 */    padding20rpx 0/* 上下内边距,左右无内边距避免截断 */  }  /* 卡尺Canvas样式 */  .caliper-canvas {    width: auto; /* 自适应宽度,不限制100% */    min-width100%/* 最小宽度为容器宽度 */    height480rpx; /* 固定高度(rpx适配不同屏幕) */    background-color#ffffff;    display: block;    margin0 auto;    border-radius16rpx;  }  /* 答题输入区域 */  .input-box {    display: flex;    align-items: center;    gap24rpx; /* 元素间距 */    margin-top40rpx;    padding0 10rpx;    flex-wrap: wrap; /* 自动换行 */  }  .unit-hint {    font-size28rpx;    color#666666;    min-width200rpx; /* 最小宽度,避免提示文字被挤压 */  }  .input-box input {    flex1/* 占满剩余宽度 */    min-width300rpx; /* 最小宽度 */    height80rpx;    border1px solid #e0e0e0;    border-radius16rpx;    padding0 30rpx;    font-size30rpx;    background-color#ffffff;    box-shadow: inset 0 2rpx 6rpx rgba(301362290.05); /* 内阴影 */  }  .input-box input:focus {    border-color#1e88e5/* 聚焦时蓝色边框 */    outline: none;  }  /* 提交/下一题按钮 */  .input-box button {    min-width180rpx;    height80rpx;    border-radius16rpx;    background-color#1e88e5;    border: none;    color#fff;    font-size30rpx;    box-shadow0 4rpx 12rpx rgba(301362290.2); /* 阴影 */  }  /* 下一题按钮(默认样式) */  .input-box button[type="default"] {    background-color#ffffff;    border1px solid #e0e0e0;    color#333;    box-shadow: none;  }  .input-box button[type="default"]:hover {    background-color#f5f5f5;  }  /* 按钮hover效果 */  .input-box button:hover {    background-color#1976d2;  }  /* 答题反馈提示 */  .feedback {    margin-top30rpx;    text-align: center;    font-size30rpx;    padding16rpx 0;    border-radius16rpx;    font-weight500;  }  /* 正确反馈(绿色) */  .feedback.success {    color#43a047;    background-color#e8f5e9;  }  /* 错误反馈(红色) */  .feedback.error {    color#e53935;    background-color#ffebee;  }  /* 控制按钮组(缩放/移动,备用) */  .control-btn-group {    display: flex;    gap20rpx;    margin30rpx auto;    justify-content: center;    max-width600rpx;  }  .control-btn-group button {    width80rpx;    height80rpx;    border-radius50%/* 圆形按钮 */    display: flex;    align-items: center;    justify-content: center;    background-color#fff;    border1px solid #e0e0e0;    color#333;  }  .control-btn-group button:hover {    background-color#f5f5f5;  }
  • .wxml
    <!-- 页面根容器 --><viewclass="container">  <!-- 分度选择+功能按钮组 -->  <viewclass="btn-group">    <picker      mode="selector"       range="{{divisionOptions}}"       range-key="name"      value="{{selectedDivisionIndex}}"      bindchange="onDivisionChange"      class="division-picker"    >      <viewclass="picker-text">当前分度:{{divisionOptions[selectedDivisionIndex].name}}</view>    </picker>    <buttonsize="mini"type="primary"bindtap="startChallengeMode">挑战模式</button>    <buttonsize="mini"type="default"bindtap="goToRecords">查看战绩</button>    <!-- 优化后的切换按钮 -->    <button      size="mini"       type="{{showAlignLine ? 'warn' : 'default'}}"       bindtap="toggleAlignLine"      class="toggle-align-btn"      disabled="{{isChallengeMode}}"    >      {{isChallengeMode ? '挑战模式隐藏对齐线' : (showAlignLine ? '隐藏对齐线' : '显示对齐线')}}    </button>  </view>  <!-- 挑战模式状态 -->  <viewclass="status-bar"wx:if="{{isChallengeMode === true}}">    <viewclass="timer">⏰ 倒计时:{{timeLeft}}秒</view>    <viewclass="score">🏆 当前分数:{{score}}</view>  </view>  <!-- Canvas容器 -->  <viewclass="caliper-container">    <canvas      id="caliperCanvas"            canvas-id="caliperCanvas"      class="caliper-canvas"         bindtouchstart="handleTouchStart"      bindtouchmove="handleTouchMove"      bindtouchend="handleTouchEnd"      bindtouchcancel="handleTouchEnd"    ></canvas>  </view>  <!-- 答题输入区域 -->  <viewclass="input-box">    <textclass="unit-hint">{{unitHint}}</text>    <input      type="digit"                      confirm-type="done"               adjust-position="true"            cursor-spacing="10"               placeholder="请输入读数"          bindinput="onInputChange"         value="{{userInput}}"           />    <buttontype="primary"bindtap="submitAnswer">提交答案</button>    <buttontype="default"bindtap="nextQuestion"wx:if="{{!isChallengeMode}}"size="mini">      下一题    </button>  </view>  <!-- 答题反馈 -->  <viewclass="feedback {{feedbackType}}"wx:if="{{feedback}}">{{feedback}}</view></view>
    代码由豆包AI生成
本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 游标卡尺读数练习微信小程序源码

评论 抢沙发

5 + 3 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮