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`:本功能包使用的图标组件。
|
- `components/TongjiIcon.vue`:本功能包使用的图标组件。
|
||||||
- `composables/useGameSfx.js`:滑动、掉落、消除、连击和大奖音效。
|
- `composables/useGameSfx.js`:滑动、掉落、消除、连击和大奖音效。
|
||||||
- `utils/svgDataUrl.js`:图标编码工具。
|
- `utils/svgDataUrl.js`:图标编码工具。
|
||||||
- `assets/food/`:本游戏使用的全部食品与驼乳粉图片。
|
|
||||||
- `viteAssetOutput.js`:把食品图输出到本功能分包,避免增加微信小程序主包体积。
|
|
||||||
|
|
||||||
## 迁移步骤
|
## 迁移步骤
|
||||||
|
|
||||||
1. 整体复制 `endless-game` 文件夹到目标项目的 `tongji/` 目录。
|
1. 整体复制 `endless-game` 文件夹到目标项目的 `tongji/` 目录。
|
||||||
2. 在目标项目的 `vite.config` 中引入素材输出规则,并放到 `build.rollupOptions.output`:
|
2. 在目标项目 `pages.json` 的 `tongji` 分包 `pages` 数组中加入:
|
||||||
|
|
||||||
```js
|
|
||||||
import { endlessGameAssetFileNames } from './tongji/endless-game/viteAssetOutput.js'
|
|
||||||
|
|
||||||
// defineConfig 内
|
|
||||||
build: {
|
|
||||||
rollupOptions: {
|
|
||||||
output: { assetFileNames: endlessGameAssetFileNames }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 在目标项目 `pages.json` 的 `tongji` 分包 `pages` 数组中加入:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -42,13 +27,13 @@ build: {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
4. 使用 `/tongji/endless-game/index` 打开游戏。
|
3. 使用 `/tongji/endless-game/index` 打开游戏。
|
||||||
5. 微信小程序后台需要允许音频域名:`https://gz-1349751149.cos.ap-guangzhou.myqcloud.com`。
|
4. 微信小程序后台需要将 `https://gz-1349751149.cos.ap-guangzhou.myqcloud.com` 配置为合法的音频与图片下载域名。
|
||||||
|
|
||||||
## 说明
|
## 说明
|
||||||
|
|
||||||
- 旧地址 `/tongji/pages/game` 和 `/tongji/pages/game-endless` 只是当前项目的兼容跳转,迁移时不需要复制。
|
- 当前项目另有 `/tongji/pages/game` 页面,不属于本独立功能包,迁移时无需复制。
|
||||||
- 当前项目原来的“糖分突袭”源码保存在 `tongji/legacy-game/game.vue`,没有注册为页面,不参与打包。
|
- 食品图片固定使用 `https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/games/food/` 前缀,文件名必须与 `FOODS` 配置 key 的大小写保持一致。
|
||||||
- 连消小目标按“连消×2”累计 2 次;连续 6 次普通消除没有连消时,下一次掉落会提供连消机会。“连消×3”保留为额外积分、驼乳粉与撒花惊喜,不阻挡主线任务。
|
- 连消小目标按“连消×2”累计 2 次;连续 6 次普通消除没有连消时,下一次掉落会提供连消机会。“连消×3”保留为额外积分、驼乳粉与撒花惊喜,不阻挡主线任务。
|
||||||
- 横向 3 个与竖向 3 个相交形成 L/T 形五消时,会在交点生成带高对比 L 标记的范围爆破棋子;该棋子再次被消除时清除周围九格。
|
- 横向 3 个与竖向 3 个相交形成 L/T 形五消时,会在交点生成带高对比 L 标记的范围爆破棋子;该棋子再次被消除时清除周围九格。
|
||||||
- 进入游戏时展示“本周 7 人同行榜”界面原型,包含前后名次、差距提示和可切换的同行/亲友鼓励;顶部“本周同行”可再次打开。当前榜单明确标注为预览数据,正式用户关系和周榜成绩需要后端接口接入。
|
- 进入游戏时展示“本周 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 不同可在同一素材上听出明显差异
|
* rate 不同可在同一素材上听出明显差异
|
||||||
*/
|
*/
|
||||||
const SFX = {
|
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 },
|
select: { src: SRC.tap, volume: 0.32, rate: 1.05, pool: 3 },
|
||||||
deselect: { src: SRC.tap, volume: 0.22, rate: 0.82, pool: 2 },
|
deselect: { src: SRC.tap, volume: 0.22, rate: 0.82, pool: 2 },
|
||||||
|
|||||||
@@ -20,14 +20,18 @@
|
|||||||
|
|
||||||
.eg-nav {
|
.eg-nav {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: grid;
|
||||||
|
box-sizing: border-box;
|
||||||
|
grid-template-columns: 156rpx minmax(0, 1fr) 156rpx;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
column-gap: 12rpx;
|
||||||
padding-right: 28rpx;
|
padding-right: 28rpx;
|
||||||
padding-bottom: 16rpx;
|
padding-bottom: 16rpx;
|
||||||
padding-left: 28rpx;
|
padding-left: 28rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.eg-nav-leading { display: flex; width: 156rpx; justify-content: flex-start; }
|
||||||
|
|
||||||
.eg-nav-btn {
|
.eg-nav-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 72rpx;
|
width: 72rpx;
|
||||||
@@ -39,10 +43,11 @@
|
|||||||
background: rgba(255,255,255,.82);
|
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-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-title { color: #155e4b; font-size: 40rpx; font-weight: 800; }
|
||||||
.eg-subtitle { margin-top: 2rpx; color: #648278; font-size: 24rpx; }
|
.eg-subtitle { margin-top: 2rpx; color: #648278; font-size: 24rpx; }
|
||||||
.eg-content { padding: 0 16rpx 48rpx; }
|
.eg-content { padding: 0 16rpx 48rpx; }
|
||||||
|
|||||||
@@ -13,9 +13,11 @@
|
|||||||
:style="piece.style"
|
:style="piece.style"
|
||||||
>{{ piece.emoji }}</text>
|
>{{ piece.emoji }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="eg-nav" :style="{ paddingTop: safeTop + 'px' }">
|
<view class="eg-nav" :style="navStyle">
|
||||||
<view class="eg-nav-btn" @tap="goBack">
|
<view class="eg-nav-leading">
|
||||||
<TongjiIcon name="chevron-left" size="md" color="#155e4b" />
|
<view class="eg-nav-btn" @tap="goBack">
|
||||||
|
<TongjiIcon name="chevron-left" size="md" color="#155e4b" />
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="eg-title-wrap">
|
<view class="eg-title-wrap">
|
||||||
<text class="eg-title">控糖消消乐</text>
|
<text class="eg-title">控糖消消乐</text>
|
||||||
@@ -297,13 +299,7 @@ import { onLoad, onUnload } from '@dcloudio/uni-app'
|
|||||||
import TongjiIcon from './components/TongjiIcon.vue'
|
import TongjiIcon from './components/TongjiIcon.vue'
|
||||||
import { useGameSfx } from './composables/useGameSfx.js'
|
import { useGameSfx } from './composables/useGameSfx.js'
|
||||||
|
|
||||||
const FOOD_IMAGE_MODULES = import.meta.glob('./assets/food/*.jpg', {
|
const FOOD_IMAGE_BASE_URL = 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/games/food/'
|
||||||
eager: true,
|
|
||||||
import: 'default'
|
|
||||||
})
|
|
||||||
const FOOD_IMAGES = Object.fromEntries(
|
|
||||||
Object.entries(FOOD_IMAGE_MODULES).map(([path, url]) => [path.split('/').pop().replace(/\.jpg$/, ''), url])
|
|
||||||
)
|
|
||||||
|
|
||||||
const ROWS = 5
|
const ROWS = 5
|
||||||
const COLS = 4
|
const COLS = 4
|
||||||
@@ -385,7 +381,7 @@ const FOODS = {
|
|||||||
milkTea: { name: '含糖奶茶', short: '奶茶', emoji: '🧋', color: 'orange', level: 'high', levelLabel: '高糖', high: true, replaceable: true, tip: '含糖奶茶可能同时含有较多添加糖和能量,应关注配方与饮用量。' },
|
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 }
|
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 = [
|
const FOOD_GROUPS = [
|
||||||
{ title: '中式早餐', safe: ['egg', 'cucumber', 'corn', 'grainMantou'], danger: 'congee' },
|
{ title: '中式早餐', safe: ['egg', 'cucumber', 'corn', 'grainMantou'], danger: 'congee' },
|
||||||
{ title: '家常粥餐', safe: ['celery', 'mushroom', 'chicken', 'taro'], danger: 'centuryCongee' },
|
{ title: '家常粥餐', safe: ['celery', 'mushroom', 'chicken', 'taro'], danger: 'centuryCongee' },
|
||||||
@@ -423,7 +419,7 @@ const comboDryTurns = ref(0)
|
|||||||
const taskRewardVisible = ref(false)
|
const taskRewardVisible = ref(false)
|
||||||
const taskRewardKey = ref(0)
|
const taskRewardKey = ref(0)
|
||||||
const statusText = ref('交换相邻食物,三个相同即可消除')
|
const statusText = ref('交换相邻食物,三个相同即可消除')
|
||||||
const safeTop = ref(20)
|
const navStyle = ref({ paddingTop: '20px' })
|
||||||
const touch = { x: 0, y: 0, index: -1 }
|
const touch = { x: 0, y: 0, index: -1 }
|
||||||
const dragIndex = ref(-1)
|
const dragIndex = ref(-1)
|
||||||
const dragTarget = ref(-1)
|
const dragTarget = ref(-1)
|
||||||
@@ -499,9 +495,36 @@ const weeklyDistance = computed(() => {
|
|||||||
const currentCheer = computed(() => CHEER_MESSAGES[cheerIndex.value % CHEER_MESSAGES.length])
|
const currentCheer = computed(() => CHEER_MESSAGES[cheerIndex.value % CHEER_MESSAGES.length])
|
||||||
const weeklyRangeLabel = computed(() => getWeekRangeLabel(new Date()))
|
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(() => {
|
onLoad(() => {
|
||||||
const info = uni.getSystemInfoSync()
|
initNavLayout()
|
||||||
safeTop.value = info.safeAreaInsets?.top || info.statusBarHeight || 20
|
|
||||||
bestScore.value = Number(uni.getStorageSync('tongji_endless_best') || 0)
|
bestScore.value = Number(uni.getStorageSync('tongji_endless_best') || 0)
|
||||||
startGame()
|
startGame()
|
||||||
weeklyRankVisible.value = true
|
weeklyRankVisible.value = true
|
||||||
@@ -592,7 +615,15 @@ function startGame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function goBack() {
|
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() {
|
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 { defineConfig } from 'vite'
|
||||||
import uni from '@dcloudio/vite-plugin-uni'
|
import uni from '@dcloudio/vite-plugin-uni'
|
||||||
import { endlessGameAssetFileNames } from './tongji/endless-game/viteAssetOutput.js'
|
|
||||||
|
|
||||||
// uni-app 工程根目录就是源码目录(HBuilderX 兼容)
|
// uni-app 工程根目录就是源码目录(HBuilderX 兼容)
|
||||||
// 编译产物默认输出到 ./dist/<mode>/<platform>
|
// 编译产物默认输出到 ./dist/<mode>/<platform>
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [uni()],
|
plugins: [uni()],
|
||||||
build: {
|
|
||||||
rollupOptions: {
|
|
||||||
output: {
|
|
||||||
assetFileNames: endlessGameAssetFileNames,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
scss: {
|
scss: {
|
||||||
|
|||||||