feat(tongji): 新增控糖消消乐小游戏
@@ -1,6 +1,6 @@
|
||||
# 控糖消消乐独立功能包
|
||||
|
||||
此目录包含“控糖消消乐”无尽三消版的全部运行文件,可以整体复制到另一个 uni-app Vue 3 项目的 `tongji/endless-game/` 下。
|
||||
此目录包含“控糖消消乐”无尽三消版的页面与运行代码,可以整体复制到另一个 uni-app Vue 3 项目的 `tongji/endless-game/` 下。食品图片由远端 COS 提供,不再存放于功能目录中。
|
||||
|
||||
## 目录内容
|
||||
|
||||
@@ -9,26 +9,11 @@
|
||||
- `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` 数组中加入:
|
||||
2. 在目标项目 `pages.json` 的 `tongji` 分包 `pages` 数组中加入:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -42,13 +27,13 @@ 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/pages/game` 和 `/tongji/pages/game-endless` 只是当前项目的兼容跳转,迁移时不需要复制。
|
||||
- 当前项目原来的“糖分突袭”源码保存在 `tongji/legacy-game/game.vue`,没有注册为页面,不参与打包。
|
||||
- 当前项目另有 `/tongji/pages/game` 页面,不属于本独立功能包,迁移时无需复制。
|
||||
- 食品图片固定使用 `https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/games/food/` 前缀,文件名必须与 `FOODS` 配置 key 的大小写保持一致。
|
||||
- 连消小目标按“连消×2”累计 2 次;连续 6 次普通消除没有连消时,下一次掉落会提供连消机会。“连消×3”保留为额外积分、驼乳粉与撒花惊喜,不阻挡主线任务。
|
||||
- 横向 3 个与竖向 3 个相交形成 L/T 形五消时,会在交点生成带高对比 L 标记的范围爆破棋子;该棋子再次被消除时清除周围九格。
|
||||
- 进入游戏时展示“本周 7 人同行榜”界面原型,包含前后名次、差距提示和可切换的同行/亲友鼓励;顶部“本周同行”可再次打开。当前榜单明确标注为预览数据,正式用户关系和周榜成绩需要后端接口接入。
|
||||
|
||||
|
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 |
@@ -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 },
|
||||
|
||||
@@ -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>
|
||||
@@ -297,13 +299,7 @@ import { onLoad, onUnload } from '@dcloudio/uni-app'
|
||||
import TongjiIcon from './components/TongjiIcon.vue'
|
||||
import { useGameSfx } from './composables/useGameSfx.js'
|
||||
|
||||
const FOOD_IMAGE_MODULES = import.meta.glob('./assets/food/*.jpg', {
|
||||
eager: true,
|
||||
import: 'default'
|
||||
})
|
||||
const FOOD_IMAGES = Object.fromEntries(
|
||||
Object.entries(FOOD_IMAGE_MODULES).map(([path, url]) => [path.split('/').pop().replace(/\.jpg$/, ''), url])
|
||||
)
|
||||
const FOOD_IMAGE_BASE_URL = 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/games/food/'
|
||||
|
||||
const ROWS = 5
|
||||
const COLS = 4
|
||||
@@ -385,7 +381,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' },
|
||||
@@ -423,7 +419,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)
|
||||
@@ -499,9 +495,36 @@ const weeklyDistance = computed(() => {
|
||||
const currentCheer = computed(() => CHEER_MESSAGES[cheerIndex.value % CHEER_MESSAGES.length])
|
||||
const weeklyRangeLabel = computed(() => getWeekRangeLabel(new Date()))
|
||||
|
||||
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(() => {
|
||||
const info = uni.getSystemInfoSync()
|
||||
safeTop.value = info.safeAreaInsets?.top || info.statusBarHeight || 20
|
||||
initNavLayout()
|
||||
bestScore.value = Number(uni.getStorageSync('tongji_endless_best') || 0)
|
||||
startGame()
|
||||
weeklyRankVisible.value = true
|
||||
@@ -592,7 +615,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,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]'
|
||||
}
|
||||
@@ -1,18 +1,10 @@
|
||||
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: {
|
||||
|
||||