646 lines
20 KiB
Markdown
646 lines
20 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 | 前 27 个 `[0,0]` 空坐标参与渲染 | 无意义计算 |
|
||
| M4 | 魔法数字散布(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
|
||
|
||
// ========== 小屋类型定义 ==========
|
||
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: 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
|
||
}
|
||
```
|
||
|
||
**注意**:`CABIN_COORDS` 已移除,改用 `generateGridCoordinates()` 动态生成。
|
||
|
||
---
|
||
|
||
### 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` — 可视小屋数组(ref)
|
||
- `currentPage` — 当前页(ref)
|
||
- `scaledCoords` — 缩放后坐标(ref)
|
||
- `cabinRenderDefs` — cabin 渲染尺寸配置
|
||
- `fetchPage(page)` / `ensurePages(center)` / `initCabin(opts)`
|
||
- `resetSquare()` — 重置广场
|
||
|
||
**关键设计**:
|
||
|
||
1. **坐标来源**:使用 `generateGridCoordinates()` 动态生成,不再依赖硬编码
|
||
|
||
2. **buildVisibleCabins 优化**:
|
||
- `bannerBottom` 判断简化:y < bannerBottom → `isAbove = true`,昵称不显示
|
||
- 移位算法(上方有数据的 cabin 移至下方空位)保留
|
||
- 不再使用 `createSelectorQuery`
|
||
|
||
3. **数据更新策略**:
|
||
- 数据更新时增量 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 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>
|
||
<!-- Top3 单独处理,不在 swiper 内 -->
|
||
<view class="top3-area" @click="$emit('top3Click')">
|
||
<BannerTop3 />
|
||
</view>
|
||
</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)保留在页面级
|
||
- 使用 `useCabin` 实现视口网格渲染
|
||
|
||
---
|
||
|
||
## 五、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 }`(在 useSwipe 中实现) |
|
||
|
||
---
|
||
|
||
## 六、性能优化汇总
|
||
|
||
| # | 优化项 | 收益 |
|
||
|---|--------|------|
|
||
| 1 | 视口网格渲染 | DOM 节点减少 90%(72→30) |
|
||
| 2 | 按需加载用户数据 | 内存占用降低 50% |
|
||
| 3 | `aspectFit` 替代 `scaleToFill` | 减少图片重绘 |
|
||
| 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 核心设计
|
||
|
||
#### 架构图
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ 背景图(无限横向平铺) │
|
||
│ ┌──────────────────────────────────┐ │
|
||
│ │ 可视区域(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
|
||
export const GRID_CONFIG = {
|
||
backgroundWidth: 2012,
|
||
backgroundHeight: 1918,
|
||
grid: {
|
||
rows: 11, // 总行数
|
||
cols: 4, // 每行格子数
|
||
startX: -260, // 起始 X(相对于背景图左上角)
|
||
startY: -20, // 起始 Y
|
||
spacingX: 515, // 水平间距
|
||
spacingY: 200, // 垂直间距
|
||
staggered: true, // 奇偶行交错
|
||
staggerOffsetX: 260, // 交错偏移量
|
||
cellWidth: 200, // 格子宽度(用于碰撞检测)
|
||
cellHeight: 150, // 格子高度
|
||
excludeRows: [0, 1, 2], // 排除的行(前 3 行被 banner 遮挡)
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 坐标生成函数
|
||
|
||
```javascript
|
||
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
|
||
}
|
||
```
|
||
|
||
### 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
|
||
export const GRID_CONFIG = {
|
||
// ... base config ...
|
||
manualAdjustments: {
|
||
// index: { x, y }
|
||
39: { x: 1505, y: 520 }, // 第 40 个位置 y 坐标微调
|
||
}
|
||
}
|
||
|
||
export function generateCoordinates(config) {
|
||
const coords = generateGridCoordinates(config)
|
||
|
||
Object.entries(config.manualAdjustments || {}).forEach(([index, adjustment]) => {
|
||
if (coords[index]) {
|
||
coords[index].x = adjustment.x ?? coords[index].x
|
||
coords[index].y = adjustment.y ?? coords[index].y
|
||
}
|
||
})
|
||
|
||
return coords
|
||
}
|
||
```
|
||
|
||
### 7.7 调试工具
|
||
|
||
为了方便调整网格参数,建议添加调试页面:
|
||
|
||
```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, generateGridCoordinates } from './config/cabin.js'
|
||
|
||
const spacingX = ref(GRID_CONFIG.grid.spacingX)
|
||
const debugCoords = computed(() => generateGridCoordinates({
|
||
...GRID_CONFIG,
|
||
grid: { ...GRID_CONFIG.grid, spacingX: spacingX.value }
|
||
}))
|
||
</script>
|
||
```
|
||
|
||
### 7.8 迁移步骤
|
||
|
||
```
|
||
Step 1: 创建 config/cabin.js(包含 GRID_CONFIG + generateGridCoordinates)
|
||
Step 2: 创建 debug-grid.vue 调试工具,调整网格参数对齐背景图
|
||
Step 3: 实现 composables/useCabin.js(使用 generateGridCoordinates)
|
||
Step 4: 实现其他 composables(useSwipe、useBanner、useDialogRotation)
|
||
Step 5: 实现 components(CabinItem、BannerCarousel、NavArrows)
|
||
Step 6: 重写 square.vue,组合所有模块
|
||
Step 7: 功能验证 + iOS/Android 测试
|
||
```
|
||
|
||
### 7.9 优势对比
|
||
|
||
| 维度 | 旧方案(硬编码) | 新方案(网格系统) |
|
||
|------|-----------------|-------------------|
|
||
| 维护成本 | 高(72 个坐标手动维护) | 低(只需调整几个参数) |
|
||
| 扩展性 | 差(增加格子需重新计算) | 好(自动生成) |
|
||
| 背景图更换 | 需重新标注所有坐标 | 只需调整网格参数 |
|
||
| 视觉一致性 | 手动标注易出错 | 自动对齐,一致性高 |
|
||
| 调试效率 | 低(需反复测试) | 高(可视化调试工具) |
|
||
|
||
---
|
||
|
||
## 八、执行顺序
|
||
|
||
```
|
||
Step 1: config/cabin.js(包含网格配置 + 坐标生成)
|
||
Step 2: debug-grid.vue(调试工具)
|
||
Step 3: composables/useCabin.js
|
||
Step 4: composables/useSwipe.js
|
||
Step 5: composables/useBanner.js
|
||
Step 6: composables/useDialogRotation.js
|
||
Step 7: components/CabinItem.vue
|
||
Step 8: components/BannerCarousel.vue
|
||
Step 9: components/NavArrows.vue
|
||
Step 10: square.vue(重写)
|
||
Step 11: 功能验证 + iOS/Android 测试
|
||
```
|