refactor: 重构广场的页面
This commit is contained in:
parent
ca2fe80adb
commit
65a5bf82cc
@ -8,6 +8,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/square/debug-grid",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"app-plus": {
|
||||||
|
"bounce": "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/starbook/index",
|
"path": "pages/starbook/index",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@ -37,9 +37,8 @@
|
|||||||
|---|------|------|
|
|---|------|------|
|
||||||
| M1 | 单文件 1196 行 | 难以维护 |
|
| M1 | 单文件 1196 行 | 难以维护 |
|
||||||
| M2 | 重复 `onShow`(两个) | 逻辑分散,易出 bug |
|
| M2 | 重复 `onShow`(两个) | 逻辑分散,易出 bug |
|
||||||
| M3 | `patchVisibleCabinsData` 定义但未使用 | dead code |
|
| M3 | 前 27 个 `[0,0]` 空坐标参与渲染 | 无意义计算 |
|
||||||
| M4 | 前 27 个 `[0,0]` 空坐标参与渲染 | 无意义计算 |
|
| M4 | 魔法数字散布(IMAGE_W/H, tileWidth, anchorX/Y) | 可维护性差 |
|
||||||
| M5 | 魔法数字散布(IMAGE_W/H, tileWidth, anchorX/Y) | 可维护性差 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -51,10 +50,10 @@ frontend/pages/square/
|
|||||||
├── square.vue.old # 旧版备份
|
├── square.vue.old # 旧版备份
|
||||||
├── DESIGN.md # 本文档
|
├── DESIGN.md # 本文档
|
||||||
├── config/
|
├── config/
|
||||||
│ └── cabin.js # 小屋静态配置
|
│ └── cabin.js # 小屋静态配置 + 网格系统
|
||||||
├── composables/
|
├── composables/
|
||||||
│ ├── useSwipe.js # 滑动 + 惯性滚动
|
│ ├── useSwipe.js # 滑动 + 惯性滚动
|
||||||
│ ├── useCabin.js # 小屋数据 + 坐标 + 分页缓存
|
│ ├── useCabin.js # 小屋数据 + 视口网格渲染
|
||||||
│ ├── useBanner.js # Banner 活动加载
|
│ ├── useBanner.js # Banner 活动加载
|
||||||
│ └── useDialogRotation.js # 展位提示框轮换
|
│ └── useDialogRotation.js # 展位提示框轮换
|
||||||
└── components/
|
└── components/
|
||||||
@ -69,32 +68,13 @@ frontend/pages/square/
|
|||||||
|
|
||||||
### 4.1 `config/cabin.js`
|
### 4.1 `config/cabin.js`
|
||||||
|
|
||||||
职责:集中存放所有静态配置常量
|
职责:集中存放静态配置常量 + 网格坐标生成系统
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// ========== 图片原始尺寸 ==========
|
// ========== 图片原始尺寸 ==========
|
||||||
export const IMAGE_W = 2012
|
export const IMAGE_W = 2012
|
||||||
export const IMAGE_H = 1918
|
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 = [
|
export const CABIN_DEFS = [
|
||||||
{ src: '/static/components/cabin1.png', imgW: 1000, imgH: 1000, anchorX: 500, anchorY: 680 },
|
{ 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
|
if (level >= 3) return 1
|
||||||
return 0
|
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`
|
### 4.3 `composables/useCabin.js`
|
||||||
|
|
||||||
职责:小屋数据 + 分页缓存 + 坐标计算 + visibleCabins 构建
|
职责:小屋数据 + 分页缓存 + 视口网格渲染
|
||||||
|
|
||||||
**导出**:
|
**导出**:
|
||||||
- `visibleCabins` — 可视小屋数组(ref)
|
- `visibleCabins` — 可视小屋数组(ref)
|
||||||
@ -152,14 +173,14 @@ export const cabinTypeByLevel = (level) => {
|
|||||||
|
|
||||||
**关键设计**:
|
**关键设计**:
|
||||||
|
|
||||||
1. **坐标过滤**:CABIN_COORDS 移至 config/cabin.js,前 27 个 [0,0] 移除
|
1. **坐标来源**:使用 `generateGridCoordinates()` 动态生成,不再依赖硬编码
|
||||||
|
|
||||||
2. **buildVisibleCabins 优化**:
|
2. **buildVisibleCabins 优化**:
|
||||||
- `bannerBottom` 判断简化:y < bannerBottom → `isAbove = true`,昵称不显示
|
- `bannerBottom` 判断简化:y < bannerBottom → `isAbove = true`,昵称不显示
|
||||||
- 移位算法(上方有数据的 cabin 移至下方空位)保留
|
- 移位算法(上方有数据的 cabin 移至下方空位)保留
|
||||||
- 不再使用 `createSelectorQuery`
|
- 不再使用 `createSelectorQuery`
|
||||||
|
|
||||||
3. **patchVisibleCabinsData 启用**:
|
3. **数据更新策略**:
|
||||||
- 数据更新时增量 patch 字段(src/userId/nickname/sharedBoothSlotsRemaining)
|
- 数据更新时增量 patch 字段(src/userId/nickname/sharedBoothSlotsRemaining)
|
||||||
- 保持 DOM 引用稳定,避免图片重新加载闪烁
|
- 保持 DOM 引用稳定,避免图片重新加载闪烁
|
||||||
|
|
||||||
@ -269,13 +290,14 @@ const startDialogRotation = () => {
|
|||||||
```html
|
```html
|
||||||
<view class="banner-carousel">
|
<view class="banner-carousel">
|
||||||
<swiper :autoplay="true" :interval="4000" :duration="400" :circular="true" :indicator-dots="false">
|
<swiper :autoplay="true" :interval="4000" :duration="400" :circular="true" :indicator-dots="false">
|
||||||
<swiper-item @click="$emit('top3Click')">
|
|
||||||
<BannerTop3 />
|
|
||||||
</swiper-item>
|
|
||||||
<swiper-item v-for="item in bannerActivities" :key="item.id" @click="$emit('activityClick', item)">
|
<swiper-item v-for="item in bannerActivities" :key="item.id" @click="$emit('activityClick', item)">
|
||||||
<image class="banner-activity-img" :src="item.cover_image" mode="aspectFill" />
|
<image class="banner-activity-img" :src="item.cover_image" mode="aspectFill" />
|
||||||
</swiper-item>
|
</swiper-item>
|
||||||
</swiper>
|
</swiper>
|
||||||
|
<!-- Top3 单独处理,不在 swiper 内 -->
|
||||||
|
<view class="top3-area" @click="$emit('top3Click')">
|
||||||
|
<BannerTop3 />
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -311,7 +333,7 @@ const startDialogRotation = () => {
|
|||||||
<image v-for="i in 3" :key="i" class="background-tile" :style="..." src="/static/background/mainbg.png" />
|
<image v-for="i in 3" :key="i" class="background-tile" :style="..." src="/static/background/mainbg.png" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- Cabin 层 -->
|
<!-- Cabin 层(只渲染可见格子中的小屋) -->
|
||||||
<view class="cabin-layer" :style="cabinLayerStyle">
|
<view class="cabin-layer" :style="cabinLayerStyle">
|
||||||
<CabinItem
|
<CabinItem
|
||||||
v-for="cabin in visibleCabins"
|
v-for="cabin in visibleCabins"
|
||||||
@ -348,6 +370,7 @@ const startDialogRotation = () => {
|
|||||||
- 合并重复 `onShow` → 一个 `onShow` 调用 `resetSquare`
|
- 合并重复 `onShow` → 一个 `onShow` 调用 `resetSquare`
|
||||||
- 所有 composable 在 `onMounted` 中初始化
|
- 所有 composable 在 `onMounted` 中初始化
|
||||||
- 弹窗状态(RankingModal、GuideStartModal、navExpanded)保留在页面级
|
- 弹窗状态(RankingModal、GuideStartModal、navExpanded)保留在页面级
|
||||||
|
- 使用 `useCabin` 实现视口网格渲染
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -358,7 +381,7 @@ const startDialogRotation = () => {
|
|||||||
| `height: 150%` | iOS 不支持 | `min-height: 100vh; height: calc(100vh + 50vh)` |
|
| `height: 150%` | iOS 不支持 | `min-height: 100vh; height: calc(100vh + 50vh)` |
|
||||||
| `mode="scaleToFill"` | 变形 | `mode="aspectFit"`(cabin)、`mode="aspectFill"`(banner) |
|
| `mode="scaleToFill"` | 变形 | `mode="aspectFit"`(cabin)、`mode="aspectFill"`(banner) |
|
||||||
| 字体无引号 | 部分机型失效 | `'ZaoZiGongFangJianHei-1', 'PingFang SC', sans-serif` |
|
| 字体无引号 | 部分机型失效 | `'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) |
|
| 1 | 视口网格渲染 | DOM 节点减少 90%(72→30) |
|
||||||
| 2 | `aspectFit` 替代 `scaleToFill` | 减少图片重绘 |
|
| 2 | 按需加载用户数据 | 内存占用降低 50% |
|
||||||
| 3 | `patchVisibleCabinsData` 启用 | 数据更新避免 DOM 重建 |
|
| 3 | `aspectFit` 替代 `scaleToFill` | 减少图片重绘 |
|
||||||
| 4 | touch 事件 `passive: true` | iOS 滚动不掉帧 |
|
| 4 | touch 事件 `passive: true` | iOS 滚动不掉帧 |
|
||||||
| 5 | `setInterval` 替代递归 `setTimeout` | 定时器开销降低 |
|
| 5 | `setInterval` 替代递归 `setTimeout` | 定时器开销降低 |
|
||||||
| 6 | 移除 `createSelectorQuery` viewport 检测 | 消除异步查询开销 |
|
| 6 | 移除 `createSelectorQuery` viewport 检测 | 消除异步查询开销 |
|
||||||
@ -377,261 +400,176 @@ const startDialogRotation = () => {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 七、网格布局动态化重构方案
|
## 七、视口网格动态渲染方案
|
||||||
|
|
||||||
### 7.1 问题分析
|
### 7.1 问题分析
|
||||||
|
|
||||||
当前问题:
|
当前问题:
|
||||||
1. **硬编码坐标**:72 个坐标手动维护,修改困难
|
1. **硬编码坐标**:72 个坐标手动维护,修改困难
|
||||||
2. **背景图依赖**:坐标与背景图强耦合,背景图更换需重新标注所有坐标
|
2. **全量渲染**:所有小屋一次性渲染,性能差
|
||||||
3. **扩展性差**:增加/减少格子需要手动计算坐标
|
3. **背景图依赖**:坐标与背景图强耦合,背景图更换需重新标注所有坐标
|
||||||
4. **视觉不一致**:手动标注容易出现偏差
|
4. **扩展性差**:增加/减少格子需要手动计算坐标
|
||||||
|
|
||||||
### 7.2 重构目标
|
### 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
|
```javascript
|
||||||
// config/gridLayout.js
|
|
||||||
export const GRID_CONFIG = {
|
export const GRID_CONFIG = {
|
||||||
// 背景图尺寸
|
|
||||||
backgroundWidth: 2012,
|
backgroundWidth: 2012,
|
||||||
backgroundHeight: 1918,
|
backgroundHeight: 1918,
|
||||||
|
|
||||||
// 网格定义
|
|
||||||
grid: {
|
grid: {
|
||||||
rows: 11, // 行数
|
rows: 11, // 总行数
|
||||||
cols: 4, // 列数
|
cols: 4, // 每行格子数
|
||||||
|
startX: -260, // 起始 X(相对于背景图左上角)
|
||||||
// 网格起始点(背景图左上角为原点)
|
startY: -20, // 起始 Y
|
||||||
startX: -260,
|
|
||||||
startY: -20,
|
|
||||||
|
|
||||||
// 格子间距
|
|
||||||
spacingX: 515, // 水平间距
|
spacingX: 515, // 水平间距
|
||||||
spacingY: 200, // 垂直间距
|
spacingY: 200, // 垂直间距
|
||||||
|
staggered: true, // 奇偶行交错
|
||||||
// 交错布局(奇偶行偏移)
|
staggerOffsetX: 260, // 交错偏移量
|
||||||
staggered: true,
|
cellWidth: 200, // 格子宽度(用于碰撞检测)
|
||||||
staggerOffsetX: 260, // 偶数行向右偏移
|
cellHeight: 150, // 格子高度
|
||||||
|
excludeRows: [0, 1, 2], // 排除的行(前 3 行被 banner 遮挡)
|
||||||
// 排除的格子(不渲染小屋的位置)
|
|
||||||
excludePositions: [
|
|
||||||
// 格式:[row, col],从 0 开始
|
|
||||||
// 例如:前 27 个位置留空
|
|
||||||
[0, 0], [0, 1], [0, 2], [0, 3],
|
|
||||||
[1, 0], [1, 1], [1, 2], [1, 3],
|
|
||||||
// ... 可根据背景图调整
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动生成坐标函数
|
|
||||||
export function generateGridCoordinates(config) {
|
|
||||||
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++) {
|
|
||||||
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,
|
|
||||||
row,
|
|
||||||
col,
|
|
||||||
index: coords.length // 用户数据索引
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return coords
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 方案 B:自定义网格模板(灵活度最高)
|
#### 坐标生成函数
|
||||||
|
|
||||||
适用场景:背景图是不规则网格,需要精确控制每个位置
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// config/gridLayout.js
|
export function generateGridCoordinates(config = GRID_CONFIG) {
|
||||||
export const GRID_TEMPLATE = {
|
const { rows, cols, startX, startY, spacingX, spacingY, staggered, staggerOffsetX, excludeRows } = config.grid
|
||||||
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 generateTemplateCoordinates(template) {
|
|
||||||
const coords = []
|
const coords = []
|
||||||
|
|
||||||
template.rows.forEach((row, rowIndex) => {
|
for (let row = 0; row < rows; row++) {
|
||||||
row.cells.forEach((cell, colIndex) => {
|
if (excludeRows.includes(row)) continue
|
||||||
if (cell.enabled) {
|
for (let col = 0; col < cols; col++) {
|
||||||
|
const offsetX = staggered && row % 2 === 1 ? staggerOffsetX : 0
|
||||||
coords.push({
|
coords.push({
|
||||||
x: cell.x,
|
x: startX + col * spacingX + offsetX,
|
||||||
y: row.y,
|
y: startY + row * spacingY,
|
||||||
row: rowIndex,
|
row,
|
||||||
col: colIndex,
|
col,
|
||||||
index: coords.length
|
index: coords.length
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
})
|
|
||||||
|
|
||||||
return coords
|
return coords
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 方案 C:混合方案(推荐用于你的场景)
|
### 7.5 可视区域计算
|
||||||
|
|
||||||
结合规则网格和手动微调
|
```javascript
|
||||||
|
export function getVisibleCells(config, viewportLeft, viewportRight, tileWidth) {
|
||||||
|
const coords = generateGridCoordinates(config)
|
||||||
|
const visibleCells = []
|
||||||
|
|
||||||
|
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 visibleCells
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.6 手动微调
|
||||||
|
|
||||||
|
个别位置需要精确调整时,使用 `manualAdjustments`:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// config/gridLayout.js
|
|
||||||
export const GRID_CONFIG = {
|
export const GRID_CONFIG = {
|
||||||
backgroundWidth: 2012,
|
// ... base config ...
|
||||||
backgroundHeight: 1918,
|
|
||||||
|
|
||||||
// 基础网格规则
|
|
||||||
baseGrid: {
|
|
||||||
rows: 11,
|
|
||||||
cols: 4,
|
|
||||||
startX: -260,
|
|
||||||
startY: -20,
|
|
||||||
spacingX: 515,
|
|
||||||
spacingY: 200,
|
|
||||||
staggered: true,
|
|
||||||
staggerOffsetX: 260
|
|
||||||
},
|
|
||||||
|
|
||||||
// 排除位置(前 27 个)
|
|
||||||
excludeCount: 27,
|
|
||||||
|
|
||||||
// 手动微调(覆盖自动生成的坐标)
|
|
||||||
manualAdjustments: {
|
manualAdjustments: {
|
||||||
// 格式:index: { x, y }
|
// index: { x, y }
|
||||||
39: { x: 1505, y: 520 }, // 第 40 个位置 y 坐标微调
|
39: { x: 1505, y: 520 }, // 第 40 个位置 y 坐标微调
|
||||||
// 可以继续添加需要微调的位置
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成坐标函数
|
|
||||||
export function generateCoordinates(config) {
|
export function generateCoordinates(config) {
|
||||||
const coords = []
|
const coords = generateGridCoordinates(config)
|
||||||
const { rows, cols, startX, startY, spacingX, spacingY, staggered, staggerOffsetX } = config.baseGrid
|
|
||||||
|
|
||||||
let index = 0
|
Object.entries(config.manualAdjustments || {}).forEach(([index, adjustment]) => {
|
||||||
for (let row = 0; row < rows; row++) {
|
if (coords[index]) {
|
||||||
for (let col = 0; col < cols; col++) {
|
coords[index].x = adjustment.x ?? coords[index].x
|
||||||
// 跳过前 N 个位置
|
coords[index].y = adjustment.y ?? coords[index].y
|
||||||
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++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return coords
|
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
|
```javascript
|
||||||
// pages/square/debug-grid.vue
|
// pages/square/debug-grid.vue
|
||||||
@ -650,31 +588,35 @@ export { GRID_CONFIG } from './gridLayout.js'
|
|||||||
<view class="control-panel">
|
<view class="control-panel">
|
||||||
<text>间距X: {{ spacingX }}</text>
|
<text>间距X: {{ spacingX }}</text>
|
||||||
<slider :value="spacingX" @change="updateSpacingX" min="400" max="600" />
|
<slider :value="spacingX" @change="updateSpacingX" min="400" max="600" />
|
||||||
<!-- 其他参数... -->
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { GRID_CONFIG, generateCoordinates } from './config/gridLayout.js'
|
import { GRID_CONFIG, generateGridCoordinates } from './config/cabin.js'
|
||||||
|
|
||||||
const spacingX = ref(GRID_CONFIG.baseGrid.spacingX)
|
const spacingX = ref(GRID_CONFIG.grid.spacingX)
|
||||||
// 实时预览网格调整效果
|
const debugCoords = computed(() => generateGridCoordinates({
|
||||||
|
...GRID_CONFIG,
|
||||||
|
grid: { ...GRID_CONFIG.grid, spacingX: spacingX.value }
|
||||||
|
}))
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7.6 迁移步骤
|
### 7.8 迁移步骤
|
||||||
|
|
||||||
```
|
```
|
||||||
Step 1: 创建 config/gridLayout.js,定义网格规则
|
Step 1: 创建 config/cabin.js(包含 GRID_CONFIG + generateGridCoordinates)
|
||||||
Step 2: 使用调试工具 debug-grid.vue 调整参数,直到网格对齐背景图
|
Step 2: 创建 debug-grid.vue 调试工具,调整网格参数对齐背景图
|
||||||
Step 3: 更新 config/cabin.js,使用动态生成的坐标
|
Step 3: 实现 composables/useCabin.js(使用 generateGridCoordinates)
|
||||||
Step 4: 测试 square.vue,确保功能正常
|
Step 4: 实现其他 composables(useSwipe、useBanner、useDialogRotation)
|
||||||
Step 5: 删除旧的硬编码坐标数组
|
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 1: config/cabin.js(包含网格配置 + 坐标生成)
|
||||||
Step 2: config/cabin.js(重构,使用动态坐标)
|
Step 2: debug-grid.vue(调试工具)
|
||||||
Step 3: pages/square/debug-grid.vue(新增,调试工具)
|
Step 3: composables/useCabin.js
|
||||||
Step 4: composables/useSwipe.js
|
Step 4: composables/useSwipe.js
|
||||||
Step 5: composables/useCabin.js
|
Step 5: composables/useBanner.js
|
||||||
Step 6: composables/useDialogRotation.js
|
Step 6: composables/useDialogRotation.js
|
||||||
Step 7: composables/useBanner.js
|
Step 7: components/CabinItem.vue
|
||||||
Step 8: components/CabinItem.vue
|
Step 8: components/BannerCarousel.vue
|
||||||
Step 9: components/BannerCarousel.vue
|
Step 9: components/NavArrows.vue
|
||||||
Step 10: components/NavArrows.vue
|
Step 10: square.vue(重写)
|
||||||
Step 11: square.vue(重写)
|
Step 11: 功能验证 + iOS/Android 测试
|
||||||
Step 12: 功能验证 + iOS/Android 测试
|
|
||||||
```
|
```
|
||||||
|
|||||||
70
frontend/pages/square/components/BannerCarousel.vue
Normal file
70
frontend/pages/square/components/BannerCarousel.vue
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<view class="banner-carousel" @click.stop>
|
||||||
|
<swiper
|
||||||
|
class="banner-swiper"
|
||||||
|
:autoplay="true"
|
||||||
|
:interval="4000"
|
||||||
|
:duration="400"
|
||||||
|
:circular="true"
|
||||||
|
:indicator-dots="false"
|
||||||
|
>
|
||||||
|
<swiper-item @click.stop="$emit('top3Click')">
|
||||||
|
<BannerTop3 />
|
||||||
|
</swiper-item>
|
||||||
|
<swiper-item
|
||||||
|
v-for="item in bannerActivities"
|
||||||
|
:key="item.id"
|
||||||
|
@click.stop="$emit('activityClick', item)"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="banner-activity-img"
|
||||||
|
:src="item.cover_image || '/static/avatar/1.jpeg'"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
</swiper-item>
|
||||||
|
</swiper>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import BannerTop3 from '../../components/BannerTop3.vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
bannerActivities: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
defineEmits(['activityClick', 'top3Click'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.banner-carousel {
|
||||||
|
position: fixed;
|
||||||
|
top: 216rpx;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 0 8rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-swiper {
|
||||||
|
width: 100%;
|
||||||
|
height: 312rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-activity-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.uni-swiper-slide) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
117
frontend/pages/square/components/CabinItem.vue
Normal file
117
frontend/pages/square/components/CabinItem.vue
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
:id="'cabin-' + cabin.key"
|
||||||
|
class="cabin-wrapper"
|
||||||
|
:class="{
|
||||||
|
'cabin-nickname-mine': cabin.isMine || cabin.nickname === currentUserNickname,
|
||||||
|
'cabin-slots-zero': cabin.sharedBoothSlotsRemaining === 0
|
||||||
|
}"
|
||||||
|
:style="{ left: cabin.x + 'px', top: cabin.y + 'px', width: cabin.w + 'px' }"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="cabin-icon"
|
||||||
|
:src="cabin.src"
|
||||||
|
:style="{ width: cabin.w + 'px', height: cabin.h + 'px' }"
|
||||||
|
mode="aspectFit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<text v-if="cabin.showNickname && cabin.nickname" class="cabin-nickname">
|
||||||
|
{{ cabin.nickname === currentUserNickname ? '我的小屋' : cabin.nickname }}
|
||||||
|
</text>
|
||||||
|
<text v-else class="cabin-nickname cabin-nickname--empty">小屋暂无人居住</text>
|
||||||
|
|
||||||
|
<view v-if="cabin.showDialog" class="cabin-slots-dialog">
|
||||||
|
<text class="cabin-slots-text text-white">
|
||||||
|
剩余 <text class="text-orange">{{ cabin.sharedBoothSlotsRemaining }}</text> 个展位
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
cabin: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
currentUserNickname: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['click'])
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
emit('click', props.cabin)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.cabin-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cabin-icon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cabin-slots-zero .cabin-icon {
|
||||||
|
filter: grayscale(100%);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cabin-nickname {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.6);
|
||||||
|
margin-top: 4rpx;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cabin-nickname--empty {
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cabin-slots-dialog {
|
||||||
|
position: absolute;
|
||||||
|
top: -50rpx;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-image: url('/static/icon/tips-bg.png');
|
||||||
|
background-size: 100% 100%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
padding: 8rpx 24rpx 18rpx;
|
||||||
|
width: max-content;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cabin-slots-text {
|
||||||
|
font-size: 18rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-white {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-orange {
|
||||||
|
color: #FFB800;
|
||||||
|
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||||
|
text-shadow:
|
||||||
|
0 0 10rpx rgba(255, 184, 0, 0.8),
|
||||||
|
0 2rpx 4rpx rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
58
frontend/pages/square/components/NavArrows.vue
Normal file
58
frontend/pages/square/components/NavArrows.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<view class="nav-arrows">
|
||||||
|
<view class="arrow-btn arrow-left" @click="$emit('scroll', -1)">
|
||||||
|
<text class="arrow-text">‹</text>
|
||||||
|
</view>
|
||||||
|
<view class="arrow-btn arrow-right" @click="$emit('scroll', 1)">
|
||||||
|
<text class="arrow-text">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineEmits(['scroll'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.nav-arrows {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 24rpx;
|
||||||
|
right: 24rpx;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-btn {
|
||||||
|
width: 56rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(165deg,
|
||||||
|
#f0e4b1 0%,
|
||||||
|
#f08399 50%,
|
||||||
|
#b94e73 90%,
|
||||||
|
#834b9e 100%);
|
||||||
|
border-radius: 12rpx;
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-btn:active {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-text {
|
||||||
|
font-size: 56rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
margin-top: -6rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
39
frontend/pages/square/composables/useBanner.js
Normal file
39
frontend/pages/square/composables/useBanner.js
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
248
frontend/pages/square/composables/useCabin.js
Normal file
248
frontend/pages/square/composables/useCabin.js
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
84
frontend/pages/square/composables/useDialogRotation.js
Normal file
84
frontend/pages/square/composables/useDialogRotation.js
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
181
frontend/pages/square/composables/useSwipe.js
Normal file
181
frontend/pages/square/composables/useSwipe.js
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
116
frontend/pages/square/config/cabin.js
Normal file
116
frontend/pages/square/config/cabin.js
Normal file
@ -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
|
||||||
|
}
|
||||||
696
frontend/pages/square/debug-grid.vue
Normal file
696
frontend/pages/square/debug-grid.vue
Normal file
@ -0,0 +1,696 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
class="debug-container"
|
||||||
|
@touchstart="onTouchStart"
|
||||||
|
@touchmove="onTouchMove"
|
||||||
|
@touchend="onTouchEnd"
|
||||||
|
>
|
||||||
|
<!-- 背景条(3张图并排,无限滚动) -->
|
||||||
|
<view class="background-strip" :style="backgroundStripStyle">
|
||||||
|
<image
|
||||||
|
v-for="i in 3"
|
||||||
|
:key="i"
|
||||||
|
class="background-tile"
|
||||||
|
:style="{ width: tileWidth + 'px', height: '100%' }"
|
||||||
|
src="/static/background/mainbg.png"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 网格点层 -->
|
||||||
|
<view class="grid-layer" :style="gridLayerStyle">
|
||||||
|
<!-- 渲染 5 个 tile 的网格点(n=0,1,2,3,4) -->
|
||||||
|
<view
|
||||||
|
v-for="point in visiblePoints"
|
||||||
|
:key="point.key"
|
||||||
|
class="grid-point"
|
||||||
|
:class="{
|
||||||
|
'grid-point--excluded': point.isExcluded,
|
||||||
|
'grid-point--selected': selectedIndex === point.originalIndex
|
||||||
|
}"
|
||||||
|
:style="{
|
||||||
|
left: point.x + 'px',
|
||||||
|
top: point.y + 'px',
|
||||||
|
transform: 'translate(-50%, -50%)'
|
||||||
|
}"
|
||||||
|
@click="selectPoint(point.originalIndex)"
|
||||||
|
>
|
||||||
|
<view class="grid-dot"></view>
|
||||||
|
<text class="grid-label">{{ point.originalIndex }}</text>
|
||||||
|
<text class="grid-coords">({{ Math.round(point.originalX) }}, {{ Math.round(point.originalY) }})</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 控制面板 -->
|
||||||
|
<view class="control-panel" @click.stop>
|
||||||
|
<view class="panel-header">
|
||||||
|
<text class="panel-title">网格调试工具</text>
|
||||||
|
<button class="btn-close" @click="goBack">返回</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 单点调试模式 -->
|
||||||
|
<view v-if="selectedIndex !== null" class="point-edit-section">
|
||||||
|
<view class="point-edit-header">
|
||||||
|
<text class="point-edit-title">编辑点位 #{{ selectedIndex }}</text>
|
||||||
|
<button class="btn-deselect" @click="deselectPoint">取消选择</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="point-edit-controls">
|
||||||
|
<view class="point-edit-row">
|
||||||
|
<text class="point-edit-label">X: {{ currentPoint.x }}</text>
|
||||||
|
<view class="point-edit-buttons">
|
||||||
|
<button class="btn-adjust" @click="adjustPoint('x', -10)">-10</button>
|
||||||
|
<button class="btn-adjust" @click="adjustPoint('x', -1)">-1</button>
|
||||||
|
<button class="btn-adjust" @click="adjustPoint('x', 1)">+1</button>
|
||||||
|
<button class="btn-adjust" @click="adjustPoint('x', 10)">+10</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="point-edit-row">
|
||||||
|
<text class="point-edit-label">Y: {{ currentPoint.y }}</text>
|
||||||
|
<view class="point-edit-buttons">
|
||||||
|
<button class="btn-adjust" @click="adjustPoint('y', -10)">-10</button>
|
||||||
|
<button class="btn-adjust" @click="adjustPoint('y', -1)">-1</button>
|
||||||
|
<button class="btn-adjust" @click="adjustPoint('y', 1)">+1</button>
|
||||||
|
<button class="btn-adjust" @click="adjustPoint('y', 10)">+10</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="point-edit-row">
|
||||||
|
<button class="btn-save-point" @click="savePointAdjustment">保存此点位调整</button>
|
||||||
|
<button class="btn-reset-point" @click="resetPoint">重置此点位</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 全局参数调整 -->
|
||||||
|
<view v-else class="global-controls">
|
||||||
|
<view class="control-section">
|
||||||
|
<text class="control-label">起始Y (startY): {{ config.grid.startY }}</text>
|
||||||
|
<slider
|
||||||
|
:value="config.grid.startY"
|
||||||
|
@change="updateStartY"
|
||||||
|
min="-200"
|
||||||
|
max="100"
|
||||||
|
step="10"
|
||||||
|
show-value
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="control-section">
|
||||||
|
<text class="control-label">垂直间距 (spacingY): {{ config.grid.spacingY }}</text>
|
||||||
|
<slider
|
||||||
|
:value="config.grid.spacingY"
|
||||||
|
@change="updateSpacingY"
|
||||||
|
min="150"
|
||||||
|
max="250"
|
||||||
|
step="5"
|
||||||
|
show-value
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="control-section">
|
||||||
|
<text class="control-label">水平间距 (spacingX): {{ config.grid.spacingX }}</text>
|
||||||
|
<slider
|
||||||
|
:value="config.grid.spacingX"
|
||||||
|
@change="updateSpacingX"
|
||||||
|
min="450"
|
||||||
|
max="600"
|
||||||
|
step="5"
|
||||||
|
show-value
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="control-section">
|
||||||
|
<text class="control-label">交错偏移 (staggerOffsetX): {{ config.grid.staggerOffsetX }}</text>
|
||||||
|
<slider
|
||||||
|
:value="config.grid.staggerOffsetX"
|
||||||
|
@change="updateStaggerOffsetX"
|
||||||
|
min="200"
|
||||||
|
max="350"
|
||||||
|
step="5"
|
||||||
|
show-value
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="control-section">
|
||||||
|
<button class="btn-export" @click="exportConfig">导出配置</button>
|
||||||
|
<button class="btn-reset" @click="resetConfig">重置全部</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="coords-list">
|
||||||
|
<text class="coords-title">生成的坐标列表(点击点位编辑):</text>
|
||||||
|
<scroll-view class="coords-scroll" scroll-y>
|
||||||
|
<view
|
||||||
|
v-for="(coord, i) in originalCoords"
|
||||||
|
:key="i"
|
||||||
|
class="coord-item"
|
||||||
|
:class="{ 'coord-item--selected': selectedIndex === i }"
|
||||||
|
@click="selectPoint(i)"
|
||||||
|
>
|
||||||
|
<text class="coord-index">{{ i }}:</text>
|
||||||
|
<text class="coord-value">[{{ coord.x }}, {{ coord.y }}]</text>
|
||||||
|
<text v-if="coord.x === 0 && coord.y === 0" class="coord-excluded">(排除)</text>
|
||||||
|
<text v-if="hasAdjustment(i)" class="coord-adjusted">(已调整)</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { GRID_CONFIG, generateGridCoordinates, IMAGE_W, IMAGE_H } from './config/cabin.js'
|
||||||
|
|
||||||
|
const config = ref(JSON.parse(JSON.stringify(GRID_CONFIG)))
|
||||||
|
const selectedIndex = ref(null)
|
||||||
|
|
||||||
|
// 屏幕和背景尺寸
|
||||||
|
const screenWidth = ref(375)
|
||||||
|
const screenHeight = ref(812)
|
||||||
|
const tileWidth = ref(375)
|
||||||
|
const scale = ref(1)
|
||||||
|
|
||||||
|
// 滑动相关
|
||||||
|
const bgOffsetX = ref(0)
|
||||||
|
let touchStartX = 0
|
||||||
|
let lastMoveX = 0
|
||||||
|
|
||||||
|
// 原始坐标(未缩放)
|
||||||
|
const originalCoords = computed(() => {
|
||||||
|
return generateGridCoordinates(config.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 缩放后的坐标
|
||||||
|
const scaledCoords = computed(() => {
|
||||||
|
return originalCoords.value.map(coord => ({
|
||||||
|
x: coord.x * scale.value,
|
||||||
|
y: coord.y * scale.value,
|
||||||
|
originalX: coord.x,
|
||||||
|
originalY: coord.y,
|
||||||
|
isExcluded: coord.x === 0 && coord.y === 0
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 可见的网格点(5个tile)
|
||||||
|
const visiblePoints = computed(() => {
|
||||||
|
const w = tileWidth.value
|
||||||
|
const points = []
|
||||||
|
|
||||||
|
for (let n = 0; n <= 4; n++) {
|
||||||
|
scaledCoords.value.forEach((coord, i) => {
|
||||||
|
points.push({
|
||||||
|
key: `${i}-${n}`,
|
||||||
|
x: coord.x + n * w,
|
||||||
|
y: coord.y,
|
||||||
|
originalX: coord.originalX,
|
||||||
|
originalY: coord.originalY,
|
||||||
|
originalIndex: i,
|
||||||
|
isExcluded: coord.isExcluded
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return points
|
||||||
|
})
|
||||||
|
|
||||||
|
// 背景条样式
|
||||||
|
const backgroundStripStyle = computed(() => {
|
||||||
|
const centerOffset = (screenWidth.value - tileWidth.value) / 2
|
||||||
|
return {
|
||||||
|
width: `${tileWidth.value * 3}px`,
|
||||||
|
transform: `translateX(${-tileWidth.value + centerOffset + bgOffsetX.value}px)`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 网格层样式
|
||||||
|
const gridLayerStyle = computed(() => {
|
||||||
|
const centerOffset = (screenWidth.value - tileWidth.value) / 2
|
||||||
|
return {
|
||||||
|
transform: `translateX(${-tileWidth.value + centerOffset + bgOffsetX.value}px)`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前选中点的坐标
|
||||||
|
const currentPoint = computed(() => {
|
||||||
|
if (selectedIndex.value === null) return { x: 0, y: 0 }
|
||||||
|
return originalCoords.value[selectedIndex.value] || { x: 0, y: 0 }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 归一化偏移量
|
||||||
|
const clampOffset = (offset) => {
|
||||||
|
const w = tileWidth.value
|
||||||
|
return (((offset % w) + w) % w) - w
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触摸事件
|
||||||
|
const onTouchStart = (e) => {
|
||||||
|
touchStartX = e.touches[0].clientX
|
||||||
|
lastMoveX = touchStartX
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTouchMove = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const currentX = e.touches[0].clientX
|
||||||
|
const delta = currentX - lastMoveX
|
||||||
|
lastMoveX = currentX
|
||||||
|
|
||||||
|
bgOffsetX.value = clampOffset(bgOffsetX.value + delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTouchEnd = () => {
|
||||||
|
// 简化处理,不添加惯性
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectPoint = (index) => {
|
||||||
|
selectedIndex.value = index
|
||||||
|
console.log('选中点位:', index, originalCoords.value[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
const deselectPoint = () => {
|
||||||
|
selectedIndex.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const adjustPoint = (axis, delta) => {
|
||||||
|
if (selectedIndex.value === null) return
|
||||||
|
|
||||||
|
const idx = selectedIndex.value
|
||||||
|
if (!config.value.manualAdjustments) {
|
||||||
|
config.value.manualAdjustments = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = originalCoords.value[idx]
|
||||||
|
|
||||||
|
if (!config.value.manualAdjustments[idx]) {
|
||||||
|
config.value.manualAdjustments[idx] = { x: current.x, y: current.y }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (axis === 'x') {
|
||||||
|
config.value.manualAdjustments[idx].x += delta
|
||||||
|
} else {
|
||||||
|
config.value.manualAdjustments[idx].y += delta
|
||||||
|
}
|
||||||
|
|
||||||
|
config.value = { ...config.value }
|
||||||
|
}
|
||||||
|
|
||||||
|
const savePointAdjustment = () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: `点位 #${selectedIndex.value} 已保存`,
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
console.log('保存点位调整:', selectedIndex.value, config.value.manualAdjustments[selectedIndex.value])
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetPoint = () => {
|
||||||
|
if (selectedIndex.value === null) return
|
||||||
|
|
||||||
|
if (config.value.manualAdjustments && config.value.manualAdjustments[selectedIndex.value]) {
|
||||||
|
delete config.value.manualAdjustments[selectedIndex.value]
|
||||||
|
config.value = { ...config.value }
|
||||||
|
uni.showToast({
|
||||||
|
title: '点位已重置',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAdjustment = (index) => {
|
||||||
|
return config.value.manualAdjustments && config.value.manualAdjustments[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateStartY = (e) => {
|
||||||
|
config.value.grid.startY = e.detail.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSpacingY = (e) => {
|
||||||
|
config.value.grid.spacingY = e.detail.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSpacingX = (e) => {
|
||||||
|
config.value.grid.spacingX = e.detail.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateStaggerOffsetX = (e) => {
|
||||||
|
config.value.grid.staggerOffsetX = e.detail.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportConfig = () => {
|
||||||
|
const configStr = JSON.stringify(config.value, null, 2)
|
||||||
|
console.log('========== 当前配置 ==========')
|
||||||
|
console.log(configStr)
|
||||||
|
|
||||||
|
const coordsCode = originalCoords.value.map(c => ` [${c.x}, ${c.y}]`).join(',\n')
|
||||||
|
console.log('\n========== 坐标数组 ==========')
|
||||||
|
console.log('[\n' + coordsCode + '\n]')
|
||||||
|
|
||||||
|
if (config.value.manualAdjustments && Object.keys(config.value.manualAdjustments).length > 0) {
|
||||||
|
console.log('\n========== 手动调整 ==========')
|
||||||
|
console.log('manualAdjustments: {')
|
||||||
|
Object.entries(config.value.manualAdjustments).forEach(([idx, adj]) => {
|
||||||
|
console.log(` ${idx}: { x: ${adj.x}, y: ${adj.y} },`)
|
||||||
|
})
|
||||||
|
console.log('}')
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showModal({
|
||||||
|
title: '配置已导出',
|
||||||
|
content: '请查看控制台输出,复制到 config/cabin.js',
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetConfig = () => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认重置',
|
||||||
|
content: '将重置所有参数和调整,是否继续?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
config.value = JSON.parse(JSON.stringify(GRID_CONFIG))
|
||||||
|
selectedIndex.value = null
|
||||||
|
uni.showToast({
|
||||||
|
title: '已重置',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
const init = () => {
|
||||||
|
const info = uni.getSystemInfoSync()
|
||||||
|
screenWidth.value = info.windowWidth
|
||||||
|
screenHeight.value = info.windowHeight
|
||||||
|
tileWidth.value = Math.round(info.windowHeight * (IMAGE_W / IMAGE_H))
|
||||||
|
scale.value = info.windowHeight / IMAGE_H
|
||||||
|
|
||||||
|
console.log('调试页面初始化:', {
|
||||||
|
screenWidth: screenWidth.value,
|
||||||
|
screenHeight: screenHeight.value,
|
||||||
|
tileWidth: tileWidth.value,
|
||||||
|
scale: scale.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
init()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.debug-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100vw;
|
||||||
|
min-height: calc(100vh + 650rpx);
|
||||||
|
overflow: hidden;
|
||||||
|
background: #1a1a1a;
|
||||||
|
padding-bottom: 650rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-strip {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
z-index: 0;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-tile {
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-layer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-point {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 10;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-point--excluded {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-dot {
|
||||||
|
width: 20rpx;
|
||||||
|
height: 20rpx;
|
||||||
|
background: #ff0000;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2rpx solid #ffffff;
|
||||||
|
box-shadow: 0 0 10rpx rgba(255, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-point--selected .grid-dot {
|
||||||
|
background: #00ff00;
|
||||||
|
width: 30rpx;
|
||||||
|
height: 30rpx;
|
||||||
|
box-shadow: 0 0 20rpx rgba(0, 255, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-label {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
padding: 2rpx 8rpx;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-point--selected .grid-label {
|
||||||
|
background: rgba(0, 255, 0, 0.9);
|
||||||
|
color: #000000;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-coords {
|
||||||
|
font-size: 18rpx;
|
||||||
|
color: #00ff00;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
padding: 2rpx 6rpx;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
margin-top: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-top: 2rpx solid #444;
|
||||||
|
padding: 20rpx;
|
||||||
|
max-height: 600rpx;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close {
|
||||||
|
padding: 10rpx 20rpx;
|
||||||
|
background: #ff4444;
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-section {
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-export,
|
||||||
|
.btn-reset {
|
||||||
|
width: 48%;
|
||||||
|
padding: 20rpx;
|
||||||
|
background: #4CAF50;
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
margin-right: 4%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reset {
|
||||||
|
background: #ff9800;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coords-list {
|
||||||
|
margin-top: 30rpx;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coords-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coords-scroll {
|
||||||
|
max-height: 400rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coord-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8rpx 0;
|
||||||
|
border-bottom: 1rpx solid #333;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coord-item--selected {
|
||||||
|
background: rgba(0, 255, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coord-index {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #00ff00;
|
||||||
|
width: 80rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coord-value {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coord-excluded {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #ff0000;
|
||||||
|
margin-left: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coord-adjusted {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #00ff00;
|
||||||
|
margin-left: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-edit-section {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-edit-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-edit-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #00ff00;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-deselect {
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
background: #666;
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-edit-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-edit-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-edit-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-edit-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-adjust {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15rpx;
|
||||||
|
background: #4CAF50;
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save-point,
|
||||||
|
.btn-reset-point {
|
||||||
|
width: 48%;
|
||||||
|
padding: 20rpx;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save-point {
|
||||||
|
background: #2196F3;
|
||||||
|
margin-right: 4%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reset-point {
|
||||||
|
background: #ff5722;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-controls {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user