520 lines
18 KiB
JavaScript
520 lines
18 KiB
JavaScript
"use strict";
|
|
const common_vendor = require("../../common/vendor.js");
|
|
const training_hooks_useMetronome = require("../hooks/useMetronome.js");
|
|
if (!Math) {
|
|
CrushCanvas();
|
|
}
|
|
const CrushCanvas = () => "./components/crush-canvas.js";
|
|
const TUTORIAL_VIDEO_URL = "https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/video/20260528/20260528173911bf6f93386.mp4";
|
|
const TUTORIAL_POSTER_URL = "https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/images/20260529/202605291030231aed12802.jpg";
|
|
const BGM_URL = "https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/file/20260528/20260528093709c669f4659.mp3";
|
|
const _sfc_defineComponent = common_vendor.defineComponent({
|
|
__name: "grip-ring",
|
|
setup(__props) {
|
|
const GRIP_PRESETS = [
|
|
{ id: "light", bpm: 60, met: 2.5, label: "轻度", desc: "热身 · 恢复" },
|
|
{ id: "medium", bpm: 80, met: 3.5, label: "中度", desc: "日常 · 锻炼" },
|
|
{ id: "heavy", bpm: 100, met: 4.5, label: "重度", desc: "强化 · 挑战" }
|
|
];
|
|
const TARGET_OPTIONS = [
|
|
{ minutes: 0, label: "自由" },
|
|
{ minutes: 5, label: "5 分钟" },
|
|
{ minutes: 10, label: "10 分钟" },
|
|
{ minutes: 15, label: "15 分钟" }
|
|
];
|
|
const REWARDS = [
|
|
{
|
|
emoji: "🥚",
|
|
title: "捏碎鸡蛋",
|
|
desc: "握力不错!",
|
|
itemType: "egg",
|
|
sound: "https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/file/20260528/202605280937077bad76716.mp3",
|
|
minReps: 8,
|
|
maxReps: 12
|
|
},
|
|
{
|
|
emoji: "🌰",
|
|
title: "捏碎核桃",
|
|
desc: "力量惊人!",
|
|
itemType: "walnut",
|
|
sound: "https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/file/20260528/20260528093707b021f5794.mp3",
|
|
minReps: 15,
|
|
maxReps: 20
|
|
},
|
|
{
|
|
emoji: "🥫",
|
|
title: "捏扁易拉罐",
|
|
desc: "太强了!",
|
|
itemType: "can",
|
|
sound: "https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/file/20260528/20260528093707ebd4d5733.mp3",
|
|
minReps: 25,
|
|
maxReps: 30
|
|
},
|
|
{
|
|
emoji: "🎈",
|
|
title: "捏爆气球",
|
|
desc: "完美!",
|
|
itemType: "balloon",
|
|
sound: "https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/file/20260528/20260528093707016f07427.mp3",
|
|
minReps: 40,
|
|
maxReps: 50
|
|
}
|
|
];
|
|
const FOOD_SUGAR_TABLE = [
|
|
{ emoji: "🍬", name: "糖果", sugar: 95, unit: "颗", weight: 5 },
|
|
// 1颗糖果约5g
|
|
{ emoji: "🍪", name: "饼干", sugar: 65, unit: "块", weight: 10 },
|
|
// 1块饼干约10g
|
|
{ emoji: "🍫", name: "巧克力", sugar: 51.5, unit: "块", weight: 50 },
|
|
// 1块巧克力约50g
|
|
{ emoji: "🍎", name: "苹果", sugar: 10.3, unit: "个", weight: 200 },
|
|
// 1个苹果约200g
|
|
{ emoji: "🍌", name: "香蕉", sugar: 12.2, unit: "根", weight: 120 },
|
|
// 1根香蕉约120g
|
|
{ emoji: "🍚", name: "米饭", sugar: 25.9, unit: "碗", weight: 150 },
|
|
// 1碗米饭约150g
|
|
{ emoji: "🥤", name: "可乐", sugar: 10.6, unit: "罐", weight: 330 }
|
|
// 1罐可乐330ml
|
|
];
|
|
const currentPreset = common_vendor.ref("medium");
|
|
const targetMinutes = common_vendor.ref(0);
|
|
const crushSignal = common_vendor.ref(null);
|
|
const showRewardPopup = common_vendor.ref(false);
|
|
const showSummary = common_vendor.ref(false);
|
|
const currentReward = common_vendor.ref(REWARDS[0]);
|
|
const crushAudioCtx = common_vendor.ref(null);
|
|
const bgmAudioCtx = common_vendor.ref(null);
|
|
const bgmEnabled = common_vendor.ref(true);
|
|
const totalReps = common_vendor.ref(0);
|
|
const elapsedSeconds = common_vendor.ref(0);
|
|
const totalSugar = common_vendor.ref(0);
|
|
const beatCount = common_vendor.ref(0);
|
|
const rewardIndex = common_vendor.ref(0);
|
|
const nextRewardAt = common_vendor.ref(0);
|
|
const startTime = common_vendor.ref(0);
|
|
const timerInterval = common_vendor.ref(null);
|
|
const preset = common_vendor.computed(() => GRIP_PRESETS.find((p) => p.id === currentPreset.value) || GRIP_PRESETS[1]);
|
|
const currentBpm = common_vendor.computed(() => preset.value.bpm);
|
|
const currentMet = common_vendor.computed(() => preset.value.met);
|
|
const targetExpanded = common_vendor.ref(false);
|
|
const currentTargetLabel = common_vendor.computed(() => {
|
|
const opt = TARGET_OPTIONS.find((t) => t.minutes === targetMinutes.value);
|
|
return opt ? opt.label : "自由";
|
|
});
|
|
const benefitExpanded = common_vendor.ref(false);
|
|
const TRAINING_BENEFIT = {
|
|
muscles: ["前臂屈肌", "手部小肌群", "握力"],
|
|
effect: "增强握力与前臂肌肉耐力,促进手部血液循环,缓解手指与腕部僵硬,适合日常碎片化锻炼及手部功能康复。"
|
|
};
|
|
const metronome = training_hooks_useMetronome.useMetronome({
|
|
initialBpm: currentBpm.value,
|
|
accentEvery: 2,
|
|
silent: true,
|
|
// 移除木鱼节奏音,仅保留节拍驱动(视觉缩放/次数统计)
|
|
onBeat
|
|
});
|
|
const { isPlaying } = metronome;
|
|
common_vendor.watch(isPlaying, (playing) => {
|
|
const videoCtx = common_vendor.index.createVideoContext("gripDemoVideo");
|
|
if (!videoCtx)
|
|
return;
|
|
if (playing) {
|
|
videoCtx.play();
|
|
} else {
|
|
videoCtx.pause();
|
|
}
|
|
});
|
|
const intervalMs = common_vendor.computed(() => 6e4 / currentBpm.value);
|
|
const hasStats = common_vendor.computed(() => isPlaying.value || totalReps.value > 0);
|
|
const isPaused = common_vendor.computed(() => !isPlaying.value && totalReps.value > 0 && !showSummary.value);
|
|
const remainingSeconds = common_vendor.computed(() => {
|
|
if (targetMinutes.value === 0)
|
|
return null;
|
|
const total = targetMinutes.value * 60;
|
|
return Math.max(0, total - elapsedSeconds.value);
|
|
});
|
|
const centerHintText = common_vendor.computed(() => isPaused.value ? "点击继续" : "点击开始");
|
|
const statusText = common_vendor.computed(() => {
|
|
if (isPlaying.value)
|
|
return "训练中";
|
|
if (isPaused.value)
|
|
return "已暂停";
|
|
return "待开始";
|
|
});
|
|
const shareTitle = common_vendor.computed(() => {
|
|
if (totalReps.value === 0)
|
|
return "握力环训练 · 一起练起来";
|
|
return `刚刚完成 ${totalReps.value} 次握力训练,约消耗 ${totalSugar.value.toFixed(1)}g 糖分 💪`;
|
|
});
|
|
const rootStyle = common_vendor.computed(() => ({
|
|
"--beat-duration": `${intervalMs.value}ms`
|
|
}));
|
|
const sugarComparisons = common_vendor.computed(() => {
|
|
if (totalSugar.value === 0)
|
|
return [];
|
|
const results = [];
|
|
for (const food of FOOD_SUGAR_TABLE) {
|
|
const foodSugar = food.sugar * food.weight / 100;
|
|
const count = totalSugar.value / foodSugar;
|
|
if (count >= 0.1 && count <= 50) {
|
|
results.push({
|
|
emoji: food.emoji,
|
|
name: food.name,
|
|
count: count >= 10 ? Math.round(count).toString() : count.toFixed(1),
|
|
unit: food.unit
|
|
});
|
|
}
|
|
}
|
|
return results.slice(0, 3);
|
|
});
|
|
function onBeat() {
|
|
beatCount.value++;
|
|
if (beatCount.value % 2 === 0) {
|
|
totalReps.value++;
|
|
if (totalReps.value >= nextRewardAt.value && rewardIndex.value < REWARDS.length) {
|
|
triggerReward();
|
|
}
|
|
}
|
|
}
|
|
function calculateSugar(seconds) {
|
|
const hours = seconds / 3600;
|
|
const weight = 60;
|
|
const calories = currentMet.value * weight * hours;
|
|
return calories / 4;
|
|
}
|
|
function formatTime(seconds) {
|
|
const mins = Math.floor(seconds / 60);
|
|
const secs = seconds % 60;
|
|
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
|
}
|
|
function triggerReward() {
|
|
const reward = REWARDS[rewardIndex.value];
|
|
currentReward.value = reward;
|
|
crushSignal.value = { type: reward.itemType, nonce: Date.now() };
|
|
playCrushSound(reward.sound);
|
|
triggerHaptic();
|
|
rewardIndex.value++;
|
|
if (rewardIndex.value < REWARDS.length) {
|
|
const nextReward = REWARDS[rewardIndex.value];
|
|
const range = nextReward.maxReps - nextReward.minReps;
|
|
nextRewardAt.value = totalReps.value + nextReward.minReps + Math.floor(Math.random() * (range + 1));
|
|
}
|
|
setTimeout(() => {
|
|
showRewardPopup.value = true;
|
|
}, 1200);
|
|
}
|
|
function closeRewardPopup() {
|
|
showRewardPopup.value = false;
|
|
}
|
|
function triggerHaptic() {
|
|
const buzz = (type) => {
|
|
common_vendor.index.vibrateShort({
|
|
type,
|
|
fail: () => {
|
|
try {
|
|
common_vendor.index.vibrateShort({});
|
|
} catch (_) {
|
|
}
|
|
}
|
|
});
|
|
};
|
|
buzz("heavy");
|
|
setTimeout(() => buzz("medium"), 120);
|
|
}
|
|
function playCrushSound(sound) {
|
|
if (!sound)
|
|
return;
|
|
if (crushAudioCtx.value) {
|
|
try {
|
|
crushAudioCtx.value.destroy();
|
|
} catch (_) {
|
|
}
|
|
}
|
|
crushAudioCtx.value = common_vendor.index.createInnerAudioContext();
|
|
crushAudioCtx.value.src = sound;
|
|
crushAudioCtx.value.obeyMuteSwitch = false;
|
|
crushAudioCtx.value.volume = 0.6;
|
|
try {
|
|
crushAudioCtx.value.play();
|
|
} catch (_) {
|
|
}
|
|
}
|
|
function startBgm() {
|
|
if (!bgmEnabled.value)
|
|
return;
|
|
if (bgmAudioCtx.value) {
|
|
try {
|
|
bgmAudioCtx.value.play();
|
|
} catch (_) {
|
|
}
|
|
return;
|
|
}
|
|
bgmAudioCtx.value = common_vendor.index.createInnerAudioContext();
|
|
bgmAudioCtx.value.src = BGM_URL;
|
|
bgmAudioCtx.value.loop = true;
|
|
bgmAudioCtx.value.obeyMuteSwitch = false;
|
|
bgmAudioCtx.value.volume = 0.4;
|
|
try {
|
|
bgmAudioCtx.value.play();
|
|
} catch (_) {
|
|
}
|
|
}
|
|
function stopBgm() {
|
|
if (bgmAudioCtx.value) {
|
|
try {
|
|
bgmAudioCtx.value.pause();
|
|
} catch (_) {
|
|
}
|
|
}
|
|
}
|
|
function toggleBgm() {
|
|
bgmEnabled.value = !bgmEnabled.value;
|
|
if (bgmEnabled.value && isPlaying.value) {
|
|
startBgm();
|
|
} else {
|
|
stopBgm();
|
|
}
|
|
}
|
|
function onPresetTap(id) {
|
|
if (isPlaying.value)
|
|
return;
|
|
currentPreset.value = id;
|
|
metronome.setBpm(preset.value.bpm);
|
|
}
|
|
function onTargetTap(minutes) {
|
|
if (isPlaying.value)
|
|
return;
|
|
targetMinutes.value = minutes;
|
|
}
|
|
function onTogglePlay() {
|
|
if (isPlaying.value) {
|
|
pauseTraining();
|
|
} else {
|
|
if (totalReps.value === 0) {
|
|
resetStats();
|
|
}
|
|
startTrainingInternal();
|
|
}
|
|
}
|
|
function startTrainingInternal() {
|
|
metronome.start();
|
|
startBgm();
|
|
startTimer();
|
|
common_vendor.index.setKeepScreenOn({ keepScreenOn: true });
|
|
}
|
|
function pauseTraining() {
|
|
metronome.stop();
|
|
stopBgm();
|
|
stopTimer();
|
|
common_vendor.index.setKeepScreenOn({ keepScreenOn: false });
|
|
}
|
|
function onEndTraining() {
|
|
if (totalReps.value === 0)
|
|
return;
|
|
pauseTraining();
|
|
showSummary.value = true;
|
|
}
|
|
function dismissSummary() {
|
|
showSummary.value = false;
|
|
resetStats();
|
|
}
|
|
function onFinish() {
|
|
showSummary.value = false;
|
|
resetStats();
|
|
}
|
|
function onRestart() {
|
|
showSummary.value = false;
|
|
resetStats();
|
|
startTrainingInternal();
|
|
}
|
|
function resetStats() {
|
|
totalReps.value = 0;
|
|
elapsedSeconds.value = 0;
|
|
totalSugar.value = 0;
|
|
beatCount.value = 0;
|
|
rewardIndex.value = 0;
|
|
startTime.value = Date.now();
|
|
const firstReward = REWARDS[0];
|
|
const range = firstReward.maxReps - firstReward.minReps;
|
|
nextRewardAt.value = firstReward.minReps + Math.floor(Math.random() * (range + 1));
|
|
}
|
|
function startTimer() {
|
|
if (timerInterval.value)
|
|
return;
|
|
startTime.value = Date.now() - elapsedSeconds.value * 1e3;
|
|
timerInterval.value = setInterval(() => {
|
|
elapsedSeconds.value = Math.floor((Date.now() - startTime.value) / 1e3);
|
|
totalSugar.value = calculateSugar(elapsedSeconds.value);
|
|
if (targetMinutes.value > 0 && elapsedSeconds.value >= targetMinutes.value * 60) {
|
|
onEndTraining();
|
|
}
|
|
}, 1e3);
|
|
}
|
|
function stopTimer() {
|
|
if (timerInterval.value) {
|
|
clearInterval(timerInterval.value);
|
|
timerInterval.value = null;
|
|
}
|
|
}
|
|
common_vendor.onShow(() => {
|
|
metronome.preload();
|
|
});
|
|
common_vendor.onShareAppMessage(() => ({
|
|
title: shareTitle.value,
|
|
path: "/training/pages/grip-ring",
|
|
imageUrl: TUTORIAL_POSTER_URL
|
|
}));
|
|
common_vendor.onShareTimeline(() => ({
|
|
title: shareTitle.value,
|
|
imageUrl: TUTORIAL_POSTER_URL
|
|
}));
|
|
function cleanupAllMedia() {
|
|
var _a;
|
|
try {
|
|
metronome.stop();
|
|
} catch (_) {
|
|
}
|
|
stopTimer();
|
|
common_vendor.index.setKeepScreenOn({ keepScreenOn: false });
|
|
try {
|
|
(_a = common_vendor.index.createVideoContext("gripDemoVideo")) == null ? void 0 : _a.pause();
|
|
} catch (_) {
|
|
}
|
|
if (bgmAudioCtx.value) {
|
|
try {
|
|
bgmAudioCtx.value.stop();
|
|
} catch (_) {
|
|
}
|
|
try {
|
|
bgmAudioCtx.value.destroy();
|
|
} catch (_) {
|
|
}
|
|
bgmAudioCtx.value = null;
|
|
}
|
|
if (crushAudioCtx.value) {
|
|
try {
|
|
crushAudioCtx.value.stop();
|
|
} catch (_) {
|
|
}
|
|
try {
|
|
crushAudioCtx.value.destroy();
|
|
} catch (_) {
|
|
}
|
|
crushAudioCtx.value = null;
|
|
}
|
|
}
|
|
common_vendor.onHide(cleanupAllMedia);
|
|
common_vendor.onUnload(cleanupAllMedia);
|
|
return (_ctx, _cache) => {
|
|
return common_vendor.e({
|
|
a: common_vendor.t(statusText.value),
|
|
b: common_vendor.unref(isPlaying) ? 1 : "",
|
|
c: isPaused.value ? 1 : "",
|
|
d: common_vendor.p({
|
|
["crush-signal"]: crushSignal.value
|
|
}),
|
|
e: TUTORIAL_VIDEO_URL,
|
|
f: TUTORIAL_POSTER_URL,
|
|
g: !common_vendor.unref(isPlaying)
|
|
}, !common_vendor.unref(isPlaying) ? {
|
|
h: common_vendor.t(centerHintText.value)
|
|
} : {
|
|
i: common_vendor.t(currentBpm.value)
|
|
}, {
|
|
j: common_vendor.o(onTogglePlay, "9b"),
|
|
k: common_vendor.unref(isPlaying) ? 1 : "",
|
|
l: common_vendor.t(bgmEnabled.value ? "🎵" : "🔇"),
|
|
m: common_vendor.o(toggleBgm, "6e"),
|
|
n: remainingSeconds.value !== null && hasStats.value
|
|
}, remainingSeconds.value !== null && hasStats.value ? {
|
|
o: common_vendor.t(formatTime(remainingSeconds.value))
|
|
} : {}, {
|
|
p: common_vendor.unref(isPlaying)
|
|
}, common_vendor.unref(isPlaying) ? {} : {}, {
|
|
q: common_vendor.f(GRIP_PRESETS, (p, k0, i0) => {
|
|
return {
|
|
a: common_vendor.t(p.label),
|
|
b: common_vendor.t(p.bpm),
|
|
c: p.id,
|
|
d: currentPreset.value === p.id ? 1 : "",
|
|
e: common_vendor.o(($event) => onPresetTap(p.id), p.id)
|
|
};
|
|
}),
|
|
r: common_vendor.unref(isPlaying) ? 1 : "",
|
|
s: common_vendor.t(currentTargetLabel.value),
|
|
t: common_vendor.t(targetExpanded.value ? "▲" : "▼"),
|
|
v: common_vendor.o(($event) => targetExpanded.value = !targetExpanded.value, "16"),
|
|
w: targetExpanded.value
|
|
}, targetExpanded.value ? {
|
|
x: common_vendor.f(TARGET_OPTIONS, (t, k0, i0) => {
|
|
return {
|
|
a: common_vendor.t(t.label),
|
|
b: t.minutes,
|
|
c: targetMinutes.value === t.minutes ? 1 : "",
|
|
d: common_vendor.o(($event) => onTargetTap(t.minutes), t.minutes)
|
|
};
|
|
}),
|
|
y: common_vendor.unref(isPlaying) ? 1 : ""
|
|
} : {}, {
|
|
z: common_vendor.t(benefitExpanded.value ? "收起 ▲" : "展开 ▼"),
|
|
A: common_vendor.o(($event) => benefitExpanded.value = !benefitExpanded.value, "ee"),
|
|
B: benefitExpanded.value
|
|
}, benefitExpanded.value ? {
|
|
C: common_vendor.f(TRAINING_BENEFIT.muscles, (m, k0, i0) => {
|
|
return {
|
|
a: common_vendor.t(m),
|
|
b: m
|
|
};
|
|
}),
|
|
D: common_vendor.t(TRAINING_BENEFIT.effect)
|
|
} : {}, {
|
|
E: isPaused.value
|
|
}, isPaused.value ? {
|
|
F: common_vendor.o(onTogglePlay, "8a"),
|
|
G: common_vendor.o(onEndTraining, "d0")
|
|
} : {}, {
|
|
H: common_vendor.unref(isPlaying) ? 1 : "",
|
|
I: isPaused.value ? 1 : "",
|
|
J: common_vendor.s(rootStyle.value),
|
|
K: showRewardPopup.value
|
|
}, showRewardPopup.value ? {
|
|
L: common_vendor.t(currentReward.value.emoji),
|
|
M: common_vendor.t(currentReward.value.title),
|
|
N: common_vendor.t(currentReward.value.desc),
|
|
O: common_vendor.t(totalReps.value),
|
|
P: common_vendor.o(closeRewardPopup, "ae"),
|
|
Q: common_vendor.o(() => {
|
|
}, "5b"),
|
|
R: common_vendor.o(closeRewardPopup, "1a")
|
|
} : {}, {
|
|
S: showSummary.value
|
|
}, showSummary.value ? common_vendor.e({
|
|
T: common_vendor.t(totalReps.value),
|
|
U: common_vendor.t(formatTime(elapsedSeconds.value)),
|
|
V: common_vendor.t(totalSugar.value.toFixed(1)),
|
|
W: sugarComparisons.value.length > 0
|
|
}, sugarComparisons.value.length > 0 ? {
|
|
X: common_vendor.f(sugarComparisons.value, (food, idx, i0) => {
|
|
return {
|
|
a: common_vendor.t(food.emoji),
|
|
b: common_vendor.t(food.count),
|
|
c: common_vendor.t(food.unit),
|
|
d: common_vendor.t(food.name),
|
|
e: idx
|
|
};
|
|
})
|
|
} : {}, {
|
|
Y: common_vendor.o(onRestart, "78"),
|
|
Z: common_vendor.o(onFinish, "a1"),
|
|
aa: common_vendor.o(() => {
|
|
}, "c4"),
|
|
ab: common_vendor.o(dismissSummary, "77")
|
|
}) : {});
|
|
};
|
|
}
|
|
});
|
|
_sfc_defineComponent.__runtimeHooks = 6;
|
|
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_defineComponent, [["__scopeId", "data-v-cebdebdc"]]);
|
|
wx.createPage(MiniProgramPage);
|
|
//# sourceMappingURL=../../../.sourcemap/mp-weixin/training/pages/grip-ring.js.map
|