diff --git a/TUICallKit-Vue3/App.vue b/TUICallKit-Vue3/App.vue index 06e86640..66fa355e 100644 --- a/TUICallKit-Vue3/App.vue +++ b/TUICallKit-Vue3/App.vue @@ -1,7 +1,6 @@ \ No newline at end of file + diff --git a/TUICallKit-Vue3/TUIKit/pages/chat/chat.vue b/TUICallKit-Vue3/TUIKit/pages/chat/chat.vue index 3ae7a849..3e8e03d5 100644 --- a/TUICallKit-Vue3/TUIKit/pages/chat/chat.vue +++ b/TUICallKit-Vue3/TUIKit/pages/chat/chat.vue @@ -239,7 +239,6 @@ import messageEnhancerPlugin from '@tencentcloud/lite-chat/plugins/message-enhancer'; import richMediaMessagePlugin from '@tencentcloud/lite-chat/plugins/rich-media-message'; import groupPlugin from '@tencentcloud/lite-chat/plugins/group'; -import * as GenerateTestUserSig from "@/debug/GenerateTestUserSig-es.js"; import { TUICallKitAPI } from '@/TUICallKit/src/TUICallService/index'; import { TUIStore } from '@/TUICallKit/src/TUICallService/CallService/index'; import { StoreName, NAME, CallStatus } from '@/TUICallKit/src/TUICallService/const/index'; @@ -561,17 +560,12 @@ const { proxy } = getCurrentInstance() }, }, false) - const {userId } = res.data; - + const { userId, userSig, sdkAppId: SDKAppID } = res.data; patient_data.value=res.data.data - const { userSig, SDKAppID,secretKey } = GenerateTestUserSig.genTestUserSig({ - userID:userId, - }); getApp().globalData.userID = userId; getApp().globalData.userSig = userSig; getApp().globalData.SDKAppID = SDKAppID; - getApp().globalData.secretKey = secretKey; uni.setStorageSync('CallManager', userId) // TUICallKit 必须在 TIM chat 登录成功后再 init,且必须传入 tim,否则无法接收通话信令(聊天正常、视频接不到) } diff --git a/TUICallKit-Vue3/doctor/pages/doctor/doctor.vue b/TUICallKit-Vue3/doctor/pages/doctor/doctor.vue index 4d0ae3e8..36dad711 100644 --- a/TUICallKit-Vue3/doctor/pages/doctor/doctor.vue +++ b/TUICallKit-Vue3/doctor/pages/doctor/doctor.vue @@ -178,7 +178,6 @@ - - diff --git a/TUICallKit-Vue3/tongji/composables/useGameFx.js b/TUICallKit-Vue3/tongji/composables/useGameFx.js deleted file mode 100644 index f35e92a9..00000000 --- a/TUICallKit-Vue3/tongji/composables/useGameFx.js +++ /dev/null @@ -1,188 +0,0 @@ -/** - * 糖分突袭 · 视觉特效状态(飘字、粒子、连消横幅、震屏、闪屏) - * 特效风格参考「开心消消乐」:糖果色星屑、弹性扩散光环、大消除闪屏。 - */ -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 deleted file mode 100644 index 530e1650..00000000 --- a/TUICallKit-Vue3/tongji/composables/useGameProgress.js +++ /dev/null @@ -1,112 +0,0 @@ -/** - * 游戏进度持久化:关卡、星级、连胜、每日打卡 - */ -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 deleted file mode 100644 index b25f3327..00000000 --- a/TUICallKit-Vue3/tongji/composables/useGameSfx.js +++ /dev/null @@ -1,303 +0,0 @@ -/** - * 糖分突袭 · 游戏音效(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 deleted file mode 100644 index 33b33f1a..00000000 --- a/TUICallKit-Vue3/tongji/data/gameFoodArt.js +++ /dev/null @@ -1,654 +0,0 @@ -/** - * 棋盘食材 · 仿真插画 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 deleted file mode 100644 index 2c35bf60..00000000 --- a/TUICallKit-Vue3/tongji/data/gameFoodImg.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * 食材图标 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 - // 优先预热自定义图片素材,缺省回退 emoji PNG - const url = (typeof f === 'object' && f?.img) ? f.img : 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 deleted file mode 100644 index 4e8321a6..00000000 --- a/TUICallKit-Vue3/tongji/data/gameFoodLibrary.js +++ /dev/null @@ -1,205 +0,0 @@ -/** - * 全量食材库: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)}` -} - -/** 含糖等级简称:低糖 / 中糖 / 高糖 */ -export const GI_SHORT = { low: '低糖', mid: '中糖', high: '高糖' } - -/** 含糖等级对应的提示主色(绿 / 橙 / 红) */ -export const GI_COLOR = { low: '#16a34a', mid: '#ea580c', high: '#dc2626' } - -/** 部分代表食物的专属科普文案(一句话,便于老人记忆) */ -const FOOD_TIP_OVERRIDE = { - whiteRice: '精制主食,升糖快,建议小份', - stickyRice: '糯米黏软,升糖很快,要少吃', - whiteBread: '精制面食,升糖快', - mantou: '白面馒头,升糖快,配菜一起吃更稳', - youtiao: '油炸又高糖,少吃为好', - sugar: '纯糖,升糖最快,尽量不吃', - maltose: '糖分很高,升糖快', - soda: '含糖饮料,升糖很快,不建议喝', - orangeJuice: '果汁含糖高,吃整果更好', - watermelon: '很甜,升糖快,一次别吃太多', - donut: '又甜又油,升糖高', - banana: '偏甜水果,适量吃', - sweetPotato: '粗粮但有糖,可代替部分主食', - corn: '粗粮主食,适量吃', - honey: '本质是糖,别当健康品多吃', - broccoli: '新鲜蔬菜,升糖很慢,可多吃', - cucumber: '清爽蔬菜,几乎不升糖', - lettuce: '绿叶菜,升糖很低,可多吃', - celery: '高纤维蔬菜,升糖低', - apple: '带皮整果,升糖较慢', - milk: '低糖,含蛋白,适量喝好', - fish: '优质蛋白,升糖低', - beef: '蛋白为主,升糖低', - milkOats: '燕麦加奶,升糖中等,选无糖燕麦更好', - grainMantou: '杂粮做的,比白馒头稳,仍要控量', - riceNoodleSoup: '米粉升糖快,汤粉要少吃', - friedNoodles: '油多又是精面,升糖快', - centuryEggCongee: '白粥熬得软烂,升糖很快,糖友要少喝', - sandwich: '夹心面包精制碳多,升糖快', - eightTreasureCongee: '八宝粥甜糯,升糖快,要少喝', - milletCongee: '小米粥熬软升糖快,糖友控量', - shandongPancake: '煎饼加油面,升糖中等偏快', - cake: '蛋糕又甜又油,升糖快', - biscuit: '饼干又甜又碎,升糖快', - wonton: '馄饨面皮精白,升糖中等,别多吃', - blackCoffee: '不加糖的黑咖啡,升糖很低' -} - -/** 含糖等级通用科普文案 */ -const GI_TIP_DEFAULT = { - low: '升糖慢,相对放心,可常吃', - mid: '升糖中等,注意分量', - high: '含糖高、升糖快,要少吃' -} - -/** 点击食物时展示的一句话科普(认知教育用) */ -export function getFoodTip(food) { - if (!food) return '' - if (food.tip) return food.tip - if (FOOD_TIP_OVERRIDE[food.key]) return FOOD_TIP_OVERRIDE[food.key] - return GI_TIP_DEFAULT[food.gi] || '' -} - -/** 自定义图片素材目录(腾讯云 COS,优先使用真实图片而非 emoji) */ -const GAMES_IMG_BASE = 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/games' - -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', img: `${GAMES_IMG_BASE}/%E7%95%AA%E8%96%AF.png` }, - { 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: 'blackCoffee', icon: '☕', name: '黑咖啡', gi: 'low', val: -1, color: '#4E342E', img: `${GAMES_IMG_BASE}/%E9%BB%91%E5%92%96%E5%95%A1.png` }, - { key: 'wonton', icon: '🥟', name: '馄饨', gi: 'mid', val: 5, color: '#E8DCC8', img: `${GAMES_IMG_BASE}/%E9%A6%84%E9%A5%A8.png` }, - { key: 'milletCongee', icon: '🥣', name: '小米粥', gi: 'mid', val: 5, color: '#F5E6B8', img: `${GAMES_IMG_BASE}/%E5%B0%8F%E7%B1%B3%E7%B2%A5.png` }, - { key: 'shandongPancake', icon: '🥞', name: '山东煎饼', gi: 'high', val: 16, color: '#D4A574', img: `${GAMES_IMG_BASE}/%E5%B1%B1%E4%B8%9C%E7%85%8E%E9%A5%BC.png` }, - { key: 'corn', icon: '🌽', name: '玉米', gi: 'mid', val: 5, color: '#F9C513' }, - { key: 'udon', icon: '🍲', name: '乌冬面', gi: 'mid', val: 5, color: '#D9B88F' }, - { key: 'milkOats', icon: '🥣', name: '奶冲麦片', gi: 'mid', val: 6, color: '#E8D9B5', img: `${GAMES_IMG_BASE}/%E5%A5%B6%E5%86%B2%E9%BA%A6%E7%89%87.png` }, - { key: 'grainMantou', icon: '🫓', name: '杂粮馒头', gi: 'mid', val: 5, color: '#C8A878', img: `${GAMES_IMG_BASE}/%E6%9D%82%E7%B2%AE%E9%A6%92%E5%A4%B4.png` }, - - // 高 GI - { key: 'whiteRice', icon: '🍚', name: '白米饭', gi: 'high', val: 18, color: '#E0E0E0', img: `${GAMES_IMG_BASE}/%E7%B1%B3%E9%A5%AD.png` }, - { 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', img: `${GAMES_IMG_BASE}/%E6%B2%B9%E6%9D%A1.png` }, - { key: 'sandwich', icon: '🥪', name: '三明治', gi: 'high', val: 16, color: '#D9B88F', img: `${GAMES_IMG_BASE}/%E4%B8%89%E6%98%8E%E6%B2%BB.png` }, - { key: 'biscuit', icon: '🍪', name: '饼干', gi: 'high', val: 18, color: '#C9A063', img: `${GAMES_IMG_BASE}/%E9%A5%BC%E5%B9%B2.png` }, - { key: 'cake', icon: '🎂', name: '蛋糕', gi: 'high', val: 22, color: '#F48FB1', img: `${GAMES_IMG_BASE}/%E8%9B%8B%E7%B3%95.png` }, - { key: 'eightTreasureCongee', icon: '🥣', name: '八宝粥', gi: 'high', val: 17, color: '#E8C4A0', img: `${GAMES_IMG_BASE}/%E5%85%AB%E5%AE%9D%E7%B2%A5.png` }, - { 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' }, - { key: 'riceNoodleSoup', icon: '🍜', name: '汤粉', gi: 'high', val: 16, color: '#E8C9A0', img: `${GAMES_IMG_BASE}/%E6%B1%A4%E7%B2%89.png` }, - { key: 'friedNoodles', icon: '🍝', name: '炒面', gi: 'high', val: 18, color: '#C8843C', img: `${GAMES_IMG_BASE}/%E7%82%92%E9%9D%A2.png` }, - { key: 'centuryEggCongee', icon: '🥣', name: '皮蛋瘦肉粥', gi: 'high', val: 15, color: '#D9CBB0', img: `${GAMES_IMG_BASE}/%E7%9A%AE%E8%9B%8B%E7%98%A6%E8%82%89%E7%B2%A5.png` } -] - -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() { - // 优先使用自定义图片素材,缺省回退到 emoji PNG - return f.img || 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: food.img || getFoodImgUrl(food.icon) - } -} diff --git a/TUICallKit-Vue3/tongji/data/gameLevels.js b/TUICallKit-Vue3/tongji/data/gameLevels.js deleted file mode 100644 index 00f2f0fc..00000000 --- a/TUICallKit-Vue3/tongji/data/gameLevels.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * 关卡配置:面向 45+ 的认知练习,节奏放慢、无失败惩罚。 - * 每组目标只比上一组略增,步数充裕(仅展示,不会因耗尽判负)。 - */ -/** - * 指定关卡固定食材(按 key)。未配置的关卡走随机抽取逻辑。 - * 第一关固定为这 16 种带真实图片素材的食物。 - */ -const FIXED_LEVEL_FOODS = { - 1: [ - 'sandwich', - 'eightTreasureCongee', - 'milkOats', - 'milletCongee', - 'shandongPancake', - 'grainMantou', - 'riceNoodleSoup', - 'youtiao', - 'friedNoodles', - 'sweetPotato', - 'centuryEggCongee', - 'whiteRice', - 'cake', - 'biscuit', - 'wonton', - 'blackCoffee' - ] -} - -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(40, 60 - tier * 2), - // 认识目标平缓增长,避免给老人压力 - goalTargets: [6 + tier, 8 + tier], - scoreTarget: 2000 + lv * 300, - startGlucose: 50, - // 固定食材(可选):存在时本关只用这些食物 - foods: FIXED_LEVEL_FOODS[lv] || null, - boosters: { - insulin: 3, - fiber: 1, - meal: 5 - } - } -} - -/** 根据本局表现计算 1~3 星:完成即得星,步数越省星越多(不依赖血糖) */ -export function calcLevelStars({ movesLeft, levelConfig }) { - const totalMoves = levelConfig?.moves || 1 - const ratio = totalMoves > 0 ? movesLeft / totalMoves : 0 - let stars = 1 - if (ratio >= 0.3) stars = 2 - if (ratio >= 0.55) stars = 3 - return stars -} diff --git a/TUICallKit-Vue3/tongji/endless-game/README.md b/TUICallKit-Vue3/tongji/endless-game/README.md index 1709868b..2f86a736 100644 --- a/TUICallKit-Vue3/tongji/endless-game/README.md +++ b/TUICallKit-Vue3/tongji/endless-game/README.md @@ -51,8 +51,7 @@ build: { ## 说明 -- 旧地址 `/tongji/pages/game` 和 `/tongji/pages/game-endless` 只是当前项目的兼容跳转,迁移时不需要复制。 -- 当前项目原来的“糖分突袭”源码保存在 `tongji/legacy-game/game.vue`,没有注册为页面,不参与打包。 +- 旧版“糖分突袭”页面及其独占依赖已经移除,项目只注册 `/tongji/endless-game/index` 这一款小游戏。 - 连消小目标按“连消×2”累计 2 次;连续 6 次普通消除没有连消时,下一次掉落会提供连消机会。“连消×3”保留为额外积分、驼乳粉与撒花惊喜,不阻挡主线任务。 - 横向 3 个与竖向 3 个相交形成 L/T 形五消时,会在交点生成带高对比 L 标记的范围爆破棋子;该棋子再次被消除时清除周围九格。 - 进入游戏时展示“本周 7 人同行榜”,包含真实平台昵称、前后名次、差距提示和可切换的同行/亲友鼓励;顶部“本周同行”可再次打开。后端部署方式见 `PLATFORM_INTEGRATION.md`。 diff --git a/TUICallKit-Vue3/tongji/endless-game/foodInlinePlugin.js b/TUICallKit-Vue3/tongji/endless-game/foodInlinePlugin.js new file mode 100644 index 00000000..f4589e6d --- /dev/null +++ b/TUICallKit-Vue3/tongji/endless-game/foodInlinePlugin.js @@ -0,0 +1,38 @@ +import { readdirSync, readFileSync } from 'node:fs' +import { resolve } from 'node:path' + +const VIRTUAL_ID = 'virtual:tongji-endless-food-images' +const RESOLVED_VIRTUAL_ID = `\0${VIRTUAL_ID}` + +/** + * 将游戏食物图内联到 tongji 分包代码。 + * 微信代码质量会累计整个代码包内的图片/音频体积;源 JPG 继续保留,便于后续增补和迁移。 + */ +export function endlessGameFoodInlinePlugin() { + return { + name: 'tongji-endless-game-food-inline', + enforce: 'pre', + resolveId(id) { + return id === VIRTUAL_ID ? RESOLVED_VIRTUAL_ID : null + }, + load(id) { + if (id !== RESOLVED_VIRTUAL_ID) return null + + const foodDir = resolve(process.cwd(), 'tongji/endless-game/assets/food') + const images = Object.fromEntries( + readdirSync(foodDir) + .filter((fileName) => fileName.toLowerCase().endsWith('.jpg')) + .sort() + .map((fileName) => { + const filePath = resolve(foodDir, fileName) + this.addWatchFile(filePath) + const key = fileName.replace(/\.jpg$/i, '') + const dataUrl = `data:image/jpeg;base64,${readFileSync(filePath).toString('base64')}` + return [key, dataUrl] + }) + ) + + return `export default ${JSON.stringify(images)}` + }, + } +} diff --git a/TUICallKit-Vue3/tongji/endless-game/index.vue b/TUICallKit-Vue3/tongji/endless-game/index.vue index 4607b3a1..662af3e4 100644 --- a/TUICallKit-Vue3/tongji/endless-game/index.vue +++ b/TUICallKit-Vue3/tongji/endless-game/index.vue @@ -303,14 +303,7 @@ import TongjiIcon from './components/TongjiIcon.vue' import { useGameSfx } from './composables/useGameSfx.js' import { useGamePlatform } from './composables/useGamePlatform.js' import { useGameAuth } from './composables/useGameAuth.js' - -const FOOD_IMAGE_MODULES = import.meta.glob('./assets/food/*.jpg', { - eager: true, - import: 'default' -}) -const FOOD_IMAGES = Object.fromEntries( - Object.entries(FOOD_IMAGE_MODULES).map(([path, url]) => [path.split('/').pop().replace(/\.jpg$/, ''), url]) -) +import FOOD_IMAGES from 'virtual:tongji-endless-food-images' const ROWS = 5 const COLS = 4 diff --git a/TUICallKit-Vue3/tongji/pages/game.vue b/TUICallKit-Vue3/tongji/pages/game.vue deleted file mode 100644 index 783d9e4e..00000000 --- a/TUICallKit-Vue3/tongji/pages/game.vue +++ /dev/null @@ -1,2406 +0,0 @@ - - - - - diff --git a/TUICallKit-Vue3/tongji/styles/game-stitch.scss b/TUICallKit-Vue3/tongji/styles/game-stitch.scss deleted file mode 100644 index 6020ad01..00000000 --- a/TUICallKit-Vue3/tongji/styles/game-stitch.scss +++ /dev/null @@ -1,1568 +0,0 @@ -/** - * game.vue — 血糖消和乐 v2 (stitch_sugar_balance_blast code.html) - */ - -.game-page { - height: 100vh; - height: 100dvh; - /* 参考稿:顶部高光的薄荷渐变背景 */ - background: #e9f0e9; - background-image: radial-gradient(circle at 50% -20%, #ffffff 0%, #e9f0e9 60%, #d5dcd5 100%); - color: #171d19; - overflow: hidden; - position: relative; - display: flex; - flex-direction: column; -} - -.game-page.is-screen-shake { - animation: gg-screen-shake 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; -} - -@keyframes gg-screen-shake { - 0%, 100% { transform: translate3d(0, 0, 0); } - 10%, 30%, 50%, 70%, 90% { transform: translate3d(-8px, -8px, 0); } - 20%, 40%, 60%, 80% { transform: translate3d(8px, 8px, 0); } -} - -.game-page .gg-glass { - background: rgba(255, 255, 255, 0.6); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid rgba(255, 255, 255, 0.8); -} - -/* ===== Header / TopAppBar(参考稿玻璃顶栏) ===== */ -.game-page .gg-header { - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 50; - background: rgba(255, 255, 255, 0.4); - backdrop-filter: blur(24px); - -webkit-backdrop-filter: blur(24px); - border-bottom: 1px solid rgba(255, 255, 255, 0.6); - box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.02); -} - -.game-page .gg-hud-nav { - display: flex; - align-items: center; - justify-content: space-between; - position: relative; - min-height: var(--menu-height, 64rpx); - margin-bottom: 0; -} - -.game-page .gg-hud-control-group { - display: flex; - align-items: center; - gap: 16rpx; -} - -/* 玻璃圆形图标按钮 */ -.game-page .gg-hud-btn { - width: 84rpx; - height: 84rpx; - border-radius: 50%; - background: rgba(255, 255, 255, 0.7); - border: 1px solid rgba(255, 255, 255, 0.9); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04), inset 0 1px 2px rgba(255, 255, 255, 0.8); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - transition: all 0.15s ease; -} - -.game-page .gg-hud-btn--pressed { - transform: scale(0.94); - background: rgba(255, 255, 255, 0.95); -} - -.game-page .gg-hud-btn.is-muted { - opacity: 0.55; -} - -.game-page .gg-hud-level-center { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - display: flex; - flex-direction: column; - align-items: center; - z-index: 5; -} - -.game-page .gg-hud-level-text { - font-size: 44rpx; - font-weight: 800; - color: #006c4b; - line-height: 1.1; - letter-spacing: 2rpx; - text-shadow: 0 2rpx 6rpx rgba(0, 108, 75, 0.12); -} - -.game-page .gg-hud-score { - display: flex; - align-items: baseline; - gap: 6rpx; - margin-top: 2rpx; -} - -.game-page .gg-hud-score-val { - font-size: 68rpx; - font-weight: 800; - color: #006c4b; - font-variant-numeric: tabular-nums; - line-height: 1; - letter-spacing: -2rpx; - text-shadow: 0 2rpx 6rpx rgba(0, 108, 75, 0.12); -} - -.game-page .gg-hud-score.is-pop .gg-hud-score-val { - animation: gg-pop-score 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -.game-page .gg-hud-score-unit { - font-size: 28rpx; - font-weight: 700; - color: #3d4a42; -} - -/* ===== 信息横幅(参考稿玻璃卡 + 绿色 info 图标) ===== */ -.game-page .gg-info-banner { - flex-shrink: 0; - width: 100%; - max-width: 750rpx; - margin: 0 auto 20rpx; - padding: 20rpx 24rpx; - box-sizing: border-box; - display: flex; - align-items: center; - gap: 20rpx; - border-radius: 28rpx; - background: rgba(255, 255, 255, 0.6); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid rgba(255, 255, 255, 0.8); - box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.06), inset 0 1px 2px rgba(255, 255, 255, 0.9); -} - -.game-page .gg-info-banner-icon { - flex-shrink: 0; - width: 80rpx; - height: 80rpx; - border-radius: 22rpx; - display: flex; - align-items: center; - justify-content: center; - background: linear-gradient(135deg, #00a676, #006c4b); - border: 1px solid rgba(255, 255, 255, 0.2); - box-shadow: 0 8rpx 18rpx rgba(0, 108, 75, 0.28); -} - -.game-page .gg-info-banner-body { - flex: 1; - min-width: 0; -} - -.game-page .gg-info-banner-text { - font-size: 28rpx; - font-weight: 500; - color: #171d19; - line-height: 1.5; -} - -.game-page .gg-info-banner-kw { - font-size: 32rpx; - font-weight: 800; - color: #006c4b; - letter-spacing: 1rpx; -} - -/* —— 消除后:该食物的含糖 + 建议 —— */ -.game-page .gg-info-banner.is-food { - gap: 18rpx; - animation: gg-info-food-in 0.28s cubic-bezier(0.22, 1, 0.36, 1); -} -.game-page .gg-info-banner.is-food.is-low { border-color: rgba(22, 163, 74, 0.28); } -.game-page .gg-info-banner.is-food.is-mid { border-color: rgba(234, 88, 12, 0.28); } -.game-page .gg-info-banner.is-food.is-high { border-color: rgba(220, 38, 38, 0.3); } - -/* 食物图标:白底 + GI 彩色描边(覆盖默认绿色渐变) */ -.game-page .gg-info-banner-icon.is-food { - background: #ffffff; - box-shadow: 0 4rpx 12rpx rgba(15, 23, 42, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.9); - border-width: 2rpx; - border-style: solid; -} -.game-page .gg-info-banner-icon.is-food.is-low { border-color: rgba(22, 163, 74, 0.4); } -.game-page .gg-info-banner-icon.is-food.is-mid { border-color: rgba(234, 88, 12, 0.4); } -.game-page .gg-info-banner-icon.is-food.is-high { border-color: rgba(220, 38, 38, 0.45); } - -.game-page .gg-info-banner-icon.is-food .food-tile-icon--goal { - width: 60rpx; - height: 60rpx; -} - -.game-page .gg-info-food-head { - display: flex; - align-items: center; - gap: 14rpx; - margin-bottom: 8rpx; -} - -.game-page .gg-info-food-name { - font-size: 34rpx; - font-weight: 900; - color: #171d19; - line-height: 1.2; - max-width: 320rpx; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.game-page .gg-info-food-gi { - flex-shrink: 0; - display: inline-flex; - align-items: center; - padding: 4rpx 16rpx; - border-radius: 999rpx; -} -.game-page .gg-info-food-gi-text { - font-size: 24rpx; - font-weight: 800; - color: #ffffff; - letter-spacing: 1rpx; -} -.game-page .gg-info-food-gi.is-low { background: linear-gradient(135deg, #5bdda8, #00a676); box-shadow: 0 2rpx 8rpx rgba(0, 166, 118, 0.32); } -.game-page .gg-info-food-gi.is-mid { background: linear-gradient(135deg, #fbbf24, #d97706); box-shadow: 0 2rpx 8rpx rgba(217, 119, 6, 0.32); } -.game-page .gg-info-food-gi.is-high { background: linear-gradient(135deg, #f87171, #ba1a1a); box-shadow: 0 2rpx 8rpx rgba(186, 26, 26, 0.34); } - -.game-page .gg-info-food-tip { - display: block; - font-size: 26rpx; - font-weight: 600; - color: #3d4a42; - line-height: 1.4; -} -.game-page .gg-info-food-tip.is-low { color: #15803d; } -.game-page .gg-info-food-tip.is-mid { color: #c2410c; } -.game-page .gg-info-food-tip.is-high { color: #b91c1c; } - -@keyframes gg-info-food-in { - from { opacity: 0.35; transform: translateY(-6rpx); } - to { opacity: 1; transform: translateY(0); } -} - -/* ===== 次:上次消除(名称 + 含糖量),GI 三色编码学习卡 ===== */ -.game-page .gg-hud-last-food { - flex-shrink: 0; - width: 100%; - max-width: 750rpx; - margin: 0 auto 14rpx; - padding: 16rpx 18rpx 18rpx; - border-radius: 26rpx; - background: rgba(255, 255, 255, 0.92); - border: 1rpx solid rgba(0, 108, 73, 0.1); - box-sizing: border-box; - box-shadow: 0 6rpx 20rpx rgba(0, 108, 73, 0.07); -} - -/* 头部:标题 + GI 状态胶囊 */ -.game-page .gg-hud-last-food-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12rpx; - margin-bottom: 12rpx; -} - -.game-page .gg-hud-last-food-kicker { - display: flex; - align-items: center; - font-size: 24rpx; - font-weight: 800; - color: #64748b; - letter-spacing: 0.08em; - line-height: 1.3; - padding-left: 16rpx; - position: relative; -} - -.game-page .gg-hud-last-food-kicker::before { - content: ''; - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); - width: 6rpx; - height: 24rpx; - border-radius: 3rpx; - background: #006c49; -} - -/* 头部 GI 胶囊 */ -.game-page .gg-hud-last-food-chip { - display: flex; - align-items: center; - gap: 8rpx; - padding: 5rpx 16rpx 5rpx 12rpx; - border-radius: 999rpx; - font-weight: 800; - line-height: 1; - border: 1rpx solid transparent; -} - -.game-page .gg-hud-last-food-chip-dot { - width: 14rpx; - height: 14rpx; - border-radius: 50%; - flex-shrink: 0; -} - -.game-page .gg-hud-last-food-chip-text { - font-size: 24rpx; - font-weight: 800; -} - -.game-page .gg-hud-last-food-chip.is-low { - background: rgba(22, 163, 74, 0.12); - border-color: rgba(22, 163, 74, 0.22); -} -.game-page .gg-hud-last-food-chip.is-low .gg-hud-last-food-chip-dot { background: #16a34a; } -.game-page .gg-hud-last-food-chip.is-low .gg-hud-last-food-chip-text { color: #15803d; } - -.game-page .gg-hud-last-food-chip.is-mid { - background: rgba(234, 88, 12, 0.12); - border-color: rgba(234, 88, 12, 0.22); -} -.game-page .gg-hud-last-food-chip.is-mid .gg-hud-last-food-chip-dot { background: #ea580c; } -.game-page .gg-hud-last-food-chip.is-mid .gg-hud-last-food-chip-text { color: #c2410c; } - -.game-page .gg-hud-last-food-chip.is-high { - background: rgba(220, 38, 38, 0.12); - border-color: rgba(220, 38, 38, 0.24); -} -.game-page .gg-hud-last-food-chip.is-high .gg-hud-last-food-chip-dot { background: #dc2626; } -.game-page .gg-hud-last-food-chip.is-high .gg-hud-last-food-chip-text { color: #b91c1c; } - -/* 主卡:左侧 GI 强调条 + 图标 + 文案 */ -.game-page .gg-hud-last-food-card { - position: relative; - display: flex; - align-items: center; - gap: 18rpx; - padding: 16rpx 18rpx 16rpx 24rpx; - border-radius: 20rpx; - overflow: hidden; - background: #f8fafc; - border: 1rpx solid rgba(15, 23, 42, 0.06); - animation: gg-last-food-in 0.24s cubic-bezier(0.22, 1, 0.36, 1); -} - -/* 左侧强调条(GI 三色) */ -.game-page .gg-hud-last-food-card::before { - content: ''; - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 8rpx; - border-radius: 8rpx; - background: #cbd5e1; -} - -@media (prefers-reduced-motion: reduce) { - .game-page .gg-hud-last-food-card { - animation: none; - } -} - -.game-page .gg-hud-last-food-card.is-high { - background: linear-gradient(90deg, rgba(254, 226, 226, 0.85) 0%, rgba(254, 242, 242, 0.6) 60%); - border-color: rgba(220, 38, 38, 0.18); -} -.game-page .gg-hud-last-food-card.is-high::before { background: #dc2626; } - -.game-page .gg-hud-last-food-card.is-mid { - background: linear-gradient(90deg, rgba(255, 237, 213, 0.85) 0%, rgba(255, 251, 235, 0.6) 60%); - border-color: rgba(234, 88, 12, 0.16); -} -.game-page .gg-hud-last-food-card.is-mid::before { background: #ea580c; } - -.game-page .gg-hud-last-food-card.is-low { - background: linear-gradient(90deg, rgba(220, 252, 231, 0.85) 0%, rgba(240, 253, 244, 0.6) 60%); - border-color: rgba(22, 163, 74, 0.16); -} -.game-page .gg-hud-last-food-card.is-low::before { background: #16a34a; } - -/* 图标:白底 + GI 彩色描边环 */ -.game-page .gg-hud-last-food-icon { - width: 80rpx; - height: 80rpx; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - border-radius: 18rpx; - background: #fff; - border: 2rpx solid rgba(15, 23, 42, 0.06); - box-shadow: 0 3rpx 10rpx rgba(15, 23, 42, 0.08); -} - -.game-page .gg-hud-last-food-icon.is-low { border-color: rgba(22, 163, 74, 0.35); } -.game-page .gg-hud-last-food-icon.is-mid { border-color: rgba(234, 88, 12, 0.35); } -.game-page .gg-hud-last-food-icon.is-high { border-color: rgba(220, 38, 38, 0.4); } - -.game-page .gg-hud-last-food-icon .food-tile-icon--goal { - width: 62rpx; - height: 62rpx; -} - -.game-page .gg-hud-last-food-body { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 10rpx; -} - -.game-page .gg-hud-last-food-name { - font-size: 36rpx; - font-weight: 900; - color: #0f172a; - line-height: 1.2; - word-break: break-all; -} - -.game-page .gg-hud-last-food-sugar-row { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8rpx 14rpx; -} - -/* 含糖等级徽标:GI 三色实底 + 白字(保证对比度) */ -.game-page .gg-hud-last-food-gi { - display: inline-flex; - align-items: center; - padding: 5rpx 16rpx; - border-radius: 999rpx; - line-height: 1.3; -} - -.game-page .gg-hud-last-food-gi-text { - font-size: 26rpx; - font-weight: 800; - color: #fff; - letter-spacing: 0.02em; -} - -.game-page .gg-hud-last-food-gi.is-low { background: #16a34a; box-shadow: 0 2rpx 8rpx rgba(22, 163, 74, 0.3); } -.game-page .gg-hud-last-food-gi.is-mid { background: #ea580c; box-shadow: 0 2rpx 8rpx rgba(234, 88, 12, 0.3); } -.game-page .gg-hud-last-food-gi.is-high { background: #dc2626; box-shadow: 0 2rpx 8rpx rgba(220, 38, 38, 0.32); } - -.game-page .gg-hud-last-food-sugar-hint { - font-size: 26rpx; - font-weight: 700; - color: #475569; - line-height: 1.35; -} - -.game-page .gg-hud-last-food-card.is-high .gg-hud-last-food-sugar-hint { color: #b91c1c; } -.game-page .gg-hud-last-food-card.is-mid .gg-hud-last-food-sugar-hint { color: #c2410c; } -.game-page .gg-hud-last-food-card.is-low .gg-hud-last-food-sugar-hint { color: #15803d; } - -/* 空态占位 */ -.game-page .gg-hud-last-food-placeholder { - display: flex; - align-items: center; - gap: 12rpx; - padding: 18rpx 16rpx; - border-radius: 20rpx; - background: #f8fafc; - border: 1rpx dashed rgba(15, 23, 42, 0.12); -} - -.game-page .gg-hud-last-food-placeholder-dot { - width: 16rpx; - height: 16rpx; - border-radius: 50%; - flex-shrink: 0; - background: #cbd5e1; -} - -.game-page .gg-hud-last-food-placeholder text { - font-size: 27rpx; - font-weight: 600; - color: #94a3b8; - line-height: 1.45; -} - -@keyframes gg-last-food-in { - from { - opacity: 0; - transform: translateY(6rpx); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* 血糖圆环(主区摘要用 mini) */ -.game-page .gg-glucose-ring { - width: 88rpx; - height: 88rpx; - border-radius: 50%; - padding: 6rpx; - flex-shrink: 0; - box-sizing: border-box; - transition: all 0.3s ease; -} - -.game-page .gg-glucose-ring-inner { - width: 100%; - height: 100%; - border-radius: 50%; - background: #fff; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 0; - box-shadow: inset 0 0 0 1rpx rgba(0, 108, 73, 0.05), 0 2rpx 6rpx rgba(0, 108, 73, 0.04); -} - -.game-page .gg-glucose-value { - font-size: 30rpx; - font-weight: 900; - color: #0f172a; - line-height: 1; - font-variant-numeric: tabular-nums; -} - -.game-page .gg-glucose-brief { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 10rpx; - min-width: 0; -} - -.game-page .gg-glucose-title { - font-size: 30rpx; - font-weight: 800; - color: #334155; - line-height: 1.2; -} - -.game-page .gg-glucose-status { - font-size: 28rpx; - font-weight: 800; - color: #ea580c; - padding: 6rpx 18rpx; - border-radius: 999rpx; - background: rgba(234, 88, 12, 0.1); - white-space: nowrap; - line-height: 1.2; -} - -.game-page .gg-glucose-status.is-warn-low { - color: #2563eb; - background: rgba(59, 130, 246, 0.1); -} - -.game-page .gg-glucose-status.is-warn-high { - color: #dc2626; - background: rgba(239, 68, 68, 0.1); - animation: gg-alert-pulse 1.5s infinite; -} - -.game-page .gg-glucose-ring--mini { - width: 56rpx; - height: 56rpx; - padding: 4rpx; -} - -.game-page .gg-glucose-ring--mini .gg-glucose-value { - font-size: 22rpx; -} - -.game-page .gg-glucose-status--mini { - font-size: 22rpx; - padding: 2rpx 10rpx; -} - -@keyframes gg-alert-pulse { - 0% { opacity: 0.8; } - 50% { opacity: 1; box-shadow: 0 0 8rpx rgba(239, 68, 68, 0.3); } - 100% { opacity: 0.8; } -} - -@keyframes gg-pop-score { - 0% { transform: scale(1); } - 50% { transform: scale(1.12); color: #ea580c; } - 100% { transform: scale(1); } -} - -/* ===== 主:棋盘区 ===== */ -.game-page .gg-board-hero { - flex: 1; - width: 100%; - min-height: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -.game-page .gg-board-hint-inline { - flex-shrink: 0; - width: 100%; - max-width: 750rpx; - display: flex; - align-items: center; - justify-content: center; - gap: 10rpx; - margin-bottom: 12rpx; - padding: 0 8rpx; - box-sizing: border-box; -} - -.game-page .gg-hint-tag { - font-size: 22rpx; - font-weight: 800; - color: #ffffff; - background: #006c49; - padding: 4rpx 12rpx; - border-radius: 8rpx; - flex-shrink: 0; - line-height: 1.3; -} - -.game-page .gg-board-hint-text { - font-size: 26rpx; - font-weight: 600; - color: #475569; - line-height: 1.35; - text-align: center; -} - -.game-page .gg-board-stack { - display: flex; - flex-direction: column; - align-items: stretch; - box-sizing: border-box; -} - -.game-page .gg-board-zone { - position: relative; - width: 100%; -} - -/* ===== Main / Board ===== */ -.game-page .gg-main { - position: fixed; - left: 0; - right: 0; - top: var(--board-area-top, 120px); - bottom: var(--board-area-bottom, 180px); - z-index: 10; - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: flex-start; - box-sizing: border-box; - overflow: visible; - padding: 14rpx 20rpx 0; -} - -/* gg-hint-banner 已移除,提示直接显示在棋盘食物上 */ - -/* 无可用移动时的小型提示条 */ -.game-page .gg-no-move-tip { - display: flex; - align-items: center; - gap: 10rpx; - padding: 12rpx 24rpx; - margin-bottom: 12rpx; - border-radius: 999rpx; - background: rgba(234, 88, 12, 0.1); - border: 1rpx solid rgba(234, 88, 12, 0.3); - box-sizing: border-box; -} - -.game-page .gg-no-move-tip-text { - font-size: 28rpx; - font-weight: 600; - color: #c2410c; -} - -.game-page .gg-board-wrap { - /* 5 行 × 4 列棋盘宽高由 JS boardWrapStyle 注入 */ - flex-shrink: 0; - position: relative; - box-sizing: border-box; -} - -.game-page .game-grid { - display: flex; - flex-direction: column; - border-radius: 48rpx; - background: rgba(255, 255, 255, 0.6); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - box-shadow: - 0 12rpx 32rpx rgba(0, 0, 0, 0.06), - inset 0 1px 2px rgba(255, 255, 255, 0.9); - border: 1px solid rgba(255, 255, 255, 0.8); - box-sizing: border-box; - overflow: visible; - flex-shrink: 0; -} - -.game-page .game-row { - display: flex; - flex-direction: row; - flex-shrink: 0; -} - -.game-page .game-cell { - position: relative; - flex-shrink: 0; - overflow: visible; - background: transparent; - border-radius: 0; - box-shadow: none; -} - -.game-page .game-tile { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - border-radius: 28rpx; - display: flex; - align-items: stretch; - justify-content: stretch; - background: linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(244, 251, 245, 0.6)); - box-shadow: - 0 10rpx 20rpx rgba(0, 108, 75, 0.08), - inset 0 2rpx 4rpx rgba(255, 255, 255, 0.8), - inset 0 -2rpx 6rpx rgba(0, 0, 0, 0.02); - border: 1px solid rgba(255, 255, 255, 0.9); - transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.25s ease, opacity 0.2s; - z-index: 1; - box-sizing: border-box; - overflow: visible; -} - -.game-page .game-tile.is-dragging { - z-index: 20; - will-change: transform; -} - -.game-page .game-tile.is-drag-neighbor { - z-index: 15; - will-change: transform; -} - -.game-page .game-tile.is-dragging.game-tile--pressed { - transform: none; -} - -.game-page .game-tile.is-drop-start { - opacity: 0; - transform: translateY(-30%); - transition: none; -} - -.game-page .game-tile.is-drop-animate { - opacity: 1; - transform: translateY(0); - transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease; -} - -.game-page .game-tile.is-selected { - transform: scale(1.08); - z-index: 20; - border: 2px solid #00a676; - border-radius: 28rpx; - box-shadow: - 0 0 20rpx rgba(0, 166, 118, 0.35), - inset 0 0 10rpx rgba(0, 166, 118, 0.12); -} - -.game-page .game-tile--pressed { - transform: scale(0.92); -} - -.game-page .game-tile.is-selected.game-tile--pressed { - transform: scale(1.06); -} - -/* 3 连:糖果弹跳消除(先弹大再缩没,带高光) */ -.game-page .game-tile.is-matched.is-match-fx-3, -.game-page .game-tile.is-matched:not(.is-match-fx-4):not(.is-match-fx-5) { - animation: gg-candy-pop 0.42s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; - z-index: 6; -} - -@keyframes gg-candy-pop { - 0% { transform: scale(1); opacity: 1; filter: brightness(1); } - 35% { transform: scale(1.35); opacity: 1; filter: brightness(1.7) saturate(1.4); } - 60% { transform: scale(1.12); opacity: 0.9; } - 100% { transform: scale(0); opacity: 0; filter: brightness(2); } -} - -/* 4 连:弹性旋转 + 金色高光 */ -.game-page .game-tile.is-matched.is-match-fx-4 { - animation: gg-candy-pop-4 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; - z-index: 7; - box-shadow: - 0 0 0 6rpx rgba(255, 213, 74, 0.6), - 0 0 28px rgba(255, 200, 40, 0.95), - 0 0 56px rgba(255, 170, 20, 0.5); -} - -@keyframes gg-candy-pop-4 { - 0% { transform: scale(1) rotate(0deg); opacity: 1; filter: brightness(1) saturate(1); } - 25% { transform: scale(1.6) rotate(-10deg); opacity: 1; filter: brightness(1.9) saturate(1.8); } - 50% { transform: scale(1.32) rotate(8deg); opacity: 0.95; filter: brightness(1.5) saturate(1.5); } - 100% { transform: scale(0) rotate(20deg); opacity: 0; filter: brightness(2.4); } -} - -/* 5 连+:彩虹爆闪 + 强光晕 */ -.game-page .game-tile.is-matched.is-match-fx-5 { - animation: gg-candy-pop-5 0.72s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; - z-index: 8; - box-shadow: - 0 0 0 8rpx rgba(255, 255, 255, 0.7), - 0 0 44px rgba(255, 215, 0, 1), - 0 0 90px rgba(255, 90, 156, 0.6); -} - -@keyframes gg-candy-pop-5 { - 0% { transform: scale(1) rotate(0deg); opacity: 1; filter: brightness(1) saturate(1) hue-rotate(0deg); } - 20% { transform: scale(2) rotate(-14deg); opacity: 1; filter: brightness(2.6) saturate(2.4) hue-rotate(30deg); } - 45% { transform: scale(1.7) rotate(12deg); opacity: 0.9; filter: brightness(2) saturate(2) hue-rotate(-20deg); } - 100% { transform: scale(0) rotate(26deg); opacity: 0; filter: brightness(3); } -} - -/* ===== 提示高亮:方块增强发光 ===== */ -.game-page .game-tile.is-hint { - border: 4rpx solid #10b981; - z-index: 10; - animation: gg-hint-glow 0.9s ease-in-out infinite alternate; - box-shadow: 0 0 0 4rpx rgba(16, 185, 129, 0.3), 0 0 20rpx rgba(16, 185, 129, 0.4); -} - -/* 提示标记角标 */ -.game-page .game-tile.is-hint::after { - content: '✦'; - position: absolute; - top: 4rpx; - right: 6rpx; - font-size: 18rpx; - color: #006c49; - line-height: 1; - pointer-events: none; - animation: gg-hint-badge-spin 1.8s linear infinite; -} - -@keyframes gg-hint-badge-spin { - from { transform: rotate(0deg) scale(1); opacity: 1; } - 50% { transform: rotate(180deg) scale(1.3); opacity: 0.8; } - to { transform: rotate(360deg) scale(1); opacity: 1; } -} - -/* ===== 提示交换箭头(覆盖在两食物正中间) ===== */ -.game-page .gg-hint-arrow { - position: absolute; - width: 48rpx; - height: 48rpx; - background: linear-gradient(135deg, #006c49, #10b981); - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-size: 26rpx; - color: #fff; - font-weight: 900; - pointer-events: none; - z-index: 30; - transform: translate(-50%, -50%); - box-shadow: 0 4rpx 16rpx rgba(0, 108, 73, 0.45); - animation: gg-hint-arrow-pulse 0.7s ease-in-out infinite alternate; -} - -@keyframes gg-hint-arrow-pulse { - from { - transform: translate(-50%, -50%) scale(1); - box-shadow: 0 4rpx 16rpx rgba(0, 108, 73, 0.4); - } - to { - transform: translate(-50%, -50%) scale(1.25); - box-shadow: 0 4rpx 28rpx rgba(0, 108, 73, 0.7), 0 0 0 8rpx rgba(16, 185, 129, 0.15); - } -} - -/* 扩散发光光环(颜色/粗细/光晕由 JS 内联,弹性扩散) */ -.game-page .gg-match-ripple { - position: absolute; - border-radius: 50%; - border-style: solid; - border-color: rgba(255, 255, 255, 0.9); - border-width: 4px; - transform: translate(-50%, -50%) scale(0.2); - animation: gg-ripple-burst 0.6s cubic-bezier(0.22, 0.61, 0.36, 1) forwards; - pointer-events: none; - will-change: transform, opacity; -} - -@keyframes gg-ripple-burst { - 0% { transform: translate(-50%, -50%) scale(0.2); opacity: 0.95; } - 55% { opacity: 0.55; } - 100% { transform: translate(-50%, -50%) scale(2.9); opacity: 0; } -} - -/* 糖果粒子:圆点用背景色,星屑用字形 + 发光 */ -.game-page .gg-particle { - position: absolute; - border-radius: 50%; - pointer-events: none; - display: flex; - align-items: center; - justify-content: center; - line-height: 1; - will-change: transform, opacity, left, top; -} - -.game-page .gg-particle.is-star { - border-radius: 0; - background-color: transparent !important; - text-shadow: 0 0 6px currentColor, 0 0 12px currentColor; -} - -.game-page .gg-particle text { - color: inherit; - line-height: 1; -} - -/* 大消除全屏闪光 */ -.game-page .gg-screen-flash { - position: fixed; - inset: 0; - pointer-events: none; - transition: opacity 0.28s ease-out; -} - -@keyframes gg-pop-out { - 0% { transform: scale(1); opacity: 1; } - 50% { transform: scale(1.3); opacity: 0.8; } - 100% { transform: scale(0); opacity: 0; } -} - -@keyframes gg-hint-glow { - from { transform: scale(1); filter: brightness(1.05); } - to { transform: scale(1.1); filter: brightness(1.25) saturate(1.3); } -} - -/* 棋盘内食材图标 — 撑满格子,避免 flex 居中导致小程序 image 贴顶 */ -.game-page .game-tile .food-tile-icon { - pointer-events: none; - z-index: 2; - flex: 1; - align-self: stretch; - width: 100%; - height: 100%; - min-width: 0; - min-height: 0; -} - -.game-page .game-tile .food-tile-icon--tile .food-tile-icon__emoji { - font-size: inherit; -} - -.game-page .tile-icon { - font-size: 48rpx; - line-height: 1; - pointer-events: none; -} - -/* ===== 棋子含糖等级常驻角标(认知核心:一眼看出高/中/低糖) ===== */ -.game-page .gg-tile-gi { - position: absolute; - top: -12rpx; - left: 50%; - transform: translateX(-50%); - z-index: 5; - min-width: 40rpx; - padding: 6rpx 16rpx; - border-radius: 999rpx; - display: flex; - align-items: center; - justify-content: center; - font-size: 22rpx; - font-weight: 800; - color: #ffffff; - line-height: 1; - letter-spacing: 1rpx; - pointer-events: none; - border: 1px solid rgba(255, 255, 255, 0.6); - text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.2); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); -} - -.game-page .gg-tile-gi.is-low { - background: linear-gradient(135deg, rgba(91, 221, 168, 0.95), rgba(0, 166, 118, 0.95)); - box-shadow: 0 4rpx 12rpx rgba(0, 166, 118, 0.4), inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4); -} -.game-page .gg-tile-gi.is-mid { - background: linear-gradient(135deg, rgba(251, 191, 36, 0.95), rgba(217, 119, 6, 0.95)); - box-shadow: 0 4rpx 12rpx rgba(217, 119, 6, 0.4), inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4); -} -.game-page .gg-tile-gi.is-high { - background: linear-gradient(135deg, rgba(248, 113, 113, 0.95), rgba(186, 26, 26, 0.95)); - box-shadow: 0 4rpx 12rpx rgba(186, 26, 26, 0.4), inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4); -} - -/* ===== 点击食物的大字科普卡 ===== */ -.game-page .gg-food-card { - position: fixed; - left: 50%; - top: 30%; - transform: translateX(-50%); - z-index: 150; - width: 560rpx; - max-width: 86vw; - padding: 32rpx 36rpx; - border-radius: 32rpx; - background: #ffffff; - border: 6rpx solid #16a34a; - box-shadow: 0 20rpx 60rpx rgba(22, 29, 25, 0.22); - pointer-events: none; - animation: gg-food-card-in 0.28s cubic-bezier(0.34, 1.56, 0.64, 1); -} - -.game-page .gg-food-card.is-mid { border-color: #ea580c; } -.game-page .gg-food-card.is-high { border-color: #dc2626; } - -.game-page .gg-food-card-head { - display: flex; - align-items: center; - gap: 20rpx; - margin-bottom: 16rpx; -} - -.game-page .gg-food-card-name { - flex: 1; - min-width: 0; - font-size: 46rpx; - font-weight: 900; - color: #161d19; - line-height: 1.2; -} - -.game-page .gg-food-card-gi { - flex-shrink: 0; - font-size: 32rpx; - font-weight: 900; - color: #ffffff; - padding: 8rpx 24rpx; - border-radius: 999rpx; - line-height: 1.2; -} - -.game-page .gg-food-card-tip { - display: block; - font-size: 34rpx; - font-weight: 600; - color: #3c4a42; - line-height: 1.5; -} - -@keyframes gg-food-card-in { - from { transform: translateX(-50%) scale(0.86); opacity: 0; } - to { transform: translateX(-50%) scale(1); opacity: 1; } -} - -/* ===== FX Layer ===== */ -.game-page .gg-fx-layer { - position: fixed; - inset: 0; - pointer-events: none; - z-index: 100; -} - -.game-page .gg-floating-val { - position: absolute; - font-weight: 900; - font-size: 44rpx; - text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1); - transform: translateX(-50%); - animation: gg-float-up 1s ease-out forwards; -} - -@keyframes gg-float-up { - 0% { transform: translate(-50%, 0) scale(0.5); opacity: 0; } - 20% { transform: translate(-50%, -20px) scale(1.2); opacity: 1; } - 100% { transform: translate(-50%, -60px) scale(1); opacity: 0; } -} - -/* ===== Footer boosters(辅,弱化) ===== */ -.game-page .gg-footer { - position: fixed; - left: 0; - right: 0; - bottom: 0; - z-index: 40; - border-radius: 64rpx 64rpx 0 0; - border-top: 1px solid #ffffff; - box-shadow: 0 -12rpx 40rpx rgba(0, 0, 0, 0.08); - padding-top: 24rpx; - padding-left: 24rpx; - padding-right: 24rpx; - background: rgba(255, 255, 255, 0.7); - backdrop-filter: blur(28px); - -webkit-backdrop-filter: blur(28px); -} - -.game-page .gg-boosters { - display: flex; - justify-content: space-around; - align-items: flex-start; - max-width: 750rpx; - margin: 0 auto; -} - -/* 单个道具:图标卡 + 角标 + 文字 */ -.game-page .gg-booster-btn { - display: flex; - flex-direction: column; - align-items: center; - padding: 8rpx 0; - width: 184rpx; - background: transparent; - border: none; - transition: transform 0.2s ease; -} - -.game-page .gg-booster-tile { - position: relative; - width: 112rpx; - height: 112rpx; - border-radius: 36rpx; - display: flex; - align-items: center; - justify-content: center; - background: rgba(255, 255, 255, 0.85); - border: 1px solid #ffffff; - box-shadow: 0 8rpx 18rpx rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.9); - transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s ease; -} - -.game-page .gg-booster-btn--pressed .gg-booster-tile { - transform: scale(0.92); - box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.06); -} - -.game-page .gg-booster-btn.is-disabled { - opacity: 0.4; - filter: grayscale(100%); - pointer-events: none !important; -} - -.game-page .gg-booster-count { - position: absolute; - top: -12rpx; - right: -12rpx; - min-width: 50rpx; - height: 50rpx; - padding: 0 10rpx; - border-radius: 26rpx; - color: #fff; - font-size: 28rpx; - font-weight: 800; - display: flex; - align-items: center; - justify-content: center; - border: 3rpx solid #ffffff; - box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.18); - text-shadow: 0 1rpx 1rpx rgba(0, 0, 0, 0.2); - z-index: 2; -} - -.game-page .gg-booster-count.is-green { background: linear-gradient(135deg, #00a676, #006c4b); box-shadow: 0 4rpx 10rpx rgba(0, 166, 118, 0.3); } -.game-page .gg-booster-count.is-orange { background: linear-gradient(135deg, #f59e0b, #d97706); box-shadow: 0 4rpx 10rpx rgba(217, 119, 6, 0.3); } -.game-page .gg-booster-count.is-rose { background: linear-gradient(135deg, #ef4444, #ba1a1a); box-shadow: 0 4rpx 10rpx rgba(186, 26, 26, 0.3); } - -.game-page .gg-booster-icon { - transition: transform 0.2s ease; -} - -.game-page .gg-booster-label { - margin-top: 14rpx; - font-size: 30rpx; - font-weight: 700; - color: #3d4a42; - letter-spacing: 1rpx; -} - -/* ===== 高糖消除红色警示弹窗 ===== */ -.game-page .gg-high-alert-overlay { - position: fixed; - inset: 0; - z-index: 210; - background: rgba(127, 29, 29, 0.45); - backdrop-filter: blur(16rpx); - display: flex; - align-items: center; - justify-content: center; - padding: 48rpx; - animation: gg-high-alert-fade-in 0.28s ease-out; -} - -.game-page .gg-high-alert-card { - width: 100%; - max-width: 560rpx; - padding: 48rpx 40rpx 44rpx; - border-radius: 40rpx; - background: linear-gradient(165deg, #fff5f5 0%, #ffffff 42%); - border: 4rpx solid #dc2626; - box-shadow: - 0 0 0 8rpx rgba(220, 38, 38, 0.12), - 0 24rpx 64rpx rgba(185, 28, 28, 0.35); - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - animation: gg-high-alert-pop 0.42s cubic-bezier(0.34, 1.45, 0.64, 1); -} - -.game-page .gg-high-alert-icon-wrap { - width: 96rpx; - height: 96rpx; - border-radius: 50%; - background: linear-gradient(145deg, #ef4444, #b91c1c); - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 24rpx; - box-shadow: 0 12rpx 32rpx rgba(220, 38, 38, 0.45); - animation: gg-high-badge-pulse 1.2s ease-in-out infinite; -} - -.game-page .gg-high-alert-badge-row { - display: flex; - align-items: center; - gap: 12rpx; - margin-bottom: 16rpx; - flex-wrap: wrap; - justify-content: center; -} - -.game-page .gg-high-alert-badge { - font-size: 32rpx; - font-weight: 900; - color: #ffffff; - background: #dc2626; - padding: 8rpx 24rpx; - border-radius: 12rpx; - letter-spacing: 4rpx; - box-shadow: 0 6rpx 16rpx rgba(220, 38, 38, 0.4); -} - -.game-page .gg-high-alert-badge-sub { - font-size: 24rpx; - font-weight: 700; - color: #b91c1c; -} - -.game-page .gg-high-alert-title { - font-size: 40rpx; - font-weight: 900; - color: #991b1b; - line-height: 1.25; - margin-bottom: 12rpx; -} - -.game-page .gg-high-alert-name { - font-size: 44rpx; - font-weight: 900; - color: #dc2626; - line-height: 1.2; - margin-bottom: 16rpx; -} - -.game-page .gg-high-alert-tip { - font-size: 30rpx; - font-weight: 600; - color: #7f1d1d; - line-height: 1.45; - opacity: 0.9; -} - -@keyframes gg-high-alert-fade-in { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes gg-high-alert-pop { - from { - opacity: 0; - transform: scale(0.82) translateY(24rpx); - } - to { - opacity: 1; - transform: scale(1) translateY(0); - } -} - -@keyframes gg-high-badge-pulse { - 0%, 100% { transform: scale(1); box-shadow: 0 12rpx 32rpx rgba(220, 38, 38, 0.45); } - 50% { transform: scale(1.06); box-shadow: 0 16rpx 40rpx rgba(220, 38, 38, 0.55); } -} - -/* ===== Result overlay ===== */ -.game-page .gg-result-overlay { - position: fixed; - inset: 0; - z-index: 200; - background: rgba(22, 29, 25, 0.35); - backdrop-filter: blur(20rpx); - display: flex; - align-items: center; - justify-content: center; - padding: 48rpx; -} - -.game-page .gg-result-card { - width: 100%; - max-width: 600rpx; - border-radius: 48rpx; - padding: 64rpx 48rpx; - background: #ffffff; - border: 1.5rpx solid rgba(255, 255, 255, 0.8); - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - box-shadow: 0 30rpx 80rpx rgba(22, 29, 25, 0.18); -} - -.game-page .gg-result-card.is-high-clear { - border: 4rpx solid #dc2626; - background: linear-gradient(180deg, #fff5f5 0%, #ffffff 36%); - box-shadow: - 0 0 0 6rpx rgba(220, 38, 38, 0.1), - 0 30rpx 80rpx rgba(185, 28, 28, 0.22); -} - -.game-page .gg-result-high-head { - display: flex; - align-items: center; - gap: 12rpx; - margin-bottom: 20rpx; -} - -.game-page .gg-result-high-badge { - font-size: 30rpx; - font-weight: 900; - color: #fff; - background: #dc2626; - padding: 6rpx 20rpx; - border-radius: 10rpx; - letter-spacing: 3rpx; - box-shadow: 0 4rpx 14rpx rgba(220, 38, 38, 0.35); -} - -.game-page .gg-result-high-tag { - font-size: 28rpx; - font-weight: 800; - color: #b91c1c; - padding: 6rpx 16rpx; - border-radius: 999rpx; - background: rgba(254, 226, 226, 0.9); - border: 2rpx solid rgba(220, 38, 38, 0.35); -} - -.game-page .gg-result-card.is-high-clear .gg-result-title { - color: #991b1b; -} - -.game-page .gg-result-card.is-high-clear .gg-result-sub { - color: #7f1d1d; - font-weight: 600; -} - -.game-page .gg-result-stats.is-high-clear { - background: #fef2f2; - border: 2rpx solid rgba(220, 38, 38, 0.2); -} - -.game-page .gg-result-stat-val.is-high-val { - color: #dc2626; - display: flex; - align-items: center; - justify-content: flex-end; - gap: 8rpx; - flex-wrap: wrap; -} - -.game-page .gg-result-inline-badge { - font-size: 22rpx; - font-weight: 900; - color: #fff; - background: #dc2626; - padding: 2rpx 10rpx; - border-radius: 6rpx; - line-height: 1.3; -} - -.game-page .gg-result-btn.is-high-primary { - background: linear-gradient(135deg, #ef4444, #b91c1c); - box-shadow: 0 10rpx 28rpx rgba(220, 38, 38, 0.35); -} - -.game-page .gg-result-title { - font-size: 44rpx; - font-weight: 900; - color: #161d19; - margin-bottom: 24rpx; -} - -.game-page .gg-result-sub { - font-size: 34rpx; - color: #2b3a32; - margin-bottom: 24rpx; - line-height: 1.6; -} - -.game-page .gg-combo-banner { - position: fixed; - left: 50%; - top: 38%; - transform: translate(-50%, -50%) scale(0.6); - z-index: 120; - padding: 20rpx 48rpx; - border-radius: 999rpx; - background: linear-gradient(135deg, #fbbf24, #f59e0b); - color: #fff; - font-size: 40rpx; - font-weight: 900; - text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2); - box-shadow: 0 12rpx 40rpx rgba(245, 158, 11, 0.45); - animation: gg-combo-pop 1.1s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; - pointer-events: none; -} - -.game-page .gg-combo-banner.is-tier-2 { - background: linear-gradient(135deg, #fb923c, #ea580c); - font-size: 44rpx; -} - -.game-page .gg-combo-banner.is-tier-3 { - background: linear-gradient(135deg, #f472b6, #db2777); - font-size: 48rpx; - box-shadow: 0 16rpx 48rpx rgba(219, 39, 119, 0.5); -} - -@keyframes gg-combo-pop { - 0% { transform: translate(-50%, -50%) scale(0.4); opacity: 0; } - 20% { transform: translate(-50%, -50%) scale(1.15); opacity: 1; } - 70% { transform: translate(-50%, -50%) scale(1); opacity: 1; } - 100% { transform: translate(-50%, -60%) scale(0.85); opacity: 0; } -} - -.game-page .gg-result-stars { - display: flex; - gap: 16rpx; - margin-bottom: 20rpx; -} - -.game-page .gg-result-star { - font-size: 56rpx; - color: #e2e8f0; - line-height: 1; -} - -.game-page .gg-result-star.is-lit { - color: #fbbf24; - text-shadow: 0 4rpx 12rpx rgba(251, 191, 36, 0.5); -} - -.game-page .gg-result-stats { - width: 100%; - margin-bottom: 32rpx; - padding: 24rpx 32rpx; - background: #f8faf9; - border-radius: 24rpx; -} - -.game-page .gg-result-stat { - display: flex; - justify-content: space-between; - padding: 10rpx 0; -} - -.game-page .gg-result-stat-label { - font-size: 32rpx; - color: #475569; -} - -.game-page .gg-result-stat-val { - font-size: 32rpx; - font-weight: 800; - color: #161d19; -} - -.game-page .gg-result-actions { - display: flex; - flex-wrap: wrap; - gap: 16rpx; - width: 100%; -} - -.game-page .gg-result-btn { - flex: 1 1 40%; - min-width: 180rpx; - padding: 32rpx 0; - border-radius: 999rpx; - font-size: 36rpx; - font-weight: 800; - text-align: center; - background: #eef6ee; - color: #2b3a32; - transition: all 0.2s; -} - -.game-page .gg-result-btn.is-primary { - background: linear-gradient(135deg, #10b981, #006c49); - color: #fff; - box-shadow: 0 10rpx 24rpx rgba(0, 108, 73, 0.2); -} - -.game-page .gg-result-btn:active { - transform: scale(0.97); -} - -@media (prefers-reduced-motion: reduce) { - .game-page.is-screen-shake, - .game-page .game-tile.is-hint, - .game-page .gg-food-card, - .game-page .gg-combo-banner, - .game-page .gg-floating-val { - animation: none !important; - } -} diff --git a/TUICallKit-Vue3/vite.config.ts b/TUICallKit-Vue3/vite.config.ts index 99a36031..a958e2c2 100644 --- a/TUICallKit-Vue3/vite.config.ts +++ b/TUICallKit-Vue3/vite.config.ts @@ -1,11 +1,25 @@ import { defineConfig } from 'vite' import uni from '@dcloudio/vite-plugin-uni' +import Optimization from '@uni-ku/bundle-optimizer' import { endlessGameAssetFileNames } from './tongji/endless-game/viteAssetOutput.js' +import { endlessGameFoodInlinePlugin } from './tongji/endless-game/foodInlinePlugin.js' // uni-app 工程根目录就是源码目录(HBuilderX 兼容) // 编译产物默认输出到 ./dist// export default defineConfig({ - plugins: [uni()], + plugins: [ + endlessGameFoodInlinePlugin(), + uni(), + Optimization({ + enable: { + optimization: true, + 'async-import': false, + 'async-component': false, + }, + dts: false, + logger: false, + }), + ], build: { rollupOptions: { output: {