Files
test/my-uniapp-cli/pages/preview/preview.vue
2026-01-16 00:41:19 +00:00

707 lines
18 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="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;
// //单纯的给marginauto的话你的宽度会被拉长需要给一个固定宽度
// 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;
//单纯的给marginauto的话你的宽度会被拉长需要给一个固定宽度
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 {
//上面父级已经给了宽度了你子级就可以给一个flex1来让其自动分配
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>