帮助文档
{{userInfo.nickname}}
用户设置 退出登录

{{wikiTitle}}

移动端添加页面说明

移动端添加页面说明

概述

CRMEB多店系统的移动端采用 uni-app 框架开发,支持编译到微信小程序、H5、APP等多个平台。本文档详细介绍如何在移动端项目中添加新页面,包括页面创建、路由配置、API对接、组件使用等完整流程。

目录结构

view/uniapp/
├── api/                    # API接口定义目录
│   ├── api.js             # 公共接口
│   ├── user.js            # 用户相关接口
│   ├── order.js           # 订单相关接口
│   ├── store.js           # 门店相关接口
│   └── activity.js        # 活动相关接口
├── components/            # 公共组件目录
│   ├── skeleton/          # 骨架屏组件
│   ├── countDown/         # 倒计时组件
│   ├── productWindow/     # 商品弹窗组件
│   └── ...               # 更多组件
├── config/               # 配置文件目录
│   └── app.js            # 应用配置(接口地址等)
├── libs/                 # 工具库目录
│   ├── login.js          # 登录相关
│   └── new_chat.js       # 聊天WebSocket
├── pages/                # 主包页面目录
│   ├── index/            # 首页
│   ├── user/             # 个人中心
│   ├── goods_details/    # 商品详情
│   └── ...               # 更多页面
├── store/                # Vuex状态管理
│   ├── index.js          # Store入口
│   ├── modules/          # 模块化Store
│   └── getters.js        # 全局Getters
├── utils/                # 工具函数目录
│   ├── request.js        # 请求封装
│   ├── util.js           # 通用工具函数
│   └── cache.js          # 缓存管理
├── static/               # 静态资源目录
├── App.vue               # 应用入口组件
├── main.js               # 应用入口文件
├── pages.json            # 页面路由配置
├── manifest.json         # 应用配置文件
└── uni.scss              # 全局样式变量

完整开发流程

第一步:创建页面文件

1.1 确定页面位置

根据页面功能确定放置位置:

  • 主包页面:放在 pages/ 目录下,启动时加载
  • 分包页面:放在 pages/users/pages/activity/ 等分包目录下
# 主包页面示例(首页相关)
view/uniapp/pages/my_feature/
├── index.vue          # 页面主文件
├── components/        # 页面私有组件
│   └── MyComponent.vue
└── static/            # 页面私有静态资源
    └── icon.png

# 分包页面示例(用户相关)
view/uniapp/pages/users/my_page/
└── index.vue

1.2 创建基础页面模板

<!-- pages/my_feature/index.vue -->
<template>
  <view class="my-feature" :style="colorStyle">
    <!-- 骨架屏(可选) -->
    <skeleton 
      :show="showSkeleton" 
      ref="skeleton" 
      loading="chiaroscuro" 
      bgcolor="#FFF">
    </skeleton>

    <!-- 页面内容 -->
    <view v-if="!showSkeleton">
      <!-- 导航栏 -->
      <view class="nav-bar">
        <text class="title">页面标题</text>
      </view>

      <!-- 主体内容 -->
      <view class="content">
        <view class="card" v-for="item in dataList" :key="item.id">
          <text>{{ item.name }}</text>
        </view>
      </view>

      <!-- 空状态 -->
      <emptyPage 
        v-if="!dataList.length && !loading" 
        title="暂无数据">
      </emptyPage>

      <!-- 加载更多 -->
      <view class="load-more" v-if="dataList.length">
        <text v-if="loading">加载中...</text>
        <text v-else-if="!hasMore">没有更多了</text>
      </view>
    </view>
  </view>
</template>

<script>
// 引入API
import { getMyDataList } from '@/api/my_api.js';
// 引入工具函数
import { toLogin } from '@/libs/login.js';
// 引入颜色混入
import colors from "@/mixins/color.js";

export default {
  name: 'MyFeature',

  // 混入主题色
  mixins: [colors],

  // 页面数据
  data() {
    return {
      showSkeleton: true,    // 骨架屏显示
      loading: false,        // 加载状态
      dataList: [],          // 数据列表
      page: 1,               // 当前页码
      limit: 10,             // 每页数量
      hasMore: true,         // 是否有更多
      // 页面参数
      pageParams: {}
    };
  },

  // 计算属性
  computed: {
    // 示例:计算总数
    totalCount() {
      return this.dataList.length;
    }
  },

  // 页面加载
  onLoad(options) {
    // 接收页面参数
    this.pageParams = options;
    // 获取初始数据
    this.getInitData();
  },

  // 页面显示
  onShow() {
    // 刷新数据等操作
  },

  // 下拉刷新
  onPullDownRefresh() {
    this.refreshData();
  },

  // 上拉加载更多
  onReachBottom() {
    if (this.hasMore && !this.loading) {
      this.loadMore();
    }
  },

  // 分享给朋友
  onShareAppMessage() {
    return {
      title: '分享标题',
      path: '/pages/my_feature/index',
      imageUrl: ''
    };
  },

  // 分享到朋友圈
  onShareTimeline() {
    return {
      title: '分享标题',
      query: ''
    };
  },

  methods: {
    // 获取初始数据
    async getInitData() {
      this.showSkeleton = true;
      try {
        await this.getDataList();
      } finally {
        this.showSkeleton = false;
      }
    },

    // 获取数据列表
    async getDataList() {
      this.loading = true;
      try {
        const params = {
          page: this.page,
          limit: this.limit
        };
        const res = await getMyDataList(params);

        if (this.page === 1) {
          this.dataList = res.data.list || [];
        } else {
          this.dataList = [...this.dataList, ...(res.data.list || [])];
        }

        // 判断是否有更多
        this.hasMore = this.dataList.length < res.data.count;
      } catch (error) {
        uni.showToast({
          title: error.msg || '获取数据失败',
          icon: 'none'
        });
      } finally {
        this.loading = false;
      }
    },

    // 刷新数据
    async refreshData() {
      this.page = 1;
      this.hasMore = true;
      await this.getDataList();
      uni.stopPullDownRefresh();
    },

    // 加载更多
    async loadMore() {
      this.page++;
      await this.getDataList();
    },

    // 跳转页面
    navigateTo(url) {
      uni.navigateTo({ url });
    },

    // 返回上一页
    navigateBack() {
      uni.navigateBack();
    }
  }
};
</script>

<style lang="scss" scoped>
.my-feature {
  min-height: 100vh;
  background-color: #f5f5f5;
  padding-bottom: env(safe-area-inset-bottom);

  .nav-bar {
    height: 88rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: #fff;

    .title {
      font-size: 32rpx;
      font-weight: 500;
      color: #333;
    }
  }

  .content {
    padding: 20rpx;

    .card {
      background-color: #fff;
      border-radius: 16rpx;
      padding: 24rpx;
      margin-bottom: 20rpx;
    }
  }

  .load-more {
    text-align: center;
    padding: 30rpx;
    color: #999;
    font-size: 24rpx;
  }
}
</style>

第二步:配置页面路由

pages.json 中配置页面路由:

2.1 主包页面配置

{
  "pages": [
    // 在此添加主包页面
    {
      "path": "pages/my_feature/index",
      "style": {
        "navigationBarTitleText": "我的功能",
        "enablePullDownRefresh": true,
        // 自定义导航栏(可选)
        "navigationStyle": "custom",
        "navigationBarTextStyle": "black",
        // APP特殊配置
        "app-plus": {
          "titleNView": {
            "type": "default"
          }
        }
      }
    }
  ]
}

2.2 分包页面配置

{
  "subPackages": [
    {
      "root": "pages/users",
      "name": "users",
      "pages": [
        // 在分包中添加页面
        {
          "path": "my_page/index",
          "style": {
            "navigationBarTitleText": "我的页面",
            "navigationBarBackgroundColor": "#f5f5f5",
            "navigationBarTextStyle": "black",
            "app-plus": {
              "titleNView": {
                "type": "default"
              }
            }
          }
        }
      ]
    }
  ]
}

2.3 页面样式配置说明

属性 说明 可选值
navigationBarTitleText 导航栏标题 字符串
navigationBarBackgroundColor 导航栏背景色 十六进制颜色
navigationBarTextStyle 导航栏标题颜色 black/white
navigationStyle 导航栏样式 default/custom
enablePullDownRefresh 开启下拉刷新 true/false
disableScroll 禁止页面滚动 true/false

第三步:创建API接口

api/ 目录下创建或编辑API文件:

3.1 创建新的API文件

// api/my_api.js
import request from "@/utils/request.js";

/**
 * 获取我的数据列表
 * @param {Object} data - 请求参数
 * @param {number} data.page - 页码
 * @param {number} data.limit - 每页数量
 */
export function getMyDataList(data) {
  return request.get('my/data/list', data);
}

/**
 * 获取我的数据详情
 * @param {number} id - 数据ID
 */
export function getMyDataDetail(id) {
  return request.get(`my/data/detail/${id}`);
}

/**
 * 提交我的数据
 * @param {Object} data - 提交的数据
 */
export function submitMyData(data) {
  return request.post('my/data/submit', data);
}

/**
 * 更新我的数据
 * @param {number} id - 数据ID
 * @param {Object} data - 更新的数据
 */
export function updateMyData(id, data) {
  return request.put(`my/data/update/${id}`, data);
}

/**
 * 删除我的数据
 * @param {number} id - 数据ID
 */
export function deleteMyData(id) {
  return request.delete(`my/data/delete/${id}`);
}

/**
 * 无需登录的接口示例
 * 设置 noAuth: true 即可无需登录访问
 */
export function getPublicData() {
  return request.get('public/data', {}, {
    noAuth: true
  });
}

3.2 请求封装说明

utils/request.js 封装了统一的请求方法:

// 请求方法
request.get(url, data, options)    // GET请求
request.post(url, data, options)   // POST请求
request.put(url, data, options)    // PUT请求
request.delete(url, data, options) // DELETE请求

// options 配置
{
  noAuth: false,   // 是否无需登录,默认false
  noVerify: false  // 是否不验证返回结果,默认false
}

// 状态码说明
// 200     - 成功
// 410000  - 请登录
// 410001  - 登录已过期
// 410002  - 登录状态有误
// 410010  - 站点升级中
// 410020  - 账号被禁止

第四步:使用组件

4.1 全局组件

main.js 中已注册的全局组件可直接使用:

<template>
  <!-- 骨架屏组件 -->
  <skeleton :show="loading" ref="skeleton"></skeleton>

  <!-- 金额显示组件 -->
  <BaseMoney :money="price" symbolSize="24" integerSize="36"></BaseMoney>

  <!-- 标签组件 -->
  <BaseTag text="热销" color="#ff5500" background="#fff5f0"></BaseTag>

  <!-- 抽屉组件 -->
  <baseDrawer :visible="showDrawer" @close="showDrawer = false">
    <view>抽屉内容</view>
  </baseDrawer>

  <!-- 图片懒加载组件 -->
  <easyLoadimage :image-src="imageUrl" mode="widthFix"></easyLoadimage>
</template>

4.2 引入局部组件

<script>
// 引入组件
import emptyPage from '@/components/emptyPage.vue';
import productWindow from '@/components/productWindow/productWindow.vue';
import countDown from '@/components/countDown/countDown.vue';

export default {
  components: {
    emptyPage,
    productWindow,
    countDown
  }
}
</script>

4.3 常用组件说明

组件 路径 用途
emptyPage components/emptyPage.vue 空状态提示
skeleton components/skeleton/index.vue 骨架屏加载
countDown components/countDown/countDown.vue 倒计时
productWindow components/productWindow/productWindow.vue 商品SKU选择弹窗
couponWindow components/couponWindow/couponWindow.vue 优惠券选择弹窗
addressWindow components/addressWindow/addressWindow.vue 地址选择弹窗
NavBar components/NavBar.vue 自定义导航栏

第五步:状态管理(Vuex)

5.1 Store结构

store/
├── index.js      # Store入口
├── getters.js    # 全局Getters
└── modules/      # 模块化Store
    └── app.js    # 应用状态模块

5.2 使用Store

<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';

export default {
  computed: {
    // 映射state
    ...mapState(['app']),
    // 获取用户信息
    userInfo() {
      return this.$store.state.app.userInfo;
    },
    // 获取token
    token() {
      return this.$store.state.app.token;
    },
    // 映射getters
    ...mapGetters(['isLogin'])
  },

  methods: {
    // 直接调用
    setUserInfo(info) {
      this.$store.commit('SET_USERINFO', info);
    },

    // 映射mutations
    ...mapMutations(['SET_TOKEN', 'SET_USERINFO']),

    // 映射actions
    ...mapActions(['logout'])
  }
}
</script>

5.3 创建新的Store模块

// store/modules/myModule.js
const state = {
  myData: null
};

const mutations = {
  SET_MY_DATA(state, data) {
    state.myData = data;
  }
};

const actions = {
  async fetchMyData({ commit }) {
    // 异步操作
    const data = await someApi();
    commit('SET_MY_DATA', data);
  }
};

export default {
  state,
  mutations,
  actions
};

第六步:混入(Mixins)

项目提供了常用的混入功能:

6.1 颜色主题混入

<script>
import colors from "@/mixins/color.js";

export default {
  mixins: [colors],

  // 可直接使用 colorStyle 计算属性
  // 在模板中::style="colorStyle"
}
</script>

<template>
  <view :style="colorStyle">
    <!-- 内容会应用主题色 -->
  </view>
</template>

第七步:工具函数

7.1 常用工具函数

// utils/util.js 中的常用函数

// 使用方式
import { formatTime, formatPrice, deepClone } from '@/utils/util.js';

// 或通过全局挂载使用
this.$util.formatTime(new Date());

7.2 缓存管理

// utils/cache.js
import Cache from '@/utils/cache.js';

// 设置缓存
Cache.set('key', 'value');
Cache.set('key', 'value', 3600); // 带过期时间(秒)

// 获取缓存
const value = Cache.get('key');

// 删除缓存
Cache.remove('key');

// 清空缓存
Cache.clear();

// 检查是否存在
const has = Cache.has('key');

7.3 登录相关

// libs/login.js
import { toLogin, checkLogin } from '@/libs/login.js';

// 检查是否登录
if (!checkLogin()) {
  // 跳转登录
  toLogin();
}

平台条件编译

uni-app支持条件编译,针对不同平台编写不同代码:

模板条件编译

<template>
  <!-- 仅在微信小程序显示 -->
  <!-- #ifdef MP-WEIXIN -->
  <button open-type="share">分享给好友</button>
  <!-- #endif -->

  <!-- 仅在H5显示 -->
  <!-- #ifdef H5 -->
  <button @click="shareH5">分享</button>
  <!-- #endif -->

  <!-- 仅在APP显示 -->
  <!-- #ifdef APP-PLUS -->
  <button @click="shareApp">APP分享</button>
  <!-- #endif -->

  <!-- 在小程序和APP中显示 -->
  <!-- #ifdef MP || APP-PLUS -->
  <view class="native-only">原生平台专属内容</view>
  <!-- #endif -->
</template>

JavaScript条件编译

export default {
  methods: {
    share() {
      // #ifdef MP-WEIXIN
      // 微信小程序分享逻辑
      wx.showShareMenu({
        withShareTicket: true
      });
      // #endif

      // #ifdef H5
      // H5分享逻辑
      this.h5Share();
      // #endif

      // #ifdef APP-PLUS
      // APP分享逻辑
      plus.share.getServices();
      // #endif
    }
  }
}

CSS条件编译

<style lang="scss">
.container {
  /* #ifdef H5 */
  padding-top: 44px; /* H5导航栏高度 */
  /* #endif */

  /* #ifdef MP */
  padding-top: 0; /* 小程序有原生导航栏 */
  /* #endif */

  /* #ifdef APP-PLUS */
  padding-top: var(--status-bar-height);
  /* #endif */
}
</style>

常用编译条件

条件 说明
MP 所有小程序
MP-WEIXIN 微信小程序
MP-ALIPAY 支付宝小程序
H5 H5网页
APP-PLUS APP(iOS和Android)
APP-PLUS-NVUE APP nvue页面

页面生命周期

export default {
  // 页面加载时触发,只触发一次
  onLoad(options) {
    console.log('页面参数:', options);
  },

  // 页面显示时触发,每次显示都触发
  onShow() {
    console.log('页面显示');
  },

  // 页面初次渲染完成
  onReady() {
    console.log('页面渲染完成');
  },

  // 页面隐藏时触发
  onHide() {
    console.log('页面隐藏');
  },

  // 页面卸载时触发
  onUnload() {
    console.log('页面卸载');
  },

  // 下拉刷新(需在pages.json中开启)
  onPullDownRefresh() {
    // 处理刷新
    uni.stopPullDownRefresh();
  },

  // 上拉触底
  onReachBottom() {
    // 加载更多
  },

  // 页面滚动
  onPageScroll(e) {
    console.log('滚动距离:', e.scrollTop);
  },

  // 用户点击分享
  onShareAppMessage() {
    return {
      title: '分享标题',
      path: '/pages/index/index'
    };
  },

  // 分享到朋友圈
  onShareTimeline() {
    return {
      title: '分享标题'
    };
  }
}

页面跳转与传参

跳转方式

// 保留当前页面,跳转到新页面
uni.navigateTo({
  url: '/pages/detail/index?id=1&name=test'
});

// 关闭当前页面,跳转到新页面
uni.redirectTo({
  url: '/pages/other/index'
});

// 关闭所有页面,跳转到新页面
uni.reLaunch({
  url: '/pages/index/index'
});

// 切换Tab页面
uni.switchTab({
  url: '/pages/user/index'
});

// 返回上一页
uni.navigateBack({
  delta: 1  // 返回的页面数
});

接收参数

export default {
  onLoad(options) {
    // 接收URL参数
    const id = options.id;
    const name = options.name;

    // 复杂参数需要编码
    const data = JSON.parse(decodeURIComponent(options.data));
  }
}

页面间通信

// 方式1:使用EventChannel(推荐)
// 发送页面
uni.navigateTo({
  url: '/pages/other/index',
  success: function(res) {
    res.eventChannel.emit('acceptData', { data: 'test' });
  }
});

// 接收页面
export default {
  onLoad() {
    const eventChannel = this.getOpenerEventChannel();
    eventChannel.on('acceptData', (data) => {
      console.log(data);
    });
  }
}

// 方式2:使用全局事件总线
// 发送
this.$eventHub.$emit('eventName', data);

// 接收
this.$eventHub.$on('eventName', (data) => {
  // 处理数据
});

// 移除监听(页面销毁时)
this.$eventHub.$off('eventName');

实战示例:创建商品列表页

完整代码示例

<!-- pages/goods/list/index.vue -->
<template>
  <view class="goods-list" :style="colorStyle">
    <!-- 搜索栏 -->
    <view class="search-bar">
      <view class="search-input" @tap="goSearch">
        <text class="iconfont icon-search"></text>
        <text class="placeholder">搜索商品</text>
      </view>
    </view>

    <!-- 筛选栏 -->
    <view class="filter-bar">
      <view 
        class="filter-item" 
        :class="{ active: sortType === 'default' }"
        @tap="changeSort('default')">
        <text>综合</text>
      </view>
      <view 
        class="filter-item" 
        :class="{ active: sortType === 'sales' }"
        @tap="changeSort('sales')">
        <text>销量</text>
      </view>
      <view 
        class="filter-item" 
        :class="{ active: sortType === 'price' }"
        @tap="changeSort('price')">
        <text>价格</text>
        <text class="iconfont" :class="priceOrder === 'asc' ? 'icon-up' : 'icon-down'"></text>
      </view>
    </view>

    <!-- 商品列表 -->
    <scroll-view 
      scroll-y 
      class="goods-scroll"
      @scrolltolower="loadMore"
      refresher-enabled
      :refresher-triggered="isRefreshing"
      @refresherrefresh="onRefresh">

      <view class="goods-grid">
        <view 
          class="goods-item" 
          v-for="item in goodsList" 
          :key="item.id"
          @tap="goDetail(item.id)">
          <easyLoadimage 
            :image-src="item.image" 
            mode="aspectFill"
            class="goods-img">
          </easyLoadimage>
          <view class="goods-info">
            <text class="goods-name">{{ item.store_name }}</text>
            <view class="goods-price">
              <BaseMoney :money="item.price" symbolSize="24" integerSize="32"></BaseMoney>
              <text class="original-price">¥{{ item.ot_price }}</text>
            </view>
            <text class="goods-sales">已售 {{ item.sales }}</text>
          </view>
        </view>
      </view>

      <!-- 空状态 -->
      <emptyPage v-if="!goodsList.length && !loading" title="暂无商品"></emptyPage>

      <!-- 加载状态 -->
      <view class="load-status">
        <text v-if="loading">加载中...</text>
        <text v-else-if="!hasMore && goodsList.length">没有更多了</text>
      </view>
    </scroll-view>
  </view>
</template>

<script>
import { getProductList } from '@/api/store.js';
import emptyPage from '@/components/emptyPage.vue';
import colors from "@/mixins/color.js";

export default {
  name: 'GoodsList',
  mixins: [colors],
  components: { emptyPage },

  data() {
    return {
      loading: false,
      isRefreshing: false,
      goodsList: [],
      page: 1,
      limit: 10,
      hasMore: true,
      sortType: 'default',
      priceOrder: 'asc',
      categoryId: ''
    };
  },

  onLoad(options) {
    if (options.cid) {
      this.categoryId = options.cid;
    }
    this.getList();
  },

  methods: {
    async getList() {
      if (this.loading) return;
      this.loading = true;

      try {
        const params = {
          page: this.page,
          limit: this.limit,
          cid: this.categoryId,
          salesOrder: this.sortType === 'sales' ? 'desc' : '',
          priceOrder: this.sortType === 'price' ? this.priceOrder : ''
        };

        const res = await getProductList(params);
        const list = res.data.list || [];

        if (this.page === 1) {
          this.goodsList = list;
        } else {
          this.goodsList = [...this.goodsList, ...list];
        }

        this.hasMore = list.length === this.limit;
      } catch (e) {
        uni.showToast({ title: e.msg || '加载失败', icon: 'none' });
      } finally {
        this.loading = false;
        this.isRefreshing = false;
      }
    },

    onRefresh() {
      this.isRefreshing = true;
      this.page = 1;
      this.hasMore = true;
      this.getList();
    },

    loadMore() {
      if (this.hasMore && !this.loading) {
        this.page++;
        this.getList();
      }
    },

    changeSort(type) {
      if (type === 'price' && this.sortType === 'price') {
        this.priceOrder = this.priceOrder === 'asc' ? 'desc' : 'asc';
      } else {
        this.sortType = type;
        this.priceOrder = 'asc';
      }
      this.page = 1;
      this.hasMore = true;
      this.goodsList = [];
      this.getList();
    },

    goSearch() {
      uni.navigateTo({ url: '/pages/goods/search/index' });
    },

    goDetail(id) {
      uni.navigateTo({ url: `/pages/goods_details/index?id=${id}` });
    }
  }
};
</script>

<style lang="scss" scoped>
.goods-list {
  min-height: 100vh;
  background: #f5f5f5;
  display: flex;
  flex-direction: column;
}

.search-bar {
  padding: 20rpx;
  background: #fff;

  .search-input {
    height: 72rpx;
    background: #f5f5f5;
    border-radius: 36rpx;
    display: flex;
    align-items: center;
    padding: 0 24rpx;

    .iconfont {
      font-size: 32rpx;
      color: #999;
      margin-right: 12rpx;
    }

    .placeholder {
      color: #999;
      font-size: 28rpx;
    }
  }
}

.filter-bar {
  display: flex;
  background: #fff;
  border-bottom: 1rpx solid #eee;

  .filter-item {
    flex: 1;
    height: 88rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 28rpx;
    color: #333;

    &.active {
      color: var(--view-theme);
    }

    .iconfont {
      margin-left: 8rpx;
      font-size: 24rpx;
    }
  }
}

.goods-scroll {
  flex: 1;
  height: 0;
}

.goods-grid {
  display: flex;
  flex-wrap: wrap;
  padding: 20rpx;
}

.goods-item {
  width: calc(50% - 10rpx);
  margin-bottom: 20rpx;
  background: #fff;
  border-radius: 16rpx;
  overflow: hidden;

  &:nth-child(odd) {
    margin-right: 20rpx;
  }

  .goods-img {
    width: 100%;
    height: 340rpx;
  }

  .goods-info {
    padding: 16rpx;

    .goods-name {
      font-size: 26rpx;
      color: #333;
      line-height: 1.4;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
      overflow: hidden;
    }

    .goods-price {
      margin-top: 12rpx;
      display: flex;
      align-items: baseline;

      .original-price {
        margin-left: 12rpx;
        font-size: 22rpx;
        color: #999;
        text-decoration: line-through;
      }
    }

    .goods-sales {
      margin-top: 8rpx;
      font-size: 22rpx;
      color: #999;
    }
  }
}

.load-status {
  text-align: center;
  padding: 30rpx;
  color: #999;
  font-size: 24rpx;
}
</style>

注意事项

1. 分包限制

  • 主包大小限制:2MB
  • 分包大小限制:2MB
  • 总包大小限制:20MB(微信小程序)

2. 跨平台兼容

  • 使用条件编译处理平台差异
  • 避免使用平台特有API
  • 测试时需要在各平台验证

3. 性能优化

  • 合理使用分包加载
  • 图片使用懒加载组件
  • 长列表使用虚拟滚动
  • 避免频繁setData

4. 安全注意

  • 敏感信息不要存储在本地
  • API请求需要Token验证
  • 支付等敏感操作需二次验证

常见问题

Q1: 页面跳转参数过长怎么办?

// 使用编码
const params = encodeURIComponent(JSON.stringify(data));
uni.navigateTo({
  url: `/pages/detail/index?data=${params}`
});

// 或使用全局存储
uni.setStorageSync('tempData', data);
uni.navigateTo({
  url: '/pages/detail/index'
});

Q2: 如何获取页面高度?

onReady() {
  uni.getSystemInfo({
    success: (res) => {
      this.windowHeight = res.windowHeight;
      this.statusBarHeight = res.statusBarHeight;
    }
  });
}

Q3: 自定义导航栏如何适配?

<template>
  <view class="custom-nav" :style="{ paddingTop: statusBarHeight + 'px' }">
    <view class="nav-content" :style="{ height: navBarHeight + 'px' }">
      <!-- 导航内容 -->
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      statusBarHeight: 0,
      navBarHeight: 44
    };
  },
  onLoad() {
    const sysInfo = uni.getSystemInfoSync();
    this.statusBarHeight = sysInfo.statusBarHeight;
    // #ifdef MP-WEIXIN
    const menuButton = uni.getMenuButtonBoundingClientRect();
    this.navBarHeight = (menuButton.top - sysInfo.statusBarHeight) * 2 + menuButton.height;
    // #endif
  }
}
</script>

Q4: 如何处理登录状态?

import { checkLogin, toLogin } from '@/libs/login.js';

// 检查登录状态
if (!checkLogin()) {
  toLogin();
  return;
}

// 在API中设置noAuth跳过登录验证
export function getPublicData() {
  return request.get('public/data', {}, { noAuth: true });
}

Q5: 页面返回如何刷新上一页数据?

// 方式1:使用getCurrentPages
uni.navigateBack({
  success: () => {
    const pages = getCurrentPages();
    const prevPage = pages[pages.length - 1];
    if (prevPage && prevPage.refreshData) {
      prevPage.refreshData();
    }
  }
});

// 方式2:使用事件总线
// 返回前触发
this.$eventHub.$emit('refreshList');
// 上一页监听
onShow() {
  this.$eventHub.$on('refreshList', this.getList);
}
{{cateWiki.like_num}}人点赞
0人点赞
评论({{cateWiki.comment_num}}) {{commentWhere.order ? '评论从旧到新':'评论从新到旧'}} {{cateWiki.page_view_num}}人看过该文档
评论(0) {{commentWhere.order ? '评论从旧到新':'评论从新到旧'}} 13人看过该文档
评论
{{item.user ? item.user.nickname : ''}} (自评)
{{item.content}}
{{item.create_time}} 删除
{{item.like ? item.like.like_num : 0}} {{replyIndex == index ? '取消回复' : '回复'}}
评论
{{items.user ? items.user.nickname : '暂无昵称'}} (自评)
{{items.content}}
{{items.create_time}} 删除
{{items.like ? items.like.like_num : 0}} {{replyIndexJ == (index+'|'+indexJ) ? '取消回复' : '回复'}}
评论
目录
  • {{item}}