707 lines
18 KiB
Vue
707 lines
18 KiB
Vue
<template>
|
||
<view class="preview">
|
||
<swiper :circular="true" :current="currentIndex" @change="swiperChange">
|
||
<swiper-item v-for="(item,index) in classList" :key="item._id">
|
||
<!-- 下面的这个v-if是进行流量控制的。看哪个页面就加载哪个页面
|
||
避免用户的流量消耗。提高用户体验。
|
||
-->
|
||
<!-- <image v-if="index===currentIndex" @click="maskChange" :src="item.picurl" mode="aspectFill"></image> -->
|
||
|
||
<!-- 也可以选择加载临近一至两张的图片 -->
|
||
|
||
<!-- 这是加载临近的左右图片 -->
|
||
<!-- <image v-if="Math.abs(index - currentIndex) <= 1" @click="maskChange" :src="item.picurl" mode="aspectFill"></image> -->
|
||
|
||
<image v-if="readImgs.includes(index)" @click="maskChange" :src="item.picurl" mode="aspectFill"></image>
|
||
</swiper-item>
|
||
</swiper>
|
||
|
||
<view class="mask" v-show="maskState">
|
||
<view class="goBack" @click="goBack" :style="{top:getPreviewBarHeight() + 'px'}">
|
||
<uni-icons type="back" color="#fff" size="20"></uni-icons>
|
||
</view>
|
||
<view class="count">{{currentIndex+1}} / {{classList.length}}</view>
|
||
<view class="time">
|
||
<uni-dateformat :date="Date.now()" format="hh:mm"></uni-dateformat>
|
||
</view>
|
||
<view class="date">
|
||
<uni-dateformat :date="Date.now()" format="MM月dd日"></uni-dateformat>
|
||
</view>
|
||
<view class="footer">
|
||
<view class="box" @click="clickInfo">
|
||
<uni-icons type="info" size="23"></uni-icons>
|
||
<view class="text">信息</view>
|
||
</view>
|
||
<view class="box" @click="clickScore">
|
||
<uni-icons type="star" size="23"></uni-icons>
|
||
<view class="text">{{currentInfo.score}}分</view>
|
||
</view>
|
||
<view class="box" @click="clickDownload">
|
||
<uni-icons type="download" size="23"></uni-icons>
|
||
<view class="text">下载</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
|
||
<uni-popup ref="infoPopup" type="bottom">
|
||
<view class="infoPopup">
|
||
<view class="popHeader">
|
||
<!-- 空占位区,用于flex布局的分布 -->
|
||
<view></view>
|
||
<view class="title">壁纸信息</view>
|
||
<view class="close" @click="clickInfoClose">
|
||
<uni-icons type="closeempty" size="18" color="#999"></uni-icons>
|
||
</view>
|
||
</view>
|
||
|
||
<scroll-view :scroll-y="true">
|
||
<view class="content">
|
||
<view class="row">
|
||
<view class="label">壁纸ID:</view>
|
||
<!-- 这里的内容是可选的,所以要用text的 selectable属性 -->
|
||
<text selectable class="value">{{currentInfo._id}}</text>
|
||
</view>
|
||
|
||
<!-- 暂时没用到 -->
|
||
<!-- <view class="row">
|
||
<view class="label">分类:</view>
|
||
<text class="value class">明星美女</text>
|
||
</view> -->
|
||
|
||
<view class="row">
|
||
<view class="label">发布者:</view>
|
||
<text class="value">{{currentInfo.nickname}}</text>
|
||
</view>
|
||
|
||
<view class="row">
|
||
<text class="label">评分:</text>
|
||
<view class="value roteBox">
|
||
<uni-rate :readonly="true" :touchable="false" :value="currentInfo.score" size="16" />
|
||
<!-- <uni-rate :readonly="true" :touchable="false" :value="5"/> -->
|
||
<text class="score">{{currentInfo.score}}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="row">
|
||
<text class="label">摘要:</text>
|
||
<view class="value">
|
||
{{currentInfo.description}}
|
||
</view>
|
||
</view>
|
||
|
||
<view class="row">
|
||
<text class="label">标签:</text>
|
||
<view class="value tabs">
|
||
<view class="tab" v-for="tab in currentInfo.tabs" :key="tab">
|
||
{{tab}}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
|
||
<view class="copyright">
|
||
声明:本图片来用户投稿,非商业使用,用于免费学习交流,如侵犯了您的权益,您可以拷贝壁纸ID举报至平台,邮箱513894357@qq.com,管理将删除侵权壁纸,维护您的权益。
|
||
</view>
|
||
|
||
<!-- 自己手动加一个安全距离 -->
|
||
<view class="safe-area-inset-bottom"></view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</uni-popup>
|
||
|
||
|
||
<!-- :is-mask-click 表示与不允许你点击旁边的蒙版进行关闭弹窗 -->
|
||
<uni-popup ref="scorePopup" :is-mask-click="false">
|
||
<view class="scorePopup">
|
||
<!-- 头部 -->
|
||
<view class="popHeader">
|
||
<!-- 空占位区,用于flex布局的分布 -->
|
||
<view></view>
|
||
<view class="title">{{isScore?'已经评分过了~':'壁纸评分'}}</view>
|
||
<view class="close" @click="clickScoreClose">
|
||
<uni-icons type="closeempty" size="18" color="#999"></uni-icons>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 中部内容-评分区 -->
|
||
<view class="content">
|
||
<!-- allowHalf 允许评分一半 -->
|
||
<uni-rate v-model="userScore" @change="onChange" :disabled="isScore" disabled-color="#FFCA3E"
|
||
allowHalf />
|
||
<text class="text">{{userScore}}分</text>
|
||
</view>
|
||
|
||
<!-- 底部内容-确认按钮 -->
|
||
<view class="footer">
|
||
<button @click="submitScore" :disabled="!userScore || isScore" type="default" size="mini"
|
||
plain>确认评分</button>
|
||
</view>
|
||
|
||
</view>
|
||
</uni-popup>
|
||
|
||
<!-- <view class="">{{readImgs}}</view> -->
|
||
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import {
|
||
ref
|
||
} from "vue"
|
||
import {
|
||
getPreviewBarHeight
|
||
} from "@/utils/system.js"
|
||
import {
|
||
onLoad
|
||
} from "@dcloudio/uni-app"
|
||
import {
|
||
apiGetSetScore,
|
||
apiWriteDownload
|
||
} from "@/api/apis.js"
|
||
// import { fail } from "assert";
|
||
import {apiDetailWall} from "@/api/apis.js"
|
||
import {onShareAppMessage,onShareTimeline} from "@dcloudio/uni-app"
|
||
|
||
const maskState = ref(true);
|
||
//信息弹窗
|
||
const infoPopup = ref(null)
|
||
//当前图片的具体信息内容
|
||
const currentInfo = ref({})
|
||
//评分弹窗
|
||
const scorePopup = ref(null)
|
||
//用户评分,v-model双向绑定
|
||
const userScore = ref(0)
|
||
//用户是否评完分了
|
||
const isScore = ref(false)
|
||
//对应分类的图片列表
|
||
const classList = ref([])
|
||
//当前图片的id
|
||
const currentId = ref(null)
|
||
//通过当前图片id获得的当前图片的index
|
||
const currentIndex = ref(0)
|
||
//已经看过的图片的索引数组
|
||
const readImgs = ref([])
|
||
|
||
|
||
//读取缓存里面的内容
|
||
//后面加一个空数组的原因是为了防止缓存里面没数据时用map会报错.
|
||
const storgClassList = uni.getStorageSync("storgClassList") || [];
|
||
classList.value = storgClassList.map(item => {
|
||
// 如果直接return item 的话,item里面所有的属性都会不见
|
||
// ...item的意思是就是将item里面的所有东西原模原样的全部拷贝到我们新return出来的数据
|
||
return {
|
||
...item,
|
||
//注意 _small.webp是小图,要看大图的话,需要将后缀改变成.jpg
|
||
picurl: item.smallPicurl.replace("_small.webp", ".jpg")
|
||
}
|
||
console.log(classList.value);
|
||
})
|
||
|
||
//onLoad方法获得页面传入的参数
|
||
onLoad(async (e) => {
|
||
// console.log(e);
|
||
//当前图片的id
|
||
currentId.value = e.id
|
||
|
||
// 判断用户是不是通过分享页面进来的
|
||
if(e.type == 'share'){
|
||
// apiDetailWall()这个函数需要一个接收图片的id
|
||
let res = await apiDetailWall({id:currentId.value})
|
||
//将返回对象的内容全部复制给classList.value,并修改picurl
|
||
classList.value = res.data.map(item =>{
|
||
return {
|
||
...item,
|
||
picurl:item.smallPicurl.replace("_small.webp", ".jpg")
|
||
}
|
||
})
|
||
}
|
||
|
||
|
||
|
||
// 注意:使用 _id 而不是 id,并确保类型匹配
|
||
//根据id找在数组中的索引
|
||
currentIndex.value = classList.value.findIndex(item => item._id == currentId.value)
|
||
// 预先加载和去重
|
||
readImgsFun()
|
||
currentInfo.value = classList.value[currentIndex.value]
|
||
})
|
||
|
||
|
||
//swiper的页面滑动时会触发swiper的change事件,有对象传入
|
||
const swiperChange = (e) => {
|
||
currentIndex.value = e.detail.current
|
||
// console.log(e);
|
||
readImgsFun()
|
||
currentInfo.value = classList.value[currentIndex.value]
|
||
}
|
||
|
||
//普通状态就是加载临近左右的两张图片,包自己共三张
|
||
//必须考虑加载页面时 第一页要加载最后一页 和 最后一页 要加载第一页的情况。
|
||
const readImgsFun = () => {
|
||
readImgs.value.push(
|
||
currentIndex.value <= 0 ? classList.value.length - 1 : currentIndex.value - 1,
|
||
currentIndex.value,
|
||
currentIndex.value >= classList.value.length - 1 ? 0 : currentIndex.value + 1
|
||
)
|
||
|
||
//new Set() 可以保持数组的唯一性。但是new Set 后的结果是一个 new Set对象
|
||
//因此我们需要将其转换为数组对象。因此对其使用 [...]
|
||
// ... 是展开运算符。将对象所有可枚举的属性逐一展开并复制到新对象中
|
||
// [] 是将其转换为数组对象
|
||
readImgs.value = [...new Set(readImgs.value)]
|
||
|
||
}
|
||
|
||
//点击info弹窗
|
||
const clickInfo = () => {
|
||
infoPopup.value.open();
|
||
}
|
||
//点击关闭info弹窗
|
||
const clickInfoClose = () => {
|
||
infoPopup.value.close();
|
||
}
|
||
|
||
//评分弹窗
|
||
const clickScore = () => {
|
||
// 判断本地是否存在这个userScore,来表明用户是否评过分了
|
||
if (currentInfo.value.userScore) {
|
||
isScore.value = true
|
||
//如果已经评完分了,将评过的分数赋值给展示的分数
|
||
userScore.value = currentInfo.value.userScore
|
||
}
|
||
scorePopup.value.open();
|
||
}
|
||
//点击关闭评分弹窗
|
||
const clickScoreClose = () => {
|
||
scorePopup.value.close();
|
||
userScore.value = 0
|
||
//复原思想,将这个flag变回原来的信息,展示的时候再变回来,可以免去许多的bug
|
||
isScore.value = false
|
||
}
|
||
|
||
//确认评分
|
||
const submitScore = async () => {
|
||
// console.log("评分了");
|
||
uni.showLoading({
|
||
title: "加载中...",
|
||
})
|
||
//冒号的作用是将后面的名字作为当前名字的别名。两字名字都代表同个东西
|
||
let {
|
||
classid,
|
||
_id: wallId
|
||
} = currentInfo.value
|
||
// 用户的评分作为参数传入,userScore是一个v-model
|
||
let res = await apiGetSetScore({
|
||
classid,
|
||
wallId,
|
||
userScore: userScore.value
|
||
})
|
||
uni.hideLoading();
|
||
// console.log(res);
|
||
if (res.errCode === 0) {
|
||
uni.showToast({
|
||
title: "评分成功",
|
||
icon: "none"
|
||
})
|
||
}
|
||
//classList.value[currentIndex.value] 表示找到这个数组里的这个对象
|
||
// .userScore 表示给这个对象加一个 userScore 属性
|
||
classList.value[currentIndex.value].userScore = userScore.value;
|
||
//然后将这个新的 classList 保存到本地
|
||
uni.setStorageSync("storgClassList", classList.value)
|
||
clickScoreClose()
|
||
}
|
||
|
||
//遮罩层状态
|
||
const maskChange = () => {
|
||
maskState.value = !maskState.value
|
||
}
|
||
|
||
//返回上一页
|
||
const goBack = () => {
|
||
uni.navigateBack({
|
||
success: () => {
|
||
|
||
},
|
||
fail:(err) => {
|
||
uni.reLaunch({
|
||
url:"/pages/index/index"
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
// 文件下载
|
||
const clickDownload = async () => {
|
||
// #ifdef H5
|
||
uni.showModal({
|
||
content: "请长按保存壁纸",
|
||
showCancel: false
|
||
})
|
||
// #endif
|
||
|
||
// #ifndef H5
|
||
try {
|
||
|
||
uni.showLoading({
|
||
title: "下载中...",
|
||
// 显示过程中不允许点击其他的东西
|
||
mask: true
|
||
})
|
||
|
||
let {
|
||
classid,
|
||
_id: wallId
|
||
} = currentInfo.value
|
||
let res = await apiWriteDownload({
|
||
classid,
|
||
wallId
|
||
})
|
||
|
||
/* != 表示5s内重复下载了
|
||
(人家接口设计的,5s内重复下载就提示这个东西,5s内只能下载一次 */
|
||
if (res.errCode != 0) {
|
||
throw res
|
||
}
|
||
|
||
uni.getImageInfo({
|
||
// 这个getImageInfo是为了根据网络地址获得一个临时下载地址,以便能够下载到相册里面
|
||
//单纯的使用 saveImageToPhotosAlbum 是不能够保存图片的
|
||
src: currentInfo.value.picurl,
|
||
success: (res) => {
|
||
uni.saveImageToPhotosAlbum({
|
||
filePath: res.path,
|
||
success: (res) => {
|
||
// console.log(res);
|
||
uni.showToast({
|
||
title:"保存成功,请到相册查看",
|
||
icon:"none"
|
||
})
|
||
},
|
||
fail: (err) => {
|
||
/* 下面这个情况是针对用户点击“下载”之后,但是没有保存时出现的
|
||
情况,,取消保存错误信息是fail cancel ,没有授权是fail auth deny */
|
||
if (err.errMsg == 'saveImagePhotoAlbum:fail cancel') {
|
||
uni.showToast({
|
||
title: "保存失败,请重新点击下载",
|
||
icon: "none"
|
||
})
|
||
//此时return 是为了结束步骤。不能继续进行到下面的代码当中
|
||
return;
|
||
}
|
||
uni.showModal({
|
||
title: "授权提示",
|
||
content: "需要授权保存相册",
|
||
//success表示系统回复了的情况
|
||
success: res => {
|
||
//这里表示点击了“确认"的情况
|
||
if (res.confirm) {
|
||
uni.openSetting({
|
||
success: setting => {
|
||
console.log(
|
||
setting);
|
||
/* setting.authSetting['scope.writePhotosAlbum']
|
||
表示的是你 "是否授权的值(true / false" */
|
||
if (setting
|
||
.authSetting[
|
||
'scope.writePhotosAlbum'
|
||
]) {
|
||
uni.showToast({
|
||
title: "获取授权成功",
|
||
icon: "none"
|
||
})
|
||
} else {
|
||
uni.showToast({
|
||
title: "获取权限失败",
|
||
icon: "none"
|
||
})
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
})
|
||
|
||
},
|
||
//complete表示无论成功还是失败都会做的事件
|
||
complete: () => {
|
||
uni.hideLoading();
|
||
}
|
||
})
|
||
|
||
}
|
||
})
|
||
|
||
} catch (err) {
|
||
uni.hideLoading()
|
||
}
|
||
// #endif
|
||
}
|
||
|
||
//分享给好友
|
||
onShareAppMessage((e)=>{
|
||
//分享这里是需要有一个 “return” 的
|
||
|
||
//在上面的onLoad中,已经将列表的id赋给了queryParams的id属性
|
||
return{
|
||
title:"hzb壁纸-",
|
||
// 必须传递id进去,否则进不去内部页面
|
||
path:"/pages/preview/preview?id="+currentId.value+"&type=share"
|
||
}
|
||
})
|
||
|
||
//分享到朋友圈
|
||
onShareTimeline(()=>{
|
||
return{
|
||
// 标题
|
||
title:"hzb壁纸~~~",
|
||
// 分享时候的图片地址。可以本地也可以网络图
|
||
// imageUrl:"/static/images/logo2.jpg"
|
||
//要想看朋友圈这个,需要带的query参数
|
||
// type表示是分享传入的
|
||
query:"id="+currentId.value+"&type=share"
|
||
}
|
||
})
|
||
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import '../../uni.scss';
|
||
// @include fix-position就是下面的这堆东西
|
||
// position: absolute;
|
||
// left: 0;
|
||
// right: 0;
|
||
// //单纯的给margin:auto的话,你的宽度会被拉长,需要给一个固定宽度
|
||
// margin: auto;
|
||
// //宽度你给多少都不能准确到达中间位置,给一个fit-content自动适应
|
||
// width:fit-content;
|
||
// color: #fff;
|
||
|
||
.preview {
|
||
width: 100%;
|
||
height: 100vh;
|
||
position: relative;
|
||
|
||
swiper {
|
||
width: 100%;
|
||
height: 100%;
|
||
|
||
image {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
}
|
||
|
||
.mask {
|
||
.goBack {
|
||
@include fix-position;
|
||
width: 38px;
|
||
height: 38px;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
left: 30rpx;
|
||
top: 0;
|
||
margin-left: 0;
|
||
border-radius: 100px;
|
||
backdrop-filter: blur(10rpx);
|
||
border: 1px solid rbga(255, 255, 255, 0.3);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.count {
|
||
position: absolute;
|
||
top: 10vh;
|
||
left: 0;
|
||
right: 0;
|
||
//单纯的给margin:auto的话,你的宽度会被拉长,需要给一个固定宽度
|
||
margin: auto;
|
||
//宽度你给多少都不能准确到达中间位置,给一个fit-content自动适应
|
||
width: fit-content;
|
||
background: rgba(0, 0, 0, 0.3);
|
||
font-size: 28rpx;
|
||
color: #fff;
|
||
border-radius: 40rpx;
|
||
padding: 8rpx 28rpx;
|
||
backdrop-filter: blur(20rpx);
|
||
}
|
||
|
||
.time {
|
||
@include fix-position;
|
||
//看设计图上面的设计
|
||
top: calc(10vh + 80rpx);
|
||
font-size: 140rpx;
|
||
font-weight: 100rpx;
|
||
//去除行高
|
||
line-height: 1em;
|
||
//加上文字阴影,避免图片是白色是时候你看不到文字
|
||
text-shadow: 0 4rpx rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.date {
|
||
@include fix-position;
|
||
top: calc(10vh + 230rpx);
|
||
font-size: 34rpx;
|
||
//加上文字阴影,避免图片是白色是时候你看不到文字
|
||
text-shadow: 0 2rpx rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.footer {
|
||
@include fix-position;
|
||
background: rgba(255, 255, 255, 0.8);
|
||
bottom: 10vh;
|
||
width: 65vw;
|
||
height: 120rpx;
|
||
border-radius: 120rpx;
|
||
color: #000;
|
||
display: flex;
|
||
justify-content: space-around;
|
||
align-items: center;
|
||
box-shadow: 0 2rpx rgba(0, 0, 0, 0.1);
|
||
backdrop-filter: blur(20rpx);
|
||
|
||
//使文字和样式在同一列上
|
||
.box {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
//给box加padding,增大接触距离
|
||
padding: 2rpx 12rpx;
|
||
|
||
.text {
|
||
font-size: 26rpx;
|
||
color: $text-font-color-2;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.popHeader {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
|
||
.title {
|
||
color: $text-font-color-2;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.close {
|
||
// 细节处理,增大图标接触面积,提高用户体验
|
||
padding: 6rpx
|
||
}
|
||
}
|
||
|
||
.infoPopup {
|
||
background: #fff;
|
||
padding: 30rpx;
|
||
border-radius: 30rpx 30rpx 0 0;
|
||
overflow: hidden;
|
||
|
||
scroll-view {
|
||
max-height: 60vh;
|
||
|
||
.content {
|
||
.row {
|
||
display: flex;
|
||
padding: 16rpx 0;
|
||
font-size: 32rpx;
|
||
// 行高控制
|
||
line-height: 1.7em;
|
||
|
||
.label {
|
||
color: $text-font-color-3;
|
||
width: 140rpx;
|
||
//文字靠右,就是word里面的右对齐
|
||
text-align: right;
|
||
font-size: 30rpx;
|
||
}
|
||
|
||
.value {
|
||
//上面父级已经给了宽度了,你子级就可以给一个flex:1来让其自动分配
|
||
flex: 1;
|
||
//因为可能 value的值可能很多,会把label挤压,所以加一个兼容性写法,宽度为0
|
||
width: 0;
|
||
font-size: 30rpx;
|
||
}
|
||
|
||
.roteBox {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.score {
|
||
font-size: 26rpx;
|
||
color: $text-font-color-2 ;
|
||
padding-left: 10rpx;
|
||
}
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
white-space: wrap;
|
||
|
||
.tab {
|
||
border: 1px solid $brand-theme-color;
|
||
color: $brand-theme-color;
|
||
font-size: 22rpx;
|
||
padding: 10rpx 30rpx;
|
||
;
|
||
border-radius: 40rpx;
|
||
line-height: 1em;
|
||
margin: 0 10rpx 10rpx 0;
|
||
}
|
||
}
|
||
|
||
.class {
|
||
color: $brand-theme-color;
|
||
}
|
||
}
|
||
|
||
.copyright {
|
||
font-size: 28rpx;
|
||
padding: 20rpx;
|
||
background: #F6F6F6;
|
||
color: #666;
|
||
border-radius: 10rpx;
|
||
margin: 20rpx 0;
|
||
line-height: 1.5em;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.scorePopup {
|
||
background: #fff;
|
||
padding: 30rpx;
|
||
width: 70vw;
|
||
border-radius: 30rpx;
|
||
overflow: hidden;
|
||
|
||
.content {
|
||
padding: 30rpx 0;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
|
||
.text {
|
||
color: #FFCA3E;
|
||
padding-left: 10rpx;
|
||
width: 80rpx;
|
||
line-height: 1em;
|
||
text-align: right;
|
||
}
|
||
}
|
||
|
||
.footer {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
}
|
||
}
|
||
</style> |