1753 lines
40 KiB
Markdown
1753 lines
40 KiB
Markdown
# uni-app-x 页面修复指南
|
||
|
||
## 📋 文档概述
|
||
|
||
本文档总结了 uni-app-x 项目中页面配置和编译错误的完整修复流程,旨在为后续开发提供标准化的解决方案和最佳实践。
|
||
|
||
## 🔍 问题根源分析
|
||
|
||
### 1. 初始错误现象
|
||
|
||
```
|
||
[plugin:uni:h5-pages-json] 页面"minimal"不存在,请确保填写的页面路径不包含文件后缀,且必须与真实的文件路径大小写保持一致。
|
||
```
|
||
|
||
```
|
||
SyntaxError: The requested module '.../vue.runtime.esm.js' does not provide an export named 'onLoad'
|
||
```
|
||
|
||
### 2. 根本原因总结
|
||
|
||
#### **原因一:pages.json 路径配置错误**
|
||
|
||
- **错误格式**: `"path": "pages/minimal"` (主页面缺少完整路径)
|
||
- **错误格式**: `"path": "pages/mall/admin/index"` (主页面错误包含完整路径)
|
||
- **错误格式**: 子包路径配置不完整
|
||
|
||
#### **原因二:AdminLayout 组件语法错误**
|
||
|
||
- **重复闭合标签**: `<script>` 标签重复闭合
|
||
- **生命周期导入错误**: 从 Vue 导入 uni-app 生命周期钩子
|
||
|
||
#### **原因三:页面跳转路径格式不一致**
|
||
|
||
- **错误格式**: `url: 'pages/mall/admin/user-management'` (缺少前缀 `/`)
|
||
- **错误格式**: `url: '/pages/mall/admin/user-management'` (某些情况下不适用)
|
||
|
||
#### **原因四:组件依赖和导入问题**
|
||
|
||
- AdminLayout 组件未正确导入
|
||
- 类型定义缺失
|
||
- 响应式数据使用错误
|
||
|
||
#### **原因五:特殊字符解析错误**
|
||
|
||
- **Emoji 字符兼容性**: uni-app-x 模板解析器对某些 emoji 字符支持不完整
|
||
- **Unicode 符号问题**: 某些特殊 Unicode 符号可能导致 "Invalid end tag" 编译错误
|
||
- **字符编码问题**: 文件编码或不可见字符可能影响模板解析
|
||
|
||
#### **原因六:缩进不一致错误**
|
||
|
||
- **混合缩进**: 同一文件中混用制表符和空格缩进
|
||
- **不一致的结束标签**: 开始标签和结束标签使用不同的缩进方式
|
||
- **Vue 解析器敏感性**: Vue 模板解析器对缩进不一致特别敏感
|
||
|
||
#### **原因七:方法调用错误**
|
||
|
||
- **方法重命名**: 重构时方法名改变但模板中未更新引用
|
||
- **未定义方法**: 模板中调用不存在的方法
|
||
- **参数不匹配**: 方法调用时的参数与定义不一致
|
||
|
||
#### **原因八:自闭合标签错误**
|
||
|
||
- **错误格式**: 使用 `></tag>` 而不是 `/>` 结束自闭合标签
|
||
- **Vue 规范**: 自闭合标签必须使用 `/>` 结尾
|
||
- **编译器敏感**: uni-app-x 对标签格式要求严格
|
||
|
||
#### **原因九:模态框嵌套问题**
|
||
|
||
- **条件嵌套**: 模态框被放在条件渲染内部,可能导致显示异常
|
||
- **作用域限制**: 模态框应该在所有条件外部,全局可用
|
||
- **层级问题**: 模态框需要最高层级显示,不应受页面模式影响
|
||
|
||
#### **原因十:.uvue文件特殊字符兼容性**
|
||
|
||
- **UTS编译器敏感性**: uni-app-x的.uvue文件对特殊字符更敏感
|
||
- **Unicode符号解析**: 某些Unicode符号可能导致UTS编译器解析失败
|
||
- **文件复杂度**: 复杂的模板结构可能超出UTS编译器的处理能力
|
||
|
||
#### **原因五:特殊字符解析错误**
|
||
|
||
- **Emoji 字符问题**: uni-app-x 模板解析器对某些 emoji 字符支持不完整
|
||
- **Unicode 字符**: 某些特殊 Unicode 符号可能导致 "Invalid end tag" 错误
|
||
- **不可见字符**: BOM 或其他不可见字符可能影响模板解析
|
||
|
||
## 🛠️ 完整修复流程
|
||
|
||
### **阶段一:基础配置简化**
|
||
|
||
#### **步骤 1.1:创建最小可用配置**
|
||
|
||
```json
|
||
// pages.json - 最简配置
|
||
{
|
||
"pages": [
|
||
{
|
||
"path": "pages/minimal",
|
||
"style": {
|
||
"navigationBarTitleText": "最小测试"
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**验证**: 确保基础页面能够正常编译和显示。
|
||
|
||
#### **步骤 1.2:验证文件完整性**
|
||
|
||
```bash
|
||
# 检查页面文件是否存在
|
||
pages/minimal.uvue ✅
|
||
pages/mall/admin/index.uvue ✅
|
||
pages/mall/admin/user-management.uvue ✅
|
||
```
|
||
|
||
### **阶段二:AdminLayout 组件修复**
|
||
|
||
#### **步骤 2.1:修复语法错误**
|
||
|
||
```vue
|
||
<!-- 错误:重复的 </script> 标签 -->
|
||
<script setup lang="uts">
|
||
import { ref, computed } from 'vue'
|
||
// ... 代码 ...
|
||
</script>
|
||
</script> <!-- ❌ 多余的闭合标签 -->
|
||
|
||
<!-- 正确 -->
|
||
<script setup lang="uts">
|
||
import { ref, computed } from 'vue'
|
||
// ... 代码 ...
|
||
</script>
|
||
```
|
||
|
||
#### **步骤 2.2:修复生命周期导入**
|
||
|
||
```javascript
|
||
// ❌ 错误:从 Vue 导入 uni-app 生命周期
|
||
import { ref, computed, onLoad } from "vue";
|
||
|
||
// ✅ 正确:uni-app-x 生命周期全局可用
|
||
import { ref, computed } from "vue";
|
||
|
||
// 在组件中直接使用
|
||
onLoad(() => {
|
||
activeMenu.value = props.currentPage;
|
||
});
|
||
```
|
||
|
||
#### **步骤 2.3:验证组件结构**
|
||
|
||
```vue
|
||
<template>
|
||
<view class="admin-layout">
|
||
<!-- 侧边栏 -->
|
||
<view class="admin-sidebar">
|
||
<!-- 菜单列表 -->
|
||
</view>
|
||
|
||
<!-- 主内容区 -->
|
||
<view class="main-container">
|
||
<!-- 页面头部 -->
|
||
<!-- 页面内容 -->
|
||
<slot></slot>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
```
|
||
|
||
### **阶段三:页面配置恢复**
|
||
|
||
#### **步骤 3.1:恢复主页面配置**
|
||
|
||
```json
|
||
{
|
||
"pages": [
|
||
{
|
||
"path": "pages/minimal",
|
||
"style": {
|
||
"navigationBarTitleText": "最小测试"
|
||
}
|
||
},
|
||
{
|
||
"path": "pages/mall/admin/index",
|
||
"style": {
|
||
"navigationBarTitleText": "管理后台",
|
||
"navigationStyle": "custom"
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### **步骤 3.2:恢复子包配置**
|
||
|
||
```json
|
||
{
|
||
"subPackages": [
|
||
{
|
||
"root": "pages/mall",
|
||
"pages": [
|
||
{
|
||
"path": "admin/user-management",
|
||
"style": {
|
||
"navigationBarTitleText": "用户管理",
|
||
"navigationStyle": "custom"
|
||
}
|
||
}
|
||
// ... 其他页面
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### **步骤 3.3:验证完整配置**
|
||
|
||
```json
|
||
{
|
||
"pages": [
|
||
{
|
||
"path": "pages/minimal",
|
||
"style": { "navigationBarTitleText": "最小测试" }
|
||
},
|
||
{
|
||
"path": "pages/mall/admin/index",
|
||
"style": {
|
||
"navigationBarTitleText": "管理后台",
|
||
"navigationStyle": "custom"
|
||
}
|
||
}
|
||
],
|
||
"subPackages": [
|
||
{
|
||
"root": "pages/mall",
|
||
"pages": [
|
||
{
|
||
"path": "admin/user-management",
|
||
"style": {
|
||
"navigationBarTitleText": "用户管理",
|
||
"navigationStyle": "custom"
|
||
}
|
||
},
|
||
{
|
||
"path": "admin/product-management",
|
||
"style": {
|
||
"navigationBarTitleText": "商品管理",
|
||
"navigationStyle": "custom"
|
||
}
|
||
},
|
||
{
|
||
"path": "admin/order-management",
|
||
"style": {
|
||
"navigationBarTitleText": "订单管理",
|
||
"navigationStyle": "custom"
|
||
}
|
||
},
|
||
{
|
||
"path": "admin/finance-management",
|
||
"style": {
|
||
"navigationBarTitleText": "财务管理",
|
||
"navigationStyle": "custom"
|
||
}
|
||
},
|
||
{
|
||
"path": "admin/system-settings",
|
||
"style": {
|
||
"navigationBarTitleText": "系统设置",
|
||
"navigationStyle": "custom"
|
||
}
|
||
}
|
||
]
|
||
}
|
||
],
|
||
"globalStyle": {
|
||
"navigationBarTextStyle": "black",
|
||
"navigationBarTitleText": "mall",
|
||
"navigationBarBackgroundColor": "#FFFFFF",
|
||
"backgroundColor": "#F8F8F8"
|
||
}
|
||
}
|
||
```
|
||
|
||
### **阶段四:路径格式统一**
|
||
|
||
### **阶段五:特殊字符处理**
|
||
|
||
#### **步骤 5.1:移除问题 Emoji 字符**
|
||
|
||
```vue
|
||
<!-- ❌ 问题:某些 emoji 字符可能导致 "Invalid end tag" 错误 -->
|
||
<template>
|
||
<view class="stats-cards">
|
||
<view class="stat-card">
|
||
<view class="stat-icon">👥</view>
|
||
<!-- 可能导致解析错误 -->
|
||
<view class="stat-content">
|
||
<text class="stat-value">{{ totalUsers }}</text>
|
||
<text class="stat-label">总用户数</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- ✅ 修复:使用安全字符替代 -->
|
||
<template>
|
||
<view class="stats-cards">
|
||
<view class="stat-card">
|
||
<view class="stat-icon">👤</view>
|
||
<!-- 使用安全字符 -->
|
||
<view class="stat-content">
|
||
<text class="stat-value">{{ totalUsers }}</text>
|
||
<text class="stat-label">总用户数</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
```
|
||
|
||
**常见问题 Emoji 及替代方案**:
|
||
|
||
```javascript
|
||
// 统计图标
|
||
'👥' → '👤' // 用户数量
|
||
'✅' → '✓' // 成功/活跃
|
||
'🚫' → '✗' // 禁用/下架
|
||
'📦' → '□' // 商品/包裹
|
||
'⏳' → '○' // 等待/处理中
|
||
'🚚' → '→' // 配送中
|
||
'⚠️' → '!' // 警告/库存不足
|
||
|
||
// 财务图标
|
||
'💰' → '$' // 收入
|
||
'📈' → '↑' // 增长
|
||
'📊' → '≡' // 图表
|
||
'💳' → '■' // 账户
|
||
```
|
||
|
||
#### **步骤 5.2:检查文件编码**
|
||
|
||
```bash
|
||
# 检查文件编码和特殊字符
|
||
file pages/mall/admin/*.uvue
|
||
|
||
# 确保文件编码为 UTF-8 无 BOM
|
||
# 移除任何不可见字符
|
||
```
|
||
|
||
#### **步骤 5.3:验证模板语法完整性**
|
||
|
||
```vue
|
||
<!-- ✅ 确保所有标签正确闭合 -->
|
||
<template>
|
||
<AdminLayout current-page="user-list">
|
||
<view class="user-management">
|
||
<!-- 所有开始标签都有对应的结束标签 -->
|
||
<view v-if="condition">内容</view>
|
||
<view v-for="item in items" :key="item.id">
|
||
{{ item.name }}
|
||
</view>
|
||
</view>
|
||
</AdminLayout>
|
||
</template>
|
||
```
|
||
|
||
#### **步骤 5.4:最终验证**
|
||
|
||
### **阶段六:缩进一致性检查**
|
||
|
||
#### **步骤 6.1:统一缩进方式**
|
||
|
||
```vue
|
||
<!-- ❌ 错误:混合使用制表符和空格 -->
|
||
<template>
|
||
<AdminLayout current-page="user-list">
|
||
<view class="user-management">
|
||
<!-- 内容 -->
|
||
</view> <!-- 制表符缩进 -->
|
||
</view> <!-- 制表符缩进 -->
|
||
</view> <!-- 制表符缩进 -->
|
||
</AdminLayout>
|
||
</template>
|
||
|
||
<!-- ✅ 正确:统一使用空格缩进 -->
|
||
<template>
|
||
<AdminLayout current-page="user-list">
|
||
<view class="user-management">
|
||
<!-- 内容 -->
|
||
</view>
|
||
</AdminLayout>
|
||
</template>
|
||
```
|
||
|
||
#### **步骤 6.2:检查缩进工具**
|
||
|
||
```bash
|
||
# 检查文件中是否存在制表符
|
||
grep -P '\t' pages/mall/admin/*.uvue
|
||
|
||
# 将制表符转换为空格(2个空格)
|
||
sed -i 's/\t/ /g' pages/mall/admin/*.uvue
|
||
```
|
||
|
||
#### **步骤 6.3:验证标签闭合**
|
||
|
||
```javascript
|
||
// 检查所有开始标签都有对应结束标签
|
||
// 检查缩进一致性
|
||
// 确保没有多余的闭合标签
|
||
```
|
||
|
||
#### **步骤 6.4:最终验证**
|
||
|
||
### **阶段七:方法调用检查**
|
||
|
||
#### **步骤 7.1:检查方法引用**
|
||
|
||
```javascript
|
||
// ❌ 错误:方法名已更改但模板未更新
|
||
<template>
|
||
<button @click="oldMethodName()">按钮</button>
|
||
</template>
|
||
|
||
// ✅ 正确:更新方法引用
|
||
<template>
|
||
<button @click="newMethodName()">按钮</button>
|
||
</template>
|
||
```
|
||
|
||
#### **步骤 7.2:验证方法定义**
|
||
|
||
```javascript
|
||
// 确保所有模板中引用的方法都已定义
|
||
const newMethodName = () => {
|
||
// 方法实现
|
||
};
|
||
```
|
||
|
||
#### **步骤 7.3:检查参数匹配**
|
||
|
||
```javascript
|
||
// 确保方法调用时的参数与定义一致
|
||
const handleClick = (param: string) => {
|
||
console.log(param)
|
||
}
|
||
|
||
// 模板调用
|
||
<button @click="handleClick('value')">按钮</button>
|
||
```
|
||
|
||
#### **步骤 7.4:最终验证**
|
||
|
||
### **阶段八:自闭合标签检查**
|
||
|
||
#### **步骤 8.1:检查自闭合标签格式**
|
||
|
||
```vue
|
||
<!-- ❌ 错误:错误的结束格式 -->
|
||
<input v-model="value" />
|
||
<image :src="url"></image>
|
||
<!-- 错误 -->
|
||
<checkbox :checked="checked"></checkbox>
|
||
<!-- 错误 -->
|
||
|
||
<!-- ✅ 正确:标准自闭合格式 -->
|
||
<input v-model="value" />
|
||
<image :src="url" />
|
||
<!-- 正确 -->
|
||
<checkbox :checked="checked" />
|
||
<!-- 正确 -->
|
||
```
|
||
|
||
#### **步骤 8.2:自动修复工具**
|
||
|
||
```bash
|
||
# 使用 sed 批量修复(注意备份文件)
|
||
sed -i 's/><\/image>/ \/>/g' pages/mall/admin/*.uvue
|
||
sed -i 's/><\/checkbox>/ \/>/g' pages/mall/admin/*.uvue
|
||
```
|
||
|
||
#### **步骤 8.3:验证标签完整性**
|
||
|
||
```javascript
|
||
// 检查所有自闭合标签都正确结束
|
||
// 确保没有多余的结束标签
|
||
```
|
||
|
||
### **阶段九:模态框位置优化**
|
||
|
||
#### **步骤 9.1:识别模态框位置问题**
|
||
|
||
```vue
|
||
<!-- ❌ 错误:模态框嵌套在条件内部 -->
|
||
<template>
|
||
<view>
|
||
<view v-if="currentMode === 'list'">
|
||
<!-- 列表内容 -->
|
||
<view v-if="showModal" class="modal">
|
||
<!-- 条件嵌套 -->
|
||
模态框内容
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- ✅ 正确:模态框在条件外部 -->
|
||
<template>
|
||
<view>
|
||
<view v-if="currentMode === 'list'">
|
||
<!-- 列表内容 -->
|
||
</view>
|
||
|
||
<!-- 模态框在外部 -->
|
||
<view v-if="showModal" class="modal"> 模态框内容 </view>
|
||
</view>
|
||
</template>
|
||
```
|
||
|
||
#### **步骤 9.2:重构模态框位置**
|
||
|
||
```vue
|
||
<template>
|
||
<view class="page-container">
|
||
<!-- 主要内容区域 -->
|
||
<view v-if="currentMode === 'list'" class="content">
|
||
<!-- 内容 -->
|
||
</view>
|
||
|
||
<view v-else-if="currentMode === 'form'" class="content">
|
||
<!-- 表单 -->
|
||
</view>
|
||
|
||
<!-- 模态框:放在最后,全局可用 -->
|
||
<Modal v-if="showAddModal" @close="closeModal"> 添加内容 </Modal>
|
||
|
||
<Modal v-if="showDeleteModal" @close="closeModal"> 删除确认 </Modal>
|
||
</view>
|
||
</template>
|
||
```
|
||
|
||
#### **步骤 9.3:验证模态框行为**
|
||
|
||
```javascript
|
||
// 确保模态框在所有页面模式下都能正常显示
|
||
// 测试不同条件下的模态框显示
|
||
```
|
||
|
||
#### **步骤 9.4:最终验证**
|
||
|
||
### **阶段十:.uvue文件特殊处理**
|
||
|
||
#### **步骤 10.1:简化复杂模板**
|
||
|
||
```vue
|
||
<!-- ❌ 复杂模板可能导致UTS编译器问题 -->
|
||
<template>
|
||
<AdminLayout current-page="user-list">
|
||
<view class="complex-layout">
|
||
<!-- 复杂的嵌套结构、大量条件渲染、复杂表达式 -->
|
||
<view v-if="complexCondition" v-for="item in largeArray">
|
||
<!-- 大量内容 -->
|
||
</view>
|
||
</view>
|
||
</AdminLayout>
|
||
</template>
|
||
|
||
<!-- ✅ 简化模板结构 -->
|
||
<template>
|
||
<AdminLayout current-page="user-list">
|
||
<view class="simple-layout">
|
||
<text>简化内容</text>
|
||
</view>
|
||
</AdminLayout>
|
||
</template>
|
||
```
|
||
|
||
#### **步骤 10.2:移除问题Unicode字符**
|
||
|
||
```vue
|
||
<!-- ❌ .uvue文件中可能导致编译错误的字符 -->
|
||
<view class="icon">✓</view>
|
||
<!-- 对勾符号 -->
|
||
<view class="icon">✗</view>
|
||
<!-- 叉号符号 -->
|
||
<view class="icon">★</view>
|
||
<!-- 星号符号 -->
|
||
|
||
<!-- ✅ 使用安全字符 -->
|
||
<view class="icon">Y</view>
|
||
<!-- 字母Y -->
|
||
<view class="icon">N</view>
|
||
<!-- 字母N -->
|
||
<view class="icon">*</view>
|
||
<!-- 星号 -->
|
||
```
|
||
|
||
#### **步骤 10.3:UTS编译器兼容性检查**
|
||
|
||
```javascript
|
||
// 检查UTS特定的语法要求
|
||
// 确保所有导入和类型定义正确
|
||
// 验证组件Props和事件处理
|
||
```
|
||
|
||
#### **步骤 10.4:渐进式开发策略**
|
||
|
||
```javascript
|
||
// 1. 从最小可用模板开始
|
||
const minimalTemplate = `
|
||
<template>
|
||
<AdminLayout current-page="page">
|
||
<view class="page">
|
||
<text>页面内容</text>
|
||
</view>
|
||
</AdminLayout>
|
||
</template>
|
||
`;
|
||
|
||
// 2. 逐步添加功能,每次只添加一个特性
|
||
// 3. 每次修改后立即编译测试
|
||
// 4. 出现问题时立即回滚到上一个稳定版本
|
||
```
|
||
|
||
#### **步骤 10.5:最终验证**
|
||
|
||
### **阶段十一:批量.uvue文件修复**
|
||
|
||
### **阶段十二:AdminLayout双侧边栏布局**
|
||
|
||
#### **步骤 11.1:识别问题文件**
|
||
|
||
```bash
|
||
# 检查所有.uvue文件是否正常编译
|
||
# 记录出现"Invalid end tag"错误的文件
|
||
# 按优先级排序修复顺序
|
||
```
|
||
|
||
#### **步骤 11.2:批量简化模板**
|
||
|
||
```javascript
|
||
// 为每个问题文件创建最小可用模板
|
||
const minimalTemplates = {
|
||
"user-management.uvue": `
|
||
<template>
|
||
<AdminLayout current-page="user-list">
|
||
<view class="user-management">
|
||
<text>用户管理</text>
|
||
</view>
|
||
</AdminLayout>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref } from 'vue'
|
||
import AdminLayout from '@/layouts/admin/index.uvue'
|
||
const currentMode = ref('list')
|
||
</script>
|
||
|
||
<style>
|
||
.user-management { padding: 20px; }
|
||
</style>
|
||
`,
|
||
|
||
"product-management.uvue": `
|
||
<template>
|
||
<AdminLayout current-page="product-management">
|
||
<view class="product-management">
|
||
<text>商品管理</text>
|
||
</view>
|
||
</AdminLayout>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref } from 'vue'
|
||
import AdminLayout from '@/layouts/admin/index.uvue'
|
||
const currentMode = ref('list')
|
||
</script>
|
||
|
||
<style>
|
||
.product-management { padding: 20px; }
|
||
</style>
|
||
`,
|
||
|
||
// 为其他页面创建类似的模板
|
||
};
|
||
```
|
||
|
||
#### **步骤 11.3:统一字符替换**
|
||
|
||
```bash
|
||
# 批量替换所有.uvue文件中的问题字符
|
||
find pages/mall/admin -name "*.uvue" -exec sed -i \
|
||
-e 's/👤/U/g' \
|
||
-e 's/✓/Y/g' \
|
||
-e 's/✗/N/g' \
|
||
-e 's/★/*/g' \
|
||
-e 's/📦/□/g' \
|
||
-e 's/⏳/○/g' \
|
||
-e 's/🚚/→/g' \
|
||
{} \;
|
||
```
|
||
|
||
#### **步骤 11.4:验证批量修复**
|
||
|
||
```javascript
|
||
// 检查所有文件是否正常编译
|
||
// 确认没有"Invalid end tag"错误
|
||
// 测试基本页面导航功能
|
||
```
|
||
|
||
#### **步骤 11.5:最终验证**
|
||
|
||
#### **步骤 12.1:设计双侧边栏布局**
|
||
|
||
```vue
|
||
<!-- ❌ 传统布局:二级菜单在主侧边栏内部 -->
|
||
<template>
|
||
<view class="admin-layout">
|
||
<view class="main-sider">
|
||
<!-- 一级菜单 -->
|
||
<view class="menu-primary"><!-- ... --></view>
|
||
<!-- 二级菜单(错误位置) -->
|
||
<view class="menu-secondary"><!-- ... --></view>
|
||
</view>
|
||
<view class="main-content">
|
||
<!-- 页面内容 -->
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- ✅ 双侧边栏布局:二级菜单在内容区左侧 -->
|
||
<template>
|
||
<view class="admin-layout">
|
||
<!-- 主侧边栏:只显示一级菜单 -->
|
||
<view class="admin-sider">
|
||
<view class="menu-primary"><!-- 一级菜单 --></view>
|
||
</view>
|
||
|
||
<!-- 主内容区 -->
|
||
<view class="admin-main">
|
||
<!-- 内容侧边栏:显示二级菜单 -->
|
||
<view class="content-sider" v-if="hasSubMenus">
|
||
<view class="content-sider-content">
|
||
<!-- 二级菜单 -->
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 内容区域 -->
|
||
<view class="content-area">
|
||
<!-- 页面内容 -->
|
||
<slot></slot>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
```
|
||
|
||
#### **步骤 12.2:实现菜单数据结构**
|
||
|
||
```javascript
|
||
const menuList = ref([
|
||
{
|
||
id: "user",
|
||
title: "用户管理",
|
||
icon: "icon-user",
|
||
path: "/pages/mall/admin/user-management",
|
||
subMenus: [
|
||
{
|
||
id: "user-list",
|
||
title: "用户列表",
|
||
path: "/pages/mall/admin/user-management",
|
||
},
|
||
{
|
||
id: "user-add",
|
||
title: "添加用户",
|
||
path: "/pages/mall/admin/user-management?action=add",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "product",
|
||
title: "商品管理",
|
||
icon: "icon-product",
|
||
path: "/pages/mall/admin/product-management",
|
||
subMenus: [
|
||
{
|
||
id: "product-list",
|
||
title: "商品列表",
|
||
path: "/pages/mall/admin/product-management",
|
||
},
|
||
{
|
||
id: "category",
|
||
title: "商品分类",
|
||
path: "/pages/mall/admin/product-management?tab=category",
|
||
},
|
||
],
|
||
},
|
||
// 没有子菜单的页面
|
||
{
|
||
id: "statistics",
|
||
title: "用户统计",
|
||
icon: "icon-statistics",
|
||
path: "/pages/mall/admin/user-statistics",
|
||
},
|
||
]);
|
||
```
|
||
|
||
#### **步骤 12.3:计算属性实现**
|
||
|
||
```javascript
|
||
// 计算当前菜单的子菜单
|
||
const activeSubMenus = computed(() => {
|
||
const menu = menuList.value.find((m) => m.id === activeMenu.value);
|
||
return menu ? menu.subMenus || [] : [];
|
||
});
|
||
|
||
// 判断是否有子菜单
|
||
const hasSubMenus = computed(() => {
|
||
return activeSubMenus.value.length > 0;
|
||
});
|
||
```
|
||
|
||
#### **步骤 12.4:菜单点击处理**
|
||
|
||
```javascript
|
||
const handleMenuClick = (menu: any) => {
|
||
activeMenu.value = menu.id
|
||
// 设置默认子菜单(如果有的话)
|
||
activeSubMenu.value = menu.subMenus && menu.subMenus.length > 0 ? menu.subMenus[0].id : ''
|
||
|
||
// 导航到菜单路径
|
||
uni.navigateTo({
|
||
url: menu.path
|
||
})
|
||
}
|
||
|
||
const handleSubMenuClick = (subMenu: any) => {
|
||
activeSubMenu.value = subMenu.id
|
||
// 导航到子菜单路径(可能包含查询参数)
|
||
uni.navigateTo({
|
||
url: subMenu.path
|
||
})
|
||
}
|
||
```
|
||
|
||
#### **步骤 12.5:样式实现**
|
||
|
||
```scss
|
||
/* 主侧边栏 */
|
||
.admin-sider {
|
||
width: 200px;
|
||
background-color: #001529;
|
||
position: fixed;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
z-index: 1000;
|
||
}
|
||
|
||
/* 主内容区 */
|
||
.admin-main {
|
||
margin-left: 200px;
|
||
display: flex;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* 内容侧边栏 */
|
||
.content-sider {
|
||
width: 180px;
|
||
background-color: #ffffff;
|
||
border-right: 1px solid #e8e8e8;
|
||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.content-sub-menu-item {
|
||
height: 36px;
|
||
padding: 0 16px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
|
||
&.active {
|
||
background-color: #e6f7ff;
|
||
color: #1890ff;
|
||
border-right: 2px solid #1890ff;
|
||
}
|
||
}
|
||
|
||
/* 内容区域 */
|
||
.content-area {
|
||
flex: 1;
|
||
background-color: #f0f2f5;
|
||
}
|
||
```
|
||
|
||
#### **步骤 12.6:响应式设计**
|
||
|
||
```scss
|
||
@media (max-width: 768px) {
|
||
.admin-sider {
|
||
width: 160px;
|
||
}
|
||
|
||
.admin-main {
|
||
margin-left: 160px;
|
||
}
|
||
|
||
.content-sider {
|
||
width: 140px;
|
||
}
|
||
}
|
||
```
|
||
|
||
#### **步骤 12.7:最终验证**
|
||
|
||
### **阶段十三:AdminLayout代码清理**
|
||
|
||
#### **步骤 13.1:移除遗留变量引用**
|
||
|
||
```javascript
|
||
// ❌ 错误:引用已删除的变量
|
||
const handleMenuClick = (menu: any) => {
|
||
activeMenu.value = menu.id
|
||
// 引用不存在的 tabs 变量
|
||
const existingTab = tabs.value.find(tab => tab.id === menu.id)
|
||
// ...
|
||
}
|
||
|
||
// ✅ 修复:移除所有遗留引用
|
||
const handleMenuClick = (menu: any) => {
|
||
activeMenu.value = menu.id
|
||
activeSubMenu.value = menu.subMenus?.[0]?.id || ''
|
||
|
||
uni.navigateTo({ url: menu.path })
|
||
}
|
||
```
|
||
|
||
#### **步骤 13.2:检查计算属性完整性**
|
||
|
||
```javascript
|
||
// 确保所有模板中使用的属性都已定义
|
||
const activeSubMenuTitle = computed(() => {
|
||
const subMenu = activeSubMenus.value.find(sm => sm.id === activeSubMenu.value)
|
||
return subMenu ? subMenu.title : ''
|
||
})
|
||
|
||
// 模板中使用
|
||
<text v-if="activeSubMenuTitle">{{ activeSubMenuTitle }}</text>
|
||
```
|
||
|
||
#### **步骤 13.3:验证模板依赖**
|
||
|
||
```vue
|
||
<!-- 检查模板中使用的所有变量和方法 -->
|
||
<template>
|
||
<!-- 确保这些属性都已定义 -->
|
||
<view v-if="activeSubMenus.length > 0">
|
||
<text>{{ activeMenuTitle }}</text>
|
||
<text>{{ activeSubMenuTitle }}</text>
|
||
</view>
|
||
</template>
|
||
```
|
||
|
||
#### **步骤 13.4:最终验证**
|
||
|
||
### **阶段十二:AdminLayout组件解析修复**
|
||
|
||
#### **步骤 12.1:检查组件导入路径**
|
||
|
||
```javascript
|
||
// ❌ 错误:缺少文件扩展名
|
||
import AdminLayout from "@/layouts/admin/index";
|
||
|
||
// ❌ 错误:路径不存在
|
||
import AdminLayout from "@/layout/admin/index.uvue";
|
||
|
||
// ✅ 正确:完整路径包含扩展名
|
||
import AdminLayout from "@/layouts/admin/index.uvue";
|
||
```
|
||
|
||
#### **步骤 12.2:简化组件结构**
|
||
|
||
```vue
|
||
<!-- ❌ 复杂组件结构 -->
|
||
<template>
|
||
<view class="complex-layout">
|
||
<!-- 大量嵌套、条件渲染、事件处理 -->
|
||
<view v-if="condition" v-for="item in items">
|
||
<!-- 复杂内容 -->
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- ✅ 简化组件结构 -->
|
||
<template>
|
||
<view class="simple-layout">
|
||
<slot></slot>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
const props = defineProps<{
|
||
currentPage: string
|
||
}>()
|
||
</script>
|
||
```
|
||
|
||
#### **步骤 12.3:验证组件可用性**
|
||
|
||
```javascript
|
||
// 在页面中正确使用组件
|
||
<template>
|
||
<AdminLayout current-page="page-name">
|
||
<view class="page-content">
|
||
<!-- 页面内容 -->
|
||
</view>
|
||
</AdminLayout>
|
||
</template>
|
||
```
|
||
|
||
#### **步骤 12.4:UTS编译器兼容性**
|
||
|
||
```javascript
|
||
// 确保组件语法符合UTS要求
|
||
// 使用 <script setup lang="uts">
|
||
// 正确定义props类型
|
||
// 避免复杂的TypeScript类型
|
||
```
|
||
|
||
#### **步骤 12.5:最终验证**
|
||
|
||
#### **步骤 4.1:AdminLayout 菜单路径**
|
||
|
||
```javascript
|
||
const menuList = ref([
|
||
{
|
||
id: "dashboard",
|
||
title: "首页",
|
||
icon: "🏠",
|
||
path: "/pages/mall/admin/index", // ✅ 完整路径
|
||
},
|
||
{
|
||
id: "user-list",
|
||
title: "用户管理",
|
||
icon: "👤",
|
||
path: "/pages/mall/admin/user-management", // ✅ 完整路径
|
||
},
|
||
// ... 其他菜单项
|
||
]);
|
||
```
|
||
|
||
#### **步骤 4.2:页面跳转路径**
|
||
|
||
```javascript
|
||
// 管理后台首页的导航方法
|
||
const goToUserManagement = () => {
|
||
uni.navigateTo({
|
||
url: "/pages/mall/admin/user-management", // ✅ 带前缀的完整路径
|
||
});
|
||
};
|
||
```
|
||
|
||
## 🎯 成功原理总结
|
||
|
||
### **原理一:正确的项目结构**
|
||
|
||
```
|
||
mall/
|
||
├── pages/
|
||
│ ├── minimal.uvue # 主页面
|
||
│ └── mall/
|
||
│ └── admin/
|
||
│ ├── index.uvue # 管理后台首页
|
||
│ ├── user-management.uvue # 用户管理
|
||
│ └── ...
|
||
├── layouts/
|
||
│ └── admin/
|
||
│ ├── index.uvue # AdminLayout 组件
|
||
│ └── ...
|
||
└── pages.json # 页面配置文件
|
||
```
|
||
|
||
### **原理二:pages.json 配置规范**
|
||
|
||
#### **主页面路径格式**:
|
||
|
||
```json
|
||
{
|
||
"pages": [
|
||
{
|
||
"path": "pages/minimal", // ✅ 完整路径包含 pages/
|
||
"style": {
|
||
/* ... */
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### **子包页面路径格式**:
|
||
|
||
```json
|
||
{
|
||
"subPackages": [
|
||
{
|
||
"root": "pages/mall", // ✅ 相对项目根目录
|
||
"pages": [
|
||
{
|
||
"path": "admin/index", // ✅ 相对 root 目录
|
||
"style": {
|
||
/* ... */
|
||
}
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### **原理三:生命周期钩子使用**
|
||
|
||
#### **uni-app-x 生命周期规范**:
|
||
|
||
```javascript
|
||
// ✅ 正确:全局可用,无需导入
|
||
onLoad(() => {
|
||
// 页面加载逻辑
|
||
});
|
||
|
||
onShow(() => {
|
||
// 页面显示逻辑
|
||
});
|
||
|
||
// ✅ 特殊情况:需要显式导入
|
||
import { onLoad } from "@dcloudio/uni-app";
|
||
```
|
||
|
||
#### **Vue 3 组合式 API**:
|
||
|
||
```javascript
|
||
import { ref, computed, onMounted } from "vue";
|
||
|
||
// 响应式数据
|
||
const count = ref(0);
|
||
|
||
// 计算属性
|
||
const doubleCount = computed(() => count.value * 2);
|
||
|
||
// 生命周期
|
||
onMounted(() => {
|
||
console.log("组件挂载");
|
||
});
|
||
```
|
||
|
||
### **原理四:组件语法正确性**
|
||
|
||
#### **Vue 3 模板语法**:
|
||
|
||
```vue
|
||
<template>
|
||
<view class="component">
|
||
<!-- 正确的标签闭合 -->
|
||
<view v-if="show">{{ message }}</view>
|
||
<view v-for="item in items" :key="item.id">
|
||
{{ item.name }}
|
||
</view>
|
||
</view>
|
||
</template>
|
||
```
|
||
|
||
#### **TypeScript 声明**:
|
||
|
||
```typescript
|
||
<script setup lang="uts">
|
||
// 正确的 Props 定义
|
||
const props = defineProps<{
|
||
currentPage: string
|
||
title?: string
|
||
}>()
|
||
|
||
// 正确的类型定义
|
||
interface MenuItem {
|
||
id: string
|
||
title: string
|
||
icon: string
|
||
path: string
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### **原理五:错误排查方法**
|
||
|
||
#### **逐步验证策略**:
|
||
|
||
1. **最小化配置**: 从最简单的配置开始
|
||
2. **逐步添加**: 每次只添加一个功能
|
||
3. **错误定位**: 根据错误信息快速定位问题
|
||
4. **备份恢复**: 修改前备份,失败时快速回滚
|
||
|
||
#### **常见错误检查清单**:
|
||
|
||
- ✅ pages.json 路径格式是否正确
|
||
- ✅ 页面文件是否存在
|
||
- ✅ 组件语法是否正确
|
||
- ✅ 导入语句是否正确
|
||
- ✅ 生命周期钩子使用是否正确
|
||
- ✅ 响应式数据使用是否正确
|
||
- ✅ 特殊字符和 emoji 是否安全
|
||
- ✅ 缩进是否一致(避免混用制表符和空格)
|
||
- ✅ 方法调用是否正确(方法名和参数匹配)
|
||
- ✅ 自闭合标签格式是否正确(使用 `/>` 而非 `></tag>`)
|
||
- ✅ 模态框位置是否正确(避免条件嵌套)
|
||
- ✅ .uvue文件复杂度是否适中(避免过度复杂的模板结构)
|
||
- ✅ 批量文件修复是否完整(所有.uvue文件都已简化处理)
|
||
- ✅ AdminLayout组件是否正确导入和解析
|
||
- ✅ AdminLayout代码清理是否完整(无遗留变量引用)
|
||
|
||
## 🚀 最佳实践指南
|
||
|
||
### **开发规范**
|
||
|
||
#### **1. 文件命名规范**
|
||
|
||
- 页面文件: `kebab-case.uvue`
|
||
- 组件文件: `PascalCase.uvue`
|
||
- 工具文件: `camelCase.uts`
|
||
|
||
#### **2. 路径配置规范**
|
||
|
||
- 主页面: `pages/page-name`
|
||
- 子包页面: `root: "pages/module"`, `path: "sub-page"`
|
||
- 组件导入: `@/layouts/...`, `@/components/...`
|
||
|
||
#### **3. 代码组织规范**
|
||
|
||
```vue
|
||
<template>
|
||
<!-- 模板内容 -->
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
// 导入语句
|
||
// 类型定义
|
||
// Props 定义
|
||
// 响应式数据
|
||
// 计算属性
|
||
// 生命周期
|
||
// 方法
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* 样式内容 */
|
||
</style>
|
||
```
|
||
|
||
### **调试技巧**
|
||
|
||
#### **1. 编译错误排查**
|
||
|
||
- 查看控制台错误信息
|
||
- 检查文件语法
|
||
- 验证导入路径
|
||
- 确认类型定义
|
||
|
||
#### **2. 运行时错误排查**
|
||
|
||
- 检查页面配置
|
||
- 验证组件 Props
|
||
- 确认事件处理
|
||
- 测试页面跳转
|
||
|
||
#### **3. 性能优化**
|
||
|
||
- 合理使用响应式数据
|
||
- 避免不必要的计算属性
|
||
- 优化组件渲染
|
||
- 使用合适的生命周期
|
||
|
||
#### **4. .uvue文件特殊处理**
|
||
|
||
- 保持模板结构简单,避免过度复杂
|
||
- 使用UTS编译器兼容的字符集
|
||
- 定期检查文件编码和特殊字符
|
||
- 采用渐进式开发策略,从简单到复杂
|
||
- 出现编译错误时优先简化模板结构
|
||
|
||
#### **5. AdminLayout组件维护**
|
||
|
||
- 保持组件结构简单,避免过度复杂的功能
|
||
- 使用正确的文件扩展名(.uvue)在导入路径中
|
||
- 定期验证组件的导出和解析
|
||
- 出现解析错误时优先简化组件结构
|
||
- 确保组件语法符合UTS编译器要求
|
||
|
||
#### **6. 双侧边栏布局设计**
|
||
|
||
- 主侧边栏只显示一级菜单图标,保持简洁
|
||
- 内容侧边栏显示二级菜单,位于内容区左侧
|
||
- 合理分配侧边栏宽度,确保移动端兼容性
|
||
- 使用计算属性动态控制侧边栏显示
|
||
- 确保路由同步和高亮状态正确
|
||
|
||
## 📚 参考资源
|
||
|
||
### **官方文档**
|
||
|
||
- [uni-app-x 官方文档](https://doc.dcloud.net.cn/uni-app-x/)
|
||
- [Vue 3 组合式 API](https://cn.vuejs.org/guide/extras/composition-api-faq.html)
|
||
- [TypeScript 指南](https://www.typescriptlang.org/docs/)
|
||
|
||
### **最佳实践**
|
||
|
||
- 遵循项目现有的代码风格
|
||
- 保持配置的一致性
|
||
- 定期检查和更新依赖
|
||
- 编写清晰的注释
|
||
|
||
---
|
||
|
||
## 🎯 总结
|
||
|
||
通过本次修复,我们建立了完整的 uni-app-x 项目开发和调试方法论:
|
||
|
||
1. **问题定位**: 快速识别配置和语法错误
|
||
2. **逐步修复**: 从简单到复杂,逐步解决问题
|
||
3. **规范建立**: 统一的代码和配置规范
|
||
4. **最佳实践**: 可复用的开发模式
|
||
|
||
### **新增问题类型**
|
||
|
||
#### **特殊字符兼容性问题**
|
||
|
||
- **现象**: `[plugin:uts] Invalid end tag` 错误
|
||
- **原因**: emoji 字符或特殊 Unicode 符号导致模板解析失败
|
||
- **解决方案**: 替换为标准 ASCII 字符或安全 Unicode 符号
|
||
- **预防**: 在模板中使用经过验证的安全字符集
|
||
|
||
---
|
||
|
||
#### **原因十四:AdminLayout代码清理不完整**
|
||
|
||
- **遗留变量引用**: 移除功能后仍引用已删除的变量
|
||
- **计算属性缺失**: 重构时遗漏必要的计算属性
|
||
- **模板依赖问题**: 模板中使用未定义的响应式属性
|
||
|
||
---
|
||
|
||
## 🎯 阶段十五: CRMEB 路由体系 1:1 复刻
|
||
|
||
### **背景与目标**
|
||
|
||
本阶段实现了 CRMEB v5 标准版管理端前端的路由体系和侧边栏布局的完整复刻,采用"内部路由/状态驱动渲染"模式,在 uni-app-x 项目中实现单页应用(SPA)体验。
|
||
|
||
### **核心架构设计**
|
||
|
||
#### **1. 内部路由系统**
|
||
|
||
不同于传统的 `uni.navigateTo` 页面栈模式,采用状态驱动的内部路由:
|
||
|
||
```
|
||
点击菜单 → 更新 activeRouteId → 切换组件渲染 → 不打开新页面
|
||
```
|
||
|
||
**优势**:
|
||
|
||
- ✅ 避免页面栈堆积
|
||
- ✅ 保持布局和侧边栏状态
|
||
- ✅ 实现 CRMEB 风格的标签页系统
|
||
- ✅ 更快的页面切换速度
|
||
|
||
#### **2. 文件结构**
|
||
|
||
```
|
||
layouts/admin/
|
||
├── router/
|
||
│ ├── adminRoutes.uts # 路由配置(映射CRMEB routes)
|
||
│ └── adminComponentMap.uts # 组件映射表(静态导入)
|
||
├── store/
|
||
│ └── adminNavStore.uts # 导航状态管理
|
||
├── components/
|
||
│ ├── AdminAside.uvue # 主侧边栏(一级菜单)
|
||
│ ├── AdminSubSider.uvue # 二级侧边栏(分组+菜单项)
|
||
│ ├── AdminHeader.uvue # 顶部栏
|
||
│ ├── AdminTagsView.uvue # 标签页
|
||
│ └── PlaceholderPage.uvue # 占位组件
|
||
└── AdminLayout.uvue # 布局容器(渲染组件)
|
||
```
|
||
|
||
#### **3. 路由数据结构**
|
||
|
||
**一级菜单 (TopMenu)**:
|
||
|
||
```typescript
|
||
{
|
||
id: 'user',
|
||
title: '用户',
|
||
icon: 'user',
|
||
path: '/pages/mall/admin/user/list', // 默认路径
|
||
order: 2,
|
||
groups: [...] // 分组列表
|
||
}
|
||
```
|
||
|
||
**路由记录 (RouteRecord)**:
|
||
|
||
```typescript
|
||
{
|
||
id: 'user_list',
|
||
title: '用户管理',
|
||
path: '/pages/mall/admin/user/list',
|
||
componentKey: 'UserList', // 映射到组件
|
||
parentId: 'user',
|
||
groupId: 'user-manage',
|
||
auth: ['admin-user-user-index']
|
||
}
|
||
```
|
||
|
||
### **实施步骤总结**
|
||
|
||
#### **步骤 1: 抽取 CRMEB 路由结构**
|
||
|
||
从 CRMEB 源码 `router/modules/*` 抽取:
|
||
|
||
- 9 个一级模块: home, user, product, order, marketing, cms, finance, statistic, setting
|
||
- 30+ 二级路由: 用户管理、商品列表、订单管理等
|
||
- 分组信息: 用户管理、会员管理、营销工具等
|
||
|
||
#### **步骤 2: 创建路由配置文件**
|
||
|
||
**文件**: `layouts/admin/router/adminRoutes.uts`
|
||
|
||
包含:
|
||
|
||
- `topMenus`: 一级菜单配置
|
||
- `routes`: 完整路由表
|
||
- 工具函数: `getTopMenus()`, `findRouteById()`, `getBreadcrumb()` 等
|
||
|
||
#### **步骤 3: 创建状态管理**
|
||
|
||
**文件**: `layouts/admin/store/adminNavStore.uts`
|
||
|
||
状态:
|
||
|
||
- `activeTopMenuId`: 当前选中的一级菜单
|
||
- `activeRouteId`: 当前激活的路由
|
||
- `tabs`: 标签页列表
|
||
- `isMainAsideCollapsed`: 主侧边栏折叠状态
|
||
|
||
方法:
|
||
|
||
- `openRoute(routeId)`: 打开路由(核心方法)
|
||
- `closeTab(tabId)`: 关闭标签页
|
||
- `initNavState()`: 初始化导航状态
|
||
|
||
#### **步骤 4: 创建组件映射表**
|
||
|
||
**文件**: `layouts/admin/router/adminComponentMap.uts`
|
||
|
||
**关键点**:
|
||
|
||
- ✅ 所有组件**必须静态导入**(确保打包可分析)
|
||
- ✅ 使用 `@` 别名(禁止相对路径)
|
||
- ✅ 占位组件统一使用 `PlaceholderPage`
|
||
|
||
```typescript
|
||
import UserList from '@/pages/mall/admin/user/list.uvue'
|
||
import ProductList from '@/pages/mall/admin/product/list.uvue'
|
||
|
||
export const componentMap: Map<string, any> = new Map([
|
||
['UserList', UserList],
|
||
['ProductList', ProductList],
|
||
...
|
||
])
|
||
```
|
||
|
||
#### **步骤 5: 重构 AdminLayout**
|
||
|
||
**核心变化**:
|
||
|
||
```vue
|
||
<!-- 旧模式: slot 渲染 -->
|
||
<slot></slot>
|
||
|
||
<!-- 新模式: 组件映射渲染 -->
|
||
<component :is="currentComponent" />
|
||
```
|
||
|
||
**计算属性**:
|
||
|
||
```typescript
|
||
const currentComponent = computed(() => {
|
||
const route = findRouteById(activeRouteId.value);
|
||
return getComponent(route.componentKey);
|
||
});
|
||
```
|
||
|
||
#### **步骤 6: 重构侧边栏组件**
|
||
|
||
**AdminAside (主侧边栏)**:
|
||
|
||
- 仅显示一级菜单图标+文本
|
||
- 宽度: 96px (CRMEB: 64px)
|
||
- 点击切换 `activeTopMenuId`
|
||
|
||
**AdminSubSider (二级侧边栏)**:
|
||
|
||
- 显示当前一级菜单的分组和子项
|
||
- 宽度: 180px (CRMEB: 200px)
|
||
- 位于内容区左侧(独立层级)
|
||
|
||
#### **步骤 7: 批量创建占位页面**
|
||
|
||
使用 Python 脚本创建 26 个占位页面:
|
||
|
||
```bash
|
||
python create_placeholder_pages.py
|
||
```
|
||
|
||
每个页面包含:
|
||
|
||
- 标题和组件Key显示
|
||
- 统一的占位样式
|
||
- TODO 注释
|
||
|
||
#### **步骤 8: 修改首页模式**
|
||
|
||
**旧模式**:
|
||
|
||
```vue
|
||
<template>
|
||
<AdminLayout currentPage="home">
|
||
<!-- 内容 -->
|
||
</AdminLayout>
|
||
</template>
|
||
```
|
||
|
||
**新模式**:
|
||
|
||
```vue
|
||
<template>
|
||
<view class="dashboard-page">
|
||
<!-- 内容 -->
|
||
</view>
|
||
</template>
|
||
<!-- 不再包裹 AdminLayout -->
|
||
```
|
||
|
||
### **关键技术点**
|
||
|
||
#### **1. 组件动态渲染**
|
||
|
||
**问题**: uni-app-x 不支持动态 `import()`
|
||
|
||
**解决方案**: 使用 Map + 静态导入
|
||
|
||
```typescript
|
||
// ❌ 不可用
|
||
const component = () => import(`@/pages/${path}.uvue`);
|
||
|
||
// ✅ 正确方式
|
||
const component = componentMap.get(componentKey);
|
||
```
|
||
|
||
#### **2. 路由同步**
|
||
|
||
状态驱动而非URL驱动:
|
||
|
||
```typescript
|
||
// 点击菜单
|
||
onRouteClick(routeId) →
|
||
activeRouteId.value = routeId →
|
||
currentComponent 重新计算 →
|
||
渲染新组件
|
||
```
|
||
|
||
#### **3. 标签页管理**
|
||
|
||
模仿 CRMEB 的标签页行为:
|
||
|
||
- 固定标签 (`isAffix`): 首页等,不可关闭
|
||
- 普通标签: 可关闭、关闭其他、关闭全部
|
||
- 关闭当前标签时自动切换到相邻标签
|
||
|
||
#### **4. 面包屑导航**
|
||
|
||
自动生成面包屑:
|
||
|
||
```typescript
|
||
getBreadcrumb('user_list')
|
||
→ [{ id: 'user', title: '用户' }, { id: 'user_list', title: '用户管理' }]
|
||
```
|
||
|
||
### **与 CRMEB 的对照表**
|
||
|
||
| CRMEB 特性 | uni-app-x 实现 | 备注 |
|
||
| ------------- | ---------------- | --------------------------- |
|
||
| Vue Router | 状态驱动内部路由 | 无 router 实例 |
|
||
| router.push() | openRoute() | 更新状态而非跳转 |
|
||
| keep-alive | 未实现 | 后续可通过组件缓存实现 |
|
||
| 菜单配置 | menu.uts | 已废弃,改用 adminRoutes.uts |
|
||
| Vuex store | UTS 响应式变量 | ref/computed 代替 |
|
||
| 动态导入 | 静态映射表 | 打包限制 |
|
||
|
||
### **常见问题与解决方案**
|
||
|
||
#### **问题 1: 组件未找到**
|
||
|
||
**现象**: `getComponent` 返回 `PlaceholderPage`
|
||
|
||
**原因**:
|
||
|
||
- componentMap 中缺少对应的 key
|
||
- 导入路径错误
|
||
|
||
**解决**:
|
||
|
||
```typescript
|
||
// 检查 adminComponentMap.uts
|
||
import UserList from "@/pages/mall/admin/user/list.uvue";
|
||
componentMap.set("UserList", UserList);
|
||
```
|
||
|
||
#### **问题 2: 标签页不显示**
|
||
|
||
**现象**: 点击菜单后标签页为空
|
||
|
||
**原因**: `tabs` 数组未正确更新
|
||
|
||
**解决**:
|
||
|
||
```typescript
|
||
// 确保在 openRoute 中添加标签
|
||
if (addTab) {
|
||
addTabItem(route);
|
||
}
|
||
```
|
||
|
||
#### **问题 3: 二级侧边栏不显示**
|
||
|
||
**现象**: 点击一级菜单后二级侧边栏空白
|
||
|
||
**原因**: 一级菜单的 `groups` 为空数组
|
||
|
||
**解决**:
|
||
|
||
```typescript
|
||
// 检查 adminRoutes.uts 中的 topMenus 配置
|
||
{
|
||
id: 'user',
|
||
groups: [
|
||
{ id: 'user-manage', title: '用户管理' } // ✅ 必须有
|
||
]
|
||
}
|
||
```
|
||
|
||
#### **问题 4: 模板编译错误**
|
||
|
||
**现象**: `Invalid end tag` 或 `Illegal '/' in tags`
|
||
|
||
**原因**:
|
||
|
||
- 组件模板中有乱码
|
||
- 标签未正确闭合
|
||
|
||
**解决**:
|
||
|
||
- 检查文件编码为 UTF-8
|
||
- 移除特殊 emoji 字符
|
||
- 确保所有标签正确闭合
|
||
|
||
### **性能优化建议**
|
||
|
||
1. **懒加载路由**: 只在需要时加载组件
|
||
2. **虚拟滚动**: 标签页数量过多时使用虚拟列表
|
||
3. **状态持久化**: 将 `activeRouteId` 等状态存入 localStorage
|
||
4. **权限控制**: 在 `openRoute` 中增加权限校验逻辑
|
||
|
||
### **扩展开发指南**
|
||
|
||
#### **添加新路由**
|
||
|
||
1. 在 `adminRoutes.uts` 中添加路由记录:
|
||
|
||
```typescript
|
||
{
|
||
id: 'custom_feature',
|
||
title: '自定义功能',
|
||
path: '/pages/mall/admin/custom/feature',
|
||
componentKey: 'CustomFeature',
|
||
parentId: 'setting',
|
||
groupId: 'setting-system'
|
||
}
|
||
```
|
||
|
||
2. 创建页面文件:
|
||
|
||
```bash
|
||
pages/mall/admin/custom/feature.uvue
|
||
```
|
||
|
||
3. 在 `adminComponentMap.uts` 中添加映射:
|
||
|
||
```typescript
|
||
import CustomFeature from "@/pages/mall/admin/custom/feature.uvue";
|
||
componentMap.set("CustomFeature", CustomFeature);
|
||
```
|
||
|
||
#### **添加新的一级菜单**
|
||
|
||
1. 在 `topMenus` 中添加:
|
||
|
||
```typescript
|
||
{
|
||
id: 'reports',
|
||
title: '报表',
|
||
icon: 'report',
|
||
path: '/pages/mall/admin/reports/index',
|
||
order: 10,
|
||
groups: [...]
|
||
}
|
||
```
|
||
|
||
2. 更新侧边栏图标映射 (`AdminAside.uvue`):
|
||
|
||
```typescript
|
||
const iconMap: Record<string, string> = {
|
||
...
|
||
'reports': 'R'
|
||
}
|
||
```
|
||
|
||
### **验收标准**
|
||
|
||
- ✅ 主侧边栏显示所有一级菜单
|
||
- ✅ 点击一级菜单,二级侧边栏正确显示分组和子项
|
||
- ✅ 点击子项,内容区渲染对应组件
|
||
- ✅ 标签页正确添加、切换、关闭
|
||
- ✅ 无页面栈堆积
|
||
- ✅ 无模板编译错误
|
||
- ✅ 无乱码
|
||
- ✅ 所有 import 使用 `@` 别名
|
||
|
||
### **文档更新**
|
||
|
||
本次重构新增以下文件和概念:
|
||
|
||
- **内部路由模式**: 状态驱动渲染,替代页面跳转
|
||
- **组件映射表**: 静态导入 + Map 查找,替代动态导入
|
||
- **CRMEB 路由映射**: 1:1 复刻 CRMEB 的路由和菜单结构
|
||
- **双侧边栏布局**: 主侧边栏(一级) + 二级侧边栏(分组)
|
||
|
||
---
|
||
|
||
这个指南现在涵盖了 uni-app-x 项目开发中最常见的 15 类问题(新增 CRMEB 路由体系复刻),为后续开发提供了完整的故障排除和最佳实践指导。 🚀
|