跳到主要内容MyLesson 小程序前台前端开发(一) | 极客日志JavaScriptWeChatAI大前端
MyLesson 小程序前台前端开发(一)
综述由AI生成MyLesson 微信小程序前台前端的开发流程。内容包括基础环境搭建(微信开发者工具、IDEA)、通用组件封装(工具类、常量、API 请求)、底部导航栏自定义实现。核心功能模块涵盖首页展示(通知、轮播、秒杀)、用户登录注册(账号、手机验证码)、课程列表(搜索、分页)及课程详情(视频播放、摘要、目录、智能客服)。基于 VantWeapp 组件库,采用 SCSS 预处理,实现了完整的前端交互逻辑。
星辰大海32 浏览 MyLesson 小程序前台前端开发(一)
S01. 基础环境搭建
微信小程序页面组成:微信小程序的每个页面都由四个同名文件组成,比如创建一个页面为 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 块中添加对应的路径。
E01. 安装基础组件
1. 样式预处理 SCSS
SCSS 语法教程:参考相关 CSS 文章。
在小程序项目中添加 SCSS 支持:
- 在 project.config.json 的 settings 块的首行添加
"useCompilerPlugins": ["sass"] 代码以添加 SCSS 支持,若已存在一个相同项,则需要将其删除:
{"setting":{"useCompilerPlugins":["sass"],...},}
- 将 app.wxss 文件修改为 app.scss,然后在该文件中添加一些全局样式,具体如下:
$bg-gray: #252a34; $black: #000000; $yellow: #f9ed69; : ; : ; : ; : ;
{
: ;
: ;
: rpx;
-align: center;
: 思源黑体,微软雅黑,Consolas, sans-serif;
}
::-webkit-scrollbar{
: none;
: ;
: ;
: transparent;
}
{: rpx;
{: rpx;
$orange
#f08a5d
$white
#eaeaea
$gray
#757171
$subBlack
#232121
page
background-color
$bg-gray
color
$white
font-size
35
text
font-family
display
width
0
height
0
color
.van-divider
padding
0
20
.baseline
height
200
2. 前端框架 VantWeapp
心法:Vant 是一个轻量、可靠的移动端组件库,于 2017 年开源,目前 Vant 官方提供了 Vue 2 版本、Vue 3 版本和微信小程序版本。
- 将 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
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])){
return true;
}
}
return false;
}
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])){
return true;
}
}
return false;
}
function datetimeFormat(dateStr){
if(isNull(dateStr))return '';
let date =new Date(dateStr);
let toDouble=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 =new Date(dateStr);
let toDouble=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}日`;
}
function randomStr(len =18){
let result = Math.random().toString(36).replaceAll('.','');
len = Math.max(len,1); len = Math.min(len,36);
return result.slice(-len);
}
function randomColor(){
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('')
}
function confirm(content, onConfirm, onCancel){
wx.showModal({
title:'提示',content: content,
success:function(sm){
if(sm.confirm && onConfirm)onConfirm();
if(sm.cancel && onCancel)onCancel();
}});
}
function tab(url, reload =true){
wx.switchTab({
url: url,
success:()=>{
if(reload){
let page =getCurrentPages().pop();
if(page) page.onLoad();
}
});
}
function page(url, reload =true){
wx.navigateTo({
url: url,
success:()=>{
if(reload){
let page =getCurrentPages().pop();
if(page) page.onLoad();
}
});
}
function success(title){
wx.showToast({
title: title,
icon:'success',
duration:2000});
}
function error(title){
wx.showToast({
title: title,
icon:'error',
duration:2000});
}
function tip(title){
wx.showToast({
title: title,
icon:'none',
duration:2000});
}
function modal(title, content, success, cancel){
wx.showModal({
title: title,content: content,
success:function(res){
if(res.confirm){
if(success)success();
}else{
if(cancel)cancel();
}
}});
}
function isLogin(){
if(!wx.getStorageSync('token')){
error('请先登录');
setTimeout(()=>{page('/pages/index/login-by-account/login-by-account',false);},500);
return false;
}
return true;
}
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 文件:
const HOST='localhost';
const LINUX_HOST='192.168.40.77';
const GATEWAY_HOST=`http://${HOST}:24101`;
const SOCKET_SERVER=`ws://${HOST}:24107`;
const MINIO_HOST=`http://${LINUX_HOST}:9001/mylesson/`;
const MINIO_AVATAR=MINIO_HOST+'/avatar/';
const MINIO_BANNER=MINIO_HOST+'/banner/';
const MINIO_COURSE_COVER=MINIO_HOST+'/course-cover/';
const MINIO_COURSE_SUMMARY=MINIO_HOST+'/course-summary/';
const MINIO_EPISODE_VIDEO=MINIO_HOST+'/episode-video/';
const MINIO_EPISODE_VIDEO_COVER=MINIO_HOST+'/episode-video-cover/';
const UPLOAD_AVATAR_URL=GATEWAY_HOST+'/user-server/api/v1/user/uploadAvatar/';
const STATUS={SUCCESS:1000}
const RULE={
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 之间'}],
}
const PROJECT_TITLE='绝对精品课 - 我的课堂';
const PROJECT_SUB_TITLE='Welcome To Lesson Project';
const PROVINCE_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:"澳门特别行政区"}]}
]
const ZODIAC_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
import util from'./util.js';
import constant from'./const.js';
function apiPrefixFormat(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}`;
}
function sendRequest(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;
return new Promise((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);
}});
});
}
function get(module, url, params =null){
return sendRequest({module, url, params});
}
function post(module, url, params =null){
return sendRequest({module, url, params,method:'POST'});
}
function put(module, url, params =null){
return sendRequest({module, url, params,method:'PUT'});
}
function del(module, url, params =null){
return sendRequest({module, url, params,method:'DELETE'});
}
module.exports ={get, post, put, del};
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:
<view custom-class="tab-bar"><van-tabbar active="{{activeTab}}" bind:change="changeTab"><van-tabbar-item wx: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 条新闻内容并使用折叠组件进行展示 |
| 整点秒杀 | 从后台取出今日的秒杀活动并分别展示每场的秒杀物品详情 |
{"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-sticky class="notice-bar"><van-notice-bar text="{{currentNotice}}" left-icon="volume-o"/></van-sticky><view class="login-tips" wx:if="{{isLogin}}"> 游客部分功能受限,点我 <text bind:tap="toLogin" class="link">登录系统</text>! </view><view class="project-title"><view class="title">{{PROJECT_TITLE}}</view><view class="sub-title">{{PROJECT_SUB_TITLE}}</view></view><view class="banner"><swiper wx:if="{{banners}}" autoplay="3000"><swiper-item wx:for="{{banners}}" wx:key="id"><van-image src="{{MINIO_BANNER}}/{{item['url']}}" width="100%" height="150px" fit="fill"/></swiper-item></swiper><van-empty wx:else custom-class="no-banner-tip" image="error" description="暂无轮播图片"/></view><view class="article"><van-divider contentPosition="center" dashed>公告列表</van-divider><van-collapse custom-class="article-collapse" value="{{ currentArticleIdx }}" bind:change="changeArticle" accordion><van-collapse-item wx:for="{{articles}}" title="{{item['title']}}" name="{{item['idx']}}" wx:key="title" icon="fire-o"> {{item['content']}}</van-collapse-item></van-collapse></view><view class="seckill"><van-divider contentPosition="center" dashed>整点秒杀</van-divider><van-tabs wx:if="{{seckills && seckills.length > 0}}" active="{{ activeSeckillIdx }}" bind:change="changeSeckill"><van-tab wx:for="{{seckills}}" title="{{seckill['title']}}" wx:for-item="seckill" wx:key="id"><view wx:if="{{seckill['seckillDetails'].length > 0}}"><van-card wx: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="描述信息"><view slot="footer"><view class="sk-btn sk-btn-0" wx:if="{{seckill['status'] === 0}}">未开始</view><view class="sk-btn sk-btn-1" wx:if="{{seckill['status'] === 1}}">秒杀中</view><view class="sk-btn sk-btn-2" wx:if="{{seckill['status'] === 2}}">已结束</view></view></van-card></view><view wx:else><van-empty image="error" description="暂无秒杀商品"/></view></van-tab></van-tabs><view wx:else><van-empty image="error" description="暂无秒杀活动"/></view></view><view class="baseline"><van-divider contentPosition="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 令牌 |
{"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"}}
<view class="login-by-account-board"><van-field label="登录账号" model:value="{{username}}" custom-class="field" right-icon="user-o" border="{{false}}" clearable="{{true}}" placeholder="请输入账号"/><van-field label="登录密码" model:value="{{password}}" custom-class="field" type="password" right-icon="eye-o" border="{{false}}" clearable="{{true}}" placeholder="请输入密码"/><van-button custom-class="login-btn" type="info" block bind:tap="loginByAccount">登录</van-button><van-row custom-class="link"><van-col span="8" bind:tap="toRegister">注册新账号</van-col><van-col span="8" bind:tap="toLoginByPhone">手机号码登录</van-col><van-col span="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 令牌 |
{"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"}}
<view class="login-by-phone-board"><van-field label="手机号码" model:value="{{phone}}" custom-class="field" right-icon="user-o" border="{{false}}" clearable="{{true}}" placeholder="请输入手机号码"/><van-field label="短信验证码" model:value="{{vcode}}" custom-class="field" center use-button-slot border="{{false}}" clearable="{{true}}" placeholder="请输入短信验证码"><van-button bind:tap="getVcode" slot="button" size="small" type="primary">发送验证码</van-button></van-field><van-button custom-class="login-btn" bind:tap="loginByPhone" type="info" block>登录</van-button><van-row custom-class="link"><van-col span="8" bind:tap="toRegister">注册新账号</van-col><van-col span="8" bind:tap="toLoginByAccount">账号密码登录</van-col><van-col span="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. 用户注册
注册页面用于注册新账号,注册成功后会跳入账号密码登录页面。
| 页面要素 | 描述 |
|---|
| 登录账号输入框 | 采集登录账号 |
| 登录密码输入框 | 采集登录密码 |
| 确认密码输入框 | 采集确认密码,仅用于规避误输入 |
| 真实姓名输入框 | 采集真实姓名 |
| 手机号码输入框 | 采集手机号码 |
| 身份证号输入框 | 采集身份证号 |
| 电子邮箱输入框 | 采集电子邮箱 |
| 注册按钮 | 向后台请求注册,提交全部输入框数据 |
| 账号密码登录按钮 | 跳转到通过账号密码登录的页面 |
| 手机号码登录按钮 | 跳转到通过手机号码登录的页面 |
| 返回首页按钮 | 跳转到首页选项卡 |
{"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"}}
<view class="register-board"><van-field label="登录账号" model:value="{{ username }}" custom-class="field" right-icon="user-o" placeholder="请输入登录账号" clearable="{{true}}" border="{{ false }}"/><van-field label="登录密码" model:value="{{ password }}" type="password" custom-class="field" right-icon="eye-o" placeholder="请输入登录密码" clearable="{{true}}" border="{{ false }}"/><van-field label="确认密码" model:value="{{ rePassword }}" type="password" custom-class="field" right-icon="eye-o" placeholder="请确认登录密码" clearable="{{true}}" border="{{ false }}"/><van-field label="真实姓名" model:value="{{ realname }}" custom-class="field" right-icon="contact-o" placeholder="请输入真实姓名" clearable="{{true}}" border="{{ false }}"/><van-field label="手机号码" model:value="{{ phone }}" custom-class="field" right-icon="phone-o" placeholder="请输入手机号码" clearable="{{true}}" border="{{ false }}"/><van-field label="身份证号" model:value="{{ idcard }}" custom-class="field" right-icon="user-circle-o" placeholder="请输入身份证号" clearable="{{true}}" border="{{ false }}"/><van-field label="电子邮箱" model:value="{{ email }}" custom-class="field" right-icon="envelop-o" placeholder="请输入电子邮箱" clearable="{{true}}" border="{{ false }}"/><van-button custom-class="register-btn" bind:tap="register" type="info" block>注册</van-button><van-row custom-class="link"><van-col span="8" bind:tap="toLoginByAccount">账号密码登录</van-col><van-col span="8" bind:tap="toLoginByPhone">手机号码登录</van-col><van-col span="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 数据库)并渲染到页面 |
{"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-search class="search-ipt" value="{{keyword}}" bind:search="searchByKeyword" bind:clear="cancelSearch" background="#252a34" shape="round" clear-trigger="always" input-align="center" placeholder="请输入课程标题进行搜索"/></van-sticky><scroll-view class="course-list" bindscrolltolower="onListEnd" scroll-y="true"><van-divider contentPosition="center" dashed>课程列表</van-divider><van-grid class="course-list-grid" column-num="2" gutter="20rpx"><van-grid-item content-class="course-item" wx:for="{{courses}}" wx:key="id" data-course-id="{{item['id']}}" bind:tap="showDetail" use-slot><van-image src="{{MINIO_COURSE_COVER}}/{{item['cover']}}" width="300rpx" height="200rpx"/><view class="title">{{ item['title'] }}</view><view class="info"><view><van-icon name="user-o"/> 作者:{{item['author']}}</view><view><van-icon name="gold-coin-o"/> 价格:{{item['price']} }元 </view></view></van-grid-item></van-grid><van-divider contentPosition="center" dashed>没有更多课程了</van-divider></scroll-view><view class="baseline"><van-divider contentPosition="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 客服页面 |
| 购物车按钮 | 点击跳入购物车选项卡 |
| 加入购物车按钮 | 点击将当前课程加入我的购物车列表 |
| 立即购买按钮 | 点击立刻购买该课程 |
{"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-sticky class="free-video"><video wx:if="{{videoSrc && videoPoster}}" src="{{videoSrc}}" title="{{videoTitle}}" poster="{{videoPoster}}" danmu-list="{{welcomeBarrage}}" controls show-mute-btn enable-danmudanmu-btn/><van-empty class="free-video-empty" wx:else image="error" description="暂无免费视频"/></van-sticky><view class="course-info" wx:if="{{course}}"><van-divider contentPosition="center" dashed>课程详情</van-divider><van-cell-group inset><van-cell title="课程类别" value="{{course['category']['title']}}" icon="flag-o"/><van-cell title="课程标题" value="{{course['title']}}" icon="notes-o"/><van-cell title="课程作者" value="{{course['author']}}" icon="user-o"/><van-cell title="创建日期" value="{{course['created']}}" icon="clock-o"/><van-cell title="更新日期" value="{{course['updated']}}" icon="clock-o"/><van-cell title="课程价格" value="{{course['price'] + ' 元'}}" icon="cash-o"/><van-cell title="课程简介" value="{{course['info']}}" icon="font-o"/></van-cell-group></view><view class="operation-tabs"><van-tabs active="{{activeTab}}" color="orange" type="card" animated tab-class="tab"><van-tab title="课程摘要" name="摘要"><view class="summary" wx:if="{{course['summary']}}"><van-image src="{{MINIO_COURSE_SUMMARY}}{{course['summary']}}" width="100%" fit="widthFix"/></view><van-empty class="summary-empty" wx:else image="error" description="暂无摘要图片"/><view class="baseline"><van-divider contentPosition="center" dashed>底线</van-divider></view></van-tab><van-tab title="课程目录" name="目录"><view class="catalog" wx:if="{{course['seasons'].length > 0}}"><view wx:for="{{course['seasons']}}" wx:key="id" wx:for-item="season" wx:for-index="si"><van-cell-group title="{{'第' + (si + 1) + '季:' + season['title']}}"><view wx:if="{{season['episodes'].length > 0}}"><van-cell wx:for="{{season['episodes']}}" wx:key="id" wx:for-item="episode" wx:for-index="ei" title="{{'第' + (ei + 1) + '集:' + episode['title']}}" use-label-slot><view slot="label" class="van-multi-ellipsis--l2">{{episode['info']}</view></van-cell></view></van-cell-group></view><view class="baseline"><van-divider contentPosition="center" dashed>底线</van-divider></view></view><van-empty class="catalog-empty" wx:else image="error" description="该课程暂无季次信息,请联系运维人员。"/></van-tab></van-tabs></view><view class="detail-foot"><van-goods-action><van-goods-action-icon text="客服" bind:click="chatMe" icon="chat-o"/><van-goods-action-icon text="购物车" bind:click="toCart" icon="cart-o"/><van-goods-action-button text="加入购物车" bind:click="addToCart" type="warning"/><van-goods-action-button text="立即购买" bind:click="pay"/></van-goods-action></view>
@import"app";
.free-video{
video{
width: 98%;
out-line: 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. 智能客服
当用户在课程详情左下角点击客服图标时跳转到本页面。
| 页面要素 | 描述 |
|---|
| 对话框 | 显示用户和智能客服的对话内容 |
| 输入框 | 采集用户问题 |
| 提问按钮 | 请求后台智能客服的回复内容并渲染在对话框中 |
{"usingComponents":{"van-field":"@vant/weapp/field/index","van-button":"@vant/weapp/button/index","van-steps":"@vant/weapp/steps/index"}}
<scroll-view class="chat-board" scroll-top="{{scrollTop}}" scroll-y="{{true}}"><van-steps custom-class="chat-dialog" id="chatDialog" steps="{{ steps }}" active="{{ active }}" direction="vertical" active-color="#f9ed69" desc-class="desc"/></scroll-view><view class="tips">{{isReplying ? '客服回复中...' : '回复完毕'}}</view><van-field custom-class="chat-textarea" value="{{ message }}" bindinput="changeMessage" type="textarea" maxlength="{{500}}" show-word-limit="{{true}}" autosize border="{{false}}" placeholder="请输入你的问题"/><van-button custom-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(new Date())}`,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(new Date())}`,desc:this.data.message });
this.data.steps.push({text:`${util.dateFormat(new Date())}`,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 =new TextDecoder('utf-8').decode(res.data,{stream:true});
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){
}});
相关免费在线工具
- RSA密钥对生成器
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
- Mermaid 预览与可视化编辑
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
- 随机西班牙地址生成器
随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online