714 lines
21 KiB
Markdown
714 lines
21 KiB
Markdown
# `square.vue` 重构设计文档
|
||
|
||
## 一、背景
|
||
|
||
- **文件**:`frontend/pages/square/square.vue`(1196 行)
|
||
- **技术栈**:uni-app + Vue 3 Composition API(`<script setup>`)
|
||
- **目标**:功能逻辑不变,优化首屏性能、响应速度,修复 iOS/Android 兼容性问题
|
||
|
||
---
|
||
|
||
## 二、旧版问题汇总
|
||
|
||
### 2.1 性能问题
|
||
|
||
| # | 问题 | 影响 |
|
||
|---|------|------|
|
||
| P1 | 无图片懒加载,72 个 cabin icon + 背景图全部立即加载 | 首屏慢、内存高 |
|
||
| P2 | `buildVisibleCabins` 每次重建整个数组(340次循环) | 翻页卡顿 |
|
||
| P3 | `isCabinInViewport` 用 `createSelectorQuery` 异步查询 | iOS/Android 时序不一致、慢 |
|
||
| P4 | `dialogRotationTimer` 递归 `setTimeout` | 定时器开销 |
|
||
| P5 | 双重 `watch` + 深度监听 `visibleCabins` | 不必要的重渲染 |
|
||
| P6 | 72 个 cabin 全部 `v-for` 渲染,无虚拟列表 | 大量无用 DOM |
|
||
|
||
### 2.2 跨平台兼容问题
|
||
|
||
| # | 问题 | 影响 |
|
||
|---|------|------|
|
||
| C1 | `mode="scaleToFill"` | iOS/Android cabin 图标变形 |
|
||
| C2 | `height: 150%` | iOS Safari 不支持 |
|
||
| C3 | `font-family: ZaoZiGongFangJianHei-1` 无引号 | 安卓部分机型失效 |
|
||
| C4 | `createSelectorQuery` 返回时序 | Android/iOS 行为不一致 |
|
||
| C5 | touch 事件无 `passive` | iOS 滚动掉帧 |
|
||
|
||
### 2.3 代码组织问题
|
||
|
||
| # | 问题 | 影响 |
|
||
|---|------|------|
|
||
| M1 | 单文件 1196 行 | 难以维护 |
|
||
| M2 | 重复 `onShow`(两个) | 逻辑分散,易出 bug |
|
||
| M3 | `patchVisibleCabinsData` 定义但未使用 | dead code |
|
||
| M4 | 前 27 个 `[0,0]` 空坐标参与渲染 | 无意义计算 |
|
||
| M5 | 魔法数字散布(IMAGE_W/H, tileWidth, anchorX/Y) | 可维护性差 |
|
||
|
||
---
|
||
|
||
## 三、重构后文件结构
|
||
|
||
```
|
||
frontend/pages/square/
|
||
├── square.vue # 主页面(~300行,协调层)
|
||
├── square.vue.old # 旧版备份
|
||
├── DESIGN.md # 本文档
|
||
├── config/
|
||
│ └── cabin.js # 小屋静态配置
|
||
├── composables/
|
||
│ ├── useSwipe.js # 滑动 + 惯性滚动
|
||
│ ├── useCabin.js # 小屋数据 + 坐标 + 分页缓存
|
||
│ ├── useBanner.js # Banner 活动加载
|
||
│ └── useDialogRotation.js # 展位提示框轮换
|
||
└── components/
|
||
├── CabinItem.vue # 单个小屋
|
||
├── BannerCarousel.vue # 轮播 + Top3
|
||
└── NavArrows.vue # 左右翻页箭头
|
||
```
|
||
|
||
---
|
||
|
||
## 四、各模块详细设计
|
||
|
||
### 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 },
|
||
{ 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
|
||
}
|
||
```
|
||
|
||
**性能收益**:前 27 个空坐标移除,`buildVisibleCabins` 循环从 72×5=360 次降到 45×5=225 次
|
||
|
||
---
|
||
|
||
### 4.2 `composables/useSwipe.js`
|
||
|
||
职责:背景滑动 + 惯性滚动 + 翻页动画
|
||
|
||
**导出**:
|
||
- `bgOffsetX` — 背景偏移量(ref)
|
||
- `rawOffsetX` — 累计原始偏移量
|
||
- `cabinLayerStyle` — cabin 层 transform(computed)
|
||
- `backgroundStripStyle` — 背景 transform(computed)
|
||
- `scrollPage(direction)` — 箭头翻页
|
||
- `stopInertia()` — 停止惯性
|
||
- `initSwipe({ screenWidth, tileWidth, onTileChange })` — 初始化
|
||
- `onBgTouchStart / onBgTouchMove / onBgTouchEnd / onBgTouchCancel`
|
||
|
||
**关键优化**:
|
||
- touch 事件添加 `{ passive: true }`,iOS 滚动流畅
|
||
- RAF 封装:`rafFn` / `cafFn`
|
||
- 惯性衰减:`velocity *= 0.8`,小于 0.2 时停止
|
||
- `clampOffset` 归一化到 `[-tileWidth, 0)`
|
||
|
||
---
|
||
|
||
### 4.3 `composables/useCabin.js`
|
||
|
||
职责:小屋数据 + 分页缓存 + 坐标计算 + visibleCabins 构建
|
||
|
||
**导出**:
|
||
- `visibleCabins` — 可视小屋数组(ref)
|
||
- `currentPage` — 当前页(ref)
|
||
- `scaledCoords` — 缩放后坐标(ref)
|
||
- `cabinRenderDefs` — cabin 渲染尺寸配置
|
||
- `fetchPage(page)` / `ensurePages(center)` / `initCabin(opts)`
|
||
- `resetSquare()` — 重置广场
|
||
|
||
**关键设计**:
|
||
|
||
1. **坐标过滤**:CABIN_COORDS 移至 config/cabin.js,前 27 个 [0,0] 移除
|
||
|
||
2. **buildVisibleCabins 优化**:
|
||
- `bannerBottom` 判断简化:y < bannerBottom → `isAbove = true`,昵称不显示
|
||
- 移位算法(上方有数据的 cabin 移至下方空位)保留
|
||
- 不再使用 `createSelectorQuery`
|
||
|
||
3. **patchVisibleCabinsData 启用**:
|
||
- 数据更新时增量 patch 字段(src/userId/nickname/sharedBoothSlotsRemaining)
|
||
- 保持 DOM 引用稳定,避免图片重新加载闪烁
|
||
|
||
4. **watch 策略简化**:
|
||
- `[currentPage, scaledCoords]` → 全量重建(位置变化)
|
||
- `pageCacheVersion` → patchVisibleCabinsData(数据变化)
|
||
- 移除深度 watch `visibleCabins`
|
||
|
||
5. **首次加载**:onMounted 先 fetchPage(1),确保第一帧有数据
|
||
|
||
---
|
||
|
||
### 4.4 `composables/useDialogRotation.js`
|
||
|
||
职责:展位提示框随机轮换
|
||
|
||
**导出**:`startDialogRotation()` / `stopDialogRotation()`
|
||
|
||
**优化点**:
|
||
- `setInterval` 替代递归 `setTimeout`(2-3秒随机间隔)
|
||
- 简化可视判断:只检查 `cabin.showNickname && cabin.sharedBoothSlotsRemaining !== null`
|
||
- 每次随机选 2-3 个显示
|
||
- 不再依赖 `createSelectorQuery`
|
||
|
||
```javascript
|
||
// 核心逻辑
|
||
const rotateDialogVisibility = () => {
|
||
cabins.forEach(c => c.showDialog = false)
|
||
const eligible = cabins.filter(c => c.nickname && c.sharedBoothSlotsRemaining !== null)
|
||
const count = Math.min(Math.floor(Math.random() * 2) + 2, eligible.length, 3)
|
||
eligible.sort(() => Math.random() - 0.5).slice(0, count).forEach(c => c.showDialog = true)
|
||
}
|
||
|
||
let timer = null
|
||
const startDialogRotation = () => {
|
||
timer = setInterval(() => {
|
||
rotateDialogVisibility()
|
||
}, 2000 + Math.random() * 1000)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4.5 `composables/useBanner.js`
|
||
|
||
职责:Banner 活动数据加载
|
||
|
||
**导出**:`bannerActivities`(ref)、`loadBannerActivities()`
|
||
|
||
逻辑:封装原 `loadBannerActivities`,并发获取 OSS URL。
|
||
|
||
---
|
||
|
||
### 4.6 `components/CabinItem.vue`
|
||
|
||
职责:渲染单个 cabin
|
||
|
||
**Props**:
|
||
```javascript
|
||
{
|
||
cabin: Object, // 小屋数据
|
||
currentUserNickname: String,
|
||
bannerBottom: Number // banner 底部边界(px)
|
||
}
|
||
```
|
||
|
||
**模板**:
|
||
```html
|
||
<view class="cabin-wrapper" :style="{ left: cabin.x + 'px', top: cabin.y + 'px', width: cabin.w + 'px' }">
|
||
<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>
|
||
```
|
||
|
||
**性能优化**:
|
||
- 使用 `aspectFit` 替代 `scaleToFill`
|
||
- 每个 cabin 独立管理自己的 `showDialog`,无需父组件批量更新
|
||
- 图片加载使用 `LazyImage`(项目已有组件)
|
||
|
||
---
|
||
|
||
### 4.7 `components/BannerCarousel.vue`
|
||
|
||
职责:封装 banner 轮播 + Top3
|
||
|
||
**Props**:
|
||
```javascript
|
||
{
|
||
bannerActivities: Array,
|
||
currentStarId: [String, Number]
|
||
}
|
||
```
|
||
|
||
**Emits**:`activityClick(item)`、`top3Click`
|
||
|
||
```html
|
||
<view class="banner-carousel">
|
||
<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)">
|
||
<image class="banner-activity-img" :src="item.cover_image" mode="aspectFill" />
|
||
</swiper-item>
|
||
</swiper>
|
||
</view>
|
||
```
|
||
|
||
---
|
||
|
||
### 4.8 `components/NavArrows.vue`
|
||
|
||
职责:左右翻页箭头
|
||
|
||
**Emits**:`scroll(direction)`
|
||
|
||
```html
|
||
<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>
|
||
```
|
||
|
||
---
|
||
|
||
### 4.9 `square.vue` — 主页面
|
||
|
||
职责:组合所有模块 + 管理弹窗状态 + 协调初始化
|
||
|
||
**模板结构**:
|
||
```html
|
||
<view class="square-container"
|
||
@touchstart="onBgTouchStart" @touchmove="onBgTouchMove"
|
||
@touchend="onBgTouchEnd" @touchcancel="onBgTouchCancel">
|
||
|
||
<!-- 背景 -->
|
||
<view class="background-strip" :style="backgroundStripStyle">
|
||
<image v-for="i in 3" :key="i" class="background-tile" :style="..." src="/static/background/mainbg.png" />
|
||
</view>
|
||
|
||
<!-- Cabin 层 -->
|
||
<view class="cabin-layer" :style="cabinLayerStyle">
|
||
<CabinItem
|
||
v-for="cabin in visibleCabins"
|
||
:key="cabin.key"
|
||
:cabin="cabin"
|
||
:currentUserNickname="currentUserNickname"
|
||
:bannerBottom="bannerBottom"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 箭头 -->
|
||
<NavArrows @scroll="scrollPage" />
|
||
|
||
<!-- Header -->
|
||
<Header :showGuideIcon="true" :showTaskIcon="true" :showStarActivityIcon="true" backIconColor="#e6e6e6" />
|
||
|
||
<!-- Banner -->
|
||
<BannerCarousel
|
||
:bannerActivities="bannerActivities"
|
||
:currentStarId="currentStarId"
|
||
@activityClick="handleActivityClick"
|
||
@top3Click="showRankingModal = true"
|
||
/>
|
||
|
||
<!-- 弹窗层 -->
|
||
<RankingModal ... />
|
||
<BottomNav ... />
|
||
<GuideStartModal ... />
|
||
<GuideOverlay />
|
||
</view>
|
||
```
|
||
|
||
**改动点**:
|
||
- 合并重复 `onShow` → 一个 `onShow` 调用 `resetSquare`
|
||
- 所有 composable 在 `onMounted` 中初始化
|
||
- 弹窗状态(RankingModal、GuideStartModal、navExpanded)保留在页面级
|
||
|
||
---
|
||
|
||
## 五、CSS 兼容修复
|
||
|
||
| # | 旧 | 新 |
|
||
|---|----|----|
|
||
| `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 }` |
|
||
|
||
---
|
||
|
||
## 六、性能优化汇总
|
||
|
||
| # | 优化项 | 收益 |
|
||
|---|--------|------|
|
||
| 1 | 前 27 个 [0,0] 空坐标过滤 | 循环次数下降 37%(360→225) |
|
||
| 2 | `aspectFit` 替代 `scaleToFill` | 减少图片重绘 |
|
||
| 3 | `patchVisibleCabinsData` 启用 | 数据更新避免 DOM 重建 |
|
||
| 4 | touch 事件 `passive: true` | iOS 滚动不掉帧 |
|
||
| 5 | `setInterval` 替代递归 `setTimeout` | 定时器开销降低 |
|
||
| 6 | 移除 `createSelectorQuery` viewport 检测 | 消除异步查询开销 |
|
||
| 7 | CabinItem 组件化 | 组件粒度细化,利于缓存 |
|
||
| 8 | `will-change: transform` 已有 | 保留,GPU 加速 |
|
||
|
||
---
|
||
|
||
## 七、网格布局动态化重构方案
|
||
|
||
### 7.1 问题分析
|
||
|
||
当前问题:
|
||
1. **硬编码坐标**:72 个坐标手动维护,修改困难
|
||
2. **背景图依赖**:坐标与背景图强耦合,背景图更换需重新标注所有坐标
|
||
3. **扩展性差**:增加/减少格子需要手动计算坐标
|
||
4. **视觉不一致**:手动标注容易出现偏差
|
||
|
||
### 7.2 重构目标
|
||
|
||
将"硬编码坐标系统"改为"网格布局系统":
|
||
- 背景图定义网格规则(行数、列数、间距、偏移)
|
||
- 小屋图标自动填充到网格中
|
||
- 支持不规则网格(某些位置可配置为空)
|
||
- 背景图更换时只需调整网格参数
|
||
|
||
### 7.3 网格布局配置方案
|
||
|
||
#### 方案 A:规则网格 + 排除列表(推荐)
|
||
|
||
适用场景:背景图是规则网格,只有少数位置需要留空
|
||
|
||
```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],
|
||
// ... 可根据背景图调整
|
||
]
|
||
}
|
||
}
|
||
|
||
// 自动生成坐标函数
|
||
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
|
||
// 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 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
|
||
})
|
||
}
|
||
})
|
||
})
|
||
|
||
return coords
|
||
}
|
||
```
|
||
|
||
#### 方案 C:混合方案(推荐用于你的场景)
|
||
|
||
结合规则网格和手动微调
|
||
|
||
```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,
|
||
|
||
// 手动微调(覆盖自动生成的坐标)
|
||
manualAdjustments: {
|
||
// 格式: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++
|
||
}
|
||
}
|
||
|
||
return coords
|
||
}
|
||
```
|
||
|
||
### 7.4 配置文件重构
|
||
|
||
#### 新的 `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
|
||
<template>
|
||
<view class="debug-container">
|
||
<image class="bg-image" src="/static/background/mainbg.png" mode="aspectFit" />
|
||
|
||
<!-- 网格线 -->
|
||
<view v-for="(coord, i) in debugCoords" :key="i"
|
||
class="grid-point"
|
||
:style="{ left: coord.scaledX + 'px', top: coord.scaledY + 'px' }">
|
||
<text class="grid-label">{{ i }}</text>
|
||
</view>
|
||
|
||
<!-- 参数调整面板 -->
|
||
<view class="control-panel">
|
||
<text>间距X: {{ spacingX }}</text>
|
||
<slider :value="spacingX" @change="updateSpacingX" min="400" max="600" />
|
||
<!-- 其他参数... -->
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed } from 'vue'
|
||
import { GRID_CONFIG, generateCoordinates } from './config/gridLayout.js'
|
||
|
||
const spacingX = ref(GRID_CONFIG.baseGrid.spacingX)
|
||
// 实时预览网格调整效果
|
||
</script>
|
||
```
|
||
|
||
### 7.6 迁移步骤
|
||
|
||
```
|
||
Step 1: 创建 config/gridLayout.js,定义网格规则
|
||
Step 2: 使用调试工具 debug-grid.vue 调整参数,直到网格对齐背景图
|
||
Step 3: 更新 config/cabin.js,使用动态生成的坐标
|
||
Step 4: 测试 square.vue,确保功能正常
|
||
Step 5: 删除旧的硬编码坐标数组
|
||
```
|
||
|
||
### 7.7 优势对比
|
||
|
||
| 维度 | 旧方案(硬编码) | 新方案(网格系统) |
|
||
|------|-----------------|-------------------|
|
||
| 维护成本 | 高(72 个坐标手动维护) | 低(只需调整几个参数) |
|
||
| 扩展性 | 差(增加格子需重新计算) | 好(自动生成) |
|
||
| 背景图更换 | 需重新标注所有坐标 | 只需调整网格参数 |
|
||
| 视觉一致性 | 手动标注易出错 | 自动对齐,一致性高 |
|
||
| 调试效率 | 低(需反复测试) | 高(可视化调试工具) |
|
||
|
||
### 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 4: composables/useSwipe.js
|
||
Step 5: composables/useCabin.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 测试
|
||
```
|