diff --git a/TUICallKit-Vue3/manifest.json b/TUICallKit-Vue3/manifest.json
index 146f31f1..b479fb4f 100644
--- a/TUICallKit-Vue3/manifest.json
+++ b/TUICallKit-Vue3/manifest.json
@@ -52,14 +52,14 @@
"mp-weixin" : {
"appid" : "wx79b9a0bfbfe7cbcd",
"setting" : {
- "urlCheck" : false
+ "urlCheck" : false,
+ "minified" : true
},
"optimization" : {
"subPackages" : true
},
"usingComponents" : true,
- "requiredBackgroundModes" : ["audio"],
-
+ "requiredBackgroundModes" : [ "audio" ],
"plugins" : {
"WechatSI" : {
"version" : "0.3.5",
diff --git a/TUICallKit-Vue3/static/zs.jpg b/TUICallKit-Vue3/static/zs.jpg
index 795f696d..00b3336a 100644
Binary files a/TUICallKit-Vue3/static/zs.jpg and b/TUICallKit-Vue3/static/zs.jpg differ
diff --git a/TUICallKit-Vue3/tongji/pages/game.vue b/TUICallKit-Vue3/tongji/pages/game.vue
index c5bfc7eb..783d9e4e 100644
--- a/TUICallKit-Vue3/tongji/pages/game.vue
+++ b/TUICallKit-Vue3/tongji/pages/game.vue
@@ -923,6 +923,9 @@ function initBoard() {
commitBoard(next)
// 注入本关唯一高糖(解锁前固定 2 个,无法三连)
ensureHighPresence()
+ // 注入高糖可能破坏“有解”性:开局静默兜底,避免出现整盘无法消除的死局
+ // (仅打乱类型、保持高糖数量不变)
+ if (!boardHasSolution()) shuffleTypesUntilPlayable()
startHintTimer()
}
@@ -1423,6 +1426,38 @@ function startHintTimer() {
hintTimer = setTimeout(showOperationHint, HINT_DELAY_MS)
}
+/**
+ * 就地打乱棋盘上所有方块的“食物类型”(位置/对象不变),直到棋盘“有解”:
+ * 无现成消除组 + 至少存在一个可用移动。仅交换 type,故高糖数量等多重集不变。
+ * 不带任何 UI 反馈,供初始化等静默场景使用。返回是否成功。
+ */
+function shuffleTypesUntilPlayable(maxRetry = 80) {
+ const tiles = []
+ for (let r = 0; r < ROWS; r++) {
+ for (let c = 0; c < COLS; c++) {
+ const t = board.value[r]?.[c]
+ if (t && !t.isRemoved) tiles.push(t)
+ }
+ }
+ if (tiles.length < 3) return false
+ for (let attempt = 0; attempt < maxRetry; attempt++) {
+ for (let i = tiles.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [tiles[i].type, tiles[j].type] = [tiles[j].type, tiles[i].type]
+ }
+ if (findMatches().length === 0 && findPossibleMoves().length > 0) {
+ bumpLayout()
+ return true
+ }
+ }
+ return false
+}
+
+/** 当前棋盘是否“有解”(无现成消除组且至少有一个可用移动) */
+function boardHasSolution() {
+ return findMatches().length === 0 && findPossibleMoves().length > 0
+}
+
/**
* 棋盘死锁自动被测与修复。
* 使用 Fisher-Yates 打乱食物类型,最多重试 maxRetry 次。
@@ -1431,7 +1466,7 @@ function startHintTimer() {
async function reshuffleIfStuck() {
if (gamePhase.value !== 'playing') return
// 已有解(无现成消除组 + 存在可用移动)则无需重排
- if (findMatches().length === 0 && findPossibleMoves().length > 0) return
+ if (boardHasSolution()) return
// 给玩家反馈
playSfx('reshuffle', 0.75)
@@ -1444,32 +1479,8 @@ async function reshuffleIfStuck() {
setTimeout(() => { floatingVals.value = floatingVals.value.filter(v => v.id !== id) }, 1400)
} catch (_) {}
- // 收集棋盘上所有有效方块(保持 tile 对象与位置不变,仅打乱其食物类型)
- const tiles = []
- for (let r = 0; r < ROWS; r++) {
- for (let c = 0; c < COLS; c++) {
- const t = board.value[r]?.[c]
- if (t && !t.isRemoved) tiles.push(t)
- }
- }
-
- // 反复打乱类型并就地试写,直到“有解且无现成三连”;
- // tiles 已是 board.value 中的同一批对象,故 findMatches/findPossibleMoves 可直接复用
- const MAX_RETRY = 60
- let ok = false
- for (let attempt = 0; attempt < MAX_RETRY; attempt++) {
- for (let i = tiles.length - 1; i > 0; i--) {
- const j = Math.floor(Math.random() * (i + 1));
- [tiles[i].type, tiles[j].type] = [tiles[j].type, tiles[i].type]
- }
- if (findMatches().length === 0 && findPossibleMoves().length > 0) {
- ok = true
- break
- }
- }
-
- if (!ok) {
- // 就地洗牌无法洗出可玩布局,回退到全棋盘重新生成
+ // 就地打乱类型直到有解;失败则回退到全棋盘重新生成
+ if (!shuffleTypesUntilPlayable(60)) {
initBoard()
return
}
@@ -1754,10 +1765,8 @@ async function processSwap(t1, t2, opts = {}) {
isProcessing.value = false
return
}
- // 按解锁进度维持高糖数量(到第 7 步起补到可消除的 3 个)
- ensureHighPresence()
- // 所有连消结束后,检查棋盘是否死锁并自动重排
- await reshuffleIfStuck()
+ // 落子算法已保证“下落即有可消除走法”;此处仅作极端兜底(静默、无横幅)
+ if (!boardHasSolution()) shuffleTypesUntilPlayable()
} else {
await animateSwapOffset(t1, t2)
swapTiles(t1, t2)
@@ -1831,9 +1840,134 @@ async function clearAndRefill() {
return false
}
+/* ——— 落子前在 next 网格上做的辅助判定(不触动可见的 board.value) ——— */
+function gridIsHigh(grid, r, c) {
+ const t = grid[r]?.[c]
+ return t && !t.isRemoved && t.type.gi === 'high'
+}
+
+function gridWouldFormHighRun(grid, r, c) {
+ let h = 1
+ for (let cc = c - 1; cc >= 0 && gridIsHigh(grid, r, cc); cc--) h++
+ for (let cc = c + 1; cc < COLS && gridIsHigh(grid, r, cc); cc++) h++
+ if (h >= 3) return true
+ let v = 1
+ for (let rr = r - 1; rr >= 0 && gridIsHigh(grid, rr, c); rr--) v++
+ for (let rr = r + 1; rr < ROWS && gridIsHigh(grid, rr, c); rr++) v++
+ return v >= 3
+}
+
+/** 在 grid 上是否存在“现成可消除组” */
+function gridHasCurrentMatch(grid) {
+ const saved = board.value
+ board.value = grid
+ const has = findMatches().length > 0
+ board.value = saved
+ return has
+}
+
+/** 在 grid 上是否存在“可交换出消除的走法” */
+function gridHasMove(grid) {
+ const saved = board.value
+ board.value = grid
+ const ok = findPossibleMoves().length > 0
+ board.value = saved
+ return ok
+}
+
+/**
+ * 仅在 next 网格上调整本局高糖数量(只动“新生成的方块”,玩家尚未看到,故无可见突变):
+ * 解锁前固定 2 个、解锁后 3~HIGH_COUNT_MAX 个;注入时避免直接连成三连。
+ */
+function balanceHighInRefill(next, newTiles) {
+ const highFood = sessionHighFood.value
+ if (!highFood) return
+ const before = playerMoves.value < HIGH_UNLOCK_MOVES
+ const minTarget = before ? HIGH_COUNT_LOCKED : HIGH_COUNT_UNLOCKED
+ const maxCap = before ? HIGH_COUNT_LOCKED : HIGH_COUNT_MAX
+
+ let high = 0
+ for (let r = 0; r < ROWS; r++) {
+ for (let c = 0; c < COLS; c++) if (gridIsHigh(next, r, c)) high++
+ }
+ const lowMidPool = activeFoods.value.filter((f) => f.gi !== 'high')
+
+ if (high > maxCap) {
+ for (const t of shuffleInPlace([...newTiles])) {
+ if (high <= maxCap) break
+ if (t.type.gi !== 'high') continue
+ const repl = lowMidPool.length ? lowMidPool[Math.floor(Math.random() * lowMidPool.length)] : null
+ if (repl) { t.type = cloneFoodType(repl); high-- }
+ }
+ }
+
+ if (high < minTarget) {
+ const cands = shuffleInPlace([...newTiles])
+ for (const t of cands) {
+ if (high >= minTarget) break
+ if (t.type.gi === 'high') continue
+ if (gridWouldFormHighRun(next, t.r, t.c)) continue
+ t.type = cloneFoodType(highFood)
+ high++
+ }
+ for (const t of cands) {
+ if (high >= minTarget) break
+ if (t.type.gi === 'high') continue
+ t.type = cloneFoodType(highFood)
+ high++
+ }
+ }
+}
+
+/**
+ * 保证“落下后必有可消除的走法”:只重 roll 新方块里的非高糖类型(落子前完成,无可见重排)。
+ * 若 next 已有现成消除组(即将触发连锁),则交给连锁处理,最终落子时再保证。
+ */
+function ensureRefillPlayable(next, newTiles) {
+ if (gridHasCurrentMatch(next)) return
+ if (gridHasMove(next)) return
+
+ const nonHighPool = activeFoods.value.filter((f) => f.gi !== 'high')
+ const rerollable = newTiles.filter((t) => t.type.gi !== 'high')
+ if (nonHighPool.length && rerollable.length) {
+ for (let attempt = 0; attempt < 100; attempt++) {
+ rerollable.forEach((t) => {
+ t.type = cloneFoodType(nonHighPool[Math.floor(Math.random() * nonHighPool.length)])
+ })
+ if (gridHasMove(next)) return
+ }
+ }
+
+ // 极端兜底:静默打乱整盘类型(保持多重集/高糖数量),无任何横幅提示
+ silentShuffleGrid(next)
+}
+
+/** 在 next 网格上静默打乱类型直至“无现成消除组且有可用走法”,仅作极端兜底 */
+function silentShuffleGrid(next) {
+ const tiles = []
+ for (let r = 0; r < ROWS; r++) {
+ for (let c = 0; c < COLS; c++) {
+ const t = next[r]?.[c]
+ if (t && !t.isRemoved) tiles.push(t)
+ }
+ }
+ if (tiles.length < 3) return
+ const saved = board.value
+ board.value = next
+ for (let attempt = 0; attempt < 80; attempt++) {
+ for (let i = tiles.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [tiles[i].type, tiles[j].type] = [tiles[j].type, tiles[i].type]
+ }
+ if (findMatches().length === 0 && findPossibleMoves().length > 0) break
+ }
+ board.value = saved
+}
+
async function applyGravity() {
const next = Array.from({ length: ROWS }, () => Array(COLS).fill(null))
const dropTiles = []
+ const newTiles = []
for (let c = 0; c < COLS; c++) {
const surviving = []
@@ -1861,10 +1995,15 @@ async function applyGravity() {
tile.isDropAnimate = false
next[r][c] = tile
dropTiles.push(tile)
+ newTiles.push(tile)
}
}
ensureBoardFull(next)
+ // 落子前只调整“新方块”:①按解锁进度维持高糖数量 ②保证落下后必有可消除走法
+ // 这样棋盘随下落即更新,无需“无解时整盘自动重排”。
+ balanceHighInRefill(next, newTiles)
+ ensureRefillPlayable(next, newTiles)
commitBoard(next)
await nextTick()
diff --git a/TUICallKit-Vue3/tongji/pages/weekly.vue b/TUICallKit-Vue3/tongji/pages/weekly.vue
index b92012a8..353204ab 100644
--- a/TUICallKit-Vue3/tongji/pages/weekly.vue
+++ b/TUICallKit-Vue3/tongji/pages/weekly.vue
@@ -193,19 +193,26 @@
×
-
+
-
dietAiAskFocused.value || String(dietAiAskText.value || '').length > 0
+)
+
/** 键盘高度(px):录入弹窗据此整体上移,避免输入框被键盘遮挡 */
const keyboardHeight = ref(0)
function onKeyboardHeightChange(res) {
diff --git a/TUICallKit-Vue3/tongji/styles/weekly-stitch.scss b/TUICallKit-Vue3/tongji/styles/weekly-stitch.scss
index c7d1160d..f22f33ff 100644
--- a/TUICallKit-Vue3/tongji/styles/weekly-stitch.scss
+++ b/TUICallKit-Vue3/tongji/styles/weekly-stitch.scss
@@ -740,13 +740,22 @@
.weekly-page .st-float-inner {
display: flex;
align-items: center;
- gap: 24rpx;
- padding: 16rpx 16rpx 16rpx 40rpx;
+ gap: 16rpx;
+ padding: 16rpx 16rpx 16rpx 28rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(20rpx);
border: 1rpx solid rgba(0, 108, 73, 0.1);
box-shadow: var(--st-shadow-float);
+ transition: border-radius 0.22s ease, padding 0.22s ease, border-color 0.22s ease;
+}
+
+/* 聚焦/有内容时展开:底部对齐让输入框向上增高,圆角收敛为圆角矩形 */
+.weekly-page .st-float-inner.is-expanded {
+ align-items: flex-end;
+ border-radius: 32rpx;
+ padding: 18rpx 16rpx 18rpx 32rpx;
+ border-color: rgba(0, 108, 73, 0.28);
}
.weekly-page .st-float-icon {
@@ -763,15 +772,30 @@
.weekly-page .st-float-input {
flex: 1;
min-width: 0;
- height: 64rpx;
+ width: 100%;
+ min-height: 64rpx;
+ max-height: 240rpx;
font-size: 28rpx;
+ line-height: 40rpx;
color: var(--on-surface);
background: transparent;
+ padding: 12rpx 0;
+ box-sizing: border-box;
+ transition: min-height 0.2s ease;
+}
+
+/* 聚焦时输入框变大;内容超长时由 auto-height 继续向上增高,超过上限可滚动 */
+.weekly-page .st-float-inner.is-expanded .st-float-input {
+ min-height: 104rpx;
}
.weekly-page .st-float-placeholder {
color: rgba(60, 74, 66, 0.6);
- font-size: 28rpx;
+ font-size: 26rpx;
+ line-height: 40rpx;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.weekly-page .st-float-mic {