前言
emmmmm遇到的插槽需求,记录一下,后续如果需要就可以看看
使用的时uniapp+springboot
需求是做一个问题发布和评论,要求每个插槽显示不同类型,并且数据显示要求能看到第一条评论的信息,类似某乎
前端页面效果
前端页面(不含样式)
<template>
<view class="content">
<TabBar :tabs="tabList" listBgColor="#fff" @getData="getListData">
<template v-for="i in tabList" v-slot:[i.name]>
<TopicItem class="topic" @jump="openInfoPage" v-for="(item, index) in list" :data="item" :key="index"></TopicItem>
</template>
</TabBar>
</view>
</template>
1.uniapp使用view作为容器,可以看作是html中的div
2.通过TabBar
组件来渲染插槽,将tabList
对象传递给 TabBar,TabBar
组件中会触发getData方法会调用 getListData
方法,从而获取数据渲染到插槽中
3.通过TopicItem
抽象容器 来遍历 tabList,v-slot:[i.name]
为 TabBar
组件中的插槽指定一个动态名称,用来实现了标签页切换时对应内容的动态渲染
4.TopicItem
组件放置在插槽中,用于显示各个插槽中的内容,通过@jump设置
事件监听器件并调用 openInfoPage
方法,遍历 list
数组元素赋值给 data
属性传递给 TopicItem
组件
<script>
import { list } from '@/api/bbs.js';
import TabBar from './components/tab-bar.vue';
import TopicItem from './components/topic-item.vue';
export default {
components: {
TabBar,
TopicItem
},
data() {
return {
tabList: [
{ name: 'choice', title: '精选', type: 0 },
{ name: 'newest', title: '最新', type: 1 },
{ name: 'follow', title: '关注', type: 2 }],
}
},
onLoad() {
this.getListData({ type: 0 });
},
methods: {
async getListData(data) {
const resp = await list({
type: data.type,
pageNum: 1,
pageSize: 10
});
if (resp.code != 200) {
this.list = null;
}else {
this.list = resp.data;
}
},
}
}
</script>
1.通过import 引入TabBar和TopicItem两个组件,并初始化组件
2.onload函数用于在页面加载完成后执行的操作,加载type为0插槽的数据
3.async异步函数结合await函数 执行 list方法,携带类型和页数参数,再判断状态码返回数据
前端组件
1.TabBar组件
(实际上是插槽的页面效果,实现了选项卡切换功能)
①类名为 "list" 的 <view>
元素,用于显示选项卡的标题部分,并判断是否选中标题来切换样式
②类名为 "tab-content" 的 <view>
元素,用于显示选项卡对应的内容部分
通过 <slot> 插槽语法,使用:name属性绑定了当前选项卡的名称,即在父页面中定义的v-slot:[i.name] ,这样做的作用是将父组件传递给子组件的内容插入到这个位置,实现了动态插入不同选项卡对应的内容
【v-slot
用于命名插槽的指令,而 <slot>
元素用于定义插槽内容的占位符】
③props
属性是用来接收父组件传递过来的数据
④switchTab
方法用于切换选项卡并更新当前选中的选项卡的名称用于修改样式,且接收参数 item(
选项卡对象)
并通过 $emit
方法向父组件发送一个自定义事件 getData
来获取数据
<template>
<view class="tab-bar">
<!-- 标题部分 -->
<view class="list" :class="listBottomBorder ? 'is-border-bottom' : ''"
:style="listStyle">
<text class="tab" :class="tabIndex == i.name ? 'tab-selected' : ''"
v-for="(i, k) in tabs" :key="k" @click="switchTab(i)">
{{ i.title }}
</text>
</view>
<!-- 内容部分 -->
<view class="tab-content">
<view class="tab-item-box" v-for="(i, k) in tabs" :key="k"
v-if="tabIndex == i.name">
<slot :name="i.name"></slot>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
tabs: Array,
listBgColor: String,
listBottomBorder: Boolean,
listBottomBorderColor: String
},
data() {
return {
tabIndex: this.tabs[0].name,
listStyle: {
'--backgroundColor': this.listBgColor,
'--listBottomBorderColor': this.listBottomBorderColor
}
};
},
methods: {
switchTab(item) {
this.tabIndex = item.name;
this.$emit('getData', item);
}
}
};
</script>
<style lang="less" vars="{ color }" scoped>
.tab-bar {
.is-border-bottom {
border-bottom: 1px solid var(--listBottomBorderColor);
}
.list {
display: flex;
justify-content: flex-start;
align-items: center;
padding-left: 1em;
font-size: 1rem;
background-color: var(--backgroundColor);
.tab {
position: relative;
padding: 0.5em 0;
margin-right: 2em;
color: #a1a4ab;
&::after {
display: none;
content: '';
position: absolute;
left: 50%;
bottom: 0;
transform: translateX(-50%);
width: 1.5em;
height: 3px;
background-color: #57deb0;
}
}
.tab-selected {
color: #57deb0;
&::after {
display: inline-block;
}
}
}
.tab-content {
.tab-item-box {
background-color: #f7f8f9;
.topic-item {
background-color: #fff;
}
}
}
}
</style>
2.TopicItem组件(不含样式)
(TopicItem组件相对业务化,通过获取后台数据展示在页面)
①通过props
属性是用来接收父组件传递过来的数据并判断数据是否为空,为空渲染空数据icon
②获取返回数据中的标题/用户头像/用户名/用户评论及其对应的统计数
<template>
<view class="topic-item" @click="jump">
<view v-if="data.length == 0" class="pa-center">
<u-empty mode="data" icon="http://cdn.uviewui.com/uview/empty/data.png"></u-empty>
</view>
<view class="header">
<!-- 获取话题名 -->
<text class="title two-line-ellipsis">{{ data.title }}</text>
</view>
<!-- 存在回复 -->
<view class="main" v-if="data.reply">
<!-- 回复人的头像昵称 -->
<view class="user">
<image class="avatar" :src="data.reply.userImg" mode="aspectFit"></image>
<text class="name">{{ data.reply.userName }}</text>
</view>
<!-- 回复内容的文本 -->
<view class="content">
<text class="info two-line-ellipsis">{{ data.reply.replyContent }}</text>
</view>
<!-- 回复内容的图片 -->
<view class="img-box">
<view class="box">
<image class="img"
:class="count == 3 ? 'one-column' : count == 6 ? 'two-column' : count == 9 ? 'three-column' : ''"
v-for="(i, k) in data.reply.imgList" :key="k"
:src="i" mode="aspectFill"
></image>
</view>
</view>
<!-- 获取话题的统计数 -->
<view class="status">
<view class="item">
<u-icon name="heart" color="#a1a4ab" size="1.2rem"></u-icon>
<text class="type">{{ data.reply.likeCount }}</text>
</view>
<view class="item">
<u-icon name="chat" color="#a1a4ab" size="1.2rem"></u-icon>
<text class="type">{{ data.reply.commentCount }}</text>
</view>
<view class="item">
<u-icon name="star" color="#a1a4ab" size="1.2rem"></u-icon>
<text class="type">{{ data.reply.collectCount }}</text>
</view>
</view>
</view>
<view class="not-reply" v-else>
<view class="box"><text>暂无回答</text></view>
</view>
</view>
</template>
<script>
export default {
props: {
tab: Object,
data: Object
},
data() {
return {
count: 6
};
},
methods: {
jump() {
let params = {};
if (this.data.reply) {
params.isReply = true;
params.id = this.data.reply.id;
} else {
params.isReply = false;
params.id = this.data.id;
}
this.$emit('jump', params);
}
}
};
</script>
后端
1.前端接口
export const list = (params) => http.get('/api/bbs/list', {
params
})
2.后端控制层
@Slf4j
@RestController
@RequestMapping("/bbs")
public class TopicController {
@Autowired
private TopicService topicService;
@GetMapping("/list")
public R<List<TopicItemVo>> list(@LoginUser Integer userId,
@RequestParam(value = "type", defaultValue = "0") Integer type,
@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {
List<TopicItemVo> topicItemVoList = topicService.list(userId, type, pageNum, pageSize);
return RespUtils.success(topicItemVoList);
}
}
3.业务实现
@Service
public class TopicServiceImpl extends ServiceImpl<TopicMapper, Topic> implements TopicService {
@Override
public List<TopicItemVo> list(Integer userId,Integer type,Integer pageNum,Integer pageSize) {
PageHelper.startPage(pageNum, pageSize);// 分页
LambdaQueryWrapper<Topic> wrapper = Wrappers.lambdaQuery(Topic.class);
wrapper.eq(Topic::getStatus, 1);//状态正常
//精选
if (type.equals(0)) {
wrapper.gt(Topic::getReplyCount, 20);//回复数>20
wrapper.orderByDesc(Topic::getPvCount);//排序浏览量
List<Topic> topicList = this.list(wrapper); // 执行查询
// 遍历查询结果,转换为视图对象
topicList.forEach(topic -> {
TopicItemVo vo = new TopicItemVo();
vo.setId(topic.getId());
vo.setTitle(topic.getTitle());
vo.setUserId(topic.getUserId());
});
//最新
} else if (type.equals(1)) {
wrapper.orderByDesc(Topic::getCreateTime);//根据最新时间排序
//关注
} else if (type.equals(2)) {
List<TopicFollow> followList = new ArrayList<TopicFollow>();
followList = topicFollowService.getFollowList(userId);
if (followList != null && !followList.isEmpty()) {
List<Long> topicIds = followList.stream()
.map(topic -> Long.valueOf(topic.getTopicId()))
.collect(Collectors.toList());
wrapper.in(Topic::getId, topicIds);
} else {
throw new BusinessException("请关注你的问题哦");
}
}
List<Topic> topicList = this.list(wrapper); // 执行查询
List<TopicItemVo> voList = new ArrayList<>();
// 遍历查询结果,转换为视图对象
topicList.forEach(topic -> {
TopicItemVo vo = new TopicItemVo();
vo.setId(topic.getId());
vo.setTitle(topic.getTitle());
vo.setUserId(topic.getUserId());
//获取主题的首条回复,并进行HTML标签的解析
TopicReplyDto replyDto = topicReplyService.firstReply(topic.getId());
if (Objects.nonNull(replyDto)) {
TopicReplyVo reply = BeanUtil.copyProperties(replyDto, TopicReplyVo.class);
reply.setReplyContent(Jsoup.parse(reply.getReplyContent()).text());
vo.setReply(reply);
}
voList.add(vo);
});
return voList;
}
}