Compare commits

..
Author SHA1 Message Date
大哥大哥的大哥哥 62467dc01c feat(tongji): 新增控糖消消乐小游戏 2026-07-17 09:30:22 +08:00
52 changed files with 2722 additions and 131 deletions
+9
View File
@@ -223,6 +223,15 @@
"backgroundColor": "#c7d2fe",
"disableScroll": true
}
},
{
"path": "endless-game/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "控糖消消乐",
"backgroundColor": "#eefbf4",
"disableScroll": false
}
}
]
}
@@ -0,0 +1,56 @@
# 控糖消消乐独立功能包
此目录包含“控糖消消乐”无尽三消版的全部运行文件,可以整体复制到另一个 uni-app Vue 3 项目的 `tongji/endless-game/` 下。
## 目录内容
- `index.vue`:页面、棋盘算法、关卡主题、任务、道具、三/四/五连奖励和适老化交互。
- `game-endless.scss`:完整页面与动画样式。
- `components/TongjiIcon.vue`:本功能包使用的图标组件。
- `composables/useGameSfx.js`:滑动、掉落、消除、连击和大奖音效。
- `utils/svgDataUrl.js`:图标编码工具。
- `assets/food/`:本游戏使用的全部食品与驼乳粉图片。
- `viteAssetOutput.js`:把食品图输出到本功能分包,避免增加微信小程序主包体积。
## 迁移步骤
1. 整体复制 `endless-game` 文件夹到目标项目的 `tongji/` 目录。
2. 在目标项目的 `vite.config` 中引入素材输出规则,并放到 `build.rollupOptions.output`
```js
import { endlessGameAssetFileNames } from './tongji/endless-game/viteAssetOutput.js'
// defineConfig 内
build: {
rollupOptions: {
output: { assetFileNames: endlessGameAssetFileNames }
}
}
```
3. 在目标项目 `pages.json``tongji` 分包 `pages` 数组中加入:
```json
{
"path": "endless-game/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "控糖消消乐",
"backgroundColor": "#eefbf4",
"disableScroll": false
}
}
```
4. 使用 `/tongji/endless-game/index` 打开游戏。
5. 微信小程序后台需要允许音频域名:`https://gz-1349751149.cos.ap-guangzhou.myqcloud.com`
## 说明
- 旧地址 `/tongji/pages/game``/tongji/pages/game-endless` 只是当前项目的兼容跳转,迁移时不需要复制。
- 当前项目原来的“糖分突袭”源码保存在 `tongji/legacy-game/game.vue`,没有注册为页面,不参与打包。
- 连消小目标按“连消×2”累计 2 次;连续 6 次普通消除没有连消时,下一次掉落会提供连消机会。“连消×3”保留为额外积分、驼乳粉与撒花惊喜,不阻挡主线任务。
- 横向 3 个与竖向 3 个相交形成 L/T 形五消时,会在交点生成带高对比 L 标记的范围爆破棋子;该棋子再次被消除时清除周围九格。
- 进入游戏时展示“本周 7 人同行榜”界面原型,包含前后名次、差距提示和可切换的同行/亲友鼓励;顶部“本周同行”可再次打开。当前榜单明确标注为预览数据,正式用户关系和周榜成绩需要后端接口接入。
- 本功能包依赖 uni-app Vue 3 和 `@dcloudio/uni-app`,不依赖当前项目其他业务组件。
- 食物风险等级和科普文案集中在 `index.vue``FOODS` 配置中,正式上线前仍需由医院医生审核。
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,190 @@
<template>
<view class="tj-icon-wrap" :class="[`tj-icon-wrap--${size}`]">
<image
v-if="!useFallback"
class="tj-icon"
:class="[`tj-icon--${name}`, `tj-icon--${size}`]"
:src="iconSrc"
mode="aspectFit"
@error="onImageError"
/>
<!-- <text
v-else
class="tj-icon-fallback"
:class="[`tj-icon-fallback--${size}`]"
:style="{ color }"
>{{ fallbackGlyph }}</text> -->
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { svgToDataUrl } from '../utils/svgDataUrl.js'
const props = defineProps({
name: { type: String, required: true },
size: { type: String, default: 'md' },
color: { type: String, default: '#204E2B' }
})
const useFallback = ref(false)
/** Lucide 风格描边路径 */
const ICON_PATHS = {
view: '<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>',
ticket: '<path d="M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"/><path d="M13 5v2"/><path d="M13 17v2"/><path d="M13 11v2"/>',
calendar: '<rect width="18" height="18" x="3" y="4" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/>',
flame: '<path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/>',
'check-circle': '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/>',
glucose: '<path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"/>',
heart: '<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/>',
activity: '<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>',
users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
user: '<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
person: '<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
send: '<path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/>',
'alert-triangle': '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
minus: '<path d="M5 12h14"/>',
droplet: '<path d="M12 22a7 7 0 0 0 7-7c0-2-1-3.5-2.5-5.5C15 7 12 2 12 2S9 7 7.5 9.5 5 13 5 15a7 7 0 0 0 7 7z"/>',
sparkles: '<path d="m12 3-1.9 5.8L4 12l5.8 1.9L12 21l1.9-5.8L20 12l-5.8-1.9L12 3Z"/><path d="M5 3v4M19 17v4M3 5h4M17 19h4"/>',
trophy: '<path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6M18 9h1.5a2.5 2.5 0 0 0 0-5H18M4 22h16M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20 7 22M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20 17 22M18 2H6v7a6 6 0 0 0 12 0V2Z"/>',
share: '<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8M16 6l-4-4-4 4M12 2v13"/>',
plus: '<path d="M5 12h14M12 5v14"/>',
refresh: '<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8M3 3v5h5M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16M21 21v-5h-5"/>',
volume: '<path d="M11 5 6 9H2v6h4l5 4V5zM15.54 8.46a5 5 0 0 1 0 7.07M19.07 4.93a10 10 0 0 1 0 14.14"/>',
pause: '<rect width="4" height="16" x="14" y="4" rx="1"/><rect width="4" height="16" x="6" y="4" rx="1"/>',
play: '<polygon points="6 3 20 12 6 21 6 3"/>',
info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/>',
sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41-1.41M17.66 6.34l1.41-1.41M6.34 4.93l1.41 1.41"/>',
moon: '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',
sunset: '<path d="M12 10V2M18.364 5.636l-2.12 2.12M5.636 18.364l2.12-2.12M22 18h-3M5 18H2M18.364 18.364l-2.12-2.12M5.636 5.636l2.12 2.12M16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z"/>',
mic: '<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/>',
camera: '<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/>',
bulb: '<path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1.3.5 2.6 1.5 3.5.8.8 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/>',
leaf: '<path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z"/><path d="M2 21c0-3 1.85-5.36 5.08-6"/>',
utensils: '<path d="M3 2v7c0 1.1.9 2 2 2a2 2 0 0 0 2-2V2"/><path d="M7 2v20"/><path d="M21 15V2a5 5 0 0 0-3 4.5v6a2 2 0 0 0 2 2h1Z"/><path d="M18 15v7"/>',
egg: '<path d="M12 22c4.97 0 8-3.27 8-7.31C20 9.65 16.42 2 12 2S4 9.65 4 14.69C4 18.73 7.03 22 12 22Z"/>',
home: '<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>',
'plus-circle': '<circle cx="12" cy="12" r="10"/><path d="M8 12h8M12 8v8"/>',
'chevron-right': '<path d="m9 18 6-6-6-6"/>',
'chevron-left': '<path d="m15 18-6-6 6-6"/>',
check: '<path d="M20 6 9 17l-5-5"/>',
settings: '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
syringe: '<path d="m18 2 4 4"/><path d="m17 7 3-3"/><path d="M19 9 8.7 19.3c-1 1-2.5 1-3.4 0l-.6-.6c-1-1-1-2.5 0-3.4L15 5"/><path d="m9 11 4 4"/><path d="m5 19-3 3"/><path d="m14 4 6 6"/>',
zap: '<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>'
}
/** 图片加载失败时的 emoji 回退 */
const ICON_FALLBACK = {
view: '👁',
ticket: '🎫',
calendar: '📅',
flame: '🔥',
'check-circle': '✓',
glucose: '💧',
heart: '❤',
activity: '🏃',
users: '👥',
user: '👤',
person: '👤',
send: '➤',
'alert-triangle': '⚠',
minus: '—',
droplet: '💧',
sparkles: '✨',
trophy: '🏆',
share: '↗',
plus: '',
refresh: '↻',
volume: '🔊',
pause: '⏸',
play: '▶',
info: '!',
sun: '☀',
moon: '🌙',
sunset: '☀',
mic: '🎤',
camera: '📷',
bulb: '💡',
leaf: '🥬',
utensils: '🍴',
egg: '🍳',
home: '🏠',
'plus-circle': '⊕',
'chevron-right': '',
'chevron-left': '',
check: '✓',
settings: '⚙',
syringe: '💉',
zap: '⚡'
}
const strokeColor = computed(() => {
const c = String(props.color || '#204E2B').trim()
return /^#[0-9A-Fa-f]{3,8}$/.test(c) ? c : '#204E2B'
})
const iconSrc = computed(() => {
const path = ICON_PATHS[props.name] || ICON_PATHS.view
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${strokeColor.value}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${path}</svg>`
return svgToDataUrl(svg)
})
const fallbackGlyph = computed(() => ICON_FALLBACK[props.name] || ICON_FALLBACK.view)
function onImageError() {
useFallback.value = true
}
</script>
<style lang="scss" scoped>
.tj-icon-wrap {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.tj-icon-wrap--sm {
width: 32rpx;
height: 32rpx;
}
.tj-icon-wrap--md {
width: 40rpx;
height: 40rpx;
}
.tj-icon-wrap--lg {
width: 48rpx;
height: 48rpx;
}
.tj-icon-wrap--xl {
width: 56rpx;
height: 56rpx;
}
.tj-icon {
display: block;
width: 100%;
height: 100%;
}
/* Lucide send 路径重心偏右上,微调使圆形按钮内视觉居中 */
.tj-icon--send {
transform: translate(-10%, 10%);
}
.tj-icon-fallback {
display: block;
line-height: 1;
font-weight: 700;
text-align: center;
}
.tj-icon-fallback--sm {
font-size: 24rpx;
}
.tj-icon-fallback--md {
font-size: 30rpx;
}
.tj-icon-fallback--lg {
font-size: 36rpx;
}
.tj-icon-fallback--xl {
font-size: 42rpx;
}
</style>
@@ -0,0 +1,343 @@
/**
* 糖分突袭 · 游戏音效(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 }
}
const WARMUP_SFX = [
'select', 'swap', 'swapFail', 'drop', 'dropLight',
'match3', 'match4', 'match5', 'combo2', 'combo3', 'comboMega',
'insulin', 'sparkle', 'reshuffle', 'lose'
]
function createAudioContext() {
if (typeof Audio !== 'undefined') {
const audio = new Audio()
audio.preload = 'auto'
return {
get src() { return audio.src },
set src(value) { audio.src = value },
get volume() { return audio.volume },
set volume(value) { audio.volume = value },
get playbackRate() { return audio.playbackRate },
set playbackRate(value) { audio.playbackRate = value },
play() {
const promise = audio.play()
if (promise?.catch) promise.catch(() => {})
},
stop() {
audio.pause()
try { audio.currentTime = 0 } catch (_) {}
},
seek(time) {
try { audio.currentTime = time } catch (_) {}
},
destroy() {
audio.pause()
audio.removeAttribute('src')
}
}
}
return uni.createInnerAudioContext()
}
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
}
createOne() {
const ctx = createAudioContext()
ctx.src = this.src
ctx.obeyMuteSwitch = false
ctx.autoplay = false
ctx.volume = this.volume
try {
ctx.playbackRate = this.rate
} catch (_) {}
this.list.push(ctx)
return ctx
}
create() {
while (this.list.length < this.size) this.createOne()
}
warmUp() {
if (this.warmedUp) return
const ctx = this.list[0] || this.createOne()
try {
ctx.volume = 0
ctx.play()
setTimeout(() => {
try {
ctx.stop()
ctx.volume = this.volume
} catch (_) {}
}, 60)
} catch (_) {}
if (this.size > 1) this.cursor = 1
this.warmedUp = true
}
play(scale = 1, rateMul = 1) {
if (this.list.length < this.size) 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()
const names = typeof Audio !== 'undefined' ? ['select'] : WARMUP_SFX
names.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
}
}
@@ -0,0 +1,559 @@
.eg-page {
min-height: 100vh;
color: #173b32;
background: linear-gradient(180deg, #eefbf4 0%, #f9f3dd 56%, #fffaf1 100%);
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
.eg-confetti-layer { position: fixed; z-index: 80; inset: 0; overflow: hidden; pointer-events: none; }
.eg-confetti-piece {
position: absolute;
top: -70rpx;
display: block;
border-radius: 3rpx;
box-shadow: 0 2rpx 4rpx rgba(0,0,0,.12);
animation-name: egConfettiFall;
animation-timing-function: cubic-bezier(.18,.72,.35,1);
animation-fill-mode: both;
}
.eg-confetti-piece.is-emoji { width: auto !important; height: auto !important; background: transparent !important; box-shadow: none; font-size: 42rpx; }
.eg-nav {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding-right: 28rpx;
padding-bottom: 16rpx;
padding-left: 28rpx;
}
.eg-nav-btn {
display: flex;
width: 72rpx;
height: 72rpx;
align-items: center;
justify-content: center;
border: 2rpx solid rgba(21, 94, 75, .12);
border-radius: 24rpx;
background: rgba(255,255,255,.82);
}
.eg-nav-actions { display: flex; width: 156rpx; justify-content: flex-end; gap: 10rpx; }
.eg-nav-btn--small { width: 68rpx; height: 68rpx; border-radius: 22rpx; }
.eg-title-wrap { position: absolute; left: 50%; display: flex; flex-direction: column; align-items: center; transform: translateX(-50%); }
.eg-title { color: #155e4b; font-size: 40rpx; font-weight: 800; }
.eg-subtitle { margin-top: 2rpx; color: #648278; font-size: 24rpx; }
.eg-content { padding: 0 16rpx 48rpx; }
.eg-score-card {
display: flex;
align-items: center;
margin-bottom: 16rpx;
padding: 18rpx 24rpx;
border: 2rpx solid rgba(21, 94, 75, .1);
border-radius: 28rpx;
background: rgba(255,255,255,.9);
box-shadow: 0 10rpx 28rpx rgba(32, 78, 43, .08);
}
.eg-stat { display: flex; flex: 1; flex-direction: column; align-items: center; }
.eg-stat--main { border-right: 2rpx solid #e2eee8; border-left: 2rpx solid #e2eee8; }
.eg-stat-label { color: #71847d; font-size: 24rpx; }
.eg-stat-value { margin-top: 4rpx; color: #204e2b; font-size: 38rpx; font-weight: 800; }
.eg-stat-score { margin-top: 2rpx; color: #e16f24; font-size: 46rpx; font-weight: 900; }
.eg-stat--rank { position: relative; cursor: pointer; }
.eg-stat--rank.is-locked { opacity: .52; }
.eg-stat-rank-value { margin-top: 1rpx; color: #176b52; font-size: 35rpx; font-weight: 1000; line-height: 1.08; }
.eg-stat-rank-hint { margin-top: 2rpx; color: #df7427; font-size: 18rpx; font-weight: 800; }
.eg-task-card {
margin-bottom: 16rpx;
padding: 18rpx 22rpx;
border-radius: 24rpx;
background: rgba(255, 249, 223, .94);
box-shadow: 0 8rpx 22rpx rgba(118, 89, 25, .08);
}
.eg-task-card { position: relative; overflow: visible; }
.eg-task-reward-flight {
position: absolute;
z-index: 72;
top: 4rpx;
left: 42%;
display: flex;
width: 112rpx;
height: 92rpx;
align-items: center;
justify-content: center;
border: 4rpx solid rgba(255,255,255,.96);
border-radius: 24rpx;
background: #fff7e8;
box-shadow: 0 10rpx 24rpx rgba(159,94,23,.28);
animation: egTaskRewardFlight 1.12s cubic-bezier(.18,.78,.22,1) both;
pointer-events: none;
}
.eg-task-reward-flight > image { position: relative; z-index: 2; width: 78rpx; height: 66rpx; }
.eg-task-reward-flight > text { position: absolute; z-index: 3; top: -14rpx; right: -16rpx; display: flex; width: 48rpx; height: 48rpx; align-items: center; justify-content: center; border: 4rpx solid #fff; border-radius: 50%; color: #fff; background: #ed7625; box-shadow: 0 5rpx 12rpx rgba(191,75,19,.3); font-size: 25rpx; font-weight: 1000; }
.eg-task-reward-glow { position: absolute; z-index: 1; inset: -15rpx; border-radius: 32rpx; background: radial-gradient(circle, rgba(255,210,67,.52), rgba(255,210,67,0) 68%); animation: egRewardGlow .42s ease-in-out infinite alternate; }
.eg-task-head { display: flex; align-items: center; justify-content: space-between; color: #7c5b1b; font-size: 27rpx; font-weight: 700; }
.eg-task-name { display: flex; align-items: center; gap: 10rpx; }
.eg-task-count { color: #9a6718; font-size: 29rpx; }
.eg-progress { height: 14rpx; margin-top: 12rpx; overflow: hidden; border-radius: 999rpx; background: #eadfbd; }
.eg-progress-fill { height: 100%; border-radius: inherit; background: linear-gradient(90deg, #f0a128, #f4c34f); transition: width .25s; }
.eg-board-shell {
position: relative;
padding: 14rpx;
border: 2rpx solid rgba(34, 103, 79, .16);
border-radius: 34rpx;
background: rgba(255,255,255,.92);
box-shadow: 0 18rpx 44rpx rgba(29, 78, 59, .12);
}
.eg-board-top { display: flex; align-items: center; justify-content: space-between; margin: 0 4rpx 14rpx; }
.eg-board-bonuses { display: flex; align-items: center; gap: 8rpx; }
.eg-combo { padding: 8rpx 16rpx; border-radius: 999rpx; color: #45685d; background: #e8f3ee; font-size: 25rpx; font-weight: 700; }
.eg-combo.is-hot { color: #fff; background: linear-gradient(135deg, #f59e0b, #ea580c); }
.eg-double-state { padding: 8rpx 14rpx; border: 2rpx solid rgba(255,255,255,.9); border-radius: 999rpx; color: #fff; background: linear-gradient(135deg, #7c3aed, #d946ef); box-shadow: 0 4rpx 12rpx rgba(124,58,237,.24); font-size: 23rpx; font-weight: 900; white-space: nowrap; }
.eg-risk { display: flex; align-items: center; gap: 10rpx; color: #8d512c; font-size: 24rpx; }
.eg-risk-dots { display: flex; gap: 5rpx; }
.eg-risk-dot { width: 12rpx; height: 12rpx; border-radius: 50%; background: #ead8c9; }
.eg-risk-dot.on { background: #e96b3b; box-shadow: 0 0 0 3rpx rgba(233,107,59,.12); }
.eg-board { display: flex; flex-direction: column; gap: 8rpx; opacity: 1; transition: opacity .15s; }
.eg-board.is-busy { opacity: .82; }
.eg-row { display: flex; gap: 8rpx; }
.eg-cell {
position: relative;
display: flex;
height: 166rpx;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
border: 5rpx solid transparent;
border-radius: 26rpx;
box-shadow: inset 0 -5rpx 0 rgba(0,0,0,.05), 0 5rpx 12rpx rgba(31, 66, 53, .08);
transition: transform .16s cubic-bezier(.2,.8,.2,1), border-color .15s, opacity .16s, filter .16s;
will-change: transform;
}
.eg-cell.is-dragging { transition: transform .055s linear, border-color .15s; }
.eg-cell.is-selected { z-index: 2; border-color: #167d61; transform: scale(1.06); box-shadow: 0 0 0 6rpx rgba(22,125,97,.14); }
.eg-cell.is-camel-target { border-color: #f59e0b; animation: egPulse 1s infinite; }
.eg-replace-target { position: absolute; z-index: 7; right: 7rpx; bottom: 6rpx; padding: 4rpx 10rpx; border: 2rpx solid #fff; border-radius: 999rpx; color: #fff; background: #1570a6; box-shadow: 0 3rpx 9rpx rgba(20,91,135,.28); font-size: 20rpx; font-weight: 900; }
.eg-cell.is-clearing {
z-index: 8;
border-color: #fff;
animation: egHit .22s cubic-bezier(.2,.9,.3,1) forwards;
filter: brightness(1.28) saturate(1.25);
}
.eg-cell.is-yellow,
.eg-cell.is-cream,
.eg-cell.is-red,
.eg-cell.is-purple { background: #fffaf0; }
.eg-cell.is-level-low { background: #f4faf6; }
.eg-cell.is-level-mid { background: #fff9eb; }
.eg-cell.is-orange,
.eg-cell.is-level-high { background: #fff0e4; }
.eg-cell.is-milk { background: #edf7ff; }
.eg-cell.is-high { border-color: #ef7b45; }
.eg-food-image { width: 132rpx; height: 92rpx; margin-top: 7rpx; border-radius: 17rpx; filter: saturate(1.06) contrast(1.03); }
.eg-food-name { margin-top: 8rpx; color: #203d34; font-size: 29rpx; font-weight: 900; letter-spacing: 1rpx; line-height: 1.1; }
.eg-sugar-tag { position: absolute; top: 5rpx; right: 5rpx; min-width: 52rpx; padding: 4rpx 10rpx; border: 2rpx solid #fff; border-radius: 999rpx; color: #fff; box-shadow: 0 3rpx 8rpx rgba(25,55,45,.22); font-size: 23rpx; font-weight: 900; line-height: 1.25; text-align: center; }
.eg-sugar-tag.is-low { color: #316a54; border-color: rgba(255,255,255,.82); background: #dcefe6; box-shadow: 0 2rpx 5rpx rgba(32,101,75,.1); }
.eg-sugar-tag.is-mid { color: #735a15; border-color: rgba(255,255,255,.9); background: #f7e7a8; box-shadow: 0 2rpx 6rpx rgba(137,91,0,.15); }
.eg-sugar-tag.is-high { background: #d9431f; box-shadow: 0 4rpx 10rpx rgba(163,45,21,.32); }
.eg-sugar-tag.is-prop { background: #2563a8; }
.eg-special-mark { position: absolute; z-index: 12; bottom: 5rpx; left: 6rpx; display: flex; width: 40rpx; height: 40rpx; align-items: center; justify-content: center; border: 3rpx solid rgba(255,255,255,.96); border-radius: 50%; color: #fff; background: #6d28d9; box-shadow: 0 4rpx 11rpx rgba(73,31,130,.3); font-size: 23rpx; font-weight: 900; }
.eg-special-mark.is-row { width: 58rpx; height: 40rpx; border-radius: 999rpx; background: linear-gradient(135deg, #ffd64d, #ff8a18 48%, #e83b22); box-shadow: 0 0 0 4rpx rgba(255,172,30,.2), 0 4rpx 15rpx rgba(203,59,26,.45); animation: egFlameReady .7s ease-in-out infinite alternate; }
.eg-special-mark.is-col { width: 40rpx; height: 58rpx; border-radius: 999rpx; background: linear-gradient(180deg, #ffd64d, #ff8a18 48%, #e83b22); box-shadow: 0 0 0 4rpx rgba(255,172,30,.2), 0 4rpx 15rpx rgba(203,59,26,.45); animation: egFlameReady .7s ease-in-out infinite alternate; }
.eg-special-mark.is-burst { width: 48rpx; height: 48rpx; border-color: #fff3a8; border-radius: 15rpx 50% 50%; background: linear-gradient(145deg, #7c3aed 5%, #dd3b70 48%, #ff8a18 100%); box-shadow: 0 4rpx 13rpx rgba(108,35,155,.38), 0 0 12rpx rgba(255,179,38,.3); animation: egLReady 1.25s ease-in-out infinite alternate; }
.eg-mini-flame { position: relative; width: 25rpx; height: 30rpx; border-radius: 70% 32% 68% 40%; background: linear-gradient(135deg, #fff8a8 8%, #ffc21f 42%, #f04420 84%); box-shadow: 0 0 14rpx rgba(255,230,92,.95); transform: rotate(43deg); }
.eg-mini-flame-core { position: absolute; right: 4rpx; bottom: 3rpx; width: 10rpx; height: 15rpx; border-radius: 70% 35% 70% 42%; background: #fffbd1; box-shadow: 0 0 7rpx rgba(255,255,210,.95); }
.eg-l-special { position: relative; width: 29rpx; height: 29rpx; filter: drop-shadow(0 1rpx 2rpx rgba(73,22,105,.3)); }
.eg-l-arm { position: absolute; left: 3rpx; bottom: 3rpx; border-radius: 999rpx; background: #fff9b8; box-shadow: 0 0 6rpx rgba(255,249,184,.92); }
.eg-l-arm.is-vertical { width: 8rpx; height: 25rpx; }
.eg-l-arm.is-horizontal { width: 25rpx; height: 8rpx; }
.eg-l-core { position: absolute; left: 1rpx; bottom: 1rpx; width: 12rpx; height: 12rpx; border: 2rpx solid #fff; border-radius: 50%; background: #ff5b25; }
.eg-cell.is-fire-clearing { border-color: #ff7a1a; box-shadow: inset 0 0 25rpx rgba(255,102,18,.36), 0 0 18rpx rgba(255,105,20,.48); filter: brightness(1.35) saturate(1.42); }
.eg-cell.is-clearing.is-fire-clearing { animation: egFireCellHit .36s cubic-bezier(.2,.78,.22,1) forwards; }
.eg-fire-clear { position: absolute; z-index: 14; inset: 2rpx; overflow: hidden; border: 3rpx solid rgba(255,210,71,.92); border-radius: 22rpx; opacity: .96; background: linear-gradient(180deg, rgba(255,230,86,.12), rgba(239,63,27,.22)); pointer-events: none; }
.eg-fire-glow { position: absolute; inset: 6%; border-radius: 18rpx; background: radial-gradient(circle, rgba(255,246,177,.74), rgba(255,131,22,.32) 47%, rgba(222,47,23,0) 75%); animation: egFireGlow .34s ease-out both; }
.eg-fire-sweep { position: absolute; top: 23%; left: -48%; width: 168%; height: 55%; border-radius: 60% 45% 55% 42%; background: linear-gradient(90deg, rgba(255,184,38,0), rgba(255,238,124,.94) 28%, rgba(255,133,22,.92) 53%, rgba(226,54,25,.82) 72%, rgba(255,184,38,0)); box-shadow: 0 0 27rpx rgba(255,91,17,.72); animation: egFireSweep .34s cubic-bezier(.2,.78,.22,1) both; }
.eg-fire-flame { position: absolute; bottom: 13%; width: 25rpx; height: 39rpx; border-radius: 72% 35% 68% 42%; background: linear-gradient(145deg, #fff7a6 4%, #ffc329 39%, #ff7419 66%, #dc321f 100%); box-shadow: 0 0 15rpx rgba(255,124,21,.88); transform: rotate(42deg); animation: egFireFlame .34s ease-out both; }
.eg-fire-flame > view { position: absolute; right: 5rpx; bottom: 4rpx; width: 10rpx; height: 18rpx; border-radius: 70% 35% 70% 42%; background: #fffbd6; }
.eg-fire-flame.is-left { left: 16%; animation-delay: .01s; }
.eg-fire-flame.is-center { left: 43%; bottom: 19%; transform: rotate(42deg) scale(1.18); animation-delay: .035s; }
.eg-fire-flame.is-right { right: 15%; animation-delay: .065s; }
.eg-fire-spark { position: absolute; width: 10rpx; height: 17rpx; border-radius: 70% 35% 70% 42%; background: #ffe45e; box-shadow: 0 0 11rpx rgba(255,139,28,.9); animation: egFireSpark .34s ease-out both; }
.eg-fire-spark.is-one { top: 46%; left: 25%; }
.eg-fire-spark.is-two { top: 34%; left: 62%; animation-delay: .035s; }
.eg-fire-spark.is-three { top: 52%; left: 78%; animation-delay: .07s; }
.eg-board-tip { display: flex; align-items: center; justify-content: center; gap: 8rpx; min-height: 58rpx; margin-top: 12rpx; color: #684817; font-size: 26rpx; font-weight: 700; text-align: center; }
.eg-impact-text {
position: absolute;
z-index: 20;
top: 46%;
left: 50%;
padding: 10rpx 24rpx;
border: 4rpx solid rgba(255,255,255,.9);
border-radius: 999rpx;
color: #fff;
background: linear-gradient(135deg, #f59e0b, #e4572e);
box-shadow: 0 10rpx 28rpx rgba(190, 70, 25, .35);
font-size: 34rpx;
font-weight: 900;
transform: translate(-50%, -50%);
animation: egImpact .52s cubic-bezier(.16,.82,.3,1) forwards;
pointer-events: none;
}
.eg-impact-text.is-level-2 { font-size: 40rpx; background: linear-gradient(135deg, #8b5cf6, #ec4899); }
.eg-impact-text.is-level-3 { font-size: 46rpx; background: linear-gradient(135deg, #ef4444, #f59e0b); box-shadow: 0 12rpx 36rpx rgba(239,68,68,.42); }
.eg-jackpot {
position: absolute;
z-index: 30;
top: 48%;
left: 50%;
width: 540rpx;
padding: 24rpx 22rpx 20rpx;
overflow: hidden;
border: 7rpx solid #ffd83d;
border-radius: 36rpx;
color: #fff;
background: radial-gradient(circle at 50% 0%, #ef4444, #9f1239 65%, #64102a);
box-shadow: 0 0 0 7rpx #fff2a8, 0 22rpx 52rpx rgba(100,16,42,.5);
text-align: center;
transform: translate(-50%, -50%);
animation: egJackpotIn 1.35s cubic-bezier(.16,.85,.25,1) forwards;
pointer-events: none;
}
.eg-jackpot.is-match-4 { background: radial-gradient(circle at 50% 0%, #8b5cf6, #5b21b6 68%, #35106d); }
.eg-jackpot.is-match-5 { border-color: #fff36a; background: radial-gradient(circle at 50% 0%, #ff6b24, #d7193f 58%, #760c2a); animation-duration: 1.9s; }
.eg-jackpot.is-match-3 {
width: 414rpx;
padding: 20rpx 22rpx 18rpx;
border-width: 4rpx;
border-color: #f1c663;
color: #31574b;
background: linear-gradient(155deg, #f7fff9, #fff5d9);
box-shadow: 0 0 0 4rpx rgba(255,255,255,.84), 0 14rpx 34rpx rgba(98,82,39,.2);
animation: egTripleIn 1.12s cubic-bezier(.2,.82,.28,1) forwards;
}
.eg-jackpot.is-match-3 .eg-jackpot-lights { display: none; }
.eg-jackpot.is-match-3 .eg-jackpot-kicker { color: #5b8c73; font-size: 21rpx; letter-spacing: 3rpx; text-shadow: none; }
.eg-jackpot.is-match-3 .eg-jackpot-title { color: #365b4e; font-size: 31rpx; text-shadow: none; }
.eg-jackpot.is-match-3 .eg-jackpot-reward { color: #ad7021; font-size: 25rpx; }
.eg-jackpot.is-match-3 .eg-jackpot-note { color: #829087; }
.eg-triple-cans { position: relative; display: flex; justify-content: center; gap: 12rpx; margin: 12rpx 0 10rpx; }
.eg-triple-can { display: flex; width: 78rpx; height: 76rpx; align-items: center; justify-content: center; border: 3rpx solid #f4d996; border-radius: 19rpx; background: rgba(255,255,255,.9); box-shadow: 0 5rpx 12rpx rgba(128,92,30,.12); animation: egTripleCan .42s cubic-bezier(.2,.86,.26,1) both; }
.eg-triple-can:nth-child(2) { animation-delay: .07s; }
.eg-triple-can:nth-child(3) { animation-delay: .14s; }
.eg-triple-can > image { width: 62rpx; height: 58rpx; }
.eg-jackpot.is-match-4 {
width: 548rpx;
padding: 25rpx 22rpx 22rpx;
border-color: #ffd95b;
background: radial-gradient(circle at 50% -10%, #a97cff, #6031b5 58%, #341066);
box-shadow: 0 0 0 6rpx #fff0a9, 0 0 36rpx rgba(255,210,76,.5), 0 24rpx 58rpx rgba(53,16,109,.48);
animation: egFourIn 1.88s cubic-bezier(.16,.84,.24,1) forwards;
}
.eg-jackpot.is-match-4 .eg-jackpot-kicker { color: #ffef8d; font-size: 24rpx; letter-spacing: 3rpx; }
.eg-jackpot.is-match-4 .eg-jackpot-title { margin-top: 9rpx; color: #fff; font-size: 39rpx; }
.eg-jackpot.is-match-4 .eg-jackpot-reward { color: #fff2a8; }
.eg-four-teaser { position: relative; margin-top: 13rpx; }
.eg-four-cans { display: flex; align-items: center; justify-content: center; gap: 8rpx; }
.eg-four-can { display: flex; width: 76rpx; height: 92rpx; align-items: center; justify-content: center; border: 4rpx solid #ffe078; border-radius: 18rpx; background: linear-gradient(180deg, #fffef7, #ffe9a5); box-shadow: inset 0 -5rpx 0 rgba(171,106,17,.12), 0 7rpx 13rpx rgba(34,12,67,.28); animation: egFourCan .48s cubic-bezier(.18,.9,.25,1.2) both; }
.eg-four-can:nth-child(2) { animation-delay: .07s; }
.eg-four-can:nth-child(3) { animation-delay: .14s; }
.eg-four-can:nth-child(4) { animation-delay: .21s; }
.eg-four-can > image { width: 64rpx; height: 70rpx; }
.eg-four-can.is-locked { border-style: dashed; border-color: rgba(255,241,161,.86); color: #fff6ae; background: rgba(48,18,91,.54); box-shadow: inset 0 0 16rpx rgba(255,225,91,.18), 0 0 18rpx rgba(255,222,91,.36); font-size: 48rpx; font-weight: 1000; animation: egLockedCan .68s .34s ease-in-out infinite alternate; }
.eg-four-promise { display: inline-flex; margin-top: 15rpx; padding: 8rpx 18rpx; border: 2rpx solid rgba(255,247,184,.72); border-radius: 999rpx; color: #3d176f; background: linear-gradient(90deg, #fff3a3, #ffd85b); box-shadow: 0 5rpx 15rpx rgba(28,7,62,.22); font-size: 25rpx; font-weight: 1000; animation: egPromisePulse .72s ease-in-out infinite alternate; }
.eg-five-celebration { position: relative; margin: 15rpx 0 10rpx; }
.eg-five-cans { display: flex; align-items: center; justify-content: center; gap: 8rpx; }
.eg-five-can { display: flex; width: 78rpx; height: 93rpx; align-items: center; justify-content: center; border: 4rpx solid #fff17b; border-radius: 18rpx; background: linear-gradient(180deg, #fff, #fff2ac); box-shadow: inset 0 -5rpx 0 rgba(184,91,12,.13), 0 7rpx 0 #bd5b13, 0 0 16rpx rgba(255,240,101,.5); animation: egFiveCan .56s cubic-bezier(.16,.92,.24,1.2) both; }
.eg-five-can:nth-child(2) { animation-delay: .07s; }
.eg-five-can:nth-child(3) { animation-delay: .14s; }
.eg-five-can:nth-child(4) { animation-delay: .21s; }
.eg-five-can:nth-child(5) { animation-delay: .28s; }
.eg-five-can > image { width: 65rpx; height: 72rpx; }
.eg-five-sevens { display: flex; justify-content: center; gap: 8rpx; margin-top: 13rpx; }
.eg-five-sevens > text { display: flex; width: 58rpx; height: 55rpx; align-items: center; justify-content: center; border: 3rpx solid #fff5a7; border-radius: 13rpx; color: #fff36a; background: linear-gradient(155deg, #d7193f, #9f1239); box-shadow: 0 5rpx 0 #70102b, 0 0 13rpx rgba(255,243,106,.45); font-size: 43rpx; font-weight: 1000; line-height: 1; text-shadow: 0 3rpx 0 #8f1733; }
.eg-jackpot-lights { position: absolute; inset: 8rpx; border: 4rpx dotted rgba(255,255,255,.88); border-radius: 25rpx; animation: egLights .24s steps(2) infinite; }
.eg-jackpot-kicker { position: relative; display: block; color: #fff36a; font-size: 28rpx; font-weight: 900; letter-spacing: 6rpx; text-shadow: 0 3rpx 0 rgba(83,19,25,.45); }
.eg-jackpot-reels { position: relative; display: flex; justify-content: center; gap: 12rpx; margin: 13rpx 0; }
.eg-jackpot-reel { display: flex; width: 112rpx; height: 126rpx; flex-direction: column; align-items: center; justify-content: center; border: 6rpx solid #ffcf31; border-radius: 20rpx; background: linear-gradient(180deg, #fff 0%, #fff7cf 48%, #ffd55e 50%, #fff 53%, #fff8dc 100%); box-shadow: inset 0 0 18rpx rgba(137,78,10,.25), 0 7rpx 0 #a85a0a; animation: egReelStop .52s cubic-bezier(.2,.9,.25,1) both; }
.eg-jackpot-reel:nth-child(2) { animation-delay: .09s; }
.eg-jackpot-reel:nth-child(3) { animation-delay: .18s; }
.eg-jackpot-seven { color: #e11d48; font-size: 61rpx; font-weight: 1000; line-height: .85; text-shadow: 0 3rpx 0 #ffd1d8; }
.eg-jackpot-milk { width: 54rpx; height: 36rpx; margin-top: 3rpx; border-radius: 7rpx; }
.eg-jackpot-title { position: relative; display: block; font-size: 37rpx; font-weight: 900; text-shadow: 0 4rpx 0 rgba(73,10,30,.5); }
.eg-jackpot-points { position: relative; display: block; margin-top: 2rpx; color: #fff36a; font-size: 66rpx; font-weight: 1000; line-height: 1.05; letter-spacing: 2rpx; text-shadow: 0 5rpx 0 #9e2813, 0 0 18rpx rgba(255,243,106,.85); animation: egPointsPop .62s .42s cubic-bezier(.16,.9,.25,1.25) both; }
.eg-jackpot-reward { position: relative; display: block; margin-top: 7rpx; color: #fff5a5; font-size: 27rpx; font-weight: 800; line-height: 1.35; }
.eg-jackpot-note { position: relative; display: block; margin-top: 8rpx; color: rgba(255,255,255,.8); font-size: 22rpx; }
.eg-tools { margin-top: 16rpx; }
.eg-tool { display: flex; align-items: center; padding: 16rpx 18rpx; border: 3rpx solid #c9e0d6; border-radius: 26rpx; background: rgba(255,255,255,.92); box-shadow: 0 7rpx 18rpx rgba(24, 84, 63, .07); }
.eg-tool.active { border-color: #f59e0b; background: #fffbeb; }
.eg-tool.disabled { opacity: .45; }
.eg-tool-icon { position: relative; display: flex; width: 88rpx; height: 88rpx; align-items: center; justify-content: center; overflow: visible; border-radius: 24rpx; font-size: 46rpx; background: linear-gradient(145deg, #e8f7ff, #d4ecff); box-shadow: inset 0 -4rpx 0 rgba(33,104,151,.08); }
.eg-tool-food-image { width: 76rpx; height: 58rpx; border-radius: 14rpx; }
.eg-replace-badge { position: absolute; right: -12rpx; bottom: -8rpx; display: flex; min-width: 78rpx; height: 38rpx; align-items: center; justify-content: center; gap: 2rpx; padding: 0 8rpx; border: 3rpx solid #fff; border-radius: 999rpx; color: #fff; background: linear-gradient(135deg, #2389b9, #145d91); box-shadow: 0 5rpx 12rpx rgba(20,93,145,.3); }
.eg-replace-badge .tj-icon-wrap { width: 22rpx; height: 22rpx; }
.eg-replace-badge > text { font-size: 20rpx; font-weight: 900; line-height: 1; }
.eg-tool-copy { display: flex; flex: 1; flex-direction: column; margin-left: 16rpx; }
.eg-tool-name { color: #244a3d; font-size: 29rpx; font-weight: 800; }
.eg-tool-desc { margin-top: 5rpx; color: #6c8179; font-size: 25rpx; }
.eg-tool-count { color: #155e4b; font-size: 32rpx; font-weight: 900; }
.eg-legend { display: flex; justify-content: center; gap: 18rpx; margin-top: 18rpx; color: #53685f; font-size: 25rpx; font-weight: 700; }
.eg-dot--safe { color: #4cae7b; }
.eg-dot--mid { color: #e2aa19; }
.eg-dot--high { color: #e4572e; }
.eg-weekly-overlay { position: fixed; z-index: 90; inset: 0; display: flex; align-items: center; justify-content: center; padding: 28rpx; background: rgba(16, 55, 43, .66); backdrop-filter: blur(5px); }
.eg-weekly-card { width: 100%; max-width: 664rpx; max-height: calc(100vh - 56rpx); overflow-y: auto; padding: 28rpx 26rpx 22rpx; border: 5rpx solid rgba(255,255,255,.96); border-radius: 38rpx; background: linear-gradient(160deg, #f5fff9 0%, #fffdf3 56%, #fff4db 100%); box-shadow: 0 30rpx 80rpx rgba(5,45,32,.38); animation: egWeeklyIn .36s cubic-bezier(.18,.88,.27,1.08) both; }
.eg-weekly-head { display: flex; align-items: center; justify-content: space-between; }
.eg-weekly-title-line { display: flex; align-items: center; gap: 10rpx; }
.eg-weekly-title { color: #155e4b; font-size: 37rpx; font-weight: 1000; }
.eg-weekly-preview { padding: 5rpx 10rpx; border-radius: 999rpx; color: #9a5b14; background: #fff0bd; font-size: 19rpx; font-weight: 900; }
.eg-weekly-sub { display: block; margin-top: 5rpx; color: #6b837a; font-size: 23rpx; font-weight: 700; }
.eg-weekly-medal { display: flex; width: 76rpx; height: 76rpx; align-items: center; justify-content: center; border: 5rpx solid #fff3b2; border-radius: 50%; color: #fff; background: linear-gradient(145deg, #f5ad24, #e26925); box-shadow: 0 8rpx 0 #b85421, 0 12rpx 24rpx rgba(181,84,33,.24); font-size: 39rpx; font-weight: 1000; }
.eg-weekly-progress-card { display: flex; align-items: center; justify-content: space-between; margin-top: 20rpx; padding: 16rpx 18rpx; border: 3rpx solid #d7ebe2; border-radius: 24rpx; background: rgba(255,255,255,.9); }
.eg-weekly-progress-label { display: block; color: #657b73; font-size: 22rpx; font-weight: 700; }
.eg-weekly-progress-value { display: block; margin-top: -2rpx; color: #e06b24; font-size: 43rpx; font-weight: 1000; }
.eg-weekly-progress-copy { display: flex; flex-direction: column; align-items: flex-end; color: #235b49; font-size: 24rpx; font-weight: 900; line-height: 1.55; }
.eg-weekly-progress-copy text:last-child { color: #b86820; }
.eg-weekly-list { display: flex; flex-direction: column; gap: 7rpx; margin-top: 15rpx; }
.eg-weekly-row { display: flex; min-height: 68rpx; align-items: center; padding: 7rpx 14rpx; border: 2rpx solid transparent; border-radius: 19rpx; color: #345c50; background: rgba(255,255,255,.72); }
.eg-weekly-row.is-me { border-color: #f2bd48; color: #174f3e; background: linear-gradient(90deg, #fff2bb, #fff9e3); box-shadow: 0 5rpx 14rpx rgba(153,102,17,.12); transform: scale(1.015); }
.eg-weekly-place { width: 43rpx; color: #6b7d76; font-size: 27rpx; font-weight: 1000; text-align: center; }
.eg-weekly-row:nth-child(1) .eg-weekly-place { color: #dd7b17; font-size: 31rpx; }
.eg-weekly-avatar { display: flex; width: 52rpx; height: 52rpx; flex: 0 0 52rpx; align-items: center; justify-content: center; margin-left: 6rpx; border: 3rpx solid rgba(255,255,255,.9); border-radius: 50%; color: #fff; background: #5d9b84; box-shadow: 0 3rpx 9rpx rgba(30,78,61,.16); font-size: 24rpx; font-weight: 1000; }
.eg-weekly-avatar.is-tone-2 { background: #e39932; }
.eg-weekly-avatar.is-tone-3 { background: #6c83c8; }
.eg-weekly-avatar.is-tone-4 { background: #e5683e; }
.eg-weekly-avatar.is-tone-5 { background: #a673bd; }
.eg-weekly-avatar.is-tone-6 { background: #448e9c; }
.eg-weekly-avatar.is-tone-7 { background: #c27b49; }
.eg-weekly-name { flex: 1; margin-left: 13rpx; overflow: hidden; font-size: 27rpx; font-weight: 850; text-overflow: ellipsis; white-space: nowrap; }
.eg-weekly-me { margin-right: 8rpx; padding: 3rpx 9rpx; border-radius: 999rpx; color: #fff; background: #dd6b27; font-size: 18rpx; font-weight: 900; }
.eg-weekly-count { min-width: 70rpx; color: #315b4d; font-size: 29rpx; font-weight: 1000; text-align: right; }
.eg-cheer-card { margin-top: 16rpx; padding: 15rpx 17rpx 14rpx; border: 3rpx solid #f0d38d; border-radius: 23rpx; background: linear-gradient(135deg, #fff9d9, #fff1c2); }
.eg-cheer-card.is-family { border-color: #d9c8ef; background: linear-gradient(135deg, #f8f2ff, #efe7ff); }
.eg-cheer-card.is-bright { border-color: #f4c77b; background: linear-gradient(135deg, #fff8dc, #ffeec7); }
.eg-cheer-top { display: flex; align-items: center; justify-content: space-between; }
.eg-cheer-source { color: #8e5c18; font-size: 21rpx; font-weight: 900; }
.eg-cheer-card.is-family .eg-cheer-source { color: #77539c; }
.eg-cheer-change { padding: 5rpx 12rpx; border-radius: 999rpx; color: #176b52; background: rgba(255,255,255,.75); font-size: 19rpx; font-weight: 900; }
.eg-cheer-copy { display: block; margin-top: 7rpx; color: #55462d; font-size: 28rpx; font-weight: 900; line-height: 1.42; }
.eg-cheer-note { display: block; margin-top: 5rpx; color: #8d7c77; font-size: 18rpx; }
.eg-weekly-start { display: flex; height: 84rpx; align-items: center; justify-content: center; margin-top: 18rpx; border-radius: 24rpx; color: #fff; background: linear-gradient(135deg, #24906c, #12654a); box-shadow: 0 9rpx 20rpx rgba(18,101,74,.24); font-size: 30rpx; font-weight: 1000; }
.eg-weekly-footnote { display: block; margin-top: 10rpx; color: #8b8b7d; font-size: 18rpx; text-align: center; }
.eg-stage-overlay { position: fixed; z-index: 58; inset: 0; display: flex; align-items: center; justify-content: center; padding: 40rpx; background: rgba(22,62,50,.42); pointer-events: none; }
.eg-stage-card { width: 100%; max-width: 620rpx; padding: 34rpx 28rpx 30rpx; border: 6rpx solid #fff; border-radius: 38rpx; background: linear-gradient(155deg, #f5fff9, #fff8da); box-shadow: 0 26rpx 70rpx rgba(10,56,41,.3); text-align: center; animation: egStageIn 1.8s cubic-bezier(.16,.86,.28,1) both; }
.eg-stage-kicker { display: block; color: #16815f; font-size: 23rpx; font-weight: 900; letter-spacing: 3rpx; }
.eg-stage-number { display: block; margin-top: 5rpx; color: #e47722; font-size: 48rpx; font-weight: 1000; line-height: 1.1; }
.eg-stage-title { display: block; margin-top: 7rpx; color: #194e3d; font-size: 38rpx; font-weight: 900; }
.eg-stage-foods { display: flex; justify-content: center; gap: 9rpx; margin-top: 22rpx; }
.eg-stage-food { display: flex; width: 92rpx; min-height: 110rpx; flex-direction: column; align-items: center; justify-content: center; border: 3rpx solid #dbece4; border-radius: 20rpx; color: #315b4e; background: #fff; font-size: 20rpx; font-weight: 800; }
.eg-stage-food > image { width: 72rpx; height: 52rpx; border-radius: 12rpx; }
.eg-stage-food > text { margin-top: 8rpx; }
.eg-stage-food.is-danger { border-color: #ef8b52; color: #a43c20; background: #fff1e6; box-shadow: 0 0 0 4rpx rgba(239,139,82,.12); }
.eg-stage-tip { display: block; margin-top: 20rpx; color: #6e776d; font-size: 21rpx; font-weight: 700; }
.eg-reshuffle-overlay { position: fixed; z-index: 65; inset: 0; display: flex; align-items: center; justify-content: center; padding: 46rpx; background: rgba(19,55,45,.5); }
.eg-reshuffle-card { width: 100%; max-width: 540rpx; padding: 38rpx 30rpx; border: 5rpx solid rgba(255,255,255,.95); border-radius: 36rpx; background: linear-gradient(155deg, #f4fff9, #e6f6ef); box-shadow: 0 24rpx 64rpx rgba(10,48,37,.3); text-align: center; }
.eg-reshuffle-icon { display: flex; width: 94rpx; height: 94rpx; align-items: center; justify-content: center; margin: 0 auto 18rpx; border-radius: 50%; background: linear-gradient(135deg, #2aa87b, #126a50); box-shadow: 0 10rpx 24rpx rgba(18,106,80,.28); animation: egReshuffleSpin .85s linear infinite; }
.eg-reshuffle-title { display: block; color: #174e3d; font-size: 36rpx; font-weight: 900; }
.eg-reshuffle-desc { display: block; margin-top: 10rpx; color: #5b746b; font-size: 24rpx; font-weight: 700; }
.eg-overlay { position: fixed; z-index: 50; top: 0; right: 0; bottom: 0; left: 0; display: flex; align-items: center; justify-content: center; padding: 36rpx; background: rgba(20, 48, 40, .62); }
.eg-modal { width: 100%; max-width: 650rpx; padding: 34rpx 28rpx 28rpx; border-radius: 38rpx; background: #fffdf8; box-shadow: 0 28rpx 70rpx rgba(0,0,0,.22); }
.eg-modal-icon { display: block; font-size: 68rpx; text-align: center; }
.eg-modal-title { display: block; margin-top: 6rpx; color: #204e3d; font-size: 38rpx; font-weight: 900; text-align: center; }
.eg-modal-sub { display: block; margin: 8rpx 0 22rpx; color: #718078; font-size: 24rpx; text-align: center; }
.eg-result-warning { display: flex; width: 92rpx; height: 92rpx; align-items: center; justify-content: center; margin: 0 auto; border-radius: 50%; color: #fff; background: #e4572e; font-size: 60rpx; font-weight: 900; }
.eg-result-food { display: block; margin: 12rpx 0 18rpx; color: #c34b27; font-size: 28rpx; font-weight: 800; text-align: center; }
.eg-knowledge-card { padding: 22rpx; border-radius: 24rpx; background: #fff3e7; }
.eg-knowledge-title { display: block; color: #8d4427; font-size: 26rpx; font-weight: 800; }
.eg-knowledge-copy { display: block; margin-top: 9rpx; color: #5f514a; font-size: 23rpx; line-height: 1.65; }
.eg-knowledge-review { display: block; margin-top: 12rpx; color: #9b7768; font-size: 19rpx; }
.eg-result-score { display: flex; align-items: center; justify-content: space-between; margin: 20rpx 6rpx; color: #466258; font-size: 25rpx; }
.eg-result-score text:last-child { color: #e16f24; font-size: 36rpx; font-weight: 900; }
.eg-result-best { display: flex; align-items: center; justify-content: space-between; margin: -12rpx 6rpx 20rpx; color: #75867f; font-size: 22rpx; }
.eg-result-best text:last-child { color: #315d4e; font-size: 27rpx; font-weight: 900; }
.eg-primary-btn, .eg-secondary-btn { display: flex; height: 88rpx; align-items: center; justify-content: center; border-radius: 24rpx; font-size: 28rpx; font-weight: 800; }
.eg-primary-btn { color: #fff; background: linear-gradient(135deg, #238b68, #126649); box-shadow: 0 10rpx 22rpx rgba(18,102,73,.23); }
.eg-secondary-btn { margin-top: 12rpx; color: #45665b; background: #edf4f1; }
@keyframes egPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(245,158,11,.2); }
50% { box-shadow: 0 0 0 8rpx rgba(245,158,11,.12); }
}
@keyframes egWeeklyIn {
0% { opacity: 0; transform: translateY(34rpx) scale(.91); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes egHit {
0% { opacity: 1; transform: scale(1); }
42% { opacity: 1; transform: scale(1.18) rotate(-3deg); }
100% { opacity: 0; transform: scale(.32) rotate(7deg); }
}
@keyframes egTaskRewardFlight {
0% { opacity: 0; transform: translate3d(-20rpx, 0, 0) scale(.62) rotate(-8deg); }
16% { opacity: 1; transform: translate3d(0, -24rpx, 0) scale(1.1) rotate(3deg); }
72% { opacity: 1; transform: translate3d(270rpx, 900rpx, 0) scale(.68) rotate(8deg); }
100% { opacity: 0; transform: translate3d(350rpx, 1205rpx, 0) scale(.3) rotate(2deg); }
}
@keyframes egRewardGlow {
from { opacity: .4; transform: scale(.86); }
to { opacity: .9; transform: scale(1.12); }
}
@keyframes egFireSweep {
0% { opacity: 0; transform: translate3d(-38%, 20rpx, 0) scaleX(.6); }
28% { opacity: 1; }
72% { opacity: .96; }
100% { opacity: 0; transform: translate3d(58%, -15rpx, 0) scaleX(1.08); }
}
@keyframes egFireSpark {
0% { opacity: 0; transform: translateY(14rpx) scale(.5) rotate(35deg); }
38% { opacity: 1; }
100% { opacity: 0; transform: translateY(-38rpx) scale(1.05) rotate(58deg); }
}
@keyframes egFireGlow {
0% { opacity: 0; transform: scale(.72); }
38% { opacity: 1; transform: scale(1.08); }
100% { opacity: 0; transform: scale(1.28); }
}
@keyframes egFireCellHit {
0% { opacity: 1; transform: scale(1); }
24% { opacity: 1; transform: scale(1.09); }
72% { opacity: 1; transform: scale(1.02); }
100% { opacity: 0; transform: scale(.48); }
}
@keyframes egFireFlame {
0% { opacity: 0; transform: translateY(20rpx) rotate(42deg) scale(.55); }
38% { opacity: 1; transform: translateY(0) rotate(42deg) scale(1.05); }
100% { opacity: 0; transform: translateY(-31rpx) rotate(48deg) scale(.86); }
}
@keyframes egFlameReady {
from { transform: scale(.94); filter: brightness(.96); }
to { transform: scale(1.08); filter: brightness(1.18); }
}
@keyframes egLReady {
from { transform: scale(.94) rotate(-2deg); filter: brightness(.96); }
to { transform: scale(1.06) rotate(2deg); filter: brightness(1.12); }
}
@keyframes egImpact {
0% { opacity: 0; transform: translate(-50%, -42%) scale(.55); }
35% { opacity: 1; transform: translate(-50%, -50%) scale(1.16); }
72% { opacity: 1; transform: translate(-50%, -54%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -76%) scale(.92); }
}
@keyframes egJackpotIn {
0% { opacity: 0; transform: translate(-50%, -45%) scale(.48) rotate(-4deg); }
20% { opacity: 1; transform: translate(-50%, -50%) scale(1.09) rotate(2deg); }
31% { transform: translate(-50%, -50%) scale(.98) rotate(0); }
78% { opacity: 1; transform: translate(-50%, -53%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -68%) scale(.92); }
}
@keyframes egTripleIn {
0% { opacity: 0; transform: translate(-50%, -44%) scale(.82); }
20% { opacity: 1; transform: translate(-50%, -50%) scale(1.03); }
74% { opacity: 1; transform: translate(-50%, -52%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -60%) scale(.96); }
}
@keyframes egTripleCan {
0% { opacity: 0; transform: translateY(18rpx) scale(.72); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes egFourIn {
0% { opacity: 0; transform: translate(-50%, -44%) scale(.62) rotate(-3deg); }
18% { opacity: 1; transform: translate(-50%, -50%) scale(1.08) rotate(1deg); }
29% { transform: translate(-50%, -50%) scale(.98) rotate(0); }
78% { opacity: 1; transform: translate(-50%, -52%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -65%) scale(.94); }
}
@keyframes egFourCan {
0% { opacity: 0; transform: translateY(-22rpx) rotate(-7deg) scale(.65); }
75% { opacity: 1; transform: translateY(5rpx) rotate(2deg) scale(1.05); }
100% { opacity: 1; transform: translateY(0) rotate(0) scale(1); }
}
@keyframes egFiveCan {
0% { opacity: 0; transform: translateY(-40rpx) scale(.52) rotate(-8deg); filter: brightness(1.5); }
70% { opacity: 1; transform: translateY(7rpx) scale(1.08) rotate(2deg); }
100% { opacity: 1; transform: translateY(0) scale(1) rotate(0); filter: brightness(1); }
}
@keyframes egLockedCan {
from { opacity: .62; transform: scale(.92); filter: brightness(.9); }
to { opacity: 1; transform: scale(1.06); filter: brightness(1.2); }
}
@keyframes egPromisePulse {
from { transform: scale(.97); box-shadow: 0 5rpx 15rpx rgba(28,7,62,.22); }
to { transform: scale(1.03); box-shadow: 0 7rpx 21rpx rgba(255,221,89,.38); }
}
@keyframes egReelStop {
0% { opacity: .2; transform: translateY(-48rpx) scaleY(1.4); filter: blur(5rpx); }
70% { opacity: 1; transform: translateY(7rpx) scaleY(.94); filter: blur(0); }
100% { transform: translateY(0) scaleY(1); }
}
@keyframes egLights {
0% { opacity: .35; }
100% { opacity: 1; }
}
@keyframes egConfettiFall {
0% { opacity: 0; transform: translate3d(0, -8vh, 0) rotate(0deg) scale(.7); }
9% { opacity: 1; }
48% { transform: translate3d(42rpx, 48vh, 0) rotate(420deg) scale(1); }
100% { opacity: .92; transform: translate3d(-28rpx, 112vh, 0) rotate(920deg) scale(.86); }
}
@keyframes egPointsPop {
0% { opacity: 0; transform: scale(.35) rotate(-7deg); }
55% { opacity: 1; transform: scale(1.28) rotate(3deg); }
78% { transform: scale(.92) rotate(0); }
100% { opacity: 1; transform: scale(1); }
}
@keyframes egStageIn {
0% { opacity: 0; transform: scale(.7) translateY(34rpx); }
18% { opacity: 1; transform: scale(1.06) translateY(0); }
30%, 76% { opacity: 1; transform: scale(1); }
100% { opacity: 0; transform: scale(.96) translateY(-22rpx); }
}
@keyframes egReshuffleSpin {
from { transform: rotate(0); }
to { transform: rotate(360deg); }
}
@media screen and (max-height: 760px) {
.eg-task-card { padding-top: 13rpx; padding-bottom: 13rpx; }
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,49 @@
/**
* 将 SVG 字符串转为小程序可用的 data URL(base64 在真机上更稳定)
*/
function utf8ToBytes(str) {
const encoded = encodeURIComponent(str)
const bytes = []
for (let i = 0; i < encoded.length; i++) {
if (encoded.charCodeAt(i) === 37) {
bytes.push(parseInt(encoded.substring(i + 1, i + 3), 16))
i += 2
} else {
bytes.push(encoded.charCodeAt(i))
}
}
return new Uint8Array(bytes)
}
function bytesToBase64(bytes) {
if (typeof uni !== 'undefined' && typeof uni.arrayBufferToBase64 === 'function') {
return uni.arrayBufferToBase64(bytes.buffer)
}
if (typeof btoa !== 'undefined') {
let binary = ''
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i])
}
return btoa(binary)
}
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
let result = ''
for (let i = 0; i < bytes.length; i += 3) {
const a = bytes[i]
const b = i + 1 < bytes.length ? bytes[i + 1] : 0
const c = i + 2 < bytes.length ? bytes[i + 2] : 0
result += chars[a >> 2]
result += chars[((a & 3) << 4) | (b >> 4)]
result += i + 1 < bytes.length ? chars[((b & 15) << 2) | (c >> 6)] : '='
result += i + 2 < bytes.length ? chars[c & 63] : '='
}
return result
}
export function svgToDataUrl(svg) {
const normalized = String(svg || '').trim()
if (!normalized) return ''
const base64 = bytesToBase64(utf8ToBytes(normalized))
return `data:image/svg+xml;base64,${base64}`
}
@@ -0,0 +1,33 @@
const FOOD_ASSET_NAMES = new Set([
'apple.jpg', 'banana.jpg', 'blackCoffee.jpg', 'broccoli.jpg', 'brownRice.jpg',
'camel.jpg', 'celery.jpg', 'centuryCongee.jpg', 'chicken.jpg', 'coffee.jpg',
'congee.jpg', 'corn.jpg', 'cucumber.jpg', 'egg.jpg', 'eightCongee.jpg',
'grainMantou.jpg', 'greenBean.jpg', 'lettuce.jpg', 'mango.jpg', 'milk.jpg',
'milkTea.jpg', 'milletCongee.jpg', 'mushroom.jpg', 'noodleSoup.jpg', 'onion.jpg',
'orangeJuice.jpg', 'pepper.jpg', 'pineapple.jpg', 'potato.jpg', 'riceNoodleSoup.jpg',
'shrimp.jpg', 'soda.jpg', 'strawberry.jpg', 'taro.jpg', 'tea.jpg', 'tomato.jpg',
'udon.jpg', 'wonton.jpg'
])
/**
* 让游戏食品图进入 tongji 分包,避免增加微信小程序主包体积。
* 迁移到其他 uni-app 项目时,在 vite.config 中复用此函数即可。
*/
export function endlessGameAssetFileNames(assetInfo = {}) {
const candidates = [
assetInfo.name,
...(assetInfo.names || []),
assetInfo.originalFileName,
...(assetInfo.originalFileNames || [])
].filter(Boolean)
const isEndlessFood = candidates.some((value) => {
const normalized = String(value).replace(/\\/g, '/')
const basename = normalized.split('/').pop()
return normalized.includes('tongji/endless-game/assets/food/') || FOOD_ASSET_NAMES.has(basename)
})
return isEndlessFood
? 'tongji/endless-game/assets/food/[name].[hash][extname]'
: 'assets/[name].[hash][extname]'
}
+1 -1
View File
@@ -1514,7 +1514,7 @@ async function goMorePage() {
function goGamePage() {
uni.navigateTo({
url: '/tongji/pages/game',
url: '/tongji/endless-game/index',
fail() {
uni.showToast({ title: '暂时无法打开游戏', icon: 'none' })
}
+8
View File
@@ -1,10 +1,18 @@
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
import { endlessGameAssetFileNames } from './tongji/endless-game/viteAssetOutput.js'
// uni-app 工程根目录就是源码目录(HBuilderX 兼容)
// 编译产物默认输出到 ./dist/<mode>/<platform>
export default defineConfig({
plugins: [uni()],
build: {
rollupOptions: {
output: {
assetFileNames: endlessGameAssetFileNames,
},
},
},
css: {
preprocessorOptions: {
scss: {
@@ -39,7 +39,6 @@ use think\facade\Log;
*
* 日志:无论分配与否,每条待指派诊单都会写入 tcm_diagnosis_auto_assign_log,记录原因(为什么分配 / 为什么不分配)。
* 成功分配同时写 tcm_diagnosis_assign_logis_inherit=0,计入次月接诊率分母),与手动指派同口径。
* 写入时 from_assistant_id 尽量带上「一中心」原医助(指派日志中最近的一中心身份,否则回退业务单创建人若属一中心),避免原医助恒为空。
*/
class AutoAssignPendingDiagnosis extends Command
{
@@ -500,12 +499,9 @@ class AutoAssignPendingDiagnosis extends Command
$relatedPoCreatorId = 0;
}
// 自动分给二中心时,原医助带上一中心身份(列表「原医助」不再恒为空)
$fromAssistantId = $this->resolveYiCenterPreviousAssistantId($diagnosisId, $relatedPoCreatorId);
Db::name('tcm_diagnosis_assign_log')->insert([
'diagnosis_id' => $diagnosisId,
'from_assistant_id' => $fromAssistantId,
'from_assistant_id' => 0,
'to_assistant_id' => $toAssistantId,
'operator_admin_id' => 0,
'operator_name' => '系统自动分配',
@@ -528,88 +524,6 @@ class AutoAssignPendingDiagnosis extends Command
}
}
/**
* 解析诊单对应的「一中心」原医助 id,供自动指派写入 from_assistant_id。
* 优先:指派日志由新到旧,取最近出现的一中心身份(先 to 后 from);
* 回退:最新业务单创建人若属一中心则用之。跳过二中心身份,避免原医助写成上一任二中心。
*/
private function resolveYiCenterPreviousAssistantId(int $diagnosisId, int $relatedPoCreatorId): int
{
if ($diagnosisId <= 0) {
return 0;
}
$yiDeptSet = DeptLogic::getYiCenterSubtreeDeptIdSet();
if ($yiDeptSet === []) {
return 0;
}
$logs = Db::name('tcm_diagnosis_assign_log')
->where('diagnosis_id', $diagnosisId)
->order('id', 'desc')
->field(['from_assistant_id', 'to_assistant_id'])
->select()
->toArray();
$candidateIds = [];
foreach ($logs as $log) {
$to = (int) ($log['to_assistant_id'] ?? 0);
$from = (int) ($log['from_assistant_id'] ?? 0);
if ($to > 0) {
$candidateIds[] = $to;
}
if ($from > 0) {
$candidateIds[] = $from;
}
}
if ($relatedPoCreatorId > 0) {
$candidateIds[] = $relatedPoCreatorId;
}
$candidateIds = array_values(array_unique(array_filter($candidateIds)));
if ($candidateIds === []) {
return 0;
}
$yiAdminSet = $this->filterAdminIdsInDeptSet($candidateIds, $yiDeptSet);
if ($yiAdminSet === []) {
return 0;
}
foreach ($logs as $log) {
$to = (int) ($log['to_assistant_id'] ?? 0);
if ($to > 0 && isset($yiAdminSet[$to])) {
return $to;
}
$from = (int) ($log['from_assistant_id'] ?? 0);
if ($from > 0 && isset($yiAdminSet[$from])) {
return $from;
}
}
if ($relatedPoCreatorId > 0 && isset($yiAdminSet[$relatedPoCreatorId])) {
return $relatedPoCreatorId;
}
return 0;
}
/**
* @param list<int> $adminIds
* @param array<int, true> $deptIdSet
*
* @return array<int, true> admin_id => true
*/
private function filterAdminIdsInDeptSet(array $adminIds, array $deptIdSet): array
{
if ($adminIds === [] || $deptIdSet === []) {
return [];
}
$rows = Db::name('admin_dept')
->whereIn('admin_id', $adminIds)
->whereIn('dept_id', array_keys($deptIdSet))
->column('admin_id');
return array_fill_keys(array_map('intval', $rows), true);
}
/**
* @param array<string,mixed> $diag 诊单行(含 id/patient_name/phone
* @param array<string,mixed> $extra action/assistant_id/assistant_name/tier/visit2_rate/round_no/reason
+1 -1
View File
@@ -18,7 +18,7 @@ use think\console\Output;
* 配置 crontab(每 10 分钟):拉快递 100 + 按履约「已发货」核对释放诊单医助(跳过已完成/已签收业务单,与是否甘草单无关)
* 0,10,20,30,40,50 * * * * cd /path/to/server && php think express:auto-update >> /dev/null 2>&1
*
* 已对「待释放医助非二中心」执行发货/签收自动释放的诊单会写入 tcm_diagnosis.shipped_non_er_assistant_cleared_at
* 已对「业务订单创建人非二中心」(或创建人为开方医生时按待释放身份判定)执行发货/签收自动释放的诊单会写入 tcm_diagnosis.shipped_non_er_assistant_cleared_at
* 后续履约核对不再重复扫描(需先执行 sql/1.9.20260507/add_diagnosis_shipped_non_er_assistant_cleared_at.sql)。
*/
class ExpressAutoUpdate extends Command
@@ -34,7 +34,7 @@ use think\console\Output;
* 0,30 * * * * cd /path/to/server && php think gancao:sync-logistics >> runtime/log/gancao_sync.log 2>&1
*
* 跳过履约状态为已完成(3)、已签收(6) 的业务订单,不再拉取甘草路由。
* 已对「待释放医助非二中心」(发货/签收自动释放规则见 ExpressTrackingService)的诊单会写入 tcm_diagnosis.shipped_non_er_assistant_cleared_at
* 已对「业务订单创建人非二中心」(发货/签收自动释放规则见 ExpressTrackingService)的诊单会写入 tcm_diagnosis.shipped_non_er_assistant_cleared_at
* 后续履约核对不再重复扫描(需先执行 sql/1.9.20260507/add_diagnosis_shipped_non_er_assistant_cleared_at.sql)。
*/
class GancaoSyncLogisticsRoute extends Command
@@ -588,7 +588,7 @@ class ExpressTrackingService
/**
* 处方业务订单履约「已发货(5) / 已签收(6)」时:清空关联诊单(患者)医助,并写入 tcm_diagnosis_assign_log(含关联业务订单 creator_id / create_time 快照,便于核对医助创建订单时间)。
* 快递 100 定时拉轨迹、甘草路由、甘草回调与履约核对共用同一规则。
* 不自动清空的情形:(1待释放医助(诊单 assistant_id,或代建回退身份)归属「二中心」部门子树;(2)诊单当前已指派、或指派日志曾 to 给「二中心」医助(含系统自动分配);(3)诊单已有 shipped_non_er_assistant_cleared_at 且 assistant_id 已为 0(视为已处理);(4)指派日志存在后台人工指派:`operator_admin_id>0` 且 `to_assistant_id>0`;或 related_po_creator_id=from_assistant_id 且 to>0 的旧数据兼容。避免「系统·已发货履约核对」覆盖人工指派链。
* 不自动清空的情形:(1按业务订单 creator_id(代建场景)或回退身份判断是否在「二中心」部门子树;(2)诊单已有 shipped_non_er_assistant_cleared_at 且 assistant_id 已为 0(视为已处理);(3)指派日志存在后台人工指派:`operator_admin_id>0` 且 `to_assistant_id>0`(系统任务为 operator_admin_id=0;或 related_po_creator_id=from_assistant_id 且 to>0 的旧数据兼容。避免「系统·已发货履约核对」覆盖人工指派链。
*
* @param array{tracking_id?:int,tracking_number?:string,source?:string} $meta
* @return array{
@@ -663,18 +663,19 @@ class ExpressTrackingService
return null;
}
// 二中心不释放:(a)待释放医助属二中心;(b)诊单当前医助属二中心;(c)指派日志曾指派给二中心医助
$skipErzhongxin = self::currentAssistantBelongsToErzhongxinDeptTree($fromAssistantId)
|| ($diagAssistantIdNow > 0 && self::currentAssistantBelongsToErzhongxinDeptTree($diagAssistantIdNow))
|| self::diagnosisHasBeenAssignedToErzhongxinAssistant($diagnosisId);
if ($skipErzhongxin) {
Log::info('Skipped auto assistant release (shipped/signed): 二中心 assistant assigned or targeted', [
/** 是否因「二中心」跳过释放:优先看业务订单创建人 creator_id(代建场景);创建人即开方医生时用 fromAssistantId */
$erZhongxinCheckAdminId = ($poCreatorId > 0 && ($rxId <= 0 || $poCreatorId !== $rxCreator))
? $poCreatorId
: $fromAssistantId;
if ($erZhongxinCheckAdminId > 0 && self::currentAssistantBelongsToErzhongxinDeptTree($erZhongxinCheckAdminId)) {
Log::info('Skipped auto assistant release (shipped/signed): order creator (or fallback from-id) under 二中心 dept tree', [
'diagnosis_id' => $diagnosisId,
'prescription_order_id' => $orderId,
'source' => $source,
'from_assistant_id' => $fromAssistantId,
'diagnosis_assistant_id' => $diagAssistantIdNow,
'prescription_order_creator_id' => $poCreatorId,
'er_zhongxin_check_admin_id' => $erZhongxinCheckAdminId,
'prescription_order_creator_id' => $poCreatorId,
'from_assistant_id_for_log' => $fromAssistantId,
]);
return null;
@@ -797,36 +798,6 @@ class ExpressTrackingService
return false;
}
/**
* 诊单指派日志是否曾指派给「二中心」医助(to_assistant_id>0,含人工指派与系统自动分配)。
*/
private static function diagnosisHasBeenAssignedToErzhongxinAssistant(int $diagnosisId): bool
{
if ($diagnosisId <= 0) {
return false;
}
$toIds = Db::name('tcm_diagnosis_assign_log')
->where('diagnosis_id', $diagnosisId)
->where('to_assistant_id', '>', 0)
->column('to_assistant_id');
if ($toIds === [] || $toIds === null) {
return false;
}
$seen = [];
foreach ($toIds as $aid) {
$aid = (int) $aid;
if ($aid <= 0 || isset($seen[$aid])) {
continue;
}
$seen[$aid] = true;
if (self::currentAssistantBelongsToErzhongxinDeptTree($aid)) {
return true;
}
}
return false;
}
/**
* 若诊单曾被后台人工指派到具体医助,则不再执行发运/签收自动清空(避免「系统·已发货履约核对」覆盖人工链)。
* 判定:operator_admin_id>0 且 to_assistant_id>0(人工);系统写入的释放/同步为 operator_admin_id=0。
@@ -1060,7 +1031,7 @@ class ExpressTrackingService
/**
* 按处方订单履约核对:仅已发货(5) 时符合条件的订单调用 applyAssistantReleaseForShippedPrescriptionOrder(已完成/已签收不再扫描)。
* 与快递 100 / 甘草来自哪个物流渠道无关;是否清空医助以该方法为准(**二中心豁免看待释放医助**参见 apply;已 shipped 释放且 assistant_id=0 视为已处理)。
* 与快递 100 / 甘草来自哪个物流渠道无关;是否清空医助以该方法为准(**二中心豁免看业务单创建人**参见 apply;已 shipped 释放且 assistant_id=0 视为已处理)。
*
* @return array{scanned: int, cleared: int, lines: list<string>}
*/