JP4-8-MyLesson前台前端(一)

JP4-8-MyLesson前台前端(一)
Java道经 - 项目 - MyLesson - 前台前端(一)


传送门:JP4-8-MyLesson前台前端(一)
传送门:JP4-8-MyLesson前台前端(二)

文章目录

心法:ml-miniapp 是 MyLesson 项目中的前台用户交互载体,作为整个项目面向终端用户的核心入口,承担着连接系统服务与用户需求的关键桥梁作用,它基于微信小程序技术生态开发,充分依托微信平台的高渗透率、轻量化使用特性,让 MyLesson 系统的目标用户无需下载独立 App,仅通过微信即可快速访问,极大降低了用户的使用门槛,提升了系统的触达效率。

项目完整文件结构

|_ ml-miniapp |_ custom-tab-bar/*.* # 自定义底部导航栏目录:包含.wxml/.wxss/.js/.json等文件,用于实现个性化底部导航|_ miniprogram_npm # 小程序专用依赖目录:存放经转换后可在小程序环境直接运行的npm包(自动生成)|_ node_modules # 原始依赖存储目录:存放通过npm/yarn安装的所有原始依赖包(含开发依赖与生产依赖)|_ pages # 页面开发目录:每个子目录对应一个页面,包含 .wxml/.wxss/.js/.json 四个文件|_ utils |_ api.js # 请求工具文件:封装基本 CRUD 请求和拦截器等|_ const.js # 常量工具文件:集中管理项目常量,如API地址、状态码、路由路径等|_ util.js # 通用工具文件:存放格式化、验证、转换等通用业务工具方法|_ app.js # 项目入口文件:定义小程序生命周期、全局数据及全局方法|_ app.json # 全局配置文件:配置页面路径、窗口表现、tabBar基础配置、网络超时等|_ app.scss # 全局样式文件:配置全局样式|_ package.json # 项目依赖配置文件|_ package-lock.json # 项目依赖配置文件(锁定版本)|_ project.config.json # 配置小程序项目名称、appid、基础库版本、编译配置等|_ project.private.config.js # 存放个人开发相关配置,会覆盖 project.config.json 中的同名配置|_ sitemap.json # 配置微信索引对小程序页面的收录规则,影响小程序搜索可见性

S01. 基础环境搭建

心法:微信小程序的开发过程中,个人推荐使用【微信小程序开发工具】作为模拟器,使用 IDEA 作为编辑器。

微信小程序页面组成:微信小程序的每个页面都由四个同名文件组成,比如创建一个页面为 Apple,则需要同时创建以下四个文件:

页面描述
Apple.json用于配置当前页面的窗口表现,例如页面标题、导航栏颜色、使用哪些组件等
Apple.wxml即微信小程序的页面结构模板文件,采用类似 HTML 的标签语法
Apple.wxss用于定义当前 Apple 页面的样式,语法与 CSS 基本一致
Apple.js是当前页面的逻辑处理文件,负责页面的生命周期管理、数据定义、事件处理以及与用户交互相关的逻辑实现

微信小程序页面创建:当 IDEA 正确添加了微信小程序相关插件后,可直接创建同时包含四个文件的页面或组件:

  • 微信页面:右键 New -> Wechat Mini program Page 并输入名称即可,页面创建成功后,自动在 app.json 文件的 pages 块中添加对应的路径。
  • 微信组件:右键 New -> Wechat Mini program Component 并能输入名称即可,和页面不同,组件创建成功后,不会在 app.json 文件的 pages 块中添加对应的路径。
武技:使用微信开发者工具 + IDEA 搭建 ml-miniapp 项目基础环境。
  1. 下载微信开发者工具 wechat_devtools_xxxx_win32_x64.exe,傻瓜式安装即可,启动后需要扫码登录自己的微信账号。
  2. 创建微信小程序项目,具体选项如下:
备注
项目名称ml-miniapp
目录必须是一个空目录,且避免中文和特殊符号建议提前在本地创建一个 ml-miniapp 目录
AppIDxxx点击 测试号 按钮直接生成
开发模式小程序
后端服务不使用云服务
模板选择JS-基础模板
  1. 点击模拟器左上角,选择机型为 iPhone 15 Pro Max 100%,并选择显示比例为 自适应 即可:
在这里插入图片描述
  1. 左上角依次点击 设置 -> 通用设置 [ctrl + ,] -> 通用,然后修改 内存限制 为 2048M:
在这里插入图片描述
  1. 右上角依次点击 详情 -> 本地设置,然后调整 调试基础库 为当时百分比最高的版本(不一定是图示中的版本,哪个百分比高选哪个):
在这里插入图片描述
  1. 右上角依次点击 详情 -> 本地设置,然后勾选 不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书 项,以便于在开发阶段快速联调未备案域名、本地 IP 或 HTTP 接口,而无需提前配置正式域名与证书:
在这里插入图片描述
  1. 使用 IDEA 打开微信小程序项目,推荐将小程序项目作为顶层项目打开,否则某些快捷提示可能会失效,影响开发体验:
    • 调整 IDEA 的 UTF-8 编码。
    • 安装 Wechat mini program support 插件(ZXY 版本)。
    • 安装 WeChat weapp Support 插件。
  2. 在 IDEA 中,优化小程序初始代码:
    • 删除 /pages/index/index.wxml 中的全部内容(文件本身留着)。
    • 删除 /pages/index/index.wxss 中的全部内容(文件本身留着)。
    • 删除 /pages/logs 目录。
    • 优化 /pages/index/index.js 文件为 Page({ data: {} }) 代码。
    • 优化 /utils/util.js 文件为 module.exports = { } 代码。
    • 优化 /app.js 文件为 App({ globalData: {} }) 代码。
    • 优化 /app.json 文件如下:
{"pages":["pages/index/index"],"window":{"navigationBarTextStyle":"black","navigationBarTitleText":"我的课堂小程序","navigationBarBackgroundColor":"#ffffff"},"style":"v2","componentFramework":"glass-easel","sitemapLocation":"sitemap.json","lazyCodeLoading":"requiredComponents"}

E01. 安装基础组件

1. 样式预处理SCSS

心法:创建项目时选择的 JS 基础模板默认不支持 SCSS,若有需要,则需要先在项目中添加 SCSS 支持,然后手动将每个 .wxss 文件修改为 .scss 文件。

SCSS 语法教程:参考 JB2-8-CSS(二)S05 文章。

武技:在小程序项目中添加 SCSS 支持。
  1. 在 project.config.json 的 settings 块的首行添加 "useCompilerPlugins": ["sass"] 代码以添加 SCSS 支持,若已存在一个相同项,则需要将其删除:
{"setting":{"useCompilerPlugins":["sass"],...},}
  1. 将 app.wxss 文件修改为 app.scss,然后在该文件中添加一些全局样式,具体如下:
/* 自定义颜色变量 */ $bg-gray: #252a34; $black: #000000; $yellow: #f9ed69; $orange: #f08a5d; $white: #eaeaea; $gray: #757171; $subBlack: #232121;/* 全局样式 */page{background-color: $bg-gray; // 背景色 color: $white; // 前景色 font-size: 35rpx; // 字体大小 text-align: center; // 文字居中 font-family: 思源黑体, 微软雅黑, Consolas, sans-serif; // 字体 }/* 取消滚动条 */::-webkit-scrollbar{display: none; // 隐藏滚动条 width: 0; // 宽度 height: 0; // 高度 color: transparent; // 颜色 }/** vant分割线 */.van-divider{padding: 0 20rpx; // 内边距 }/** 底线 */.baseline{height: 200rpx; // 高度 padding-bottom: 50rpx; // 下边距 }

2. 前端框架VantWeapp

心法:Vant 是一个 轻量、可靠的移动端组件库,于 2017 年开源,目前 Vant 官方提供了 Vue 2 版本Vue 3 版本微信小程序版本,并由社区团队维护 React 版本支付宝小程序版本
武技:在小程序项目中添加 SCSS 支持。
  1. 安装 VantWeapp 依赖:
npminstall @vant/[email protected] --save
  1. 将 app.json 中的 "style": "v2" 去除,规避部分组件样式混乱。
  2. 在 project.config.json 文件中开启 NPM 支持:
    1. 使用 "packNpmManually": true 代码开启 NPM 支持。
    2. 使用 "packNpmRelationList" -> "packageJsonPath": "" 指定 NPM 依赖文件的位置。
    3. 使用 "packNpmRelationList" -> "miniprogramNpmDistDir": "" 指定 NPM 在哪个目录中打包。
{"setting":{..."packNpmManually":true,"packNpmRelationList":[{"packageJsonPath":"./package.json","miniprogramNpmDistDir":"./"}],}}
  1. 在微信小程序开发者工具的右上角依次点击 工具 -> 构建 npm,构建完成后,即可引入组件:
在这里插入图片描述

E02. 封装通用组件

1. 封装通用工具util

武技:开发通用工具 utils/util.js 文件。
/** * 判断非空值 * * @param value 被判断的值 * @return boolean 返回 true 表示不为 null 也不为 undefined */functionisNotNull(value){return value !==null&& value !==undefined;}/** * 判断空值 * * @param value 被判断的值 * @return boolean 返回 true 表示为 null 或 undefined */functionisNull(value){return!isNotNull(value);}/** * 判断是否存在空值 * * @param values 被判断的值,不定长列表 * @return boolean 返回 true 表示包含 null 或 undefined */functionhasNull(...values){for(let i in values){if(isNull(values[i])){returntrue;}}returnfalse;}/** * 判断空字符串 * * @param value 被判断的值 * @return boolean 返回 true 表示不为 null 或 undefined 或空字符串 */functionisNotEmpty(value){return value !==null&& value !==undefined&& value !=='';}/** * 判断非空字符串 * * @param value 被判断的值 * @return boolean 返回 true 表示为 null 或 undefined 或空字符串 */functionisEmpty(value){return!isNotEmpty(value);}/** * 判断是否存在空字符串 * * @param values 被判断的值,不定长列表 * @return boolean 返回 true 表示包含 null 或 undefined 或空字符串 */functionhasEmpty(...values){for(let i in values){if(isEmpty(values[i])){returntrue;}}returnfalse;}/** * 日期字符串处理:1999-01-02T12:12:12 -> 1999年01月02日 12:12 * * @param dateStr 日期字符串 * @return string 返回格式化后的日期字符串 */functiondatetimeFormat(dateStr){if(isNull(dateStr))return'';// 将日期字符串转为日期格式let date =newDate(dateStr);// 获取日期中的元素: 年,月,日,时,分lettoDouble=e=> e <10?'0'+ e : e;let yy =toDouble(date.getFullYear());let mm =toDouble(date.getMonth()+1);let dd =toDouble(date.getDate());let hh =toDouble(date.getHours());let mi =toDouble(date.getMinutes());// 返回美化后的日期字符串return`${yy}年${mm}月${dd}日 ${hh}:${mi}`;}/** * 日期字符串处理:1999-01-02T12:12:12 -> 1999年01月02日 * * @param dateStr 日期字符串 * @return string 返回格式化后的日期字符串 */functiondateFormat(dateStr){if(isNull(dateStr))return'';// 将日期字符串转为日期格式let date =newDate(dateStr);// 获取日期中的元素: 年,月,日,时,分lettoDouble=e=> e <10?'0'+ e : e;let yy =toDouble(date.getFullYear());let mm =toDouble(date.getMonth()+1);let dd =toDouble(date.getDate());// 返回美化后的日期字符串return`${yy}年${mm}月${dd}日`;}/** * 随机生成一个len位的字符串 * * @param len 随机字符串位数,范围在1 ~ 36之间,默认为18 * @return string 随机字符串 * */functionrandomStr(len =18){// 随机一个小数,并将其转为36进制字符并消除所有点符号let result = Math.random().toString(36).replaceAll('.','');// 截取后len位字符串并返回: len不小于1且不超过36 len = Math.max(len,1); len = Math.min(len,36);return result.slice(-len);}/** * 随机生成一个十六进制的颜色值 * * @returns {string} 十六进制的颜色值如 '#000000' */functionrandomColor(){const rgb =[]for(let i =0; i <3;++i){let color = Math.floor(Math.random()*256).toString(16) color = color.length ===1?'0'+ color : color rgb.push(color)}return'#'+ rgb.join('')}/** * 带按钮的确认弹框 * * @param content 弹框内容 * @param onConfirm 确认时的回调函数 * @param onCancel 取消时的回调函数 */functionconfirm(content, onConfirm, onCancel){ wx.showModal({title:'提示',content: content,success:function(sm){if(sm.confirm && onConfirm)onConfirm();if(sm.cancel && onCancel)onCancel();}});}/** * 跳转到另一个标签页 * * @param url 目标页面地址 * @param reload 是否重新加载目标页面,默认true */functiontab(url, reload =true){ wx.switchTab({url: url,success:()=>{if(reload){let page =getCurrentPages().pop();if(page) page.onLoad();}}});}/** * 跳转到另一个普通页 * * @param url 目标页面地址 * @param reload 是否重新加载目标页面,默认true */functionpage(url, reload =true){ wx.navigateTo({url: url,success:()=>{if(reload){let page =getCurrentPages().pop();if(page) page.onLoad();}}});}/** * 成功提示框(微信) * * @param title 提示文字内容,长度限制约 7 个汉字(超出会被截断)。 */functionsuccess(title){ wx.showToast({title: title,icon:'success',duration:2000});}/** * 失败提示框(微信) * * @param title 提示文字内容,长度限制约 7 个汉字(超出会被截断)。 */functionerror(title){ wx.showToast({title: title,icon:'error',duration:2000});}/** * 普通提示框(微信) * * @param title 提示内容,长度无限制 */functiontip(title){ wx.showToast({title: title,icon:'none',duration:2000});}/** * 模态框(微信) * * @param title 提示文字 * @param content 提示内容 * @param success 用户点击确定后的回调函数 * @param cancel 用户点击取消后的回调函数 */functionmodal(title, content, success, cancel){ wx.showModal({title: title,content: content,success:function(res){if(res.confirm){if(success)success();}else{if(cancel)cancel();}}});}/** * 判断用户是否登录 * * @returns {boolean} false未登录,true已登录 */functionisLogin(){if(!wx.getStorageSync('token')){error('请先登录');setTimeout(()=>{page('/pages/index/login-by-account/login-by-account',false);},500);returnfalse;}returntrue;}// 导出模块 module.exports ={ isNotNull, isNull, hasNull, isNotEmpty, isEmpty, hasEmpty, dateFormat, datetimeFormat, randomStr, randomColor, confirm, tab, page, success, error, tip, modal, isLogin }

2. 封装常量工具const

武技:开发常量工具 utils/const.js 文件。
// 环境IP地址constHOST='localhost';constLINUX_HOST='192.168.40.77';constGATEWAY_HOST=`http://${HOST}:24101`;constSOCKET_SERVER=`ws://${HOST}:24107`;constMINIO_HOST=`http://${LINUX_HOST}:9001/mylesson/`;constMINIO_AVATAR=MINIO_HOST+'/avatar/';constMINIO_BANNER=MINIO_HOST+'/banner/';constMINIO_COURSE_COVER=MINIO_HOST+'/course-cover/';constMINIO_COURSE_SUMMARY=MINIO_HOST+'/course-summary/';constMINIO_EPISODE_VIDEO=MINIO_HOST+'/episode-video/';constMINIO_EPISODE_VIDEO_COVER=MINIO_HOST+'/episode-video-cover/';constUPLOAD_AVATAR_URL=GATEWAY_HOST+'/user-server/api/v1/user/uploadAvatar/';// 常用请求状态码constSTATUS={SUCCESS:1000}// 常用表单规则constRULE={TITLE:[{pattern:/^.{1,42}$/,message:'标题长度必须在1~42之间'}],AUTHOR:[{pattern:/^.{1,42}$/,message:'作者名称长度必须在1~42之间'}],INFO:[{pattern:/^.{1,170}$/,message:'描述长度必须在1~170之间'}],CONTENT:[{pattern:/^.{1,170}$/,message:'内容长度必须在1~170之间'}],VCODE:[{pattern:/^[0-9]{6}$/,message:'验证码必须是6位数字'}],MENU_URL:[{pattern:/^\/[a-zA-Z]{0,256}$/,message:'跳转地址必须以 / 开头,后续内容仅支持0~256个英文字母'}],MENU_ICON:[{pattern:/^[a-zA-Z]{1,256}$/,message:'图标仅支持1~256个英文字母'}],USERNAME:[{pattern:/^[a-zA-Z0-9]{4,20}$/,message:'账号必须由4到20个英文字母或数字组成'}],PASSWORD:[{pattern:/^[a-zA-Z0-9]{4,20}$/,message:'密码必须由4到20个英文字母或数字组成'}],REALNAME:[{pattern:/^[\u4e00-\u9fa5]{2,6}$/,message:'真实姓名必须由2到6个中文组成'}],NICKNAME:[{pattern:/^[\u4e00-\u9fa5|_a-zA-Z0-9]{2,10}$/,message:'昵称必须由2到10个中文、英文或数字组成'}],IDCARD:[{pattern:/^[1-9]\d{5}(19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/,message:'身份证号格式不正确'}],PHONE:[{pattern:/^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\d{8}$/,message:'手机号码格式不正确'}],EMAIL:[{pattern:/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/,message:'电子邮箱格式不正确'}],PROVINCE:[{pattern:/^[\u4e00-\u9fa5]{2,20}$/,message:'省份必须由2到20个中文组成'}],CODE:[{pattern:/^.{1,42}$/,message:'兑换口令长度必须在1~42之间'}],SN:[{pattern:/^.{1,42}$/,message:'订单编号长度必须在1~42之间'}],}// 项目大标题constPROJECT_TITLE='绝对精品课 - 我的课堂';// 项目小标题constPROJECT_SUB_TITLE='Welcome To Lesson Project';// 省份选项constPROVINCE_OPTIONS=[{id:0,text:"华北地区",children:[{text:"北京市",id:"北京市"},{text:"天津市",id:"天津市"},{text:"河北省",id:"河北省"},{text:"山西省",id:"山西省"},{text:"内蒙古自治区",id:"内蒙古自治区"}]},{id:1,text:"东北地区",children:[{text:"辽宁省",id:"辽宁省"},{text:"吉林省",id:"吉林省"},{text:"黑龙江省",id:"黑龙江省"}]},{id:2,text:"华东地区",children:[{text:"上海市",id:"上海市"},{text:"江苏省",id:"江苏省"},{text:"浙江省",id:"浙江省"},{text:"安徽省",id:"安徽省"},{text:"福建省",id:"福建省"},{text:"江西省",id:"江西省"},{text:"山东省",id:"山东省"}]},{id:3,text:"中南地区",children:[{text:"河南省",id:"河南省"},{text:"湖北省",id:"湖北省"},{text:"湖南省",id:"湖南省"},{text:"广东省",id:"广东省"},{text:"广西壮族自治区",id:"广西壮族自治区"},{text:"海南省",id:"海南省"}]},{id:4,text:"西南地区",children:[{text:"重庆市",id:"重庆市"},{text:"四川省",id:"四川省"},{text:"贵州省",id:"贵州省"},{text:"云南省",id:"云南省"},{text:"西藏自治区",id:"西藏自治区"}]},{id:5,text:"西北地区",children:[{text:"陕西省",id:"陕西省"},{text:"甘肃省",id:"甘肃省"},{text:"青海省",id:"青海省"},{text:"宁夏回族自治区",id:"宁夏回族自治区"},{text:"新疆维吾尔自治区",id:"新疆维吾尔自治区"}]},{id:6,text:"港澳台地区",children:[{text:"台湾省",id:"台湾省"},{text:"香港特别行政区",id:"香港特别行政区"},{text:"澳门特别行政区",id:"澳门特别行政区"}]}]// 星座选项constZODIAC_OPTIONS=['白羊座(Aries)','金牛座(Taurus)','双子座(Gemini)','巨蟹座(Cancer)','狮子座(Leo)','处女座(Virgo)','天秤座(Libra)','天蝎座(Scorpio)','射手座(Sagittarius)','摩羯座(Capricorn)','水瓶座(Aquarius)','双鱼座(Pisces)'];// 导出 module.exports ={GATEWAY_HOST,SOCKET_SERVER,STATUS,RULE,MINIO_AVATAR,MINIO_BANNER,MINIO_COURSE_COVER,MINIO_COURSE_SUMMARY,MINIO_EPISODE_VIDEO,MINIO_EPISODE_VIDEO_COVER,UPLOAD_AVATAR_URL,PROJECT_TITLE,PROJECT_SUB_TITLE,PROVINCE_OPTIONS,ZODIAC_OPTIONS}

3. 封装请求工具api

武技:开发常量工具 utils/api.js 文件。
import util from'./util.js';import constant from'./const.js';/** * (私有)API前缀处理:根据模块名称返回API前缀 * * @param module 模块名称,如 user, course 等 * @return 对应的API前缀,如 /user-server/api/v1/user 等,末尾无 / 符号 * */functionapiPrefixFormat(module){// 微服务与模块的映射关系:key 为微服务名称,value 为模块名称数组const serviceModuleMap ={'user-server':['menu','role','user'],'course-server':['category','comment','course','episode','report','season'],'sale-server':['article','banner','coupons','notice','seckill','seckillDetail'],'order-server':['cart','order','orderDetail']};// 查找匹配的微服务名称:遍历微服务与模块的映射关系,找到包含当前模块 module 的微服务名称const microServiceName = Object.keys(serviceModuleMap).find(key=> serviceModuleMap[key].includes(module));// 返回拼接后的API前缀return`/${microServiceName}/api/v1/${module}`;}/** * (私有)请求方法 * * <p> module: 模块名称,如 user,必传。 * <p> url: 请求地址,如 /selectById,必传。 * <p> method: 请求方式,默认 GET。 * <p> header: 请求头,默认 application/json 类型并自动携带 Token 令牌。 * <p> params: API请求函数参数,JSON格式,作为请求参数。 * * @returns Promise 函数 */functionsendRequest(config){// 必传参数(需要空值保护)let module = config['module'];let url = config['url'];if(util.hasNull(module, url))return;// 可选参数(需要设置默认值)let method = config['method'];let params = config['params'];let header = config['header'];if(util.isEmpty(method)) method ='GET';if(util.isNull(header)){let token = wx.getStorageSync('token')||null; header ={'Content-Type':'application/json','token': token};}// 处理 url 参数:添加对应的网关路由名称 url = constant.GATEWAY_HOST+apiPrefixFormat(module)+ url;// 发送请求returnnewPromise((resolve, reject)=>{ wx.request({url: url,method: method,data: params,header: header,success(res){if(util.isNull(res)) util.error('服务器无响应');// 若存在2层data,则直接拆除第1层data res =undefined!== res.data &&undefined!== res.data['data']? res.data : res;// 请求成功,返回 data 数据if(res['code']=== constant.STATUS.SUCCESS){// DQL 操作成功时返回查询到的数据,DML 操作成功时返回 trueresolve(util.isNotNull(res.data)? res.data :true);}// 请求失败 - 服务器异常else{ util.error(res['message']); console.error(res['coderMessage']);// TODO 生产环境下删除}},// 请求失败fail(err){reject('请求异常: '+ err);}});});}/** * 发送 GET 请求 * * @param module 模块名称,如 user 等 * @param url 请求地址,如 /selectById 等 * @param params API请求函数参数,JSON格式,默认 null * @returns {Promise<unknown>} */functionget(module, url, params =null){returnsendRequest({module, url, params});}/** * 发送 POST 请求 * * @param module 模块名称,如 user 等 * @param url 请求地址,如 /selectById 等 * @param params API请求函数参数,JSON格式,默认 null * @returns {Promise<unknown>} */functionpost(module, url, params =null){returnsendRequest({module, url, params,method:'POST'});}/** * 发送 PUT 请求 * * @param module 模块名称,如 user 等 * @param url 请求地址,如 /selectById 等 * @param params API请求函数参数,JSON格式,默认 null * @returns {Promise<unknown>} */functionput(module, url, params =null){returnsendRequest({module, url, params,method:'PUT'});}/** * 发送 DEL 请求 * * @param module 模块名称,如 user 等 * @param url 请求地址,如 /selectById 等,默认 null * @param params API请求函数参数,JSON格式 * @returns {Promise<unknown>} */functiondel(module, url, params =null){returnsendRequest({module, url, params,method:'DELETE'});}// 导出 module.exports ={get, post, put, del};

E03. 开发底部导航栏

心法:微信小程序的底部支持自定义导航栏。
在这里插入图片描述
武技:开发小程序的底部导航栏。

1. 开发导航栏相关页面

武技:通过右键 New -> Wechat Mini program Page 的方式创建 “首页 + 课程 + 购物车 + 我的” 四个选项卡相关的页面(其中首页 index 是项目本身就存在的)。
  1. 开发 /pages/index/index 页面(json + wxml + scss + js)。
  2. 开发 /pages/course/course 页面(json + wxml + scss + js)。
  3. 开发 /pages/cart/cart 页面(json + wxml + scss + js)。
  4. 开发 /pages/user/user 页面(json + wxml + scss + js)。

2. 开发底部导航栏组件

心法:微信小程序自定义的底部导航栏需要自行创建,且注意目录名称,组件名称和位置均固定,不可随意更改。
武技:在项目根目录开发底部导航栏组件(自定义)。
  1. 在项目根目录创建 custom-tab-bar 目录,然后右键 New -> Wechat Mini program Component 创建 index 组件:
|_ ml-miniapp |_ custom-tab-bar |_ index.js |_ index.json |_ index.scss |_ index.wxml ... 
  1. 开发 index 组件:

custom-tab-bar/index.json

{"component":true,"usingComponents":{"van-tabbar":"@vant/weapp/tabbar/index","van-tabbar-item":"@vant/weapp/tabbar-item/index"}}

custom-tab-bar/index.wxml

<viewcustom-class="tab-bar"><van-tabbaractive="{{activeTab}}"bind:change="changeTab"><van-tabbar-itemwx:for="{{ tabs }}"wx:key="index"icon="{{item['icon']}}"> {{ item['text'] }} </van-tabbar-item></van-tabbar></view>

custom-tab-bar/index.scss

/**暂无*/

custom-tab-bar/index.js

import util from"../utils/util.js";Component({data:{// 底部导航栏 - 当前标签(索引)activeTab:0,// 底部导航栏 - 全部标签tabs:[{pagePath:"/pages/index/index",text:"首页",icon:'home-o'},{pagePath:"/pages/course/course",text:"课程",icon:'shop-o'},{pagePath:"/pages/cart/cart",text:"购物车",icon:'cart-o'},{pagePath:"/pages/user/user",text:"我的",icon:'user-o'}]},methods:{// 点击标签按钮时,切换标签页changeTab(ev){// 获取索引:首页0,课程1,购物车2,我的3let tabIndex = ev.detail;// 获取对应的标签页let tab =this.data.tabs[tabIndex];// 获取对应的跳转路径let pagePath = tab['pagePath'];// 切换标签页 util.tab(pagePath);}}});
  1. 在 app.json 中配置底部导航栏:
{..."tabBar":{"custom":true,"list":[{"pagePath":"pages/index/index"},{"pagePath":"pages/course/course"},{"pagePath":"pages/user/user"},{"pagePath":"pages/cart/cart"}]}}

3. 配置导航栏切换效果

武技:分别在四个标签页面的 onLoad 函数中修改 activeTab 变量,此时点击四个选项卡按钮的时候,会有切换页面的效果。

pages/index/index.js

Page({data:{},onLoad:function(options){this.getTabBar().setData({"activeTab":0});}})

pages/course/course.js

Page({data:{},onLoad:function(options){this.getTabBar().setData({"activeTab":1});}});

pages/cart/cart.js

Page({data:{},onLoad:function(options){this.getTabBar().setData({"activeTab":2});}});

pages/user/user.js

Page({data:{},onLoad:function(options){this.getTabBar().setData({"activeTab":3});}});

S02. 导航栏 - 首页

心法:本模块包含项目首页展示,用户登录页面(包括 账号登录手机 + 验证码登录 两种)和用户注册页面。

E01. 项目首页

心法:项目首页主要用于展示通知,标题,横幅,公告新闻和秒杀活动等营销类型的内容,此界面无需用户登录即可查看。

页面要素:位置自上而下:

页面要素描述
一条通知从后台取出第 1 条通知内容并滚动播放
登录提示若用户未登录时,提供提示和登录按钮
大小标题展示项目大标题和小标题
轮播图片从后台取出前 5 条横幅内容并滚动播放
公告列表从后台取出前 5 条新闻内容并使用折叠组件进行展示
整点秒杀从后台取出今日的秒杀活动并分别展示每场的秒杀物品详情

图示如下

在这里插入图片描述


在这里插入图片描述
武技:在 pages/index/ 目录下,开发项目首页。

index.json

{"usingComponents":{"van-sticky":"@vant/weapp/sticky/index","van-notice-bar":"@vant/weapp/notice-bar/index","van-image":"@vant/weapp/image/index","van-divider":"@vant/weapp/divider/index","van-collapse":"@vant/weapp/collapse/index","van-collapse-item":"@vant/weapp/collapse-item/index","van-tab":"@vant/weapp/tab/index","van-tabs":"@vant/weapp/tabs/index","van-card":"@vant/weapp/card/index","van-button":"@vant/weapp/button/index","van-empty":"@vant/weapp/empty/index"}}

index.wxml

<van-stickyclass="notice-bar"><van-notice-bartext="{{currentNotice}}"left-icon="volume-o"/></van-sticky><viewclass="login-tips"wx:if="{{isLogin}}"> 游客部分功能受限,点我 <textbind:tap="toLogin"class="link">登录系统</text>! </view><viewclass="project-title"><viewclass="title">{{PROJECT_TITLE}}</view><viewclass="sub-title">{{PROJECT_SUB_TITLE}}</view></view><viewclass="banner"><swiperwx:if="{{banners}}"autoplay="3000"><swiper-itemwx:for="{{banners}}"wx:key="id"><van-imagesrc="{{MINIO_BANNER}}/{{item['url']}}"width="100%"height="150px"fit="fill"/></swiper-item></swiper><van-emptywx:elsecustom-class="no-banner-tip"image="error"description="暂无轮播图片"/></view><viewclass="article"><van-dividercontentPosition="center"dashed>公告列表</van-divider><van-collapsecustom-class="article-collapse"value="{{ currentArticleIdx }}"bind:change="changeArticle"accordion><van-collapse-itemwx:for="{{articles}}"title="{{item['title']}}"name="{{item['idx']}}"wx:key="title"icon="fire-o"> {{item['content']}} </van-collapse-item></van-collapse></view><viewclass="seckill"><van-dividercontentPosition="center"dashed>整点秒杀</van-divider><van-tabswx:if="{{seckills && seckills.length > 0}}"active="{{ activeSeckillIdx }}"bind:change="changeSeckill"><van-tabwx:for="{{seckills}}"title="{{seckill['title']}}"wx:for-item="seckill"wx:key="id"><viewwx:if="{{seckill['seckillDetails'].length > 0}}"><van-cardwx:for="{{seckill['seckillDetails']}}"wx:key="id"title="{{item['courseTitle']}}"price="{{item['skPrice']}}"origin-price="{{item['coursePrice']}}"thumb="{{MINIO}}{{item['courseCover']}}"thumb-mode="scaleToFill"tag="秒杀"desc="描述信息"><viewslot="footer"><viewclass="sk-btn sk-btn-0"wx:if="{{seckill['status'] === 0}}">未开始</view><viewclass="sk-btn sk-btn-1"wx:if="{{seckill['status'] === 1}}">秒杀中</view><viewclass="sk-btn sk-btn-2"wx:if="{{seckill['status'] === 2}}">已结束</view></view></van-card></view><viewwx:else><van-emptyimage="error"description="暂无秒杀商品"/></view></van-tab></van-tabs><viewwx:else><van-emptyimage="error"description="暂无秒杀活动"/></view></view><viewclass="baseline"><van-dividercontentPosition="center"dashed>底线</van-divider></view>

index.scss

@import"app";.login-tips{color: $gray; // 前景色 margin-top: 20rpx;// 外边距 .link{color: $orange; // 前景色 }}.project-title{letter-spacing: 5rpx; // 字间距 line-height: 80rpx; // 行高 margin: 40rpx 60rpx;// 外边距 .title{font-size: 45rpx; // 字体大小 }.sub-title{font-size: 40rpx; // 字体大小 color: $orange; // 字体颜色 }}.banner{height: 400rpx; // 高度 margin: 0 20rpx;// 外边距 swiper, image{height: 400rpx; // 高度 }.no-banner-tip{height: 400rpx;// 高度 .van-empty__image{height: 210px; // 高度 width: 400rpx; // 宽度 }}}.article{text-align: left;// 文本对齐方式 .van-cell{background-color: $black; // 背景颜色 color: $yellow; // 字体颜色 font-weight: bold; // 字体粗细 letter-spacing: 5rpx; // 字间距 }.article-collapse{margin: 20rpx; // 外边距 }.van-collapse-item__content{color: $black; // 字体颜色 font-size: 25rpx; // 字体大小 }}.seckill{padding: 0 20rpx;// 内边距 .van-tab{background: $bg-gray; // 背景颜色 color: $white; // 字体颜色 }.van-tab--active{background: $black;// 背景颜色 .van-ellipsis{color: $yellow; // 字体颜色 }}.van-card{color: $white; // 字体颜色 background: $bg-gray; // 背景颜色 border: 2rpx solid $black; // 边框圆角 margin: 10rpx 0 0; // 外边距 }.van-card__content{padding-left: 30rpx; // 内边距 text-align: left; // 文本对齐方式 }.van-card__title{padding-top: 20rpx; // 内边距 font-size: large; // 字体大小 margin-bottom: 10rpx; // 外边距 color: $yellow; // 字体颜色 }.van-card__price{color: $orange; // 前景色 }.van-card__thumb{margin-top: 20rpx; // 外边距 width: 220rpx; // 宽度 }.sk-btn{width: 170rpx; // 宽度 height: 70rpx; // 高度 line-height: 70rpx; // 行高 text-align: center; // 文本对齐方式 font-size: large; // 字体大小 margin-top: -70px; // 外边距 float: right; // 浮动 rotate: -25deg; // 旋转 border-top-left-radius: 50%; // 边框圆角 border-bottom-right-radius: 50%; // 边框圆角 }.sk-btn-0, .sk-btn-2{opacity: 0.6; // 透明度 background-color: $subBlack; // 背景颜色 color: $white; // 字体颜色 }.sk-btn-1{background-color: $yellow; // 背景颜色 color: $orange; // 字体颜色 }}

index.js

import api from'../../utils/api.js';import constant from'../../utils/const.js';import util from"../../utils/util.js";Page({data:{isLogin:false,// 是否已登录currentNotice:null,// 当前通知对象PROJECT_TITLE: constant.PROJECT_TITLE,// 项目主标题PROJECT_SUB_TITLE: constant.PROJECT_SUB_TITLE,// 项目副标题MINIO_BANNER: constant.MINIO_BANNER,// 横幅轮播图片MINIO地址banners:null,// 横幅列表对象currentArticleIdx:1,// 当前公告的value值,用于切换公告articles:null,// 公告列表对象seckills:null,// 秒杀活动列表对象activeSeckillIdx:0,// 当前秒杀活动的value值,用于切换秒杀活动MINIO: constant.MINIO_COURSE_COVER// 课程封面图片MINIO地址},// 跳入登录页面toLogin:function(){ util.page('/pages/index/login-by-account/login-by-account',true);},// 查询1条通知记录topNotice1:function(){let that =this; api.get('notice','/top/1').then(res=> that.setData({currentNotice: res[0]['content']})).catch(err=> console.error(err));},// 查询5条轮播图记录topBanner5:function(){let that =this; api.get('banner','/top/5').then(res=> that.setData({banners: res})).catch(err=> console.error(err));},// 查询5条通知记录topArticle5:function(){let that =this; api.get('article','/top/5').then(res=> that.setData({articles: res})).catch(err=> console.error(err));},// 查询今日的秒杀活动todaySeckill:function(){let that =this; api.get('seckill','/today').then(res=> that.setData({seckills: res})).catch(err=> console.error(err));},// 切换公告changeArticle:function(ev){this.setData({'currentArticleIdx': ev.detail});},// 切换秒杀活动changeSeckill:function(ev){this.setData({'activeSeckillIdx': ev.detail});},// 加载函数onLoad:function(){this.setData({isLogin:!wx.getStorageSync('token')});this.topNotice1();this.topBanner5();this.topArticle5();this.todaySeckill();this.getTabBar().setData({"activeTab":0});},});

E02. 用户登录

1. 账号登录

心法:账号登录页面用于通过账号和密码进行系统登录,登录成功在前端保存用户信息和 Token 令牌,并会跳回首页。

页面要素:位置自上而下:

页面要素描述
账号输入框采集登录账号,测试环境下固定为 admin,开发环境下置空
密码输入框采集登录密码,测试环境下固定为 123456789,开发环境下置空
登录按钮向后台请求登录,提交账号和密码,登录成功后在前端保存用户信息和 Token 令牌
注册新账号按钮跳转到注册页面
手机号码登录按钮跳转到通过手机号码登录的页面
返回首页按钮跳转到首页选项卡
加载函数在前端清空用户信息和 Token 令牌

图示如下

在这里插入图片描述
武技:在 pages/index/login-by-account/ 目录下,开发账号登录页面。

login-by-account.json

{"usingComponents":{"van-field":"@vant/weapp/field/index","van-button":"@vant/weapp/button/index","van-row":"@vant/weapp/row/index","van-col":"@vant/weapp/col/index"}}

login-by-account.wxml

<viewclass="login-by-account-board"><van-fieldlabel="登录账号"model:value="{{username}}"custom-class="field"right-icon="user-o"border="{{false}}"clearable="{{true}}"placeholder="请输入账号"/><van-fieldlabel="登录密码"model:value="{{password}}"custom-class="field"type="password"right-icon="eye-o"border="{{false}}"clearable="{{true}}"placeholder="请输入密码"/><van-buttoncustom-class="login-btn"type="info"blockbind:tap="loginByAccount">登录</van-button><van-rowcustom-class="link"><van-colspan="8"bind:tap="toRegister">注册新账号</van-col><van-colspan="8"bind:tap="toLoginByPhone">手机号码登录</van-col><van-colspan="8"bind:tap="toIndex">返回首页</van-col></van-row></view>

login-by-account.scss

@import"app";.login-by-account-board{padding: 20rpx;// 内边距 .field{margin: 20rpx auto; // 外边距 border-radius: 15rpx; // 圆角 background-color: $bg-gray; // 背景颜色 border: 3rpx solid $black; // 边框 color: $white; // 字体颜色 text-align: left; // 左对齐 }.van-field__label{color: $yellow; // 字体颜色 }.van-field__control{color: $white; // 字体颜色 }.login-btn{font-size: large; // 字体大小 border-radius: 15rpx; // 圆角 }.link{font-size: 30rpx; // 字体大小 margin: 20rpx; // 外边距 color: $gray; // 字体颜色 }}

login-by-account.js

import util from"../../../utils/util.js";import api from"../../../utils/api.js";import constant from"../../../utils/const.js";Page({data:{username:'admin',// 登录账号password:'123456789',// 登录密码},// 通过账号密码登录系统loginByAccount:function(){let username =this.data.username;let password =this.data.password;// 空值校验if(util.hasEmpty(username, password)){ util.tip('账号或密码不能为空');return;}// 校验登录账号规则if(!constant.RULE.USERNAME[0]['pattern'].test(username)){ util.tip(constant.RULE.USERNAME[0]['message']);return;}// 校验登录密码规则if(!constant.RULE.PASSWORD[0]['pattern'].test(password)){ util.tip(constant.RULE.PASSWORD[0]['message']);return;}// 发送登录请求 api.post('user','/loginByAccount',{username, password}).then(res=>{// 将用户信息以及对应的Token令牌存储起来 wx.setStorageSync('token', res['token']); wx.setStorageSync('user', res['user']); util.success('登录成功');// 0.5秒后切换回 "首页" 选项卡setTimeout(()=> util.tab('/pages/index/index'),500);}).catch(err=> console.error(err));},// 跳转到注册页面toRegister:function(){ util.page('/pages/index/register/register',false);},// 跳转到手机登录页面toLoginByPhone:function(){ util.page('/pages/index/login-by-phone/login-by-phone',false);},// 返回首页toIndex:function(){ util.tab('/pages/index/index');},// 加载函数onLoad:function(options){ wx.removeStorageSync('token'); wx.removeStorageSync('user');}});

2. 手机登录

心法:手机登录页面用于通过手机号码和验证码进行系统登录:用户输入手机号码,然后点击 “发送验证码” 按钮,获取到一个验证码(测试环境下直接返回给前端并自动填充,生产环境下给对应手机号码发送短信)。用户输入验证码,点击登录按钮,向后台请求登录,提交手机号码和验证码。登录成功后在前端保存用户信息和 Token 令牌,并跳回首页。

页面要素:位置自上而下:

页面要素描述
手机号码输入框采集手机号码,测试环境下固定为 17766541438,开发环境下置空
短信验证码输入框采集验证码
发送验证码按钮向后台请求一个验证码,并自动填充到短信验证码输入框中
登录按钮向后台请求登录,提交手机号码和验证码,登录成功后在前端保存用户信息和 Token 令牌
注册新账号按钮跳转到注册页面
账号密码登录按钮跳转到通过账号密码登录的页面
返回首页按钮跳转到首页选项卡
加载函数在前端清空用户信息和 Token 令牌

图示如下

在这里插入图片描述
武技:在 pages/index/login-by-phone/ 目录下,开发手机登录页面。

login-by-phone.json

{"usingComponents":{"van-field":"@vant/weapp/field/index","van-button":"@vant/weapp/button/index","van-row":"@vant/weapp/row/index","van-col":"@vant/weapp/col/index"}}

login-by-phone.wxml

<viewclass="login-by-phone-board"><van-fieldlabel="手机号码"model:value="{{phone}}"custom-class="field"right-icon="user-o"border="{{false}}"clearable="{{true}}"placeholder="请输入手机号码"/><van-fieldlabel="短信验证码"model:value="{{vcode}}"custom-class="field"centeruse-button-slotborder="{{false}}"clearable="{{true}}"placeholder="请输入短信验证码"><van-buttonbind:tap="getVcode"slot="button"size="small"type="primary">发送验证码</van-button></van-field><van-buttoncustom-class="login-btn"bind:tap="loginByPhone"type="info"block>登录</van-button><van-rowcustom-class="link"><van-colspan="8"bind:tap="toRegister">注册新账号</van-col><van-colspan="8"bind:tap="toLoginByAccount">账号密码登录</van-col><van-colspan="8"bind:tap="toIndex">返回首页</van-col></van-row></view>

login-by-phone.scss

@import"app";.login-by-phone-board{padding: 20rpx;// 内边距 .field{margin: 20rpx auto; // 外边距 border-radius: 15rpx; // 圆角 background-color: $bg-gray; // 背景颜色 border: 3rpx solid $black; // 边框 color: $white; // 字体颜色 text-align: left; // 左对齐 }.van-field__label{color: $yellow; // 字体颜色 }.van-field__control{color: $white; // 字体颜色 }.login-btn{font-size: large; // 字体大小 border-radius: 15rpx; // 圆角 }.link{font-size: 30rpx; // 字体大小 margin: 20rpx; // 外边距 color: $gray; // 字体颜色 }}

login-by-phone.js

import api from"../../../utils/api.js";import util from"../../../utils/util.js";import constant from"../../../utils/const.js";Page({data:{phone:'17766541438',// 手机号码vcode:'',// 短信验证码},// 获取短信验证码getVcode:function(){let that =this;let phone =this.data.phone;// 空值校验if(util.isEmpty(phone)){ util.tip('手机号码不能为空');return;}// 校验手机号码规则if(!constant.RULE.PHONE[0]['pattern'].test(phone)){ util.tip(constant.RULE.PHONE[0]['message']);return;}// 发送请求:根据手机号码获取短信验证码 api.get('user','/getVcode/'+ phone).then(res=>{ util.success('验证码获取成功'); that.setData({vcode: res});}).catch(err=> console.error(err));},// 根据手机号码和验证码登录系统loginByPhone:function(){let phone =this.data.phone;let vcode =this.data.vcode;// 空值校验if(util.hasEmpty(phone, vcode)){ util.tip('手机号码或验证码不能为空');return;}// 校验手机号码规则if(!constant.RULE.PHONE[0]['pattern'].test(phone)){ util.tip(constant.RULE.PHONE[0]['message']);return;}// 发送登录请求 api.post('user','/loginByPhone',{phone, vcode}).then(res=>{// 将用户信息以及对应的Token令牌存储起来 wx.setStorageSync('token', res['token']); wx.setStorageSync('user', res['user']); util.success('登录成功');// 0.5秒后切换到 "首页" 选项卡setTimeout(()=> util.tab('/pages/index/index'),500);}).catch(err=> console.error(err));},// 跳转到注册页面toRegister:function(){ util.page('/pages/index/register/register',false);},// 跳转到账号登录页面toLoginByAccount:function(){ util.page('/pages/index/login-by-account/login-by-account',false);},// 返回首页toIndex:function(){ util.tab('/pages/index/index');},// 加载函数onLoad:function(options){ wx.removeStorageSync('token'); wx.removeStorageSync('user');}});

E03. 用户注册

心法:注册页面用于注册新账号,注册成功后会跳入账号密码登录页面。

页面要素:位置自上而下:

页面要素描述
登录账号输入框采集登录账号
登录密码输入框采集登录密码
确认密码输入框采集确认密码,仅用于规避误输入
真实姓名输入框采集真实姓名
手机号码输入框采集手机号码
身份证号输入框采集身份证号
电子邮箱输入框采集电子邮箱
注册按钮向后台请求注册,提交全部输入框数据
账号密码登录按钮跳转到通过账号密码登录的页面
手机号码登录按钮跳转到通过手机号码登录的页面
返回首页按钮跳转到首页选项卡

图示如下

在这里插入图片描述
武技:在 pages/index/register/ 目录下,开发注册页面。

register.json

{"usingComponents":{"van-field":"@vant/weapp/field/index","van-button":"@vant/weapp/button/index","van-row":"@vant/weapp/row/index","van-col":"@vant/weapp/col/index"}}

register.wxml

<viewclass="register-board"><van-fieldlabel="登录账号"model:value="{{ username }}"custom-class="field"right-icon="user-o"placeholder="请输入登录账号"clearable="{{true}}"border="{{ false }}"/><van-fieldlabel="登录密码"model:value="{{ password }}"type="password"custom-class="field"right-icon="eye-o"placeholder="请输入登录密码"clearable="{{true}}"border="{{ false }}"/><van-fieldlabel="确认密码"model:value="{{ rePassword }}"type="password"custom-class="field"right-icon="eye-o"placeholder="请确认登录密码"clearable="{{true}}"border="{{ false }}"/><van-fieldlabel="真实姓名"model:value="{{ realname }}"custom-class="field"right-icon="contact-o"placeholder="请输入真实姓名"clearable="{{true}}"border="{{ false }}"/><van-fieldlabel="手机号码"model:value="{{ phone }}"custom-class="field"right-icon="phone-o"placeholder="请输入手机号码"clearable="{{true}}"border="{{ false }}"/><van-fieldlabel="身份证号"model:value="{{ idcard }}"custom-class="field"right-icon="user-circle-o"placeholder="请输入身份证号"clearable="{{true}}"border="{{ false }}"/><van-fieldlabel="电子邮箱"model:value="{{ email }}"custom-class="field"right-icon="envelop-o"placeholder="请输入电子邮箱"clearable="{{true}}"border="{{ false }}"/><van-buttoncustom-class="register-btn"bind:tap="register"type="info"block>注册</van-button><van-rowcustom-class="link"><van-colspan="8"bind:tap="toLoginByAccount">账号密码登录</van-col><van-colspan="8"bind:tap="toLoginByPhone">手机号码登录</van-col><van-colspan="8"bind:tap="toIndex">返回首页</van-col></van-row></view>

register.scss

@import"app";.register-board{padding: 20rpx;// 内边距 .field{margin: 20rpx auto; // 外边距 border-radius: 15rpx; // 圆角 background-color: $bg-gray; // 背景颜色 border: 3rpx solid $black; // 边框 color: $white; // 字体颜色 text-align: left; // 左对齐 }.van-field__label{color: $yellow; // 字体颜色 }.van-field__control{color: $white; // 字体颜色 }.register-btn{font-size: large; // 字体大小 border-radius: 15rpx; // 圆角 }.link{font-size: 30rpx; // 字体大小 margin: 20rpx; // 外边距 color: $gray; // 字体颜色 }}

register.js

import util from"../../../utils/util.js";import api from"../../../utils/api.js";import constant from"../../../utils/const.js";Page({data:{username:'',password:'',rePassword:'',realname:'',phone:'',idcard:'',email:'',},// 注册账号register:function(){let username =this.data.username;let password =this.data.password;let rePassword =this.data.rePassword;let realname =this.data.realname;let phone =this.data.phone;let idcard =this.data.idcard;let email =this.data.email;// 检验两次密码是否一致if(password !== rePassword){ util.tip('两次密码不一致');return;}// 校验登录账号规则if(!constant.RULE.USERNAME[0]['pattern'].test(username)){ util.tip(constant.RULE.USERNAME[0]['message']);return;}// 校验登录密码规则if(!constant.RULE.PASSWORD[0]['pattern'].test(password)){ util.tip(constant.RULE.PASSWORD[0]['message']);return;}// 校验真实姓名规则if(!constant.RULE.REALNAME[0]['pattern'].test(realname)){ util.tip(constant.RULE.REALNAME[0]['message']);return;}// 校验电话号码规则if(!constant.RULE.PHONE[0]['pattern'].test(phone)){ util.tip(constant.RULE.PHONE[0]['message']);return;}// 校验身份证号规则if(!constant.RULE.IDCARD[0]['pattern'].test(idcard)){ util.tip(constant.RULE.IDCARD[0]['message']);return;}// 校验电子邮箱规则if(!constant.RULE.EMAIL[0]['pattern'].test(email)){ util.tip(constant.RULE.EMAIL[0]['message']);return;}// 发送登录请求let param ={username, password, realname, phone, idcard, email}; api.post('user','/insert', param).then(res=>{ util.success('注册成功');// 0.5秒后切换到登录页面setTimeout(()=>{ util.page('/pages/index/login-by-account/login-by-account',false);},500);}).catch(err=> console.error(err));},// 跳转到账号登录页面toLoginByAccount:function(){ util.page('/pages/index/login-by-account/login-by-account',false);},// 跳转到手机登录页面toLoginByPhone:function(){ util.page('/pages/index/login-by-phone/login-by-phone',false);},// 返回首页toIndex:function(){ util.tab('/pages/index/index');},// 加载函数onLoad:function(options){}});

S03. 导航栏 - 课程

心法:本模块包含课程列表页面和课程详情页面,这两个页面均不需要用户登录即可访问。

课程列表:课程列表是导航页面,当用户点击底部导航栏 “课程” 按钮时进行分页展示全部课程列表。

课程详情:当点击课程列表中的某个具体课程封面时,跳转到对应该课程的详情页面。

E01. 课程列表

心法:当用户点击底部选项卡 “课程” 的时候,跳转到课程列表页面,页面加载后,向后台请求课程列表(ElasticSearch 数据库)并渲染到页面,出于性能考虑,页面默认显示 12 条课程数据,当滚动条触底时,进行下一次分页查询(再查 12 条)。

页面要素:位置自上而下:

页面要素描述
搜索框用户可通过课程名称或者作者名称进行分词搜索
课程列表向后台请求课程列表(ElasticSearch 数据库)并渲染到页面

图示如下

在这里插入图片描述
武技:在 pages/course/ 目录下,开发课程列表相关页面。

course.json

{"usingComponents":{"van-sticky":"@vant/weapp/sticky/index","van-search":"@vant/weapp/search/index","van-divider":"@vant/weapp/divider/index","van-grid":"@vant/weapp/grid/index","van-grid-item":"@vant/weapp/grid-item/index","van-image":"@vant/weapp/image/index","van-icon":"@vant/weapp/icon/index","van-rate":"@vant/weapp/rate/index"}}

course.wxml

<van-sticky><van-searchclass="search-ipt"value="{{keyword}}"bind:search="searchByKeyword"bind:clear="cancelSearch"background="#252a34"shape="round"clear-trigger="always"input-align="center"placeholder="请输入课程标题进行搜索"/></van-sticky><scroll-viewclass="course-list"bindscrolltolower="onListEnd"scroll-y="true"><van-dividercontentPosition="center"dashed>课程列表</van-divider><van-gridclass="course-list-grid"column-num="2"gutter="20rpx"><van-grid-itemcontent-class="course-item"wx:for="{{courses}}"wx:key="id"data-course-id="{{item['id']}}"bind:tap="showDetail"use-slot><van-imagesrc="{{MINIO_COURSE_COVER}}/{{item['cover']}}"width="300rpx"height="200rpx"/><viewclass="title">{{ item['title'] }}</view><viewclass="info"><view><van-iconname="user-o"/> 作者: {{item['author']}} </view><view><van-iconname="gold-coin-o"/> 价格: {{item['price']}} 元 </view></view></van-grid-item></van-grid><van-dividercontentPosition="center"dashed>没有更多课程了</van-divider></scroll-view><viewclass="baseline"><van-dividercontentPosition="center"dashed>底线</van-divider></view>

course.scss

@import"app";.course-list{height: 90vh;// 高度 .course-item{background-color: $black;// 背景色 .title{font-weight: bold; // 加粗 white-space: nowrap; // 不换行 overflow: hidden; // 超出隐藏 text-overflow: ellipsis; // 超出部分显示... color: $yellow; // 字体颜色 font-size: 30rpx; // 字体大小 margin-top: 10rpx; // 上边距 margin-left: 20rpx; // 左边距 margin-bottom: 20rpx; // 下边距 text-align: left; // 文字左对齐 width: 100%; // 宽度 }.info{font-size: small; // 字体大小 text-align: left; // 文字左对齐 line-height: 40rpx; // 行高 width: 100%; // 宽度 margin-left: 20rpx; // 左边距 }}}

course.js

import util from'../../utils/util.js';import api from"../../utils/api.js";import constant from"../../utils/const.js";Page({data:{MINIO_COURSE_COVER: constant.MINIO_COURSE_COVER,// 课程封面路径pageInfo:{pageNum:1,pageSize:12,totalPage:0,totalRow:0},// 分页信息courses:null,// 课程列表对象keyword:'',// 课程标题},// 分页搜索课程记录page:function(){let that =this;let keyword =this.data.keyword;let pageNum =this.data.pageInfo['pageNum'];let pageSize =this.data.pageInfo['pageSize'];if(util.isNull(pageNum)) pageNum =1;if(util.isNull(pageSize)) pageSize =5;// 搜索关键字不能超过42个字if(keyword.length >42){ util.error('搜索关键字过长');return;}// 请求数据let params ={pageNum, pageSize,keyword: keyword.trim()}; api.get('course','/search', params).then(res=>{ that.setData({'courses': pageNum ===1? res['records']: that.data.courses.concat(res['records']),'pageInfo.pageNum': res['pageNum'],'pageInfo.pageSize': res['pageSize'],'pageInfo.totalPage': res['totalPage'],'pageInfo.totalRow': res['totalRow'],});}).catch(err=> console.error(err));},// 列表触底时onListEnd:function(){let that =this;let pageNum = that.data.pageInfo['pageNum'];let totalPage = that.data.pageInfo['totalPage'];if(pageNum < totalPage){// 查询下一页数据this.setData({'pageInfo.pageNum': pageNum +1});this.page();}},// 按课程标题搜索课程searchByKeyword:function(ev){this.setData({'keyword': ev.detail,'pageInfo.pageNum':1,});this.page();},// 取消搜索cancelSearch:function(ev){this.setData({'keyword':'','pageInfo.pageNum':1,});this.page();},// 查看课程详情showDetail:function(ev){let courseId = ev.currentTarget.dataset['courseId']; util.page('/pages/course/detail/detail?courseId='+ courseId,false);},// 加载函数onLoad:function(options){this.page();this.getTabBar().setData({"activeTab":1});}});

E02. 课程详情

心法:当用户在课程列表界面点击某个课程封面时,会跳转到对应的课程详情页面。

页面要素:位置自上而下:

页面要素描述
视频组件点击可播放当前课程的第一章第一集的视频内容
课程详情列表展示当前课程的详情,如类别,标题,作者,价格等
课程摘要选项卡,点击可展示课程摘要图片
课程目录选项卡,点击可展示课程章集目录
客服按钮点击跳入 AI 客服页面
购物车按钮点击跳入购物车选项卡
加入购物车按钮点击将当前课程加入我的购物车列表
立即购买按钮点击立刻购买该课程

图示如下

在这里插入图片描述|196


在这里插入图片描述
武技:在 pages/course/detail/ 目录下,开发课程详情相关页面。

detail.json

{"usingComponents":{"van-sticky":"@vant/weapp/sticky/index","van-divider":"@vant/weapp/divider/index","van-cell-group":"@vant/weapp/cell-group/index","van-cell":"@vant/weapp/cell/index","van-tabs":"@vant/weapp/tabs/index","van-tab":"@vant/weapp/tab/index","van-rate":"@vant/weapp/rate/index","van-image":"@vant/weapp/image/index","van-goods-action":"@vant/weapp/goods-action/index","van-goods-action-icon":"@vant/weapp/goods-action-icon/index","van-goods-action-button":"@vant/weapp/goods-action-button/index","van-empty":"@vant/weapp/empty/index"}}

detail.wxml

<van-stickyclass="free-video"><videowx:if="{{videoSrc && videoPoster}}"src="{{videoSrc}}"title="{{videoTitle}}"poster="{{videoPoster}}"danmu-list="{{welcomeBarrage}}"controlsshow-mute-btnenable-danmudanmu-btn/><van-emptyclass="free-video-empty"wx:elseimage="error"description="暂无免费视频"/></van-sticky><viewclass="course-info"wx:if="{{course}}"><van-dividercontentPosition="center"dashed>课程详情</van-divider><van-cell-groupinset><van-celltitle="课程类别"value="{{course['category']['title']}}"icon="flag-o"/><van-celltitle="课程标题"value="{{course['title']}}"icon="notes-o"/><van-celltitle="课程作者"value="{{course['author']}}"icon="user-o"/><van-celltitle="创建日期"value="{{course['created']}}"icon="clock-o"/><van-celltitle="更新日期"value="{{course['updated']}}"icon="clock-o"/><van-celltitle="课程价格"value="{{course['price'] + ' 元'}}"icon="cash-o"/><van-celltitle="课程简介"value="{{course['info']}}"icon="font-o"/></van-cell-group></view><viewclass="operation-tabs"><van-tabsactive="{{activeTab}}"color="orange"type="card"animatedtab-class="tab"><van-tabtitle="课程摘要"name="摘要"><viewclass="summary"wx:if="{{course['summary']}}"><van-imagesrc="{{MINIO_COURSE_SUMMARY}}{{course['summary']}}"width="100%"fit="widthFix"/></view><van-emptyclass="summary-empty"wx:elseimage="error"description="暂无摘要图片"/><viewclass="baseline"><van-dividercontentPosition="center"dashed>底线</van-divider></view></van-tab><van-tabtitle="课程目录"name="目录"><viewclass="catalog"wx:if="{{course['seasons'].length > 0}}"><viewwx:for="{{course['seasons']}}"wx:key="id"wx:for-item="season"wx:for-index="si"><van-cell-grouptitle="{{'第' + (si + 1) + '季:' + season['title']}}"><viewwx:if="{{season['episodes'].length > 0}}"><van-cellwx:for="{{season['episodes']}}"wx:key="id"wx:for-item="episode"wx:for-index="ei"title="{{'第' + (ei + 1) + '集:' + episode['title']}}"use-label-slot><viewslot="label"class="van-multi-ellipsis--l2">{{episode['info']}}</view></van-cell></view></van-cell-group></view><viewclass="baseline"><van-dividercontentPosition="center"dashed>底线</van-divider></view></view><van-emptyclass="catalog-empty"wx:elseimage="error"description="该课程暂无季次信息,请联系运维人员。"/></van-tab></van-tabs></view><viewclass="detail-foot"><van-goods-action><van-goods-action-icontext="客服"bind:click="chatMe"icon="chat-o"/><van-goods-action-icontext="购物车"bind:click="toCart"icon="cart-o"/><van-goods-action-buttontext="加入购物车"bind:click="addToCart"type="warning"/><van-goods-action-buttontext="立即购买"bind:click="pay"/></van-goods-action></view>

detail.scss

@import"app";.free-video{video{width: 98%; // 宽度 outline: 10rpx solid $black; // 轮廓 box-sizing: border-box; // 边框 }}.course-info{.van-cell{background-color: $bg-gray; // 背景色 color: $yellow;// 文字颜色 .van-cell__title{text-align: left; // 文字对齐方式 }.van-cell__value{color: $white; // 文字颜色 }}}.operation-tabs{height: 80vh; // 高度 margin: 20rpx 30rpx;// 外边距 .van-tab{color: $subBlack !important; // 文字颜色 font-weight: bold !important; // 字体粗细 }.summary{padding-top: 20rpx; // 内边距 }.catalog{padding-top: 20rpx; // 内边距 padding-bottom: 150rpx;// 内边距 .van-cell-group__title{color: $black; // 文字颜色 font-weight: bold; // 字体粗细 background: $yellow; // 背景色 padding-bottom: 15px; // 内边距 }.van-cell{color: $white; // 文字颜色 background: $black; // 背景色 text-align: left !important; // 文字对齐方式 }}}

detail.js

import api from'../../../utils/api.js';import util from'../../../utils/util.js';import constant from"../../../utils/const.js";Page({data:{MINIO_COURSE_SUMMARY: constant.MINIO_COURSE_SUMMARY,// 视频封面MINIO地址course:null,// 课程对象videoSrc:null,// 免费视频地址videoPoster:null,// 免费视频封面videoTitle:null,// 免费视频标题welcomeBarrage:[{text:'一大波弹幕即将来袭',color:'#ff0000',time:1}],// 欢迎弹幕activeTab:'摘要'// 当前选中的tab的name值},// 查询视频详情getCourseInfo:function(courseId){let that =this;let param ='/select/'+ courseId; api.get('course', param).then(res=>{ res['created']= util.dateFormat(res['created']); res['updated']= util.dateFormat(res['updated']);if(res['seasons'].length >0&& res['seasons'][0]['episodes'].length >0){let firstEpisode = res['seasons'][0]['episodes'][0]; that.setData({'videoSrc': constant.MINIO_EPISODE_VIDEO+ firstEpisode['video'],'videoPoster': constant.MINIO_EPISODE_VIDEO_COVER+ firstEpisode['cover'],'videoTitle': firstEpisode['title'],});} that.setData({'course': res});}).catch(err=> console.error(err));},// 跳转到购物车页面toCart:function(){if(util.isLogin()){ util.tab('/pages/cart/cart');}},// 添加购物车addToCart:function(){if(util.isLogin()){let that =this;let params ={'fkUserId': wx.getStorageSync('user').id,"fkCourseId": that.data.course['id'],}; api.post('cart','/insert', params).then(res=>{ util.error('加购成功');setTimeout(()=>{ util.tab('/pages/cart/cart',true);},500);}).catch(err=> console.error(err))}},// 客服chatMe:function(){ util.page('/pages/course/detail/chat/chat')},// 立即购买pay:function(){if(util.isLogin()){ util.error('功能暂未开放');}},// 加载函数onLoad:function(ev){// 查询视频详情:获取路径传递过来的值this.getCourseInfo(ev['courseId']);}});

1. 智能客服

心法:当用户在课程详情左下角点击客服图标时跳转到本页面。

页面要素:位置自上而下:

页面要素描述
对话框显示用户和智能客服的对话内容
输入框采集用户问题
提问按钮请求后台智能客服的回复内容并渲染在对话框中

图示如下

在这里插入图片描述
武技:在 pages/course/detail/chat/ 目录下,开发智能客服页面。

chat.json

{"usingComponents":{"van-field":"@vant/weapp/field/index","van-button":"@vant/weapp/button/index","van-steps":"@vant/weapp/steps/index"}}

chat.wxml

<scroll-viewclass="chat-board"scroll-top="{{scrollTop}}"scroll-y="{{true}}"><van-stepscustom-class="chat-dialog"id="chatDialog"steps="{{ steps }}"active="{{ active }}"direction="vertical"active-color="#f9ed69"desc-class="desc"/></scroll-view><viewclass="tips">{{isReplying ? '客服回复中...' : '回复完毕'}}</view><van-fieldcustom-class="chat-textarea"value="{{ message }}"bindinput="changeMessage"type="textarea"maxlength="{{500}}"show-word-limit="{{true}}"autosizeborder="{{false}}"placeholder="请输入你的问题"/><van-buttoncustom-class="submit-btn"bind:tap="sendMessage"type="info"block>提问</van-button>

chat.scss

@import"app";.chat-board{height: 1000rpx; // 高度 text-align: left; // 文本对齐方式 margin: 20rpx auto 50rpx;// 外边距 .chat-dialog{background-color: $bg-gray;// 背景色 .van-step:nth-child(even){text-align: right; // 文本对齐方式 }.van-step--finish{color: $white; // 字体颜色 }.desc{margin-top: 20rpx; // 上边距 font-size: larger; // 字体大小 font-weight: bolder; // 字体粗细 line-height: 1.5; // 行高 }}}.tips{font-size: smaller; // 字体大小 text-align: right; // 文本对齐方式 margin-right: 30rpx; // 右边距 color: $gray; // 字体颜色 }.chat-textarea{border: 3rpx solid $black; // 边框 width: auto !important; // 宽度 margin: 20rpx; // 外边距;border-radius: 15rpx; // 圆角;background-color: $bg-gray !important; // 背景色 }.van-field__control{color: $white !important; // 字体颜色 height: 100rpx !important; // 高度 }.submit-btn{width: auto !important; // 宽度 margin: 20rpx; // 外边距;font-size: large; // 字体大小 border-radius: 15rpx; // 圆角 }

chat.js

import util from"../../../../utils/util.js";Page({data:{message:'一首李白的诗',// 用户输入的问题active:0,// 当前激活的步骤steps:[{text:`${util.dateFormat(newDate())}`,desc:'你好,有什么可以帮助您?'}],sseServerUrl:'http://localhost:24107/api/v1/base/chat',// SSE服务端地址isReplying:false,// AI是否正在回复中scrollTop:0// 滚动条位置},// 当内容改变时触发changeMessage:function(ev){this.setData({'message': ev.detail});},// 发送消息sendMessage:function(){let that =this;// 如果AI正在回复中或用户输入的内容为空,均不处理if(this.data.isReplying || util.isEmpty(this.data.message))return;// 加入用户输入的信息this.data.steps.push({text:`${util.dateFormat(newDate())}`,desc:this.data.message });// 加入AI回复的信息this.data.steps.push({text:`${util.dateFormat(newDate())}`,desc:'waiting...'});// 更新聊天框this.setData({steps:this.data.steps,active:this.data.active +2,isReplying:true});// 发送请求并接收分块响应的消息 wx.request({url:this.data.sseServerUrl +'?msg='+this.data.message,enableChunked:true// 启用分块传输}).onChunkReceived((res)=>{// 获取当前AI回复的信息let desc = that.data.steps[that.data.active]['desc'];// 如果当前AI回复的信息是 "waiting...",则移除它if(desc ==='waiting...'){ that.data.steps[that.data.active]['desc']= desc.replaceAll('waiting...','');}// AI回复的信息类型固定为arrayBuffer,需要开发者自己进行转换为字符串let str =newTextDecoder('utf-8').decode(res.data,{stream:true});// 移除 "data:" str = str.replaceAll('data:','').trim();// 如果不是 "[over]",则拼接AI回复的信息if(!str.includes('[over]')){ that.data.steps[that.data.active]['desc']+= str;// 更新 that.setData({steps: that.data.steps});}// 否则,AI回复完毕else{// 更新 that.setData({isReplying:false});}// 滚动到底部const query = wx.createSelectorQuery().in(this); query.select('#chatDialog').boundingClientRect((rect)=>{if(rect)this.setData({scrollTop: rect.height});}).exec();});// 清空用户输入的内容 that.setData({'message':''});},onLoad:function(options){}});
Java道经 - 项目 - MyLesson - 前台前端(一)


传送门:JP4-8-MyLesson前台前端(一)
传送门:JP4-8-MyLesson前台前端(二)
Could not load content