更新
This commit is contained in:
@@ -0,0 +1,519 @@
|
||||
"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
|
||||
Reference in New Issue
Block a user