Files
t/uni_modules/uni-cms-article/pages/detail/detail.vue
T
2025-03-12 20:11:03 +08:00

1102 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="">
<unicloud-db v-slot:default="{data, loading, error, options}" :collection="collection" :options="formData"
:getone="true" :where="where" :manual="true" ref="detail" foreignKey="uni-cms-articles.user_id"
@load="loadData" class="article">
<template v-if="!loading && data">
<view class="meta">
<view class="title">
<text class="text">{{ data.title }}</text>
</view>
<view class="excerpt">
<text class="text">{{ data.excerpt }}</text>
</view>
<view class="author">
<template>
<cloud-image
height="64rpx"
width="64rpx"
borderRadius="50%"
:src="data.user_id[0].avatar_file.url"></cloud-image>
<text class="at">{{ data.user_id[0].nickname || '' }}</text>
<text class="split">·</text>
</template>
<text class="date">{{ publishTime( data.publish_date) }}</text>
</view>
</view>
<render-article-detail :content="data.content" :content-images="data.content_images"
:ad-config="{ adpId, watchAdUniqueType }"></render-article-detail>
<!-- <view class="" style="font-size: 20rpx;color: #999;margin-left: 50rpx;margin-top: 20rpx;">
更新于 2024-12-26 {{ publishTime(data.last_modify_date) }}
</view> -->
</template>
<view class="detail-loading" v-else>
<uni-icons type="spinner-cycle" size="35px" />
</view>
<uni-grid :column="3" :highlight="true" @change="change" :show-border="false">
<uni-grid-item
v-for="(item, index) in gridLst"
:index="index"
:key="index">
<view class="grid-item-box" style="background-color: #fff;">
<uni-icons :type="item.icon" :size="30" :color="item.select_flag?'#e66':'#777'" />
<text class="text">{{item.value}}</text>
</view>
</uni-grid-item>
</uni-grid>
<view class="">
<text class="date" @click="complaint">投诉</text> | 阅读 {{ data.view_count }}
</view>
<view class="example-body" style="justify-content:center" v-for="(item, index) in cmsArticleLogList" :key="index">
<cloud-image
v-if="item1.user_id[0].avatar_file.url"
height="64rpx"
width="64rpx"
borderRadius="50%"
v-for="(item1, index1) in item"
:key="index1"
:src="item1.user_id[0].avatar_file.url"></cloud-image>
<image
v-else
src="https://env-00jxt5y29hrd.normal.cloudstatic.cn/static/static/center/avatar.png?expire_at=1741142218&er_sign=2f723854c2a07dcb5571d2ede5458f7f"
style="width: 64rpx;height: 64rpx;border-radius: 50%;"></image>
</view>
<uni-drawer ref="showLeft" mode="left" :width="320" @change="change($event,'showLeft')">
<view class="close">
<button @click="closeDrawer('showLeft')"><text class="word-btn-white">关闭Drawer</text></button>
</view>
</uni-drawer>
<!-- comment_count -->
<view class="comment-eg">
<hb-comment ref="hbComment" @add="add" @del="del" @like="like" @relike="relike" @focusOn="focusOn"
@replyAdd="replyAdd" @getTreeComment="getTreeComment" :deleteTip="'确认删除?'" :cmData="commentData"
:articlesData="data" v-if="commentData">
</hb-comment>
</view>
</unicloud-db>
</view>
</template>
<script>
import uniNavBar from '@/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-nav-bar.vue';
import renderArticleDetail from "@/uni_modules/uni-cms-article/components/render-article-detail/index.vue";
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time";
const uniIdCo = uniCloud.importObject("uni-id-co")
import {
store,
mutations
} from '@/uni_modules/uni-id-pages/common/store.js'
const db = uniCloud.database()
const articleDBName = 'uni-cms-articles'
const userDBName = 'uni-id-users'
export default {
components: {
uniNavBar,
renderArticleDetail
},
data() {
return {
gridLst:[
{
text: "点赞",
icon: "heart",
value: 0,
select_flag: false
},
{
text: "收藏",
icon: "folder-add",
value: 0,
select_flag: false
},
{
text: "打赏",
icon: "gift",
value: 0,
select_flag: false
}
],
cmsArticleLogList:[],
templateSrc: "https://env-00jxt5y29hrd.normal.cloudstatic.cn/static/bg.jpg?expire_at=1741101988&er_sign=c9732cddaaead925cf110d9a11751231",
"commentData": null,
"reqFlag": false,
id: "", // 文章ID
title: "", // 文章标题
formData: {}, // 表单数据
// 广告相关配置
adpId: "", // TODO: 请填写广告位ID
watchAdUniqueType: "device" // TODO: 观看广告的唯一标识类型,可选值为 user 或者 device,user 表示用户唯一,device 表示设备唯一
}
},
onShow() {
let uid = this.userInfo._id;
console.log(uid);
if (this.id) {
this.detailLog(this.id, uid);
this.getComment();
}
},
computed: {
userInfo() {
return store.userInfo
},
realNameStatus() {
if (!this.userInfo.realNameAuth) {
return 0
}
return this.userInfo.realNameAuth.authStatus
},
where() {
//拼接where条件 查询条件 ,更多详见 :https://uniapp.dcloud.net.cn/uniCloud/unicloud-db?id=jsquery
return `_id =="${this.id}"`
},
collection() {
return [
db.collection(articleDBName).where(this.where).field(
'user_id,thumbnail,excerpt,publish_date,title,content,last_modify_date,view_count,_id,comment_count,like_count,last_modify_ip,_id,gift_count,collect_count'
).getTemp(),
db.collection(userDBName).field('_id, nickname,avatar_file').getTemp()
]
}
},
onReady() {
// 开始加载数据,修改 where 条件后才开始去加载 clinetDB 的数据 ,需要等组件渲染完毕后才开始执行 loadData,所以不能再 onLoad 中执行
if (this.id) { // ID 不为空,则发起查询
this.$refs.detail.loadData()
} else {
uni.showToast({
icon: 'none',
title: 'id 不能为空'
})
}
},
async onLoad(event) {
//获取文章id,通常 id 来自上一个页面
if (event.id) {
this.id = event.id
}
await this.getArticleInfo();
await this.getCmsArticleLog(this.id);
// 监听解锁内容事件
uni.$on('onUnlockContent', this.onUnlockContent)
},
onUnload() {
// 页面卸载时,移除监听事件
uni.$off('onUnlockContent', this.onUnlockContent)
},
onPageScroll(e) {
// 根据滚动位置判断是否显示导航栏
if (e.scrollTop > 100) {
uni.setNavigationBarTitle({
title: this.title
})
} else {
uni.setNavigationBarTitle({
title: ''
})
}
},
async created() {
},
methods: {
chunkDataIntoGroups(originalArray) {
const MAX_PER_GROUP = 7;
const MAX_TOTAL = 21;
const result = [];
const workingArray = originalArray.slice(0, MAX_TOTAL);
for (let i = 0; i < workingArray.length; i += MAX_PER_GROUP) {
const group = workingArray.slice(i, i + MAX_PER_GROUP);
result.push(group);
}
return result;
},
async getCmsArticleLog(id) {
try {
let res = await uniCloud.importObject("article_info").getCmsArticleLog({
article_id: id
});
console.log(res);
if (res.code === 200 ) {
if (res.cmsArticleLog.length > 0 ) {
this.cmsArticleLogList = this.chunkDataIntoGroups(res.cmsArticleLog);
}
console.log(this.cmsArticleLogList);
}
console.log(this.cmsArticleLogList);
} catch(e) {
console.log(e.message);
}
},
async getArticleInfo() {
try {
let res = await uniCloud.importObject("article_info").getInfo({
user_id: this.userInfo._id,
article_id: this.id
});
console.log(res);
if (res.code === 200 ) {
this.gridLst[0].select_flag = res.isLike;
this.gridLst[1].select_flag = res.isCollect;
}
} catch(e) {
console.log(e.message);
}
},
async change(e) {
let index = e.detail.index;
if (this.gridLst[index].text === "点赞") {
if (!this.gridLst[index].select_flag) {
let res = await uniCloud.importObject("cms-articles-like").likeArticle({
user_id: this.userInfo._id,
article_id: this.id
});
console.log(res);
this.gridLst[index].value++;
} else {
let res = await uniCloud.importObject("cms-articles-like").relikeArticle({
user_id: this.userInfo._id,
article_id: this.id
});
console.log(res);
this.gridLst[index].value--;
}
} else if(this.gridLst[index].text === "收藏") {
if (!this.gridLst[index].select_flag) {
let res = await uniCloud.importObject("cms-articles-collect").collectArticle({
user_id: this.userInfo._id,
article_id: this.id
});
console.log(res);
this.gridLst[index].value++;
} else {
let res = await uniCloud.importObject("cms-articles-collect").recollectArticle({
user_id: this.userInfo._id,
article_id: this.id
});
console.log(res);
this.gridLst[index].value--;
}
} else if (this.gridLst[index].text === "送礼") {
console.log("点击了送礼");
}
this.gridLst[index].select_flag = !this.gridLst[index].select_flag;
},
async detailLog(id, uid) {
console.log(uid);
let res = await uniCloud.importObject("uni-cms-articles").detailLog({
articleId: id,
userId: uid,
});
},
publishTime(timestamp) {
return translatePublishTime(timestamp)
},
setReadHistory() {
const historyCache = uni.getStorageSync('readHistory') || []
const readHistory = historyCache.filter(item => item.article_id !== this.id)
readHistory.unshift({
article_id: this.id,
last_time: Date.now()
})
uni.setStorageSync('readHistory', readHistory)
},
loadData(data) {
console.log(data);
this.gridLst[0].value = data.like_count;
this.gridLst[1].value = data.collect_count
this.gridLst[2].value = data.gift_count
this.title = data.title
this.setReadHistory()
},
async onUnlockContent() {
this.$refs.detail.loadData()
},
check() {
const token = uni.getStorageSync('uni_id_token');
const tokenExpired = uni.getStorageSync('uni_id_token_expired');
const now = Date.now();
// 检查 Token 存在性 + 有效期
if (token && now < tokenExpired) {
return true;
} else if (token && now >= tokenExpired) {
uni.showToast({
title: '登录已过期',
icon: 'none'
});
this.clearLoginState(); // 清理过期凭证
return false;
}
},
checkLogin() {
// 防抖处理(防止连续触发)
if (this._lastCheckTime && Date.now() - this._lastCheckTime < 1500) return false;
this._lastCheckTime = Date.now();
try {
// 增强校验逻辑(结合本地存储和服务端验证)
const token = uni.getStorageSync('uni_id_token');
const isLogin = !!token && this.verifyTokenExpiration(token);
if (isLogin) return true;
// 优化提示交互(带图标和动画)
uni.showModal({
title: '🔐 登录提示',
content: '体验完整功能需要先登录哦~',
confirmText: '立即登录',
cancelText: '稍后再说',
confirmColor: '#007AFF',
animationType: 'slide-in-bottom',
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: "/pages/login/login"
})
}
}
});
return false;
} catch (error) {
uni.showToast({
title: '登录状态异常,请重新登录',
icon: 'error'
});
this.clearInvalidSession();
return false;
}
},
// 新增 Token 校验方法
verifyTokenExpiration(token) {
return true;
},
// 新增会话清理
clearInvalidSession() {
uni.removeStorageSync('uni_id_token');
},
// 输入框聚焦
focusOn() {
this.checkLogin();
},
async getTreeComment(root_id) {
try {
let query = {
root_id: root_id
};
const res = await uniCloud.importObject("comments").getReplyComment(query);
console.log(res.data.commentList)
const index = this.commentData.comment.findIndex(
item => item._id === root_id
)
console.log(1, this.commentData, index)
if (index !== -1) {
console.log(this.commentData.comment);
this.commentData.comment[index].children = [...this.commentData.comment[index].children, ...res
.data.commentList
];
this.commentData.comment[index].replyCount -= res.data.commentList.length;
}
} catch (error) {
console.error('获取评论失败:', error);
// 设置默认值或提示用户
this.commentData = {
readNumer: 0,
commentSize: 0,
comment: []
};
uni.showToast({
title: '评论加载失败,请重试',
icon: 'none'
});
}
},
async getComment() {
try {
console.log(this.id);
console.log('开始获取评论');
let query = {
article_id: this.id
};
const res = await uniCloud.importObject("comments").getComments(query);
const readNumer_res = await uniCloud.importObject("comments").getReadCount(query);
// 处理正常响应
this.commentData = {
readNumer: readNumer_res.data.view_count,
commentSize: res.data.commentList.length,
comment: res.data.commentList
};
} catch (error) {
console.error('获取评论失败:', error);
// 设置默认值或提示用户
this.commentData = {
readNumer: 0,
commentSize: 0,
comment: []
};
uni.showToast({
title: '评论加载失败,请重试',
icon: 'none'
});
}
},
// 新增评论
async add(content) {
try {
// 获取当前用户信息(需确保已登录)
const uniIdToken = uni.getStorageSync('uni_id_token');
if (!uniIdToken) {
uni.showToast({
title: '请先登录',
icon: 'none'
});
return;
}
console.log(this.id);
// 构建符合后端接口规范的评论对象
const comment = {
article_id: this.id,
content: content,
parent_id: 0,
};
// 调用云对象方法
const commentObj = uniCloud.importObject("comments");
const res = await commentObj.addComment(comment);
// 处理响应结果
if (res.code === 200) {
uni.showToast({
title: '评论成功'
});
this.commentData.comment.splice(0, 0, res.data);
this.commentData.commentSize++;
// 更新前端显示(示例)
// this.$refs.commentList.unshift({
// _id: res.data.comment_id,
// content: comment.content,
// create_time: new Date().toISOString(),
// depth: res.data.depth,
// reply_count: 0
// });
}
} catch (e) {
console.error('评论失败:', e);
// 错误分类处理(根据后端错误码映射)
const errorHandler = {
'PERMISSION_DENIED': '登录状态失效,请重新登录',
'INVALID_PARAMS': '评论内容不能为空',
'CONTENT_BLOCKED': '包含违禁词汇: ' + e.message.split(':')[1],
'PARENT_COMMENT_NOT_FOUND': '回复的评论不存在'
};
uni.showToast({
title: errorHandler[e.errorType] || '评论失败,请稍后重试',
icon: 'none'
});
}
},
// 点赞评论
// 修改后的点赞方法(带防抖和状态反馈)
async like(comment) {
if (!this.checkLogin()) {
return;
}
// 防抖处理(500ms间隔)
if (this._lastClickTime && Date.now() - this._lastClickTime < 500) {
uni.vibrateShort(); // 触觉反馈[2](@ref)
return;
}
this._lastClickTime = Date.now();
try {
// 显示加载状态
let commentId = comment._id;
let userInfo = uniCloud.getCurrentUserInfo();
console.log(userInfo);
let uid = userInfo.uid;
const commentObj = uniCloud.importObject("comments");
const res = await commentObj.likeComment(uid, commentId);
// 更新UI状态(带动画效果)
this.$refs.hbComment.likeComplete(comment);
// 视觉反馈
} catch (error) {
console.error(error);
// 错误处理
uni.showToast({
title: error.message || '操作失败',
icon: 'error'
});
} finally {
this.reqFlag = false;
}
},
async relike(comment) {
if (!this.checkLogin()) {
return;
}
// 防抖处理(500ms间隔)
if (this._lastClickTime && Date.now() - this._lastClickTime < 500) {
uni.vibrateShort(); // 触觉反馈[2](@ref)
return;
}
this._lastClickTime = Date.now();
try {
// 显示加载状态
let commentId = comment._id;
let userInfo = uniCloud.getCurrentUserInfo();
console.log(userInfo);
let uid = userInfo.uid;
const commentObj = uniCloud.importObject("comments");
const res = await commentObj.relikeComment(uid, commentId);
// 更新UI状态(带动画效果)
this.$refs.hbComment.likeComplete(comment);
// 视觉反馈
} catch (error) {
console.error(error);
// 错误处理
uni.showToast({
title: error.message || '操作失败',
icon: 'error'
});
} finally {
this.reqFlag = false;
}
},
async replyAdd(commentReq) {
try {
let content = commentReq.content;
// 获取当前用户信息(需确保已登录)
const uniIdToken = uni.getStorageSync('uni_id_token');
if (!uniIdToken) {
uni.showToast({
title: '请先登录',
icon: 'none'
});
return;
}
// return;
// 构建符合后端接口规范的评论对象
const comment = {
article_id: this.id,
root_id: commentReq.root_id,
content: commentReq.content,
parent_id: commentReq.pId
};
console.log(comment)
// 调用云对象方法
const commentObj = uniCloud.importObject("comments");
const res = await commentObj.replyComment(comment);
// 处理响应结果
if (res.code === 200) {
uni.showToast({
title: '评论成功'
});
// this.commentData.comment.splice(0, 0, res.data);
const index = this.commentData.comment.findIndex(
item => item._id === commentReq.root_id
);
console.log(this.commentData);
console.log(index);
if (index !== -1) {
this.commentData.comment[index].children.splice(0, 0, res.data)
this.commentData.commentSize++;
}
console.log(this.commentData.comment);
// 更新前端显示(示例)
// this.$refs.commentList.unshift({
// _id: res.data.comment_id,
// content: comment.content,
// create_time: new Date().toISOString(),
// depth: res.data.depth,
// reply_count: 0
// });
}
} catch (e) {
console.error('评论失败:', e);
// 错误分类处理(根据后端错误码映射)
const errorHandler = {
'PERMISSION_DENIED': '登录状态失效,请重新登录',
'INVALID_PARAMS': '评论内容不能为空',
'CONTENT_BLOCKED': '包含违禁词汇: ' + e.message.split(':')[1],
'PARENT_COMMENT_NOT_FOUND': '回复的评论不存在'
};
uni.showToast({
title: errorHandler[e.errorType] || '评论失败,请稍后重试',
icon: 'none'
});
}
},
// 删除评论
async del(item) {
try {
// 1. 操作防抖与权限预校验
if (this.reqFlag) {
uni.showToast({
title: '操作受限',
icon: 'none'
});
return;
}
console.log(item);
this.reqFlag = true;
uni.hideToast();
// 2. 双重加载态管理
uni.showLoading({
title: '安全检查中...',
mask: true
});
// 3. 带权限校验的二次确认
const {
confirm
} = await uni.showModal({
title: '删除确认',
content: '确定删除这条评论?删除后不可恢复',
confirmText: '确认删除',
confirmColor: '#dd514d'
});
if (!confirm) {
this._resetOperationState();
return;
}
let commentId = item.id;
// // 4. 更新加载状态(网页1)
uni.showLoading({
title: '数据删除中...',
mask: true
});
// // 5. 带重试机制的请求(网页4扩展)
// 调用云对象方法
const commentObj = uniCloud.importObject("comments");
const res = await commentObj.deleteComment(commentId, item.root_id);
console.log(res);
console.log(this.commentData.comment);
if (item.root_id === 0) {
const index = this.commentData.comment.findIndex(
item => item._id === commentId
);
if (index !== -1) {
this.commentData.comment.splice(index, 1);
}
this.commentData.commentSize--;
uni.showLoading({
title: '删除成功',
mask: true
});
} else {
const index = this.commentData.comment.findIndex(
i => i._id === item.root_id
);
if (index !== -1) {
let comment = this.commentData.comment[index].children;
const index2 = comment.findIndex(
i => i._id === item.root_id
);
this.commentData.comment[index].children.splice(index2, 1);
uni.showLoading({
title: '删除成功',
mask: true
});
}
}
} catch (error) {
this._handleDeleteError(error);
this.$refs.commentList?.refresh();
} finally {
this._resetOperationState();
}
},
_updateLocalData(commentId, serverData) {
this.$store.commit('comments/REMOVE', commentId);
this.$store.commit('articles/UPDATE_STATS', {
id: serverData.article_id,
type: 'comment',
value: -1
});
},
// 错误处理(网页4/6)
_handleDeleteError(error) {
const errorConfig = {
'PERMISSION_DENIED': {
title: '操作权限不足',
icon: 'none'
},
'COMMENT_NOT_FOUND': {
title: '内容已删除',
icon: 'none'
},
'NETWORK_ERROR': {
title: '网络不稳定',
icon: 'none'
},
'default': {
title: error.errMsg || '操作失败',
icon: 'none'
}
};
console.log(error);
uni.showToast(errorConfig[error.code] || errorConfig.default);
},
// 状态重置(网页4)
_resetOperationState() {
try {
uni.hideLoading();
} catch (e) {}
this.reqFlag = false;
},
// 列表按照父子转换成树
getTree(data) {
let result = [];
let map = {};
data.forEach(item => {
map[item.id] = item;
});
data.forEach(item => {
let parent = map[item.parentId];
if (parent) {
(parent.children || (parent.children = [])).push(item);
} else {
result.push(item);
}
});
return result;
},
complaint() {
}
}
}
</script>
<style scoped lang="scss">
/* #ifdef APP-NVUE */
.article {
background-color: #fff;
}
/* #endif */
@mixin cp {
padding: 0 30rpx;
}
.detail-loading {
margin: 100rpx auto 0;
width: 35px;
height: 35px;
animation: rotate360 2s linear infinite;
}
@keyframes rotate360 {
0% {
transform: rotate(0deg);
transform-origin: center center;
}
100% {
transform: rotate(360deg);
transform-origin: center center;
}
}
.meta {
@include cp;
position: relative;
z-index: 1;
padding-top: 20rpx;
.title {
.text {
font-size: 40rpx;
line-height: 66rpx;
font-weight: bold;
color: #333;
}
}
.excerpt {
margin-top: 10rpx;
.text {
font-size: 26rpx;
line-height: 40rpx;
color: #999;
}
}
.author {
display: flex;
align-items: center;
justify-content: flex-start;
flex-direction: row;
margin-top: 20rpx;
.at,
.split,
.date {
font-size: 26rpx;
color: #ccc;
}
.split {
margin: 0 10rpx;
}
}
}
.example-body {
/* #ifndef APP-PLUS-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: flex-start;
/* align-items: flex-end; */
/* flex-wrap: wrap; */
padding: 10rpx;
}
.btn {
text-align: center;
color: #fff;
padding: 20rpx;
margin: 20rpx;
border-radius: 20rpx;
background-color: #2979ff;
}
.tab {
text-indent: 2rem;
}
.v-preview {
.img {
width: 100%;
}
.btn-area {
position: relative;
top: -50rpx;
}
.red-btn-area {
display: flex;
justify-content: center;
padding: 10rpx 0;
margin: 0 40rpx 20rpx 40rpx;
font-size: 20rpx;
.red-btn {
position: relative;
color: #fff;
font-weight: 700;
font-size: 24rpx;
height: 40rpx;
line-height: 40rpx;
margin: 0;
background-image: linear-gradient(to bottom, #c11c1f, #5c0201);
+.red-btn {
margin-left: 20rpx;
}
}
}
.red-btn-plain-area {
display: flex;
justify-content: center;
padding: 10rpx 0;
margin: 0 40rpx;
font-size: 21rpx;
border-top: 2px solid;
border-bottom: 2px solid;
border-image: linear-gradient(to right,
#fff,
#f44336bf,
#b13636,
#f44336bf,
#fff) 1;
.red-btn-plain {
position: relative;
color: #b13636;
font-weight: 700;
padding-left: 15rpx;
+.red-btn-plain {
margin-left: 10rpx;
&::before {
// border-left: 2px solid #f44336;
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 2px;
height: 20rpx;
background-color: #b13636;
}
}
}
}
}
.game_content .person_info:nth-child(1) .person_detail .user_avatar_box .rank_box {
background-image: url();
}
.game_content .person_info:nth-child(2) .person_detail .user_avatar_box .rank_box {
background-image: url();
}
.game_content .person_info:nth-child(3) .person_detail .user_avatar_box .rank_box {
background-image: url();
}
.image {
width: 25px;
height: 25px;
}
.text {
font-size: 14px;
margin-top: 5px;
}
.example-body {
/* #ifndef APP-NVUE */
// display: block;
/* #endif */
}
.grid-dynamic-box {
margin-bottom: 15px;
}
.grid-item-box {
flex: 1;
// position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
align-items: center;
justify-content: center;
padding: 15px 0;
}
.grid-item-box-row {
flex: 1;
// position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: center;
padding: 15px 0;
}
.grid-dot {
position: absolute;
top: 5px;
right: 15px;
}
.swiper {
height: 420px;
}
</style>