Merge remote-tracking branch 'origin/master' into feature/tongji-endless-game
# Conflicts: # TUICallKit-Vue3/tongji/endless-game/README.md # TUICallKit-Vue3/tongji/endless-game/index.vue # TUICallKit-Vue3/vite.config.ts
@@ -1,36 +1,19 @@
|
||||
# 控糖消消乐独立功能包
|
||||
|
||||
此目录包含“控糖消消乐”无尽三消版的全部运行文件,可以整体复制到另一个 uni-app Vue 3 项目的 `tongji/endless-game/` 下。
|
||||
此目录包含“控糖消消乐”无尽三消版的页面与运行代码。食品图片由远端 COS 提供,图标组件复用 `tongji` 分包的公共组件。
|
||||
|
||||
## 目录内容
|
||||
|
||||
- `index.vue`:页面、棋盘算法、关卡主题、任务、道具、三/四/五连奖励和适老化交互。
|
||||
- `game-endless.scss`:完整页面与动画样式。
|
||||
- `components/TongjiIcon.vue`:本功能包使用的图标组件。
|
||||
- `composables/useGameAuth.js`:复用主小程序账号并处理 token 过期重登。
|
||||
- `composables/useGamePlatform.js`:周榜、成绩补传与微信分享的平台连接层。
|
||||
- `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` 数组中加入:
|
||||
1. 复制 `endless-game` 文件夹到目标项目的 `tongji/` 目录,并确保目标项目同时提供 `tongji/components/TongjiIcon.vue` 与 `tongji/utils/svgDataUrl.js`。
|
||||
2. 在目标项目 `pages.json` 的 `tongji` 分包 `pages` 数组中加入:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -46,14 +29,15 @@ build: {
|
||||
}
|
||||
```
|
||||
|
||||
4. 使用 `/tongji/endless-game/index` 打开游戏。
|
||||
5. 微信小程序后台需要允许音频域名:`https://gz-1349751149.cos.ap-guangzhou.myqcloud.com`。
|
||||
3. 使用 `/tongji/endless-game/index` 打开游戏。
|
||||
4. 微信小程序后台需要将 `https://gz-1349751149.cos.ap-guangzhou.myqcloud.com` 配置为合法的音频与图片下载域名。
|
||||
|
||||
## 说明
|
||||
|
||||
- 旧版“糖分突袭”页面及其独占依赖已经移除,项目只注册 `/tongji/endless-game/index` 这一款小游戏。
|
||||
- 食品图片固定使用 `https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/games/food/` 前缀,文件名必须与 `FOODS` 配置 key 的大小写保持一致。
|
||||
- 连消小目标按“连消×2”累计 2 次;连续 6 次普通消除没有连消时,下一次掉落会提供连消机会。“连消×3”保留为额外积分、驼乳粉与撒花惊喜,不阻挡主线任务。
|
||||
- 横向 3 个与竖向 3 个相交形成 L/T 形五消时,会在交点生成带高对比 L 标记的范围爆破棋子;该棋子再次被消除时清除周围九格。
|
||||
- 进入游戏时展示“本周 7 人同行榜”,包含真实平台昵称、前后名次、差距提示和可切换的同行/亲友鼓励;顶部“本周同行”可再次打开。后端部署方式见 `PLATFORM_INTEGRATION.md`。
|
||||
- 本功能包依赖 uni-app Vue 3 和 `@dcloudio/uni-app`,不依赖当前项目其他业务组件。
|
||||
- 本功能包依赖 uni-app Vue 3、`@dcloudio/uni-app` 以及 `tongji` 分包内的公共 `TongjiIcon` 组件。
|
||||
- 食物风险等级和科普文案集中在 `index.vue` 的 `FOODS` 配置中,正式上线前仍需由医院医生审核。
|
||||
|
||||
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,190 +0,0 @@
|
||||
<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>
|
||||
@@ -25,6 +25,13 @@ const SRC = {
|
||||
* rate 不同可在同一素材上听出明显差异
|
||||
*/
|
||||
const SFX = {
|
||||
// —— 基础素材别名(供分层音效直接调用)——
|
||||
tap: { src: SRC.tap, volume: 0.32, rate: 1, pool: 3 },
|
||||
pop: { src: SRC.pop, volume: 0.3, rate: 1, pool: 4 },
|
||||
fanfare: { src: SRC.fanfare, volume: 0.56, rate: 1, pool: 2 },
|
||||
blast: { src: SRC.blast, volume: 0.62, rate: 1, pool: 2 },
|
||||
sparkle: { src: SRC.sparkle, volume: 0.5, rate: 1, pool: 2 },
|
||||
|
||||
// —— 交互 ——
|
||||
select: { src: SRC.tap, volume: 0.32, rate: 1.05, pool: 3 },
|
||||
deselect: { src: SRC.tap, volume: 0.22, rate: 0.82, pool: 2 },
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
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)}`
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -20,14 +20,18 @@
|
||||
|
||||
.eg-nav {
|
||||
position: relative;
|
||||
display: flex;
|
||||
display: grid;
|
||||
box-sizing: border-box;
|
||||
grid-template-columns: 156rpx minmax(0, 1fr) 156rpx;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
column-gap: 12rpx;
|
||||
padding-right: 28rpx;
|
||||
padding-bottom: 16rpx;
|
||||
padding-left: 28rpx;
|
||||
}
|
||||
|
||||
.eg-nav-leading { display: flex; width: 156rpx; justify-content: flex-start; }
|
||||
|
||||
.eg-nav-btn {
|
||||
display: flex;
|
||||
width: 72rpx;
|
||||
@@ -39,10 +43,11 @@
|
||||
background: rgba(255,255,255,.82);
|
||||
}
|
||||
|
||||
.eg-nav-actions { display: flex; width: 156rpx; justify-content: flex-end; gap: 10rpx; }
|
||||
.eg-nav-actions { display: flex; width: 156rpx; flex-shrink: 0; 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-wrap { display: flex; min-width: 0; flex-direction: column; align-items: center; }
|
||||
.eg-title, .eg-subtitle { overflow: hidden; max-width: 100%; white-space: nowrap; text-overflow: ellipsis; }
|
||||
.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; }
|
||||
|
||||
@@ -13,9 +13,11 @@
|
||||
:style="piece.style"
|
||||
>{{ piece.emoji }}</text>
|
||||
</view>
|
||||
<view class="eg-nav" :style="{ paddingTop: safeTop + 'px' }">
|
||||
<view class="eg-nav-btn" @tap="goBack">
|
||||
<TongjiIcon name="chevron-left" size="md" color="#155e4b" />
|
||||
<view class="eg-nav" :style="navStyle">
|
||||
<view class="eg-nav-leading">
|
||||
<view class="eg-nav-btn" @tap="goBack">
|
||||
<TongjiIcon name="chevron-left" size="md" color="#155e4b" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="eg-title-wrap">
|
||||
<text class="eg-title">控糖消消乐</text>
|
||||
@@ -299,11 +301,12 @@
|
||||
<script setup>
|
||||
import { computed, getCurrentInstance, ref } from 'vue'
|
||||
import { onLoad, onShareAppMessage, onShareTimeline, onShow, onUnload } from '@dcloudio/uni-app'
|
||||
import TongjiIcon from './components/TongjiIcon.vue'
|
||||
import TongjiIcon from '../components/TongjiIcon.vue'
|
||||
import { useGameSfx } from './composables/useGameSfx.js'
|
||||
import { useGamePlatform } from './composables/useGamePlatform.js'
|
||||
import { useGameAuth } from './composables/useGameAuth.js'
|
||||
import FOOD_IMAGES from 'virtual:tongji-endless-food-images'
|
||||
|
||||
const FOOD_IMAGE_BASE_URL = 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/games/food/'
|
||||
|
||||
const ROWS = 5
|
||||
const COLS = 4
|
||||
@@ -371,7 +374,7 @@ const FOODS = {
|
||||
milkTea: { name: '含糖奶茶', short: '奶茶', emoji: '🧋', color: 'orange', level: 'high', levelLabel: '高糖', high: true, replaceable: true, tip: '含糖奶茶可能同时含有较多添加糖和能量,应关注配方与饮用量。' },
|
||||
camel: { name: '驼乳', short: '驼乳', emoji: '🥛', color: 'milk', level: 'prop', levelLabel: '道具', product: true }
|
||||
}
|
||||
Object.keys(FOODS).forEach((key) => { FOODS[key].img = FOOD_IMAGES[key] || '' })
|
||||
Object.keys(FOODS).forEach((key) => { FOODS[key].img = `${FOOD_IMAGE_BASE_URL}${key}.jpg` })
|
||||
const FOOD_GROUPS = [
|
||||
{ title: '中式早餐', safe: ['egg', 'cucumber', 'corn', 'grainMantou'], danger: 'congee' },
|
||||
{ title: '家常粥餐', safe: ['celery', 'mushroom', 'chicken', 'taro'], danger: 'centuryCongee' },
|
||||
@@ -426,7 +429,7 @@ const comboDryTurns = ref(0)
|
||||
const taskRewardVisible = ref(false)
|
||||
const taskRewardKey = ref(0)
|
||||
const statusText = ref('交换相邻食物,三个相同即可消除')
|
||||
const safeTop = ref(20)
|
||||
const navStyle = ref({ paddingTop: '20px' })
|
||||
const touch = { x: 0, y: 0, index: -1 }
|
||||
const dragIndex = ref(-1)
|
||||
const dragTarget = ref(-1)
|
||||
@@ -557,9 +560,36 @@ onShareTimeline(() => {
|
||||
}
|
||||
})
|
||||
|
||||
function initNavLayout() {
|
||||
let paddingTop = 20
|
||||
let paddingRight = uni.upx2px(28)
|
||||
|
||||
try {
|
||||
const info = uni.getSystemInfoSync()
|
||||
paddingTop = Number(info.safeAreaInsets?.top || info.statusBarHeight) || 20
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
const menu = typeof uni.getMenuButtonBoundingClientRect === 'function'
|
||||
? uni.getMenuButtonBoundingClientRect()
|
||||
: null
|
||||
if (menu && menu.width > 0) {
|
||||
paddingTop = Math.max(paddingTop, Number(menu.top) || 0)
|
||||
paddingRight = Math.max(
|
||||
paddingRight,
|
||||
(Number(info.windowWidth) || 0) - Number(menu.left) + uni.upx2px(8)
|
||||
)
|
||||
}
|
||||
// #endif
|
||||
} catch (_) {}
|
||||
|
||||
navStyle.value = {
|
||||
paddingTop: `${paddingTop}px`,
|
||||
paddingRight: `${paddingRight}px`
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((options = {}) => {
|
||||
const info = uni.getSystemInfoSync()
|
||||
safeTop.value = info.safeAreaInsets?.top || info.statusBarHeight || 20
|
||||
initNavLayout()
|
||||
bestScore.value = Number(uni.getStorageSync('tongji_endless_best') || 0)
|
||||
incomingInviteCode = String(options.invite_code || '').trim()
|
||||
startGame()
|
||||
@@ -684,7 +714,15 @@ function startGame() {
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack({ fail: () => uni.switchTab({ url: '/pages/index/index' }) })
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: () => uni.redirectTo({ url: '/tongji/pages/weekly' })
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.redirectTo({ url: '/tongji/pages/weekly' })
|
||||
}
|
||||
|
||||
function showRules() {
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* 将 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}`
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
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]'
|
||||
}
|
||||
@@ -98,15 +98,27 @@
|
||||
<view v-if="chartUseMockData" class="st-chart-foot">
|
||||
<text class="st-chart-mock-hint">示例数据,累计记录满 7 天后显示您的真实趋势</text>
|
||||
</view>
|
||||
<view class="st-game-entry st-game-entry--in-card" @click="goGamePage" style="margin: 0 20rpx 0 20rpx;">
|
||||
<view class="st-game-entry-icon">
|
||||
<TongjiIcon name="activity" size="sm" color="#006c49" />
|
||||
<view class="st-game-entry-list">
|
||||
<view class="st-game-entry st-game-entry--in-card" @click="goSugarRaidGamePage">
|
||||
<view class="st-game-entry-icon">
|
||||
<TongjiIcon name="activity" size="sm" color="#006c49" />
|
||||
</view>
|
||||
<view class="st-game-entry-body">
|
||||
<text class="st-game-entry-title">识糖小课堂</text>
|
||||
<text class="st-game-entry-sub">认一认:哪些食物含糖高</text>
|
||||
</view>
|
||||
<TongjiIcon name="chevron-right" size="sm" color="#64748b" />
|
||||
</view>
|
||||
<view class="st-game-entry-body">
|
||||
<text class="st-game-entry-title">识糖小课堂</text>
|
||||
<text class="st-game-entry-sub">认一认:哪些食物含糖高</text>
|
||||
<view class="st-game-entry st-game-entry--in-card" @click="goEndlessGamePage">
|
||||
<view class="st-game-entry-icon">
|
||||
<TongjiIcon name="sparkles" size="sm" color="#006c49" />
|
||||
</view>
|
||||
<view class="st-game-entry-body">
|
||||
<text class="st-game-entry-title">控糖消消乐</text>
|
||||
<text class="st-game-entry-sub">无尽消除,挑战控糖高分</text>
|
||||
</view>
|
||||
<TongjiIcon name="chevron-right" size="sm" color="#64748b" />
|
||||
</view>
|
||||
<TongjiIcon name="chevron-right" size="sm" color="#64748b" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="st-training-section">
|
||||
@@ -1512,15 +1524,23 @@ async function goMorePage() {
|
||||
uni.navigateTo({ url: buildMorePageUrl() })
|
||||
}
|
||||
|
||||
function goGamePage() {
|
||||
function openGamePage(url) {
|
||||
uni.navigateTo({
|
||||
url: '/tongji/endless-game/index',
|
||||
url,
|
||||
fail() {
|
||||
uni.showToast({ title: '暂时无法打开游戏', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function goSugarRaidGamePage() {
|
||||
openGamePage('/tongji/pages/game')
|
||||
}
|
||||
|
||||
function goEndlessGamePage() {
|
||||
openGamePage('/tongji/endless-game/index')
|
||||
}
|
||||
|
||||
function navToUser() {
|
||||
uni.redirectTo({ url: '/pages/user/user' })
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 糖分突袭游戏入口 */
|
||||
/* 游戏入口 */
|
||||
.weekly-page .st-game-entry {
|
||||
|
||||
margin-top: 24rpx;
|
||||
@@ -388,8 +388,15 @@
|
||||
}
|
||||
|
||||
/* 识糖入口嵌在趋势卡内:轻量分隔,避免大块留白 */
|
||||
.weekly-page .st-chart-card .st-game-entry-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
margin: 12rpx 20rpx 0;
|
||||
}
|
||||
|
||||
.weekly-page .st-chart-card .st-game-entry--in-card {
|
||||
margin-top: 12rpx;
|
||||
margin-top: 0;
|
||||
padding: 16rpx 20rpx;
|
||||
gap: 16rpx;
|
||||
border-radius: 24rpx;
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
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/<mode>/<platform>
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
endlessGameFoodInlinePlugin(),
|
||||
uni(),
|
||||
Optimization({
|
||||
enable: {
|
||||
@@ -20,13 +17,6 @@ export default defineConfig({
|
||||
logger: false,
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
assetFileNames: endlessGameAssetFileNames,
|
||||
},
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
|
||||