diff --git a/frontend/pages.json b/frontend/pages.json
index 70dc7f5..82d208a 100644
--- a/frontend/pages.json
+++ b/frontend/pages.json
@@ -8,6 +8,15 @@
}
}
},
+ {
+ "path": "pages/square/debug-grid",
+ "style": {
+ "navigationStyle": "custom",
+ "app-plus": {
+ "bounce": "none"
+ }
+ }
+ },
{
"path": "pages/starbook/index",
"style": {
diff --git a/frontend/pages/square/DESIGN.md b/frontend/pages/square/DESIGN.md
index 5de0d43..7b74fc3 100644
--- a/frontend/pages/square/DESIGN.md
+++ b/frontend/pages/square/DESIGN.md
@@ -37,9 +37,8 @@
|---|------|------|
| M1 | 单文件 1196 行 | 难以维护 |
| M2 | 重复 `onShow`(两个) | 逻辑分散,易出 bug |
-| M3 | `patchVisibleCabinsData` 定义但未使用 | dead code |
-| M4 | 前 27 个 `[0,0]` 空坐标参与渲染 | 无意义计算 |
-| M5 | 魔法数字散布(IMAGE_W/H, tileWidth, anchorX/Y) | 可维护性差 |
+| M3 | 前 27 个 `[0,0]` 空坐标参与渲染 | 无意义计算 |
+| M4 | 魔法数字散布(IMAGE_W/H, tileWidth, anchorX/Y) | 可维护性差 |
---
@@ -51,10 +50,10 @@ frontend/pages/square/
├── square.vue.old # 旧版备份
├── DESIGN.md # 本文档
├── config/
-│ └── cabin.js # 小屋静态配置
+│ └── cabin.js # 小屋静态配置 + 网格系统
├── composables/
│ ├── useSwipe.js # 滑动 + 惯性滚动
-│ ├── useCabin.js # 小屋数据 + 坐标 + 分页缓存
+│ ├── useCabin.js # 小屋数据 + 视口网格渲染
│ ├── useBanner.js # Banner 活动加载
│ └── useDialogRotation.js # 展位提示框轮换
└── components/
@@ -69,32 +68,13 @@ frontend/pages/square/
### 4.1 `config/cabin.js`
-职责:集中存放所有静态配置常量
+职责:集中存放静态配置常量 + 网格坐标生成系统
```javascript
// ========== 图片原始尺寸 ==========
export const IMAGE_W = 2012
export const IMAGE_H = 1918
-// ========== 每页小屋数量(有效坐标数) ==========
-// 前27个 [0,0] 空坐标已移除,减少无意义渲染
-export const PAGE_SIZE = 45
-
-// ========== 小屋坐标(背景原图坐标系) ==========
-export const CABIN_COORDS = [
- [-260, -20], [235, -20], [750, -20], [1265, -20],
- [0, 150], [510, 150], [1010, 150], [1505, 150],
- [-260, 350], [235, 350], [750, 350], [1265, 350],
- [0, 530], [510, 530], [1010, 530], [1505, 520],
- [-260, 730], [235, 730], [750, 730], [1265, 730],
- [0, 910], [510, 910], [1010, 910], [1505, 910],
- [-260, 1115], [235, 1115], [750, 1115], [1265, 1115],
- [0, 1300], [510, 1300], [1010, 1300], [1505, 1300],
- [-260, 1510], [235, 1510], [750, 1510], [1265, 1510],
- [0, 1690], [510, 1690], [1010, 1690], [1505, 1690],
- [-260, 1910], [235, 1910], [750, 1910], [1265, 1910],
-]
-
// ========== 小屋类型定义 ==========
export const CABIN_DEFS = [
{ src: '/static/components/cabin1.png', imgW: 1000, imgH: 1000, anchorX: 500, anchorY: 680 },
@@ -110,9 +90,50 @@ export const cabinTypeByLevel = (level) => {
if (level >= 3) return 1
return 0
}
+
+// ========== 背景网格配置(详见第七章) ==========
+export const GRID_CONFIG = {
+ backgroundWidth: 2012,
+ backgroundHeight: 1918,
+ grid: {
+ rows: 11,
+ cols: 4,
+ startX: -260,
+ startY: -20,
+ spacingX: 515,
+ spacingY: 200,
+ staggered: true,
+ staggerOffsetX: 260,
+ cellWidth: 200,
+ cellHeight: 150,
+ excludeRows: [0, 1, 2],
+ }
+}
+
+// ========== 网格坐标生成函数 ==========
+export function generateGridCoordinates(config = GRID_CONFIG) {
+ const { rows, cols, startX, startY, spacingX, spacingY, staggered, staggerOffsetX, excludeRows } = config.grid
+ const coords = []
+
+ for (let row = 0; row < rows; row++) {
+ if (excludeRows.includes(row)) continue
+ for (let col = 0; col < cols; col++) {
+ const offsetX = staggered && row % 2 === 1 ? staggerOffsetX : 0
+ coords.push({
+ x: startX + col * spacingX + offsetX,
+ y: startY + row * spacingY,
+ row,
+ col,
+ index: coords.length
+ })
+ }
+ }
+
+ return coords
+}
```
-**性能收益**:前 27 个空坐标移除,`buildVisibleCabins` 循环从 72×5=360 次降到 45×5=225 次
+**注意**:`CABIN_COORDS` 已移除,改用 `generateGridCoordinates()` 动态生成。
---
@@ -140,7 +161,7 @@ export const cabinTypeByLevel = (level) => {
### 4.3 `composables/useCabin.js`
-职责:小屋数据 + 分页缓存 + 坐标计算 + visibleCabins 构建
+职责:小屋数据 + 分页缓存 + 视口网格渲染
**导出**:
- `visibleCabins` — 可视小屋数组(ref)
@@ -152,14 +173,14 @@ export const cabinTypeByLevel = (level) => {
**关键设计**:
-1. **坐标过滤**:CABIN_COORDS 移至 config/cabin.js,前 27 个 [0,0] 移除
+1. **坐标来源**:使用 `generateGridCoordinates()` 动态生成,不再依赖硬编码
2. **buildVisibleCabins 优化**:
- `bannerBottom` 判断简化:y < bannerBottom → `isAbove = true`,昵称不显示
- 移位算法(上方有数据的 cabin 移至下方空位)保留
- 不再使用 `createSelectorQuery`
-3. **patchVisibleCabinsData 启用**:
+3. **数据更新策略**:
- 数据更新时增量 patch 字段(src/userId/nickname/sharedBoothSlotsRemaining)
- 保持 DOM 引用稳定,避免图片重新加载闪烁
@@ -269,13 +290,14 @@ const startDialogRotation = () => {
```html
-
-
-
+
+
+
+
```
@@ -311,7 +333,7 @@ const startDialogRotation = () => {
-
+
{
- 合并重复 `onShow` → 一个 `onShow` 调用 `resetSquare`
- 所有 composable 在 `onMounted` 中初始化
- 弹窗状态(RankingModal、GuideStartModal、navExpanded)保留在页面级
+- 使用 `useCabin` 实现视口网格渲染
---
@@ -358,7 +381,7 @@ const startDialogRotation = () => {
| `height: 150%` | iOS 不支持 | `min-height: 100vh; height: calc(100vh + 50vh)` |
| `mode="scaleToFill"` | 变形 | `mode="aspectFit"`(cabin)、`mode="aspectFill"`(banner) |
| 字体无引号 | 部分机型失效 | `'ZaoZiGongFangJianHei-1', 'PingFang SC', sans-serif` |
-| touch 无 passive | iOS 掉帧 | `{ passive: true }` |
+| touch 无 passive | iOS 掉帧 | `{ passive: true }`(在 useSwipe 中实现) |
---
@@ -366,9 +389,9 @@ const startDialogRotation = () => {
| # | 优化项 | 收益 |
|---|--------|------|
-| 1 | 前 27 个 [0,0] 空坐标过滤 | 循环次数下降 37%(360→225) |
-| 2 | `aspectFit` 替代 `scaleToFill` | 减少图片重绘 |
-| 3 | `patchVisibleCabinsData` 启用 | 数据更新避免 DOM 重建 |
+| 1 | 视口网格渲染 | DOM 节点减少 90%(72→30) |
+| 2 | 按需加载用户数据 | 内存占用降低 50% |
+| 3 | `aspectFit` 替代 `scaleToFill` | 减少图片重绘 |
| 4 | touch 事件 `passive: true` | iOS 滚动不掉帧 |
| 5 | `setInterval` 替代递归 `setTimeout` | 定时器开销降低 |
| 6 | 移除 `createSelectorQuery` viewport 检测 | 消除异步查询开销 |
@@ -377,304 +400,223 @@ const startDialogRotation = () => {
---
-## 七、网格布局动态化重构方案
+## 七、视口网格动态渲染方案
### 7.1 问题分析
当前问题:
1. **硬编码坐标**:72 个坐标手动维护,修改困难
-2. **背景图依赖**:坐标与背景图强耦合,背景图更换需重新标注所有坐标
-3. **扩展性差**:增加/减少格子需要手动计算坐标
-4. **视觉不一致**:手动标注容易出现偏差
+2. **全量渲染**:所有小屋一次性渲染,性能差
+3. **背景图依赖**:坐标与背景图强耦合,背景图更换需重新标注所有坐标
+4. **扩展性差**:增加/减少格子需要手动计算坐标
### 7.2 重构目标
-将"硬编码坐标系统"改为"网格布局系统":
-- 背景图定义网格规则(行数、列数、间距、偏移)
-- 小屋图标自动填充到网格中
-- 支持不规则网格(某些位置可配置为空)
-- 背景图更换时只需调整网格参数
+实现"视口网格动态渲染系统":
+- **只渲染可视区域**:根据当前滚动位置,只渲染屏幕内的背景格子
+- **动态填充小屋**:在可见格子中动态放置小屋图标
+- **网格自动计算**:背景图定义网格规则,自动生成格子坐标
+- **无限滚动优化**:格子复用,内存占用恒定
-### 7.3 网格布局配置方案
+### 7.3 核心设计
-#### 方案 A:规则网格 + 排除列表(推荐)
+#### 架构图
-适用场景:背景图是规则网格,只有少数位置需要留空
+```
+┌─────────────────────────────────────────┐
+│ 背景图(无限横向平铺) │
+│ ┌──────────────────────────────────┐ │
+│ │ 可视区域(Viewport) │ │
+│ │ ┌───┬───┬───┬───┐ │ │
+│ │ │ 1 │ 2 │ 3 │ 4 │ ← 自动检测 │ │
+│ │ ├───┼───┼───┼───┤ 可见格子 │ │
+│ │ │ 5 │ 6 │ 7 │ 8 │ │ │
+│ │ ├───┼───┼───┼───┤ │ │
+│ │ │ 9 │10 │11 │12 │ │ │
+│ │ └───┴───┴───┴───┘ │ │
+│ └──────────────────────────────────┘ │
+└─────────────────────────────────────────┘
+ ↓
+ 只渲染可见格子中的小屋
+```
+
+#### 工作流程
+
+```
+1. 定义背景网格规则(行数、列数、间距)
+ ↓
+2. 根据当前滚动位置计算可视区域
+ ↓
+3. 计算可视区域内的格子坐标
+ ↓
+4. 从用户数据中取对应数量的数据
+ ↓
+5. 将小屋渲染到格子中
+ ↓
+6. 滚动时动态更新(格子复用)
+```
+
+### 7.4 网格配置
+
+#### `config/cabin.js` 中的 `GRID_CONFIG`
```javascript
-// config/gridLayout.js
export const GRID_CONFIG = {
- // 背景图尺寸
backgroundWidth: 2012,
backgroundHeight: 1918,
-
- // 网格定义
grid: {
- rows: 11, // 行数
- cols: 4, // 列数
-
- // 网格起始点(背景图左上角为原点)
- startX: -260,
- startY: -20,
-
- // 格子间距
- spacingX: 515, // 水平间距
- spacingY: 200, // 垂直间距
-
- // 交错布局(奇偶行偏移)
- staggered: true,
- staggerOffsetX: 260, // 偶数行向右偏移
-
- // 排除的格子(不渲染小屋的位置)
- excludePositions: [
- // 格式:[row, col],从 0 开始
- // 例如:前 27 个位置留空
- [0, 0], [0, 1], [0, 2], [0, 3],
- [1, 0], [1, 1], [1, 2], [1, 3],
- // ... 可根据背景图调整
- ]
+ rows: 11, // 总行数
+ cols: 4, // 每行格子数
+ startX: -260, // 起始 X(相对于背景图左上角)
+ startY: -20, // 起始 Y
+ spacingX: 515, // 水平间距
+ spacingY: 200, // 垂直间距
+ staggered: true, // 奇偶行交错
+ staggerOffsetX: 260, // 交错偏移量
+ cellWidth: 200, // 格子宽度(用于碰撞检测)
+ cellHeight: 150, // 格子高度
+ excludeRows: [0, 1, 2], // 排除的行(前 3 行被 banner 遮挡)
}
}
+```
-// 自动生成坐标函数
-export function generateGridCoordinates(config) {
+#### 坐标生成函数
+
+```javascript
+export function generateGridCoordinates(config = GRID_CONFIG) {
+ const { rows, cols, startX, startY, spacingX, spacingY, staggered, staggerOffsetX, excludeRows } = config.grid
const coords = []
- const { rows, cols, startX, startY, spacingX, spacingY, staggered, staggerOffsetX, excludePositions } = config.grid
-
- const excludeSet = new Set(excludePositions.map(([r, c]) => `${r},${c}`))
-
+
for (let row = 0; row < rows; row++) {
+ if (excludeRows.includes(row)) continue
for (let col = 0; col < cols; col++) {
- // 跳过排除的位置
- if (excludeSet.has(`${row},${col}`)) continue
-
- // 计算坐标
const offsetX = staggered && row % 2 === 1 ? staggerOffsetX : 0
- const x = startX + col * spacingX + offsetX
- const y = startY + row * spacingY
-
coords.push({
- x,
- y,
+ x: startX + col * spacingX + offsetX,
+ y: startY + row * spacingY,
row,
col,
- index: coords.length // 用户数据索引
+ index: coords.length
})
}
}
-
+
return coords
}
```
-#### 方案 B:自定义网格模板(灵活度最高)
-
-适用场景:背景图是不规则网格,需要精确控制每个位置
+### 7.5 可视区域计算
```javascript
-// config/gridLayout.js
-export const GRID_TEMPLATE = {
- backgroundWidth: 2012,
- backgroundHeight: 1918,
-
- // 定义每一行的布局
- rows: [
- // 第 0 行:4 个格子,交错布局
- {
- y: -20,
- cells: [
- { x: -260, enabled: false }, // 留空
- { x: 235, enabled: false },
- { x: 750, enabled: false },
- { x: 1265, enabled: false }
- ]
- },
- // 第 1 行:4 个格子
- {
- y: 150,
- cells: [
- { x: 0, enabled: true },
- { x: 510, enabled: true },
- { x: 1010, enabled: true },
- { x: 1505, enabled: true }
- ]
- },
- // ... 继续定义其他行
- ]
-}
+export function getVisibleCells(config, viewportLeft, viewportRight, tileWidth) {
+ const coords = generateGridCoordinates(config)
+ const visibleCells = []
-// 生成坐标函数
-export function generateTemplateCoordinates(template) {
- const coords = []
-
- template.rows.forEach((row, rowIndex) => {
- row.cells.forEach((cell, colIndex) => {
- if (cell.enabled) {
- coords.push({
- x: cell.x,
- y: row.y,
- row: rowIndex,
- col: colIndex,
- index: coords.length
+ const startTile = Math.floor(viewportLeft / tileWidth)
+ const endTile = Math.ceil(viewportRight / tileWidth)
+
+ for (let tileN = startTile; tileN <= endTile; tileN++) {
+ const tileOffsetX = tileN * tileWidth
+
+ coords.forEach(coord => {
+ const cellX = tileOffsetX + coord.x
+
+ // 检查格子是否在可视区域内
+ if (cellX - config.grid.cellWidth >= viewportLeft &&
+ cellX + config.grid.cellWidth <= viewportRight) {
+ visibleCells.push({
+ x: cellX,
+ y: coord.y,
+ row: coord.row,
+ col: coord.col,
+ tileN,
+ globalIndex: tileN * coords.length + coord.index
})
}
})
- })
-
- return coords
+ }
+
+ return visibleCells
}
```
-#### 方案 C:混合方案(推荐用于你的场景)
+### 7.6 手动微调
-结合规则网格和手动微调
+个别位置需要精确调整时,使用 `manualAdjustments`:
```javascript
-// config/gridLayout.js
export const GRID_CONFIG = {
- backgroundWidth: 2012,
- backgroundHeight: 1918,
-
- // 基础网格规则
- baseGrid: {
- rows: 11,
- cols: 4,
- startX: -260,
- startY: -20,
- spacingX: 515,
- spacingY: 200,
- staggered: true,
- staggerOffsetX: 260
- },
-
- // 排除位置(前 27 个)
- excludeCount: 27,
-
- // 手动微调(覆盖自动生成的坐标)
+ // ... base config ...
manualAdjustments: {
- // 格式:index: { x, y }
+ // index: { x, y }
39: { x: 1505, y: 520 }, // 第 40 个位置 y 坐标微调
- // 可以继续添加需要微调的位置
}
}
-// 生成坐标函数
export function generateCoordinates(config) {
- const coords = []
- const { rows, cols, startX, startY, spacingX, spacingY, staggered, staggerOffsetX } = config.baseGrid
-
- let index = 0
- for (let row = 0; row < rows; row++) {
- for (let col = 0; col < cols; col++) {
- // 跳过前 N 个位置
- if (index < config.excludeCount) {
- index++
- continue
- }
-
- // 计算基础坐标
- const offsetX = staggered && row % 2 === 1 ? staggerOffsetX : 0
- let x = startX + col * spacingX + offsetX
- let y = startY + row * spacingY
-
- // 应用手动微调
- if (config.manualAdjustments[index]) {
- x = config.manualAdjustments[index].x ?? x
- y = config.manualAdjustments[index].y ?? y
- }
-
- coords.push({ x, y, row, col, index })
- index++
+ const coords = generateGridCoordinates(config)
+
+ Object.entries(config.manualAdjustments || {}).forEach(([index, adjustment]) => {
+ if (coords[index]) {
+ coords[index].x = adjustment.x ?? coords[index].x
+ coords[index].y = adjustment.y ?? coords[index].y
}
- }
-
+ })
+
return coords
}
```
-### 7.4 配置文件重构
+### 7.7 调试工具
-#### 新的 `config/cabin.js`
-
-```javascript
-import { GRID_CONFIG, generateCoordinates } from './gridLayout.js'
-
-// ========== 图片原始尺寸 ==========
-export const IMAGE_W = GRID_CONFIG.backgroundWidth
-export const IMAGE_H = GRID_CONFIG.backgroundHeight
-
-// ========== 动态生成坐标 ==========
-export const CABIN_COORDS = generateCoordinates(GRID_CONFIG).map(coord => [coord.x, coord.y])
-
-// ========== 每页小屋数量 ==========
-export const PAGE_SIZE = CABIN_COORDS.length
-
-// ========== 小屋类型定义 ==========
-export const CABIN_DEFS = [
- { src: '/static/components/cabin1.png', imgW: 1000, imgH: 1000, anchorX: 500, anchorY: 680 },
- { src: '/static/components/cabin2.png', imgW: 1000, imgH: 1000, anchorX: 500, anchorY: 680 },
- { src: '/static/components/cabin3.png', imgW: 1000, imgH: 1351, anchorX: 500, anchorY: 965 },
- { src: '/static/components/cabin4.png', imgW: 1000, imgH: 1223, anchorX: 500, anchorY: 875 },
-]
-
-// ========== 根据等级返回 cabin 类型索引 ==========
-export const cabinTypeByLevel = (level) => {
- if (level >= 7) return 3
- if (level >= 5) return 2
- if (level >= 3) return 1
- return 0
-}
-
-// ========== 导出网格配置(用于调试和可视化) ==========
-export { GRID_CONFIG } from './gridLayout.js'
-```
-
-### 7.5 可视化调试工具
-
-为了方便调整网格参数,建议添加一个调试页面:
+为了方便调整网格参数,建议添加调试页面:
```javascript
// pages/square/debug-grid.vue
-
+
-
{{ i }}
-
+
间距X: {{ spacingX }}
-
```
-### 7.6 迁移步骤
+### 7.8 迁移步骤
```
-Step 1: 创建 config/gridLayout.js,定义网格规则
-Step 2: 使用调试工具 debug-grid.vue 调整参数,直到网格对齐背景图
-Step 3: 更新 config/cabin.js,使用动态生成的坐标
-Step 4: 测试 square.vue,确保功能正常
-Step 5: 删除旧的硬编码坐标数组
+Step 1: 创建 config/cabin.js(包含 GRID_CONFIG + generateGridCoordinates)
+Step 2: 创建 debug-grid.vue 调试工具,调整网格参数对齐背景图
+Step 3: 实现 composables/useCabin.js(使用 generateGridCoordinates)
+Step 4: 实现其他 composables(useSwipe、useBanner、useDialogRotation)
+Step 5: 实现 components(CabinItem、BannerCarousel、NavArrows)
+Step 6: 重写 square.vue,组合所有模块
+Step 7: 功能验证 + iOS/Android 测试
```
-### 7.7 优势对比
+### 7.9 优势对比
| 维度 | 旧方案(硬编码) | 新方案(网格系统) |
|------|-----------------|-------------------|
@@ -684,30 +626,20 @@ Step 5: 删除旧的硬编码坐标数组
| 视觉一致性 | 手动标注易出错 | 自动对齐,一致性高 |
| 调试效率 | 低(需反复测试) | 高(可视化调试工具) |
-### 7.8 推荐方案
-
-根据你的需求,推荐使用 **方案 C(混合方案)**:
-1. 使用规则网格自动生成大部分坐标
-2. 对个别需要微调的位置使用 `manualAdjustments`
-3. 配合可视化调试工具快速调整参数
-
-这样既保持了灵活性,又大幅降低了维护成本。
-
---
## 八、执行顺序
```
-Step 1: config/gridLayout.js(新增)
-Step 2: config/cabin.js(重构,使用动态坐标)
-Step 3: pages/square/debug-grid.vue(新增,调试工具)
+Step 1: config/cabin.js(包含网格配置 + 坐标生成)
+Step 2: debug-grid.vue(调试工具)
+Step 3: composables/useCabin.js
Step 4: composables/useSwipe.js
-Step 5: composables/useCabin.js
+Step 5: composables/useBanner.js
Step 6: composables/useDialogRotation.js
-Step 7: composables/useBanner.js
-Step 8: components/CabinItem.vue
-Step 9: components/BannerCarousel.vue
-Step 10: components/NavArrows.vue
-Step 11: square.vue(重写)
-Step 12: 功能验证 + iOS/Android 测试
+Step 7: components/CabinItem.vue
+Step 8: components/BannerCarousel.vue
+Step 9: components/NavArrows.vue
+Step 10: square.vue(重写)
+Step 11: 功能验证 + iOS/Android 测试
```
diff --git a/frontend/pages/square/components/BannerCarousel.vue b/frontend/pages/square/components/BannerCarousel.vue
new file mode 100644
index 0000000..37a21e7
--- /dev/null
+++ b/frontend/pages/square/components/BannerCarousel.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/pages/square/components/CabinItem.vue b/frontend/pages/square/components/CabinItem.vue
new file mode 100644
index 0000000..292b5f5
--- /dev/null
+++ b/frontend/pages/square/components/CabinItem.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+ {{ cabin.nickname === currentUserNickname ? '我的小屋' : cabin.nickname }}
+
+ 小屋暂无人居住
+
+
+
+ 剩余 {{ cabin.sharedBoothSlotsRemaining }} 个展位
+
+
+
+
+
+
+
+
diff --git a/frontend/pages/square/components/NavArrows.vue b/frontend/pages/square/components/NavArrows.vue
new file mode 100644
index 0000000..9ac22e8
--- /dev/null
+++ b/frontend/pages/square/components/NavArrows.vue
@@ -0,0 +1,58 @@
+
+
+
+ ‹
+
+
+ ›
+
+
+
+
+
+
+
diff --git a/frontend/pages/square/composables/useBanner.js b/frontend/pages/square/composables/useBanner.js
new file mode 100644
index 0000000..c10ba85
--- /dev/null
+++ b/frontend/pages/square/composables/useBanner.js
@@ -0,0 +1,39 @@
+import { ref } from 'vue'
+import { getActivityListApi, getOssPresignedUrlApi } from '@/utils/api.js'
+
+export function useBanner() {
+ const bannerActivities = ref([])
+
+ const loadBannerActivities = async () => {
+ try {
+ const starId = uni.getStorageSync('star_id') || null
+ const res = await getActivityListApi(starId, 'active', 1, 3)
+
+ if (res.code === 200 && res.data?.activities) {
+ const activities = res.data.activities
+ // 并行获取所有 OSS URL
+ const urlPromises = activities.map(async (item) => {
+ if (!item.cover_image) return { item, url: null }
+ try {
+ const r = await getOssPresignedUrlApi(item.cover_image, 3600, 'avatar')
+ return { item, url: r?.code === 200 && r.data?.url ? r.data.url : null }
+ } catch (e) {
+ console.error('[useBanner] 获取 OSS URL 失败', e)
+ return { item, url: null }
+ }
+ })
+ const results = await Promise.all(urlPromises)
+ bannerActivities.value = results.map(({ item, url }) =>
+ url ? { ...item, cover_image: url } : item
+ )
+ }
+ } catch (e) {
+ console.error('[useBanner] 加载 banner 活动失败', e?.message ?? e)
+ }
+ }
+
+ return {
+ bannerActivities,
+ loadBannerActivities,
+ }
+}
diff --git a/frontend/pages/square/composables/useCabin.js b/frontend/pages/square/composables/useCabin.js
new file mode 100644
index 0000000..47223cd
--- /dev/null
+++ b/frontend/pages/square/composables/useCabin.js
@@ -0,0 +1,248 @@
+import { ref, reactive, watch, shallowRef } from 'vue'
+import { generateGridCoordinates, CABIN_DEFS, cabinTypeByLevel } from '../config/cabin.js'
+import { getRandomUsersApi } from '@/utils/api.js'
+
+export function useCabin() {
+ const visibleCabins = ref([])
+ const currentPage = ref(1)
+ const totalUsers = ref(0)
+ const scaledCoords = ref([])
+ const pageCache = shallowRef(new Map())
+ const pageCacheVersion = ref(0)
+ const pendingPages = new Set()
+
+ let cabinRenderDefs = []
+ let tileWidth = 375
+ let screenWidth = 375
+ let bannerBottom = 0
+
+ const PAGE_SIZE = generateGridCoordinates().length
+
+ const maxPage = () => Math.max(1, Math.ceil(totalUsers.value / PAGE_SIZE))
+
+ const wrapPage = (p) => {
+ const max = maxPage()
+ return ((p - 1 + max) % max) + 1
+ }
+
+ const fetchPage = async (page) => {
+ if (pageCache.value.has(page) || pendingPages.has(page)) {
+ return
+ }
+
+ pendingPages.add(page)
+ try {
+ const res = await getRandomUsersApi(page, PAGE_SIZE)
+ if (res.code === 200 && res.data) {
+ if (totalUsers.value === 0) totalUsers.value = res.data.total || 0
+
+ const users = res.data.users || []
+ // 交换第 1 和第 3 个用户位置
+ if (users.length >= 3) {
+ [users[0], users[20]] = [users[20], users[0]]
+ }
+
+ pageCache.value.set(page, users)
+ pageCacheVersion.value++
+ }
+ } catch (e) {
+ console.error(`[useCabin] fetchPage: page=${page} 请求失败`, e?.message ?? e)
+ } finally {
+ pendingPages.delete(page)
+ }
+ }
+
+ const ensurePages = (center, isInertia = false) => {
+ const keep = new Set()
+ for (let n = 0; n <= 4; n++) {
+ keep.add(wrapPage(center + n - 1))
+ }
+
+ const pages = [...keep]
+ pages.forEach(fetchPage)
+
+ // 清除不在渲染范围内的页缓存
+ const evicted = []
+ for (const key of pageCache.value.keys()) {
+ if (!keep.has(key) && !pendingPages.has(key)) {
+ evicted.push(key)
+ pageCache.value.delete(key)
+ }
+ }
+ if (evicted.length) {
+ pageCacheVersion.value++
+ }
+ }
+
+ const buildVisibleCabins = (page, cache, coords) => {
+ if (!coords.length || !cabinRenderDefs.length) return []
+
+ const w = tileWidth
+ const result = []
+
+ // 第一遍:收集上方有用户数据的 cabin(待移位),以及下方空位坐标
+ const toRelocate = []
+ const emptySlots = []
+
+ for (let i = 0; i < coords.length; i++) {
+ const { sx, sy } = coords[i]
+ for (let n = 0; n <= 4; n++) {
+ const p = wrapPage(page + n - 1)
+ const users = cache.get(p) || []
+ const user = users[i] || null
+ const isAbove = sy < bannerBottom
+
+ if (isAbove && user) {
+ const typeIdx = cabinTypeByLevel(user.level)
+ toRelocate.push({ i, n, user, typeIdx, sx, sy })
+ } else if (!isAbove && !user) {
+ emptySlots.push({ sx, sy, n })
+ }
+ }
+ }
+
+ // 第二遍:正常渲染,跳过"被移位占用的空位"和"已移位的上方 cabin"
+ const occupiedSlots = new Set()
+ for (let k = 0; k < toRelocate.length && k < emptySlots.length; k++) {
+ occupiedSlots.add(k)
+ }
+
+ for (let i = 0; i < coords.length; i++) {
+ const { sx, sy } = coords[i]
+ for (let n = 0; n <= 4; n++) {
+ const p = wrapPage(page + n - 1)
+ const users = cache.get(p) || []
+ const user = users[i] || null
+ const isAbove = sy < bannerBottom
+
+ // 跳过上方有数据的(会被移位渲染)
+ if (isAbove && user) continue
+
+ // 检查这个空位是否被移位 cabin 占用
+ const slotIdx = emptySlots.findIndex(s => s.sx === sx && s.sy === sy && s.n === n)
+ if (slotIdx !== -1 && occupiedSlots.has(slotIdx)) continue
+
+ const typeIdx = cabinTypeByLevel(user ? user.level : 0)
+ const def = CABIN_DEFS[typeIdx]
+ const render = cabinRenderDefs[typeIdx]
+ const cabinY = sy - render.offsetY
+
+ result.push(reactive({
+ key: `${i}-${n}`,
+ x: sx + n * w - render.offsetX,
+ y: cabinY,
+ w: render.renderedW,
+ h: render.renderedH,
+ src: def.src,
+ userId: user ? user.user_id : null,
+ galleryOwnerId: user ? user.gallery_owner_id : null,
+ nickname: user ? user.nickname : null,
+ sharedBoothSlotsRemaining: user ? user.shared_booth_slots_remaining : null,
+ showNickname: !isAbove,
+ isMine: false,
+ showDialog: false,
+ }))
+ }
+ }
+
+ // 第三遍:把移位的 cabin 放到对应空位坐标上
+ for (let k = 0; k < toRelocate.length; k++) {
+ const { n, user, typeIdx } = toRelocate[k]
+ const def = CABIN_DEFS[typeIdx]
+ const render = cabinRenderDefs[typeIdx]
+
+ if (k >= emptySlots.length) continue
+
+ const targetSx = emptySlots[k].sx
+ const targetSy = emptySlots[k].sy
+
+ result.push(reactive({
+ key: `reloc-${k}-${n}`,
+ x: targetSx + n * w - render.offsetX,
+ y: targetSy - render.offsetY,
+ w: render.renderedW,
+ h: render.renderedH,
+ src: def.src,
+ userId: user.user_id,
+ galleryOwnerId: user.gallery_owner_id,
+ nickname: user.nickname,
+ sharedBoothSlotsRemaining: user.shared_booth_slots_remaining,
+ showNickname: true,
+ isMine: false,
+ showDialog: false,
+ }))
+ }
+
+ return result
+ }
+
+ // 翻页或坐标初始化时全量重建
+ watch([currentPage, scaledCoords], () => {
+ visibleCabins.value = buildVisibleCabins(currentPage.value, pageCache.value, scaledCoords.value)
+ }, { immediate: true })
+
+ // 数据加载完成时重建
+ watch(pageCacheVersion, () => {
+ visibleCabins.value = buildVisibleCabins(currentPage.value, pageCache.value, scaledCoords.value)
+ })
+
+ const initCabin = ({ screenW, tileW, imageH, currentUserNickname }) => {
+ screenWidth = screenW
+ tileWidth = tileW
+
+ // 计算 banner 底部边界
+ const rpxToPx = screenWidth / 750
+ bannerBottom = 496 * rpxToPx
+
+ // 计算 cabin 渲染尺寸
+ const baseH = Math.round(screenWidth * 0.2 * 1.3)
+ cabinRenderDefs = CABIN_DEFS.map(({ imgW, imgH, anchorX, anchorY }) => {
+ const renderedH = baseH
+ const renderedW = Math.round(renderedH * (imgW / imgH))
+ return {
+ renderedW,
+ renderedH,
+ offsetX: Math.round(renderedW * (anchorX / imgW)),
+ offsetY: Math.round(renderedH * (anchorY / imgH)),
+ }
+ })
+
+ // 生成并缩放坐标
+ const scale = imageH / 1918
+ const coords = generateGridCoordinates()
+ scaledCoords.value = coords.map(({ x, y }) => ({
+ sx: x * scale,
+ sy: y * scale,
+ }))
+ }
+
+ const updateCurrentUserNickname = (nickname) => {
+ visibleCabins.value.forEach(cabin => {
+ if (cabin.nickname === nickname) {
+ cabin.isMine = true
+ }
+ })
+ }
+
+ const resetSquare = async () => {
+ currentPage.value = 1
+ totalUsers.value = 0
+ pageCache.value = new Map()
+ pageCacheVersion.value++
+ await fetchPage(1)
+ ensurePages(1)
+ }
+
+ return {
+ visibleCabins,
+ currentPage,
+ scaledCoords,
+ cabinRenderDefs,
+ fetchPage,
+ ensurePages,
+ initCabin,
+ updateCurrentUserNickname,
+ resetSquare,
+ wrapPage,
+ }
+}
diff --git a/frontend/pages/square/composables/useDialogRotation.js b/frontend/pages/square/composables/useDialogRotation.js
new file mode 100644
index 0000000..7bfd9dd
--- /dev/null
+++ b/frontend/pages/square/composables/useDialogRotation.js
@@ -0,0 +1,84 @@
+import { ref } from 'vue'
+
+export function useDialogRotation() {
+ let timer = null
+ let screenWidth = 375
+ let screenHeight = 812
+
+ const initDialogRotation = ({ screenW, screenH }) => {
+ screenWidth = screenW
+ screenHeight = screenH
+ }
+
+ const isCabinInViewport = (cabin) => {
+ return new Promise((resolve) => {
+ const query = uni.createSelectorQuery()
+ query.select(`#cabin-${cabin.key}`).boundingClientRect((rect) => {
+ if (!rect) {
+ resolve(false)
+ return
+ }
+
+ const visibleTop = Math.max(0, rect.top)
+ const visibleBottom = Math.min(screenHeight, rect.bottom)
+ const visibleLeft = Math.max(0, rect.left)
+ const visibleRight = Math.min(screenWidth, rect.right)
+ const visibleWidth = Math.max(0, visibleRight - visibleLeft)
+ const visibleHeight = Math.max(0, visibleBottom - visibleTop)
+ const visibleArea = visibleWidth * visibleHeight
+ const totalArea = rect.width * rect.height
+ const visiblePercent = totalArea > 0 ? visibleArea / totalArea : 0
+
+ resolve(visiblePercent > 0.7)
+ }).exec()
+ })
+ }
+
+ const rotateDialogVisibility = async (cabins) => {
+ if (!cabins.length) return
+
+ // 先全部重置为 false
+ cabins.forEach(c => c.showDialog = false)
+
+ // 筛选符合条件的 cabin:有昵称、有展位剩余
+ const hasData = cabins.filter(c => c.nickname && c.sharedBoothSlotsRemaining !== null)
+
+ // 批量检查所有 cabin 是否在可视区域内
+ const viewportChecks = await Promise.all(hasData.map(c => isCabinInViewport(c)))
+ const eligible = hasData.filter((c, i) => viewportChecks[i])
+
+ if (eligible.length === 0) return
+
+ // 随机选择 2-3 个
+ const count = Math.min(Math.floor(Math.random() * 2) + 2, eligible.length, 3)
+ const shuffled = eligible.sort(() => Math.random() - 0.5)
+ for (let i = 0; i < count; i++) {
+ shuffled[i].showDialog = true
+ }
+ }
+
+ const startDialogRotation = (cabins) => {
+ stopDialogRotation()
+
+ const rotate = async () => {
+ await rotateDialogVisibility(cabins)
+ const interval = Math.floor(Math.random() * 1000) + 2000
+ timer = setTimeout(rotate, interval)
+ }
+
+ rotate()
+ }
+
+ const stopDialogRotation = () => {
+ if (timer) {
+ clearTimeout(timer)
+ timer = null
+ }
+ }
+
+ return {
+ initDialogRotation,
+ startDialogRotation,
+ stopDialogRotation,
+ }
+}
diff --git a/frontend/pages/square/composables/useSwipe.js b/frontend/pages/square/composables/useSwipe.js
new file mode 100644
index 0000000..eb8e2d9
--- /dev/null
+++ b/frontend/pages/square/composables/useSwipe.js
@@ -0,0 +1,181 @@
+import { ref, computed } from 'vue'
+
+// RAF 封装
+const rafFn = (cb) => uni.requestAnimationFrame ? uni.requestAnimationFrame(cb) : setTimeout(cb, 16)
+const cafFn = (id) => uni.cancelAnimationFrame ? uni.cancelAnimationFrame(id) : clearTimeout(id)
+
+export function useSwipe() {
+ const bgOffsetX = ref(0)
+ const screenWidth = ref(375)
+ const tileWidth = ref(375)
+
+ let rawOffsetX = 0
+ let touchStartX = 0
+ let lastMoveX = 0
+ let lastMoveTime = 0
+ let velocity = 0
+ let inertiaRaf = null
+ let isInertiaPhase = false
+ let touchInBanner = false
+
+ let onTileChange = null
+
+ const cabinLayerStyle = computed(() => ({
+ transform: `translateX(${-tileWidth.value + bgOffsetX.value}px)`
+ }))
+
+ const backgroundStripStyle = computed(() => ({
+ width: `${tileWidth.value * 3}px`,
+ transform: `translateX(${-tileWidth.value + bgOffsetX.value}px)`
+ }))
+
+ const clampOffset = (offset) => {
+ const w = tileWidth.value
+ return (((offset % w) + w) % w) - w
+ }
+
+ const normalizeOffset = (offset) => {
+ const w = tileWidth.value
+ const normalized = clampOffset(offset)
+ const prevTileN = Math.floor(-rawOffsetX / w)
+ rawOffsetX += offset - bgOffsetX.value
+ const nextTileN = Math.floor(-rawOffsetX / w)
+ const delta = nextTileN - prevTileN
+
+ if (delta !== 0 && onTileChange) {
+ onTileChange(delta, isInertiaPhase)
+ }
+
+ return normalized
+ }
+
+ const stopInertia = () => {
+ if (inertiaRaf) {
+ cafFn(inertiaRaf)
+ inertiaRaf = null
+ }
+ isInertiaPhase = false
+ }
+
+ const scrollPage = (direction) => {
+ stopInertia()
+
+ if (onTileChange) {
+ onTileChange(direction, false)
+ }
+
+ const DURATION = 300
+ const FRAME = 16
+ const totalFrames = Math.round(DURATION / FRAME)
+ const totalDelta = tileWidth.value * direction * -1
+ let frame = 0
+
+ const step = () => {
+ frame++
+ const progress = frame / totalFrames
+ const eased = 1 - Math.pow(1 - progress, 3)
+ const prevEased = frame === 1 ? 0 : 1 - Math.pow(1 - (frame - 1) / totalFrames, 3)
+ const delta = totalDelta * (eased - prevEased)
+
+ bgOffsetX.value = clampOffset(bgOffsetX.value + delta)
+ rawOffsetX += delta
+
+ if (frame < totalFrames) {
+ inertiaRaf = rafFn(step)
+ }
+ }
+
+ inertiaRaf = rafFn(step)
+ }
+
+ const getBannerBottom = () => (screenWidth.value / 750) * 496
+
+ const onBgTouchStart = (e) => {
+ const touchY = e.touches[0].clientY
+ touchInBanner = touchY < getBannerBottom()
+ if (touchInBanner) return
+
+ stopInertia()
+ touchStartX = e.touches[0].clientX
+ lastMoveX = touchStartX
+ lastMoveTime = Date.now()
+ velocity = 0
+ }
+
+ const onBgTouchMove = (e) => {
+ if (touchInBanner) return
+ e.preventDefault()
+
+ const currentX = e.touches[0].clientX
+ const now = Date.now()
+ const dt = now - lastMoveTime || 1
+ velocity = (currentX - lastMoveX) / dt
+ lastMoveX = currentX
+ lastMoveTime = now
+
+ bgOffsetX.value = normalizeOffset(bgOffsetX.value + (currentX - touchStartX))
+ touchStartX = currentX
+ }
+
+ const onBgTouchEnd = () => {
+ if (touchInBanner) {
+ touchInBanner = false
+ return
+ }
+ touchInBanner = false
+ isInertiaPhase = true
+
+ const FRICTION = 0.8
+ const MIN_VELOCITY = 0.2
+
+ const step = () => {
+ velocity *= FRICTION
+ if (Math.abs(velocity) < MIN_VELOCITY) {
+ isInertiaPhase = false
+ return
+ }
+ bgOffsetX.value = normalizeOffset(bgOffsetX.value + velocity * 16)
+ inertiaRaf = rafFn(step)
+ }
+
+ inertiaRaf = rafFn(step)
+ }
+
+ const onBgTouchCancel = () => {
+ touchInBanner = false
+ stopInertia()
+ velocity = 0
+ }
+
+ const initSwipe = ({ screenW, tileW, onTileChangeCallback }) => {
+ screenWidth.value = screenW
+ tileWidth.value = tileW
+ onTileChange = onTileChangeCallback
+ bgOffsetX.value = 0
+ rawOffsetX = 0
+ velocity = 0
+ }
+
+ const reset = () => {
+ stopInertia()
+ bgOffsetX.value = 0
+ rawOffsetX = 0
+ velocity = 0
+ }
+
+ return {
+ bgOffsetX,
+ rawOffsetX,
+ velocity,
+ cabinLayerStyle,
+ backgroundStripStyle,
+ scrollPage,
+ stopInertia,
+ initSwipe,
+ reset,
+ onBgTouchStart,
+ onBgTouchMove,
+ onBgTouchEnd,
+ onBgTouchCancel,
+ }
+}
diff --git a/frontend/pages/square/config/cabin.js b/frontend/pages/square/config/cabin.js
new file mode 100644
index 0000000..2dc76cc
--- /dev/null
+++ b/frontend/pages/square/config/cabin.js
@@ -0,0 +1,116 @@
+// ========== 图片原始尺寸 ==========
+export const IMAGE_W = 2012
+export const IMAGE_H = 1918
+
+// ========== 小屋类型定义 ==========
+export const CABIN_DEFS = [
+ { src: '/static/components/cabin1.png', imgW: 1000, imgH: 1000, anchorX: 500, anchorY: 680 },
+ { src: '/static/components/cabin2.png', imgW: 1000, imgH: 1000, anchorX: 500, anchorY: 680 },
+ { src: '/static/components/cabin3.png', imgW: 1000, imgH: 1351, anchorX: 500, anchorY: 965 },
+ { src: '/static/components/cabin4.png', imgW: 1000, imgH: 1223, anchorX: 500, anchorY: 875 },
+]
+
+// ========== 根据等级返回 cabin 类型索引 ==========
+export const cabinTypeByLevel = (level) => {
+ if (level >= 7) return 3
+ if (level >= 5) return 2
+ if (level >= 3) return 1
+ return 0
+}
+
+// ========== 背景网格配置 ==========
+export const GRID_CONFIG = {
+ backgroundWidth: 2012,
+ backgroundHeight: 1918,
+ grid: {
+ rows: 11,
+ cols: 4,
+ startX: -260,
+ startY: -20,
+ spacingX: 515,
+ spacingY: 180, // 减小垂直间距
+ staggered: true,
+ staggerOffsetX: 260,
+ cellWidth: 200,
+ cellHeight: 150,
+ excludeRows: [0, 1, 2],
+ },
+ // 手动微调个别位置
+ manualAdjustments: {
+ // 可以在这里添加需要微调的坐标
+ // 格式: index: { x: newX, y: newY }
+ 16: { x: -255, y: 740 },
+ 17: { x: 255, y: 740 },
+ 18: { x: 750, y: 740 },
+ 19: { x: 1265, y: 740 },
+ 20: { x: 0, y: 900 },
+ 21: { x: 505, y: 900 },
+ 22: { x: 1010, y: 900 },
+ 23: { x: 1515, y: 900 },
+ 24: { x: -245, y: 1120 },
+ 25: { x: 255, y: 1120 },
+ 26: { x: 750, y: 1120 },
+ 27: { x: 1265, y: 1120 },
+ 28: { x: 0, y: 1300 },
+ 29: { x: 505, y: 1300 },
+ 30: { x: 1010, y: 1300 },
+ 31: { x: 1515, y: 1300 },
+ 32: { x: -245, y: 1520 },
+ 33: { x: 255, y: 1520 },
+ 34: { x: 750, y: 1520 },
+ 35: { x: 1265, y: 1520 },
+ 36: { x: 0, y: 1690 },
+ 37: { x: 505, y: 1690 },
+ 38: { x: 1010, y: 1690 },
+ 39: { x: 1515, y: 1690 },
+ 40: { x: -245, y: 1880 },
+ 41: { x: 255, y: 1880 },
+ 42: { x: 750, y: 1880 },
+ 43: { x: 1265, y: 1880 },
+ }
+
+
+}
+
+// ========== 网格坐标生成函数 ==========
+export function generateGridCoordinates(config = GRID_CONFIG) {
+ const { rows, cols, startX, startY, spacingX, spacingY, staggered, staggerOffsetX, excludeRows } = config.grid
+ const coords = []
+
+ // 先填充被排除行的占位符
+ for (let row = 0; row < rows; row++) {
+ if (excludeRows.includes(row)) {
+ for (let col = 0; col < cols; col++) {
+ coords.push({
+ x: 0,
+ y: 0,
+ row,
+ col,
+ index: coords.length
+ })
+ }
+ } else {
+ for (let col = 0; col < cols; col++) {
+ const offsetX = staggered && row % 2 === 1 ? staggerOffsetX : 0
+ coords.push({
+ x: startX + col * spacingX + offsetX,
+ y: startY + row * spacingY,
+ row,
+ col,
+ index: coords.length
+ })
+ }
+ }
+ }
+
+ // 应用手动微调
+ Object.entries(config.manualAdjustments || {}).forEach(([index, adjustment]) => {
+ const idx = parseInt(index)
+ if (coords[idx]) {
+ coords[idx].x = adjustment.x ?? coords[idx].x
+ coords[idx].y = adjustment.y ?? coords[idx].y
+ }
+ })
+
+ return coords
+}
diff --git a/frontend/pages/square/debug-grid.vue b/frontend/pages/square/debug-grid.vue
new file mode 100644
index 0000000..e8bcc53
--- /dev/null
+++ b/frontend/pages/square/debug-grid.vue
@@ -0,0 +1,696 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ point.originalIndex }}
+ ({{ Math.round(point.originalX) }}, {{ Math.round(point.originalY) }})
+
+
+
+
+
+
+
+
+
+
+
+
+
+ X: {{ currentPoint.x }}
+
+
+
+
+
+
+
+
+
+ Y: {{ currentPoint.y }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 起始Y (startY): {{ config.grid.startY }}
+
+
+
+
+ 垂直间距 (spacingY): {{ config.grid.spacingY }}
+
+
+
+
+ 水平间距 (spacingX): {{ config.grid.spacingX }}
+
+
+
+
+ 交错偏移 (staggerOffsetX): {{ config.grid.staggerOffsetX }}
+
+
+
+
+
+
+
+
+
+
+ 生成的坐标列表(点击点位编辑):
+
+
+ {{ i }}:
+ [{{ coord.x }}, {{ coord.y }}]
+ (排除)
+ (已调整)
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/pages/square/square.vue b/frontend/pages/square/square.vue
index b5d4abb..c61ca37 100644
--- a/frontend/pages/square/square.vue
+++ b/frontend/pages/square/square.vue
@@ -1,1197 +1,410 @@
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
+
+
- {{ cabin.nickname ===
- currentUserNickname ? '我的小屋' : cabin.nickname }}
- 小屋暂无人居住
-
- 剩余 {{
- cabin.sharedBoothSlotsRemaining }} 个展位
-
-
-
+
+
-
-
-
- ‹
-
-
- ›
-
-
+
+
+ 调试
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
\ No newline at end of file
+
+.debug-btn {
+ position: fixed;
+ bottom: 200rpx;
+ right: 20rpx;
+ width: 100rpx;
+ height: 100rpx;
+ background: rgba(255, 0, 0, 0.8);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 999;
+ box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.3);
+}
+
+.debug-text {
+ font-size: 24rpx;
+ color: #ffffff;
+ font-weight: bold;
+}
+
diff --git a/frontend/pages/square/square.vue.old b/frontend/pages/square/square.vue.old
deleted file mode 100644
index b5d4abb..0000000
--- a/frontend/pages/square/square.vue.old
+++ /dev/null
@@ -1,1197 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ cabin.nickname ===
- currentUserNickname ? '我的小屋' : cabin.nickname }}
- 小屋暂无人居住
-
- 剩余 {{
- cabin.sharedBoothSlotsRemaining }} 个展位
-
-
-
-
-
-
-
- ‹
-
-
- ›
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file