跳到主要内容MyLesson 微信小程序前台前端开发(一) | 极客日志JavaScriptWeChat大前端
MyLesson 微信小程序前台前端开发(一)
MyLesson 微信小程序前台前端开发,包括环境搭建、SCSS 支持、VantWeapp 集成、通用工具封装、底部导航栏定制、首页营销模块、用户登录注册、课程列表分页查询及详情展示、智能客服对接等核心功能实现。
星辰大海30 浏览 文章目录
提示:ml-miniapp 是 MyLesson 项目中的前台用户交互载体,作为整个项目面向终端用户的核心入口,承担着连接系统服务与用户需求的关键桥梁作用,它基于微信小程序技术生态开发,充分依托微信平台的高渗透率、轻量化使用特性,让 MyLesson 系统的目标用户无需下载独立 App,仅通过微信即可快速访问,极大降低了用户的使用门槛,提升了系统的触达效率。
项目完整文件结构:
|_ ml-miniapp
|_ custom-tab-bar/*.*
|_ miniprogram_npm
|_ node_modules
|_ pages
|_ utils
|_ api.js
|_ const.js
|_ util.js
|_ app.js
|_ app.json
|_ app.scss
|_ package.json
|_ package-lock.json
|_ project.config.json
|_ project..config.js
|_ sitemap.json
private
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 项目基础环境。
| 项 | 值 | 备注 |
|---|
| 项目名称 | ml-miniapp | |
| 目录 | 必须是一个空目录,且避免中文和特殊符号 | 建议提前在本地创建一个 ml-miniapp 目录 |
| AppID | xxx | 点击 测试号 按钮直接生成 |
| 开发模式 | 小程序 | |
| 后端服务 | 不使用云服务 | |
| 模板选择 | JS-基础模板 | |
- 点击模拟器左上角,选择机型为 iPhone 15 Pro Max 100%,并选择显示比例为 自适应 即可:
- 左上角依次点击
设置 -> 通用设置 [ctrl + ,] -> 通用,然后修改 内存限制 为 2048M:
- 右上角依次点击
详情 -> 本地设置,然后调整 调试基础库 为当时百分比最高的版本(不一定是图示中的版本,哪个百分比高选哪个):
- 右上角依次点击
详情 -> 本地设置,然后勾选 不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书 项,以便于在开发阶段快速联调未备案域名、本地 IP 或 HTTP 接口,而无需提前配置正式域名与证书:
- 使用 IDEA 打开微信小程序项目,推荐将小程序项目作为顶层项目打开,否则某些快捷提示可能会失效,影响开发体验:
- 调整 IDEA 的 UTF-8 编码。
- 安装 Wechat mini program support 插件(ZXY 版本)。
- 安装 WeChat weapp Support 插件。
- 在 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 文件。
- 在 project.config.json 的 settings 块的首行添加
"useCompilerPlugins": ["sass"] 代码以添加 SCSS 支持,若已存在一个相同项,则需要将其删除:
{"setting":{"useCompilerPlugins":["sass"],...},}
- 将 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;
}
.van-divider{
padding: 0 20rpx;
}
.baseline{
height: 200rpx;
padding-bottom: 50rpx;
}
2. 前端框架 VantWeapp
- 将 app.json 中的
"style": "v2" 去除,规避部分组件样式混乱。
- 在 project.config.json 文件中开启 NPM 支持:
- 使用
"packNpmManually": true 代码开启 NPM 支持。
- 使用
"packNpmRelationList" -> "packageJsonPath": "" 指定 NPM 依赖文件的位置。
- 使用
"packNpmRelationList" -> "miniprogramNpmDistDir": "" 指定 NPM 在哪个目录中打包。
{"setting":{...
"packNpmManually":true,
"packNpmRelationList":[{"packageJsonPath":"./package.json","miniprogramNpmDistDir":"./"}],}}
- 在微信小程序开发者工具的右上角依次点击
工具 -> 构建 npm,构建完成后,即可引入组件:
E02. 封装通用组件
1. 封装通用工具 util
提示:开发通用工具 utils/util.js 文件。
function isNotNull(value){
return value !==null&& value !==undefined;
}
function isNull(value){
return!isNotNull(value);
}
function hasNull(...values){
for(let i in values){
if(isNull(values[i])){
returntrue;
}
}
returnfalse;
}
function isNotEmpty(value){
return value !==null&& value !==undefined&& value !=='';
}
function isEmpty(value){
return!isNotEmpty(value);
}
function hasEmpty(...values){
for(let i in values){
if(isEmpty(values[i])){
returntrue;
}
}
returnfalse;
}
function datetimeFormat(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}`;
}
function dateFormat(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}日`;
}
functionrandomStr(len =18){
let result = Math.random().toString(36).replaceAll('.','');
len = Math.max(len,1); len = Math.min(len,36);
return result.slice(-len);
}
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('')
}
functionconfirm(content, onConfirm, onCancel){
wx.showModal({
title:'提示',
content: content,
success:function(sm){
if(sm.confirm && onConfirm)onConfirm();
if(sm.cancel && onCancel)onCancel();
}});
}
functiontab(url, reload =true){
wx.switchTab({
url: url,
success:()=>{
if(reload){
let page =getCurrentPages().pop();
if(page) page.onLoad();
}});
}
functionpage(url, reload =true){
wx.navigateTo({
url: url,
success:()=>{
if(reload){
let page =getCurrentPages().pop();
if(page) page.onLoad();
}});
}
functionsuccess(title){
wx.showToast({
title: title,
icon:'success',
duration:2000});
}
functionerror(title){
wx.showToast({
title: title,
icon:'error',
duration:2000});
}
functiontip(title){
wx.showToast({
title: title,
icon:'none',
duration:2000});
}
functionmodal(title, content, success, cancel){
wx.showModal({
title: title,
content: content,
success:function(res){
if(res.confirm){
if(success)success();
}else{
if(cancel)cancel();
}});
}
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 文件。
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';
functionapiPrefixFormat(module){
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']};
const microServiceName = Object.keys(serviceModuleMap).find(key=> serviceModuleMap[key].includes(module));
return`/${microServiceName}/api/v1/${module}`;
}
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 = 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('服务器无响应');
res =undefined!== res.data &&undefined!== res.data['data']? res.data : res;
if(res['code']=== constant.STATUS.SUCCESS){
}
else{ util.error(res['message']); console.error(res['coderMessage']);
fail(err){reject('请求异常:'+ err);
}});
});
}
functionget(module, url, params =null){
returnsendRequest({module, url, params});
}
functionpost(module, url, params =null){
returnsendRequest({module, url, params,method:'POST'});
}
functionput(module, url, params =null){
returnsendRequest({module, url, params,method:'PUT'});
}
functiondel(module, url, params =null){
returnsendRequest({module, url, params,method:'DELETE'});
}
E03. 开发底部导航栏
1. 开发导航栏相关页面
提示:通过右键 New -> Wechat Mini program Page 的方式创建'首页 + 课程 + 购物车 + 我的'四个选项卡相关的页面(其中首页 index 是项目本身就存在的)。
- 开发
/pages/index/index 页面(json + wxml + scss + js)。
- 开发
/pages/course/course 页面(json + wxml + scss + js)。
- 开发
/pages/cart/cart 页面(json + wxml + scss + js)。
- 开发
/pages/user/user 页面(json + wxml + scss + js)。
2. 开发底部导航栏组件
提示:微信小程序自定义的底部导航栏需要自行创建,且注意目录名称,组件名称和位置均固定,不可随意更改。
- 在项目根目录创建 custom-tab-bar 目录,然后右键
New -> Wechat Mini program Component 创建 index 组件:
|_ ml-miniapp
|_ custom-tab-bar
|_ index.js
|_ index.json
|_ index.scss
|_ index.wxml
...
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:
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){
let tabIndex = ev.detail;
let tab =this.data.tabs[tabIndex];
let pagePath = tab['pagePath');
util.tab(pagePath);
}}});
{...
"tabBar":{"custom":true,"list":[{"pagePath":"pages/index/index"},{"pagePath":"pages/course/course"},{"pagePath":"pages/user/user"},{"pagePath":"pages/cart/cart"}]}}
3. 配置导航栏切换效果
提示:分别在四个标签页面的 onLoad 函数中修改 activeTab 变量,此时点击四个选项卡按钮的时候,会有切换页面的效果。
Page({data:{},onLoad:function(options){this.getTabBar().setData({"activeTab":0});}})
Page({data:{},onLoad:function(options){this.getTabBar().setData({"activeTab":1});}});
Page({data:{},onLoad:function(options){this.getTabBar().setData({"activeTab":2});}});
Page({data:{},onLoad:function(options){this.getTabBar().setData({"activeTab":3});}});
S02. 导航栏 - 首页
提示:本模块包含项目首页展示,用户登录页面(包括 账号登录 和 手机 + 验证码登录 两种)和用户注册页面。
E01. 项目首页
提示:项目首页主要用于展示通知,标题,横幅,公告新闻和秒杀活动等营销类型的内容,此界面无需用户登录即可查看。
| 页面要素 | 描述 |
|---|
| 一条通知 | 从后台取出第 1 条通知内容并滚动播放 |
| 登录提示 | 若用户未登录时,提供提示和登录按钮 |
| 大小标题 | 展示项目大标题和小标题 |
| 轮播图片 | 从后台取出前 5 条横幅内容并滚动播放 |
| 公告列表 | 从后台取出前 5 条新闻内容并使用折叠组件进行展示 |
| 整点秒杀 | 从后台取出今日的秒杀活动并分别展示每场的秒杀物品详情 |
提示:在 pages/index/ 目录下,开发项目首页。
{"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"}}
<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>
@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;
}}
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,
banners:null,
currentArticleIdx:1,
articles:null,
seckills:null,
activeSeckillIdx:0,
MINIO: constant.MINIO_COURSE_COVER
},
toLogin:function(){ util.page('/pages/index/login-by-account/login-by-account',true);},
topNotice1:function(){
let that =this; api.get('notice','/top/1').then(res=> that.setData({currentNotice: res[0]['content']})).catch(err=> console.error(err));},
topBanner5:function(){
let that =this; api.get('banner','/top/5').then(res=> that.setData({banners: res})).catch(err=> console.error(err));},
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/ 目录下,开发账号登录页面。
{"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"}}
<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>
@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;
}}
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=>{
wx.setStorageSync('token', res['token']); wx.setStorageSync('user', res['user']); util.success('登录成功');
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/ 目录下,开发手机登录页面。
{"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"}}
<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>
@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;
}}
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=>{
wx.setStorageSync('token', res['token']); wx.setStorageSync('user', res['user']); util.success('登录成功');
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/ 目录下,开发注册页面。
{"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"}}
<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>
@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;
}}
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('注册成功');
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/ 目录下,开发课程列表相关页面。
{"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"}}
<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>
@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;
}}}
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;
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 客服页面 |
| 购物车按钮 | 点击跳入购物车选项卡 |
| 加入购物车按钮 | 点击将当前课程加入我的购物车列表 |
| 立即购买按钮 | 点击立刻购买该课程 |
提示:在 pages/course/detail/ 目录下,开发课程详情相关页面。
{"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"}}
<van-stickyclass="free-video"><videowx:if="{{videoSrc && videoPoster}}"src="{{videoSrc}}"title="{{videoTitle}}"poster="{{videoPoster}}"danmu-list="{{welcomeBarrage}}"controls"show-mute-btn"enable-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>
@import"app";
.free-video{
video{
width: 98%;
outine: 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;
}}}
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,
course:null,
videoSrc:null,
videoPoster:null,
videoTitle:null,
welcomeBarrage:[{text:'一大波弹幕即将来袭',color:'#ff0000',time:1}],
activeTab:'摘要'
},
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/ 目录下,开发智能客服页面。
{"usingComponents":{"van-field":"@vant/weapp/field/index","van-button":"@vant/weapp/button/index","van-steps":"@vant/weapp/steps/index"}}
<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>
@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;
}
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',
isReplying:false,
scrollTop:0
},
changeMessage:function(ev){this.setData({'message': ev.detail});},
sendMessage:function(){
let that =this;
if(this.data.isReplying || util.isEmpty(this.data.message))return;
this.data.steps.push({text:`${util.dateFormat(newDate())}`,desc:this.data.message });
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)=>{
let desc = that.data.steps[that.data.active]['desc'];;
if(desc ==='waiting...'){ that.data.steps[that.data.active]['desc']= desc.replaceAll('waiting...','');}
let str =newTextDecoder('utf-8').decode(res.data,{stream:true});
str = str.replaceAll('data:','').trim();
if(!str.includes('[over]')){ that.data.steps[that.data.active]['desc']+= str;
else{
const query = wx.createSelectorQuery().in(this); query.select('#chatDialog').boundingClientRect((rect)=>{
if(rect)this.setData({scrollTop: rect.height});
}).exec();
}
onLoad:function(options){}});
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online