8.4 简单留言板开发实例深化
本节深入探讨简单留言板小程序的开发细节,从需求分析、视图设计、数据库构建到关键技术实现,全方位展示小程序项目的构建过程。
8.4.1 需求分析细化
功能模块拆解:
- 浏览留言:不仅展示留言列表,还需实现分页加载、搜索过滤等功能,提高用户体验。留言列表应包含留言摘要、用户昵称、留言时间等基本信息。
- 发表留言:表单需验证用户输入,包括留言标题不能为空、内容长度限制等。支持表情、基本富文本编辑,提升表达丰富度。
- 删除留言:提供确认弹窗,防止误操作。仅留言作者或管理员有权删除。
- 编辑留言:在详情页增加编辑按钮,点击跳转至编辑页面,保留原留言内容以便修改。编辑后的留言应重新审核或直接生效,视安全策略而定。
**用户体验优化**:
- 加载动画:在数据加载时展示,增强等待时的友好度。
- 操作反馈:提交成功或失败时,给出明确提示,增强交互的确定性。
8.4.2 视图层设计深化
**页面结构细化**:
- **首页(留言展示页)**:
- 顶部导航栏,含搜索框、筛选条件(如按时间、热门排序)。
- 留言列表区,每条留言卡片展示:头像、用户名、留言时间、留言内容摘要及操作按钮(点赞、回复、更多操作)。
- 分页器,支持上下滑动加载更多或传统数字分页。
- 发表留言页:
- 输入框区域:标题、内容输入框,支持富文本编辑工具栏。
- 上传图片区域,限制图片数量和大小。
- 提交按钮,附带表单验证提示。
- 留言页:与发表留言页相似,但默认填充原有留言内容,额外显示“原始内容”与“编辑后内容”的对比预览。
- 留言详情页:
- 显示完整留言内容,支持展开/收起长文。
- 用户互动区:评论列表、点赞数、回复功能。
- 编辑和删除按钮,仅对留言作者可见。
UI/UX设计原则:保持界面简洁清爽,色彩和谐,图标直观,确保良好的视觉传达效果。
8.4.3 数据库设计深化
数据表结构优化:
- 表名:`messages`
- `id`(主键,自增),确保每条记录唯一。
- `user_id`(外键,关联用户表),记录留言者身份。
- `title`(字符串),留言标题,必要时可为空。
- `content`(文本),留言正文,支持HTML存储以实现富文本展示。
- `image_urls`(字符串数组),存放图片URL,支持多图上传。
- `timestamp`(时间戳),留言创建时间,用于排序。
- `status`(枚举:'active', 'deleted', 'edited'),标记留言状态,便于管理和检索。
**使用Bmob后端云服务**:
- 利用Bmob提供的RESTful API,实现数据的CRUD(创建Create、读取Read、更新Update、删除Delete)操作。
- 设定权限控制,确保用户只能访问和修改自己的留言数据,管理员拥有更多权限。
- 开启数据索引,针对频繁查询的字段(如`timestamp`)优化查询性能。
技术栈推荐
-前端:微信小程序框架(WXML + WXSS + JavaScript),利用小程序自带API处理页面渲染、事件监听、网络请求等。
- 后端:Bmob后端云服务,简化服务器部署与维护,提供强大的数据管理功能。
- 辅助工具:ESLint保证代码质量,Git进行版本控制,微信开发者工具加速调试效率。
通过上述细致规划与技术实施,可确保简单留言板小程序不仅功能完善,而且在用户体验、数据安全及性能优化方面均达到较高标准。
8.4.4 代码实现
1.应用配置
小程序代码实现的第一步是设置整个应用的配置,修改app.json,示例代码如下:
{
"pages": [
"pages/index/index",
"pages/logs/logs"
],
"window":{
"backgroundTextStyle":"light",
"navigationBarBackgroundColor":"#3891f8",
"navigationBarTitleText":"小小留言板",
"navigationBarTextStyle": "black"
}
}
首页实现留言内容的显示、添加、修改、获取、添加。
index.wxml代码如下:
<image class ="toWrite " bindtap ="toAddDiary"src ="../image/" style="width: 400px;"/>
<!-- 显示留言 -->
<view class = "page">
<scroll-view lower-threshold="800"
bindscrolltolower="pullUp-Load"upper-threshold ="0 "scroll-y="true"
style = "height: {{win-dowHeight}}px;">
<view class ="page__bd">
<button style="width: 100px;height: 50px; border: 1px saddlebrown;"><view class = "weui-panel__hd" style="text-align: center;">留言列表</view>
</button>
<view>
<block wx:if="{{diaryList.length >0}}">
<navigator class="weui-media-box weui-media-box_text"
wx:for="{{diaryList}}"
wx:key = "diaryItem"url ="/pages/index /detail? ob-jectId={{item.objectId}}&count={{item.count}}" >
<view class="title">
主题:{{item.title}}</view>
<view class ="content">留言内容:{{item.content}}</view><view class= "info">
<view class="time">时间:{{item.updatedAt}}</view ><view class="count">浏览:{{item.count}}</view>
<view class="operate">
<icon type ="cancel dels"size ="16" > </icon>
<text class ="del"catchtap ="deleteDiary"data-id ="iitem.objectId}}">删除</text>
<icon type="success edits"size="16"></icon>
<text catchtap ="toModifyDiary" data-id ="{{item.objectId}}"data-content="{{item.content}}"data-title="{{item.title}}">编辑</text>
</view>
</view>
</navigator>
</block>
</view>
</view>
</scroll-view>
</view>
<!-- 添加留言 -->
<view class ="js dialog" id="androidDialog1" style="opacity:1;"wx:if="{{writeDiary}}">
<view class="weui-mask"></view>
<view class ="weui-dialog weui-skin_android">
<view class="weui-dialog__hd">
<strong class="weui-dialog__title" style="margin-left: 150px;">添加留言</strong>
</view>
<form bindsubmit="addDiary" report-submit="true">
<view class="weui-dialog__bd">
<view class="weui-cells__title" style="text-align: center;">标题</view>
<view class="weui-cells weui-cells after-title" >
<view class="weui-cell weui-cell input">
<view class="weui-cell__bd">
<input class="weui-input"name="title" placeholder="请输入标题" style="text-align: center;"/>
</view>
</view>
</view>
<view class ="weui-cel1s title" style="text-align: center;">留言内容</view>
<view class="weui-cells weui-cells after-title">
<view class="weui-cell">
<view class="weui-cell bd">
<textarea class ="weui-textarea"name ="content"placeholder ="请输入留言内容" style="margin-left: 130px;" />
<view class="weui-textarea-counter" style="text-align: center;">0/200</view>
</view>
</view>
</view>
<view class="pic">
<view class ="pictext"bindtap="uppic" style="text-align: center;">添加图片</view><block wx:if="{isTypeof(url)})">
<image src="/pages/image/bei.jpg" style="width: 400px;"/>
</block>
<block wx:else>
<image src="{{url}}"/>
</block>
</view>
</view >
<view class="weui-dialog ft">
<button loading="{{loading}}"
class="weui-dialog__btn weui-diaLog__btn__primary"formType="info">取消</button>
<button loading="{{loading}}"
class="weui-dialog__btn weui-diaLog__btn__primary"formType="info">提交</button>
</view>
</form>
</view>
</view>
<!-- 修改留言 -->
<view class ="js_dialog"id="androidDialog2"style ="opacity: 1;"
wx:if ="{{modifyDiarys}}">
<view class="weui-mask"></view>
<view class="weui-dialog weui-skin android">
<view class="weui-dialog hd">
<strong class="weui-dialog title" style="text-align: center;">修改留言</strong>
</view >
<form bindsubmit="modifyDiary">
<view class="weui-dialog bd">
<view class="weui-cells title" style="text-align: center;">标题</view>
<input class ="weui =input"name ="title" value ="inowTitle"placeholder="请输入标题" style="text-align: center;"/>
<view class="weui-cells title" style="text-align: center;">留言内容</view>
<view class="weui-cells weui-cells after-title">
<view class="weui-cell">
<view class="weui-cell bd">
<textarea class ="weui-textarea"name ="content"value ="{{nowContent}}"placeholder="请输入留言内容"style="margin-left: 150px;" />
<view class="weui-textarea-counter" style="text-align: center;" style="text-align: center;">0/200 </view>
</view>
</view>
</view >
</view>
<view class="weui-dialog ft">
<view class="weui-dialog btn weui -dialog btn_default" bindtag="noneWindows">取消</view>
<button loading ="{{loading}}" class ="weui-dialog btn weui-diaLog btn primary"
formType="submit">提交</button>
</view>
</form>
</view>
</view>
index.js代码如下://引入Bmob逻辑文件及初始化数据
var Bmob = require('../../utils/bmob.js');
var common = require('../../utils/underscore');
var app = getApp();
var that;
var url = ""
Page({
data:{
writeDiary:false,//写留言
loading:false,
windowHeight:0,//定义窗口高度
windowWidth:0,//定义窗口宽度
limit:10, //定义数据提取条数
diaryList:[],//定义数据列表
modifyDiarys:false
},
//获取并显示留言数据
onShow:function(){
getList(this);
wx.getSystemInfo({ success:(res) => { that.setData({
windowHeight:res.windowHeight, windowWidth:res.windowWidth
})
}
})
/*
*获取数据*/
function getList(t,k){
that =t;
var Diary = Bmob.Object.extend("happy");//数据表 happy
var query = new Bmob.Query(Diary);
var queryl = new Bmob.Query(Diary);
query. descending('createdAt'); query.include( "own")//查询所有数据
query.limit(that.data.limit);
var mainQuery = Bmob.Query.or(query);
mainQuery.find({
success:function(results){//循环处理查询到的数据
console.log(results);
that.setData({
diaryList:results
})
},
error:function(error){
console.log("查询失败:"+error.code +"" + error.message);
}
});
}
//添加数据
},
toAddDiary:function() {
that.setData({
writeDiary:true
})
},
//添加图片
uppic:function(){
var that = this;
wx.chooseImage({
count:1,//默认9
sizeType:['compressed'],//可以指定是原图还是压缩图,默认二者都有
sourceType:['album','camera'],//可以指定来源是相册还是相机,默认二者都有
success:function(res){
var tempFilePaths=res.tempFilePaths;
if(tempFilePaths.length >0){
var newDate =new Date();
var newDateStr=newDate.toLocaleDateString();//获取当前日期做文件主
var tempFilePath=[tempFilePaths[0]];
var extension=/\([^.]*)$/.exec(tempFilePath[0]);//获取文件扩展名
if(extension){
extension =extension[1].toLowerCase();
}
var name =newDateStr+"."+extension;//上传的图片的别名
var file = new Bmob.File(name,tempFilePaths);
file.save().then(function(res){
console.log(res.url());
url = res.url();
that.setData({
url:ur1
})
},
function(error){
console.log(error);
}
)
}
}
})
},
//添加留言数据
addDiary:function(event){
var title =event.detail.value.title;
var content=event.detail.value.content;
var formId =event.detail.formId;
console.log("event".event)
if(! title){
common.showTip("标题不能为空","loading");
elseif(! content)
common.showTip("内容不能为空","loading");
}
else
that.setData({
loading:true
})
var currentUser=Bmob.User.current();
var User =Bmob.Object.extend("User");var UserModel=new User();
//增加留言
var Diary =Bmob.Object.extend("happy");//数据表
var diary =new Diary();
diary.set("title",title);//保存title字段内容
diary.set("formId",formId);//保存 formId
diary.set("content",content);//保存content字段内容
diary.set("image",url)//保存图片地址
diary.set("count",1)//保存浏览次数
if(currentUser){
UserModel.id =currentUser.id;
diary.set("own",UserModel);
}
//添加数据,第一个入口参数是nu11
diary.save(null,{
success:function(result){
common.showTip('添加日记成功');
that.setData({
writeDiary:false,
loading:false
})
var currentUser =Bmob.User.current();
that.onShow();
},
error:function(result,error){//添加失败
common.showTip('添加留言失败,请重新发布','loading');
}
});
},
//删除留言
deleteDiary:function(event){
var that =this;
var objectId=event.target.dataset.id;
wx.showModal({
title:'操作提示',
content:'确定要删除要留言?',
success:function(res){
if(res.confirm){
//删除留言
var Diary = Bmob.Object.extend("happy");
//创建查询对象,入口参数是对象类的实例
var query =new Bmob.Query(Diary);
query.get(objectId,{
success:function(object){
//The object was retrieved successfully.
object.destroy({
success:function(deleteObject){
console.log('删除留言成功');
getList(that)
},
error:function(object,error){
console.log('删除留言失败');
}
});
},
error,function(object,error){
console.log("query object fail");
}
});
}
}
})
},
toModifyDiary:function(event){
var nowTile =event.target.dataset.title;
var nowContent =event.target.dataset.content;
var nowId =event.target.dataset.id;
that.setData({
modifyDiarys :true,
nowTitle:nowTile,
nowContent:nowContent,
nowId:nowId
})
modifyDiary,function(e){
var t =this;
modify(t,e)
function modify(t,e){
var that = t;//修改日记
var modyTitle = e.detail.value.title;
var modyContent = e.detail.value.content;
var objectId =e.detail.value.content;
var thatTitle = that.data.nowTitle;
var thatContent= that.data.nowContent;
if((modyTitle != thatTitle ||modyContent != thatContent))
{ if(modyTitle == "" ||lmodyContent == ""){
common.showTip('标题或内容不能为空','1oading');}
else {
console.log(modyContent)
var Diary = Bmob.Object.extend("happy");
var query = new Bmob.Query(Diary);
//这个id是要修改条目的id,你在生成这个存储并成功时可以获取到,请看前面的文档
query.get(that.data.nowId,{
success:function(result){
//回调中可以取得这个GameScore对象的一个实例,然后就可以修改它了
result.set('title',modyTitle);
result.set('content',modyContent);
result.save();
common.showTip('留言修改成功','success',function(){ that.onShow(); that.setData({
modifyDiarys:false
})
});
},
error:function(object,error){
}
});
}
elseif(modyTitle == "" ||lmodyContent == "");{
common.showTip('标题或内容不能为空','1oading');
}
}
else {
that.setData({
modifyDiarys:false
})
common.showTip('修改成功','loading');
}
}
}
},
})
index.css代码如下:
page {
height: 100vh;
display: flex;
flex-direction: column;
}
.scrollarea {
flex: 1;
overflow-y: hidden;
}
.userinfo {
display: flex;
flex-direction: column;
align-items: center;
color: #aaa;
width: 80%;
}
.userinfo-avatar {
overflow: hidden;
width: 128rpx;
height: 128rpx;
margin: 20rpx;
border-radius: 50%;
}
.usermotto {
margin-top: 200px;
}
/* .toWrite{
position: absolute;
left: 30px;
width: 300px;
height: 300px;
} */
.avatar-wrapper {
padding: 0;
width: 56px !important;
border-radius: 8px;
margin-top: 40px;
margin-bottom: 40px;
}
/* .weui-panel__hd{
margin-top: 330px;
margin-left: 130px;
} */
.avatar {
display: block;
width: 56px;
height: 56px;
}
.nickname-wrapper {
display: flex;
width: 100%;
padding: 16px;
box-sizing: border-box;
border-top: .5px solid rgba(0, 0, 0, 0.1);
border-bottom: .5px solid rgba(0, 0, 0, 0.1);
color: black;
}
.nickname-label {
width: 105px;
}
.nickname-input {
flex: 1;
}
2.详情页
详情页用来详细显示某一留言信息:
log.wxml代码如下:
<view class= "page">
<view>
<view>
<view>留言主题:</view>
<view>{{rows.title}}</view><view>
<view>留言内容:</view>
<view>{{rows.content}}</view>
<view class = "pic">
<image src="{{rows.image}}"/></view><view>
浏览次数:{{rows.count}}</view >
<view>创建时间:{{rows.createdAt}} </view>
</view></view></view>
<view class="footer">
<text> Copyright©2017-2019www.smartbull.cn</text></view></view>
log.js代码如下:
var Bmob = require('../../utils/underscore');
Page({ data:{
rows:{} //留言详情
},
onLoad:function(e){
//页面初始化options为页面跳转所带来的参数
console.log(e.objectId)
var objectid = e.objectid;
var newcount =e.count;
var that = this;
var Diary = Bmob.Object.extend("test");
var query = new Bmob.Query(Diary);
query.get(objectId,{
success:function(result) {
console.log(result);
that.setData({ rows:result,})
newcount = parseInt(newcount)+1//浏览次数加1
result.set("count",newcount)//保存浏览次数 result.save()
},
error:function(result,error){ console.log("查询失败");
}
});
}
})
log.wxss代码如下:
page {
height: 100vh;
display: flex;
flex-direction: column;
}
.scrollarea {
flex: 1;
overflow-y: hidden;
}
.log-item {
margin-top: 20rpx;
text-align: center;
}
.log-item:last-child {
padding-bottom: env(safe-area-inset-bottom);
}