From 7cfa4d269a2a95ebeabd6c557b92d75b78e0713b Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 1 Jun 2026 09:34:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TUICallKit-Vue3/package-lock.json | 69 - TUICallKit-Vue3/pages.json | 14 +- .../tongji/components/FoodTileIcon.vue | 116 + .../tongji/components/TongjiIcon.vue | 14 +- .../tongji/composables/useGameFx.js | 188 ++ .../tongji/composables/useGameProgress.js | 112 + .../tongji/composables/useGameSfx.js | 303 +++ TUICallKit-Vue3/tongji/data/gameFoodArt.js | 654 +++++ TUICallKit-Vue3/tongji/data/gameFoodImg.js | 49 + .../tongji/data/gameFoodLibrary.js | 127 + TUICallKit-Vue3/tongji/data/gameLevels.js | 32 + .../tongji/design-system/more-stitch.html | 596 +++++ TUICallKit-Vue3/tongji/pages/game.vue | 2175 +++++++++++++++++ TUICallKit-Vue3/tongji/pages/more.vue | 1251 ++++++---- TUICallKit-Vue3/tongji/pages/weekly.vue | 36 + .../tongji/styles/game-stitch.scss | 1294 ++++++++++ .../tongji/styles/more-stitch.scss | 1169 +++++++++ .../tongji/styles/stitch-vitalmint-theme.scss | 6 +- .../tongji/styles/weekly-stitch.scss | 72 +- server/app/api/controller/TcmController.php | 37 +- server/app/api/logic/tcm/DailyGamifyLogic.php | 44 +- 21 files changed, 7756 insertions(+), 602 deletions(-) create mode 100644 TUICallKit-Vue3/tongji/components/FoodTileIcon.vue create mode 100644 TUICallKit-Vue3/tongji/composables/useGameFx.js create mode 100644 TUICallKit-Vue3/tongji/composables/useGameProgress.js create mode 100644 TUICallKit-Vue3/tongji/composables/useGameSfx.js create mode 100644 TUICallKit-Vue3/tongji/data/gameFoodArt.js create mode 100644 TUICallKit-Vue3/tongji/data/gameFoodImg.js create mode 100644 TUICallKit-Vue3/tongji/data/gameFoodLibrary.js create mode 100644 TUICallKit-Vue3/tongji/data/gameLevels.js create mode 100644 TUICallKit-Vue3/tongji/design-system/more-stitch.html create mode 100644 TUICallKit-Vue3/tongji/pages/game.vue create mode 100644 TUICallKit-Vue3/tongji/styles/game-stitch.scss create mode 100644 TUICallKit-Vue3/tongji/styles/more-stitch.scss diff --git a/TUICallKit-Vue3/package-lock.json b/TUICallKit-Vue3/package-lock.json index 77cad593..e1978253 100644 --- a/TUICallKit-Vue3/package-lock.json +++ b/TUICallKit-Vue3/package-lock.json @@ -4142,9 +4142,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4161,9 +4158,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4180,9 +4174,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4199,9 +4190,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4433,9 +4421,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4456,9 +4441,6 @@ "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4479,9 +4461,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4502,9 +4481,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4525,9 +4501,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4548,9 +4521,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4729,9 +4699,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4745,9 +4712,6 @@ "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4761,9 +4725,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4777,9 +4738,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4793,9 +4751,6 @@ "cpu": [ "loong64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4809,9 +4764,6 @@ "cpu": [ "loong64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4825,9 +4777,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4841,9 +4790,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4857,9 +4803,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4873,9 +4816,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4889,9 +4829,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4905,9 +4842,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4921,9 +4855,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/TUICallKit-Vue3/pages.json b/TUICallKit-Vue3/pages.json index da9081ed..23bbd49c 100644 --- a/TUICallKit-Vue3/pages.json +++ b/TUICallKit-Vue3/pages.json @@ -186,10 +186,9 @@ { "path": "pages/more", "style": { + "navigationStyle": "custom", "navigationBarTitleText": "日常护理", - "navigationBarBackgroundColor": "#204e2b", - "navigationBarTextStyle": "white", - "backgroundColor": "#faf9f5", + "backgroundColor": "#f4fbf4", "enablePullDownRefresh": true } }, @@ -209,6 +208,15 @@ } } } + }, + { + "path": "pages/game", + "style": { + "navigationStyle": "custom", + "navigationBarTitleText": "糖分突袭", + "backgroundColor": "#c7d2fe", + "disableScroll": true + } } ] } diff --git a/TUICallKit-Vue3/tongji/components/FoodTileIcon.vue b/TUICallKit-Vue3/tongji/components/FoodTileIcon.vue new file mode 100644 index 00000000..d5b500e6 --- /dev/null +++ b/TUICallKit-Vue3/tongji/components/FoodTileIcon.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/TUICallKit-Vue3/tongji/components/TongjiIcon.vue b/TUICallKit-Vue3/tongji/components/TongjiIcon.vue index 90f59470..0159c670 100644 --- a/TUICallKit-Vue3/tongji/components/TongjiIcon.vue +++ b/TUICallKit-Vue3/tongji/components/TongjiIcon.vue @@ -66,7 +66,12 @@ const ICON_PATHS = { egg: '', home: '', 'plus-circle': '', - 'chevron-right': '' + 'chevron-right': '', + 'chevron-left': '', + check: '', + settings: '', + syringe: '', + zap: '' } /** 图片加载失败时的 emoji 回退 */ @@ -106,7 +111,12 @@ const ICON_FALLBACK = { egg: '🍳', home: '🏠', 'plus-circle': '⊕', - 'chevron-right': '›' + 'chevron-right': '›', + 'chevron-left': '‹', + check: '✓', + settings: '⚙', + syringe: '💉', + zap: '⚡' } const strokeColor = computed(() => { diff --git a/TUICallKit-Vue3/tongji/composables/useGameFx.js b/TUICallKit-Vue3/tongji/composables/useGameFx.js new file mode 100644 index 00000000..f35e92a9 --- /dev/null +++ b/TUICallKit-Vue3/tongji/composables/useGameFx.js @@ -0,0 +1,188 @@ +/** + * 糖分突袭 · 视觉特效状态(飘字、粒子、连消横幅、震屏、闪屏) + * 特效风格参考「开心消消乐」:糖果色星屑、弹性扩散光环、大消除闪屏。 + */ +import { ref } from 'vue' + +let fxSeq = 0 +function nextFxId(prefix) { + fxSeq += 1 + return `${prefix}-${fxSeq}-${Date.now()}` +} + +// 开心消消乐式糖果色板(彩虹爆裂用) +const CANDY_COLORS = ['#FF5E9C', '#FFC93C', '#5BD16A', '#4D9DFF', '#FF8A3D', '#B57BFF', '#FF6F91', '#2FE6C8'] +// 星屑字形 +const STAR_GLYPHS = ['✦', '✧', '★', '✨'] + +export function useGameFx() { + const floatingTexts = ref([]) + const burstParticles = ref([]) + const ripples = ref([]) + const comboBanner = ref({ show: false, text: '', tier: 1 }) + const screenShaking = ref(false) + const flash = ref({ active: false, color: '#ffffff', opacity: 0 }) + + let comboBannerTimer = null + let shakeTimer = null + let flashTimer = null + let flashFadeTimer = null + + function spawnFloatingText(payload) { + const id = nextFxId('ft') + floatingTexts.value.push({ id, ...payload }) + const duration = payload.danger ? 1500 : payload.isCombo ? 1400 : 1200 + setTimeout(() => { + floatingTexts.value = floatingTexts.value.filter((f) => f.id !== id) + }, duration) + return id + } + + /** + * 糖果爆裂粒子(星屑 + 圆点混合,上抛后受重力下落,带旋转与缓动)。 + * @param {number} cx,cy 爆裂中心(屏幕坐标) + * @param {string} color 基础颜色 + * @param {number} count 粒子数量 + * @param {object} opts { star, rainbow, gravity, sizeMin, sizeMax, distMin, distMax, life } + */ + function spawnBurst(cx, cy, color, count = 8, opts = {}) { + const { + star = true, + rainbow = false, + gravity = 64, + sizeMin = 8, + sizeMax = 16, + distMin = 36, + distMax = 110, + life = 850 + } = opts + const dur = (life / 1000).toFixed(2) + + for (let i = 0; i < count; i++) { + const id = nextFxId('p') + const isStar = star && Math.random() < 0.65 + const pSize = Math.random() * (sizeMax - sizeMin) + sizeMin + const angle = Math.random() * Math.PI * 2 + const distance = Math.random() * (distMax - distMin) + distMin + const tx = Math.cos(angle) * distance + // 先上抛、再叠加重力下落,形成喷泉抛物线 + const ty = Math.sin(angle) * distance - distance * 0.35 + gravity + const half = pSize / 2 + const pColor = rainbow + ? CANDY_COLORS[Math.floor(Math.random() * CANDY_COLORS.length)] + : color + const rot = Math.random() * 720 - 360 + const glyph = isStar ? STAR_GLYPHS[Math.floor(Math.random() * STAR_GLYPHS.length)] : '' + + const particle = { + id, + left: cx - half, + top: cy - half, + size: pSize, + color: pColor, + char: glyph, + opacity: 1, + transform: 'scale(0.4) rotate(0deg)', + transition: 'none' + } + burstParticles.value.push(particle) + + setTimeout(() => { + particle.transition = + `left ${dur}s cubic-bezier(0.16,0.7,0.3,1), top ${dur}s cubic-bezier(0.3,0.1,0.4,1), opacity ${dur}s ease-in, transform ${dur}s ease-out` + particle.left = cx - half + tx + particle.top = cy - half + ty + particle.opacity = 0 + particle.transform = `scale(${isStar ? 1.15 : 0.5}) rotate(${rot}deg)` + }, 16) + + setTimeout(() => { + burstParticles.value = burstParticles.value.filter((p) => p.id !== id) + }, life + 80) + } + } + + /** + * 弹性扩散发光光环(开心消消乐式的扩散光圈)。 + * @param {object} opts { thickness, life } + */ + function spawnRipple(cx, cy, color, tier = 1, opts = {}) { + const id = nextFxId('rp') + const size = 36 + tier * 18 + const { thickness = Math.max(3, tier + 2), life = 600 } = opts + ripples.value.push({ + id, + left: cx, + top: cy, + size, + color, + thickness, + opacity: 0.95 + }) + setTimeout(() => { + ripples.value = ripples.value.filter((r) => r.id !== id) + }, life) + } + + /** + * 全屏闪光(大消除瞬间的爆闪),先点亮再淡出。 + */ + function triggerFlash(color = '#ffffff', intensity = 0.5, life = 280) { + if (flashFadeTimer) clearTimeout(flashFadeTimer) + if (flashTimer) clearTimeout(flashTimer) + flash.value = { active: true, color, opacity: intensity } + flashFadeTimer = setTimeout(() => { + flash.value = { ...flash.value, opacity: 0 } + }, 24) + flashTimer = setTimeout(() => { + flash.value = { active: false, color, opacity: 0 } + }, life) + } + + function showComboBanner(chain, points) { + if (chain <= 1) return + const tier = chain >= 4 ? 3 : chain >= 3 ? 2 : 1 + comboBanner.value = { + show: true, + text: `连消 ×${chain} +${points}`, + tier + } + if (comboBannerTimer) clearTimeout(comboBannerTimer) + comboBannerTimer = setTimeout(() => { + comboBanner.value = { ...comboBanner.value, show: false } + }, 1100) + } + + function triggerShake(intensity = 1) { + screenShaking.value = true + if (shakeTimer) clearTimeout(shakeTimer) + shakeTimer = setTimeout(() => { + screenShaking.value = false + }, intensity >= 2 ? 420 : 280) + } + + function clearAll() { + floatingTexts.value = [] + burstParticles.value = [] + ripples.value = [] + comboBanner.value = { show: false, text: '', tier: 1 } + screenShaking.value = false + flash.value = { active: false, color: '#ffffff', opacity: 0 } + } + + return { + floatingTexts, + burstParticles, + ripples, + comboBanner, + screenShaking, + flash, + spawnFloatingText, + spawnBurst, + spawnRipple, + showComboBanner, + triggerShake, + triggerFlash, + clearAll + } +} diff --git a/TUICallKit-Vue3/tongji/composables/useGameProgress.js b/TUICallKit-Vue3/tongji/composables/useGameProgress.js new file mode 100644 index 00000000..530e1650 --- /dev/null +++ b/TUICallKit-Vue3/tongji/composables/useGameProgress.js @@ -0,0 +1,112 @@ +/** + * 游戏进度持久化:关卡、星级、连胜、每日打卡 + */ +import { ref } from 'vue' +import { calcLevelStars } from '../data/gameLevels.js' + +const STORAGE_KEY = 'tongji_game_progress_v1' + +const DEFAULT = { + currentLevel: 1, + maxUnlocked: 1, + stars: {}, + winStreak: 0, + dailyStreak: 0, + lastPlayDate: '', + totalWins: 0, + bestCombo: 0, + totalStars: 0 +} + +function todayStr() { + const d = new Date() + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` +} + +function sumStars(starsMap) { + return Object.values(starsMap || {}).reduce((s, n) => s + (Number(n) || 0), 0) +} + +function loadRaw() { + try { + const raw = uni.getStorageSync(STORAGE_KEY) + if (raw && typeof raw === 'object') return { ...DEFAULT, ...raw } + } catch (_) {} + return { ...DEFAULT } +} + +function saveRaw(data) { + try { + uni.setStorageSync(STORAGE_KEY, data) + } catch (_) {} +} + +export function useGameProgress() { + const progress = ref(loadRaw()) + + function persist() { + progress.value.totalStars = sumStars(progress.value.stars) + saveRaw(progress.value) + } + + /** 进入游戏时更新每日打卡 */ + function touchDailyPlay() { + const today = todayStr() + const last = progress.value.lastPlayDate + if (last === today) return progress.value.dailyStreak + + const yesterday = new Date() + yesterday.setDate(yesterday.getDate() - 1) + const yStr = `${yesterday.getFullYear()}-${String(yesterday.getMonth() + 1).padStart(2, '0')}-${String(yesterday.getDate()).padStart(2, '0')}` + + if (last === yStr) { + progress.value.dailyStreak = (progress.value.dailyStreak || 0) + 1 + } else { + progress.value.dailyStreak = 1 + } + progress.value.lastPlayDate = today + persist() + return progress.value.dailyStreak + } + + function getStars(levelId) { + return progress.value.stars[String(levelId)] || 0 + } + + function recordWin({ levelId, movesLeft, glucoseLevel, score, levelConfig, maxCombo }) { + const stars = calcLevelStars({ movesLeft, glucoseLevel, score, levelConfig }) + const key = String(levelId) + const prev = progress.value.stars[key] || 0 + if (stars > prev) progress.value.stars[key] = stars + + progress.value.winStreak = (progress.value.winStreak || 0) + 1 + progress.value.totalWins = (progress.value.totalWins || 0) + 1 + if (maxCombo > (progress.value.bestCombo || 0)) progress.value.bestCombo = maxCombo + + const next = levelId + 1 + if (next > progress.value.maxUnlocked) progress.value.maxUnlocked = next + progress.value.currentLevel = next + persist() + return { stars, newBest: stars > prev } + } + + function recordLoss() { + progress.value.winStreak = 0 + persist() + } + + function setCurrentLevel(levelId) { + progress.value.currentLevel = Math.max(1, Math.min(progress.value.maxUnlocked, levelId)) + persist() + } + + return { + progress, + touchDailyPlay, + getStars, + recordWin, + recordLoss, + setCurrentLevel, + persist + } +} diff --git a/TUICallKit-Vue3/tongji/composables/useGameSfx.js b/TUICallKit-Vue3/tongji/composables/useGameSfx.js new file mode 100644 index 00000000..b25f3327 --- /dev/null +++ b/TUICallKit-Vue3/tongji/composables/useGameSfx.js @@ -0,0 +1,303 @@ +/** + * 糖分突袭 · 游戏音效(InnerAudioContext 池化,H5/小程序通用) + * 每种操作独立音轨 + playbackRate 变调,避免“全是同一个声” + */ +import { ref, onUnmounted } from 'vue' + +const STORAGE_KEY = 'tongji_game_sfx_enabled' +const COS = 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/file' + +/** 9 条基础素材(已上传 COS,小程序域名白名单内) */ +const SRC = { + tap: `${COS}/20260526/20260526105128557ef8669.mp3`, + swoosh: `${COS}/20260527/202605271539371ebe13672.mp3`, + pop: `${COS}/20260526/202605261051282a0a94508.mp3`, + buzz: `${COS}/20260527/202605271539376ff628472.mp3`, + chime: `${COS}/20260528/202605280937077bad76716.mp3`, + fanfare: `${COS}/20260528/20260528093707b021f5794.mp3`, + blast: `${COS}/20260528/20260528093707ebd4d5733.mp3`, + sparkle: `${COS}/20260528/20260528093707016f07427.mp3`, + stinger: `${COS}/20260528/20260528093709c669f4659.mp3` +} + +/** + * 音效表:src / volume / rate(playbackRate) / pool + * rate 不同可在同一素材上听出明显差异 + */ +const SFX = { + // —— 交互 —— + select: { src: SRC.tap, volume: 0.32, rate: 1.05, pool: 3 }, + deselect: { src: SRC.tap, volume: 0.22, rate: 0.82, pool: 2 }, + hint: { src: SRC.tap, volume: 0.26, rate: 1.28, pool: 2 }, + + swap: { src: SRC.swoosh, volume: 0.38, rate: 1.0, pool: 3 }, + swapFail: { src: SRC.buzz, volume: 0.36, rate: 1.18, pool: 2 }, + + drop: { src: SRC.pop, volume: 0.3, rate: 0.88, pool: 4 }, + dropLight: { src: SRC.pop, volume: 0.22, rate: 1.22, pool: 2 }, + + // —— 消除(按连数)—— + match3: { src: SRC.chime, volume: 0.48, rate: 1.0, pool: 4 }, + match4: { src: SRC.fanfare, volume: 0.54, rate: 1.06, pool: 3 }, + match5: { src: SRC.blast, volume: 0.62, rate: 1.0, pool: 3 }, + + // —— 消除(按 GI 主色)—— + matchLowGi: { src: SRC.chime, volume: 0.42, rate: 0.86, pool: 2 }, + matchMidGi: { src: SRC.chime, volume: 0.46, rate: 1.02, pool: 2 }, + matchHighGi: { src: SRC.buzz, volume: 0.44, rate: 0.92, pool: 2 }, + + // —— 连消 —— + combo2: { src: SRC.fanfare, volume: 0.56, rate: 1.12, pool: 2 }, + combo3: { src: SRC.blast, volume: 0.64, rate: 1.08, pool: 2 }, + comboMega: { src: SRC.blast, volume: 0.72, rate: 1.22, pool: 2 }, + + // —— 道具 —— + insulin: { src: SRC.sparkle, volume: 0.5, rate: 1.15, pool: 2 }, + fiber: { src: SRC.swoosh, volume: 0.48, rate: 1.32, pool: 2 }, + meal: { src: SRC.stinger, volume: 0.42, rate: 1.35, pool: 1 }, + reshuffle: { src: SRC.swoosh, volume: 0.46, rate: 0.72, pool: 2 }, + + // —— 反馈 —— + score: { src: SRC.tap, volume: 0.28, rate: 1.38, pool: 2 }, + goalTick: { src: SRC.chime, volume: 0.36, rate: 1.45, pool: 2 }, + meterWarn: { src: SRC.buzz, volume: 0.5, rate: 1.0, pool: 2 }, + meterDanger: { src: SRC.buzz, volume: 0.58, rate: 0.78, pool: 2 }, + meterStable: { src: SRC.pop, volume: 0.24, rate: 1.05, pool: 1 }, + + win: { src: SRC.fanfare, volume: 0.66, rate: 1.0, pool: 1 }, + winStinger: { src: SRC.stinger, volume: 0.38, rate: 1.5, pool: 1 }, + lose: { src: SRC.buzz, volume: 0.52, rate: 0.68, pool: 1 }, + + // 兼容旧名 + match: { src: SRC.chime, volume: 0.48, rate: 1.0, pool: 4 }, + combo: { src: SRC.fanfare, volume: 0.56, rate: 1.1, pool: 3 }, + heal: { src: SRC.pop, volume: 0.44, rate: 1.18, pool: 2 }, + boost: { src: SRC.sparkle, volume: 0.52, rate: 1.0, pool: 2 }, + invalid: { src: SRC.buzz, volume: 0.4, rate: 1.15, pool: 2 }, + danger: { src: SRC.blast, volume: 0.58, rate: 0.95, pool: 2 } +} + +class SfxPool { + constructor(src, size, volume, rate = 1) { + this.src = src + this.size = size + this.volume = volume + this.rate = rate + this.list = [] + this.cursor = 0 + this.warmedUp = false + } + + create() { + for (let i = 0; i < this.size; i++) { + const ctx = uni.createInnerAudioContext() + ctx.src = this.src + ctx.obeyMuteSwitch = false + ctx.autoplay = false + ctx.volume = this.volume + try { + ctx.playbackRate = this.rate + } catch (_) {} + this.list.push(ctx) + } + } + + warmUp() { + if (this.warmedUp) return + if (!this.list.length) this.create() + this.list.forEach((ctx) => { + try { + ctx.volume = 0 + ctx.play() + setTimeout(() => { + try { + ctx.stop() + ctx.volume = this.volume + } catch (_) {} + }, 60) + } catch (_) {} + }) + this.warmedUp = true + } + + play(scale = 1, rateMul = 1) { + if (!this.list.length) this.create() + const ctx = this.list[this.cursor % this.list.length] + this.cursor += 1 + const vol = Math.min(1, this.volume * scale) + const rate = Math.min(2, Math.max(0.5, this.rate * rateMul)) + try { + ctx.volume = vol + try { + ctx.playbackRate = rate + } catch (_) {} + ctx.stop() + ctx.seek(0) + ctx.play() + } catch (_) { + try { + ctx.play() + } catch (e) {} + } + } + + destroy() { + this.list.forEach((ctx) => { + try { + ctx.stop() + ctx.destroy() + } catch (_) {} + }) + this.list = [] + this.warmedUp = false + } +} + +function dominantGi(tiles = []) { + const cnt = { low: 0, mid: 0, high: 0 } + tiles.forEach((t) => { + const g = t?.type?.gi + if (g && cnt[g] !== undefined) cnt[g] += 1 + }) + let best = 'mid' + let max = 0 + Object.entries(cnt).forEach(([k, v]) => { + if (v > max) { + max = v + best = k + } + }) + return best +} + +export function useGameSfx() { + const enabled = ref(true) + const pools = {} + + try { + const stored = uni.getStorageSync(STORAGE_KEY) + if (stored === false || stored === '0' || stored === 0) { + enabled.value = false + } + } catch (_) {} + + function ensureAudioOption() { + // #ifdef MP-WEIXIN + try { + uni.setInnerAudioOption({ + obeyMuteSwitch: false, + mixWithOther: true + }) + } catch (_) {} + // #endif + } + + function getPool(name) { + const cfg = SFX[name] + if (!cfg) return null + if (!pools[name]) { + pools[name] = new SfxPool(cfg.src, cfg.pool, cfg.volume, cfg.rate ?? 1) + } + return pools[name] + } + + function warmUp() { + if (!enabled.value) return + ensureAudioOption() + Object.keys(SFX).forEach((name) => { + getPool(name)?.warmUp() + }) + } + + function play(name, scale = 1, rateMul = 1) { + if (!enabled.value) return + getPool(name)?.play(scale, rateMul) + } + + function playLayer(primary, secondary, delayMs = 70, secScale = 0.6) { + play(primary) + if (secondary) { + setTimeout(() => play(secondary, secScale), delayMs) + } + } + + function playMatch({ matchCount = 3, chain = 0, tiles = [] } = {}) { + if (!enabled.value) return + const giTrack = (() => { + const gi = dominantGi(tiles) + if (gi === 'low') return 'matchLowGi' + if (gi === 'high') return 'matchHighGi' + return 'matchMidGi' + })() + + if (chain >= 3) { + playLayer('comboMega', giTrack, 90, 0.55) + return + } + if (chain === 2) { + playLayer('combo3', giTrack, 80, 0.5) + return + } + if (chain === 1) { + playLayer('combo2', giTrack, 70, 0.45) + return + } + + if (matchCount >= 5) { + playLayer('match5', 'matchHighGi', 85, 0.5) + } else if (matchCount >= 4) { + playLayer('match4', giTrack, 65, 0.48) + } else { + play(giTrack, 1, 1) + setTimeout(() => play('match3', 0.85), 40) + } + } + + function playDrop(count = 1) { + if (!enabled.value || count <= 0) return + const name = count >= 4 ? 'drop' : 'dropLight' + play(name, Math.min(1.2, 0.85 + count * 0.04), count >= 6 ? 0.9 : 1) + } + + function playMeter(delta, level) { + if (!enabled.value) return + if (level > 70) play('meterDanger', 1 + (level - 70) * 0.02) + else if (level < 30) play('meterWarn', 0.9, 0.95) + else if (delta > 8) play('meterWarn', 0.75) + else if (delta < -5) play('meterStable', 1.1) + } + + function toggleEnabled() { + enabled.value = !enabled.value + try { + uni.setStorageSync(STORAGE_KEY, enabled.value ? '1' : '0') + } catch (_) {} + if (enabled.value) { + warmUp() + play('select') + } + } + + function destroyAll() { + Object.keys(pools).forEach((key) => { + pools[key]?.destroy() + delete pools[key] + }) + } + + onUnmounted(() => { + destroyAll() + }) + + return { + enabled, + play, + playLayer, + playMatch, + playDrop, + playMeter, + warmUp, + toggleEnabled, + destroyAll + } +} diff --git a/TUICallKit-Vue3/tongji/data/gameFoodArt.js b/TUICallKit-Vue3/tongji/data/gameFoodArt.js new file mode 100644 index 00000000..33b33f1a --- /dev/null +++ b/TUICallKit-Vue3/tongji/data/gameFoodArt.js @@ -0,0 +1,654 @@ +/** + * 棋盘食材 · 仿真插画 SVG(渐变 + 高光 + 阴影,开心消消乐式 2.5D) + */ +import { svgToDataUrl } from '../utils/svgDataUrl.js' + +function gid(key, suffix) { + return `f-${String(key).replace(/[^a-z0-9]/gi, '')}-${suffix}` +} + +function wrap(key, body) { + return `${body}` +} + +function shadow(key) { + const id = gid(key, 'sh') + return `` +} + +function shine(x, y, rx, ry) { + return `` +} + +/** 圆形水果 */ +function artRound(key, base, dark, light, extra = '') { + const g = gid(key, 'g') + return wrap(key, ` + + + + + + + ${shadow(key)} + + + + ${shine(24, 28, 7, 5)} + + + ${extra} + `) +} + +/** 叶菜 */ +function artLeafy(key, light, mid, dark) { + const g1 = gid(key, 'g1') + const g2 = gid(key, 'g2') + return wrap(key, ` + + + + + + + + ${shadow(key)} + + + + + ${shine(26, 24, 6, 8)} + `) +} + +/** 西兰花 */ +function artBroccoli(key) { + const g = gid(key, 'g') + return wrap(key, ` + + + + + ${shadow(key)} + + + + + + ${shine(28, 20, 5, 4)} + `) +} + +/** 长条(黄瓜/香蕉/玉米) */ +function artLong(key, base, dark, light, type = 'cucumber') { + const g = gid(key, 'g') + if (type === 'banana') { + return wrap(key, ` + + + ${shadow(key)} + + + + ${shine(30, 26, 5, 3)} + `) + } + if (type === 'corn') { + return wrap(key, ` + + + ${shadow(key)} + + + ${Array.from({ length: 5 }, (_, r) => + Array.from({ length: 4 }, (_, c) => + `` + ).join('') + ).join('')} + + ${shine(26, 26, 5, 8)} + `) + } + return wrap(key, ` + + + ${shadow(key)} + + + + `) +} + +/** 茄子 */ +function artEggplant(key) { + const g = gid(key, 'g') + return wrap(key, ` + + + ${shadow(key)} + + + + ${shine(26, 32, 5, 7)} + `) +} + +/** 番茄 */ +function artTomato(key) { + return artRound(key, '#e53935', '#b71c1c', '#ef5350', + '') +} + +/** 甜椒 */ +function artPepper(key, color, dark) { + const g = gid(key, 'g') + return wrap(key, ` + + + ${shadow(key)} + + + + ${shine(28, 26, 5, 6)} + `) +} + +/** 洋葱 */ +function artOnion(key) { + const g = gid(key, 'g') + return wrap(key, ` + + + ${shadow(key)} + + + + ${shine(24, 30, 6, 5)} + `) +} + +/** 葡萄串 */ +function artGrapes(key) { + const g = gid(key, 'g') + return wrap(key, ` + + + ${shadow(key)} + + + + + + + ${shine(28, 24, 3, 2)} + `) +} + +/** 樱桃 */ +function artCherry(key) { + const g = gid(key, 'g') + return wrap(key, ` + + + ${shadow(key)} + + + + ${shine(19, 29, 3, 2)}${shine(39, 29, 3, 2)} + `) +} + +/** 草莓 */ +function artStrawberry(key) { + const g = gid(key, 'g') + return wrap(key, ` + + + ${shadow(key)} + + + + ${shine(26, 28, 4, 6)} + ${[28, 34, 38, 30, 36].map((x, i) => + `` + ).join('')} + `) +} + +/** 豆类 */ +function artBeans(key) { + const g = gid(key, 'g') + return wrap(key, ` + + + ${shadow(key)} + + + + + `) +} + +/** 饮品杯 */ +function artCup(key, liquid, cup = '#eceff1', type = 'glass') { + const g = gid(key, 'g') + if (type === 'bottle') { + return wrap(key, ` + + + ${shadow(key)} + + + + ${shine(28, 30, 4, 10)} + `) + } + if (type === 'wine') { + return wrap(key, ` + + + ${shadow(key)} + + + + + `) + } + return wrap(key, ` + + + ${shadow(key)} + + + + ${shine(26, 32, 4, 8)} + `) +} + +/** 蘑菇 */ +function artMushroom(key) { + const g = gid(key, 'g') + return wrap(key, ` + + + ${shadow(key)} + + + + + + ${shine(24, 24, 6, 4)} + `) +} + +/** 芹菜 */ +function artCelery(key) { + return artLeafy(key, '#aed581', '#7cb342', '#558b2f') +} + +/** 四季豆 */ +function artGreenBean(key) { + const g = gid(key, 'g') + return wrap(key, ` + + + ${shadow(key)} + + + + `) +} + +/** 饭碗 */ +function artRiceBowl(key, rice, bowl = '#e0e0e0', dark = '#bdbdbd') { + const g = gid(key, 'g') + return wrap(key, ` + + + ${shadow(key)} + + + + ${shine(24, 34, 6, 3)} + `) +} + +/** 薯类 */ +function artRoot(key, base, dark, light) { + const g = gid(key, 'g') + return wrap(key, ` + + + ${shadow(key)} + + + + ${shine(24, 30, 5, 6)} + `) +} + +/** 肉类 */ +function artMeat(key, base, dark, type = 'steak') { + const g = gid(key, 'g') + if (type === 'chicken') { + return wrap(key, ` + + + ${shadow(key)} + + + + + ${shine(26, 32, 5, 6)} + `) + } + if (type === 'fish') { + return wrap(key, ` + + + ${shadow(key)} + + + + + ${shine(28, 30, 6, 4)} + `) + } + return wrap(key, ` + + + ${shadow(key)} + + + + ${shine(26, 28, 6, 4)} + `) +} + +/** 虾/蟹 */ +function artSeafood(key, type = 'shrimp') { + const g = gid(key, 'g') + if (type === 'crab') { + return wrap(key, ` + + + ${shadow(key)} + + + + + `) + } + return wrap(key, ` + + + ${shadow(key)} + + + ${shine(30, 28, 5, 4)} + `) +} + +/** 菠萝 */ +function artPineapple(key) { + const g = gid(key, 'g') + return wrap(key, ` + + + ${shadow(key)} + + + ${[-8, 0, 8].map(o => + `` + ).join('')} + + ${shine(26, 32, 5, 6)} + `) +} + +/** 奇异果 */ +function artKiwi(key) { + const g = gid(key, 'g') + return wrap(key, ` + + + ${shadow(key)} + + + + + ${shine(26, 28, 4, 3)} + `) +} + +/** 蜜瓜 */ +function artMelon(key) { + return artRound(key, '#aed581', '#689f38', '#c5e1a5', '') +} + +/** 蜂蜜 */ +function artHoney(key) { + return artCup(key, '#ffb300', '#ff8f00', 'bottle') +} + +/** 面包类 */ +function artBread(key, base, dark, type = 'loaf') { + const g = gid(key, 'g') + if (type === 'donut') { + return wrap(key, ` + + + ${shadow(key)} + + + + + ${shine(24, 28, 5, 4)} + `) + } + if (type === 'mantou') { + return wrap(key, ` + + + ${shadow(key)} + + + + ${shine(24, 32, 6, 4)} + `) + } + return wrap(key, ` + + + ${shadow(key)} + + + ${shine(22, 28, 8, 5)} + `) +} + +/** 面条 */ +function artNoodle(key, soup = '#d7ccc8') { + const g = gid(key, 'g') + return wrap(key, ` + + + ${shadow(key)} + + + + + + `) +} + +/** 南瓜 */ +function artPumpkin(key) { + const g = gid(key, 'g') + return wrap(key, ` + + + ${shadow(key)} + + + + + + ${shine(26, 28, 5, 6)} + `) +} + +/** 薯条 */ +function artFries(key) { + const g = gid(key, 'g') + return wrap(key, ` + + + ${shadow(key)} + + + ${[26, 32, 38, 44].map(x => ``).join('')} + `) +} + +/** 西瓜 */ +function artWatermelon(key) { + const g = gid(key, 'g') + return wrap(key, ` + + + ${shadow(key)} + + + + ${[24, 32, 40].map(x => ``).join('')} + ${shine(24, 30, 4, 3)} + `) +} + +/** 糖果 */ +function artCandy(key, base, dark, type = 'lollipop') { + const g = gid(key, 'g') + if (type === 'wrap') { + return wrap(key, ` + + + ${shadow(key)} + + + + `) + } + return wrap(key, ` + + + ${shadow(key)} + + + + ${shine(26, 24, 4, 3)} + `) +} + +/** 糯米团 */ +function artDango(key) { + const g = gid(key, 'g') + return wrap(key, ` + + + ${shadow(key)} + + + + + + `) +} + +/** 爆米花 */ +function artPopcorn(key) { + const g = gid(key, 'g') + return wrap(key, ` + + + ${shadow(key)} + + + + + `) +} + +const FOOD_ART = { + lettuce: () => artLeafy('lettuce', '#c5e1a5', '#81c784', '#388e3c'), + broccoli: () => artBroccoli('broccoli'), + apple: () => artRound('apple', '#e53935', '#b71c1c', '#ef5350'), + cucumber: () => artLong('cucumber', '#66bb6a', '#2e7d32', '#a5d6a7'), + eggplant: () => artEggplant('eggplant'), + tomato: () => artTomato('tomato'), + pepper: () => artPepper('pepper', '#43a047', '#1b5e20'), + onion: () => artOnion('onion'), + pear: () => artRound('pear', '#c0ca33', '#827717', '#dce775', ''), + orange: () => artRound('orange', '#fb8c00', '#e65100', '#ffb74d'), + peach: () => artRound('peach', '#ff8a65', '#e64a19', '#ffab91'), + cherry: () => artCherry('cherry'), + grape: () => artGrapes('grape'), + strawberry: () => artStrawberry('strawberry'), + soybean: () => artBeans('soybean'), + milk: () => artCup('milk', '#eceff1', '#b0bec5'), + tea: () => artCup('tea', '#a1887f', '#6d4c41'), + mushroom: () => artMushroom('mushroom'), + celery: () => artCelery('celery'), + greenBean: () => artGreenBean('greenBean'), + brownRice: () => artRiceBowl('brownRice', '#bcaaa4', '#8d6e63', '#6d4c41'), + sweetPotato: () => artRoot('sweetPotato', '#c75b39', '#8d2600', '#e57373'), + taro: () => artRoot('taro', '#b39ddb', '#5e35b1', '#d1c4e9'), + fish: () => artMeat('fish', '#4fc3f7', '#0277bd', 'fish'), + chicken: () => artMeat('chicken', '#ffb74d', '#e65100', 'chicken'), + beef: () => artMeat('beef', '#c0504d', '#7f0000', 'steak'), + leanMeat: () => artMeat('leanMeat', '#b5654d', '#8d4000', 'steak'), + shrimp: () => artSeafood('shrimp', 'shrimp'), + crab: () => artSeafood('crab', 'crab'), + banana: () => artLong('banana', '#fdd835', '#f9a825', '#fff176', 'banana'), + mango: () => artRound('mango', '#ffb300', '#ff6f00', '#ffca28'), + pineapple: () => artPineapple('pineapple'), + kiwi: () => artKiwi('kiwi'), + melon: () => artMelon('melon'), + honey: () => artHoney('honey'), + wine: () => artCup('wine', '#9b2c3b', '#4a0e16', 'wine'), + beer: () => artCup('beer', '#ffb300', '#ff8f00'), + coffee: () => artCup('coffee', '#6f4e37', '#3e2723'), + corn: () => artLong('corn', '#f9c513', '#f57f17', '#fff176', 'corn'), + udon: () => artNoodle('udon', '#d9b88f'), + whiteRice: () => artRiceBowl('whiteRice', '#f5f5f5', '#eeeeee', '#bdbdbd'), + whiteBread: () => artBread('whiteBread', '#d9a85c', '#a1887f', 'loaf'), + mantou: () => artBread('mantou', '#ece0c8', '#bcaaa4', 'mantou'), + youtiao: () => artBread('youtiao', '#d4943f', '#a1660a', 'loaf'), + donut: () => artBread('donut', '#e87fb0', '#ad1457', 'donut'), + popcorn: () => artPopcorn('popcorn'), + ramen: () => artNoodle('ramen', '#e0a96d'), + pumpkin: () => artPumpkin('pumpkin'), + bakedPotato: () => artFries('bakedPotato'), + watermelon: () => artWatermelon('watermelon'), + sugar: () => artCandy('sugar', '#ff79b0', '#c2185b', 'lollipop'), + maltose: () => artCandy('maltose', '#ffa726', '#e65100', 'wrap'), + soda: () => artCup('soda', '#c62828', '#880e0e', 'bottle'), + orangeJuice: () => artCup('orangeJuice', '#ff9f1c', '#e65100', 'bottle'), + stickyRice: () => artDango('stickyRice') +} + +const imgCache = {} + +/** 获取食材插画 data URL(带缓存) */ +export function getFoodImg(key) { + if (imgCache[key]) return imgCache[key] + const builder = FOOD_ART[key] + if (!builder) return '' + imgCache[key] = svgToDataUrl(builder()) + return imgCache[key] +} + +export function warmUpFoodImgs(keys) { + keys.forEach((k) => getFoodImg(k)) +} diff --git a/TUICallKit-Vue3/tongji/data/gameFoodImg.js b/TUICallKit-Vue3/tongji/data/gameFoodImg.js new file mode 100644 index 00000000..da960330 --- /dev/null +++ b/TUICallKit-Vue3/tongji/data/gameFoodImg.js @@ -0,0 +1,49 @@ +/** + * 食材图标 URL(Twemoji PNG,小程序 / H5 通用) + * 微信小程序 不支持 SVG data URL,必须用 https PNG + */ +const TWEMOJI_BASE = 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72' + +/** emoji → Twemoji PNG 地址 */ +export function emojiToTwemojiUrl(emoji) { + if (!emoji) return '' + const parts = [] + for (let i = 0; i < emoji.length;) { + const cp = emoji.codePointAt(i) + if (!cp) break + parts.push(cp.toString(16)) + i += cp > 0xffff ? 2 : 1 + } + return `${TWEMOJI_BASE}/${parts.join('-')}.png` +} + +const imgCache = {} + +export function getFoodImgUrl(emoji) { + if (!emoji) return '' + if (imgCache[emoji]) return imgCache[emoji] + const url = emojiToTwemojiUrl(emoji) + imgCache[emoji] = url + return url +} + +/** 预加载本局食材图标(小程序提前拉取,减少首屏空白) */ +export function warmUpFoodImgs(foods) { + const list = Array.isArray(foods) ? foods : [] + list.forEach((f) => { + const icon = typeof f === 'string' ? f : f?.icon + const url = getFoodImgUrl(icon) + if (!url) return + // #ifdef MP-WEIXIN + try { + uni.getImageInfo({ src: url, fail: () => {} }) + } catch (_) {} + // #endif + // #ifdef H5 + try { + const img = new Image() + img.src = url + } catch (_) {} + // #endif + }) +} diff --git a/TUICallKit-Vue3/tongji/data/gameFoodLibrary.js b/TUICallKit-Vue3/tongji/data/gameFoodLibrary.js new file mode 100644 index 00000000..7ca958e5 --- /dev/null +++ b/TUICallKit-Vue3/tongji/data/gameFoodLibrary.js @@ -0,0 +1,127 @@ +/** + * 全量食材库:key 用于消除匹配,img 为 Twemoji PNG(小程序可用) + */ +import { getFoodImgUrl } from './gameFoodImg.js' + +export const GI_LABEL = { low: '低血糖', mid: '中血糖', high: '高血糖' } + +/** 若实际食用时的升糖幅度(教育提示用,正值=升糖) */ +export function getEatVal(food) { + if (food.eatVal != null) return food.eatVal + if (food.gi === 'high') return Math.abs(food.val) + if (food.gi === 'mid') return Math.max(1, food.val) + return Math.max(1, Math.abs(food.val)) +} + +/** 从餐盘消除该食物后的血糖变化(负值=控糖有益) */ +export function getClearVal(food) { + if (food.clearVal != null) return food.clearVal + const eat = getEatVal(food) + if (food.gi === 'high') return -Math.max(8, Math.round(eat * 0.5)) + if (food.gi === 'mid') return -1 + return 0 +} + +/** 点击食材时的升糖说明文案 */ +export function formatEatLabel(food) { + if (food.gi === 'low') return '升糖低' + return `若食用 +${getEatVal(food)}` +} + +const RAW_FOODS = [ + // 低 GI + { key: 'lettuce', icon: '🥬', name: '生菜', gi: 'low', val: -3, color: '#8BC34A' }, + { key: 'broccoli', icon: '🥦', name: '西兰花', gi: 'low', val: -3, color: '#4E8A3A' }, + { key: 'apple', icon: '🍎', name: '苹果', gi: 'low', val: -2, color: '#E53935' }, + { key: 'cucumber', icon: '🥒', name: '黄瓜', gi: 'low', val: -4, color: '#66A82F' }, + { key: 'eggplant', icon: '🍆', name: '茄子', gi: 'low', val: -3, color: '#7E57C2' }, + { key: 'tomato', icon: '🍅', name: '番茄', gi: 'low', val: -2, color: '#EF5350' }, + { key: 'pepper', icon: '🫑', name: '青椒', gi: 'low', val: -3, color: '#43A047' }, + { key: 'onion', icon: '🧅', name: '洋葱', gi: 'low', val: -2, color: '#CDA06B' }, + { key: 'pear', icon: '🍐', name: '雪梨', gi: 'low', val: -2, color: '#C0CA33' }, + { key: 'orange', icon: '🍊', name: '橙子', gi: 'low', val: -2, color: '#FB8C00' }, + { key: 'peach', icon: '🍑', name: '桃子', gi: 'low', val: -2, color: '#FF8A65' }, + { key: 'cherry', icon: '🍒', name: '樱桃', gi: 'low', val: -2, color: '#C2185B' }, + { key: 'grape', icon: '🍇', name: '葡萄', gi: 'low', val: -1, color: '#8E24AA' }, + { key: 'strawberry', icon: '🍓', name: '草莓', gi: 'low', val: -2, color: '#EC407A' }, + { key: 'soybean', icon: '🫘', name: '黄豆', gi: 'low', val: -3, color: '#C9A063' }, + { key: 'milk', icon: '🥛', name: '牛奶', gi: 'low', val: -2, color: '#B0BEC5' }, + { key: 'tea', icon: '🍵', name: '红茶', gi: 'low', val: -1, color: '#A1573B' }, + { key: 'mushroom', icon: '🍄', name: '香菇', gi: 'low', val: -3, color: '#8D6E63' }, + { key: 'celery', icon: '🌿', name: '芹菜', gi: 'low', val: -4, color: '#7CB342' }, + { key: 'greenBean', icon: '🫛', name: '四季豆', gi: 'low', val: -3, color: '#689F38' }, + + // 中 GI + { key: 'brownRice', icon: '🍙', name: '糙米饭', gi: 'mid', val: 4, color: '#C9A26B' }, + { key: 'sweetPotato', icon: '🍠', name: '番薯', gi: 'mid', val: 5, color: '#C75B39' }, + { key: 'taro', icon: '🥔', name: '芋头', gi: 'mid', val: 5, color: '#B39DDB' }, + { key: 'fish', icon: '🐟', name: '鱼肉', gi: 'mid', val: 2, color: '#4FA3C7' }, + { key: 'chicken', icon: '🍗', name: '鸡肉', gi: 'mid', val: 3, color: '#D6A15A' }, + { key: 'beef', icon: '🥩', name: '牛肉', gi: 'mid', val: 3, color: '#C0504D' }, + { key: 'leanMeat', icon: '🍖', name: '瘦肉', gi: 'mid', val: 3, color: '#B5654D' }, + { key: 'shrimp', icon: '🦐', name: '虾仁', gi: 'mid', val: 2, color: '#FF7043' }, + { key: 'crab', icon: '🦀', name: '螃蟹', gi: 'mid', val: 2, color: '#F4623A' }, + { key: 'banana', icon: '🍌', name: '香蕉', gi: 'mid', val: 6, color: '#FDD835' }, + { key: 'mango', icon: '🥭', name: '芒果', gi: 'mid', val: 6, color: '#FFB300' }, + { key: 'pineapple', icon: '🍍', name: '菠萝', gi: 'mid', val: 6, color: '#FBC02D' }, + { key: 'kiwi', icon: '🥝', name: '奇异果', gi: 'mid', val: 4, color: '#9CCC65' }, + { key: 'melon', icon: '🍈', name: '哈密瓜', gi: 'mid', val: 6, color: '#AED581' }, + { key: 'honey', icon: '🍯', name: '蜂蜜', gi: 'mid', val: 6, color: '#F9A825' }, + { key: 'wine', icon: '🍷', name: '红酒', gi: 'mid', val: 4, color: '#9B2C3B' }, + { key: 'beer', icon: '🍺', name: '啤酒', gi: 'mid', val: 5, color: '#E0A83E' }, + { key: 'coffee', icon: '☕', name: '咖啡', gi: 'mid', val: 2, color: '#6F4E37' }, + { key: 'corn', icon: '🌽', name: '玉米', gi: 'mid', val: 5, color: '#F9C513' }, + { key: 'udon', icon: '🍲', name: '乌冬面', gi: 'mid', val: 5, color: '#D9B88F' }, + + // 高 GI + { key: 'whiteRice', icon: '🍚', name: '白米饭', gi: 'high', val: 18, color: '#E0E0E0' }, + { key: 'whiteBread', icon: '🍞', name: '白面包', gi: 'high', val: 16, color: '#D9A85C' }, + { key: 'mantou', icon: '🥯', name: '馒头', gi: 'high', val: 17, color: '#ECE0C8' }, + { key: 'youtiao', icon: '🥖', name: '油条', gi: 'high', val: 20, color: '#D4943F' }, + { key: 'donut', icon: '🍩', name: '甜甜圈', gi: 'high', val: 22, color: '#E87FB0' }, + { key: 'popcorn', icon: '🍿', name: '爆米花', gi: 'high', val: 18, color: '#F5E6B3' }, + { key: 'ramen', icon: '🍜', name: '拉面', gi: 'high', val: 17, color: '#E0A96D' }, + { key: 'pumpkin', icon: '🎃', name: '南瓜', gi: 'high', val: 15, color: '#EF6C00' }, + { key: 'bakedPotato', icon: '🍟', name: '焗薯', gi: 'high', val: 20, color: '#F2C14E' }, + { key: 'watermelon', icon: '🍉', name: '西瓜', gi: 'high', val: 16, color: '#F0506A' }, + { key: 'sugar', icon: '🍭', name: '砂糖', gi: 'high', val: 25, color: '#FF79B0' }, + { key: 'maltose', icon: '🍬', name: '麦芽糖', gi: 'high', val: 24, color: '#FFA726' }, + { key: 'soda', icon: '🥤', name: '汽水', gi: 'high', val: 25, color: '#C62828' }, + { key: 'orangeJuice', icon: '🧃', name: '柳橙汁', gi: 'high', val: 22, color: '#FF9F1C' }, + { key: 'stickyRice', icon: '🍡', name: '糯米饭', gi: 'high', val: 19, color: '#F2A7B8' } +] + +export const FOOD_LIBRARY = RAW_FOODS.map((f, i) => { + const eatVal = getEatVal(f) + const clearVal = getClearVal({ ...f, eatVal }) + return { + id: i + 1, + ...f, + eatVal, + clearVal, + giLabel: GI_LABEL[f.gi], + get img() { + return getFoodImgUrl(f.icon) + } + } +}) + +/** 按 key 查找食材(目标栏等用) */ +export function getFoodByKey(key) { + return FOOD_LIBRARY.find((f) => f.key === key) || null +} + +/** 为棋盘 tile 使用的 plain 对象(img 预计算,避免 getter 在响应式里反复触发) */ +export function cloneFoodType(food) { + return { + id: food.id, + key: food.key, + icon: food.icon, + name: food.name, + gi: food.gi, + giLabel: food.giLabel, + val: food.val, + color: food.color, + img: getFoodImgUrl(food.icon) + } +} diff --git a/TUICallKit-Vue3/tongji/data/gameLevels.js b/TUICallKit-Vue3/tongji/data/gameLevels.js new file mode 100644 index 00000000..653e5ebd --- /dev/null +++ b/TUICallKit-Vue3/tongji/data/gameLevels.js @@ -0,0 +1,32 @@ +/** + * 关卡配置:难度随关卡递增,驱动“再闯一关”动力 + */ +export function getLevelConfig(levelId) { + const lv = Math.max(1, Math.min(999, Number(levelId) || 1)) + const tier = Math.floor((lv - 1) / 5) + + return { + id: lv, + moves: Math.max(14, 26 - tier * 2 - (lv % 3)), + goalTargets: [8 + tier * 2 + (lv % 2), 10 + tier * 2 + Math.floor(lv / 4)], + scoreTarget: 2800 + lv * 450, + startGlucose: 50, + /** 餐后消化倒计时(秒):归零时按棋盘上剩余食物升糖 */ + digestIntervalSec: Math.max(5, 10 - tier), + digestMultiplier: 1 + tier * 0.12, + boosters: { + insulin: Math.max(1, 3 - Math.floor(tier / 2)), + fiber: tier >= 3 ? 0 : 1, + meal: Math.max(2, 5 - tier) + } + } +} + +/** 根据本局表现计算 1~3 星 */ +export function calcLevelStars({ movesLeft, glucoseLevel, score, levelConfig }) { + let stars = 1 + const stable = glucoseLevel >= 30 && glucoseLevel <= 70 + if (stable && movesLeft >= 2) stars = 2 + if (stars >= 2 && score >= (levelConfig?.scoreTarget ?? 3000) && movesLeft >= 4) stars = 3 + return stars +} diff --git a/TUICallKit-Vue3/tongji/design-system/more-stitch.html b/TUICallKit-Vue3/tongji/design-system/more-stitch.html new file mode 100644 index 00000000..12d2fd63 --- /dev/null +++ b/TUICallKit-Vue3/tongji/design-system/more-stitch.html @@ -0,0 +1,596 @@ + + + +Daily Care - Health Dashboard + + + + + + + + + +
+
+
+
+ +
+
+

Daily Care

+

吕帅

+
+
+
+ + +
+
+
+
+ +
+ + +
+ +
+
+
+

健康日历

+

2026年5月 · 已记录 3 / 7 天

+
+ +
+
+ + + + + + + +18 +19 +20 +21 +22 +
23
+
+24 + +
+
+25 + +
+
+26 + +
+27 +28 +
+29 +
+30 +31 +
+
+今日还没有记录 + +
+
+ +
+
+

血糖明细

+共 3 天 +
+
+
+
+05-26 +周二 +
+
+
+空腹 +
+10.4 +mmol/L +
+
+
+餐后 +—— mmol/L +
+
+
+
+
+05-25 +周一 +
+
+空腹 +
+10.2 +mmol/L +
+
+
+
+
+ +
+
+
+

稳糖乐园

+

用健康打卡浇灌你的稳糖树

+
+
+110 +稳糖积分 +
+
+ +
+ + + + +
+

今日控糖任务

+
+
+
+
+check +
+测血糖血压 +
+
++10积分 +已完成 +
+
+
+
+
+ +
+记今日饮食 +
+
++10积分 + +
+
+
+ +
+ +
+
+
+Lv.4 +枝繁叶茂 +
+

还差 15 XP 升级至 Lv.4

+
+
+5/50 XP +
+
+ +
+
+
+ +
+ +
+ +
+eco +
+ +
+🪴 +
+
+ +
+ +

浇水可消耗 5 积分,获得 10 XP

+
+
+
+ +
+
+
+favorite +
+
+

邀请家人点赞

+

分享战报给家人,邀请他们为您点赞鼓励

+
+
+chevron_right +
+ +
+chat_bubble +
+

先完成血糖记录,今日第一格就亮起来

+ +
+
+ +
+
+就诊卡 +10 张 +
+
+吕帅 +23岁 +
+
+霍爱玲 +57岁 +
+
+
+ +
+

数据每日同步 · 阈值仅作参考,请遵医嘱

+
+ + + + + + \ No newline at end of file diff --git a/TUICallKit-Vue3/tongji/pages/game.vue b/TUICallKit-Vue3/tongji/pages/game.vue new file mode 100644 index 00000000..28b6e0a6 --- /dev/null +++ b/TUICallKit-Vue3/tongji/pages/game.vue @@ -0,0 +1,2175 @@ + + + + + diff --git a/TUICallKit-Vue3/tongji/pages/more.vue b/TUICallKit-Vue3/tongji/pages/more.vue index 1f0cd174..b79c8923 100644 --- a/TUICallKit-Vue3/tongji/pages/more.vue +++ b/TUICallKit-Vue3/tongji/pages/more.vue @@ -1,503 +1,360 @@