跳到主要内容MyLesson 微信小程序前台前端开发(一) | 极客日志JavaScriptWeChat大前端java
MyLesson 微信小程序前台前端开发(一)
综述由AI生成档详细介绍了 MyLesson 微信小程序前台前端的第一阶段开发过程。内容涵盖基础环境搭建,包括 SCSS 样式预处理和 VantWeapp 组件库的安装配置;通用工具封装,涉及 util 工具类、const 常量管理及 api 请求拦截器的实现;底部导航栏的开发与配置;首页功能实现,包括通知、轮播图、公告及秒杀活动的展示;用户登录与注册模块,支持账号密码及手机验证码两种方式;以及课程列表与详情页的开发,含分页加载、搜索功能及智能客服接入。文章提供了完整的代码示例与关键配置说明,适合前端开发者参考学习。
星落40 浏览 MyLesson 微信小程序前台前端开发(一)
S01. 基础环境搭建
设计思路: ml-miniapp 是 MyLesson 项目中的前台用户交互载体,作为整个项目面向终端用户的核心入口,承担着连接系统服务与用户需求的关键桥梁作用。它基于微信小程序技术生态开发,充分依托微信平台的高渗透率、轻量化使用特性。
项目完整文件结构:
|_ 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
|_ project.config.json
E01. 安装基础组件
1. 样式预处理 SCSS
实现步骤: 在小程序项目中添加 SCSS 支持。
- 在
project.config.json 的 settings 块的首行添加 "useCompilerPlugins": ["sass"] 代码以添加 SCSS 支持。
- 将
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: ;
: rpx;
-align: center;
: 思源黑体,微软雅黑,Consolas, sans-serif;
}
::-webkit-scrollbar {
: none;
: ;
: ;
: transparent;
}
{
: rpx;
}
{
: rpx;
: rpx;
}
$white
font-size
35
text
font-family
display
width
0
height
0
color
.van-divider
padding
0
20
.baseline
height
200
padding-bottom
50
2. 前端框架 VantWeapp
实现步骤: 在小程序项目中集成 VantWeapp。
- 安装 VantWeapp 依赖:
npm install @vant/[email protected] --save
- 将
app.json 中的 "style": "v2" 去除,规避部分组件样式混乱。
- 在
project.config.json 文件中开启 NPM 支持。
- 在微信小程序开发者工具的右上角依次点击
工具 -> 构建 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])) {
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: /^[一-龥]{2,6}$/, message: '真实姓名必须由 2 到 6 个中文组成' }],
NICKNAME: [{ pattern: /^[一-龥|_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: /^[一-龥]{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
实现步骤: 开发常量工具 utils/api.js 文件。
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) {
resolve(util.isNotNull(res.data) ? res.data : true);
}
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 组件。
- 开发 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:
<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 条新闻内容并使用折叠组件进行展示 |
| 整点秒杀 | 从后台取出今日的秒杀活动并分别展示每场的秒杀物品详情 |
实现步骤: 在 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-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 令牌,并会跳回首页。
实现步骤: 在 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"}}
<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. 手机登录
设计思路: 手机登录页面用于通过手机号码和验证码进行系统登录。
实现步骤: 在 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"}}
<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. 用户注册
设计思路: 注册页面用于注册新账号,注册成功后会跳入账号密码登录页面。
实现步骤: 在 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"}}
<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 条课程数据,当滚动条触底时,进行下一次分页查询。
实现步骤: 在 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-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. 课程详情
设计思路: 当用户在课程列表界面点击某个课程封面时,会跳转到对应的课程详情页面。
实现步骤: 在 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-sticky class="free-video">
<video wx:if="{{videoSrc && videoPoster}}" src="{{videoSrc}}" title="{{videoTitle}}" poster="{{videoPoster}}" danmu-list="{{welcomeBarrage}}" controls show-mute-btn enable-danmu danmu-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%;
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;
}
}
}
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-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 });
str = str.replaceAll('data:', '').trim();
if (!str.includes('[over]')) {
that.data.steps[that.data.active]['desc'] += str;
that.setData({ steps: that.data.steps });
} else {
that.setData({ isReplying: false });
}
const query = wx.createSelectorQuery().in(this);
query.select('#chatDialog').boundingClientRect((rect) => {
if (rect) that.setData({ scrollTop: rect.height });
}).exec();
});
that.setData({ 'message': '' });
},
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