前言:
今天我们要做的是基于Elasticsearch搜索和thymeleaf渲染页面,在今天的任务中将简单的介绍thymeleaf的基本使用,以及结合项目如何使用thymeleaf渲染页面。
观看thymeleaf的基本使用我已经写了一篇博客,连接地址:https://www.jianshu.com/p/29afd801bcc4 通过上一篇《基于Elasticsearch商品搜索(模仿某东搜索页面的简单实现)》
基础数据渲染
(1)更新SearchController,定义跳转搜索结果页面方法
@GetMapping("list")
public String list(@RequestParam Map<String, String> searchMap, Model model) {
//对搜索入参带有特殊符号进行处理
if (null != searchMap) {
Set<Map.Entry<String, String>> entries = searchMap.entrySet();
for (Map.Entry<String, String> entry : entries) {
if (entry.getKey().startsWith("spec_")) {
searchMap.put(entry.getKey(), entry.getValue().replace("+", "%2B"));
}
}
}
Map resultMap = searchService.search(searchMap);
//设置视图数据
//返回数据
model.addAttribute("resultMap", resultMap);
//查询条件数据
model.addAttribute("searchMap", searchMap);
return "search";
}
(2) 搜索结果页面渲染
(2.1)用户选择条件回显
<div class="bread">
<ul class="fl sui-breadcrumb">
<li>
<a href="#">全部结果</a>
</li>
<li class="active"><span th:text="${searchMap.keywords}"></span></li>
</ul>
<ul class="fl sui-tag">
<!-- 品牌-->
<li class="with-x" th:if="${#maps.containsKey(searchMap,'brand')}">品牌:
<span th:text="${searchMap.brand}"></span>
<i>×</i>
</li>
<!-- 价格-->
<li class="with-x" th:if="${#maps.containsKey(searchMap,'price')}">价格:<span
th:text="${searchMap.price}"></span>
<i>×</i>
</li>
<!-- 规格-->
<li class="with-x" th:each="sm:${searchMap}" th:if="${#strings.startsWith(sm.key,'spec_')}">
<span th:text="${#strings.replace(sm.key,'spec_','')}"></span>:
<span th:text="${#strings.replace(sm.value,'%2B','+')}"></span>
<i>×</i>
</li>
</ul>
<form class="fl sui-form form-dark">
<div class="input-control control-right">
<input type="text"/>
<i class="sui-icon icon-touch-magnifier"></i>
</div>
</form>
</div>
(2.2)商品属性及规格显示
<!--selector-->
<div class="clearfix selector">
<div class="type-wrap logo" th:unless="${#maps.containsKey(searchMap,'brand')}">
<div class="fl key brand">品牌</div>
<div class="value logos">
<ul class="logo-list">
<li th:each="brand,brandSate:${resultMap.brandList}">
<a th:text="${brand}" th:href="@{${url}(brand=${brand})}"></a>
</li>
</ul>
</div>
<div class="ext">
<a href="javascript:void(0);" class="sui-btn">多选</a>
<a href="javascript:void(0);">更多</a>
</div>
</div>
<div class="type-wrap" th:each="spec,specStat:${resultMap.skuSpecList}"
th:unless="${#maps.containsKey(searchMap,'spec_'+spec.key)}">
<div class="fl key" th:text="${spec.key}"></div>
<div class="fl value">
<ul class="type-list">
<li th:each="op,opStat:${spec.value}">
<a th:text="${op}" th:href="@{${url}('spec_'+${spec.key}=${op})}"></a>
</li>
</ul>
</div>
<div class="fl ext"></div>
</div>
<div class="type-wrap" th:unless="${#maps.containsKey(searchMap,'price')}">
<div class="fl key">价格</div>
<div class="fl value">
<ul class="type-list">
<li>
<a th:text="0-500元" th:href="@{${url}(price='0-500')}"></a>
</li>
<li>
<a th:text="500-1000元" th:href="@{${url}(price='500-1000')}"></a>
</li>
<li>
<a th:text="1000-1500元" th:href="@{${url}(price='1000-1500')}"></a>
</li>
<li>
<a th:text="1500-2000元" th:href="@{${url}(price='1500-2000')}"></a>
</li>
<li>
<a th:text="2000-3000元" th:href="@{${url}(price='2000-3000')}"></a>
</li>
<li>
<a th:text="3000元以上" th:href="@{${url}(price='3000')}"></a>
</li>
</ul>
</div>
<div class="fl ext">
</div>
</div>
<div class="type-wrap">
<div class="fl key">更多筛选项</div>
<div class="fl value">
<ul class="type-list">
<li>
<a>特点</a>
</li>
<li>
<a>系统</a>
</li>
<li>
<a>手机内存 </a>
</li>
<li>
<a>单卡双卡</a>
</li>
<li>
<a>其他</a>
</li>
</ul>
</div>
<div class="fl ext">
</div>
</div>
</div>
(2.3)商品列表显示
<!--商品列表-->
<div class="goods-list">
<ul class="yui3-g">
<li class="yui3-u-1-5" th:each="sku,skuStat:${resultMap.rows}">
<div class="list-wrap">
<div class="p-img">
<a href="item.html" target="_blank"><img src="/img/_/mobile01.png" /></a>
</div>
<div class="price">
<strong>
<em>¥</em>
<i th:text="${sku.price}"></i>
</strong>
</div>
<div class="attr">
<a target="_blank" href="item.html" th:title="${sku.spec}" th:utext="${sku.name}"></a>
</div>
<div class="commit">
<i class="command">已有<span>2000</span>人评价</i>
</div>
<div class="operate">
<a href="success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a>
<a href="javascript:void(0);" class="sui-btn btn-bordered">收藏</a>
</div>
</div>
</li>
</ul>
</div>
(3)关键字搜索
<div class="search">
<form th:action="@{/search/list}" class="sui-form form-inline">
<!--searchAutoComplete-->
<div class="input-append">
<input th:type="text" id="autocomplete" name="keywords"
th:value="${searchMap.keywords}" class="input-error input-xxlarge"/>
<button class="sui-btn btn-xlarge btn-danger" th:type="submit">搜索</button>
</div>
</form>
</div>
(4) 条件搜索实现
用户每次点击搜索的时候,其实在上次搜索的基础之上加上了新的搜索条件,也就是在上一次请求的URL后面追加了新的搜索条件,我们可以在后台每次拼接组装出上次搜索的URL,然后每次将URL存入到Model中,页面每次点击不同条件的时候,从Model中取出上次请求的URL,然后再加上新点击的条件参数实现跳转即可。
(4.1)后台记录搜索URL
StringBuilder url = new StringBuilder("/search/list");
//拼接url
if (searchMap != null && searchMap.size() > 0) {
url.append("?");
for (String paramKey : searchMap.keySet()) {
if (!"sortRule".equals(paramKey) && !"sortField".equals(paramKey) && !"PageNum".equals(paramKey)) {
url.append(paramKey).append("=").append(searchMap.get(paramKey)).append("&");
}
}
String urlString = url.toString();
//去掉最后一个&
urlString = urlString.substring(0, urlString.length() - 1);
model.addAttribute("url", urlString);
} else {
model.addAttribute("url", url);
}
(4.2)页面搜索对接
<div class="clearfix selector">
<div class="type-wrap logo" th:unless="${#maps.containsKey(searchMap,'brand')}">
<div class="fl key brand">品牌</div>
<div class="value logos">
<ul class="logo-list">
<li th:each="brand,brandSate:${resultMap.brandList}">
<a th:text="${brand}" th:href="@{${url}(brand=${brand})}"></a>
</li>
</ul>
</div>
<div class="ext">
<a href="javascript:void(0);" class="sui-btn">多选</a>
<a href="javascript:void(0);">更多</a>
</div>
</div>
<div class="type-wrap" th:each="spec,specStat:${resultMap.skuSpecList}"
th:unless="${#maps.containsKey(searchMap,'spec_'+spec.key)}">
<div class="fl key" th:text="${spec.key}"></div>
<div class="fl value">
<ul class="type-list">
<li th:each="op,opStat:${spec.value}">
<a th:text="${op}" th:href="@{${url}('spec_'+${spec.key}=${op})}"></a>
</li>
</ul>
</div>
<div class="fl ext"></div>
</div>
<div class="type-wrap" th:unless="${#maps.containsKey(searchMap,'price')}">
<div class="fl key">价格</div>
<div class="fl value">
<ul class="type-list">
<li>
<a th:text="0-500元" th:href="@{${url}(price='0-500')}"></a>
</li>
<li>
<a th:text="500-1000元" th:href="@{${url}(price='500-1000')}"></a>
</li>
<li>
<a th:text="1000-1500元" th:href="@{${url}(price='1000-1500')}"></a>
</li>
<li>
<a th:text="1500-2000元" th:href="@{${url}(price='1500-2000')}"></a>
</li>
<li>
<a th:text="2000-3000元" th:href="@{${url}(price='2000-3000')}"></a>
</li>
<li>
<a th:text="3000元以上" th:href="@{${url}(price='3000')}"></a>
</li>
</ul>
</div>
<div class="fl ext">
</div>
</div>
<div class="type-wrap">
<div class="fl key">更多筛选项</div>
<div class="fl value">
<ul class="type-list">
<li>
<a>特点</a>
</li>
<li>
<a>系统</a>
</li>
<li>
<a>手机内存 </a>
</li>
<li>
<a>单卡双卡</a>
</li>
<li>
<a>其他</a>
</li>
</ul>
</div>
<div class="fl ext">
</div>
</div>
</div>
(5)移除搜索条件
用户点击条件搜索后,要将选中的条件显示出来,并提供移除条件的 x 按钮,显示条件我们可以从searchMap中获取,移除其实就是将之前的请求地址中的指定条件删除即可。
<ul class="fl sui-tag">
<li class="with-x" th:if="${#maps.containsKey(searchMap,'brand')}">品牌:
<span th:text="${searchMap.brand}"></span>
<a th:href="@{${#strings.replace(url,'&brand='+searchMap.brand,'')}}">×</a>
</li>
<li class="with-x" th:if="${#maps.containsKey(searchMap,'price')}">价格:<span
th:text="${searchMap.price}"></span>
<a th:href="@{${#strings.replace(url,'&price='+searchMap.price,'')}}">×</a>
</li>
<!--规格-->
<li class="with-x" th:each="sm:${searchMap}" th:if="${#strings.startsWith(sm.key,'spec_')}">
<span th:text="${#strings.replace(sm.key,'spec_','')}"></span>:
<span th:text="${#strings.replace(sm.value,'%2B','+')}"></span>
<a th:href="@{${#strings.replace(url,'&'+sm.key+'='+sm.value,'')}}">×</a>
</li>
</ul>
(6)排序
<li>
<a th:href="@{${url}(sortRule='ASC',sortField='price')}">价格↑</a>
</li>
<li>
<a th:href="@{${url}(sortRule='DESC',sortField='price')}">价格↓</a>
</li>
(7)分页
加入分页工具类——传送门:https://www.jianshu.com/p/acdffb8b3859
实现分页信息封装:
//设置分页查询数据
Page<Skuinfo> page = new Page<Skuinfo>(
Long.parseLong(String.valueOf(resultMap.get("total"))),
Integer.parseInt(String.valueOf(resultMap.get("pageNum"))),
Page.pageSize
);
看累了吧,来几张图片放松一下
商品详情页静态化
需求分析
当系统审核完成商品,需要将商品详情页进行展示,那么采用静态页面生成的方能的web服务器中进行访问是比较合适的。所以,开发流程如下图所示:
执行步骤解释:
系统管理员(商家运维人员)修改或者审核商品的时候, 会更改数据库中商品上架状态并发送商品id给rabbitMq中的上架交换器。
上架交换器会将商品id发给静态页生成队列。
静态页微服务设置监听器, 监听静态页生成队列, 根据商品id获取商品详细数据并使用thymeleaf的模板技术生成静态页。
静态页面生成服务中的具体代码实现:
1.创建PageListener监听类,监听page_create_queue队列,获取消息,并生成静态化页面
@Component
public class PageListener {
@Autowired
private PageService pageService;
@RabbitListener(queues = RabbitmqConfig.PAGE_CREATE_QUEUE)
public void receiveMessage(String spuId) {
System.out.println("生成静态页面的spuId:" + spuId);
pageService.generateHtml(spuId);
}
}
2.PageServiceImpl实现:
@Service
public class PageServiceImpl implements PageService {
@Value("${pagepath}")
private String pagepath;
@Autowired
private TemplateEngine templateEngine;
/**
* @param spuId
* @return void
* @description: 生产静态化页面
* @author 大佬味的小男孩
* @date 2020/07/27 21:57
*/
@Override
public void generateHtml(String spuId) {
//获取context对象
Context context = new Context();
//获取静态化页面的数据
Map<String, Object> map = this.getItemData(spuId);
context.setVariables(map);
//获取商品存储位置(本地磁盘)
File file = new File(pagepath);
if (!file.exists()) {
//判断存储静态化页面的文件夹是否存在 如果不存在 就创建
file.mkdir();
}
//定义输出流 生产文件
File file1 = new File(file + "/" + spuId + ".html");
Writer out = null;
try {
out = new PrintWriter(file1);
templateEngine.process("item", context, out);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try {
//关闭流
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Autowired
private SpuFeign spuFeign;
@Autowired
private CategoryFeign categoryFeign;
@Autowired
private SkuFeign skuFeign;
/**
* @param spuId
* @return java.util.Map<java.lang.String, java.lang.Object>
* @description: //获取静态化页面的数据
* @author 大佬味的小男孩
* @date 2020/07/27 22:01
*/
private Map<String, Object> getItemData(String spuId) {
HashMap<String, Object> map = new HashMap<>();
//获取spu数据
Spu spu = spuFeign.findSpuById(spuId).getData();
map.put("spu", spu);
//获取图片数据
if (spu != null) {
if (!StringUtils.isEmpty(spu.getImages())) {
map.put("ImageList", spu.getImages().split(","));
}
}
//获取商品分类数据
Category category1 = categoryFeign.findById(spu.getCategory1Id());
Category category2 = categoryFeign.findById(spu.getCategory2Id());
Category category3 = categoryFeign.findById(spu.getCategory3Id());
map.put("category1", category1);
map.put("category2", category2);
map.put("category3", category3);
//获取sku的数据
List<Sku> skuList = skuFeign.findSkuListByspuid(spuId);
map.put("skuList", skuList);
//获取规格信息
map.put("specifcationList", JSON.parseObject(spu.getSpecItems(), Map.class));
return map;
}
}
3. 数据监控服务我就不演示了
4 模板填充
(1)面包屑数据
<div class="crumb-wrap">
<ul class="sui-breadcrumb">
<li>
<a href="#" th:text="${category1.name}"></a>
</li>
<li>
<a href="#" th:text="${category2.name}"></a>
</li>
<li>
<a href="#" th:text="${category3.name}"></a>
</li>
</ul>
</div>
(2)商品图片
<div class="fl preview-wrap">
<!--放大镜效果-->
<div class="zoom">
<!--默认第一个预览-->
<div id="preview" class="spec-preview">
<span class="jqzoom"><img th:jqimg="${ImageList[0]}" th:src="${ImageList[0]}"/></span>
</div>
<!--下方的缩略图-->
<div class="spec-scroll">
<a class="prev"><</a>
<!--左右按钮-->
<div class="items">
<ul>
<li th:each="img:${ImageList}"><img th:src="${img}" th:bimg="${img}" onmousemove="preview(this)"/></li>
</ul>
</div>
<a class="next">></a>
</div>
</div>
</div>
(3)规格输出
<div id="specification" class="summary-wrap clearfix">
<!--循环MAP-->
<dl th:each="spec,specStat:${specifcationList}">
<dt>
<div class="fl title">
<i th:text="${spec.key}"></i>
</div>
</dt>
<dd th:each="arrValue:${specStat.current.value}">
<a href="javascript:;"
th:v-bind:class="|{selected:sel('${spec.key}','${arrValue}')}|"
th:@click="|selectSpecification('${spec.key}','${arrValue}')|">
<i th:text="${arrValue}"></i>
<span title="点击取消选择"> </span>
</a>
</dd>
</dl>
</div>
(4)默认SKU显示
静态页生成后,需要显示默认的Sku,我们这里默认显示第1个Sku即可,这里可以结合着Vue一起实现。可以先定义一个集合,再定义一个spec和sku,用来存储当前选中的Sku信息和Sku的规格,代码如下:
<script th:inline="javascript">
var item = new Vue({
el: '#itemArray',
data: {
skuList: [[${skuList}]], //SKU集合
sku: {}, //当前选中的sku
spec: {}, //选中的sku的规格
num: 1
},
created: function () {
//默认选中第1个SKU,深克隆
this.sku = JSON.parse(JSON.stringify(this.skuList[0]));
//第1个SKU的规格,深克隆
this.spec = JSON.parse(this.skuList[0].spec);
}
})
</script>
最后请原谅我不知道怎么总结前端页面!