Compare commits
72 Commits
33b5e299da
...
de35597c69
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de35597c69 | ||
|
|
434fa24723 | ||
|
|
3904393163 | ||
|
|
b852fa62e4 | ||
|
|
c7eb7ed879 | ||
|
|
61584a5d20 | ||
|
|
006223b01d | ||
|
|
09e47df0f2 | ||
|
|
7673b32f96 | ||
|
|
9e01fa0032 | ||
|
|
86eb7d47e0 | ||
|
|
e83260c6ff | ||
|
|
11f1a6d727 | ||
|
|
8dadc2be09 | ||
|
|
430a4a1491 | ||
|
|
ba4bc25785 | ||
|
|
5397dc9d8a | ||
|
|
e5979f605e | ||
|
|
6e320c3944 | ||
|
|
43abd1f3d6 | ||
|
|
afc79a0f26 | ||
|
|
4ca1b9cf42 | ||
|
|
071ab8de73 | ||
|
|
607ff57b2f | ||
|
|
05ef454ac8 | ||
|
|
4544b678d0 | ||
|
|
53d8455890 | ||
|
|
0b7edc39d4 | ||
|
|
464761e3f9 | ||
|
|
c758477084 | ||
|
|
9b00f8a04b | ||
|
|
c44d30294c | ||
|
|
e40f72db16 | ||
|
|
1d598d149b | ||
|
|
89336f5621 | ||
|
|
89d55643f6 | ||
|
|
9b4734acc5 | ||
|
|
293b3e6a35 | ||
|
|
07e0630824 | ||
|
|
2f79feb17e | ||
|
|
a0d99cad11 | ||
|
|
c1608eef5a | ||
|
|
75d515fbc6 | ||
|
|
dc39275e6e | ||
|
|
6e84da988c | ||
|
|
b09524e012 | ||
|
|
9a45c7aa03 | ||
|
|
c0c1f0716c | ||
|
|
7c31a17146 | ||
|
|
0a894696a7 | ||
|
|
15dd107326 | ||
|
|
6097c2b495 | ||
|
|
8b3f81f8da | ||
|
|
0b24b01054 | ||
|
|
f002705479 | ||
|
|
288772c534 | ||
|
|
96859c920f | ||
|
|
eb358d594b | ||
|
|
b0f1a37ece | ||
|
|
30b08f1933 | ||
|
|
debbf34b73 | ||
|
|
8b7b27d4ff | ||
|
|
6717bd10e7 | ||
|
|
956d5b5e3a | ||
|
|
2b187418e5 | ||
|
|
632cec0873 | ||
|
|
86de361ff8 | ||
|
|
a6dbc4692c | ||
|
|
42d8814157 | ||
|
|
54a2da7d22 | ||
|
|
b0eb163c21 | ||
|
|
f0e9803e5d |
3
.gitignore
vendored
@ -8,8 +8,7 @@ pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
yarn.lock
|
||||
|
||||
uni_modules
|
||||
mp-weixin
|
||||
node_modules
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
90
App.vue
@ -1,11 +1,62 @@
|
||||
<script setup>
|
||||
import { onLaunch,onShow,onHide } from '@dcloudio/uni-app'
|
||||
onLaunch(()=>{
|
||||
console.log('App Launch')
|
||||
console.log('App Launch');
|
||||
// #ifdef MP-WEIXIN
|
||||
// 获取小程序更新管理器实例
|
||||
const updateManager = wx.getUpdateManager();
|
||||
|
||||
// 监听检查更新事件,当小程序有新版本时会触发此回调
|
||||
updateManager.onCheckForUpdate(function (res) {
|
||||
// 请求完新版本信息的回调
|
||||
|
||||
console.log(res.hasUpdate); // 打印是否有新版本
|
||||
});
|
||||
|
||||
// 监听更新准备就绪事件,当新版本下载完成时会触发此回调
|
||||
updateManager.onUpdateReady(function () {
|
||||
wx.showModal({
|
||||
title: '更新提示',
|
||||
showCancel:false,
|
||||
content: '小程序版本已经更新,请重新进入!',
|
||||
success(res) {
|
||||
if (res.confirm) {
|
||||
// 用户确认更新,调用 applyUpdate 应用新版本并重启小程序
|
||||
updateManager.applyUpdate();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
updateManager.onUpdateFailed(function () {
|
||||
wx.showModal({
|
||||
title: '更新失败',
|
||||
content:'已经有新版本了,请您删除当前小程序,重新搜索打开',
|
||||
});
|
||||
// 新版本下载失败,可以在这里处理失败逻辑,例如提示用户稍后再试
|
||||
});
|
||||
// #endif
|
||||
});
|
||||
onShow(()=>{
|
||||
console.log('App Show')
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
wx.getSystemInfo({
|
||||
success: (res) => {
|
||||
// windows | mac为pc端
|
||||
// android | ios为手机端
|
||||
console.log('getSystemInfo,', res.platform);
|
||||
if( res.platform=="windows" || res.platform=="mac"){
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
showCancel:false,
|
||||
content: '请使用手机端登录',
|
||||
complete: (res) => {
|
||||
wx.exitMiniProgram()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
});
|
||||
onHide(()=>{
|
||||
console.log('App Hide')
|
||||
@ -33,4 +84,39 @@
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
.back{
|
||||
/* margin-top: -8rpx; */
|
||||
padding:0 20rpx 0rpx 20rpx;
|
||||
}
|
||||
.ellipsis-one-lines {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
/* #ifdef H5 */
|
||||
.zp-container{
|
||||
z-index: 0!important;
|
||||
}
|
||||
/* #endif */
|
||||
.u-image{
|
||||
background: none!important;
|
||||
}
|
||||
.u-steps-item__line{
|
||||
height:36px!important;
|
||||
}
|
||||
.casesdown.active{
|
||||
color:#3CC7C0!important;
|
||||
}
|
||||
.u-dropdown__menu__item__arrow--rotate .uicon-arrow-down{
|
||||
color: #3cc7c0 !important;
|
||||
}
|
||||
.tagbox{
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.tagbox .tag{
|
||||
margin:5rpx 0 ;
|
||||
}
|
||||
.zp-paging-container{
|
||||
background-color: #fff!important;
|
||||
}
|
||||
</style>
|
||||
30
api/api.js
@ -9,7 +9,7 @@ const api = {
|
||||
return request('/login/mobile', data, 'post', true);
|
||||
},
|
||||
getCode(data) {
|
||||
return request('/code/phone', data, 'post', true);
|
||||
return request('/expertAPI/smsSend', data, 'post', true);
|
||||
},
|
||||
|
||||
getHomeData(data) { //首页数据
|
||||
@ -36,9 +36,15 @@ const api = {
|
||||
},
|
||||
cancelCollectVideo(id){
|
||||
return request('/clinical/video/collect/'+id,{}, 'delete',false);
|
||||
},
|
||||
collectExchange(id){
|
||||
return request('/exchange/collect/'+id, {}, 'post',false);
|
||||
},
|
||||
cancelCollectExchange(id){
|
||||
return request('/exchange/collect/'+id,{}, 'delete',false);
|
||||
},
|
||||
addVideoComment(id,data){
|
||||
return request('/clinical/video/comment/'+id,data, 'post',false,'application/json');
|
||||
return request('/clinical/video/comment/'+id,data, 'post',true,'application/json');
|
||||
},
|
||||
delVideoComment(id){
|
||||
return request('/clinical/video/comment/'+id, {}, 'delete',false);
|
||||
@ -51,7 +57,7 @@ const api = {
|
||||
return request('/clinical/video/comment/top/'+id, {}, 'delete',false);
|
||||
},
|
||||
addArticleComment(id,data){
|
||||
return request('/clinical/article/comment/'+id, data, 'post',false,'application/json');
|
||||
return request('/clinical/article/comment/'+id, data, 'post',true,'application/json');
|
||||
},
|
||||
delArticleComment(id){
|
||||
return request('/clinical/article/comment/'+id, {}, 'delete',false);
|
||||
@ -81,7 +87,7 @@ const api = {
|
||||
return request('/clinical/hospital/search', data, 'post',true,'application/json');
|
||||
},
|
||||
getStaticDoctor(id){
|
||||
return request('/clinical/stats/doctor/'+id,{}, 'get',false);
|
||||
return request('/clinical/stats/doctor/'+id,{}, 'get',true);
|
||||
},
|
||||
getStaticSick(data) { //首页统计数据
|
||||
return request('/clinical/stats/label', data, 'get', false);
|
||||
@ -117,7 +123,7 @@ const api = {
|
||||
return request('/exchange/collect/'+id, {}, 'delete',false);
|
||||
},
|
||||
addExchangeComment(id,data){
|
||||
return request('/exchange/comment/'+id, data, 'post',false);
|
||||
return request('/exchange/comment/'+id, data, 'post',true,'application/json');
|
||||
},
|
||||
delExchangeComment(id){
|
||||
return request('/exchange/comment/'+id, {}, 'delete',false);
|
||||
@ -141,7 +147,7 @@ const api = {
|
||||
return request('/exchange/draft/search', data, 'post',false,'application/json');
|
||||
},
|
||||
saveDraft(data){
|
||||
return request('/exchange/draft', data, 'post',false,'application/json');
|
||||
return request('/exchange/draft', data, 'post',true,'application/json');
|
||||
},
|
||||
delDraft(id){
|
||||
return request('/exchange/draft/'+id, {}, 'delete',false);
|
||||
@ -179,12 +185,18 @@ const api = {
|
||||
givePoint(data){
|
||||
return request('/reward',data,'post',true);
|
||||
},
|
||||
h5Login(data){
|
||||
return request('/login/hcp',data,'post');
|
||||
h5Login(data){
|
||||
return request('/login/hcp',data,'post',true);
|
||||
},
|
||||
readRecord(data){
|
||||
return request('/user/case/read',data,'post',false,'application/json');
|
||||
}
|
||||
},
|
||||
updateExchange(id,data){
|
||||
return request('/exchange/'+id, data, 'put',true,'application/json');
|
||||
},
|
||||
delExchange(id){
|
||||
return request('/exchange/'+id, {}, 'delete',true);
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
|
||||
183
components/backDetailNav/backDetailNav.vue
Normal file
@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<view class="navbox">
|
||||
<view class="bg"></view>
|
||||
<view class="namebox">
|
||||
<view class="back" @click="goBack">
|
||||
<u-icon name="arrow-left" color="#000" size="24"></u-icon>
|
||||
</view>
|
||||
<view class="logo" @click="goHome">
|
||||
<up--image
|
||||
:src="logoImg"
|
||||
width="62rpx"
|
||||
height="62rpx"
|
||||
radius="50%"
|
||||
|
||||
></up--image>
|
||||
</view>
|
||||
<view class="name" @click="goHome">
|
||||
{{ navName }}
|
||||
<view class="navbg">
|
||||
<up--image
|
||||
:src="navbg"
|
||||
width="100rpx"
|
||||
height="31rpx"
|
||||
></up--image>
|
||||
</view>
|
||||
</view>
|
||||
<view class="user" @click="goMy">
|
||||
<up--image
|
||||
:src="useImg?useImg:headImg"
|
||||
width="62rpx"
|
||||
height="62rpx"
|
||||
radius="50%"
|
||||
|
||||
></up--image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import navTo from "@/utils/navTo.js";
|
||||
import logoImg from "@/static/logo.png";
|
||||
import headImg from "@/static/headImg.png";
|
||||
import navbg from "@/static/navbg.png"
|
||||
import { onMounted,ref} from 'vue';
|
||||
const props = defineProps({
|
||||
navName: {
|
||||
type: String,
|
||||
default: "我的",
|
||||
}
|
||||
|
||||
});
|
||||
const useImg=ref('')
|
||||
const goBack = () => {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail(){
|
||||
uni.redirectTo({
|
||||
url:'/pages/index/index'
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const goHome=()=>{
|
||||
console.log(props.navName);
|
||||
if(props.navName=="肝胆相照病例交流园地"){
|
||||
uni.reLaunch({
|
||||
url:'/pages/caseTalk/caseTalk'
|
||||
})
|
||||
}else{
|
||||
uni.reLaunch({
|
||||
url:'/pages/index/index'
|
||||
})
|
||||
};
|
||||
|
||||
}
|
||||
const goMy=()=>{
|
||||
navTo({
|
||||
url:'/pages/my/my'
|
||||
})
|
||||
};
|
||||
onMounted(()=>{
|
||||
let userInfo=uni.getStorageSync('userInfo');
|
||||
if(userInfo && userInfo.avatar){
|
||||
useImg.value=userInfo.avatar;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbox {
|
||||
padding-bottom: 20rpx;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
/* #ifdef H5 */
|
||||
height:120rpx;
|
||||
/* #endif */
|
||||
/* #ifdef MP-WEIXIN */
|
||||
height:200rpx;
|
||||
/* #endif */
|
||||
|
||||
background: radial-gradient(
|
||||
60% 90% at 4% 2%,
|
||||
#43c9c3 0%,
|
||||
#fff 100%
|
||||
);
|
||||
}
|
||||
.bg {
|
||||
z-index: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
// background: radial-gradient(
|
||||
// 43% 90% at 84% 6%,
|
||||
// #ffd6c9 0%,
|
||||
// rgba(255, 255, 255, 0) 100%
|
||||
// );
|
||||
}
|
||||
.namebox {
|
||||
/* #ifdef H5 */
|
||||
padding-top: 51rpx;
|
||||
/* #endif */
|
||||
/* #ifdef MP-WEIXIN */
|
||||
padding-top: 102rpx;
|
||||
/* #endif */
|
||||
|
||||
margin: 0rpx 30rpx 0rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.logo{
|
||||
margin-left: 35rpx;
|
||||
}
|
||||
.back{
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
.user{
|
||||
margin-left:55rpx;
|
||||
}
|
||||
.name {
|
||||
margin-left: 16rpx;
|
||||
font-size: 30rpx;
|
||||
color: #111827;
|
||||
position: relative;
|
||||
.navbg{
|
||||
position: absolute;
|
||||
z-index:-1;
|
||||
top:10rpx;
|
||||
left:18rpx;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.search {
|
||||
margin: 40rpx 30rpx 0rpx;
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.searchwrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
padding-left: 28rpx;
|
||||
margin-right: 23rpx;
|
||||
height: 80rpx;
|
||||
background: #fbfbfb;
|
||||
box-shadow: 0px 4rpx 10rpx 0px rgba(153, 153, 153, 0.5);
|
||||
border-radius: 40rpx;
|
||||
|
||||
.ipt {
|
||||
margin-left: 15rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -14,7 +14,16 @@
|
||||
|
||||
></up--image>
|
||||
</view>
|
||||
<view class="name">{{ navName }}</view>
|
||||
<view class="name">
|
||||
{{ navName }}
|
||||
<view class="navbg">
|
||||
<up--image
|
||||
:src="navbg"
|
||||
width="100rpx"
|
||||
height="31rpx"
|
||||
></up--image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
@ -22,6 +31,7 @@
|
||||
|
||||
<script setup>
|
||||
import logoImg from "@/static/logo.png";
|
||||
import navbg from "@/static/navbg.png"
|
||||
const props = defineProps({
|
||||
navName: {
|
||||
type: String,
|
||||
@ -32,6 +42,11 @@ const props = defineProps({
|
||||
const goBack = () => {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail(){
|
||||
uni.redirectTo({
|
||||
url:'/pages/index/index'
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@ -61,7 +76,13 @@ const goBack = () => {
|
||||
);
|
||||
}
|
||||
.namebox {
|
||||
padding-top: 102rpx;
|
||||
/* #ifdef H5 */
|
||||
padding-top: 51rpx;
|
||||
/* #endif */
|
||||
/* #ifdef MP-WEIXIN */
|
||||
padding-top: 102rpx;
|
||||
/* #endif */
|
||||
|
||||
margin: 0rpx 30rpx 0rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -78,6 +99,15 @@ const goBack = () => {
|
||||
margin-left: 16rpx;
|
||||
font-size: 30rpx;
|
||||
color: #111827;
|
||||
position: relative;
|
||||
.navbg{
|
||||
position: absolute;
|
||||
z-index:-1;
|
||||
top:10rpx;
|
||||
left:18rpx;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.search {
|
||||
|
||||
@ -6,7 +6,9 @@
|
||||
<u-icon name="arrow-left" color="#000" size="24"></u-icon>
|
||||
</view>
|
||||
<!-- <view class="logo">logo</view> -->
|
||||
<view class="name">{{ navName }}</view>
|
||||
<view class="name">
|
||||
{{ navName }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
@ -23,6 +25,11 @@ const props = defineProps({
|
||||
const goBack = () => {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail(){
|
||||
uni.redirectTo({
|
||||
url:'/pages/index/index'
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@ -51,7 +58,13 @@ const goBack = () => {
|
||||
);
|
||||
}
|
||||
.namebox {
|
||||
padding-top: 102rpx;
|
||||
/* #ifdef H5 */
|
||||
padding-top: 51rpx;
|
||||
/* #endif */
|
||||
/* #ifdef MP-WEIXIN */
|
||||
padding-top: 102rpx;
|
||||
/* #endif */
|
||||
|
||||
justify-content: center;
|
||||
margin: 0rpx 30rpx 0rpx;
|
||||
position: relative;
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
const playJs=ref("//player.polyv.net/resp/live-h5-player/latest/liveplayer.min.js");
|
||||
import { onMounted, onUnmounted, ref,watch} from 'vue';
|
||||
const playJs=ref("//player.polyv.net/script/player.js");
|
||||
const player=ref(null);
|
||||
const props=defineProps({
|
||||
vid:{
|
||||
@ -12,6 +12,10 @@ const props=defineProps({
|
||||
default:''
|
||||
}
|
||||
})
|
||||
const user_id=ref('');
|
||||
watch(()=>props.vid,()=>{
|
||||
loadPlayerScript(loadPlayer);
|
||||
})
|
||||
const loadPlayerScript=(callback)=> {
|
||||
if (!window.polyvLivePlayer) {
|
||||
const myScript = document.createElement('script');
|
||||
@ -23,17 +27,22 @@ const props=defineProps({
|
||||
}
|
||||
};
|
||||
const loadPlayer=()=> {
|
||||
let userInfo=uni.getStorageSync('userInfo');
|
||||
if(userInfo.user_id){
|
||||
user_id.value=userInfo.user_id;
|
||||
}
|
||||
|
||||
const polyvLivePlayer = window.polyvLivePlayer;
|
||||
player.value = polyvLivePlayer({
|
||||
player.value = polyvPlayer({
|
||||
wrap: '#player',
|
||||
width: '100%',
|
||||
height: 250,
|
||||
forceH5:true,
|
||||
df:3,
|
||||
uid:user_id.vlaue,
|
||||
vid: props.vid,
|
||||
});
|
||||
}
|
||||
onMounted(()=>{
|
||||
loadPlayerScript(loadPlayer);
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
player.value.destroy();
|
||||
})
|
||||
|
||||
@ -11,7 +11,10 @@
|
||||
|
||||
></up--image>
|
||||
</view>
|
||||
<view class="name">{{ navName }}</view>
|
||||
<view class="name">
|
||||
{{ navName }}
|
||||
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
@ -46,7 +49,13 @@
|
||||
);
|
||||
}
|
||||
.namebox {
|
||||
padding-top: 102rpx;
|
||||
/* #ifdef H5 */
|
||||
padding-top: 51rpx;
|
||||
/* #endif */
|
||||
/* #ifdef MP-WEIXIN */
|
||||
padding-top: 102rpx;
|
||||
/* #endif */
|
||||
|
||||
margin: 0rpx 30rpx 0rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -54,6 +63,15 @@
|
||||
margin-left: 16rpx;
|
||||
font-size: 30rpx;
|
||||
color: #111827;
|
||||
position: relative;
|
||||
.navbg{
|
||||
position: absolute;
|
||||
z-index:-1;
|
||||
top:10rpx;
|
||||
left:18rpx;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.search {
|
||||
|
||||
@ -11,7 +11,16 @@
|
||||
|
||||
></up--image>
|
||||
</view>
|
||||
<view class="name">{{ navName }}</view>
|
||||
<view class="name">
|
||||
{{ navName }}
|
||||
<view class="navbg">
|
||||
<up--image
|
||||
:src="navbg"
|
||||
width="100rpx"
|
||||
height="31rpx"
|
||||
></up--image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="search">
|
||||
<view class="searchwrap">
|
||||
@ -21,7 +30,7 @@
|
||||
<up--image
|
||||
:src="userHeadImg"
|
||||
@click="goMy"
|
||||
mode="widthFix"
|
||||
|
||||
width="62rpx"
|
||||
height="62rpx"
|
||||
radius="50%"
|
||||
@ -34,6 +43,7 @@
|
||||
import { ref, watch} from "vue";
|
||||
import headImg from "@/static/headImg.png";
|
||||
import logoImg from "@/static/logo.png";
|
||||
import navbg from "@/static/navbg.png"
|
||||
import navTo from "@/utils/navTo.js";
|
||||
import { onShow,onLoad } from "@dcloudio/uni-app";
|
||||
const userHeadImg = ref('');
|
||||
@ -74,11 +84,11 @@ const goMy=()=>{
|
||||
const search=()=>{
|
||||
if(props.type=='home'){
|
||||
navTo({
|
||||
url: `/pages/search/search?keyWord=${keyWord.value}`,
|
||||
url: `/pages/search/search?keyWord=${keyWord.value}&from=home`,
|
||||
});
|
||||
}else if(props.type=='caseTalk'){
|
||||
navTo({
|
||||
url: `/pages/specialList/specialList?keyWord=${keyWord.value}`,
|
||||
url: `/pages/specialList/specialList?keyWord=${keyWord.value}&from=talkHome`,
|
||||
});
|
||||
}
|
||||
// if (!keyWord.value) {
|
||||
@ -104,6 +114,7 @@ onLoad(()=>{
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbox {
|
||||
|
||||
padding-bottom: 20rpx;
|
||||
background-color: #f9fafb;
|
||||
position: relative;
|
||||
@ -127,7 +138,13 @@ onLoad(()=>{
|
||||
);
|
||||
}
|
||||
.namebox {
|
||||
padding-top: 102rpx;
|
||||
/* #ifdef H5 */
|
||||
padding-top: 51rpx;
|
||||
/* #endif */
|
||||
/* #ifdef MP-WEIXIN */
|
||||
padding-top: 102rpx;
|
||||
/* #endif */
|
||||
|
||||
margin: 0rpx 30rpx 0rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -135,6 +152,15 @@ onLoad(()=>{
|
||||
margin-left: 16rpx;
|
||||
font-size: 30rpx;
|
||||
color: #111827;
|
||||
position: relative;
|
||||
.navbg{
|
||||
position: absolute;
|
||||
z-index:-1;
|
||||
top:10rpx;
|
||||
left:18rpx;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.search {
|
||||
|
||||
@ -14,7 +14,16 @@
|
||||
|
||||
></up--image>
|
||||
</view>
|
||||
<view class="name">{{ navName }}</view>
|
||||
<view class="name">
|
||||
{{ navName }}
|
||||
<view class="navbg">
|
||||
<up--image
|
||||
:src="navbg"
|
||||
width="100rpx"
|
||||
height="31rpx"
|
||||
></up--image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="search">
|
||||
<view class="searchwrap">
|
||||
@ -22,9 +31,9 @@
|
||||
<up-icon name="search" size="26" color="#999" @click="search"></up-icon>
|
||||
</view>
|
||||
<up--image
|
||||
:src="headImg"
|
||||
:src="userHeadImg"
|
||||
@click="goMy"
|
||||
mode="widthFix"
|
||||
|
||||
width="62rpx"
|
||||
height="62rpx"
|
||||
radius="50%"
|
||||
@ -37,6 +46,7 @@
|
||||
import { ref, watch,defineEmits} from "vue";
|
||||
import headImg from "@/static/headImg.png";
|
||||
import logoImg from "@/static/logo.png";
|
||||
import navbg from "@/static/navbg.png"
|
||||
import { onShow,onLoad } from "@dcloudio/uni-app";
|
||||
import navTo from "@/utils/navTo.js";
|
||||
const keyWord = ref('');
|
||||
@ -56,14 +66,23 @@ const props=defineProps({
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
const placeholder = ref("输入疾病名称、标题、作者搜索");
|
||||
watch(()=>props.type,(newVal)=>{
|
||||
if(newVal==='doctor'){
|
||||
placeholder.value='输入医生姓名'
|
||||
}else if(newVal==='hospital'){
|
||||
placeholder.value='输入医院名称'
|
||||
}else if(newVal==='video' || newVal==='article' || newVal==='case' ){
|
||||
placeholder.value='搜索疾病、症状、医院'
|
||||
}else if(newVal==='video'){
|
||||
placeholder.value='输入疾病名称、标题、作者搜索'
|
||||
}else if(newVal==='article' ){
|
||||
placeholder.value='搜索疾病、症状、医院'
|
||||
}else if(newVal==='case'){
|
||||
placeholder.value='输入疾病名称'
|
||||
}else if(newVal==='myCase'){
|
||||
placeholder.value='输入疾病名称、标题搜索'
|
||||
}else if(newVal==='cert'){
|
||||
placeholder.value='输入疾病名称、标题搜索'
|
||||
}
|
||||
|
||||
},{immediate: true })
|
||||
@ -80,6 +99,11 @@ const goMy=()=>{
|
||||
const goBack = () => {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail(){
|
||||
uni.redirectTo({
|
||||
url:'/pages/index/index'
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
onLoad(()=>{
|
||||
@ -130,8 +154,17 @@ const search=()=>{
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
}
|
||||
:deep(.u-image){
|
||||
background: none!important;
|
||||
}
|
||||
.namebox {
|
||||
padding-top: 102rpx;
|
||||
/* #ifdef H5 */
|
||||
padding-top: 51rpx;
|
||||
/* #endif */
|
||||
/* #ifdef MP-WEIXIN */
|
||||
padding-top: 102rpx;
|
||||
/* #endif */
|
||||
|
||||
margin: 0rpx 30rpx 0rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -141,7 +174,17 @@ const search=()=>{
|
||||
.name {
|
||||
margin-left: 16rpx;
|
||||
font-size: 30rpx;
|
||||
margin-right: 200rpx;
|
||||
color: #111827;
|
||||
position: relative;
|
||||
.navbg{
|
||||
position: absolute;
|
||||
z-index:-1;
|
||||
top:10rpx;
|
||||
left:18rpx;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.search {
|
||||
|
||||
1059
components/pEditor/pEditor.vue
Normal file
@ -7,7 +7,7 @@
|
||||
:zIndex="99"
|
||||
:placeholder="false"
|
||||
activeColor="#3CC7C0"
|
||||
:safeAreaInsetBottom="false"
|
||||
:safeAreaInsetBottom="true"
|
||||
>
|
||||
<up-tabbar-item text="临床病例库" @click="handleClick" :activeIcon="ku_on" :inactiveIcon="ku">
|
||||
<!-- <view class="aa" slot="active-icon">222</view> -->
|
||||
@ -86,7 +86,7 @@ const handleClick = (e) => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style >
|
||||
<style lang='scss'>
|
||||
.u-page__item__slot-icon{
|
||||
width: 47rpx;
|
||||
height: 47rpx;
|
||||
@ -94,13 +94,12 @@ const handleClick = (e) => {
|
||||
.u-tabbar__content__item-wrapper{
|
||||
border-top: 2rpx solid #f3f4f6!important;
|
||||
}
|
||||
/* .page{
|
||||
:deep(.u-tabbar){
|
||||
border-top: 2rpx solid #f3f4f6!important;
|
||||
}
|
||||
:deep(.u-tabbar--fixed){
|
||||
border-top: 2rpx solid #f3f4f6!important;
|
||||
}
|
||||
} */
|
||||
|
||||
|
||||
.u-tabbar-item__text{
|
||||
margin-bottom: 20rpx!important;
|
||||
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
44
main.js
@ -1,10 +1,9 @@
|
||||
import App from './App'
|
||||
import navTo from './utils/navTo'
|
||||
// #ifdef h5
|
||||
import api from './api/api'
|
||||
import cookie from './utils/cookie'
|
||||
// #endif
|
||||
import uviewPlus, { setConfig } from 'uview-plus'
|
||||
// #ifdef H5
|
||||
//import VConsole from 'vconsole';
|
||||
// const vConsole = new VConsole();
|
||||
// #endif
|
||||
// #ifndef VUE3
|
||||
import Vue from 'vue'
|
||||
import './uni.promisify.adaptor'
|
||||
@ -20,39 +19,7 @@ import { createSSRApp } from 'vue'
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
|
||||
app.use(uviewPlus, async() => {
|
||||
// if(process.env.UNI_PLATFORM =="h5"){
|
||||
|
||||
// let token = '';
|
||||
// if(window.location.href.indexOf('//casedata.igandan.com')>-1){
|
||||
// token = uni.getStorageSync('AUTH_TOKEN_CASEDATA');
|
||||
// }else{
|
||||
// token = uni.getStorageSync('DEV_AUTH_TOKEN_CASEDATA');
|
||||
// }
|
||||
// if(!token){
|
||||
// let video_token = cookie.readCookie('video_token');
|
||||
// alert(video_token)
|
||||
// const res = await api.h5Login({
|
||||
// token: video_token
|
||||
// });
|
||||
// let result = res.data;
|
||||
// if (window.location.href.indexOf('//casedata.igandan.com')>-1) {
|
||||
// uni.setStorageSync("AUTH_TOKEN_CASEDATA",result.token);
|
||||
// } else {
|
||||
// uni.setStorageSync("DEV_AUTH_TOKEN_CASEDATA",result.token);
|
||||
// };
|
||||
// uni.setStorageSync("userInfo",{
|
||||
// avatar:result.avatar,
|
||||
// user_id:result.user_id,
|
||||
// status:result.status,
|
||||
// user_name:result.user_name,
|
||||
// doctor_id:result.doctor_id,
|
||||
// });
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
|
||||
app.use(uviewPlus, async() => {
|
||||
return {
|
||||
options: {
|
||||
// 修改$u.config对象的属性
|
||||
@ -64,7 +31,6 @@ export function createApp() {
|
||||
}
|
||||
})
|
||||
|
||||
//app.config.globalProperties.$navTo = navTo
|
||||
return {
|
||||
app
|
||||
}
|
||||
|
||||
@ -61,13 +61,12 @@
|
||||
"minified" : true,
|
||||
"postcss" : true
|
||||
},
|
||||
"plugins": {
|
||||
"polyv-player": {
|
||||
"version": "1.9.0",
|
||||
"provider": "wx4a350a258a6f7876"
|
||||
"plugins" : {
|
||||
"polyv-player" : {
|
||||
"version" : "1.9.1",
|
||||
"provider" : "wx4a350a258a6f7876"
|
||||
}
|
||||
},
|
||||
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-alipay" : {
|
||||
@ -86,10 +85,11 @@
|
||||
"h5" : {
|
||||
"router" : {
|
||||
"mode" : "history",
|
||||
"base" : ""
|
||||
"base" : "/web/"
|
||||
},
|
||||
"devServer" : {
|
||||
"https" : false
|
||||
}
|
||||
},
|
||||
"title" : "肝胆相照临床病例库"
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"dayjs": "^1.11.13",
|
||||
"uview-plus": "^3.4.4"
|
||||
"uview-plus": "^3.4.4",
|
||||
"vconsole": "^3.15.1"
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,6 +116,13 @@
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/web/web",
|
||||
"style": {
|
||||
"navigationBarTitleText": "肝胆相照临床病例库",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/searchList/searchList",
|
||||
"style": {
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<view class="name">病例数</view>
|
||||
</view>
|
||||
<view class="cell">
|
||||
<view class="num">{{numInfo.exchange_collect_num}}</view>
|
||||
<view class="num">{{numInfo.exchange_comment_num}}</view>
|
||||
<view class="name">互动数</view>
|
||||
</view>
|
||||
<view class="cell">
|
||||
@ -28,7 +28,7 @@
|
||||
<view class="list">
|
||||
<view class="cell" v-for="item in most_read_articles" :key="item.exchange_id" @click="goDetail(item.exchange_id)">
|
||||
<view class="circle"></view>
|
||||
<view class="title ellipsis-two-lines">{{ item.exchange_title }}</view>
|
||||
<view class="title ellipsis-one-lines">{{ item.exchange_title }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -62,7 +62,8 @@
|
||||
>{{ item.user_name }}({{ item.hospital_name }})</view
|
||||
>
|
||||
</view>
|
||||
<view class="content" @click="goDetail(item.exchange_id)"> {{ item.exchange_content }} </view>
|
||||
<view class="content ellipsis-two-lines" @click="goDetail(item.exchange_id)"> {{ htmlToText(item.exchange_content) }} </view>
|
||||
|
||||
<view
|
||||
class="imgbox"
|
||||
@click="goDetail(item.exchange_id)"
|
||||
@ -73,7 +74,8 @@
|
||||
>
|
||||
<view
|
||||
class="imgcell"
|
||||
v-for="unit in item.exchange_content_image"
|
||||
v-for="unit in item.exchange_content_image.slice(0,3)"
|
||||
:key="unit"
|
||||
>
|
||||
<up--image
|
||||
:src="unit"
|
||||
@ -85,28 +87,32 @@
|
||||
</up--image>
|
||||
</view>
|
||||
</view>
|
||||
<view class="videocon">
|
||||
<view class="videocon" @click="goDetail(item.exchange_id)">
|
||||
<view
|
||||
class="imgbox"
|
||||
@click="goDetail(item.exchange_id)"
|
||||
v-if="
|
||||
item.exchange_content_video &&
|
||||
item.exchange_content_video.length > 0
|
||||
"
|
||||
>
|
||||
<view class="videomask"></view>
|
||||
<video
|
||||
:key="index"
|
||||
v-for="(videoCell, index) in item.exchange_content_video"
|
||||
:key="index"
|
||||
v-for="(videoCell, index) in item.exchange_content_video.slice(0,1)"
|
||||
class="myVideo"
|
||||
|
||||
:src="videoCell"
|
||||
@error="videoErrorCallback"
|
||||
controls
|
||||
></video>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tagbox" @click="goDetail(item.exchange_id)" v-if="item.label && item.label.length > 0">
|
||||
|
||||
<view class="tagbox" @click="goDetail(item.exchange_id)" v-if="item.exchange_label">
|
||||
<view
|
||||
class="tag"
|
||||
v-for="cell in item.label"
|
||||
v-for="cell in item.exchange_label"
|
||||
:key="cell.exchange_label_id"
|
||||
>{{ cell.label_name }}</view
|
||||
>
|
||||
@ -117,7 +123,7 @@
|
||||
<up-icon name="eye" color="#4B5563" size="28rpx"></up-icon>
|
||||
<view class="num">{{ item.read_num }}</view>
|
||||
</view>
|
||||
<view class="collect item">
|
||||
<view class="collect item" v-if="item.collect_num>0">
|
||||
<up-icon
|
||||
name="heart"
|
||||
color="#4B5563"
|
||||
@ -125,7 +131,7 @@
|
||||
></up-icon>
|
||||
<view class="num">{{ item.collect_num }}</view>
|
||||
</view>
|
||||
<view class="chat item">
|
||||
<view class="chat item" v-if="item.comment_num>0">
|
||||
<up-icon name="chat" color="#4B5563" size="28rpx"></up-icon>
|
||||
<view class="num">{{ item.comment_num }}</view>
|
||||
</view>
|
||||
@ -142,6 +148,9 @@
|
||||
</view>
|
||||
|
||||
<tabBar :value="2"></tabBar>
|
||||
<view class="publish" @click="goPublish">
|
||||
<up-icon name="plus" color="#fff" size="18"></up-icon>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -152,12 +161,12 @@
|
||||
import navTo from "@/utils/navTo.js";
|
||||
import api from "@/api/api";
|
||||
import { reactive,ref } from 'vue';
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import { onShow,onLoad } from "@dcloudio/uni-app";
|
||||
const numInfo=reactive({})
|
||||
const most_read_articles=ref([]);
|
||||
const new_articles=ref([]);
|
||||
|
||||
|
||||
const hasUse=ref(false);
|
||||
const formatdate = (date) => {
|
||||
return dayjs(date).format('YYYY-MM-DD')
|
||||
};
|
||||
@ -190,15 +199,18 @@ const goDetail = (id) => {
|
||||
});
|
||||
};
|
||||
const videoErrorCallback = (e) => {
|
||||
uni.showModal({
|
||||
content: e.target.errMsg,
|
||||
showCancel: false
|
||||
})
|
||||
// uni.showModal({
|
||||
// content: e.target.errMsg,
|
||||
// showCancel: false
|
||||
// })
|
||||
};
|
||||
const searchList = async () => {
|
||||
let searchForm = {
|
||||
page: 1,
|
||||
page_size: 10
|
||||
page_size: 20,
|
||||
order:{
|
||||
push_date:'desc'
|
||||
}
|
||||
};
|
||||
|
||||
api.searchExchage({
|
||||
@ -211,10 +223,20 @@ const goDetail = (id) => {
|
||||
|
||||
});
|
||||
};
|
||||
const htmlToText=(html)=>{
|
||||
return html
|
||||
.replace(/<[^>]*>/g, '') // 移除所有HTML标签
|
||||
.replace(/ /gi, ' ') // 将HTML实体转换为字符
|
||||
.replace(/<br\s*\/?>/gi, '\n').replace(/<img\s[^>]*>/gi, '') // 移除<img>标签及其内容
|
||||
.replace(/<video[^>]*>[\s\S]*?/gi, ''); // 将换行符替换为实际的换行符
|
||||
}
|
||||
const goodList=()=>{
|
||||
api.searchExchageGood({
|
||||
is_selected:1,
|
||||
limit:5
|
||||
order:{
|
||||
push_date:'desc'
|
||||
},
|
||||
limit:3
|
||||
}).then(res=>{
|
||||
most_read_articles.value=res.data.data
|
||||
})
|
||||
@ -224,10 +246,31 @@ const hotList=()=>{
|
||||
is_selected:0,
|
||||
limit:5,
|
||||
page:1,
|
||||
|
||||
page_size:5
|
||||
}).then(res=>{
|
||||
})
|
||||
}
|
||||
const goPublish=()=>{
|
||||
navTo({
|
||||
url:'/pages/publish/publish'
|
||||
})
|
||||
}
|
||||
onLoad((options)=>{
|
||||
console.log(options.from && !hasUse.value)
|
||||
if(options.from && !hasUse.value){
|
||||
|
||||
setTimeout(()=>{
|
||||
uni.showToast({
|
||||
duration:2000,
|
||||
title: "提交成功,请耐心等待审核",
|
||||
icon: "none",
|
||||
})
|
||||
hasUse.value=true;
|
||||
},300)
|
||||
}
|
||||
|
||||
})
|
||||
onShow(()=>{
|
||||
getStatic();
|
||||
goodList();
|
||||
@ -236,18 +279,39 @@ onShow(()=>{
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.publish{
|
||||
width: 92rpx;
|
||||
height: 92rpx;
|
||||
background: #3CC7C0;
|
||||
border-radius: 50%;
|
||||
position: fixed;
|
||||
z-index:9;
|
||||
right:30rpx;
|
||||
bottom:180rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.upage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 100rpx);
|
||||
height: calc(100vh - 120rpx);
|
||||
}
|
||||
.mostread{
|
||||
.title{
|
||||
font-weight: bold;
|
||||
}
|
||||
.cell{
|
||||
padding: 22rpx 0rpx!important;
|
||||
margin:0 30rpx;
|
||||
display: flex;
|
||||
.title{
|
||||
font-weight: normal!important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.box {
|
||||
flex: 1;
|
||||
@ -341,7 +405,13 @@ onShow(()=>{
|
||||
.imgbox {
|
||||
display: flex;
|
||||
margin-top: 24rpx;
|
||||
|
||||
position: relative;
|
||||
.videomask{
|
||||
position: absolute;
|
||||
width:100%;
|
||||
height:100%;
|
||||
z-index:2;
|
||||
}
|
||||
.imgcell {
|
||||
width: 220rpx;
|
||||
height: 220rpx;
|
||||
@ -357,6 +427,7 @@ onShow(()=>{
|
||||
}
|
||||
|
||||
.special {
|
||||
background-color: #fff;
|
||||
padding-top: 30rpx;
|
||||
|
||||
.list {
|
||||
@ -366,6 +437,7 @@ onShow(()=>{
|
||||
border-bottom: 2rpx solid #f3f4f6;
|
||||
|
||||
.circle {
|
||||
flex-shrink: 0;
|
||||
margin-top: 15rpx;
|
||||
width: 15rpx;
|
||||
height: 15rpx;
|
||||
@ -377,6 +449,7 @@ onShow(()=>{
|
||||
margin-left: 10rpx;
|
||||
font-size: 32rpx;
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
line-height: 46rpx;
|
||||
}
|
||||
}
|
||||
@ -391,7 +464,10 @@ onShow(()=>{
|
||||
margin: 0 30rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.title{
|
||||
font-weight: bold;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
.more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<backNav :navName="'病例收录证书'"></backNav>
|
||||
<view class="imgbox">
|
||||
<u--image
|
||||
:showLoading="true"
|
||||
:src="src"
|
||||
width="100%"
|
||||
mode="widthFix"
|
||||
></u--image>
|
||||
<view class="box">
|
||||
<u--image
|
||||
:showLoading="true"
|
||||
:src="src"
|
||||
width="100%"
|
||||
mode="widthFix"
|
||||
></u--image>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -27,6 +29,16 @@ onLoad((options) => {
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.imgbox{
|
||||
margin-top: -30rpx;
|
||||
margin: -30rpx 30rpx 0;
|
||||
overflow-y: scroll;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction:column;
|
||||
justify-content: center;
|
||||
min-height:calc(100vh - 200rpx);
|
||||
.box{
|
||||
width:100%;
|
||||
height:100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -9,10 +9,10 @@
|
||||
@query="queryList"
|
||||
>
|
||||
<template #top>
|
||||
<navBarSearch :searchWord="keyWord" :navName="navName" @changeWord="changeWord"></navBarSearch>
|
||||
<view class="detail" v-if="isSearch">
|
||||
<view class="desc" >检索到:<text class="red">{{numInfo.search_article_num}}篇文章</text></view>
|
||||
<view class="desc">检索到:<text class="red">{{numInfo.search_video_num}}个视频</text></view>
|
||||
<navBarSearch :searchWord="keyWord" :navName="navName" @changeWord="changeWord" :type="'cert'"></navBarSearch>
|
||||
<view class="detail" v-if="isSearch">
|
||||
<view class="desc" v-if="tab==0">检索到:<text class="red">{{total}}篇文章</text></view>
|
||||
<view class="desc" v-if="tab==1">检索到:<text class="red">{{total}}个视频</text></view>
|
||||
<view class="desc" v-if="keyWord">检索词:<text class="red">{{ keyWord }}</text></view>
|
||||
</view>
|
||||
<view class="tabcon">
|
||||
@ -25,10 +25,10 @@
|
||||
</view>
|
||||
</template>
|
||||
<view class="item" v-for="(item, index) in dataList" :key="index" @click="goDetail(item.cert_image)">
|
||||
<view class="title ellipsis">{{tab==0?item.article_title:item.video_title }}</view>
|
||||
<view class="tagsbox">
|
||||
<view class="title ellipsis-two-lines">{{tab==0?item.article_title:item.video_title }}</view>
|
||||
<!-- <view class="tagsbox">
|
||||
<view class="tag" v-for="tag in item.author" :key="tag.author_id">{{ tag.doctor_name }}</view>
|
||||
</view>
|
||||
</view> -->
|
||||
<view class="deal">
|
||||
<view class="left">
|
||||
<view class="recored">
|
||||
@ -55,7 +55,7 @@ import { ref, reactive } from "vue";
|
||||
import navBarSearch from "@/components/navBarSearch/navBarSearch.vue";
|
||||
import list from "@/uni_modules/z-paging/components/z-paging/z-paging";
|
||||
import api from "@/api/api";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { onLoad,onShow } from "@dcloudio/uni-app";
|
||||
import dayjs from "dayjs";
|
||||
import switchImg from "@/static/switch.png";
|
||||
import navTo from "@/utils/navTo.js";
|
||||
@ -70,7 +70,7 @@ const tab=ref(0);
|
||||
const hospital_id=ref('');
|
||||
const doctor_id=ref('');
|
||||
const numInfo=reactive({});
|
||||
const navName=ref('肝胆相照临床病例库')
|
||||
const navName=ref('肝胆相照临床病例库收录证书')
|
||||
|
||||
const formatdate=(date)=>{
|
||||
return dayjs(date).format('YYYY-MM-DD')
|
||||
@ -93,6 +93,13 @@ onLoad((options) => {
|
||||
if(options.doctor_id){
|
||||
doctor_id.value=options.doctor_id;
|
||||
}
|
||||
let userInfo=uni.getStorageSync('userInfo');
|
||||
if(userInfo.user_name){
|
||||
navName.value=userInfo.user_name+"临床病例库收录证书"
|
||||
}
|
||||
})
|
||||
onShow(()=>{
|
||||
paging.value?.refresh();
|
||||
})
|
||||
const goDetail=(url)=>{
|
||||
|
||||
@ -278,6 +285,7 @@ const queryList = (pageNo, pageSize) => {
|
||||
.tagsbox {
|
||||
margin-top: 20rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.tag {
|
||||
padding: 0 10rpx;
|
||||
margin-right: 16rpx;
|
||||
|
||||
@ -68,10 +68,10 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="listbox">
|
||||
<view class="cell" v-for="item in new_articles" :key="item.article_id" @click="goDetail(item.article_id)">
|
||||
<view class="cell" v-for="item in new_articles" :key="item.article_id" @click="goDetail(item.article_id,item.is_link,item.is_link_url)">
|
||||
<view class="circle"></view>
|
||||
<view class="info">
|
||||
<view class="name">{{item.article_title }}</view>
|
||||
<view class="name ellipsis-two-lines">{{item.article_title }}</view>
|
||||
<view class="tagsbox">
|
||||
<view class="tag" v-for="tag in item.author" :key="tag.author_id">{{ tag.doctor_name }}</view>
|
||||
</view>
|
||||
@ -89,10 +89,10 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="listbox">
|
||||
<view class="cell" v-for="item in most_read_articles" :key="item.article_id" @click="goDetail(item.article_id)">
|
||||
<view class="circle"></view>
|
||||
<view class="cell" v-for="item in most_read_articles" :key="item.article_id" @click="goDetail(item.article_id,item.is_link,item.is_link_url)">
|
||||
<view class="circle "></view>
|
||||
<view class="info">
|
||||
<view class="name">{{item.article_title }}</view>
|
||||
<view class="name ellipsis-two-lines">{{item.article_title }}</view>
|
||||
<view class="tagsbox">
|
||||
<view class="tag" v-for="tag in item.author" :key="tag.author_id">{{ tag.doctor_name }}</view>
|
||||
</view>
|
||||
@ -109,7 +109,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="list">
|
||||
<view class="cell" v-for="item in recommend_hospital" :key="item.hospital_id" @click="goListBy(item.hospital_id,item.hospital_name ,'hospital')">{{ item.hospital_name }}</view>
|
||||
<view class="cell ellipsis-one-lines" v-for="item in recommend_hospital" :key="item.hospital_id" @click="goListBy(item.hospital_id,item.hospital_name ,'hospital')">{{ item.hospital_name }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="recbox">
|
||||
@ -121,12 +121,13 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="list">
|
||||
<view class="cell" v-for="item in recommend_doctor" :key="item.doctor_id" @click="goListBy(item.doctor_id,item.doctor_name ,'doctor')">{{ item.doctor_name }}</view>
|
||||
<view class="cell ellipsis-one-lines" v-for="item in recommend_doctor" :key="item.doctor_id" @click="goListBy(item.doctor_id,item.doctor_name ,'doctor')">{{ item.doctor_name }}({{item.hospital_name}})</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<tabBar></tabBar>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -149,10 +150,28 @@ const new_articles = ref([]);
|
||||
const recommend_doctor=ref([]);
|
||||
const recommend_hospital=ref([]);
|
||||
const numInfo=reactive({})
|
||||
const goDetail = (id) => {
|
||||
navTo({
|
||||
url: `/pages/detail/detail?id=${id}&type=article`,
|
||||
});
|
||||
const goDetail = (id,isLink,src) => {
|
||||
if(isLink==1){
|
||||
api.readRecord({
|
||||
type:1,
|
||||
id:id
|
||||
}).then(res=>{
|
||||
|
||||
})
|
||||
// #ifdef MP-WEIXIN
|
||||
navTo({
|
||||
url: `/pages/web/web?src=${src}`,
|
||||
});
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
window.location.href=`${src}`
|
||||
// #endif
|
||||
}else{
|
||||
navTo({
|
||||
url: `/pages/detail/detail?id=${id}&type=article`,
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
const goListBy=(id,name,type)=>{
|
||||
let url=''
|
||||
@ -208,6 +227,7 @@ const goHospital=()=>{
|
||||
url:'/pages/searchList/searchList?type=hospital&name=医院&id='
|
||||
})
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
getStatic();
|
||||
getData();
|
||||
@ -218,6 +238,7 @@ onShow(() => {
|
||||
.tagsbox {
|
||||
margin-top: 20rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.tag {
|
||||
padding: 0 10rpx;
|
||||
margin-right: 16rpx;
|
||||
@ -261,6 +282,7 @@ onShow(() => {
|
||||
margin-top: 18rpx;
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
flex-shrink: 0;
|
||||
background: #3cc7c0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
@ -280,6 +302,10 @@ onShow(() => {
|
||||
margin: 0 30rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.title{
|
||||
font-weight: bold;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
.more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -296,7 +322,7 @@ onShow(() => {
|
||||
}
|
||||
.page {
|
||||
background: #f9fafb;
|
||||
height: calc(100vh - 400rpx);
|
||||
height: calc(100vh - 425rpx);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.kubox {
|
||||
@ -344,4 +370,5 @@ onShow(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<view class="logincontent">
|
||||
<view class="title">登录观看</view>
|
||||
<view class="title">欢迎登录</view>
|
||||
<up-form labelPosition="left" ref="form" labelWidth="115rpx">
|
||||
<up-form-item
|
||||
label="手机号"
|
||||
@ -10,7 +10,7 @@
|
||||
>
|
||||
<up-input
|
||||
v-model="phone"
|
||||
placeholder="请输入肝胆相照专家版手机号"
|
||||
placeholder="请输入手机号"
|
||||
></up-input>
|
||||
</up-form-item>
|
||||
<!-- <view v-if="isPhoneLogin" class="pwdbox">
|
||||
@ -122,10 +122,11 @@
|
||||
<view class="right" @click="goRegister"> 注册 </view>
|
||||
</view>
|
||||
<view class="row" style="margin-top:10rpx">
|
||||
<radio-group @click="radioChange" :labelDisabled="true" >
|
||||
<label class="radio"><radio :labelDisabled="true" value="1" :checked="checked" color="rgb(41, 121, 255)" />
|
||||
我已阅读并同意<a class="agree">《用户协议》</a>
|
||||
<radio-group :labelDisabled="true" class="group" >
|
||||
<label class="radio"><radio :labelDisabled="true" @click="radioChange" value="1" :checked="checked" color="rgb(41, 121, 255)" />
|
||||
|
||||
</label>
|
||||
我已阅读并同意<text class="agree" @click="goAgree">《用户协议》</text>
|
||||
</radio-group>
|
||||
<!-- <up-radio-group v-model="checked">
|
||||
<up-radio
|
||||
@ -136,16 +137,16 @@
|
||||
<view class="row">
|
||||
<view class="tip">操作说明</view>
|
||||
</view>
|
||||
<view class="line">
|
||||
<!-- <view class="line">
|
||||
<view class="qq">1、</view>
|
||||
肝胆相照注册账号与微信绑定,肝胆相照相关直播、视频无忧随心看
|
||||
</view>
|
||||
<view class="line">
|
||||
</view> -->
|
||||
<!-- <view class="line">
|
||||
<view class="qq">2、</view>
|
||||
仅需操作一次,后续通过微信观看直播、视频无需额外操作,立即进入
|
||||
</view>
|
||||
</view> -->
|
||||
<view class="line">
|
||||
<view class="qq">3、</view>
|
||||
<!-- <view class="qq">3、</view> -->
|
||||
若您还未注册肝胆相照专家版App, 请直接点击“注册”进行注册操作
|
||||
</view>
|
||||
<view class="desc">
|
||||
@ -155,6 +156,19 @@
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
<up-overlay :show="showModal" :mask-click-able="false">
|
||||
<view class="zanboxpop">
|
||||
<view class="zanwraper">
|
||||
<view class="title">提示</view>
|
||||
<view class="content">{{reason}} </view>
|
||||
|
||||
<view class="btnbox" style="justify-content: center;">
|
||||
<view class="cancle" @click="showModal = false">取消</view>
|
||||
<view class="ok" @click="goPrefect">去完善</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</up-overlay>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -162,10 +176,13 @@ import { ref, reactive } from "vue";
|
||||
import { onShow,onLoad } from "@dcloudio/uni-app";
|
||||
import api from "@/api/api";
|
||||
import auth from "@/utils/auth";
|
||||
import navTo from "../../utils/navTo";
|
||||
const code = ref('');
|
||||
const phone = ref(null);
|
||||
const showModal=ref(false)
|
||||
const isPwdPic = ref(false);
|
||||
const isPhoneLogin = ref(true);
|
||||
const reason=ref('');
|
||||
const customStyle = reactive({
|
||||
height: "90rpx",
|
||||
fontSize: "36rpx",
|
||||
@ -234,6 +251,7 @@ const getPhoneNumber = (e) => {
|
||||
source:1
|
||||
})
|
||||
.then((data) => {
|
||||
uni.hideKeyboard()
|
||||
let result=data.data.data;
|
||||
if(process.env.UNI_PLATFORM =="h5"){
|
||||
if(window.location.href.indexOf('//casedata.igandan.com')>-1){
|
||||
@ -259,15 +277,36 @@ const getPhoneNumber = (e) => {
|
||||
doctor_id:result.doctor_id,
|
||||
});
|
||||
goPage()
|
||||
}).catch((err)=>{
|
||||
uni.showToast({
|
||||
title: err,
|
||||
icon: "none",
|
||||
});
|
||||
});
|
||||
}).catch(err=>{
|
||||
let err_result=err.data;
|
||||
if(err_result.code==10007){
|
||||
showModal.value=true;
|
||||
reason.value=err_result.message;
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
const goAgree=()=>{
|
||||
uni.navigateTo({
|
||||
url:'/pages/web/web?src=https://doc.igandan.com/app/integral/expert_zcxy.html'
|
||||
})
|
||||
}
|
||||
const goPrefect=()=>{
|
||||
showModal.value=false;
|
||||
const { envVersion } = uni.getAccountInfoSync().miniProgram;
|
||||
let src =''
|
||||
if (envVersion == "release") {
|
||||
src = "https://wx.igandan.com/hcp/perfect?mobile="+phone.value+"&fromType=weChat";
|
||||
} else {
|
||||
src = "https://dev-wx.igandan.com/hcp/perfect?mobile="+phone.value+"&fromType=weChat";
|
||||
}
|
||||
uni.navigateTo({
|
||||
url:'/pages/register/register?src='+encodeURIComponent(src)
|
||||
})
|
||||
}
|
||||
const phoneLogin = () => {
|
||||
if (!isPhoneNum(phone.value)) return;
|
||||
if (!code.value){
|
||||
@ -291,6 +330,7 @@ const phoneLogin = () => {
|
||||
wx_code: res,
|
||||
})
|
||||
.then((data) => {
|
||||
uni.hideKeyboard()
|
||||
let result=data.data.data;
|
||||
const { envVersion } = uni.getAccountInfoSync().miniProgram;
|
||||
if (envVersion == "release") {
|
||||
@ -306,7 +346,15 @@ const phoneLogin = () => {
|
||||
doctor_id:result.doctor_id,
|
||||
});
|
||||
goPage()
|
||||
});
|
||||
}).catch(err=>{
|
||||
let err_result=err.data;
|
||||
if(err_result.code==10007){
|
||||
showModal.value=true;
|
||||
reason.value=err_result.message;
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -372,18 +420,26 @@ const start = () => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.radio{
|
||||
display: inline-flex;
|
||||
font-size: 28rpx;
|
||||
radio{
|
||||
transform:scale(0.8) translateY(-2px);
|
||||
|
||||
}
|
||||
.agree {
|
||||
background: none;
|
||||
color:#3cc7c0;
|
||||
}
|
||||
.group{
|
||||
display: flex;
|
||||
font-size: 28rpx;
|
||||
.agree {
|
||||
background: none;
|
||||
color:#3cc7c0;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.radio{
|
||||
display: inline-flex;
|
||||
|
||||
font-size: 28rpx;
|
||||
radio{
|
||||
transform:scale(0.8) translateY(-2px);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.logincontent {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
@ -392,7 +448,7 @@ const start = () => {
|
||||
width: 180rpx !important;
|
||||
}
|
||||
.smsbox ::v-deep .u-form-item:first-child .u-form-item__body__left {
|
||||
width: 180rpx !important;
|
||||
width: 114rpx !important;
|
||||
}
|
||||
::v-deep .u-form-item__body__left__content__label {
|
||||
font-size: 34rpx;
|
||||
@ -410,7 +466,7 @@ const start = () => {
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
.row {
|
||||
margin-top: 30rpx;
|
||||
margin-top: 40rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 30rpx;
|
||||
@ -468,4 +524,137 @@ const start = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
.zanboxpop {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
.zanwraper {
|
||||
width: 630rpx;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 50rpx;
|
||||
background: #f5f6f9;
|
||||
border-radius: 16rpx;
|
||||
.title {
|
||||
margin-top: 48rpx;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
font-size: 36rpx;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
.content {
|
||||
padding: 30rpx 0;
|
||||
background: #f5f6f9;
|
||||
text-align: center;
|
||||
:deep(.imgstyle){
|
||||
max-width: 100%!important;
|
||||
}
|
||||
:deep(._block){
|
||||
text-align: left!important;
|
||||
}
|
||||
}
|
||||
.count {
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
font-weight: 400;
|
||||
font-size: 28rpx;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
.num {
|
||||
color: #ff0000;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
.earn {
|
||||
font-size: 32rpx;
|
||||
color: #3cc7c0;
|
||||
}
|
||||
}
|
||||
.countbox {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin: 30rpx 0px 40rpx;
|
||||
padding: 0 40rpx;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
.minus {
|
||||
flex-shrink: 0;
|
||||
margin-left: 10rp;
|
||||
width: 90rpx;
|
||||
height: 90rpx;
|
||||
font-size: 60rpx;
|
||||
color: #333;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
border: 2rpx solid #e9e9e9;
|
||||
}
|
||||
.add {
|
||||
flex-shrink: 0;
|
||||
width: 90rpx;
|
||||
margin-left: 10rpx;
|
||||
height: 90rpx;
|
||||
font-size: 60rpx;
|
||||
color: #333;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
background: #3cc7c0;
|
||||
border-radius: 12rpx;
|
||||
border: 2rpx solid #3cc7c0;
|
||||
}
|
||||
:deep(.u-input__content__field-wrapper__field) {
|
||||
height: 60rpx;
|
||||
|
||||
font-size: 34rpx !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
:deep(.u-input) {
|
||||
background: #f2f2f2;
|
||||
width: 200rpx !important;
|
||||
flex: none;
|
||||
}
|
||||
:deep(.u-input--radius) {
|
||||
border-radius: 24rrpx;
|
||||
}
|
||||
}
|
||||
.btnbox {
|
||||
margin: 0px 40rpx 0px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.cancle {
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
width: 256rpx;
|
||||
height: 88rpx;
|
||||
background: #f5f6f9;
|
||||
border-radius: 25px;
|
||||
border: 2rpx solid rgba(0, 0, 0, 0.15);
|
||||
font-weight: 500;
|
||||
margin-right: 18rpx;
|
||||
font-size: 32rpx;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.ok {
|
||||
color: #fff;
|
||||
width: 256rpx;
|
||||
height: 88rpx;
|
||||
background: #3cc7c0;
|
||||
border-radius: 25px;
|
||||
border: 2rpx solid #3cc7c0;
|
||||
font-weight: 500;
|
||||
font-size: 32rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
107
pages/my/my.vue
@ -17,7 +17,7 @@
|
||||
|
||||
</view>
|
||||
</view>
|
||||
<view class="databox">
|
||||
<view class="databox" v-if="numInfo.article_num+numInfo.video_num>=5">
|
||||
<view class="cell">
|
||||
<view class="num">{{ numInfo.article_num }}</view>
|
||||
<view class="name">文章</view>
|
||||
@ -32,45 +32,75 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="listbox">
|
||||
<view class="titlename">我的临床病例库</view>
|
||||
<view class="cell" @click="goDoctor(userInfo.doctor_id,userInfo.user_name)" v-if="numInfo.article_num>0">
|
||||
<view class="left">
|
||||
<u-icon name="chat-fill" color="#000" size="28"></u-icon>
|
||||
<!-- <view class="titlename">我的临床病例库</view> -->
|
||||
<view class="cell" :class="{on:isHarmonyos}" @click="goDoctor(userInfo.doctor_id,userInfo.user_name)" v-if="numInfo.article_num+numInfo.video_num>=5"> <!-- 5 -->
|
||||
<view class="left" >
|
||||
<up--image
|
||||
:src="myFile"
|
||||
width="34rpx"
|
||||
height="34rpx"
|
||||
|
||||
></up--image>
|
||||
<view class="title">我的病例库</view>
|
||||
</view>
|
||||
<u-icon name="arrow-right" color="#9CA3AF" size="18"></u-icon>
|
||||
</view>
|
||||
<view class="cell" @click="goCert">
|
||||
<view class="left">
|
||||
<u-icon name="chat-fill" color="#000" size="28"></u-icon>
|
||||
<view class="cell" :class="{on:isHarmonyos}" @click="goCert" v-if="numInfo.article_num+numInfo.video_num>=5">
|
||||
<view class="left" >
|
||||
<up--image
|
||||
:src="myDownload"
|
||||
width="42rpx"
|
||||
height="42rpx"
|
||||
|
||||
></up--image>
|
||||
<view class="title">临床病例库收录证书下载</view>
|
||||
</view>
|
||||
<u-icon name="arrow-right" color="#9CA3AF" size="18"></u-icon>
|
||||
</view>
|
||||
<view class="cell" @click="goHospital(userInfo.hospital_id,userInfo.hospital_name)" v-if="hospitalInfo.video_read_num>0 || hospitalInfo.article_read_num>0">
|
||||
<view class="left">
|
||||
<u-icon name="chat-fill" color="#000" size="28"></u-icon>
|
||||
<view class="cell" :class="{on:isHarmonyos}" @click="goHospital(userInfo.hospital_id,userInfo.hospital_name)" v-if="hospitalInfo.video_num+hospitalInfo.article_num>=10">
|
||||
<view class="left" >
|
||||
<up--image
|
||||
:src="myHospital"
|
||||
width="39rpx"
|
||||
height="39rpx"
|
||||
|
||||
></up--image>
|
||||
<view class="title">{{userInfo.hospital_name}}临床病例库</view>
|
||||
</view>
|
||||
<u-icon name="arrow-right" color="#9CA3AF" size="18"></u-icon>
|
||||
</view>
|
||||
<view class="cell" @click="mySpecial">
|
||||
<view class="left">
|
||||
<u-icon name="chat-fill" color="#000" size="28"></u-icon>
|
||||
<view class="cell" :class="{on:isHarmonyos}" @click="mySpecial">
|
||||
<view class="left" >
|
||||
<up--image
|
||||
:src="myTalk"
|
||||
width="39rpx"
|
||||
height="39rpx"
|
||||
|
||||
></up--image>
|
||||
<view class="title">我的病例交流</view>
|
||||
</view>
|
||||
<u-icon name="arrow-right" color="#9CA3AF" size="18"></u-icon>
|
||||
</view>
|
||||
<view class="cell" @click="goJoin">
|
||||
<view class="left">
|
||||
<u-icon name="chat-fill" color="#000" size="28"></u-icon>
|
||||
<view class="cell" @click="goJoin" :class="{on:isHarmonyos}">
|
||||
<view class="left" >
|
||||
<up--image
|
||||
:src="myJoin"
|
||||
width="39rpx"
|
||||
height="39rpx"
|
||||
|
||||
></up--image>
|
||||
<view class="title">我的参与互动</view>
|
||||
</view>
|
||||
<u-icon name="arrow-right" color="#9CA3AF" size="18"></u-icon>
|
||||
</view>
|
||||
<view class="cell" @click="goCollect">
|
||||
<view class="left">
|
||||
<u-icon name="chat-fill" color="#000" size="28"></u-icon>
|
||||
<view class="cell" @click="goCollect" :class="{on:isHarmonyos}">
|
||||
<view class="left" >
|
||||
<up--image
|
||||
:src="myCollect"
|
||||
width="39rpx"
|
||||
height="39rpx"
|
||||
|
||||
></up--image>
|
||||
<view class="title">浏览与收藏</view>
|
||||
</view>
|
||||
<u-icon name="arrow-right" color="#9CA3AF" size="18"></u-icon>
|
||||
@ -80,7 +110,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="publish" @click="goPublish">
|
||||
<u-icon name="plus" color="#fff" size="18"></u-icon>
|
||||
<up-icon name="plus" color="#fff" size="18"></up-icon>
|
||||
</view>
|
||||
</div>
|
||||
</template>
|
||||
@ -88,10 +118,18 @@
|
||||
<script setup>
|
||||
import dNav from "@/components/backNav/backNav.vue";
|
||||
import headImg from "@/static/headImg.png";
|
||||
import myFile from "@/static/myFile.png";
|
||||
import myDownload from "@/static/myDownload.png";
|
||||
import myCollect from "@/static/myCollect.png";
|
||||
import myHospital from "@/static/myHospital.png";
|
||||
import myJoin from "@/static/myJoin.png";
|
||||
import myTalk from "@/static/myTalk.png";
|
||||
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import navTo from "@/utils/navTo.js";
|
||||
import api from "@/api/api";
|
||||
import { ref,reactive} from "vue";
|
||||
const isHarmonyos=ref(false)
|
||||
const userInfo=reactive({})
|
||||
const numInfo=reactive({})
|
||||
const hospitalInfo=reactive({})
|
||||
@ -123,6 +161,15 @@ const getHospitalNum=(id)=>{
|
||||
}
|
||||
onLoad(()=>{
|
||||
getInfo()
|
||||
uni.getSystemInfo({
|
||||
success(res){
|
||||
console.log(res.platform);
|
||||
console.log(res.platform=="ohos")
|
||||
if(res.platform=="ohos"){
|
||||
isHarmonyos.value=true;
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
const goCert = () => {
|
||||
navTo({
|
||||
@ -131,17 +178,17 @@ const goCert = () => {
|
||||
};
|
||||
const goDoctor=(id,name)=>{
|
||||
navTo({
|
||||
url:'/pages/searchList/searchList?type=doctor&id='+id+'&name='+name
|
||||
url:'/pages/search/search?doctor_id='+id+'&doctor_name='+name+"&from=myCase"
|
||||
})
|
||||
}
|
||||
const goHospital=(id,name)=>{
|
||||
navTo({
|
||||
url:'/pages/searchList/searchList?type=hospital&id='+id+'&name='+name
|
||||
url:'/pages/search/search?hospital_id='+id+'&hospital_name='+name
|
||||
})
|
||||
}
|
||||
const mySpecial=()=>{
|
||||
navTo({
|
||||
url:'/pages/specialList/specialList?userId='+userInfo.user_id
|
||||
url:'/pages/specialList/specialList?userId='+userInfo.user_id+'&type=mine'
|
||||
})
|
||||
}
|
||||
const goJoin=()=>{
|
||||
@ -164,9 +211,11 @@ const goPublish=()=>{
|
||||
.upage{
|
||||
height:100vh;
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
flex-direction: column;
|
||||
}
|
||||
.con{
|
||||
background-color: #fff;
|
||||
height:calc(100vh - 100rpx);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
@ -196,6 +245,10 @@ const goPublish=()=>{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
:deep(.u-image){
|
||||
margin-top: 9rpx;
|
||||
background: none!important;
|
||||
}
|
||||
.left{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -208,6 +261,12 @@ const goPublish=()=>{
|
||||
}
|
||||
|
||||
}
|
||||
.cell.on{
|
||||
:deep(.u-image){
|
||||
margin-top: 0rpx;
|
||||
background: none!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.databox {
|
||||
height: 162rpx;
|
||||
|
||||
@ -10,6 +10,11 @@
|
||||
>
|
||||
<template #top>
|
||||
<navBarSearch :searchWord="keyWord" :navName="navName" @changeWord="changeWord"></navBarSearch>
|
||||
<view class="detail" v-if="isSearch">
|
||||
<view class="desc" >检索到:<text class="red">{{total}}篇</text></view>
|
||||
|
||||
<view class="desc" v-if="keyWord">检索词:<text class="red">{{ keyWord }}</text></view>
|
||||
</view>
|
||||
<view class="filterbox">
|
||||
<!-- <view class="type" @click="swicthType">{{!isArticle?'文章':'视频'}}<up--image :src="switchImg" width="31rpx" height="31rpx"></up--image></view> -->
|
||||
<up-dropdown class="u-dropdown" ref="uDropdownRef">
|
||||
@ -29,8 +34,8 @@
|
||||
|
||||
</view>
|
||||
</template>
|
||||
<view class="item" v-for="(item, index) in dataList" @click="goDetail()" :key="item.collect_id">
|
||||
<view class="title ellipsis">{{item.data.title}}</view>
|
||||
<view class="item" v-for="(item, index) in dataList" @click="goDetail(item.data.id)" :key="item.collect_id">
|
||||
<view class="title ellipsis-two-lines">{{item.data.title}}</view>
|
||||
<view class="tagsbox">
|
||||
<view class="tag" v-for="tag in item.data.author" :key="tag.author_id">{{ tag.doctor_name }}</view>
|
||||
</view>
|
||||
@ -60,7 +65,7 @@ import { ref, reactive } from "vue";
|
||||
import navBarSearch from "@/components/navBarSearch/navBarSearch.vue";
|
||||
import list from "@/uni_modules/z-paging/components/z-paging/z-paging";
|
||||
import api from "@/api/api";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { onLoad,onShow} from "@dcloudio/uni-app";
|
||||
import dayjs from "dayjs";
|
||||
import switchImg from "@/static/switch.png";
|
||||
import navTo from "@/utils/navTo.js";
|
||||
@ -71,6 +76,7 @@ const total = ref(0);
|
||||
const value = ref("");
|
||||
const keyWord = ref("");
|
||||
const isArticle=ref(true);
|
||||
const isSearch=ref(false);
|
||||
const navName=ref('肝胆相照临床病例库');
|
||||
const type1=ref(0);
|
||||
const type2=ref(1);
|
||||
@ -110,6 +116,9 @@ onLoad((options) => {
|
||||
keyWord.value = options.keyWord;
|
||||
};
|
||||
});
|
||||
onShow(()=>{
|
||||
paging.value?.refresh();
|
||||
})
|
||||
const changetype1=(e)=>{
|
||||
type1.value=e;
|
||||
paging.value.reload();
|
||||
@ -123,14 +132,22 @@ const formatdate=(date)=>{
|
||||
return dayjs(date).format('YYYY-MM-DD')
|
||||
}
|
||||
const goDetail=(id)=>{
|
||||
console.log(11111)
|
||||
console.log(type1.value)
|
||||
console.log(id)
|
||||
let type=isArticle.value?'article':'video'
|
||||
let type="article";
|
||||
if(type2.value==1){
|
||||
type="article";
|
||||
}else if(type2.value==2){
|
||||
type="video";
|
||||
}else if(type2.value==3){
|
||||
type="exchange";
|
||||
}
|
||||
navTo({
|
||||
url: `/pages/detail/detail?id=${id}&type=${type}`
|
||||
})
|
||||
}
|
||||
const changeWord=(val)=>{
|
||||
isSearch.value=true;
|
||||
keyWord.value=val;
|
||||
paging.value.reload();
|
||||
}
|
||||
@ -203,6 +220,7 @@ const queryList = (pageNo, pageSize) => {
|
||||
}
|
||||
}
|
||||
.filterbox{
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
height:128rpx;
|
||||
align-items: center;
|
||||
@ -232,7 +250,7 @@ const queryList = (pageNo, pageSize) => {
|
||||
|
||||
background: #fff;
|
||||
z-index:1;
|
||||
margin-left:-30rpx;
|
||||
margin-left:0rpx;
|
||||
|
||||
}
|
||||
:deep(.u-dropdown__menu__item){
|
||||
@ -242,9 +260,13 @@ const queryList = (pageNo, pageSize) => {
|
||||
background: #F3F4F6;
|
||||
border-radius: 15rpx;
|
||||
flex:none;
|
||||
margin-left: 60rpx;
|
||||
margin-left: 30rpx;
|
||||
|
||||
}
|
||||
:deep(.u-dropdown__menu__item__text){
|
||||
font-size: 14px!important;
|
||||
color:#3CC7C0!important;
|
||||
}
|
||||
.deal {
|
||||
margin-top: 20rpx;
|
||||
display: flex;
|
||||
@ -273,6 +295,7 @@ const queryList = (pageNo, pageSize) => {
|
||||
}
|
||||
}
|
||||
.item {
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #f3f4f6;
|
||||
padding: 30rpx;
|
||||
.title {
|
||||
@ -284,6 +307,7 @@ const queryList = (pageNo, pageSize) => {
|
||||
.tagsbox {
|
||||
margin-top: 20rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.tag {
|
||||
padding: 0 10rpx;
|
||||
margin-right: 16rpx;
|
||||
|
||||
@ -90,7 +90,7 @@ import { ref, reactive } from "vue";
|
||||
import backLogoNav from "@/components/backLogoNav/backLogoNav.vue";
|
||||
import list from "@/uni_modules/z-paging/components/z-paging/z-paging";
|
||||
import api from "@/api/api";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { onLoad,onShow } from "@dcloudio/uni-app";
|
||||
import dayjs from "dayjs";
|
||||
import switchImg from "@/static/switch.png";
|
||||
import certImg from "@/static/cert.png";
|
||||
@ -193,7 +193,9 @@ const switchTab = (item) => {
|
||||
onLoad((options) => {
|
||||
|
||||
});
|
||||
|
||||
onShow(()=>{
|
||||
paging.value?.refresh();
|
||||
})
|
||||
const formatdate = (date) => {
|
||||
return dayjs(date).format("YYYY-MM-DD");
|
||||
};
|
||||
@ -530,6 +532,7 @@ const queryList = (pageNo, pageSize) => {
|
||||
.tagsbox {
|
||||
margin-top: 20rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.tag {
|
||||
padding: 0 10rpx;
|
||||
margin-right: 16rpx;
|
||||
|
||||
2083
pages/publish/publish222.vue
Normal file
@ -63,7 +63,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
const src = ref("https://wx.igandan.com/hcp/toRegister?fromType=wx");
|
||||
const src = ref("https://dev-wx.igandan.com/hcp/toRegister?fromType=wx");
|
||||
const model = reactive({
|
||||
name: "",
|
||||
});
|
||||
@ -137,14 +137,20 @@ const end = () => {
|
||||
const start = () => {
|
||||
customCode.opacity = 0.5;
|
||||
};
|
||||
onLoad(() => {
|
||||
onLoad((options) => {
|
||||
const { envVersion } = uni.getAccountInfoSync().miniProgram;
|
||||
console.log(envVersion)
|
||||
if (envVersion == "release") {
|
||||
src.value = "https://wx.igandan.com/hcp/toRegister";
|
||||
} else {
|
||||
src.value = "https://dev-wx.igandan.com/hcp/toRegister?fromType=weChat";
|
||||
|
||||
if(options.src){
|
||||
console.log(decodeURIComponent(options.src))
|
||||
src.value=decodeURIComponent(options.src);
|
||||
}else{
|
||||
if (envVersion == "release") {
|
||||
src.value = "https://wx.igandan.com/hcp/toRegister?fromType=weChat";
|
||||
} else {
|
||||
src.value = "https://dev-wx.igandan.com/hcp/toRegister?fromType=weChat";
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@ -9,14 +9,14 @@
|
||||
@query="queryList"
|
||||
>
|
||||
<template #top>
|
||||
<navBarSearch :searchWord="keyWord" :navName="navName" @changeWord="changeWord"></navBarSearch>
|
||||
<navBarSearch :searchWord="keyWord" :navName="navName" @changeWord="changeWord" :type="navType"></navBarSearch>
|
||||
<view class="databox" v-if="hospital_id || doctor_id || (label_iden && !canOpenCase)">
|
||||
<view class="cell">
|
||||
<view class="num">{{ numInfo.article_collect_num }}</view>
|
||||
<view class="num">{{ numInfo.article_num }}</view>
|
||||
<view class="name">文章</view>
|
||||
</view>
|
||||
<view class="cell">
|
||||
<view class="num">{{ numInfo.video_collect_num }}</view>
|
||||
<view class="num">{{ numInfo.video_num }}</view>
|
||||
<view class="name">视频</view>
|
||||
</view>
|
||||
<view class="cell">
|
||||
@ -27,31 +27,53 @@
|
||||
<view class="bar"></view>
|
||||
<view class="detail" v-if="isSearch">
|
||||
<view class="desc" >检索到:<text class="red">{{searchInfo.search_article_num}}篇文章</text></view>
|
||||
<view class="desc" >检索到:<text class="red">{{searchInfo.search_video_num}}个视频</text></view>
|
||||
<view class="desc" v-if="!hideType">检索到:<text class="red">{{searchInfo.search_video_num}}个视频</text></view>
|
||||
<view class="desc" v-if="keyWord">检索词:<text class="red">{{ keyWord }}</text></view>
|
||||
</view>
|
||||
<view class="filterbox" :class="{active:canOpenCase}">
|
||||
<view class="type" @click="swicthType">{{!isArticle?'文章':'视频'}}<up--image :src="switchImg" width="31rpx" height="31rpx"></up--image></view>
|
||||
<view class="casesdown" @click="openCase" v-if="canOpenCase">筛选<up--image :src="caseImg" width="31rpx" height="31rpx"></up--image></view>
|
||||
<up-dropdown class="u-dropdown" ref="uDropdownRef">
|
||||
<view class="filterbox" :class="{active:canOpenCase,'on':hideType,'isCase':isCase}">
|
||||
<view class="type" v-if="!hideType && !isCase" @click="swicthType">{{!isArticle?'视频':'文章'}}<up--image :src="switchImg" width="31rpx" height="31rpx"></up--image></view>
|
||||
<view class="casesdown" :class="{active:label_iden}" @click="openCase" v-if="canOpenCase">筛选<up--image :src="caseImg" width="31rpx" height="31rpx"></up--image></view>
|
||||
<up-dropdown class="u-dropdown" ref="uDropdownRef">
|
||||
<up-dropdown-item
|
||||
v-model="order.push_date"
|
||||
title="发布时间"
|
||||
@change="change"
|
||||
:options="options"
|
||||
></up-dropdown-item>
|
||||
<up-dropdown-item
|
||||
|
||||
:title="dropTitle"
|
||||
|
||||
|
||||
>
|
||||
<view class="dropcontent">
|
||||
<up-radio-group
|
||||
@change="changeDate"
|
||||
v-model="orderFilter"
|
||||
iconPlacement="right"
|
||||
placement="column"
|
||||
>
|
||||
<view
|
||||
class="column"
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:class="[orderFilter==item.value?'active':'']"
|
||||
>
|
||||
<up-radio
|
||||
activeColor="#3CC7C0"
|
||||
:label="item.label"
|
||||
:name="item.value"
|
||||
></up-radio>
|
||||
</view>
|
||||
</up-radio-group>
|
||||
</view>
|
||||
</up-dropdown-item>
|
||||
<!-- <up-dropdown-item
|
||||
v-model="order.read_num"
|
||||
title="阅读量"
|
||||
@change="change"
|
||||
@change="changeRead"
|
||||
:options="options"
|
||||
></up-dropdown-item>
|
||||
></up-dropdown-item> -->
|
||||
</up-dropdown>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
<view class="item" v-for="(item, index) in dataList" :key="index" @click="isArticle?goDetail(item.article_id):goDetail(item.video_id)">
|
||||
<view class="title ellipsis">{{isArticle?item.article_title:item.video_title }}</view>
|
||||
<view class="item" v-for="(item, index) in dataList" :key="index" @click="isArticle?goDetail(item.article_id,item.is_link,item.is_link_url):goDetail(item.video_id,item.is_link,item.is_link_url)">
|
||||
<view class="title ellipsis-two-lines">{{isArticle?item.article_title:item.video_title }}</view>
|
||||
<view class="tagsbox">
|
||||
<view class="tag" v-for="tag in item.author" :key="tag.author_id">{{ tag.doctor_name }}</view>
|
||||
</view>
|
||||
@ -61,9 +83,9 @@
|
||||
<up-icon name="eye" color="#6B7280" size="28rpx"></up-icon>
|
||||
<view class="num">{{item.read_num }}</view>
|
||||
</view>
|
||||
<view class="collect">
|
||||
<view class="collect" v-if="item.collect_num>0">
|
||||
<up-icon name="heart" color="#6B7280" size="28rpx"></up-icon>
|
||||
<view class="num">{{item.collect_num }}</view>
|
||||
<view class="num" >{{item.collect_num }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="time">
|
||||
@ -77,30 +99,60 @@
|
||||
<up-popup
|
||||
:round="10"
|
||||
zIndex="9"
|
||||
:closeOnClickOverlay="false"
|
||||
:show="showCase"
|
||||
mode="bottom"
|
||||
@close="closeCase"
|
||||
>
|
||||
<view class="votepop casepop">
|
||||
<view class="titlebox">
|
||||
<view class="left" @click="showCase = false">取消</view>
|
||||
<view class="left continue" @click="continueCase" v-show="level != 3"
|
||||
>继续选择</view
|
||||
>
|
||||
<view class="left" @click="closeCase">取消</view>
|
||||
|
||||
<view class="right" @click="confirmCase">确定</view>
|
||||
</view>
|
||||
<view class="casecon">
|
||||
<view class="stepbox">
|
||||
<up-steps :current="level-1" direction="column" :key="freshKey">
|
||||
<up-steps-item >
|
||||
<template v-slot:content>
|
||||
<view class="slot-content" @click="openCaseLevel('1')">
|
||||
|
||||
<view class="left">{{!caseValue1.name?'请选择选项':caseValue1.name}}</view>
|
||||
<u-icon name="arrow-right"></u-icon>
|
||||
</view>
|
||||
</template>
|
||||
</up-steps-item>
|
||||
<up-steps-item v-if="caseValue1.name && labelObj.list2.length>0">
|
||||
<template v-slot:content>
|
||||
<view class="slot-content" @click="openCaseLevel('2')">
|
||||
<view class="left">{{!caseValue2.name?'请选择选项':caseValue2.name}}</view>
|
||||
<u-icon name="arrow-right"></u-icon>
|
||||
</view>
|
||||
</template>
|
||||
</up-steps-item>
|
||||
<up-steps-item v-if="caseValue2.name && labelObj.list3.length>0">
|
||||
<template v-slot:content>
|
||||
<view class="slot-content" @click="openCaseLevel('3')">
|
||||
<view class="left">{{!caseValue3.name?'请选择选项':caseValue3.name}}</view>
|
||||
<u-icon name="arrow-right"></u-icon>
|
||||
</view>
|
||||
</template>
|
||||
</up-steps-item>
|
||||
</up-steps>
|
||||
</view>
|
||||
<scroll-view class="casecon" scroll-y="true">
|
||||
<view v-show="level == 1" >
|
||||
<up-radio-group
|
||||
v-model="caseValue1"
|
||||
|
||||
v-model="caseValue1.value"
|
||||
name="group1"
|
||||
iconPlacement="right"
|
||||
placement="column"
|
||||
@change="groupChange1"
|
||||
>
|
||||
<view
|
||||
class="column"
|
||||
v-for="item in labelObj.list1"
|
||||
:key="item.app_iden"
|
||||
v-show="item.label_name!='热门话题'"
|
||||
>
|
||||
<up-radio
|
||||
activeColor="#3CC7C0 "
|
||||
@ -112,7 +164,9 @@
|
||||
</view>
|
||||
<view v-show="level == 2" >
|
||||
<up-radio-group
|
||||
v-model="caseValue2"
|
||||
name="group2"
|
||||
@change="groupChange2"
|
||||
v-model="caseValue2.value"
|
||||
iconPlacement="right"
|
||||
placement="column"
|
||||
>
|
||||
@ -131,14 +185,15 @@
|
||||
</view>
|
||||
<view v-show="level == 3" >
|
||||
<up-radio-group
|
||||
v-model="caseValue3"
|
||||
|
||||
name="group3"
|
||||
@change="groupChange3"
|
||||
v-model="caseValue3.value"
|
||||
iconPlacement="right"
|
||||
placement="column"
|
||||
>
|
||||
<view
|
||||
class="column"
|
||||
v-for="item in labelObj.list2"
|
||||
v-for="item in labelObj.list3"
|
||||
:key="item.app_iden"
|
||||
>
|
||||
<up-radio
|
||||
@ -149,7 +204,7 @@
|
||||
</view>
|
||||
</up-radio-group>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</up-popup>
|
||||
</template>
|
||||
@ -159,13 +214,12 @@ import { ref, reactive } from "vue";
|
||||
import navBarSearch from "@/components/navBarSearch/navBarSearch.vue";
|
||||
import list from "@/uni_modules/z-paging/components/z-paging/z-paging";
|
||||
import api from "@/api/api";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { onLoad,onShow} from "@dcloudio/uni-app";
|
||||
import dayjs from "dayjs";
|
||||
import switchImg from "@/static/switch.png";
|
||||
import caseImg from "@/static/caseIcon.png";
|
||||
import navTo from "@/utils/navTo.js";
|
||||
const paging = ref(null);
|
||||
const uDropdownRef=ref(null);
|
||||
|
||||
const dataList = ref([]);
|
||||
const total = ref(0);
|
||||
const keyWord = ref("");
|
||||
@ -176,98 +230,167 @@ const hospital_name=ref('');
|
||||
const doctor_id=ref('');
|
||||
const doctor_name=ref('');
|
||||
const numInfo=reactive({});
|
||||
const searchInfo=reactive({})
|
||||
const searchInfo=reactive({});
|
||||
const freshKey=ref(0);
|
||||
const navType=ref('')
|
||||
const navName=ref('肝胆相照临床病例库')
|
||||
const paging = ref(null);
|
||||
const hideType=ref(false)
|
||||
const dropTitle=ref('发布时间');
|
||||
const orderFilter=ref('1')
|
||||
const uDropdownRef=ref(null);
|
||||
const isCase=ref(false);
|
||||
const options= ref([
|
||||
{
|
||||
label: "正序",
|
||||
value: 'asc',
|
||||
label: "发布时间",
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: "倒序",
|
||||
value: 'desc',
|
||||
label: "阅读次数",
|
||||
value: '2',
|
||||
},
|
||||
]);
|
||||
const isSearch=ref(false);
|
||||
const order=reactive({
|
||||
read_num:'',
|
||||
push_date:''
|
||||
push_date:'desc'
|
||||
})
|
||||
const showCase = ref(false);
|
||||
const canOpenCase = ref(false);
|
||||
const caseValue1 = ref("");
|
||||
const caseValue2 = ref("");
|
||||
const caseValue3 = ref("");
|
||||
const caseValue1 = reactive({
|
||||
value:'',
|
||||
name:'',
|
||||
});
|
||||
const caseValue2 = reactive({
|
||||
value:'',
|
||||
name:'',
|
||||
});
|
||||
const caseValue3 = reactive({
|
||||
value:'',
|
||||
name:'',
|
||||
});
|
||||
const level = ref(1);
|
||||
const labelObj = reactive({
|
||||
list1: [],
|
||||
list2: [],
|
||||
list3: [],
|
||||
list3: []
|
||||
});
|
||||
const openCaseLevel=(lev)=>{
|
||||
freshKey.value++;
|
||||
level.value=lev;
|
||||
if(lev==1){
|
||||
caseValue2.name='';
|
||||
caseValue2.value='';
|
||||
labelObj.list2=[]
|
||||
}else if(lev==2){
|
||||
labelObj.list3=[];
|
||||
caseValue3.name='';
|
||||
caseValue3.value=''
|
||||
}
|
||||
};
|
||||
const groupChange1=(e)=>{
|
||||
caseValue1.value=e;
|
||||
for (var i = 0; i <labelObj.list1.length; i++) {
|
||||
if(labelObj.list1[i].app_iden==caseValue1.value){
|
||||
caseValue1.name=labelObj.list1[i].label_name;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
getCaseLabel(2,e)
|
||||
}
|
||||
const groupChange2=(e)=>{
|
||||
caseValue2.value=e;
|
||||
for (var i = 0; i <labelObj.list2.length; i++) {
|
||||
if(labelObj.list2[i].app_iden==caseValue2.value){
|
||||
caseValue2.name=labelObj.list2[i].label_name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
getCaseLabel(3,e)
|
||||
}
|
||||
const groupChange3=(e)=>{
|
||||
caseValue3.value=e;
|
||||
level.value =3
|
||||
for (var i = 0; i <labelObj.list3.length; i++) {
|
||||
if(labelObj.list3[i].app_iden==caseValue3.value){
|
||||
caseValue3.name=labelObj.list3[i].label_name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
const getCaseLabel = (lev,pid=0) => {
|
||||
api.getCaseLabel({
|
||||
pid:pid
|
||||
pId:pid
|
||||
}).then((res) => {
|
||||
level.value = lev;
|
||||
if (lev == 1) {
|
||||
labelObj.list1 = res.data.data;
|
||||
labelObj.list1 = res.data.data;
|
||||
//label_iden.value = caseValue1.value;
|
||||
|
||||
} else if (lev == 2) {
|
||||
labelObj.list2 = res.data.data;
|
||||
if(res.data.data.length==0){
|
||||
level.value = 1
|
||||
}
|
||||
//label_iden.value = caseValue2.value;
|
||||
|
||||
} else if (lev == 3) {
|
||||
labelObj.list3 = res.data.data;
|
||||
}
|
||||
level.value = lev;
|
||||
if(res.data.data.length==0){
|
||||
level.value = 2
|
||||
}
|
||||
//label_iden.value = caseValue3.value;
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
const openCase=()=>{
|
||||
getCaseLabel(1,0)
|
||||
|
||||
showCase.value = true;
|
||||
|
||||
}
|
||||
const cancelCase=()=>{
|
||||
showCase.value=false;
|
||||
level.value=1;
|
||||
caseValue1.name='';
|
||||
caseValue1.value='';
|
||||
caseValue2.name='';
|
||||
caseValue2.value='';
|
||||
caseValue3.name='';
|
||||
caseValue3.value='';
|
||||
//labelObj.list1=[];
|
||||
labelObj.list2=[];
|
||||
labelObj.list3=[];
|
||||
}
|
||||
const confirmCase = () => {
|
||||
if (level.value == 1 && caseValue1.value == "") {
|
||||
uni.showToast({ title: "请选择疾病", icon: "none" });
|
||||
uni.showToast({ title: "请选择疾病选项", icon: "none" });
|
||||
return false;
|
||||
}
|
||||
if (level.value == 2 && caseValue2.value == "") {
|
||||
uni.showToast({ title: "请选择疾病", icon: "none" });
|
||||
return false;
|
||||
}
|
||||
if (level.value == 3 && caseValue3.value == "") {
|
||||
uni.showToast({ title: "请选择疾病", icon: "none" });
|
||||
return false;
|
||||
}
|
||||
showCase.value = false;
|
||||
};
|
||||
if(level.value == 1){
|
||||
label_iden.value = caseValue1.value
|
||||
}
|
||||
label_iden.value = caseValue1.value;
|
||||
}
|
||||
if(level.value == 2 ){
|
||||
label_iden.value = caseValue2.value
|
||||
if(!caseValue2.value){
|
||||
label_iden.value = caseValue1.value;
|
||||
}else{
|
||||
label_iden.value = caseValue2.value;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
if(level.value == 3 ){
|
||||
label_iden.value = caseValue3.value
|
||||
}
|
||||
|
||||
};
|
||||
const continueCase = () => {
|
||||
if (level.value == 1 && caseValue1.value == "") {
|
||||
uni.showToast({ title: "请选择疾病", icon: "none" });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (level.value == 2 && caseValue2.value == "") {
|
||||
uni.showToast({ title: "请选择疾病", icon: "none" });
|
||||
return false;
|
||||
}
|
||||
if (level.value == 3 && caseValue3.value == "") {
|
||||
uni.showToast({ title: "请选择疾病", icon: "none" });
|
||||
return false;
|
||||
}
|
||||
if(level.value == 1 ){
|
||||
getCaseLabel(2,caseValue1.value);
|
||||
}else if(level.value == 2 ){
|
||||
getCaseLabel(3,caseValue2.value);
|
||||
if(!caseValue3.value){
|
||||
label_iden.value = caseValue2.value;
|
||||
}else{
|
||||
label_iden.value = caseValue3.value;
|
||||
}
|
||||
}
|
||||
paging.value.reload();
|
||||
showCase.value=false;
|
||||
//cancelCase();
|
||||
};
|
||||
const closeCase = () => {
|
||||
showCase.value = false;
|
||||
@ -279,14 +402,23 @@ const changeWord=(value)=>{
|
||||
|
||||
}
|
||||
onLoad((options) => {
|
||||
if(options.from=='home'){
|
||||
isSearch.value=true;
|
||||
}else if(options.from=="myCase"){
|
||||
navType.value="myCase"
|
||||
}
|
||||
if(options.keyWord){
|
||||
keyWord.value = options.keyWord;
|
||||
};
|
||||
if(options.order=='new'){
|
||||
order.push_date='asc'
|
||||
order.push_date='desc';
|
||||
hideType.value=true;
|
||||
navName.value='文章临床病例库'
|
||||
};
|
||||
if(options.order=='read'){
|
||||
order.read_num='desc'
|
||||
order.push_date='desc';
|
||||
hideType.value=true;
|
||||
navName.value='文章临床病例库'
|
||||
};
|
||||
if(options.order=='video'){
|
||||
isArticle.value=false;
|
||||
@ -307,38 +439,81 @@ onLoad((options) => {
|
||||
if(options.case_id){
|
||||
label_iden.value=options.case_id;
|
||||
navName.value= options.case_name+'临床病例库'
|
||||
getStaticSick(label_iden.value)
|
||||
getStaticSick(label_iden.value);
|
||||
//isCase.value=true;
|
||||
//console.log(111113)
|
||||
}else{
|
||||
canOpenCase.value = true;
|
||||
}
|
||||
getCaseLabel(1,0)
|
||||
});
|
||||
const change=(e)=>{
|
||||
onShow(()=>{
|
||||
paging.value?.refresh();
|
||||
})
|
||||
const changeDate=(e)=>{
|
||||
if(e==1){
|
||||
order.push_date='desc';
|
||||
order.read_num='';
|
||||
dropTitle.value="发布时间"
|
||||
}else{
|
||||
order.push_date='';
|
||||
order.read_num="desc";
|
||||
dropTitle.value="阅读次数"
|
||||
}
|
||||
orderFilter.value=e;
|
||||
uDropdownRef.value.close();
|
||||
paging.value.reload();
|
||||
}
|
||||
const changeRead=(e)=>{
|
||||
console.log(e);
|
||||
order.push_date=''
|
||||
paging.value.reload();
|
||||
}
|
||||
const formatdate=(date)=>{
|
||||
return dayjs(date).format('YYYY-MM-DD')
|
||||
}
|
||||
const goDetail=(id)=>{
|
||||
console.log(11111)
|
||||
console.log(id)
|
||||
let type=isArticle.value?'article':'video'
|
||||
navTo({
|
||||
url: `/pages/detail/detail?id=${id}&type=${type}`
|
||||
})
|
||||
const goDetail=(id,isLink,src)=>{
|
||||
console.log(isLink)
|
||||
if(isLink==1){
|
||||
api.readRecord({
|
||||
type:isArticle.value?1:2,
|
||||
id:id
|
||||
}).then(res=>{
|
||||
|
||||
})
|
||||
// #ifdef MP-WEIXIN
|
||||
navTo({
|
||||
url: `/pages/web/web?src=${src}`,
|
||||
});
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
window.location.href=`${src}`
|
||||
// #endif
|
||||
}else{
|
||||
let type=isArticle.value?'article':'video'
|
||||
navTo({
|
||||
url: `/pages/detail/detail?id=${id}&type=${type}`
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
const swicthType=()=>{
|
||||
isArticle.value=!isArticle.value;
|
||||
dataList.value=[];
|
||||
order.read_num='';
|
||||
order.push_date='';
|
||||
// order.read_num='';
|
||||
// order.push_date='';
|
||||
paging.value.reload();
|
||||
}
|
||||
const searchArticle =(params) => {
|
||||
let searchForm={
|
||||
keyword: keyWord.value,
|
||||
hospital_id:hospital_id.value,
|
||||
doctor_id:doctor_id.value
|
||||
doctor_id:doctor_id.value,
|
||||
label_iden:label_iden.value
|
||||
}
|
||||
if(label_iden.value===''){
|
||||
delete searchForm.label_iden
|
||||
}
|
||||
if(isSearch.value){
|
||||
searchForm.is_need_num=1;
|
||||
}
|
||||
@ -368,8 +543,12 @@ const searchVideo = async(params) => {
|
||||
let searchForm={
|
||||
keyword: keyWord.value,
|
||||
hospital_id:hospital_id.value,
|
||||
doctor_id:doctor_id.value
|
||||
doctor_id:doctor_id.value,
|
||||
label_iden:label_iden.value
|
||||
}
|
||||
if(label_iden.value===''){
|
||||
delete searchForm.label_iden
|
||||
}
|
||||
if(isSearch.value){
|
||||
searchForm.is_need_num=1;
|
||||
}
|
||||
@ -458,6 +637,42 @@ const queryList = (pageNo, pageSize) => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropcontent{
|
||||
padding-top: 40rpx;
|
||||
padding-bottom: 20rpx;
|
||||
background-color: #fff;
|
||||
:deep(.u-radio){
|
||||
margin-bottom: 6px!important;
|
||||
margin-top: 16px!important;
|
||||
}
|
||||
.column {
|
||||
padding: 0 30rpx;
|
||||
border-top: 2rpx solid #e5e7eb;
|
||||
:deep(.u-radio__text){
|
||||
color: #333 !important;
|
||||
}
|
||||
}
|
||||
.column.active{
|
||||
:deep(.u-radio__text){
|
||||
color: #3CC7C0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.stepbox{
|
||||
padding:15rpx 30rpx;
|
||||
border-bottom: 2rpx dotted #f3f4f6;
|
||||
:deep(.u-steps-item__content){
|
||||
margin-top: -5rpx!important;
|
||||
}
|
||||
.slot-content{
|
||||
width:100%;
|
||||
margin-bottom: 25rpx;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.bar {
|
||||
width: 100%;
|
||||
background: #f9fafb;
|
||||
@ -556,16 +771,23 @@ const queryList = (pageNo, pageSize) => {
|
||||
color: #2878ff !important;
|
||||
}
|
||||
}
|
||||
.casecon {
|
||||
.casecon{
|
||||
flex:1;
|
||||
max-height: calc(100vh - 800rpx);
|
||||
overflow-y: scroll;
|
||||
padding-top: 10rpx;
|
||||
padding-top: 22rpx;
|
||||
padding-bottom: 20rpx;
|
||||
min-height: 350rpx;
|
||||
:deep(.u-radio){
|
||||
margin-bottom: 10px!important;
|
||||
margin-top: 0px!important;
|
||||
}
|
||||
.column {
|
||||
padding: 0 30rpx;
|
||||
border-bottom: 2rpx solid #e5e7eb;
|
||||
}
|
||||
}
|
||||
|
||||
.databox {
|
||||
height: 162rpx;
|
||||
background: #ffffff;
|
||||
@ -591,6 +813,7 @@ const queryList = (pageNo, pageSize) => {
|
||||
}
|
||||
}
|
||||
.filterbox{
|
||||
background:#fff;
|
||||
:deep(.u-flex) {
|
||||
display: flex;
|
||||
flex-direction:row;
|
||||
@ -600,11 +823,12 @@ const queryList = (pageNo, pageSize) => {
|
||||
:deep(.u-dropdown__menu){
|
||||
background: #fff;
|
||||
z-index:1;
|
||||
margin-left: 160rpx;
|
||||
margin-left: 150rpx;
|
||||
|
||||
}
|
||||
:deep(.u-dropdown__menu__item__text){
|
||||
font-size: 14px!important;
|
||||
color:#3CC7C0!important;
|
||||
}
|
||||
:deep(.u-dropdown__menu__item){
|
||||
|
||||
@ -710,6 +934,25 @@ const queryList = (pageNo, pageSize) => {
|
||||
z-index:2;
|
||||
}
|
||||
}
|
||||
.filterbox.on{
|
||||
.casesdown{
|
||||
left:30rpx;
|
||||
}
|
||||
:deep(.u-dropdown__menu){
|
||||
background: #fff;
|
||||
z-index:1;
|
||||
margin-left: 170rpx;
|
||||
|
||||
}
|
||||
}
|
||||
.filterbox.isCase{
|
||||
:deep(.u-dropdown__menu){
|
||||
background: #fff;
|
||||
z-index:1;
|
||||
margin-left: -30rpx;
|
||||
|
||||
}
|
||||
}
|
||||
.u-page {
|
||||
|
||||
.deal {
|
||||
@ -741,6 +984,7 @@ const queryList = (pageNo, pageSize) => {
|
||||
}
|
||||
.item {
|
||||
border-bottom: 1rpx solid #f3f4f6;
|
||||
background-color: #fff;
|
||||
padding: 30rpx;
|
||||
.title {
|
||||
font-size: 30rpx;
|
||||
@ -751,6 +995,7 @@ const queryList = (pageNo, pageSize) => {
|
||||
.tagsbox {
|
||||
margin-top: 20rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.tag {
|
||||
padding: 0 10rpx;
|
||||
margin-right: 16rpx;
|
||||
|
||||
@ -31,20 +31,43 @@
|
||||
</view> -->
|
||||
<view class="filterbox">
|
||||
<!-- <view class="type" @click="swicthType">{{!isArticle?'文章':'视频'}}<up--image :src="switchImg" width="31rpx" height="31rpx"></up--image></view> -->
|
||||
<up-dropdown class="u-dropdown" ref="uDropdownRef">
|
||||
<up-dropdown-item
|
||||
v-model="order.push_date"
|
||||
title="发布时间"
|
||||
@change="change"
|
||||
:options="options"
|
||||
></up-dropdown-item>
|
||||
<!-- <up-dropdown-item
|
||||
v-model="order.read_num"
|
||||
title="阅读量"
|
||||
@change="change"
|
||||
:options="options"
|
||||
></up-dropdown-item> -->
|
||||
</up-dropdown>
|
||||
<up-dropdown class="u-dropdown" ref="uDropdownRef">
|
||||
<up-dropdown-item
|
||||
|
||||
:title="dropTitle"
|
||||
|
||||
|
||||
>
|
||||
<view class="dropcontent">
|
||||
<up-radio-group
|
||||
@change="change"
|
||||
v-model="orderFilter"
|
||||
iconPlacement="right"
|
||||
placement="column"
|
||||
>
|
||||
<view
|
||||
class="column"
|
||||
v-for="item in option"
|
||||
:key="item.value"
|
||||
:class="[orderFilter==item.value?'active':'']"
|
||||
>
|
||||
<up-radio
|
||||
activeColor="#3CC7C0"
|
||||
:label="item.label"
|
||||
:name="item.value"
|
||||
></up-radio>
|
||||
</view>
|
||||
</up-radio-group>
|
||||
</view>
|
||||
</up-dropdown-item>
|
||||
<!-- <up-dropdown-item
|
||||
v-model="order.read_num"
|
||||
title="阅读量"
|
||||
@change="changeRead"
|
||||
:options="options"
|
||||
></up-dropdown-item> -->
|
||||
</up-dropdown>
|
||||
|
||||
|
||||
</view>
|
||||
</template>
|
||||
@ -77,7 +100,7 @@ import { ref, reactive} from "vue";
|
||||
import navBarSearch from "@/components/navBarSearch/navBarSearch.vue";
|
||||
import list from "@/uni_modules/z-paging/components/z-paging/z-paging";
|
||||
import api from "@/api/api";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { onLoad,onShow } from "@dcloudio/uni-app";
|
||||
import dayjs from "dayjs";
|
||||
import switchImg from "@/static/switch.png";
|
||||
import navTo from "@/utils/navTo.js";
|
||||
@ -91,26 +114,32 @@ const keyWord = ref("");
|
||||
const isArticle=ref(true)
|
||||
const type=ref('doctor')
|
||||
const navName=ref('医生临床病例库');
|
||||
const options= ref([
|
||||
const orderFilter=ref('1')
|
||||
const dropTitle=ref('更新时间')
|
||||
const option= ref([
|
||||
{
|
||||
label: "正序",
|
||||
value: 'asc',
|
||||
label: "更新时间",
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: "倒序",
|
||||
value: 'desc',
|
||||
label: "医生名称",
|
||||
value: '2',
|
||||
},
|
||||
{
|
||||
label: "病例数",
|
||||
value: '3',
|
||||
},
|
||||
]);
|
||||
|
||||
const order=reactive({
|
||||
read_num:'',
|
||||
push_date:''
|
||||
push_date:'desc'
|
||||
})
|
||||
|
||||
onLoad((options) => {
|
||||
if(options.type=='hospital'){
|
||||
type.value ='hospital';
|
||||
(option.value)[1].label="医院名称"
|
||||
}
|
||||
console.log( type.value)
|
||||
keyWord.value=options.name;
|
||||
if(options.name){
|
||||
navName.value=options.name+'临床病例库'
|
||||
@ -125,7 +154,43 @@ onLoad((options) => {
|
||||
order.read_num='desc'
|
||||
};
|
||||
});
|
||||
onShow(()=>{
|
||||
paging.value?.refresh();
|
||||
})
|
||||
const change=(e)=>{
|
||||
if(e==1){
|
||||
order.push_date='desc';
|
||||
if(type.value =='hospital'){
|
||||
order.hospital_name='';
|
||||
}else{
|
||||
order.doctor_name='';
|
||||
|
||||
}
|
||||
order.article_num='';
|
||||
dropTitle.value="更新时间"
|
||||
}else if(e==2){
|
||||
order.push_date='';
|
||||
order.article_num='';
|
||||
if(type.value =='hospital'){
|
||||
order.hospital_name='asc';
|
||||
dropTitle.value="医院名称"
|
||||
}else{
|
||||
order.doctor_name='asc';
|
||||
dropTitle.value="医生名称"
|
||||
}
|
||||
}else if(e==3){
|
||||
order.push_date='';
|
||||
if(type.value =='hospital'){
|
||||
order.hospital_name='';
|
||||
}else{
|
||||
order.doctor_name='';
|
||||
|
||||
}
|
||||
order.article_num='desc';
|
||||
dropTitle.value="病例数"
|
||||
}
|
||||
orderFilter.value=e;
|
||||
uDropdownRef.value.close();
|
||||
paging.value.reload();
|
||||
}
|
||||
const formatdate=(date)=>{
|
||||
@ -190,15 +255,19 @@ const searchHospital = async(params) => {
|
||||
let searchForm={
|
||||
hospital_name: keyWord.value,
|
||||
}
|
||||
if(!order.read_num){
|
||||
delete order.read_num
|
||||
}
|
||||
|
||||
if(!order.push_date){
|
||||
delete order.push_date
|
||||
delete order.push_date
|
||||
}
|
||||
if(!order.article_num){
|
||||
delete order.article_num
|
||||
}
|
||||
if(order.read_num || order.push_date){
|
||||
searchForm.order=order
|
||||
}
|
||||
if(!order.hospital_name){
|
||||
delete order.hospital_name
|
||||
}
|
||||
if(order.push_date || order.article_num || order.hospital_name){
|
||||
searchForm.order=order
|
||||
}
|
||||
api.searchHospital({
|
||||
...searchForm,
|
||||
...params
|
||||
@ -213,13 +282,16 @@ const searchDoctor = async(params) => {
|
||||
let searchForm={
|
||||
doctor_name: keyWord.value,
|
||||
}
|
||||
if(!order.read_num){
|
||||
delete order.read_num
|
||||
}
|
||||
if(!order.push_date){
|
||||
delete order.push_date
|
||||
delete order.push_date
|
||||
}
|
||||
if(!order.article_num){
|
||||
delete order.article_num
|
||||
}
|
||||
if(order.read_num || order.push_date){
|
||||
if(!order.doctor_name){
|
||||
delete order.doctor_name
|
||||
}
|
||||
if(order.push_date || order.article_num || order.doctor_name){
|
||||
searchForm.order=order
|
||||
}
|
||||
api.searchDoctor({
|
||||
@ -257,20 +329,49 @@ const goDetail=(id,name)=>{
|
||||
*/
|
||||
const changeWord=(value)=>{
|
||||
console.log(value);
|
||||
if(value){
|
||||
navName.value=value+'临床病例库';
|
||||
|
||||
if(type.value=='hospital'){
|
||||
navName.value='医院临床病例库';
|
||||
order.hospital_name='';
|
||||
}else{
|
||||
navName.value='医生临床病例库';
|
||||
order.doctor_name='';
|
||||
}
|
||||
|
||||
keyWord.value=value;
|
||||
dataList.value=[];
|
||||
order.read_num='';
|
||||
order.push_date='';
|
||||
dataList.value=[];
|
||||
order.push_date='desc';
|
||||
order.article_num='';
|
||||
|
||||
paging.value.reload();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.dropcontent{
|
||||
padding-top: 40rpx;
|
||||
padding-bottom: 20rpx;
|
||||
background-color: #fff;
|
||||
:deep(.u-radio){
|
||||
margin-bottom: 6px!important;
|
||||
margin-top: 16px!important;
|
||||
}
|
||||
.column {
|
||||
padding: 0 30rpx;
|
||||
border-top: 2rpx solid #e5e7eb;
|
||||
:deep(.u-radio__text){
|
||||
color: #333 !important;
|
||||
}
|
||||
}
|
||||
.column.active{
|
||||
:deep(.u-radio__text){
|
||||
color: #3CC7C0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.databox {
|
||||
height: 162rpx;
|
||||
background: #ffffff;
|
||||
@ -308,6 +409,7 @@ const changeWord=(value)=>{
|
||||
position: relative;
|
||||
:deep(.u-dropdown__menu__item__text){
|
||||
font-size: 14px!important;
|
||||
color:#3CC7C0!important;
|
||||
}
|
||||
.type{
|
||||
position: absolute;
|
||||
@ -402,6 +504,7 @@ const changeWord=(value)=>{
|
||||
.tagsbox {
|
||||
margin-top: 20rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.tag {
|
||||
padding: 0 10rpx;
|
||||
margin-right: 16rpx;
|
||||
|
||||
@ -31,26 +31,43 @@
|
||||
</view> -->
|
||||
<view class="filterbox">
|
||||
<!-- <view class="type" @click="swicthType">{{!isArticle?'文章':'视频'}}<up--image :src="switchImg" width="31rpx" height="31rpx"></up--image></view> -->
|
||||
<up-dropdown class="u-dropdown" ref="uDropdownRef">
|
||||
<up-dropdown-item
|
||||
v-model="order.push_date"
|
||||
title="发布时间"
|
||||
@change="change"
|
||||
:options="options"
|
||||
></up-dropdown-item>
|
||||
<up-dropdown-item
|
||||
v-model="order.article_num"
|
||||
title="文章数量"
|
||||
@change="change"
|
||||
:options="options"
|
||||
></up-dropdown-item>
|
||||
<!-- <up-dropdown-item
|
||||
v-model="order.article_num"
|
||||
title="阅读量"
|
||||
@change="change"
|
||||
:options="options"
|
||||
></up-dropdown-item> -->
|
||||
</up-dropdown>
|
||||
<up-dropdown class="u-dropdown" ref="uDropdownRef">
|
||||
<up-dropdown-item
|
||||
|
||||
:title="dropTitle"
|
||||
|
||||
|
||||
>
|
||||
<view class="dropcontent">
|
||||
<up-radio-group
|
||||
@change="changeDate"
|
||||
v-model="orderFilter"
|
||||
iconPlacement="right"
|
||||
placement="column"
|
||||
>
|
||||
<view
|
||||
class="column"
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:class="[orderFilter==item.value?'active':'']"
|
||||
>
|
||||
<up-radio
|
||||
activeColor="#3CC7C0"
|
||||
:label="item.label"
|
||||
:name="item.value"
|
||||
></up-radio>
|
||||
</view>
|
||||
</up-radio-group>
|
||||
</view>
|
||||
</up-dropdown-item>
|
||||
<!-- <up-dropdown-item
|
||||
v-model="order.read_num"
|
||||
title="阅读量"
|
||||
@change="changeRead"
|
||||
:options="options"
|
||||
></up-dropdown-item> -->
|
||||
</up-dropdown>
|
||||
|
||||
|
||||
</view>
|
||||
</template>
|
||||
@ -83,7 +100,7 @@ import { ref, reactive} from "vue";
|
||||
import navBarSearch from "@/components/navBarSearch/navBarSearch.vue";
|
||||
import list from "@/uni_modules/z-paging/components/z-paging/z-paging";
|
||||
import api from "@/api/api";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { onLoad,onShow} from "@dcloudio/uni-app";
|
||||
import dayjs from "dayjs";
|
||||
import switchImg from "@/static/switch.png";
|
||||
import navTo from "@/utils/navTo.js";
|
||||
@ -96,22 +113,26 @@ const value = ref("");
|
||||
const keyWord = ref("");
|
||||
const isArticle=ref(true)
|
||||
const type=ref('doctor')
|
||||
const dropTitle=ref('更新时间');
|
||||
const navName=ref('疾病临床病例库');
|
||||
|
||||
const orderFilter=ref('1')
|
||||
const options= ref([
|
||||
{
|
||||
label: "正序",
|
||||
value: 'asc',
|
||||
label: "更新时间",
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: "倒序",
|
||||
value: 'desc',
|
||||
label: "疾病名称",
|
||||
value: '2',
|
||||
},
|
||||
{
|
||||
label: "病例数",
|
||||
value: '3',
|
||||
},
|
||||
]);
|
||||
|
||||
const order=reactive({
|
||||
article_num:'',
|
||||
push_date:''
|
||||
updated_at:'desc'
|
||||
})
|
||||
|
||||
onLoad((options) => {
|
||||
@ -132,9 +153,31 @@ onLoad((options) => {
|
||||
order.article_num='desc'
|
||||
};
|
||||
});
|
||||
const change=(e)=>{
|
||||
onShow(()=>{
|
||||
paging.value?.refresh();
|
||||
})
|
||||
const changeDate=(e)=>{
|
||||
if(e==1){
|
||||
order.updated_at='desc';
|
||||
order.label_name='';
|
||||
order.article_num='';
|
||||
dropTitle.value="更新时间"
|
||||
}else if(e==2){
|
||||
order.updated_at='';
|
||||
order.label_name='asc';
|
||||
order.article_num='';
|
||||
dropTitle.value="疾病名称"
|
||||
}else if(e==3){
|
||||
order.updated_at='';
|
||||
order.label_name='';
|
||||
order.article_num='desc';
|
||||
dropTitle.value="病例数"
|
||||
}
|
||||
orderFilter.value=e;
|
||||
uDropdownRef.value.close();
|
||||
paging.value.reload();
|
||||
}
|
||||
|
||||
const formatdate=(date)=>{
|
||||
return dayjs(date).format('YYYY-MM-DD')
|
||||
}
|
||||
@ -146,10 +189,13 @@ const searchList = async(params) => {
|
||||
if(!order.article_num){
|
||||
delete order.article_num
|
||||
}
|
||||
if(!order.push_date){
|
||||
delete order.push_date
|
||||
if(!order.updated_at){
|
||||
delete order.updated_at
|
||||
}
|
||||
if(order.article_num || order.push_date){
|
||||
if(!order.label_name){
|
||||
delete order.label_name
|
||||
}
|
||||
if(order.article_num || order.updated_at || order.label_name){
|
||||
searchForm.order=order
|
||||
}
|
||||
api.getSearchLabel({
|
||||
@ -181,20 +227,41 @@ const goDetail=(id,name)=>{
|
||||
* @param value 输入的关键词
|
||||
*/
|
||||
const changeWord=(value)=>{
|
||||
console.log(value);
|
||||
if(value){
|
||||
navName.value=value+'临床病例库';
|
||||
navName.value='疾病临床病例库';
|
||||
keyWord.value=value;
|
||||
dataList.value=[];
|
||||
order.article_num='';
|
||||
order.push_date='';
|
||||
order.updated_at='desc';
|
||||
order.article_num='';
|
||||
order.label_name='';
|
||||
paging.value.reload();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropcontent{
|
||||
padding-top: 40rpx;
|
||||
padding-bottom: 20rpx;
|
||||
background-color: #fff;
|
||||
:deep(.u-radio){
|
||||
margin-bottom: 6px!important;
|
||||
margin-top: 16px!important;
|
||||
}
|
||||
.column {
|
||||
padding: 0 30rpx;
|
||||
border-top: 2rpx solid #e5e7eb;
|
||||
:deep(.u-radio__text){
|
||||
color: #333 !important;
|
||||
}
|
||||
}
|
||||
.column.active{
|
||||
:deep(.u-radio__text){
|
||||
color: #3CC7C0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.votepop {
|
||||
.confirm {
|
||||
margin: 39rpx 30rpx 0;
|
||||
@ -364,9 +431,13 @@ const changeWord=(value)=>{
|
||||
|
||||
background: #fff;
|
||||
z-index:1;
|
||||
margin-left:30rpx;
|
||||
margin-left:0rpx;
|
||||
|
||||
}
|
||||
:deep(.u-dropdown__menu__item__text){
|
||||
font-size: 14px!important;
|
||||
color:#3CC7C0!important;
|
||||
}
|
||||
:deep(.u-dropdown__menu__item){
|
||||
|
||||
height:74rpx;
|
||||
@ -427,6 +498,7 @@ const changeWord=(value)=>{
|
||||
.tagsbox {
|
||||
margin-top: 20rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.tag {
|
||||
padding: 0 10rpx;
|
||||
margin-right: 16rpx;
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
<view class="name">文章</view>
|
||||
</view>
|
||||
<view class="cell" v-else>
|
||||
<view class="num">{{ numInfo.video_collect_num }}</view>
|
||||
<view class="num">{{ numInfo.video_num }}</view>
|
||||
<view class="name">视频</view>
|
||||
</view>
|
||||
<view class="cell" v-if="isArticle">
|
||||
@ -29,32 +29,48 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="bar"></view>
|
||||
<!-- <view class="detail" v-if="isSearch">
|
||||
<view class="desc" v-if="isArticle">检索到:<text class="red">{{searchInfo.search_article_num}}篇文章</text></view>
|
||||
<view class="desc" v-else>检索到:<text class="red">{{searchInfo.search_video_num}}个视频</text></view>
|
||||
<view class="detail" v-if="isSearch">
|
||||
<view class="desc" >检索到:<text class="red">{{searchInfo.search_video_num}}个视频</text></view>
|
||||
<view class="desc" v-if="keyWord">检索词:<text class="red">{{ keyWord }}</text></view>
|
||||
</view> -->
|
||||
</view>
|
||||
<view class="filterbox">
|
||||
<view class="casesdown" :class="{active:label_iden}" @click="openCase" >筛选<up--image :src="caseImg" width="31rpx" height="31rpx"></up--image></view>
|
||||
<!-- <view class="type" @click="swicthType">{{!isArticle?'文章':'视频'}}<up--image :src="switchImg" width="31rpx" height="31rpx"></up--image></view> -->
|
||||
<up-dropdown class="u-dropdown" ref="uDropdownRef">
|
||||
<up-dropdown-item
|
||||
v-model="order.push_date"
|
||||
title="发布时间"
|
||||
@change="change"
|
||||
:options="options"
|
||||
></up-dropdown-item>
|
||||
<up-dropdown-item
|
||||
v-model="order.read_num"
|
||||
title="阅读量"
|
||||
@change="change"
|
||||
:options="options"
|
||||
></up-dropdown-item>
|
||||
</up-dropdown>
|
||||
<up-dropdown-item
|
||||
|
||||
:title="dropTitle"
|
||||
|
||||
|
||||
>
|
||||
<view class="dropcontent">
|
||||
<up-radio-group
|
||||
@change="changeDate"
|
||||
v-model="orderFilter"
|
||||
iconPlacement="right"
|
||||
placement="column"
|
||||
>
|
||||
<view
|
||||
class="column"
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:class="[orderFilter==item.value?'active':'']"
|
||||
>
|
||||
<up-radio
|
||||
activeColor="#3CC7C0"
|
||||
:label="item.label"
|
||||
:name="item.value"
|
||||
></up-radio>
|
||||
</view>
|
||||
</up-radio-group>
|
||||
</view>
|
||||
</up-dropdown-item>
|
||||
</up-dropdown>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
<view class="item" v-for="(item, index) in dataList" :key="index" @click="isArticle?goDetail(item.article_id):goDetail(item.video_id)">
|
||||
<view class="title ellipsis">{{isArticle?item.article_title:item.video_title }}</view>
|
||||
<view class="item" v-for="(item, index) in dataList" :key="index" @click="goDetail(item.video_id,item.is_link,item.is_link_url)">
|
||||
<view class="title ellipsis-two-lines">{{isArticle?item.article_title:item.video_title }}</view>
|
||||
<view class="tagsbox">
|
||||
<view class="tag" v-for="tag in item.author" :key="tag.author_id">{{ tag.doctor_name }}</view>
|
||||
</view>
|
||||
@ -64,7 +80,7 @@
|
||||
<up-icon name="eye" color="#6B7280" size="28rpx"></up-icon>
|
||||
<view class="num">{{item.read_num }}</view>
|
||||
</view>
|
||||
<view class="collect">
|
||||
<view class="collect" v-if="item.collect_num>0">
|
||||
<up-icon name="heart" color="#6B7280" size="28rpx"></up-icon>
|
||||
<view class="num">{{item.collect_num }}</view>
|
||||
</view>
|
||||
@ -77,6 +93,117 @@
|
||||
</view>
|
||||
</z-paging>
|
||||
</view>
|
||||
<up-popup
|
||||
:round="10"
|
||||
zIndex="9"
|
||||
:closeOnClickOverlay="false"
|
||||
:show="showCase"
|
||||
mode="bottom"
|
||||
@close="closeCase"
|
||||
>
|
||||
<view class="votepop casepop">
|
||||
<view class="titlebox">
|
||||
<view class="left" @click="closeCase">取消</view>
|
||||
|
||||
<view class="right" @click="confirmCase">确定</view>
|
||||
</view>
|
||||
<view class="stepbox">
|
||||
<up-steps :current="level-1" direction="column" :key="freshKey">
|
||||
<up-steps-item >
|
||||
<template v-slot:content>
|
||||
<view class="slot-content" @click="openCaseLevel('1')">
|
||||
|
||||
<view class="left">{{!caseValue1.name?'请选择选项':caseValue1.name}}</view>
|
||||
<u-icon name="arrow-right"></u-icon>
|
||||
</view>
|
||||
</template>
|
||||
</up-steps-item>
|
||||
<up-steps-item v-if="caseValue1.name && labelObj.list2.length>0">
|
||||
<template v-slot:content>
|
||||
<view class="slot-content" @click="openCaseLevel('2')">
|
||||
<view class="left">{{!caseValue2.name?'请选择选项':caseValue2.name}}</view>
|
||||
<u-icon name="arrow-right"></u-icon>
|
||||
</view>
|
||||
</template>
|
||||
</up-steps-item>
|
||||
<up-steps-item v-if="caseValue2.name && labelObj.list3.length>0">
|
||||
<template v-slot:content>
|
||||
<view class="slot-content" @click="openCaseLevel('3')">
|
||||
<view class="left">{{!caseValue3.name?'请选择选项':caseValue3.name}}</view>
|
||||
<u-icon name="arrow-right"></u-icon>
|
||||
</view>
|
||||
</template>
|
||||
</up-steps-item>
|
||||
</up-steps>
|
||||
</view>
|
||||
<scroll-view class="casecon" scroll-y="true">
|
||||
<view v-show="level == 1" >
|
||||
<up-radio-group
|
||||
v-model="caseValue1.value"
|
||||
name="group1"
|
||||
iconPlacement="right"
|
||||
placement="column"
|
||||
@change="groupChange1"
|
||||
>
|
||||
<view
|
||||
class="column"
|
||||
v-for="item in labelObj.list1"
|
||||
:key="item.app_iden"
|
||||
v-show="item.label_name!='热门话题'"
|
||||
>
|
||||
<up-radio
|
||||
activeColor="#3CC7C0 "
|
||||
:label="item.label_name"
|
||||
:name="item.app_iden"
|
||||
></up-radio>
|
||||
</view>
|
||||
</up-radio-group>
|
||||
</view>
|
||||
<view v-show="level == 2" >
|
||||
<up-radio-group
|
||||
name="group2"
|
||||
@change="groupChange2"
|
||||
v-model="caseValue2.value"
|
||||
iconPlacement="right"
|
||||
placement="column"
|
||||
>
|
||||
<view
|
||||
class="column"
|
||||
v-for="item in labelObj.list2"
|
||||
:key="item.app_iden"
|
||||
>
|
||||
<up-radio
|
||||
activeColor="#3CC7C0 "
|
||||
:label="item.label_name"
|
||||
:name="item.app_iden"
|
||||
></up-radio>
|
||||
</view>
|
||||
</up-radio-group>
|
||||
</view>
|
||||
<view v-show="level == 3" >
|
||||
<up-radio-group
|
||||
name="group3"
|
||||
@change="groupChange3"
|
||||
v-model="caseValue3.value"
|
||||
iconPlacement="right"
|
||||
placement="column"
|
||||
>
|
||||
<view
|
||||
class="column"
|
||||
v-for="item in labelObj.list3"
|
||||
:key="item.app_iden"
|
||||
>
|
||||
<up-radio
|
||||
activeColor="#3CC7C0 "
|
||||
:label="item.label_name"
|
||||
:name="item.app_iden"
|
||||
></up-radio>
|
||||
</view>
|
||||
</up-radio-group>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</up-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -85,11 +212,11 @@ import navBarSearch from "@/components/navBarSearch/navBarSearch.vue";
|
||||
import list from "@/uni_modules/z-paging/components/z-paging/z-paging";
|
||||
import api from "@/api/api";
|
||||
import navTo from "@/utils/navTo.js";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { onLoad,onShow} from "@dcloudio/uni-app";
|
||||
import dayjs from "dayjs";
|
||||
import switchImg from "@/static/switch.png";
|
||||
import caseImg from "@/static/caseIcon.png";
|
||||
const paging = ref(null);
|
||||
const uDropdownRef=ref(null);
|
||||
const dataList = ref([]);
|
||||
const total = ref(0);
|
||||
const keyWord = ref("");
|
||||
@ -102,21 +229,166 @@ const doctor_name=ref('');
|
||||
const numInfo=reactive({});
|
||||
const searchInfo=reactive({})
|
||||
const navName=ref('视频临床病例库')
|
||||
const dropTitle=ref('发布时间');
|
||||
const orderFilter=ref('1')
|
||||
const uDropdownRef=ref(null);
|
||||
const label_iden=ref(null);
|
||||
const options= ref([
|
||||
{
|
||||
label: "正序",
|
||||
value: 'asc',
|
||||
label: "发布时间",
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: "倒序",
|
||||
value: 'desc',
|
||||
label: "阅读次数",
|
||||
value: '2',
|
||||
},
|
||||
]);
|
||||
const isSearch=ref(false);
|
||||
const order=reactive({
|
||||
read_num:'',
|
||||
push_date:''
|
||||
push_date:'desc'
|
||||
})
|
||||
const showCase = ref(false);
|
||||
const canOpenCase = ref(false);
|
||||
const freshKey=ref(0);
|
||||
const caseValue1 = reactive({
|
||||
value:'',
|
||||
name:'',
|
||||
});
|
||||
const caseValue2 = reactive({
|
||||
value:'',
|
||||
name:'',
|
||||
});
|
||||
const caseValue3 = reactive({
|
||||
value:'',
|
||||
name:'',
|
||||
});
|
||||
const level = ref(1);
|
||||
const labelObj = reactive({
|
||||
list1: [],
|
||||
list2: [],
|
||||
list3: []
|
||||
});
|
||||
const openCaseLevel=(lev)=>{
|
||||
freshKey.value++;
|
||||
level.value=lev;
|
||||
if(lev==1){
|
||||
caseValue2.name='';
|
||||
caseValue2.value='';
|
||||
labelObj.list2=[]
|
||||
}else if(lev==2){
|
||||
labelObj.list3=[];
|
||||
caseValue3.name='';
|
||||
caseValue3.value=''
|
||||
}
|
||||
};
|
||||
const groupChange1=(e)=>{
|
||||
caseValue1.value=e;
|
||||
for (var i = 0; i <labelObj.list1.length; i++) {
|
||||
if(labelObj.list1[i].app_iden==caseValue1.value){
|
||||
caseValue1.name=labelObj.list1[i].label_name;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
getCaseLabel(2,e)
|
||||
}
|
||||
const groupChange2=(e)=>{
|
||||
caseValue2.value=e;
|
||||
for (var i = 0; i <labelObj.list2.length; i++) {
|
||||
if(labelObj.list2[i].app_iden==caseValue2.value){
|
||||
caseValue2.name=labelObj.list2[i].label_name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
getCaseLabel(3,e)
|
||||
}
|
||||
const groupChange3=(e)=>{
|
||||
caseValue3.value=e;
|
||||
level.value =3
|
||||
for (var i = 0; i <labelObj.list3.length; i++) {
|
||||
if(labelObj.list3[i].app_iden==caseValue3.value){
|
||||
caseValue3.name=labelObj.list3[i].label_name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
const getCaseLabel = (lev,pid=0) => {
|
||||
api.getCaseLabel({
|
||||
pId:pid
|
||||
}).then((res) => {
|
||||
level.value = lev;
|
||||
if (lev == 1) {
|
||||
labelObj.list1 = res.data.data;
|
||||
//label_iden.value = caseValue1.value;
|
||||
|
||||
} else if (lev == 2) {
|
||||
labelObj.list2 = res.data.data;
|
||||
if(res.data.data.length==0){
|
||||
level.value = 1
|
||||
}
|
||||
//label_iden.value = caseValue2.value;
|
||||
|
||||
} else if (lev == 3) {
|
||||
labelObj.list3 = res.data.data;
|
||||
if(res.data.data.length==0){
|
||||
level.value = 2
|
||||
}
|
||||
//label_iden.value = caseValue3.value;
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
const openCase=()=>{
|
||||
|
||||
showCase.value = true;
|
||||
|
||||
}
|
||||
const cancelCase=()=>{
|
||||
showCase.value=false;
|
||||
level.value=1;
|
||||
caseValue1.name='';
|
||||
caseValue1.value='';
|
||||
caseValue2.name='';
|
||||
caseValue2.value='';
|
||||
caseValue3.name='';
|
||||
caseValue3.value='';
|
||||
//labelObj.list1=[];
|
||||
labelObj.list2=[];
|
||||
labelObj.list3=[];
|
||||
}
|
||||
const confirmCase = () => {
|
||||
if (level.value == 1 && caseValue1.value == "") {
|
||||
uni.showToast({ title: "请选择疾病选项", icon: "none" });
|
||||
return false;
|
||||
};
|
||||
if(level.value == 1){
|
||||
label_iden.value = caseValue1.value;
|
||||
}
|
||||
if(level.value == 2 ){
|
||||
if(!caseValue2.value){
|
||||
label_iden.value = caseValue1.value;
|
||||
}else{
|
||||
label_iden.value = caseValue2.value;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
if(level.value == 3 ){
|
||||
if(!caseValue3.value){
|
||||
label_iden.value = caseValue2.value;
|
||||
}else{
|
||||
label_iden.value = caseValue3.value;
|
||||
}
|
||||
}
|
||||
paging.value.reload();
|
||||
showCase.value=false;
|
||||
//cancelCase();
|
||||
};
|
||||
const closeCase = () => {
|
||||
showCase.value = false;
|
||||
};
|
||||
|
||||
const changeWord=(value)=>{
|
||||
keyWord.value=value;
|
||||
@ -151,20 +423,58 @@ onLoad((options) => {
|
||||
getStaticDoctor(hospital_id.value)
|
||||
|
||||
}
|
||||
getCaseLabel(1,0)
|
||||
});
|
||||
const change=(e)=>{
|
||||
onShow(()=>{
|
||||
paging.value?.refresh();
|
||||
})
|
||||
const changeDate=(e)=>{
|
||||
if(e==1){
|
||||
order.push_date='desc';
|
||||
order.read_num='';
|
||||
dropTitle.value="发布时间"
|
||||
}else{
|
||||
order.push_date='';
|
||||
order.read_num="desc";
|
||||
dropTitle.value="阅读次数"
|
||||
}
|
||||
orderFilter.value=e;
|
||||
uDropdownRef.value.close();
|
||||
paging.value.reload();
|
||||
}
|
||||
const changeRead=(e)=>{
|
||||
console.log(e);
|
||||
order.push_date=''
|
||||
paging.value.reload();
|
||||
}
|
||||
const formatdate=(date)=>{
|
||||
return dayjs(date).format('YYYY-MM-DD')
|
||||
}
|
||||
const goDetail=(id)=>{
|
||||
console.log(11111)
|
||||
console.log(id)
|
||||
let type=isArticle.value?'article':'video'
|
||||
navTo({
|
||||
url: `/pages/detail/detail?id=${id}&type=${type}`
|
||||
})
|
||||
const goDetail=(id,isLink,src)=>{
|
||||
console.log(999999)
|
||||
console.log(isLink,src)
|
||||
if(isLink==1){
|
||||
api.readRecord({
|
||||
type:2,
|
||||
id:id
|
||||
}).then(res=>{
|
||||
|
||||
})
|
||||
// #ifdef MP-WEIXIN
|
||||
navTo({
|
||||
url: `/pages/web/web?src=${src}`,
|
||||
});
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
window.location.href=`${src}`
|
||||
// #endif
|
||||
}else{
|
||||
let type=isArticle.value?'article':'video'
|
||||
navTo({
|
||||
url: `/pages/detail/detail?id=${id}&type=${type}`
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
const getStatic=()=>{
|
||||
api.getHomeStatics().then((res) => {
|
||||
@ -176,8 +486,12 @@ const searchArticle =(params) => {
|
||||
let searchForm={
|
||||
keyword: keyWord.value,
|
||||
hospital_id:hospital_id.value,
|
||||
doctor_id:doctor_id.value
|
||||
}
|
||||
doctor_id:doctor_id.value,
|
||||
label_iden:label_iden.value
|
||||
}
|
||||
if(label_iden.value===''){
|
||||
delete searchForm.label_iden
|
||||
}
|
||||
if(isSearch.value){
|
||||
searchForm.is_need_num=1;
|
||||
}
|
||||
@ -207,7 +521,11 @@ const searchVideo = async(params) => {
|
||||
let searchForm={
|
||||
keyword: keyWord.value,
|
||||
hospital_id:hospital_id.value,
|
||||
doctor_id:doctor_id.value
|
||||
doctor_id:doctor_id.value,
|
||||
label_iden:label_iden.value
|
||||
}
|
||||
if(label_iden.value===''){
|
||||
delete searchForm.label_iden
|
||||
}
|
||||
if(isSearch.value){
|
||||
searchForm.is_need_num=1;
|
||||
@ -245,6 +563,42 @@ const queryList = (pageNo, pageSize) => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropcontent{
|
||||
padding-top: 40rpx;
|
||||
padding-bottom: 20rpx;
|
||||
background-color: #fff;
|
||||
:deep(.u-radio){
|
||||
margin-bottom: 6px!important;
|
||||
margin-top: 16px!important;
|
||||
}
|
||||
.column {
|
||||
padding: 0 30rpx;
|
||||
border-top: 2rpx solid #e5e7eb;
|
||||
:deep(.u-radio__text){
|
||||
color: #333 !important;
|
||||
}
|
||||
}
|
||||
.column.active{
|
||||
:deep(.u-radio__text){
|
||||
color: #3CC7C0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.stepbox{
|
||||
padding:15rpx 30rpx;
|
||||
border-bottom: 2rpx dotted #f3f4f6;
|
||||
:deep(.u-steps-item__content){
|
||||
margin-top: -5rpx!important;
|
||||
}
|
||||
.slot-content{
|
||||
width:100%;
|
||||
margin-bottom: 25rpx;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.bar {
|
||||
width: 100%;
|
||||
background: #f9fafb;
|
||||
@ -276,13 +630,30 @@ const queryList = (pageNo, pageSize) => {
|
||||
}
|
||||
}
|
||||
.filterbox{
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
height:128rpx;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border-bottom: 2rpx solid #f3f4f6;
|
||||
:deep(.u-dropdown__menu__item__text){
|
||||
font-size: 14px!important;
|
||||
:deep(.u-dropdown__menu__item__text){
|
||||
font-size: 14px!important;
|
||||
color:#3CC7C0!important;
|
||||
}
|
||||
.casesdown{
|
||||
font-size:14px;
|
||||
color: #606266;
|
||||
position: absolute;
|
||||
left:30rpx;
|
||||
top:24rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #F3F4F6;
|
||||
border-radius: 15rpx;
|
||||
height: 74rpx;
|
||||
padding:0 25rpx;
|
||||
z-index:2;
|
||||
}
|
||||
.type{
|
||||
color: #606266;
|
||||
@ -311,7 +682,7 @@ const queryList = (pageNo, pageSize) => {
|
||||
|
||||
background: #fff;
|
||||
z-index:1;
|
||||
margin-left: 30rpx;
|
||||
margin-left: 165rpx;
|
||||
|
||||
}
|
||||
:deep(.u-dropdown__menu__item){
|
||||
@ -321,7 +692,7 @@ const queryList = (pageNo, pageSize) => {
|
||||
background: #F3F4F6;
|
||||
border-radius: 15rpx;
|
||||
flex:none;
|
||||
margin-left: 30rpx;
|
||||
margin-left: 30rpx;
|
||||
|
||||
}
|
||||
.deal {
|
||||
@ -352,6 +723,7 @@ const queryList = (pageNo, pageSize) => {
|
||||
}
|
||||
}
|
||||
.item {
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #f3f4f6;
|
||||
padding: 30rpx;
|
||||
.title {
|
||||
@ -363,6 +735,7 @@ const queryList = (pageNo, pageSize) => {
|
||||
.tagsbox {
|
||||
margin-top: 20rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.tag {
|
||||
padding: 0 10rpx;
|
||||
margin-right: 16rpx;
|
||||
@ -389,4 +762,114 @@ const queryList = (pageNo, pageSize) => {
|
||||
color: #FF0000;
|
||||
}
|
||||
}
|
||||
.votepop {
|
||||
.confirm {
|
||||
margin: 39rpx 30rpx 0;
|
||||
height: 92rpx;
|
||||
background: #3cc7c0;
|
||||
border-radius: 15rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 31rpx;
|
||||
color: #ffffff;
|
||||
justify-content: center;
|
||||
}
|
||||
.del {
|
||||
margin: 30rpx 30rpx 30rpx;
|
||||
height: 92rpx;
|
||||
background: #fff;
|
||||
border-radius: 15rpx;
|
||||
font-size: 31rpx;
|
||||
color: #666666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.tips {
|
||||
margin-top: 30rpx;
|
||||
font-size: 27rpx;
|
||||
color: #9ca3af;
|
||||
line-height: 38rpx;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
.add {
|
||||
margin: 0 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 92rpx;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
border-radius: 15rpx;
|
||||
font-size: 31rpx;
|
||||
color: #4b5563;
|
||||
.desc {
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
}
|
||||
.titlebox {
|
||||
padding: 0 30rpx;
|
||||
height: 86rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 2rpx solid #f3f4f6;
|
||||
.left {
|
||||
font-size: 31rpx;
|
||||
color: #4b5563;
|
||||
}
|
||||
.right {
|
||||
font-size: 31rpx;
|
||||
color: #3cc7c0;
|
||||
}
|
||||
}
|
||||
.votecon {
|
||||
max-height: calc(100vh - 530rpx);
|
||||
overflow-y: scroll;
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.iconbox {
|
||||
margin-left: 15rpx;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.titlebox {
|
||||
border: none;
|
||||
margin: 30rpx 0 20rpx;
|
||||
.title {
|
||||
font-size: 31rpx;
|
||||
color: #111827;
|
||||
}
|
||||
.desc {
|
||||
font-size: 27rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.casepop{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 400rpx);
|
||||
.continue{
|
||||
color: #2878ff !important;
|
||||
}
|
||||
}
|
||||
.casecon{
|
||||
flex:1;
|
||||
max-height: calc(100vh - 800rpx);
|
||||
overflow-y: scroll;
|
||||
padding-top: 22rpx;
|
||||
padding-bottom: 20rpx;
|
||||
min-height: 350rpx;
|
||||
:deep(.u-radio){
|
||||
margin-bottom: 10px!important;
|
||||
margin-top: 0px!important;
|
||||
}
|
||||
.column {
|
||||
padding: 0 30rpx;
|
||||
border-bottom: 2rpx solid #e5e7eb;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
16
pages/web/web.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<web-view :src="src"></web-view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
const src=ref('')
|
||||
onLoad((option)=>{
|
||||
src.value=option.src
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
BIN
static/benren.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 48 KiB |
BIN
static/draft.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 43 KiB |
BIN
static/myCollect.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
static/myDownload.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
static/myFile.png
Normal file
|
After Width: | Height: | Size: 783 B |
BIN
static/myHospital.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
static/myJoin.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
static/myTalk.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
static/navbg.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
static/ppt.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
static/sick.png
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 45 KiB |
BIN
static/video.png
|
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 44 KiB |
BIN
static/videoface.jpg
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
static/voteon.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
72
uni_modules/qf-image-cropper/changelog.md
Normal file
@ -0,0 +1,72 @@
|
||||
## 2.2.5(2024-07-30)
|
||||
* 修复 当 checkRange=true 时,拖动四个伸缩角放大图片时还可能会超出或未到边界的问题
|
||||
* 修复 当 checkRange=false 时,图片旋转时会放大图片适应裁剪尺寸的问题
|
||||
* 修复 当 checkRange=true 时,图片旋转 90° 或 270° 进行缩放可能会无法拖动图片的问题
|
||||
## 2.2.4(2024-06-21)
|
||||
* 新增 reverseRotatable 属性,是否支持逆向翻转
|
||||
* 修复 `2.1.7` 版本导致旋转后图片没有自动适配裁剪框的问题
|
||||
|
||||
## 2.2.3(2024-06-21)
|
||||
* 新增 gpu 属性,是否开启硬件加速,图片缩放过程中如果出现元素的“留影”或“重影”效果,可通过该方式解决或减轻这一问题
|
||||
* 修复 组件使用 `v-if` 并设置 `src` 属性时可能会出现图片渲染位置存在偏差的问题
|
||||
|
||||
## 2.2.2(2024-06-21)
|
||||
* 优化 组件实例 chooseImage 方法支持传参
|
||||
* 修复 组件使用 `v-if` 时组件无非正常渲染的问题
|
||||
|
||||
## 2.2.1(2024-06-15)
|
||||
* 修复 H5平台不支持手势拖动图片的问题
|
||||
|
||||
## 2.2.0(2024-05-31)
|
||||
* 修复 APP平台 `vue2` 项目因 `2.1.9` 版本修复 `vue3` 项目bug而引发的问题
|
||||
|
||||
## 2.1.9(2024-05-29)
|
||||
* 修复 APP平台 `vue3` 项目因 uniapp `renderjs` 中未支持条件编译,导致运行了H5平台代码报错的问题
|
||||
|
||||
## 2.1.8(2024-05-29)
|
||||
* 新增 zIndex 属性,调整组件层级
|
||||
* 新增 组件内容插槽
|
||||
* 优化 微信小程序平台动态修改元素style时的多余内容
|
||||
|
||||
## 2.1.7(2024-05-28)
|
||||
* 新增 checkRange 属性,当 checkRange=false 时允许图片位置超出裁剪边界
|
||||
* 新增 minScale 属性,图片最小缩放倍数,当 minScale<0 时可使图片宽高不再受裁剪区域宽高限制
|
||||
* 新增 backgroundColor 属性,生成图片背景色,如果裁剪区域没有完全包含在图片中时,不设置该属性生成图片存在一定的透明块
|
||||
* 优化 动态修改图片宽高但没有传入src时,尺寸适应问题
|
||||
* 修复 APP平台通过 `this.$ownerInstance` 获取组件实例时机过早,其值为 `undefined` 导致报错界面没有正常渲染的问题
|
||||
|
||||
## 2.1.6(2023-04-16)
|
||||
* 修复 组件使用 v-show 指令会导致选择图片后初始位置严重偏位的问题
|
||||
|
||||
## 2.1.5(2023-04-15)
|
||||
* 新增 兼容APP平台
|
||||
|
||||
## 2.1.4(2023-03-13)
|
||||
* 新增 fileType 属性,用于指定生成文件的类型,只支持 'jpg' 或 'png',默认为 'png'
|
||||
* 新增 delay 属性,微信小程序平台使用 `Canvas 2D` 绘制时控制图片从绘制到生成所需时间
|
||||
* 优化 当生成图片的尺寸宽/高超过 Canvas 2D 最大限制(1365*1365)则将画布尺寸缩放在限制范围内绘制完成后输出目标尺寸
|
||||
* 优化 旋转图标指示方向与实际旋转方向不符
|
||||
|
||||
## 2.1.3(2023-02-06)
|
||||
* 优化 vue3支持
|
||||
|
||||
## 2.1.2(2023-02-03)
|
||||
* 新增 navigation 属性,H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 "navigationStyle": "custom" 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差
|
||||
* 修复 H5平台部分设备(已知iPhone11以下机型)拍照的图片缩放时会闪动的问题
|
||||
|
||||
## 2.1.1(2022-12-06)
|
||||
* 修复 横屏适配问题
|
||||
|
||||
## 2.1.0(2022-12-06)
|
||||
* 新增 兼容H5平台,使用 renderjs 响应手势事件
|
||||
|
||||
## 2.0.0(2022-12-05)
|
||||
* 重构 插件,使用 WXS 响应手势事件
|
||||
* 新增 图片翻转
|
||||
* 新增 拉伸裁剪框放大图片
|
||||
* 新增 监听PC鼠标滚轮触发缩放
|
||||
* 新增 圆形、圆角矩形的图片裁剪
|
||||
* 优化 图片缩放,移动端以双指触摸中心点为缩放中心点,PC端以鼠标所在点为缩放中心点
|
||||
* 优化 裁剪框样式
|
||||
* 优化 图片位置拖动 支持边界回弹效果(滑动时可滑出边界,释放时回弹到边界)
|
||||
* 优化 生成图片使用新版 Canvas 2D 接口
|
||||
@ -0,0 +1,855 @@
|
||||
/**
|
||||
* 图片编辑器-手势监听
|
||||
* 1. 支持编译到app-vue(uni-app 2.5.5及以上版本)、H5上
|
||||
*/
|
||||
/** 图片偏移量 */
|
||||
var offset = { x: 0, y: 0 };
|
||||
/** 图片缩放比例 */
|
||||
var scale = 1;
|
||||
/** 图片最小缩放比例 */
|
||||
var minScale = 1;
|
||||
/** 图片旋转角度 */
|
||||
var rotate = 0;
|
||||
/** 触摸点 */
|
||||
var touches = [];
|
||||
/** 图片布局信息 */
|
||||
var img = {};
|
||||
/** 系统信息 */
|
||||
var sys = {};
|
||||
/** 裁剪区域布局信息 */
|
||||
var area = {};
|
||||
/** 触摸行为类型 */
|
||||
var touchType = '';
|
||||
/** 操作角的位置 */
|
||||
var activeAngle = 0;
|
||||
/** 裁剪区域布局信息偏移量 */
|
||||
var areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
|
||||
/** 元素ID */
|
||||
var elIds = {
|
||||
'imageStyles': 'crop-image',
|
||||
'maskStylesList': 'crop-mask-block',
|
||||
'borderStyles': 'crop-border',
|
||||
'circleBoxStyles': 'crop-circle-box',
|
||||
'circleStyles': 'crop-circle',
|
||||
'gridStylesList': 'crop-grid',
|
||||
'angleStylesList': 'crop-angle',
|
||||
}
|
||||
/** 记录上次初始化时间戳,排除APP重复更新 */
|
||||
var timestamp = 0;
|
||||
/** vue3 renderjs 条件编译无效,以此方式区别 APP 和 H5 */
|
||||
// #ifdef H5
|
||||
var platform = 'H5';
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
var platform = 'APP';
|
||||
// #endif
|
||||
/** 容错值 */
|
||||
var fault = 0.000001;
|
||||
/**
|
||||
* 获取a、b两数中的最小正数
|
||||
* @param a
|
||||
* @param b
|
||||
*/
|
||||
function minimum(a, b) {
|
||||
if (a > 0 && b < 0) return a;
|
||||
if (a < 0 && b > 0) return b;
|
||||
if (a > 0 && b > 0) return Math.min(a, b);
|
||||
return 0;
|
||||
}
|
||||
/**
|
||||
* 在容错访问内获取n近似值
|
||||
* @param n
|
||||
*/
|
||||
function num(n) {
|
||||
var m = parseFloat((n).toFixed(6));
|
||||
return m === fault || m === -fault ? 0 : m;
|
||||
}
|
||||
/**
|
||||
* 比较a值在容错值范围内是否等于b值
|
||||
* @param a
|
||||
* @param b
|
||||
*/
|
||||
function equalsByFault(a, b) {
|
||||
return Math.abs(a - b) <= fault;
|
||||
}
|
||||
/**
|
||||
* 比较a值在容错值范围内是否小于b值
|
||||
* @param a
|
||||
* @param b
|
||||
*/
|
||||
function lessThanByFault(a, b) {
|
||||
var c = a - b;
|
||||
return c < 0 ? c < -fault : c < fault;
|
||||
}
|
||||
/**
|
||||
* 验证并获取有效最大值
|
||||
* @param v
|
||||
* @param max
|
||||
* @param isInclude
|
||||
* @param x
|
||||
* @param y
|
||||
* @param rate
|
||||
* @returns
|
||||
*/
|
||||
function validMax(v, max, isInclude, x, y, rate) {
|
||||
if(typeof max === 'number') {
|
||||
if(isInclude && equalsByFault(max, y)) { // 宽高不等时,x轴用y轴值要做等比例转换
|
||||
var n = num(max * rate);
|
||||
if (n <= x) return n; // 转化后值在x轴最大值范围内
|
||||
return x; // 转化后值超出x轴最大值范围则用最大值
|
||||
}
|
||||
return max;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
/**
|
||||
* 样式对象转字符串
|
||||
* @param {Object} style 样式对象
|
||||
*/
|
||||
function styleToString(style) {
|
||||
if(typeof style === 'string') return style;
|
||||
var str = '';
|
||||
for (let k in style) {
|
||||
str += k + ':' + style[k] + ';';
|
||||
}
|
||||
return str;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {Object} instance 页面实例对象
|
||||
* @param {Object} key 要修改样式的key
|
||||
* @param {Object|Array} style 样式
|
||||
*/
|
||||
function setStyle(instance, key, style) {
|
||||
// console.log('setStyle', instance, key, JSON.stringify(style))
|
||||
// #ifdef APP-PLUS
|
||||
if(platform === 'APP') {
|
||||
if(Object.prototype.toString.call(style) === '[object Array]') {
|
||||
for (var i = 0, len = style.length; i < len; i++) {
|
||||
var el = window.document.getElementById(elIds[key] + '-' + (i + 1));
|
||||
el && (el.style = styleToString(style[i]));
|
||||
}
|
||||
} else {
|
||||
var el = window.document.getElementById(elIds[key]);
|
||||
el && (el.style = styleToString(style));
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
if(platform === 'H5') instance[key] = style;
|
||||
// #endif
|
||||
}
|
||||
/**
|
||||
* 触发页面实例指定方法
|
||||
* @param {Object} instance 页面实例对象
|
||||
* @param {Object} name 方法名称
|
||||
* @param {Object} obj 传递参数
|
||||
*/
|
||||
function callMethod(instance, name, obj) {
|
||||
// #ifdef APP-PLUS
|
||||
if(platform === 'APP') instance.callMethod(name, obj);
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
if(platform === 'H5') instance[name](obj);
|
||||
// #endif
|
||||
}
|
||||
/**
|
||||
* 计算两点间距
|
||||
* @param {Object} touches 触摸点信息
|
||||
*/
|
||||
function getDistanceByTouches(touches) {
|
||||
// 根据勾股定理求两点间距离
|
||||
var a = touches[1].pageX - touches[0].pageX;
|
||||
var b = touches[1].pageY - touches[0].pageY;
|
||||
var c = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
|
||||
// 求两点间的中点坐标
|
||||
// 1. a、b可能为负值
|
||||
// 2. 在求a、b时,如用touches[1]减touches[0],则求中点坐标也得用touches[1]减a/2、b/2
|
||||
// 3. 同理,在求a、b时,也可用touches[0]减touches[1],则求中点坐标也得用touches[0]减a/2、b/2
|
||||
var x = touches[1].pageX - a / 2;
|
||||
var y = touches[1].pageY - b / 2;
|
||||
return { c, x, y };
|
||||
};
|
||||
|
||||
/**
|
||||
* 修正取值
|
||||
* @param {Object} a
|
||||
* @param {Object} b
|
||||
* @param {Object} c
|
||||
* @param {Object} reverse 是否反向
|
||||
*/
|
||||
function correctValue(a, b, c, reverse) {
|
||||
return num(reverse ? Math.max(Math.min(a, b), c) : Math.min(Math.max(a, b), c));
|
||||
}
|
||||
|
||||
/**
|
||||
* 旋转90°或270°时检查边界:限制 x、y 拖动范围,禁止滑出边界
|
||||
* @param {Object} e 点坐标
|
||||
* @param {Object} xReverse x是否反向
|
||||
* @param {Object} yReverse y是否反向
|
||||
*/
|
||||
function checkRotateRange(e, xReverse, yReverse) {
|
||||
var o = num((img.height - img.width) / 2); // 宽高差值一半
|
||||
return {
|
||||
x: correctValue(e.x, -img.height + o + area.width + area.left, area.left + o, xReverse),
|
||||
y: correctValue(e.y, -img.width - o + area.height + area.top, area.top - o, yReverse)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查边界:限制 x、y 拖动范围,禁止滑出边界
|
||||
* @param {Object} e 点坐标
|
||||
*/
|
||||
function checkRange(e) {
|
||||
var r = rotate / 90 % 2;
|
||||
if(r === 1) { // 因图片宽高可能不等,翻转 90° 或 270° 后图片宽高需反着计算,且左右和上下边界要根据差值做偏移
|
||||
if (area.width === area.height) {
|
||||
return checkRotateRange(e, img.height < area.height, img.width < area.width);
|
||||
}
|
||||
var isInclude = img.height < area.width && img.width < area.height; // 图片是否包含在裁剪区域内
|
||||
if (img.width < area.height || img.height < area.width) {
|
||||
if (area.width < area.height && img.width < img.height) {
|
||||
return isInclude
|
||||
? checkRotateRange(e, area.width < area.height, area.width < area.height)
|
||||
: checkRotateRange(e, false, true);
|
||||
}
|
||||
if (area.height < area.width && img.height < img.width) {
|
||||
return isInclude
|
||||
? checkRotateRange(e, area.height < area.width, area.height < area.width)
|
||||
: checkRotateRange(e, true, false);
|
||||
}
|
||||
}
|
||||
if (img.height >= area.width && img.width >= area.height) {
|
||||
return checkRotateRange(e, false, false);
|
||||
}
|
||||
if (isInclude) {
|
||||
return area.height < area.width
|
||||
? checkRotateRange(e, true, true)
|
||||
: checkRotateRange(e, area.width < area.height, area.width < area.height);
|
||||
}
|
||||
if (img.height < area.width && !img.width < area.height) {
|
||||
return checkRotateRange(e, true, false);
|
||||
}
|
||||
if (!img.height < area.width && img.width < area.height) {
|
||||
return checkRotateRange(e, false, true);
|
||||
}
|
||||
return checkRotateRange(e, img.height < area.height, img.width < area.width);
|
||||
}
|
||||
return {
|
||||
x: correctValue(e.x, -img.width + area.width + area.left, area.left, img.width < area.width),
|
||||
y: correctValue(e.y, -img.height + area.height + area.top, area.top, img.height < area.height)
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 变更图片布局信息
|
||||
* @param {Object} e 布局信息
|
||||
*/
|
||||
function changeImageRect(e) {
|
||||
// console.log('changeImageRect', e)
|
||||
offset.x += e.x || 0;
|
||||
offset.y += e.y || 0;
|
||||
if(e.check && area.checkRange) { // 检查边界
|
||||
var point = checkRange(offset);
|
||||
if(offset.x !== point.x || offset.y !== point.y) {
|
||||
offset = point;
|
||||
}
|
||||
}
|
||||
|
||||
// 因频繁修改 width/height 会造成大量的内存消耗,改为scale
|
||||
// e.instance.imageStyles = {
|
||||
// width: img.width + 'px',
|
||||
// height: img.height + 'px',
|
||||
// transform: 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + ox) + 'px) rotate(' + rotate +'deg)'
|
||||
// };
|
||||
var ox = (img.width - img.oldWidth) / 2;
|
||||
var oy = (img.height - img.oldHeight) / 2;
|
||||
// e.instance.imageStyles = {
|
||||
// width: img.oldWidth + 'px',
|
||||
// height: img.oldHeight + 'px',
|
||||
// transform: 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px) rotate(' + rotate +'deg) scale(' + scale + ')'
|
||||
// };
|
||||
setStyle(e.instance, 'imageStyles', {
|
||||
width: img.oldWidth + 'px',
|
||||
height: img.oldHeight + 'px',
|
||||
transform: (img.gpu ? 'translateZ(0) ' : '') + 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px' + ') rotate(' + rotate +'deg) scale(' + scale + ')'
|
||||
});
|
||||
callMethod(e.instance, 'dataChange', {
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
x: offset.x,
|
||||
y: offset.y,
|
||||
rotate: rotate
|
||||
});
|
||||
};
|
||||
/**
|
||||
* 变更裁剪区域布局信息
|
||||
* @param {Object} e 布局信息
|
||||
*/
|
||||
function changeAreaRect(e) {
|
||||
// console.log('changeAreaRect', e)
|
||||
// 变更蒙版样式
|
||||
setStyle(e.instance, 'maskStylesList', [
|
||||
{
|
||||
left: 0,
|
||||
width: (area.left + areaOffset.left) + 'px',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
'z-index': area.zIndex + 2
|
||||
},
|
||||
{
|
||||
left: (area.right + areaOffset.right) + 'px',
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
'z-index': area.zIndex + 2
|
||||
},
|
||||
{
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
top: 0,
|
||||
height: (area.top + areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 2
|
||||
},
|
||||
{
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
top: (area.bottom + areaOffset.bottom) + 'px',
|
||||
// height: (area.top - areaOffset.bottom + sys.offsetBottom) + 'px',
|
||||
bottom: 0,
|
||||
'z-index': area.zIndex + 2
|
||||
}
|
||||
]);
|
||||
// 变更边框样式
|
||||
if(area.showBorder) {
|
||||
setStyle(e.instance, 'borderStyles', {
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
top: (area.top + areaOffset.top) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
});
|
||||
}
|
||||
|
||||
// 变更参考线样式
|
||||
if(area.showGrid) {
|
||||
setStyle(e.instance, 'gridStylesList', [
|
||||
{
|
||||
'border-width': '1px 0 0 0',
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
right: (area.right + areaOffset.right) + 'px',
|
||||
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) / 3 - 0.5) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '1px 0 0 0',
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
right: (area.right + areaOffset.right) + 'px',
|
||||
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) * 2 / 3 - 0.5) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '0 1px 0 0',
|
||||
top: (area.top + areaOffset.top) + 'px',
|
||||
bottom: (area.bottom + areaOffset.bottom) + 'px',
|
||||
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) / 3 - 0.5) + 'px',
|
||||
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '0 1px 0 0',
|
||||
top: (area.top + areaOffset.top) + 'px',
|
||||
bottom: (area.bottom + areaOffset.bottom) + 'px',
|
||||
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) * 2 / 3 - 0.5) + 'px',
|
||||
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
// 变更四个伸缩角样式
|
||||
if(area.showAngle) {
|
||||
setStyle(e.instance, 'angleStylesList', [
|
||||
{
|
||||
'border-width': area.angleBorderWidth + 'px 0 0 ' + area.angleBorderWidth + 'px',
|
||||
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
|
||||
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0 0',
|
||||
left: (area.right + areaOffset.right - area.angleSize) + 'px',
|
||||
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '0 0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px',
|
||||
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
|
||||
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0',
|
||||
left: (area.right + areaOffset.right - area.angleSize) + 'px',
|
||||
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
// 变更圆角样式
|
||||
if(area.radius > 0) {
|
||||
var radius = area.radius;
|
||||
if(area.width === area.height && area.radius >= area.width / 2) { // 圆形
|
||||
radius = (area.width / 2);
|
||||
} else { // 圆角矩形
|
||||
if(area.width !== area.height) { // 限制圆角半径不能超过短边的一半
|
||||
radius = Math.min(area.width / 2, area.height / 2, radius);
|
||||
}
|
||||
}
|
||||
setStyle(e.instance, 'circleBoxStyles', {
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
top: (area.top + areaOffset.top) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 2
|
||||
});
|
||||
setStyle(e.instance, 'circleStyles', {
|
||||
'box-shadow': '0 0 0 ' + Math.max(area.width, area.height) + 'px rgba(51, 51, 51, 0.8)',
|
||||
'border-radius': radius + 'px'
|
||||
});
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 缩放图片
|
||||
* @param {Object} e 布局信息
|
||||
*/
|
||||
function scaleImage(e) {
|
||||
// console.log('scaleImage', e)
|
||||
var last = scale;
|
||||
scale = Math.min(Math.max(e.scale + scale, minScale), img.maxScale);
|
||||
if(last !== scale) {
|
||||
img.width = num(img.oldWidth * scale);
|
||||
img.height = num(img.oldHeight * scale);
|
||||
// 参考问题:有一个长4000px、宽4000px的四方形ABCD,A点的坐标固定在(-2000,-2000),
|
||||
// 该四边形上有一个点E,坐标为(-100,-300),将该四方形复制一份并缩小到90%后,
|
||||
// 新四边形的A点坐标为多少时可使新四边形的E点与原四边形的E点重合?
|
||||
// 预期效果:从图中选取某点(参照物)为中心点进行缩放,缩放时无论图像怎么变化,该点位置始终固定不变
|
||||
// 计算方法:以相同起点先计算缩放前后两点间的距离,再加上原图像偏移量即可
|
||||
e.x = num((e.x - offset.x) * (1 - scale / last));
|
||||
e.y = num((e.y - offset.y) * (1 - scale / last));
|
||||
changeImageRect(e);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* 获取触摸点在哪个角
|
||||
* @param {number} x 触摸点x轴坐标
|
||||
* @param {number} y 触摸点y轴坐标
|
||||
* @return {number} 角的位置:0=无;1=左上;2=右上;3=左下;4=右下;
|
||||
*/
|
||||
function getToucheAngle(x, y) {
|
||||
// console.log('getToucheAngle', x, y, JSON.stringify(area))
|
||||
var o = area.angleBorderWidth; // 需扩大触发范围则把 o 值加大即可
|
||||
var oy = sys.navigation ? 0 : sys.windowTop;
|
||||
if(y >= area.top - o + oy && y <= area.top + area.angleSize + o + oy) {
|
||||
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
|
||||
return 1; // 左上角
|
||||
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
|
||||
return 2; // 右上角
|
||||
}
|
||||
} else if(y >= area.bottom - area.angleSize - o + oy && y <= area.bottom + o + oy) {
|
||||
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
|
||||
return 3; // 左下角
|
||||
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
|
||||
return 4; // 右下角
|
||||
}
|
||||
}
|
||||
return 0; // 无触摸到角
|
||||
};
|
||||
/**
|
||||
* 重置数据
|
||||
*/
|
||||
function resetData() {
|
||||
offset = { x: 0, y: 0 };
|
||||
scale = 1;
|
||||
minScale = img.minScale;
|
||||
rotate = 0;
|
||||
};
|
||||
function getTouchs(touches) {
|
||||
var result = [];
|
||||
var len = touches ? touches.length : 0
|
||||
for (var i = 0; i < len; i++) {
|
||||
result[i] = {
|
||||
pageX: touches[i].pageX,
|
||||
// h5无标题栏时,窗口顶部距离仍为标题栏高度,且触摸点y轴坐标还是有标题栏的值,即减去标题栏高度的值
|
||||
pageY: touches[i].pageY + sys.windowTop
|
||||
};
|
||||
}
|
||||
return result;
|
||||
};
|
||||
var mouseEvent = false;
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
imageStyles: {},
|
||||
maskStylesList: [{}, {}, {}, {}],
|
||||
borderStyles: {},
|
||||
gridStylesList: [{}, {}, {}, {}],
|
||||
angleStylesList: [{}, {}, {}, {}],
|
||||
circleBoxStyles: {},
|
||||
circleStyles: {}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 监听 PC 端鼠标滚轮
|
||||
// #ifdef H5
|
||||
platform === 'H5' && window.addEventListener('mousewheel', async (e) => {
|
||||
var touchs = getTouchs([e])
|
||||
img.src && scaleImage({
|
||||
instance: await this.getInstance(),
|
||||
check: true,
|
||||
// 鼠标向上滚动时,deltaY 固定 -100,鼠标向下滚动时,deltaY 固定 100
|
||||
scale: e.deltaY > 0 ? -0.05 : 0.05,
|
||||
x: touchs[0].pageX,
|
||||
y: touchs[0].pageY
|
||||
});
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
// #ifdef H5
|
||||
mounted() {
|
||||
platform === 'H5' && this.initH5Events();
|
||||
},
|
||||
// #endif
|
||||
setPlatform(p) {
|
||||
platform = p;
|
||||
},
|
||||
methods: {
|
||||
// #ifdef H5
|
||||
getTouchEvent(e) {
|
||||
e.touches = [
|
||||
{ pageX: e.pageX, pageY: e.pageY }
|
||||
];
|
||||
return e;
|
||||
},
|
||||
initH5Events() {
|
||||
const preview = document.getElementById('pic-preview');
|
||||
preview?.addEventListener('mousedown', (e, ev) => {
|
||||
mouseEvent = true;
|
||||
this.touchstart(this.getTouchEvent(e));
|
||||
});
|
||||
preview?.addEventListener('mousemove', (e) => {
|
||||
if (!mouseEvent) return;
|
||||
this.touchmove(this.getTouchEvent(e));
|
||||
});
|
||||
preview?.addEventListener('mouseup', (e) => {
|
||||
mouseEvent = false;
|
||||
this.touchend(this.getTouchEvent(e))
|
||||
});
|
||||
preview?.addEventListener('mouseleave', (e) => {
|
||||
mouseEvent = false;
|
||||
this.touchend(this.getTouchEvent(e))
|
||||
});
|
||||
},
|
||||
// #endif
|
||||
async getInstance() {
|
||||
// #ifdef APP-PLUS
|
||||
if(platform === 'APP')
|
||||
return this.$ownerInstance
|
||||
? Promise.resolve(this.$ownerInstance)
|
||||
: new Promise((resolve) => {
|
||||
setTimeout(async () => {
|
||||
resolve(await this.getInstance());
|
||||
});
|
||||
});
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
if(platform === 'H5')
|
||||
return Promise.resolve(this);
|
||||
// #endif
|
||||
},
|
||||
/**
|
||||
* 初始化:观察数据变更
|
||||
* @param {Object} newVal 新数据
|
||||
* @param {Object} oldVal 旧数据
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
initObserver: async function(newVal, oldVal, o, i) {
|
||||
// console.log('initObserver', newVal, oldVal, o, i)
|
||||
if(newVal && (!img.src || timestamp !== newVal.timestamp)) {
|
||||
timestamp = newVal.timestamp;
|
||||
img = newVal.img;
|
||||
sys = newVal.sys;
|
||||
area = newVal.area;
|
||||
minScale = img.minScale;
|
||||
resetData();
|
||||
const instance = await this.getInstance()
|
||||
img.src && changeImageRect({
|
||||
instance,
|
||||
x: (sys.windowWidth - img.width) / 2,
|
||||
y: (sys.windowHeight + sys.windowTop - sys.offsetBottom - img.height) / 2
|
||||
});
|
||||
changeAreaRect({
|
||||
instance
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 鼠标滚轮滚动
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
mousewheel: function(e, o) {
|
||||
// h5平台 wheel 事件无法判断滚轮滑动方向,需使用 mousewheel
|
||||
},
|
||||
/**
|
||||
* 触摸开始
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
touchstart: function(e, o) {
|
||||
if(!img.src) return;
|
||||
touches = getTouchs(e.touches);
|
||||
activeAngle = area.showAngle ? getToucheAngle(touches[0].pageX, touches[0].pageY) : 0;
|
||||
if(touches.length === 1 && activeAngle !== 0) {
|
||||
touchType = 'stretch'; // 伸缩裁剪区域
|
||||
} else {
|
||||
touchType = '';
|
||||
}
|
||||
// console.log('touchstart', e, activeAngle)
|
||||
},
|
||||
/**
|
||||
* 触摸移动
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
touchmove: async function(e, o) {
|
||||
if(!img.src) return;
|
||||
// console.log('touchmove', e, o)
|
||||
e.touches = getTouchs(e.touches);
|
||||
if(touchType === 'stretch') { // 触摸四个角进行拉伸
|
||||
var point = e.touches[0];
|
||||
var start = touches[0];
|
||||
var x = point.pageX - start.pageX;
|
||||
var y = point.pageY - start.pageY;
|
||||
if(x !== 0 || y !== 0) {
|
||||
var maxX = num(area.width * (1 - area.minScale));
|
||||
var maxY = num(area.height * (1 - area.minScale));
|
||||
// console.log(x, y, maxX, maxY, offset, area)
|
||||
touches[0] = point;
|
||||
var r = rotate / 90 % 2;
|
||||
var m = r === 1 ? num((img.height - img.width) / 2) : 0; // 宽高差值一半
|
||||
var xCompare = r === 1 ? lessThanByFault(img.height, area.width) : lessThanByFault(img.width, area.width);
|
||||
var yCompare = r === 1 ? lessThanByFault(img.width, area.height) : lessThanByFault(img.height, area.height)
|
||||
var isInclude = xCompare && yCompare;
|
||||
var isIntersect = area.checkRange && (xCompare || yCompare); // 图片是否包含在裁剪区域内
|
||||
var isReverse = !isInclude || num((offset.x - area.left) / area.width) <= num((offset.y - area.top) / area.height) || (area.width > area.height && img.width < img.height && r === 1);
|
||||
switch(activeAngle) {
|
||||
case 1: // 左上角
|
||||
x = num(x + areaOffset.left);
|
||||
y = num(y + areaOffset.top);
|
||||
if(x >= 0 && y >= 0) { // 有效滑动
|
||||
var t = num(offset.y + m - area.top);
|
||||
var l = num(offset.x - m - area.left);
|
||||
// && (offset.x + img.width < area.right || offset.y + img.height < area.bottom)
|
||||
var max = isIntersect && ((l >= 0) || (t >= 0))
|
||||
? minimum(t, l)
|
||||
: false;
|
||||
if(x > y && isReverse) { // 以x轴滑动距离为缩放基准
|
||||
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
|
||||
if(x > maxX) x = maxX;
|
||||
y = num(x * area.height / area.width);
|
||||
} else { // 以y轴滑动距离为缩放基准
|
||||
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
|
||||
if(y > maxY) y = maxY;
|
||||
x = num(y * area.width / area.height);
|
||||
}
|
||||
areaOffset.left = x;
|
||||
areaOffset.top = y;
|
||||
}
|
||||
break;
|
||||
case 2: // 右上角
|
||||
x = num(x + areaOffset.right);
|
||||
y = num(y + areaOffset.top);
|
||||
if(x <= 0 && y >= 0) { // 有效滑动
|
||||
var w = (r === 1 ? img.height : img.width);
|
||||
var t = num(offset.y + m - area.top);
|
||||
var l = num(area.right + m - offset.x - w);
|
||||
var max = isIntersect && ((t >= 0) || (l >= 0))
|
||||
? minimum(t, l)
|
||||
: false;
|
||||
if(-x > y && isReverse) { // 以x轴滑动距离为缩放基准
|
||||
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
|
||||
if(-x > maxX) x = -maxX;
|
||||
y = num(-x * area.height / area.width);
|
||||
} else { // 以y轴滑动距离为缩放基准
|
||||
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
|
||||
if(y > maxY) y = maxY;
|
||||
x = num(-y * area.width / area.height);
|
||||
}
|
||||
areaOffset.right = x;
|
||||
areaOffset.top = y;
|
||||
}
|
||||
break;
|
||||
case 3: // 左下角
|
||||
x += num(x + areaOffset.left);
|
||||
y += num(y + areaOffset.bottom);
|
||||
if(x >= 0 && y <= 0) { // 有效滑动
|
||||
var w = (r === 1 ? img.width : img.height);
|
||||
var t = num(area.bottom - m - offset.y - w);
|
||||
var l = num(offset.x - m - area.left);
|
||||
var max = isIntersect && ((l >= 0) || (t >= 0))
|
||||
? minimum(t, l)
|
||||
: false;
|
||||
if(x > -y && isReverse) { // 以x轴滑动距离为缩放基准
|
||||
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
|
||||
if(x > maxX) x = maxX;
|
||||
y = num(-x * area.height / area.width);
|
||||
} else { // 以y轴滑动距离为缩放基准
|
||||
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
|
||||
if(-y > maxY) y = -maxY;
|
||||
x = num(-y * area.width / area.height);
|
||||
}
|
||||
areaOffset.left = x;
|
||||
areaOffset.bottom = y;
|
||||
}
|
||||
break;
|
||||
case 4: // 右下角
|
||||
x = num(x + areaOffset.right);
|
||||
y = num(y + areaOffset.bottom);
|
||||
if(x <= 0 && y <= 0) { // 有效滑动
|
||||
var w = (r === 1 ? img.height : img.width);
|
||||
var h = (r === 1 ? img.width : img.height);
|
||||
var t = num(area.bottom - offset.y - h - m);
|
||||
var l = num(area.right + m - offset.x - w);
|
||||
var max = isIntersect && ((l >= 0) || (t >= 0))
|
||||
? minimum(t, l)
|
||||
: false;
|
||||
if(-x > -y && isReverse) { // 以x轴滑动距离为缩放基准
|
||||
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
|
||||
if(-x > maxX) x = -maxX;
|
||||
y = num(x * area.height / area.width);
|
||||
} else { // 以y轴滑动距离为缩放基准
|
||||
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
|
||||
if(-y > maxY) y = -maxY;
|
||||
x = num(y * area.width / area.height);
|
||||
}
|
||||
areaOffset.right = x;
|
||||
areaOffset.bottom = y;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// console.log(x, y, JSON.stringify(areaOffset))
|
||||
changeAreaRect({
|
||||
instance: await this.getInstance(),
|
||||
});
|
||||
// this.draw();
|
||||
}
|
||||
} else if (e.touches.length == 2) { // 双点触摸缩放
|
||||
var start = getDistanceByTouches(touches);
|
||||
var end = getDistanceByTouches(e.touches);
|
||||
scaleImage({
|
||||
instance: await this.getInstance(),
|
||||
check: !area.bounce,
|
||||
scale: (end.c - start.c) / 100,
|
||||
x: end.x,
|
||||
y: end.y
|
||||
});
|
||||
touchType = 'scale';
|
||||
} else if(touchType === 'scale') {// 从双点触摸变成单点触摸 / 从缩放变成拖动
|
||||
touchType = 'move';
|
||||
} else {
|
||||
changeImageRect({
|
||||
instance: await this.getInstance(),
|
||||
check: !area.bounce,
|
||||
x: e.touches[0].pageX - touches[0].pageX,
|
||||
y: e.touches[0].pageY - touches[0].pageY
|
||||
});
|
||||
touchType = 'move';
|
||||
}
|
||||
touches = e.touches;
|
||||
},
|
||||
/**
|
||||
* 触摸结束
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
touchend: async function(e, o) {
|
||||
if(!img.src) return;
|
||||
if(touchType === 'stretch') { // 拉伸裁剪区域的四个角缩放
|
||||
// 裁剪区域宽度被缩放到多少
|
||||
var left = areaOffset.left;
|
||||
var right = areaOffset.right;
|
||||
var top = areaOffset.top;
|
||||
var bottom = areaOffset.bottom;
|
||||
var w = area.width + right - left;
|
||||
var h = area.height + bottom - top;
|
||||
// 图像放大倍数
|
||||
var p = scale * (area.width / w) - scale;
|
||||
// 复原裁剪区域
|
||||
areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
|
||||
changeAreaRect({
|
||||
instance: await this.getInstance(),
|
||||
});
|
||||
scaleImage({
|
||||
instance: await this.getInstance(),
|
||||
scale: p,
|
||||
x: area.left + left + (1 === activeAngle || 3 === activeAngle ? w : 0),
|
||||
y: area.top + top + (1 === activeAngle || 2 === activeAngle ? h : 0)
|
||||
});
|
||||
} else if (area.bounce) { // 检查边界并矫正,实现拖动到边界时有回弹效果
|
||||
changeImageRect({
|
||||
instance: await this.getInstance(),
|
||||
check: true
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 顺时针翻转图片90°
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
rotateImage: async function(r) {
|
||||
rotate = (rotate + (r || 90)) % 360;
|
||||
|
||||
if(img.minScale >= 1 && area.checkRange) {
|
||||
// 因图片宽高可能不等,翻转后图片宽高需足够填满裁剪区域
|
||||
minScale = 1;
|
||||
if(img.width < area.height) {
|
||||
minScale = area.height / img.oldWidth;
|
||||
} else if(img.height < area.width) {
|
||||
minScale = area.width / img.oldHeight;
|
||||
}
|
||||
if(minScale !== 1) {
|
||||
scaleImage({
|
||||
instance: await this.getInstance(),
|
||||
scale: minScale - scale,
|
||||
x: sys.windowWidth / 2,
|
||||
y: (sys.windowHeight - sys.offsetBottom) / 2
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 由于拖动画布后会导致图片位置偏移,翻转时的旋转中心点需是图片区域+偏移区域的中心点
|
||||
// 翻转x轴中心点 = (超出裁剪区域右侧的图片宽度 - 超出裁剪区域左侧的图片宽度) / 2
|
||||
// 翻转y轴中心点 = (超出裁剪区域下方的图片宽度 - 超出裁剪区域上方的图片宽度) / 2
|
||||
var ox = ((offset.x + img.width - area.right) - (area.left - offset.x)) / 2;
|
||||
var oy = ((offset.y + img.height - area.bottom) - (area.top - offset.y)) / 2;
|
||||
changeImageRect({
|
||||
instance: await this.getInstance(),
|
||||
check: true,
|
||||
x: -ox - oy,
|
||||
y: -oy + ox
|
||||
});
|
||||
},
|
||||
rotateImage90: function() {
|
||||
this.rotateImage(90)
|
||||
},
|
||||
rotateImage270: function() {
|
||||
this.rotateImage(270)
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,752 @@
|
||||
<template>
|
||||
<view class="image-cropper" :style="{ zIndex }" @wheel="cropper.mousewheel">
|
||||
<canvas v-if="use2d" type="2d" id="imgCanvas" class="img-canvas" :style="{
|
||||
width: `${canvansWidth}px`,
|
||||
height: `${canvansHeight}px`
|
||||
}"></canvas>
|
||||
<canvas v-else id="imgCanvas" canvas-id="imgCanvas" class="img-canvas" :style="{
|
||||
width: `${canvansWidth}px`,
|
||||
height: `${canvansHeight}px`
|
||||
}"></canvas>
|
||||
<view id="pic-preview" class="pic-preview" :change:init="cropper.initObserver" :init="initData" @touchstart="cropper.touchstart" @touchmove="cropper.touchmove" @touchend="cropper.touchend">
|
||||
<image v-if="imgSrc" id="crop-image" class="crop-image" :style="cropper.imageStyles" :src="imgSrc" webp></image>
|
||||
<view v-for="(item, index) in maskList" :key="item.id" :id="item.id" class="crop-mask-block" :style="cropper.maskStylesList[index]"></view>
|
||||
<view v-if="showBorder" id="crop-border" class="crop-border" :style="cropper.borderStyles"></view>
|
||||
<view v-if="radius > 0" id="crop-circle-box" class="crop-circle-box" :style="cropper.circleBoxStyles">
|
||||
<view class="crop-circle" id="crop-circle" :style="cropper.circleStyles"></view>
|
||||
</view>
|
||||
<block v-if="showGrid">
|
||||
<view v-for="(item, index) in gridList" :key="item.id" :id="item.id" class="crop-grid" :style="cropper.gridStylesList[index]"></view>
|
||||
</block>
|
||||
<block v-if="showAngle">
|
||||
<view v-for="(item, index) in angleList" :key="item.id" :id="item.id" class="crop-angle" :style="cropper.angleStylesList[index]">
|
||||
<view :style="[{
|
||||
width: `${angleSize}px`,
|
||||
height: `${angleSize}px`
|
||||
}]"></view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
<slot />
|
||||
<view class="fixed-bottom safe-area-inset-bottom" :style="{ zIndex: initData.area.zIndex + 99 }">
|
||||
<view v-if="(rotatable || reverseRotatable) && !!imgSrc" class="action-bar">
|
||||
<view v-if="reverseRotatable" class="rotate-icon" @click="cropper.rotateImage270"></view>
|
||||
<view v-if="rotatable" class="rotate-icon is-reverse" @click="cropper.rotateImage90"></view>
|
||||
</view>
|
||||
<view v-if="!choosable" class="choose-btn" @click="cropClick">确定</view>
|
||||
<block v-else-if="!!imgSrc">
|
||||
<view class="rechoose" @click="chooseImage">重选</view>
|
||||
<button class="button warn" type="primary" size="mini" @click="cropCancel">取消</button>
|
||||
<button class="button" size="mini" @click="cropClick">确定</button>
|
||||
</block>
|
||||
<view v-else class="choose-btn" @click="chooseImage">选择图片</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- #ifdef APP-VUE -->
|
||||
<script module="cropper" lang="renderjs">
|
||||
import cropper from './qf-image-cropper.render.js';
|
||||
// vue3 app renderjs中条件编译无效
|
||||
cropper.setPlatform('APP');
|
||||
export default {
|
||||
mixins: [ cropper ]
|
||||
}
|
||||
</script>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef H5 -->
|
||||
<script module="cropper" lang="renderjs">
|
||||
import cropper from './qf-image-cropper.render.js';
|
||||
export default {
|
||||
mixins: [ cropper ]
|
||||
}
|
||||
</script>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-WEIXIN || MP-QQ -->
|
||||
<script module="cropper" lang="wxs" src="./qf-image-cropper.wxs"></script>
|
||||
<!-- #endif -->
|
||||
<script>
|
||||
/** 裁剪区域最大宽高所占屏幕宽度百分比 */
|
||||
const AREA_SIZE = 75;
|
||||
/** 图片默认宽高 */
|
||||
const IMG_SIZE = 300;
|
||||
|
||||
export default {
|
||||
name:"qf-image-cropper",
|
||||
// #ifdef MP-WEIXIN
|
||||
options: {
|
||||
// 表示启用样式隔离,在自定义组件内外,使用 class 指定的样式将不会相互影响
|
||||
styleIsolation: "isolated"
|
||||
},
|
||||
// #endif
|
||||
props: {
|
||||
/** 图片资源地址 */
|
||||
src: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/** 裁剪宽度,有些平台或设备对于canvas的尺寸有限制,过大可能会导致无法正常绘制 */
|
||||
width: {
|
||||
type: Number,
|
||||
default: IMG_SIZE
|
||||
},
|
||||
/** 裁剪高度,有些平台或设备对于canvas的尺寸有限制,过大可能会导致无法正常绘制 */
|
||||
height: {
|
||||
type: Number,
|
||||
default: IMG_SIZE
|
||||
},
|
||||
/** 是否绘制裁剪区域边框 */
|
||||
showBorder: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 是否绘制裁剪区域网格参考线 */
|
||||
showGrid: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 是否展示四个支持伸缩的角 */
|
||||
showAngle: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 裁剪区域最小缩放倍数 */
|
||||
areaScale: {
|
||||
type: Number,
|
||||
default: 0.3
|
||||
},
|
||||
/** 图片最小缩放倍数 */
|
||||
minScale: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
/** 图片最大缩放倍数 */
|
||||
maxScale: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
/** 检查图片位置是否超出裁剪边界,如果超出则会矫正位置 */
|
||||
checkRange: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 生成图片背景色:如果裁剪区域没有完全包含在图片中时,不设置该属性生成图片存在一定的透明块 */
|
||||
backgroundColor: {
|
||||
type: String
|
||||
},
|
||||
/** 是否有回弹效果:当 checkRange 为 true 时有效,拖动时可以拖出边界,释放时会弹回边界 */
|
||||
bounce: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 是否支持翻转 */
|
||||
rotatable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 是否支持逆向翻转 */
|
||||
reverseRotatable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 是否支持从本地选择素材 */
|
||||
choosable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/** 是否开启硬件加速,图片缩放过程中如果出现元素的“留影”或“重影”效果,可通过该方式解决或减轻这一问题 */
|
||||
gpu: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 四个角尺寸,单位px */
|
||||
angleSize: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
/** 四个角边框宽度,单位px */
|
||||
angleBorderWidth: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
zIndex: {
|
||||
type: [Number, String]
|
||||
},
|
||||
/** 裁剪图片圆角半径,单位px */
|
||||
radius: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/** 生成文件的类型,只支持 'jpg' 或 'png'。默认为 'png' */
|
||||
fileType: {
|
||||
type: String,
|
||||
default: 'png'
|
||||
},
|
||||
/**
|
||||
* 图片从绘制到生成所需时间,单位ms
|
||||
* 微信小程序平台使用 `Canvas 2D` 绘制时有效
|
||||
* 如绘制大图或出现裁剪图片空白等情况应适当调大该值,因 `Canvas 2d` 采用同步绘制,需自己把控绘制完成时间
|
||||
*/
|
||||
delay: {
|
||||
type: Number,
|
||||
default: 1000
|
||||
},
|
||||
// #ifdef H5
|
||||
/**
|
||||
* 页面是否是原生标题栏
|
||||
* H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 "navigationStyle": "custom" 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差。
|
||||
* 注:因H5平台的窗口高度是包含标题栏的,而屏幕触摸点的坐标是不包含的
|
||||
*/
|
||||
navigation: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
emits: ["crop"],
|
||||
data() {
|
||||
return {
|
||||
// 用不同 id 使 v-for key 不重复
|
||||
maskList: [
|
||||
{ id: 'crop-mask-block-1' },
|
||||
{ id: 'crop-mask-block-2' },
|
||||
{ id: 'crop-mask-block-3' },
|
||||
{ id: 'crop-mask-block-4' },
|
||||
],
|
||||
gridList: [
|
||||
{ id: 'crop-grid-1' },
|
||||
{ id: 'crop-grid-2' },
|
||||
{ id: 'crop-grid-3' },
|
||||
{ id: 'crop-grid-4' },
|
||||
],
|
||||
angleList: [
|
||||
{ id: 'crop-angle-1' },
|
||||
{ id: 'crop-angle-2' },
|
||||
{ id: 'crop-angle-3' },
|
||||
{ id: 'crop-angle-4' },
|
||||
],
|
||||
/** 本地缓存的图片路径 */
|
||||
imgSrc: '',
|
||||
/** 图片的裁剪宽度 */
|
||||
imgWidth: IMG_SIZE,
|
||||
/** 图片的裁剪高度 */
|
||||
imgHeight: IMG_SIZE,
|
||||
/** 裁剪区域最大宽度所占屏幕宽度百分比 */
|
||||
widthPercent: AREA_SIZE,
|
||||
/** 裁剪区域最大高度所占屏幕宽度百分比 */
|
||||
heightPercent: AREA_SIZE,
|
||||
/** 裁剪区域布局信息 */
|
||||
area: {},
|
||||
/** 未被缩放过的图片宽 */
|
||||
oldWidth: 0,
|
||||
/** 未被缩放过的图片高 */
|
||||
oldHeight: 0,
|
||||
/** 系统信息 */
|
||||
sys: uni.getSystemInfoSync(),
|
||||
scaleWidth: 0,
|
||||
scaleHeight: 0,
|
||||
rotate: 0,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
use2d: false,
|
||||
canvansWidth: 0,
|
||||
canvansHeight: 0,
|
||||
// imageStyles: {},
|
||||
// maskStylesList: [{}, {}, {}, {}],
|
||||
// borderStyles: {},
|
||||
// gridStylesList: [{}, {}, {}, {}],
|
||||
// angleStylesList: [{}, {}, {}, {}],
|
||||
// circleBoxStyles: {},
|
||||
// circleStyles: {},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
initData() {
|
||||
// console.log('initData')
|
||||
return {
|
||||
timestamp: new Date().getTime(),
|
||||
area: {
|
||||
...this.area,
|
||||
bounce: this.bounce,
|
||||
showBorder: this.showBorder,
|
||||
showGrid: this.showGrid,
|
||||
showAngle: this.showAngle,
|
||||
angleSize: this.angleSize,
|
||||
angleBorderWidth: this.angleBorderWidth,
|
||||
minScale: this.areaScale,
|
||||
widthPercent: this.widthPercent,
|
||||
heightPercent: this.heightPercent,
|
||||
radius: this.radius,
|
||||
checkRange: this.checkRange,
|
||||
zIndex: +this.zIndex || 0,
|
||||
},
|
||||
sys: this.sys,
|
||||
img: {
|
||||
minScale: this.minScale,
|
||||
maxScale: this.maxScale,
|
||||
src: this.imgSrc,
|
||||
width: this.oldWidth,
|
||||
height: this.oldHeight,
|
||||
oldWidth: this.oldWidth,
|
||||
oldHeight: this.oldHeight,
|
||||
gpu: this.gpu,
|
||||
}
|
||||
}
|
||||
},
|
||||
imgProps() {
|
||||
return {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
src: this.src,
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
imgProps: {
|
||||
handler(val, oldVal) {
|
||||
// 自定义裁剪尺,示例如下:
|
||||
this.imgWidth = Number(val.width) || IMG_SIZE;
|
||||
this.imgHeight = Number(val.height) || IMG_SIZE;
|
||||
let use2d = true;
|
||||
// #ifndef MP-WEIXIN
|
||||
use2d = false;
|
||||
// #endif
|
||||
// if(use2d && (this.imgWidth > 1365 || this.imgHeight > 1365)) {
|
||||
// use2d = false;
|
||||
// }
|
||||
let canvansWidth = this.imgWidth;
|
||||
let canvansHeight = this.imgHeight;
|
||||
let size = Math.max(canvansWidth, canvansHeight)
|
||||
let scalc = 1;
|
||||
if(size > 1365) {
|
||||
scalc = 1365 / size;
|
||||
}
|
||||
this.canvansWidth = canvansWidth * scalc;
|
||||
this.canvansHeight = canvansHeight * scalc;
|
||||
this.use2d = use2d;
|
||||
this.initArea();
|
||||
const src = val.src || this.imgSrc;
|
||||
src && this.initImage(src, oldVal === undefined);
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/** 提供给wxs调用,用来接收图片变更数据 */
|
||||
dataChange(e) {
|
||||
// console.log('dataChange', e)
|
||||
this.scaleWidth = e.width;
|
||||
this.scaleHeight = e.height;
|
||||
this.rotate = e.rotate;
|
||||
this.offsetX = e.x;
|
||||
this.offsetY = e.y;
|
||||
},
|
||||
/** 初始化裁剪区域布局信息 */
|
||||
initArea() {
|
||||
// 底部操作栏高度 = 底部底部操作栏内容高度 + 设备底部安全区域高度
|
||||
this.sys.offsetBottom = uni.upx2px(100) + this.sys.safeAreaInsets.bottom;
|
||||
// #ifndef H5
|
||||
this.sys.windowTop = 0;
|
||||
this.sys.navigation = true;
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
// h5平台的窗口高度是包含标题栏的
|
||||
this.sys.windowTop = this.sys.windowTop || 44;
|
||||
this.sys.navigation = this.navigation;
|
||||
// #endif
|
||||
let wp = this.widthPercent;
|
||||
let hp = this.heightPercent;
|
||||
if (this.imgWidth > this.imgHeight) {
|
||||
hp = hp * this.imgHeight / this.imgWidth;
|
||||
} else if (this.imgWidth < this.imgHeight) {
|
||||
wp = wp * this.imgWidth / this.imgHeight;
|
||||
}
|
||||
const size = this.sys.windowWidth > this.sys.windowHeight ? this.sys.windowHeight : this.sys.windowWidth;
|
||||
const width = size * wp / 100;
|
||||
const height = size * hp / 100;
|
||||
const left = (this.sys.windowWidth - width) / 2;
|
||||
const right = left + width;
|
||||
const top = (this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - height) / 2;
|
||||
const bottom = this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - top;
|
||||
this.area = { width, height, left, right, top, bottom };
|
||||
this.scaleWidth = width;
|
||||
this.scaleHeight = height;
|
||||
},
|
||||
/** 从本地选取图片 */
|
||||
chooseImage(options) {
|
||||
// #ifdef MP-WEIXIN || MP-JD
|
||||
if(uni.chooseMedia) {
|
||||
uni.chooseMedia({
|
||||
...options,
|
||||
count: 1,
|
||||
mediaType: ['image'],
|
||||
success: (res) => {
|
||||
this.resetData();
|
||||
this.initImage(res.tempFiles[0].tempFilePath);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
// #endif
|
||||
uni.chooseImage({
|
||||
...options,
|
||||
count: 1,
|
||||
success: (res) => {
|
||||
this.resetData();
|
||||
this.initImage(res.tempFiles[0].path);
|
||||
}
|
||||
});
|
||||
},
|
||||
/** 重置数据 */
|
||||
resetData() {
|
||||
this.imgSrc = '';
|
||||
this.rotate = 0;
|
||||
this.offsetX = 0;
|
||||
this.offsetY = 0;
|
||||
this.initArea();
|
||||
},
|
||||
/**
|
||||
* 初始化图片信息
|
||||
* @param {String} url 图片链接
|
||||
*/
|
||||
initImage(url, isFirst) {
|
||||
uni.getImageInfo({
|
||||
src: url,
|
||||
success: async (res) => {
|
||||
if (isFirst && this.src === url) await (new Promise((resolve) => setTimeout(resolve, 50)));
|
||||
this.imgSrc = res.path;
|
||||
let scale = res.width / res.height;
|
||||
let areaScale = this.area.width / this.area.height;
|
||||
if (scale > 1) { // 横向图片
|
||||
if (scale >= areaScale) { // 图片宽不小于目标宽,则高固定,宽自适应
|
||||
this.scaleWidth = (this.scaleHeight / res.height) * this.scaleWidth * (res.width / this.scaleWidth);
|
||||
} else { // 否则宽固定、高自适应
|
||||
this.scaleHeight = res.height * this.scaleWidth / res.width;
|
||||
}
|
||||
} else { // 纵向图片
|
||||
if (scale <= areaScale) { // 图片高不小于目标高,宽固定,高自适应
|
||||
this.scaleHeight = (this.scaleWidth / res.width) * this.scaleHeight / (this.scaleHeight / res.height);
|
||||
} else { // 否则高固定,宽自适应
|
||||
this.scaleWidth = res.width * this.scaleHeight / res.height;
|
||||
}
|
||||
}
|
||||
// 记录原始宽高,为缩放比列做限制
|
||||
this.oldWidth = +this.scaleWidth.toFixed(2);
|
||||
this.oldHeight = +this.scaleHeight.toFixed(2);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error(err)
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 剪切图片圆角
|
||||
* @param {Object} ctx canvas 的绘图上下文对象
|
||||
* @param {Number} radius 圆角半径
|
||||
* @param {Number} scale 生成图片的实际尺寸与截取区域比
|
||||
* @param {Function} drawImage 执行剪切时所调用的绘图方法,入参为是否执行了剪切
|
||||
*/
|
||||
drawClipImage(ctx, radius, scale, drawImage) {
|
||||
if(radius > 0) {
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
const w = this.canvansWidth;
|
||||
const h = this.canvansHeight;
|
||||
if(w === h && radius >= w / 2) { // 圆形
|
||||
ctx.arc(w / 2, h / 2, w / 2, 0, 2 * Math.PI);
|
||||
} else { // 圆角矩形
|
||||
if(w !== h) { // 限制圆角半径不能超过短边的一半
|
||||
radius = Math.min(w / 2, h / 2, radius);
|
||||
// radius = Math.min(Math.max(w, h) / 2, radius);
|
||||
}
|
||||
ctx.moveTo(radius, 0);
|
||||
ctx.arcTo(w, 0, w, h, radius);
|
||||
ctx.arcTo(w, h, 0, h, radius);
|
||||
ctx.arcTo(0, h, 0, 0, radius);
|
||||
ctx.arcTo(0, 0, w, 0, radius);
|
||||
ctx.closePath();
|
||||
}
|
||||
ctx.clip();
|
||||
drawImage && drawImage(true);
|
||||
ctx.restore();
|
||||
} else {
|
||||
drawImage && drawImage(false);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 旋转图片
|
||||
* @param {Object} ctx canvas 的绘图上下文对象
|
||||
* @param {Number} rotate 旋转角度
|
||||
* @param {Number} scale 生成图片的实际尺寸与截取区域比
|
||||
*/
|
||||
drawRotateImage(ctx, rotate, scale) {
|
||||
if(rotate !== 0) {
|
||||
// 1. 以图片中心点为旋转中心点
|
||||
const x = this.scaleWidth * scale / 2;
|
||||
const y = this.scaleHeight * scale / 2;
|
||||
ctx.translate(x, y);
|
||||
// 2. 旋转画布
|
||||
ctx.rotate(rotate * Math.PI / 180);
|
||||
// 3. 旋转完画布后恢复设置旋转中心时所做的偏移
|
||||
ctx.translate(-x, -y);
|
||||
}
|
||||
},
|
||||
drawImage(ctx, image, callback) {
|
||||
// 生成图片的实际尺寸与截取区域比
|
||||
const scale = this.canvansWidth / this.area.width;
|
||||
if(this.backgroundColor) {
|
||||
if(ctx.setFillStyle) ctx.setFillStyle(this.backgroundColor);
|
||||
else ctx.fillStyle = this.backgroundColor;
|
||||
ctx.fillRect(0, 0, this.canvansWidth, this.canvansHeight);
|
||||
}
|
||||
this.drawClipImage(ctx, this.radius, scale, () => {
|
||||
this.drawRotateImage(ctx, this.rotate, scale);
|
||||
const r = this.rotate / 90;
|
||||
ctx.drawImage(
|
||||
image,
|
||||
[
|
||||
(this.offsetX - this.area.left),
|
||||
(this.offsetY - this.area.top),
|
||||
-(this.offsetX - this.area.left),
|
||||
-(this.offsetY - this.area.top)
|
||||
][r] * scale,
|
||||
[
|
||||
(this.offsetY - this.area.top),
|
||||
-(this.offsetX - this.area.left),
|
||||
-(this.offsetY - this.area.top),
|
||||
(this.offsetX - this.area.left)
|
||||
][r] * scale,
|
||||
this.scaleWidth * scale,
|
||||
this.scaleHeight * scale
|
||||
);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 绘图
|
||||
* @param {Object} canvas
|
||||
* @param {Object} ctx canvas 的绘图上下文对象
|
||||
* @param {String} src 图片路径
|
||||
* @param {Function} callback 开始绘制时回调
|
||||
*/
|
||||
draw2DImage(canvas, ctx, src, callback) {
|
||||
// console.log('draw2DImage', canvas, ctx, src, callback)
|
||||
if(canvas) {
|
||||
const image = canvas.createImage();
|
||||
image.onload = () => {
|
||||
this.drawImage(ctx, image);
|
||||
// 如果觉得`生成时间过长`或`出现生成图片空白`可尝试调整延迟时间
|
||||
callback && setTimeout(callback, this.delay);
|
||||
};
|
||||
image.onerror = (err) => {
|
||||
console.error(err)
|
||||
uni.hideLoading();
|
||||
};
|
||||
image.src = src;
|
||||
} else {
|
||||
this.drawImage(ctx, src);
|
||||
setTimeout(() => {
|
||||
ctx.draw(false, callback);
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 画布转图片到本地缓存
|
||||
* @param {Object} canvas
|
||||
* @param {String} canvasId
|
||||
*/
|
||||
canvasToTempFilePath(canvas, canvasId) {
|
||||
// console.log('canvasToTempFilePath', canvas, canvasId)
|
||||
uni.canvasToTempFilePath({
|
||||
canvas,
|
||||
canvasId,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: this.canvansWidth,
|
||||
height: this.canvansHeight,
|
||||
destWidth: this.imgWidth, // 必要,保证生成图片宽度不受设备分辨率影响
|
||||
destHeight: this.imgHeight, // 必要,保证生成图片高度不受设备分辨率影响
|
||||
fileType: this.fileType, // 目标文件的类型,默认png
|
||||
success: (res) => {
|
||||
// 生成的图片临时文件路径
|
||||
this.handleImage(res.tempFilePath);
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '裁剪失败,生成图片异常!', icon: 'none' });
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
cropCancel(){
|
||||
this.$emit('cancel');
|
||||
},
|
||||
/** 确认裁剪 */
|
||||
cropClick() {
|
||||
uni.showLoading({ title: '裁剪中...', mask: true });
|
||||
if(!this.use2d) {
|
||||
const ctx = uni.createCanvasContext('imgCanvas', this);
|
||||
ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
|
||||
this.draw2DImage(null, ctx, this.imgSrc, () => {
|
||||
this.canvasToTempFilePath(null, 'imgCanvas');
|
||||
});
|
||||
return;
|
||||
}
|
||||
// #ifdef MP-WEIXIN
|
||||
const query = uni.createSelectorQuery().in(this);
|
||||
query.select('#imgCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res) => {
|
||||
const canvas = res[0].node;
|
||||
|
||||
const dpr = uni.getSystemInfoSync().pixelRatio;
|
||||
canvas.width = res[0].width * dpr;
|
||||
canvas.height = res[0].height * dpr;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.scale(dpr, dpr);
|
||||
ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
|
||||
|
||||
this.draw2DImage(canvas, ctx, this.imgSrc, () => {
|
||||
this.canvasToTempFilePath(canvas);
|
||||
});
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
handleImage(tempFilePath){
|
||||
// 在H5平台下,tempFilePath 为 base64
|
||||
// console.log(tempFilePath)
|
||||
uni.hideLoading();
|
||||
this.$emit('crop', { tempFilePath });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.image-cropper {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #000;
|
||||
.img-canvas {
|
||||
position: absolute !important;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
.pic-preview {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
.crop-mask-block {
|
||||
background-color: rgba(51, 51, 51, 0.8);
|
||||
z-index: 2;
|
||||
position: fixed;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
}
|
||||
.crop-circle-box {
|
||||
position: fixed;
|
||||
box-sizing: border-box;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
.crop-circle {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.crop-image {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
display: block !important;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
.crop-border {
|
||||
position: fixed;
|
||||
border: 1px solid #fff;
|
||||
box-sizing: border-box;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
}
|
||||
.crop-grid {
|
||||
position: fixed;
|
||||
z-index: 3;
|
||||
border-style: dashed;
|
||||
border-color: #fff;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.crop-angle {
|
||||
position: fixed;
|
||||
z-index: 3;
|
||||
border-style: solid;
|
||||
border-color: #fff;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-bottom {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: $uni-bg-color-grey;
|
||||
|
||||
.action-bar {
|
||||
position: absolute;
|
||||
top: -90rpx;
|
||||
left: 10rpx;
|
||||
display: flex;
|
||||
.rotate-icon {
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAABCFJREFUaEPtml3IpVMUx3//ko/ChTIyiGFSMyhllI8bc4F85yuNC2FCqLmQC1+FZORiEkUMNW7UjKjJULgxV+NzSkxDhEkZgwsyigv119J63p7zvOc8z37OmXdOb51dz82711r7/99r7bXXXucVi3xokeNnRqCvB20fDmwAlgK/5bcD+FTSr33tHXQP2H4MeHQE0A+B5yRtLiUyDQJrgVc6AAaBpyV93kXkoBMIQLbfBS5NcK8BRwDXNcD+AdwnaVMbiWkRCPBBohpxHuK7M7865sclRdgNHVMhkF6IMIpwirFEUhzo8M7lwIvASTXEqyVtH8ZgagQSbOzsDknv18HZXpHn5IL8+94IOUm7miSmSqAttjPdbgGuTrnNktYsGgLpoYuAD2qg1zRTbG8P2D4SOC6/Q7vSHPALsE/S7wWy80RsPw/ckxMfSTq/LtRJwPbxwF3ASiCUTxwHCPAnEBfVF8AWSTtL7Ng+LfWOTfmlkn6udFsJ5K15R6a4kvX6yGyUFBvTOWzHXXFzCt4g6c1OArYj9iIGh43YgR+BvztXh1PSa4cMkd0jaVmXDduPAE+k3HpJD7cSGFKvfAc8FQUX8IOk/V2L1udtB/hTgdOBW4Aba/M7Ja1qs2f7euCNlHlZUlx4/495IWQ7Jl+qGbxX0gt9AHfJ2o6zFBVoNVrDKe+F3Sm8VdK1bQQ+A85JgXckXdkFaJx527cC9TpnVdvBtl3h2iapuhsGPdBw1b9xnUvaNw7AEh3bnwDnpuwGSfeP0rN9NvAMELXRXFkxEEK2nwQeSiOtRVQJwC4Z29cAW1Nuu6TVXTrN+SaBt4ErUug2Sa/2NdhH3vZy4NvU2S/p6D768w5xI3WOrAD7LtISFpGdIhVXKfaYvjd20wP13L9M0p4DBbaFRKToSLExVkr6qs+aIwlI6iwz+izUQqC+ab29PiMwqRcmPXczD8w8MFj1zg7xXEqbpdHCw7FgWSjafZL+KcQxtpjteCeflwYulFR/J3TabSslVkj6utPChAK2f6q9uZdLitKieLQRuExSvX9ZbLRUMFs09efpUZL+KtUfVo1GW/umNHC3pOhRLtiwfSbwZS6wV9IJfRdreuBBYH0a2STp9r4G+8jbXgc8mzoDT8VSO00ClwDv1ZR7XyylC4ec7ejaLUmdsV6Aw7oSbwFXpdFdks7qA6pU1na0aR6owgeIR/1cx63UzjAC0YXYVjMQHlkn6ZtSo21ytuPZGKFagQ/xsXZ/3iGuFrYdjafXG0DiQMeBi47c9/GV3BO247UV38n5o0UAP6xmu7jFOGxjRr66On5NPBDOCBsDTapxjHY1dyOcolNXnYlx1himE53p2PmNkxosevfavhg4Izt2k7TXPwZ2S6p6QZPin/2rwcQ7OKmBohCadJGF1P8PG6aaQBKVX/8AAAAASUVORK5CYII=');
|
||||
background-size: 60% 60%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
&.is-reverse {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rechoose {
|
||||
color: $uni-color-primary;
|
||||
padding: 0 $uni-spacing-row-lg;
|
||||
line-height: 100rpx;
|
||||
}
|
||||
|
||||
.choose-btn {
|
||||
color: $uni-color-primary;
|
||||
text-align: center;
|
||||
line-height: 100rpx;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin: auto $uni-spacing-row-lg auto auto;
|
||||
background-color: $uni-color-success;
|
||||
color: #fff;
|
||||
}
|
||||
.warn {
|
||||
margin: auto $uni-spacing-row-lg auto auto;
|
||||
background-color:$uni-color-error;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.safe-area-inset-bottom {
|
||||
padding-bottom: 0;
|
||||
padding-bottom: constant(safe-area-inset-bottom); // 兼容 IOS<11.2
|
||||
padding-bottom: env(safe-area-inset-bottom); // 兼容 IOS>=11.2
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,727 @@
|
||||
/**
|
||||
* 图片编辑器-手势监听
|
||||
* 1. wxs 暂不支持 es6 语法
|
||||
* 2. 支持编译到微信小程序、QQ小程序、app-vue、H5上(uni-app 2.2.5及以上版本)
|
||||
*/
|
||||
/** 图片偏移量 */
|
||||
var offset = { x: 0, y: 0 };
|
||||
/** 图片缩放比例 */
|
||||
var scale = 1;
|
||||
/** 图片最小缩放比例 */
|
||||
var minScale = 1;
|
||||
/** 图片旋转角度 */
|
||||
var rotate = 0;
|
||||
/** 触摸点 */
|
||||
var touches = [];
|
||||
/** 图片布局信息 */
|
||||
var img = {};
|
||||
/** 系统信息 */
|
||||
var sys = {};
|
||||
/** 裁剪区域布局信息 */
|
||||
var area = {};
|
||||
/** 触摸行为类型 */
|
||||
var touchType = '';
|
||||
/** 操作角的位置 */
|
||||
var activeAngle = 0;
|
||||
/** 裁剪区域布局信息偏移量 */
|
||||
var areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
|
||||
/** 容错值 */
|
||||
var fault = 0.000001;
|
||||
/**
|
||||
* 获取a、b两数中的最小正数
|
||||
* @param a
|
||||
* @param b
|
||||
*/
|
||||
function minimum(a, b) {
|
||||
if (a > 0 && b < 0) return a;
|
||||
if (a < 0 && b > 0) return b;
|
||||
if (a > 0 && b > 0) return Math.min(a, b);
|
||||
return 0;
|
||||
}
|
||||
/**
|
||||
* 在容错访问内获取n近似值
|
||||
* @param n
|
||||
*/
|
||||
function num(n) {
|
||||
var m = parseFloat((n).toFixed(6));
|
||||
return m === fault || m === -fault ? 0 : m;
|
||||
}
|
||||
/**
|
||||
* 比较a值在容错值范围内是否等于b值
|
||||
* @param a
|
||||
* @param b
|
||||
*/
|
||||
function equalsByFault(a, b) {
|
||||
return Math.abs(a - b) <= fault;
|
||||
}
|
||||
/**
|
||||
* 比较a值在容错值范围内是否小于b值
|
||||
* @param a
|
||||
* @param b
|
||||
*/
|
||||
function lessThanByFault(a, b) {
|
||||
var c = a - b;
|
||||
return c < 0 ? c < -fault : c < fault;
|
||||
}
|
||||
/**
|
||||
* 验证并获取有效最大值
|
||||
* @param v
|
||||
* @param max
|
||||
* @param isInclude
|
||||
* @param x
|
||||
* @param y
|
||||
* @param rate
|
||||
* @returns
|
||||
*/
|
||||
function validMax(v, max, isInclude, x, y, rate) {
|
||||
if(typeof max === 'number') {
|
||||
if(isInclude && equalsByFault(max, y)) { // 宽高不等时,x轴用y轴值要做等比例转换
|
||||
var n = num(max * rate);
|
||||
if (n <= x) return n; // 转化后值在x轴最大值范围内
|
||||
return x; // 转化后值超出x轴最大值范围则用最大值
|
||||
}
|
||||
return max;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
/**
|
||||
* 计算两点间距
|
||||
* @param {Object} touches 触摸点信息
|
||||
*/
|
||||
function getDistanceByTouches(touches) {
|
||||
// 根据勾股定理求两点间距离
|
||||
var a = touches[1].pageX - touches[0].pageX;
|
||||
var b = touches[1].pageY - touches[0].pageY;
|
||||
var c = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
|
||||
// 求两点间的中点坐标
|
||||
// 1. a、b可能为负值
|
||||
// 2. 在求a、b时,如用touches[1]减touches[0],则求中点坐标也得用touches[1]减a/2、b/2
|
||||
// 3. 同理,在求a、b时,也可用touches[0]减touches[1],则求中点坐标也得用touches[0]减a/2、b/2
|
||||
var x = touches[1].pageX - a / 2;
|
||||
var y = touches[1].pageY - b / 2;
|
||||
return { c, x, y };
|
||||
};
|
||||
/**
|
||||
* 修正取值
|
||||
* @param {Object} a
|
||||
* @param {Object} b
|
||||
* @param {Object} c
|
||||
* @param {Object} reverse 是否反向
|
||||
*/
|
||||
function correctValue(a, b, c, reverse) {
|
||||
return num(reverse ? Math.max(Math.min(a, b), c) : Math.min(Math.max(a, b), c));
|
||||
}
|
||||
|
||||
/**
|
||||
* 旋转90°或270°时检查边界:限制 x、y 拖动范围,禁止滑出边界
|
||||
* @param {Object} e 点坐标
|
||||
* @param {Object} xReverse x是否反向
|
||||
* @param {Object} yReverse y是否反向
|
||||
*/
|
||||
function checkRotateRange(e, xReverse, yReverse) {
|
||||
var o = num((img.height - img.width) / 2); // 宽高差值一半
|
||||
return {
|
||||
x: correctValue(e.x, -img.height + o + area.width + area.left, area.left + o, xReverse),
|
||||
y: correctValue(e.y, -img.width - o + area.height + area.top, area.top - o, yReverse)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查边界:限制 x、y 拖动范围,禁止滑出边界
|
||||
* @param {Object} e 点坐标
|
||||
*/
|
||||
function checkRange(e) {
|
||||
var r = rotate / 90 % 2;
|
||||
if(r === 1) { // 因图片宽高可能不等,翻转 90° 或 270° 后图片宽高需反着计算,且左右和上下边界要根据差值做偏移
|
||||
if (area.width === area.height) {
|
||||
return checkRotateRange(e, img.height < area.height, img.width < area.width);
|
||||
}
|
||||
var isInclude = img.height < area.width && img.width < area.height; // 图片是否包含在裁剪区域内
|
||||
if (img.width < area.height || img.height < area.width) {
|
||||
if (area.width < area.height && img.width < img.height) {
|
||||
return isInclude
|
||||
? checkRotateRange(e, area.width < area.height, area.width < area.height)
|
||||
: checkRotateRange(e, false, true);
|
||||
}
|
||||
if (area.height < area.width && img.height < img.width) {
|
||||
return isInclude
|
||||
? checkRotateRange(e, area.height < area.width, area.height < area.width)
|
||||
: checkRotateRange(e, true, false);
|
||||
}
|
||||
}
|
||||
if (img.height >= area.width && img.width >= area.height) {
|
||||
return checkRotateRange(e, false, false);
|
||||
}
|
||||
if (isInclude) {
|
||||
return area.height < area.width
|
||||
? checkRotateRange(e, true, true)
|
||||
: checkRotateRange(e, area.width < area.height, area.width < area.height);
|
||||
}
|
||||
if (img.height < area.width && !img.width < area.height) {
|
||||
return checkRotateRange(e, true, false);
|
||||
}
|
||||
if (!img.height < area.width && img.width < area.height) {
|
||||
return checkRotateRange(e, false, true);
|
||||
}
|
||||
return checkRotateRange(e, img.height < area.height, img.width < area.width);
|
||||
}
|
||||
return {
|
||||
x: correctValue(e.x, -img.width + area.width + area.left, area.left, img.width < area.width),
|
||||
y: correctValue(e.y, -img.height + area.height + area.top, area.top, img.height < area.height)
|
||||
};
|
||||
};
|
||||
/**
|
||||
* 变更图片布局信息
|
||||
* @param {Object} e 布局信息
|
||||
*/
|
||||
function changeImageRect(e) {
|
||||
offset.x += e.x || 0;
|
||||
offset.y += e.y || 0;
|
||||
var image = e.instance.selectComponent('.crop-image');
|
||||
if(e.check && area.checkRange) { // 检查边界
|
||||
var point = checkRange(offset);
|
||||
if(offset.x !== point.x || offset.y !== point.y) {
|
||||
offset = point;
|
||||
}
|
||||
}
|
||||
// image.setStyle({
|
||||
// width: img.width + 'px',
|
||||
// height: img.height + 'px',
|
||||
// transform: 'translate(' + offset.x + 'px, ' + offset.y + 'px) rotate(' + rotate +'deg)'
|
||||
// });
|
||||
var ox = (img.width - img.oldWidth) / 2;
|
||||
var oy = (img.height - img.oldHeight) / 2;
|
||||
image.setStyle({
|
||||
width: img.oldWidth + 'px',
|
||||
height: img.oldHeight + 'px',
|
||||
transform: (img.gpu ? 'translateZ(0) ' : '') + 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px) rotate(' + rotate +'deg) scale(' + scale + ')'
|
||||
});
|
||||
|
||||
e.instance.callMethod('dataChange', {
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
x: offset.x,
|
||||
y: offset.y,
|
||||
rotate: rotate
|
||||
});
|
||||
};
|
||||
/**
|
||||
* 变更裁剪区域布局信息
|
||||
* @param {Object} e 布局信息
|
||||
*/
|
||||
function changeAreaRect(e) {
|
||||
// 变更蒙版样式
|
||||
var masks = e.instance.selectAllComponents('.crop-mask-block');
|
||||
var maskStyles = [
|
||||
{
|
||||
left: 0,
|
||||
width: (area.left + areaOffset.left) + 'px',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
'z-index': area.zIndex + 2
|
||||
},
|
||||
{
|
||||
left: (area.right + areaOffset.right) + 'px',
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
'z-index': area.zIndex + 2
|
||||
},
|
||||
{
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
top: 0,
|
||||
height: (area.top + areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 2
|
||||
},
|
||||
{
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
top: (area.bottom + areaOffset.bottom) + 'px',
|
||||
// height: (area.top - areaOffset.bottom + sys.offsetBottom) + 'px',
|
||||
bottom: 0,
|
||||
'z-index': area.zIndex + 2
|
||||
}
|
||||
];
|
||||
var len = masks.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
masks[i].setStyle(maskStyles[i]);
|
||||
}
|
||||
|
||||
// 变更边框样式
|
||||
if(area.showBorder) {
|
||||
var border = e.instance.selectComponent('.crop-border');
|
||||
border.setStyle({
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
top: (area.top + areaOffset.top) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
});
|
||||
}
|
||||
|
||||
// 变更参考线样式
|
||||
if(area.showGrid) {
|
||||
var grids = e.instance.selectAllComponents('.crop-grid');
|
||||
var gridStyles = [
|
||||
{
|
||||
'border-width': '1px 0 0 0',
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
right: (area.right + areaOffset.right) + 'px',
|
||||
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) / 3 - 0.5) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '1px 0 0 0',
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
right: (area.right + areaOffset.right) + 'px',
|
||||
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) * 2 / 3 - 0.5) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '0 1px 0 0',
|
||||
top: (area.top + areaOffset.top) + 'px',
|
||||
bottom: (area.bottom + areaOffset.bottom) + 'px',
|
||||
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) / 3 - 0.5) + 'px',
|
||||
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '0 1px 0 0',
|
||||
top: (area.top + areaOffset.top) + 'px',
|
||||
bottom: (area.bottom + areaOffset.bottom) + 'px',
|
||||
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) * 2 / 3 - 0.5) + 'px',
|
||||
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
}
|
||||
];
|
||||
var len = grids.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
grids[i].setStyle(gridStyles[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 变更四个伸缩角样式
|
||||
if(area.showAngle) {
|
||||
var angles = e.instance.selectAllComponents('.crop-angle');
|
||||
var angleStyles = [
|
||||
{
|
||||
'border-width': area.angleBorderWidth + 'px 0 0 ' + area.angleBorderWidth + 'px',
|
||||
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
|
||||
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0 0',
|
||||
left: (area.right + areaOffset.right - area.angleSize) + 'px',
|
||||
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '0 0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px',
|
||||
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
|
||||
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
},
|
||||
{
|
||||
'border-width': '0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0',
|
||||
left: (area.right + areaOffset.right - area.angleSize) + 'px',
|
||||
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
|
||||
'z-index': area.zIndex + 3
|
||||
}
|
||||
];
|
||||
var len = angles.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
angles[i].setStyle(angleStyles[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 变更圆角样式
|
||||
if(area.radius > 0) {
|
||||
var circleBox = e.instance.selectComponent('.crop-circle-box');
|
||||
var circle = e.instance.selectComponent('.crop-circle');
|
||||
var radius = area.radius;
|
||||
if(area.width === area.height && area.radius >= area.width / 2) { // 圆形
|
||||
radius = (area.width / 2);
|
||||
} else { // 圆角矩形
|
||||
if(area.width !== area.height) { // 限制圆角半径不能超过短边的一半
|
||||
radius = Math.min(area.width / 2, area.height / 2, radius);
|
||||
}
|
||||
}
|
||||
circleBox.setStyle({
|
||||
left: (area.left + areaOffset.left) + 'px',
|
||||
top: (area.top + areaOffset.top) + 'px',
|
||||
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
|
||||
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
|
||||
'z-index': area.zIndex + 2
|
||||
});
|
||||
circle.setStyle({
|
||||
'box-shadow': '0 0 0 ' + Math.max(area.width, area.height) + 'px rgba(51, 51, 51, 0.8)',
|
||||
'border-radius': radius + 'px'
|
||||
});
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 缩放图片
|
||||
* @param {Object} e 布局信息
|
||||
*/
|
||||
function scaleImage(e) {
|
||||
var last = scale;
|
||||
scale = Math.min(Math.max(e.scale + scale, minScale), img.maxScale);
|
||||
if(last !== scale) {
|
||||
img.width = num(img.oldWidth * scale);
|
||||
img.height = num(img.oldHeight * scale);
|
||||
// 参考问题:有一个长4000px、宽4000px的四方形ABCD,A点的坐标固定在(-2000,-2000),
|
||||
// 该四边形上有一个点E,坐标为(-100,-300),将该四方形复制一份并缩小到90%后,
|
||||
// 新四边形的A点坐标为多少时可使新四边形的E点与原四边形的E点重合?
|
||||
// 预期效果:从图中选取某点(参照物)为中心点进行缩放,缩放时无论图像怎么变化,该点位置始终固定不变
|
||||
// 计算方法:以相同起点先计算缩放前后两点间的距离,再加上原图像偏移量即可
|
||||
e.x = num((e.x - offset.x) * (1 - scale / last));
|
||||
e.y = num((e.y - offset.y) * (1 - scale / last));
|
||||
changeImageRect(e);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* 获取触摸点在哪个角
|
||||
* @param {number} x 触摸点x轴坐标
|
||||
* @param {number} y 触摸点y轴坐标
|
||||
* @return {number} 角的位置:0=无;1=左上;2=右上;3=左下;4=右下;
|
||||
*/
|
||||
function getToucheAngle(x, y) {
|
||||
// console.log('getToucheAngle', x, y, JSON.stringify(area))
|
||||
var o = area.angleBorderWidth; // 需扩大触发范围则把 o 值加大即可
|
||||
if(y >= area.top - o && y <= area.top + area.angleSize + o) {
|
||||
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
|
||||
return 1; // 左上角
|
||||
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
|
||||
return 2; // 右上角
|
||||
}
|
||||
} else if(y >= area.bottom - area.angleSize - o && y <= area.bottom + o) {
|
||||
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
|
||||
return 3; // 左下角
|
||||
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
|
||||
return 4; // 右下角
|
||||
}
|
||||
}
|
||||
return 0; // 无触摸到角
|
||||
};
|
||||
/**
|
||||
* 重置数据
|
||||
*/
|
||||
function resetData() {
|
||||
offset = { x: 0, y: 0 };
|
||||
scale = 1;
|
||||
minScale = img.minScale;
|
||||
rotate = 0;
|
||||
};
|
||||
/**
|
||||
* 顺时针翻转图片90°
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
function rotateImage(e, o, r) {
|
||||
rotate = (rotate + r) % 360;
|
||||
if(img.minScale >= 1 && area.checkRange) {
|
||||
// 因图片宽高可能不等,翻转后图片宽高需足够填满裁剪区域
|
||||
minScale = 1;
|
||||
if(img.width < area.height) {
|
||||
minScale = area.height / img.oldWidth;
|
||||
} else if(img.height < area.width) {
|
||||
minScale = area.width / img.oldHeight;
|
||||
}
|
||||
if(minScale !== 1) {
|
||||
scaleImage({
|
||||
instance: o,
|
||||
scale: minScale - scale,
|
||||
x: sys.windowWidth / 2,
|
||||
y: (sys.windowHeight - sys.offsetBottom) / 2
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 由于拖动画布后会导致图片位置偏移,翻转时的旋转中心点需是图片区域+偏移区域的中心点
|
||||
// 翻转x轴中心点 = (超出裁剪区域右侧的图片宽度 - 超出裁剪区域左侧的图片宽度) / 2
|
||||
// 翻转y轴中心点 = (超出裁剪区域下方的图片宽度 - 超出裁剪区域上方的图片宽度) / 2
|
||||
var ox = ((offset.x + img.width - area.right) - (area.left - offset.x)) / 2;
|
||||
var oy = ((offset.y + img.height - area.bottom) - (area.top - offset.y)) / 2;
|
||||
changeImageRect({
|
||||
instance: o,
|
||||
check: true,
|
||||
x: -ox - oy,
|
||||
y: -oy + ox
|
||||
});
|
||||
};
|
||||
module.exports = {
|
||||
/**
|
||||
* 初始化:观察数据变更
|
||||
* @param {Object} newVal 新数据
|
||||
* @param {Object} oldVal 旧数据
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
initObserver: function(newVal, oldVal, o, i) {
|
||||
if(newVal) {
|
||||
img = newVal.img;
|
||||
sys = newVal.sys;
|
||||
area = newVal.area;
|
||||
minScale = img.minScale;
|
||||
resetData();
|
||||
img.src && changeImageRect({
|
||||
instance: o,
|
||||
x: (sys.windowWidth - img.width) / 2,
|
||||
y: (sys.windowHeight - sys.offsetBottom - img.height) / 2
|
||||
});
|
||||
changeAreaRect({
|
||||
instance: o
|
||||
});
|
||||
// console.log('initRect', JSON.stringify(newVal))
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 鼠标滚轮滚动
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
mousewheel: function(e, o) {
|
||||
if(!img.src) return;
|
||||
scaleImage({
|
||||
instance: o,
|
||||
check: true,
|
||||
// 鼠标向上滚动时,deltaY 固定 -100,鼠标向下滚动时,deltaY 固定 100
|
||||
scale: e.detail.deltaY > 0 ? -0.05 : 0.05,
|
||||
x: e.touches[0].pageX,
|
||||
y: e.touches[0].pageY
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 触摸开始
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
touchstart: function(e, o) {
|
||||
if(!img.src) return;
|
||||
touches = e.touches;
|
||||
activeAngle = area.showAngle ? getToucheAngle(touches[0].pageX, touches[0].pageY) : 0;
|
||||
if(touches.length === 1 && activeAngle !== 0) {
|
||||
touchType = 'stretch'; // 伸缩裁剪区域
|
||||
} else {
|
||||
touchType = '';
|
||||
}
|
||||
// console.log('touchstart', JSON.stringify(e), activeAngle)
|
||||
},
|
||||
/**
|
||||
* 触摸移动
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
touchmove: function(e, o) {
|
||||
if(!img.src) return;
|
||||
// console.log('touchmove', JSON.stringify(e), JSON.stringify(o))
|
||||
if(touchType === 'stretch') { // 触摸四个角进行拉伸
|
||||
var point = e.touches[0];
|
||||
var start = touches[0];
|
||||
var x = point.pageX - start.pageX;
|
||||
var y = point.pageY - start.pageY;
|
||||
if(x !== 0 || y !== 0) {
|
||||
var maxX = num(area.width * (1 - area.minScale));
|
||||
var maxY = num(area.height * (1 - area.minScale));
|
||||
// console.log(x, y, maxX, maxY, offset, area)
|
||||
touches[0] = point;
|
||||
var r = rotate / 90 % 2;
|
||||
var m = r === 1 ? num((img.height - img.width) / 2) : 0; // 宽高差值一半
|
||||
var xCompare = r === 1 ? lessThanByFault(img.height, area.width) : lessThanByFault(img.width, area.width);
|
||||
var yCompare = r === 1 ? lessThanByFault(img.width, area.height) : lessThanByFault(img.height, area.height)
|
||||
var isInclude = xCompare && yCompare;
|
||||
var isIntersect = area.checkRange && (xCompare || yCompare); // 图片是否包含在裁剪区域内
|
||||
var isReverse = !isInclude || num((offset.x - area.left) / area.width) <= num((offset.y - area.top) / area.height) || (area.width > area.height && img.width < img.height && r === 1);
|
||||
switch(activeAngle) {
|
||||
case 1: // 左上角
|
||||
x = num(x + areaOffset.left);
|
||||
y = num(y + areaOffset.top);
|
||||
if(x >= 0 && y >= 0) { // 有效滑动
|
||||
var t = num(offset.y + m - area.top);
|
||||
var l = num(offset.x - m - area.left);
|
||||
// && (offset.x + img.width < area.right || offset.y + img.height < area.bottom)
|
||||
var max = isIntersect && ((l >= 0) || (t >= 0))
|
||||
? minimum(t, l)
|
||||
: false;
|
||||
if(x > y && isReverse) { // 以x轴滑动距离为缩放基准
|
||||
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
|
||||
if(x > maxX) x = maxX;
|
||||
y = num(x * area.height / area.width);
|
||||
} else { // 以y轴滑动距离为缩放基准
|
||||
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
|
||||
if(y > maxY) y = maxY;
|
||||
x = num(y * area.width / area.height);
|
||||
}
|
||||
areaOffset.left = x;
|
||||
areaOffset.top = y;
|
||||
}
|
||||
break;
|
||||
case 2: // 右上角
|
||||
x = num(x + areaOffset.right);
|
||||
y = num(y + areaOffset.top);
|
||||
if(x <= 0 && y >= 0) { // 有效滑动
|
||||
var w = (r === 1 ? img.height : img.width);
|
||||
var t = num(offset.y + m - area.top);
|
||||
var l = num(area.right + m - offset.x - w);
|
||||
var max = isIntersect && ((t >= 0) || (l >= 0))
|
||||
? minimum(t, l)
|
||||
: false;
|
||||
// var max = isInclude && ((offset.x > 0 && offset.x + img.width <= area.right) || (offset.y > 0 && offset.y >= area.top))
|
||||
// ? minimum(offset.y - area.top, area.right - offset.x - img.width)
|
||||
// : false;
|
||||
// console.log(offset.x, offset.y, img.width, img.height, area.top, area.right, m, max)
|
||||
// console.log(offset.y + m - area.top, area.right + m - offset.x - w)
|
||||
if(-x > y && isReverse) { // 以x轴滑动距离为缩放基准
|
||||
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
|
||||
if(-x > maxX) x = -maxX;
|
||||
y = num(-x * area.height / area.width);
|
||||
} else { // 以y轴滑动距离为缩放基准
|
||||
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
|
||||
if(y > maxY) y = maxY;
|
||||
x = num(-y * area.width / area.height);
|
||||
}
|
||||
areaOffset.right = x;
|
||||
areaOffset.top = y;
|
||||
}
|
||||
break;
|
||||
case 3: // 左下角
|
||||
x += num(x + areaOffset.left);
|
||||
y += num(y + areaOffset.bottom);
|
||||
if(x >= 0 && y <= 0) { // 有效滑动
|
||||
var w = (r === 1 ? img.width : img.height);
|
||||
var t = num(area.bottom - m - offset.y - w);
|
||||
var l = num(offset.x - m - area.left);
|
||||
var max = isIntersect && ((l >= 0) || (t >= 0))
|
||||
? minimum(t, l)
|
||||
: false;
|
||||
if(x > -y && isReverse) { // 以x轴滑动距离为缩放基准
|
||||
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
|
||||
if(x > maxX) x = maxX;
|
||||
y = num(-x * area.height / area.width);
|
||||
} else { // 以y轴滑动距离为缩放基准
|
||||
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
|
||||
if(-y > maxY) y = -maxY;
|
||||
x = num(-y * area.width / area.height);
|
||||
}
|
||||
areaOffset.left = x;
|
||||
areaOffset.bottom = y;
|
||||
}
|
||||
break;
|
||||
case 4: // 右下角
|
||||
x = num(x + areaOffset.right);
|
||||
y = num(y + areaOffset.bottom);
|
||||
if(x <= 0 && y <= 0) { // 有效滑动
|
||||
var w = (r === 1 ? img.height : img.width);
|
||||
var h = (r === 1 ? img.width : img.height);
|
||||
var t = num(area.bottom - offset.y - h - m);
|
||||
var l = num(area.right + m - offset.x - w);
|
||||
var max = isIntersect && ((l >= 0) || (t >= 0))
|
||||
? minimum(t, l)
|
||||
: false;
|
||||
if(-x > -y && isReverse) { // 以x轴滑动距离为缩放基准
|
||||
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
|
||||
if(-x > maxX) x = -maxX;
|
||||
y = num(x * area.height / area.width);
|
||||
} else { // 以y轴滑动距离为缩放基准
|
||||
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
|
||||
if(-y > maxY) y = -maxY;
|
||||
x = num(y * area.width / area.height);
|
||||
}
|
||||
areaOffset.right = x;
|
||||
areaOffset.bottom = y;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// console.log(x, y, JSON.stringify(areaOffset))
|
||||
changeAreaRect({
|
||||
instance: o,
|
||||
});
|
||||
// this.draw();
|
||||
}
|
||||
} else if (e.touches.length == 2) { // 双点触摸缩放
|
||||
var start = getDistanceByTouches(touches);
|
||||
var end = getDistanceByTouches(e.touches);
|
||||
scaleImage({
|
||||
instance: o,
|
||||
check: !area.bounce,
|
||||
scale: (end.c - start.c) / 100,
|
||||
x: end.x,
|
||||
y: end.y
|
||||
});
|
||||
touchType = 'scale';
|
||||
} else if(touchType === 'scale') {// 从双点触摸变成单点触摸 / 从缩放变成拖动
|
||||
touchType = 'move';
|
||||
} else {
|
||||
changeImageRect({
|
||||
instance: o,
|
||||
check: !area.bounce,
|
||||
x: e.touches[0].pageX - touches[0].pageX,
|
||||
y: e.touches[0].pageY - touches[0].pageY
|
||||
});
|
||||
touchType = 'move';
|
||||
}
|
||||
touches = e.touches;
|
||||
},
|
||||
/**
|
||||
* 触摸结束
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
touchend: function(e, o) {
|
||||
if(!img.src) return;
|
||||
if(touchType === 'stretch') { // 拉伸裁剪区域的四个角缩放
|
||||
// 裁剪区域宽度被缩放到多少
|
||||
var left = areaOffset.left;
|
||||
var right = areaOffset.right;
|
||||
var top = areaOffset.top;
|
||||
var bottom = areaOffset.bottom;
|
||||
var w = area.width + right - left;
|
||||
var h = area.height + bottom - top;
|
||||
// 图像放大倍数
|
||||
var p = scale * (area.width / w) - scale;
|
||||
// 复原裁剪区域
|
||||
areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
|
||||
changeAreaRect({
|
||||
instance: o,
|
||||
});
|
||||
scaleImage({
|
||||
instance: o,
|
||||
scale: p,
|
||||
x: area.left + left + (1 === activeAngle || 3 === activeAngle ? w : 0),
|
||||
y: area.top + top + (1 === activeAngle || 2 === activeAngle ? h : 0)
|
||||
});
|
||||
} else if (area.bounce) { // 检查边界并矫正,实现拖动到边界时有回弹效果
|
||||
changeImageRect({
|
||||
instance: o,
|
||||
check: true
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 顺时针翻转图片90°
|
||||
* @param {Object} e 事件对象
|
||||
* @param {Object} o 组件实例对象
|
||||
*/
|
||||
rotateImage: function(e, o) {
|
||||
rotateImage(e, o, 90);
|
||||
},
|
||||
rotateImage90: function(e, o) {
|
||||
rotateImage(e, o, 90)
|
||||
},
|
||||
rotateImage270: function(e, o) {
|
||||
rotateImage(e, o, 270)
|
||||
},
|
||||
// 此处只用于对齐其他平台端的样式参数,防止异常,无作用
|
||||
imageStyles: '',
|
||||
maskStylesList: ['', '', '', ''],
|
||||
borderStyles: '',
|
||||
gridStylesList: ['', '', '', ''],
|
||||
angleStylesList: ['', '', '', ''],
|
||||
circleBoxStyles: '',
|
||||
circleStyles: '',
|
||||
}
|
||||
81
uni_modules/qf-image-cropper/package.json
Normal file
@ -0,0 +1,81 @@
|
||||
{
|
||||
"id": "qf-image-cropper",
|
||||
"displayName": "图片裁剪插件",
|
||||
"version": "2.2.5",
|
||||
"description": "图片裁剪插件,支持自定义尺寸、定点等比例缩放、拖动、图片翻转、剪切圆形/圆角图片、定制样式,功能多性能高体验好注释全。",
|
||||
"keywords": [
|
||||
"qf-image-cropper",
|
||||
"图片裁剪",
|
||||
"图片编辑",
|
||||
"头像裁剪",
|
||||
"小程序"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.1.0"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "插件不采集任何数据",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"client": {
|
||||
"Vue": {
|
||||
"vue2": "y",
|
||||
"vue3": "y"
|
||||
},
|
||||
"App": {
|
||||
"app-vue": "y",
|
||||
"app-nvue": "n"
|
||||
},
|
||||
"H5-mobile": {
|
||||
"Safari": "y",
|
||||
"Android Browser": "y",
|
||||
"微信浏览器(Android)": "y",
|
||||
"QQ浏览器(Android)": "u"
|
||||
},
|
||||
"H5-pc": {
|
||||
"Chrome": "u",
|
||||
"IE": "u",
|
||||
"Edge": "u",
|
||||
"Firefox": "u",
|
||||
"Safari": "u"
|
||||
},
|
||||
"小程序": {
|
||||
"微信": "y",
|
||||
"阿里": "n",
|
||||
"百度": "n",
|
||||
"字节跳动": "n",
|
||||
"QQ": "u",
|
||||
"钉钉": "n",
|
||||
"快手": "n",
|
||||
"飞书": "n",
|
||||
"京东": "n"
|
||||
},
|
||||
"快应用": {
|
||||
"华为": "n",
|
||||
"联盟": "n"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
97
uni_modules/qf-image-cropper/readme.md
Normal file
@ -0,0 +1,97 @@
|
||||
# qf-image-cropper
|
||||
## 图片裁剪插件
|
||||
uniapp微信小程序图片裁剪插件,支持自定义尺寸、定点等比例缩放、拖动、图片翻转、剪切圆形/圆角图片、定制样式,功能多性能高体验好注释全。
|
||||
|
||||
### 平台支持:
|
||||
1. 支持微信小程序:移动端、PC端、开发者工具
|
||||
2. 支持H5平台(2.1.0版本起)
|
||||
3. 支持APP平台(2.1.5版本起):Android、IOS
|
||||
4. 其他平台暂未测试兼容性未知
|
||||
|
||||
### 支持功能:
|
||||
1. 自定义裁剪尺寸
|
||||
2. 定点等比例缩放:移动端以双指触摸中心点为缩放中心点,PC端以鼠标所在点为缩放中心点
|
||||
3. 自由拖动:支持限制滑出边界,也支持回弹效果(滑动时可滑出边界,释放时回弹到边界)
|
||||
4. 图片翻转:在裁剪尺寸非 1:1 的情况下,翻转时宽高无法铺满裁剪区域时,图片会自动放大到合适尺寸
|
||||
5. 裁剪生成新图片
|
||||
6. 本地选择图片
|
||||
7. 可定制样式:可自由选择是否渲染裁剪边框、可伸缩裁剪顶角、参考线
|
||||
8. 裁剪圆角图片:圆形、圆角矩形
|
||||
|
||||
### 属性说明
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
|:---|:---|:---|:---|
|
||||
| src | String | | 图片资源地址 |
|
||||
| width | Number | 300 | 裁剪宽度 |
|
||||
| height | Number | 300 | 裁剪高度 |
|
||||
| showBorder | Boolean | true | 是否绘制裁剪区域边框 |
|
||||
| showGrid | Boolean | true | 是否绘制裁剪区域网格参考线 |
|
||||
| showAngle | Boolean | true | 是否展示四个支持伸缩的角 |
|
||||
| areaScale | Number | 0.3 | 裁剪区域最小缩放倍数 |
|
||||
| minScale | Number | 1 | 图片最小缩放倍数 |
|
||||
| maxScale | Number | 5 | 图片最大缩放倍数 |
|
||||
| checkRange | Boolean | true | 检查图片位置是否超出裁剪边界,如果超出则会矫正位置 |
|
||||
| backgroundColor | String | | 生成图片背景色:如果裁剪区域没有完全包含在图片中时,不设置该属性则生成图片存在一定的透明块 |
|
||||
| bounce | Boolean | true | 是否有回弹效果:当 checkRange 为 true 时有效,拖动时可以拖出边界,释放时会弹回边界 |
|
||||
| rotatable | Boolean | true | 是否支持翻转 |
|
||||
| reverseRotatable | Boolean | false | 是否支持逆向翻转 |
|
||||
| choosable | Boolean | true | 是否支持从本地选择素材 |
|
||||
| gpu | Boolean | false | 是否开启硬件加速,图片缩放过程中如果出现元素的“留影”或“重影”效果,可通过该方式解决或减轻这一问题 |
|
||||
| angleSize | Number | 20 | 四个角尺寸,单位px |
|
||||
| angleBorderWidth | Number | 2 | 四个角边框宽度,单位px |
|
||||
| zIndex | Number/String | | 调整组件层级 |
|
||||
| radius | Number | | 裁剪图片圆角半径,单位px |
|
||||
| fileType | String | png | 生成文件的类型,只支持 'jpg' 或 'png'。默认为 'png' |
|
||||
| delay | Number | 1000 | 图片从绘制到生成所需时间,单位ms<br>微信小程序平台使用 `Canvas 2D` 绘制时有效<br>如绘制大图或出现裁剪图片空白等情况应适当调大该值,因 `Canvas 2d` 采用同步绘制,需自己把控绘制完成时间 |
|
||||
| navigation | Boolean | true | 页面是否是原生标题栏:<br>H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 `"navigationStyle": "custom"` 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差。<br>注:因H5平台的窗口高度是包含标题栏的,而屏幕触摸点的坐标是不包含的 |
|
||||
| @crop | EventHandle | | 剪裁完成后触发,event = { tempFilePath }。在H5平台下,tempFilePath 为 base64 |
|
||||
|
||||
### 基本用法
|
||||
```
|
||||
<template>
|
||||
<div>
|
||||
<qf-image-cropper :width="500" :height="500" :radius="30" @crop="handleCrop"></qf-image-cropper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import QfImageCropper from '@/components/qf-image-cropper/qf-image-cropper.vue';
|
||||
export default {
|
||||
components: {
|
||||
QfImageCropper
|
||||
},
|
||||
methods: {
|
||||
handleCrop(e) {
|
||||
uni.previewImage({
|
||||
urls: [e.tempFilePath],
|
||||
current: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
通过ref组件实例可在进入页面后直接打开相册选择图片
|
||||
```
|
||||
mounted() {
|
||||
this.$refs.qfImageCropper.chooseImage({ sourceType: ['album'] });
|
||||
}
|
||||
```
|
||||
### 使用说明
|
||||
1.建议在`pages.json`中将引用插件的页面添加一下配置禁止下拉刷新和禁止页面滑动,防止出现性能或页面抖动等问题。
|
||||
```
|
||||
{
|
||||
"enablePullDownRefresh": false,
|
||||
"disableScroll": true
|
||||
}
|
||||
```
|
||||
2.建议使用本插件不要设置过大宽高的目标图片尺寸,建议1365x1365以内,否则可能会导致如下问题:
|
||||
```
|
||||
1.界面卡顿,内存占用过高
|
||||
2.生成图片失真(模糊)
|
||||
3.确定裁剪后一直显示 `裁剪中...`,该问题是由 `uni.canvasToTempFilePath` 无法回调导致,不同平台不同设备限制可能有所不同。
|
||||
```
|
||||
3.如裁剪后的图片存在偏移的问题,请检查是否受自己项目中父组件或全局样式影响。
|
||||
4.src属性设置网络图片时,图片资源必须是能触发 `getImageInfo` API 的 success 回调才可用于插件裁剪。因此小程序平台获取网络图片信息需先配置download域名白名单才能生效。
|
||||
5.如果组件无法正常渲染且使用了 `v-if` 时,可尝试将 `v-if` 替换为 `v-show`
|
||||
6.如果App端导入组件后无法正常渲染,请尝试重新运行
|
||||
35
uni_modules/sv-editor/changelog.md
Normal file
@ -0,0 +1,35 @@
|
||||
## 1.1.2(2025-04-14)
|
||||
1. 更新视频封面图片地址,之前的已失效
|
||||
2. 更新文档
|
||||
## 1.1.1(2025-03-19)
|
||||
1. 更新vue2环境下状态销毁异常的问题
|
||||
## 1.1.0(2025-03-07)
|
||||
1. 更新状态销毁逻辑
|
||||
## 1.0.9(2025-01-18)
|
||||
1. 修复调色板在微信小程序vue2环境下的问题
|
||||
## 1.0.8(2025-01-18)
|
||||
1. 修复了微信小程序在vue2环境下的报错
|
||||
## 1.0.7(2025-01-18)
|
||||
1. 修复了微信小程序在vue2环境下出现的报错
|
||||
## 1.0.6(2024-12-17)
|
||||
1. 优化了ios端兼容性问题
|
||||
2. 更新示例工程和文档
|
||||
## 1.0.5(2024-12-17)
|
||||
1. 更新文档
|
||||
## 1.0.4(2024-12-17)
|
||||
1. 新增扩展功能
|
||||
2. 更新文档
|
||||
3. 更新示例工程
|
||||
## 1.0.3(2024-12-11)
|
||||
1. 优化了多编辑器实例模式,现在单页面可以存在多个编辑器了
|
||||
2. 更新了文档与示例工程,多实例可以参考示例二
|
||||
## 1.0.2(2024-12-10)
|
||||
1. 添加调色板功能
|
||||
2. 预设更多样式格式
|
||||
3. 更新示例工程和文档
|
||||
## 1.0.1(2024-12-06)
|
||||
1. v1正式版发布
|
||||
2. 更新文档
|
||||
3. 上传示例工程
|
||||
## 1.0.0(2024-11-29)
|
||||
1. 基于uni-editor的仿知乎富文本初稿
|
||||
656
uni_modules/sv-editor/components/backup/sv-editor-plugin.vue
Normal file
@ -0,0 +1,656 @@
|
||||
<template>
|
||||
<text
|
||||
:eid="eid"
|
||||
:change:eid="quillEditor.watchEID"
|
||||
:sid="sid"
|
||||
:change:sid="quillEditor.watchStartID"
|
||||
:video="videoUrl"
|
||||
:change:pastemode="quillEditor.watchPasteMode"
|
||||
:pastemode="pastemode"
|
||||
:change:video="quillEditor.watchVideoUrl"
|
||||
:cover="coverUrl"
|
||||
:change:cover="quillEditor.watchCoverUrl"
|
||||
:coverios="coverUrlIOS"
|
||||
:change:coverios="quillEditor.watchCoverUrlIOS"
|
||||
></text>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 富文本plugin特殊扩展
|
||||
* @author sonve
|
||||
* @version 1.0.0
|
||||
* @date 2024-12-14
|
||||
*/
|
||||
|
||||
export default {
|
||||
props: {
|
||||
sid: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
eid: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
videoUrl: '', // 视频地址
|
||||
coverUrl: '', // 封面地址
|
||||
coverUrlIOS: '', // ios端封面地址
|
||||
pastemode: 'text' // 粘贴模式 text | origin
|
||||
}
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
changePasteMode(e) {
|
||||
this.pastemode = e
|
||||
},
|
||||
editorPaste(e) {
|
||||
this.$emit('epaste', e)
|
||||
},
|
||||
createVideoThumbnail(url) {
|
||||
this.videoUrl = url
|
||||
},
|
||||
getVideoThumbnail(e) {
|
||||
// e: { video, cover }
|
||||
uni.$emit(`E_EDITOR_GET_VIDEO_THUMBNAIL_${e.video}`, e)
|
||||
},
|
||||
createCoverThumbnail(url) {
|
||||
// #ifdef H5
|
||||
this.coverUrl = url
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
const isIOS = uni.getSystemInfoSync().platform == 'ios'
|
||||
if (isIOS) {
|
||||
this.coverUrlIOS = url // iOS用不了OffscreenCanvas
|
||||
} else {
|
||||
this.coverUrl = url
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
getCoverThumbnail(e) {
|
||||
// e: { image, cover }
|
||||
uni.$emit(`E_EDITOR_GET_COVER_THUMBNAIL_${e.image}`, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script module="quillEditor" lang="renderjs">
|
||||
import config from '../common/config.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
editorID: '',
|
||||
idStack: [], // sid栈
|
||||
matcherMode: '' // 粘贴模式 text | origin
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 注意:watch频繁触发时需要异步修改,否则可能会导致监听不到
|
||||
*/
|
||||
watchPasteMode(newValue, oldValue, ownerInstance, instance) {
|
||||
if (newValue) {
|
||||
this.matcherMode = newValue
|
||||
}
|
||||
},
|
||||
watchStartID(newValue, oldValue, ownerInstance, instance) {
|
||||
if (newValue) {
|
||||
this.addMatcher(newValue)
|
||||
}
|
||||
},
|
||||
watchEID(newValue, oldValue, ownerInstance, instance) {
|
||||
if (newValue) {
|
||||
this.editorID = newValue
|
||||
}
|
||||
},
|
||||
watchVideoUrl(newValue, oldValue, ownerInstance, instance) {
|
||||
if (newValue) {
|
||||
this.generateVideoThumbnail(newValue).then((res) => {
|
||||
this.$ownerInstance.callMethod('getVideoThumbnail', {
|
||||
video: newValue,
|
||||
cover: res
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
watchCoverUrl(newValue, oldValue, ownerInstance, instance) {
|
||||
if (newValue) {
|
||||
this.generateCoverThumbnail(newValue).then((res) => {
|
||||
this.$ownerInstance.callMethod('getCoverThumbnail', {
|
||||
image: newValue,
|
||||
cover: res
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Only Apple Can Do !!!
|
||||
*/
|
||||
watchCoverUrlIOS(newValue, oldValue, ownerInstance, instance) {
|
||||
if (newValue) {
|
||||
this.generateCoverThumbnailIOS(newValue).then((res) => {
|
||||
this.$ownerInstance.callMethod('getCoverThumbnail', {
|
||||
image: newValue,
|
||||
cover: res
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 保留格式粘贴内容
|
||||
* @description 此方式尽可能保留原格式,易于再编辑,但是部分格式会丢失
|
||||
* @param {String} sid 当前编辑器id
|
||||
*/
|
||||
addMatcher(sid) {
|
||||
if(this.idStack.includes(sid)) return // 禁止重复添加Matcher
|
||||
this.idStack.push(sid)
|
||||
|
||||
const el = document.querySelector(`#${sid}`);
|
||||
const quill = Quill.find(el);
|
||||
|
||||
const getStyleAttributes = (node, style) => {
|
||||
let attributes = {}
|
||||
|
||||
// node属性
|
||||
const width = node.getAttribute('width');
|
||||
const height = node.getAttribute('height');
|
||||
if (width) attributes.width = width
|
||||
if (height) attributes.height = height
|
||||
const dataCustom = node.getAttribute('data-custom');
|
||||
if (dataCustom) attributes['data-custom'] = dataCustom;
|
||||
|
||||
// style样式
|
||||
if (style.textAlign) attributes.align = style.textAlign;
|
||||
if (style.fontWeight === 'bold' || style.fontWeight === '700') attributes.bold = true;
|
||||
if (style.fontStyle === 'italic') attributes.italic = true;
|
||||
if (style.textDecoration.includes('underline')) attributes.underline = true;
|
||||
if (style.textDecoration.includes('line-through')) attributes.strike = true;
|
||||
if (style.verticalAlign === 'super') attributes.script = 'super'
|
||||
if (style.verticalAlign === 'sub') attributes.script = 'sub'
|
||||
if (style.fontFamily) attributes.font = style.fontFamily;
|
||||
if (style.fontSize) attributes.size = parseFloat(style.fontSize);
|
||||
if (style.color) attributes.color = style.color;
|
||||
if (style.backgroundColor) attributes.background = style.backgroundColor;
|
||||
|
||||
return attributes
|
||||
}
|
||||
|
||||
const processNode = (node) => {
|
||||
let ops = [];
|
||||
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const computedStyle = document.defaultView.getComputedStyle(node);
|
||||
|
||||
// 处理 <img> 标签
|
||||
if (node.tagName === 'IMG') {
|
||||
const imgSrc = node.getAttribute('src');
|
||||
if (imgSrc) {
|
||||
ops.push({ insert: '\n' }); // 插入换行符,确保图片前有一个空行
|
||||
ops.push({
|
||||
insert: { image: imgSrc },
|
||||
attributes: getStyleAttributes(node, computedStyle)
|
||||
});
|
||||
ops.push({ insert: '\n' }); // 插入换行符,确保图片后有一个空行
|
||||
|
||||
return ops; // 不参与递归
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 <p> 和 <div> 标签
|
||||
else if (node.tagName === 'P' || node.tagName === 'DIV') {
|
||||
ops.push({ insert: '\n' }); // 插入换行符
|
||||
}
|
||||
|
||||
// 处理 <ol> 标签 有序
|
||||
else if (node.tagName === 'OL') {
|
||||
// ops.push({ insert: '\n', attributes: { list: 'ordered' } });
|
||||
}
|
||||
|
||||
// 处理 <ul> 标签 无序
|
||||
else if (node.tagName === 'UL') {
|
||||
// ops.push({ insert: '\n', attributes: { list: 'bullet' } });
|
||||
}
|
||||
|
||||
// 处理 <li> 标签
|
||||
else if (node.tagName === 'LI') {
|
||||
ops.push({ insert: '\n' });
|
||||
}
|
||||
|
||||
|
||||
// 处理 <hr> 标签
|
||||
else if (node.tagName === 'HR') {
|
||||
ops.push({ insert: '\n' }); // 插入换行符
|
||||
ops.push({ insert: { divider: true } });
|
||||
|
||||
return ops; // 不参与递归
|
||||
}
|
||||
|
||||
// 处理 <a> 标签
|
||||
else if (node.tagName === 'A') {
|
||||
const href = node.getAttribute('href');
|
||||
const textContent = node.textContent.trim();
|
||||
|
||||
if (href && textContent) {
|
||||
ops.push({
|
||||
insert: ' ' + textContent + ' ',
|
||||
attributes: {
|
||||
link: href,
|
||||
textDecoration: computedStyle.textDecoration,
|
||||
...getStyleAttributes(node, computedStyle)
|
||||
}
|
||||
});
|
||||
|
||||
return ops; // 不参与递归
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 <h1> 到 <h6> 标题
|
||||
else if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(node.tagName)) {
|
||||
// 处理 <h1> 到 <h6> 标题,生成 header 操作
|
||||
const headerLevel = parseInt(node.tagName.charAt(1), 10);
|
||||
const textContent = node.textContent.trim();
|
||||
|
||||
if (textContent) {
|
||||
ops.push({
|
||||
insert: textContent + '\n', // 必须要加上换行
|
||||
attributes: {
|
||||
header: headerLevel,
|
||||
...getStyleAttributes(node, computedStyle)
|
||||
}
|
||||
});
|
||||
|
||||
return ops; // 不参与递归
|
||||
}
|
||||
}
|
||||
|
||||
// 递归处理子节点
|
||||
for (let child of node.childNodes) {
|
||||
ops = ops.concat(processNode(child));
|
||||
}
|
||||
|
||||
} else if (node.nodeType === Node.TEXT_NODE) {
|
||||
const textContent = node.nodeValue.trim();
|
||||
if (textContent) {
|
||||
// 从父元素获取样式
|
||||
const parentNode = node.parentNode;
|
||||
if (parentNode) {
|
||||
const computedStyle = document.defaultView.getComputedStyle(parentNode);
|
||||
ops.push({
|
||||
insert: textContent,
|
||||
attributes: getStyleAttributes(parentNode, computedStyle)
|
||||
});
|
||||
} else {
|
||||
// 如果没有父元素,直接插入文本
|
||||
ops.push({ insert: textContent });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ops;
|
||||
}
|
||||
|
||||
quill.clipboard.addMatcher(Node.ELEMENT_NODE, (node, delta) => {
|
||||
if (this.matcherMode == 'origin') {
|
||||
let newOps = processNode(node);
|
||||
if (newOps.length > 0) {
|
||||
return { ops: newOps };
|
||||
}
|
||||
}
|
||||
return delta;
|
||||
})
|
||||
|
||||
const cleanClipboardHTML = (html, text) => {
|
||||
if(!html) return text
|
||||
// 使用正则表达式匹配 <!--StartFragment--> 和 <!--EndFragment--> 之间的内容
|
||||
const fragmentRegex = /<!--StartFragment-->([\s\S]*?)<!--EndFragment-->/;
|
||||
const match = html.match(fragmentRegex);
|
||||
if (match && match[1]) {
|
||||
// 返回匹配到的内容
|
||||
return match[1].trim();
|
||||
}
|
||||
// 如果没有匹配到片段内容,返回原始 HTML
|
||||
return html;
|
||||
}
|
||||
|
||||
el.addEventListener('paste', (e) => {
|
||||
let clipboardText = e.clipboardData.getData('text/plain'); // 获取剪切板中的纯文本内容
|
||||
let clipboardHtml = e.clipboardData.getData('text/html'); // 获取剪切板中的 HTML 内容(如果存在)
|
||||
clipboardHtml = cleanClipboardHTML(clipboardHtml, clipboardText)
|
||||
|
||||
setTimeout(() => {
|
||||
this.$ownerInstance.callMethod('editorPaste', {
|
||||
id: sid,
|
||||
text: clipboardText,
|
||||
html: clipboardHtml,
|
||||
range: quill.getSelection() // 获取当前光标位置
|
||||
})
|
||||
}, 100);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 保留格式粘贴内容
|
||||
* @description 此方式虽然可以极大程度保留格式,但是会导致粘贴下来的内容为一整个块,且不易再编辑
|
||||
* @param {String} sid 当前编辑器id
|
||||
*/
|
||||
/*
|
||||
addMatcher(sid) {
|
||||
if(this.idStack.includes(sid)) return // 禁止重复添加Matcher
|
||||
this.idStack.push(sid)
|
||||
|
||||
// 引入源码中的BlockEmbed
|
||||
const BlockEmbed = Quill.import('blots/block/embed');
|
||||
// 定义新的blot类型
|
||||
class AppPanelEmbed extends BlockEmbed {
|
||||
static create(value) {
|
||||
const node = super.create(value);
|
||||
node.setAttribute('width', '100%');
|
||||
// 设置自定义html
|
||||
node.innerHTML = this.transformValue(value)
|
||||
return node;
|
||||
}
|
||||
static transformValue(value) {
|
||||
let handleArr = value.split('\n')
|
||||
handleArr = handleArr.map(e => e.replace(/^[\s]+/, '').replace(/[\s]+$/, ''))
|
||||
return handleArr.join('')
|
||||
}
|
||||
// 返回节点自身的value值 用于撤销操作
|
||||
static value(node) {
|
||||
return node.innerHTML
|
||||
}
|
||||
}
|
||||
// blotName
|
||||
AppPanelEmbed.blotName = 'AppPanelEmbed';
|
||||
// 标签类型自定义
|
||||
AppPanelEmbed.tagName = 'p';
|
||||
Quill.register(AppPanelEmbed, true);
|
||||
|
||||
const el = document.querySelector(`#${sid}`);
|
||||
const quill = Quill.find(el);
|
||||
|
||||
const cleanClipboardHTML = (html, text) => {
|
||||
if(!html) return text
|
||||
// 使用正则表达式匹配 <!--StartFragment--> 和 <!--EndFragment--> 之间的内容
|
||||
const fragmentRegex = /<!--StartFragment-->([\s\S]*?)<!--EndFragment-->/;
|
||||
const match = html.match(fragmentRegex);
|
||||
|
||||
if (match && match[1]) {
|
||||
// 返回匹配到的内容
|
||||
return match[1].trim();
|
||||
}
|
||||
|
||||
// 如果没有匹配到片段内容,返回原始 HTML
|
||||
return html;
|
||||
}
|
||||
|
||||
el.addEventListener('paste', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let clipboardText = e.clipboardData.getData('text/plain'); // 获取剪切板中的纯文本内容
|
||||
let clipboardHtml = e.clipboardData.getData('text/html'); // 获取剪切板中的 HTML 内容(如果存在)
|
||||
clipboardHtml = cleanClipboardHTML(clipboardHtml, clipboardText)
|
||||
|
||||
this.$ownerInstance.callMethod('editorPaste', {
|
||||
id: sid,
|
||||
text: clipboardText,
|
||||
html: clipboardHtml
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
const range = quill.getSelection(); // 获取当前光标位置
|
||||
quill.insertEmbed(range.index, 'AppPanelEmbed', clipboardHtml);
|
||||
}, 100);
|
||||
});
|
||||
},
|
||||
*/
|
||||
/**
|
||||
* 生成视频封面图片(不兼容iOS)
|
||||
* @property {String} videoUrl 视频地址
|
||||
* @returns {Promise} 视频封面图片 注意异步处理
|
||||
*/
|
||||
async generateVideoThumbnail(videoUrl) {
|
||||
// 绘制播放按钮图标
|
||||
// @param {CanvasContext} context canvas上下文
|
||||
// @param {Canvas} canvas
|
||||
const drawPlayButton = (context, canvas) => {
|
||||
// 创建一个 <img> 元素来加载播放图标
|
||||
const img = new Image();
|
||||
img.src = config.video_playicon;
|
||||
|
||||
// 等待图像加载完成
|
||||
return new Promise((resolve, reject) => {
|
||||
img.onload = () => {
|
||||
// 计算播放按钮的位置和大小
|
||||
// const playButtonSize = Math.min(canvas.width, canvas.height) * 0.15;
|
||||
const playButtonSize = canvas.width * 0.15;
|
||||
const playButtonX = (canvas.width - playButtonSize) / 2;
|
||||
const playButtonY = (canvas.height - playButtonSize) / 2;
|
||||
|
||||
// 绘制播放按钮到 canvas
|
||||
context.drawImage(img, playButtonX, playButtonY, playButtonSize, playButtonSize);
|
||||
|
||||
resolve();
|
||||
};
|
||||
|
||||
img.onerror = (error) => {
|
||||
reject(new Error('Failed to load SVG image.'));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
// 创建一个新的 video 元素,并设置 crossOrigin 属性
|
||||
const video = document.createElement('video');
|
||||
video.crossOrigin = 'anonymous'; // 添加 crossOrigin 属性
|
||||
video.preload = 'metadata';
|
||||
video.src = videoUrl;
|
||||
|
||||
// 创建一个新的 canvas 元素
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
// 监听视频加载元数据完成
|
||||
video.onloadedmetadata = async () => {
|
||||
// 设置 canvas 尺寸与视频相同
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
|
||||
// 尝试绘制视频的第一帧到 canvas
|
||||
video.currentTime = 0; // 确保我们从视频的第一帧开始
|
||||
video.onseeked = async () => {
|
||||
try {
|
||||
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 绘制播放按钮
|
||||
await drawPlayButton(context, canvas);
|
||||
|
||||
// 将 canvas 内容转换为 Data URL
|
||||
// resolve(canvas.toDataURL('image/png')); // base64太长了,不建议使用
|
||||
|
||||
// 将 canvas 内容转换为 Blob 对象
|
||||
canvas.toBlob((blob) => {
|
||||
resolve(URL.createObjectURL(blob));
|
||||
}, 'image/png');
|
||||
|
||||
} catch (error) {
|
||||
reject(new Error('Failed to draw image to canvas.'));
|
||||
}
|
||||
};
|
||||
|
||||
// 如果 seek 操作没有成功,尝试直接绘制当前帧
|
||||
setTimeout(async () => {
|
||||
if (!video.seeking) {
|
||||
try {
|
||||
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
// 绘制播放按钮
|
||||
await drawPlayButton(context, canvas);
|
||||
|
||||
// 将 canvas 内容转换为 Data URL
|
||||
// resolve(canvas.toDataURL('image/png')); // base64太长了,不建议使用
|
||||
|
||||
// 将 canvas 内容转换为 Blob 对象
|
||||
canvas.toBlob((blob) => {
|
||||
resolve(URL.createObjectURL(blob));
|
||||
}, 'image/png');
|
||||
|
||||
} catch (error) {
|
||||
reject(new Error('Failed to draw image to canvas.'));
|
||||
}
|
||||
}
|
||||
}, 1000); // 等待1秒后尝试绘制,防止 seek 操作未完成
|
||||
};
|
||||
|
||||
// 监听视频加载错误
|
||||
video.onerror = (error) => {
|
||||
// reject(new Error('Failed to load video or get metadata. PS: Maybe the browser cannot play videos.'));
|
||||
|
||||
// 不直接抛出错误,而是抛出一个默认的封面图片,但是需要加以警告提示
|
||||
console.warn('Failed to load video or get metadata. PS: Maybe the browser cannot play videos.');
|
||||
resolve(config.video_thumbnail);
|
||||
};
|
||||
} catch (error) {
|
||||
// reject(error);
|
||||
// 不直接抛出错误,而是抛出一个默认的封面图片,但是需要加以警告提示
|
||||
console.warn(error)
|
||||
resolve(config.video_thumbnail);
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 生成封面图片,OffscreenCanvas方式(不兼容iOS)
|
||||
* @param {Object} coverUrl 封面图片地址
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async generateCoverThumbnail(coverUrl) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
// 内联 Worker 代码
|
||||
const workerCode = `
|
||||
self.onmessage = async function(e) {
|
||||
const { imageUrl, iconBase64 } = e.data;
|
||||
|
||||
try {
|
||||
// 加载图片并创建 ImageBitmap
|
||||
let imgResponse = await fetch(imageUrl);
|
||||
if (!imgResponse.ok) {
|
||||
throw new Error(\`Failed to load image from \${imageUrl}: \${imgResponse.statusText}\`);
|
||||
}
|
||||
let imgBlob = await imgResponse.blob();
|
||||
let imgBitmap = await createImageBitmap(imgBlob);
|
||||
|
||||
// 创建 OffscreenCanvas 并绘制图片
|
||||
const offscreen = new OffscreenCanvas(imgBitmap.width, imgBitmap.height);
|
||||
const ctx = offscreen.getContext('2d');
|
||||
ctx.drawImage(imgBitmap, 0, 0);
|
||||
|
||||
// 加载图标并创建 ImageBitmap
|
||||
let iconResponse = await fetch(iconBase64);
|
||||
if (!iconResponse.ok) {
|
||||
throw new Error(\`Failed to load icon from \${iconBase64}: \${iconResponse.statusText}\`);
|
||||
}
|
||||
let iconBlob = await iconResponse.blob();
|
||||
let iconBitmap = await createImageBitmap(iconBlob);
|
||||
|
||||
// 计算图标的中心位置并绘制
|
||||
const x = (imgBitmap.width - iconBitmap.width) / 2;
|
||||
const y = (imgBitmap.height - iconBitmap.height) / 2;
|
||||
ctx.drawImage(iconBitmap, x, y);
|
||||
|
||||
// 获取处理后的图像数据
|
||||
const result = await offscreen.convertToBlob();
|
||||
|
||||
// 发送结果回主线程
|
||||
self.postMessage(result);
|
||||
} catch (error) {
|
||||
console.error('Error processing image:', error.message);
|
||||
self.postMessage({ error: error.message });
|
||||
}
|
||||
};
|
||||
`
|
||||
|
||||
// 创建 Blob
|
||||
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
||||
|
||||
// 创建一个指向 Blob 的 URL
|
||||
const workerUrl = URL.createObjectURL(blob);
|
||||
|
||||
// 实例化 Worker
|
||||
const worker = new Worker(workerUrl);
|
||||
|
||||
// 监听来自 Worker 的消息
|
||||
worker.onmessage = (e) => {
|
||||
if (e.data instanceof Blob) {
|
||||
resolve(URL.createObjectURL(e.data));
|
||||
} else {
|
||||
console.warn(e.data.error);
|
||||
// 不直接抛出错误,而是抛出一个默认的封面图片,但是需要加以警告提示
|
||||
resolve(config.video_thumbnail);
|
||||
}
|
||||
worker.terminate(); // 处理完成后终止 worker
|
||||
};
|
||||
|
||||
// 向 Worker 发送消息
|
||||
worker.postMessage({ imageUrl: coverUrl, iconBase64: config.video_playicon });
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 生成封面图片,普通方式,可能影响性能(兼容iOS)
|
||||
* @param {Object} coverUrl 封面图片地址
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async generateCoverThumbnailIOS(coverUrl){
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
// 创建 Image 对象并加载封面图片
|
||||
const img = new Image();
|
||||
img.src = coverUrl;
|
||||
await new Promise(resolve => img.onload = resolve);
|
||||
|
||||
// 创建 Canvas 并绘制封面图片
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
// 加载播放按钮图标并绘制
|
||||
const icon = new Image();
|
||||
icon.src = config.video_playicon; // 假设这是播放按钮图标的 URL
|
||||
await new Promise(resolve => icon.onload = resolve);
|
||||
|
||||
// 计算播放按钮的位置和大小
|
||||
// const playButtonSize = Math.min(canvas.width, canvas.height) * 0.15;
|
||||
const playButtonSize = canvas.width * 0.15;
|
||||
const playButtonX = (canvas.width - playButtonSize) / 2;
|
||||
const playButtonY = (canvas.height - playButtonSize) / 2;
|
||||
|
||||
// 确保播放按钮图标按比例缩放
|
||||
const iconAspectRatio = icon.width / icon.height;
|
||||
const iconWidth = playButtonSize;
|
||||
const iconHeight = iconWidth / iconAspectRatio;
|
||||
|
||||
// 绘制播放按钮图标到 Canvas
|
||||
ctx.drawImage(icon, playButtonX, playButtonY, iconWidth, iconHeight);
|
||||
|
||||
// 将 canvas 内容转换为 Blob 对象
|
||||
canvas.toBlob((blob) => {
|
||||
resolve(URL.createObjectURL(blob));
|
||||
}, 'image/png');
|
||||
|
||||
} catch (error) {
|
||||
// iOS Safari 的安全策略通常比其他浏览器更严格,本地file://协议也会导致跨域
|
||||
console.warn('iOS createCoverThumbnail error :', error);
|
||||
// reject(error);
|
||||
// 不直接抛出错误,而是抛出一个默认的封面图片,但是需要加以警告提示
|
||||
resolve(config.video_thumbnail);
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 富文本plugin微信小程序特殊扩展
|
||||
* @author sonve
|
||||
* @version 1.0.0
|
||||
* @date 2024-12-17
|
||||
*/
|
||||
import config from '../common/config.js'
|
||||
|
||||
/**
|
||||
* 微信小程序特有的OffscreenCanvas方法
|
||||
* @param {String} coverImageUrl 封面资源地址
|
||||
* @returns {Promise<String>} 处理后的封面图片的临时文件路径
|
||||
*/
|
||||
export function wxCreateCoverThumbnail(coverImageUrl) {
|
||||
const loadImage = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getImageInfo({
|
||||
src: coverImageUrl,
|
||||
success: (info) => {
|
||||
resolve(info)
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const imageInfo = await loadImage()
|
||||
|
||||
// 创建离屏 Canvas
|
||||
const canvas = uni.createOffscreenCanvas({
|
||||
type: '2d',
|
||||
width: imageInfo.width,
|
||||
height: imageInfo.height
|
||||
})
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
// 创建一个图片
|
||||
const coverImg = canvas.createImage()
|
||||
// 等待图片加载
|
||||
await new Promise((resolve) => {
|
||||
coverImg.onload = resolve
|
||||
coverImg.src = coverImageUrl // 要加载的图片 url
|
||||
})
|
||||
|
||||
// 绘制封面图片到离屏 Canvas
|
||||
ctx.drawImage(coverImg, 0, 0, canvas.width, canvas.height)
|
||||
|
||||
// 加载播放按钮图标
|
||||
const playIcon = canvas.createImage()
|
||||
// 等待图片加载
|
||||
await new Promise((resolve) => {
|
||||
playIcon.onload = resolve
|
||||
playIcon.src = config.video_playicon // 要加载的图片 url
|
||||
})
|
||||
|
||||
// 计算播放按钮的位置和大小
|
||||
// const playButtonSize = Math.min(canvas.width, canvas.height) * 0.15
|
||||
const playButtonSize = canvas.width * 0.15
|
||||
const playButtonX = (canvas.width - playButtonSize) / 2
|
||||
const playButtonY = (canvas.height - playButtonSize) / 2
|
||||
|
||||
// 确保播放按钮图标按比例缩放
|
||||
const iconAspectRatio = playIcon.width / playIcon.height
|
||||
const iconWidth = playButtonSize
|
||||
const iconHeight = iconWidth / iconAspectRatio
|
||||
|
||||
// 绘制播放按钮图标到离屏 Canvas
|
||||
ctx.drawImage(playIcon, playButtonX, playButtonY, iconWidth, iconHeight)
|
||||
|
||||
// 获取画完后的数据
|
||||
uni.canvasToTempFilePath({
|
||||
canvas: canvas,
|
||||
destWidth: canvas.width,
|
||||
destHeight: canvas.height,
|
||||
fileType: 'png',
|
||||
success: (res) => {
|
||||
resolve(res.tempFilePath)
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error('Failed to convert canvas to image.'))
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
wxCreateCoverThumbnail
|
||||
}
|
||||
16
uni_modules/sv-editor/components/common/config.js
Normal file
261
uni_modules/sv-editor/components/common/file-handler.js
Normal file
@ -0,0 +1,261 @@
|
||||
/**
|
||||
* 以下方法出自 image-tools
|
||||
* @see https://ext.dcloud.net.cn/plugin?id=123
|
||||
*/
|
||||
|
||||
function getLocalFilePath(path) {
|
||||
if (path.indexOf('_www') === 0 || path.indexOf('_doc') === 0 || path.indexOf('_documents') === 0 || path.indexOf(
|
||||
'_downloads') === 0) {
|
||||
return path
|
||||
}
|
||||
if (path.indexOf('file://') === 0) {
|
||||
return path
|
||||
}
|
||||
if (path.indexOf('/storage/emulated/0/') === 0) {
|
||||
return path
|
||||
}
|
||||
if (path.indexOf('/') === 0) {
|
||||
let localFilePath = plus.io.convertAbsoluteFileSystem(path)
|
||||
if (localFilePath !== path) {
|
||||
return localFilePath
|
||||
} else {
|
||||
path = path.substr(1)
|
||||
}
|
||||
}
|
||||
return '_www/' + path
|
||||
}
|
||||
|
||||
function dataUrlToBase64(str) {
|
||||
let array = str.split(',')
|
||||
return array[array.length - 1]
|
||||
}
|
||||
|
||||
let index = 0
|
||||
|
||||
function getNewFileId() {
|
||||
return Date.now() + String(index++)
|
||||
}
|
||||
|
||||
function biggerThan(v1, v2) {
|
||||
let v1Array = v1.split('.')
|
||||
let v2Array = v2.split('.')
|
||||
let update = false
|
||||
for (let index = 0; index < v2Array.length; index++) {
|
||||
let diff = v1Array[index] - v2Array[index]
|
||||
if (diff !== 0) {
|
||||
update = diff > 0
|
||||
break
|
||||
}
|
||||
}
|
||||
return update
|
||||
}
|
||||
|
||||
export function pathToBase64(path) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (typeof window === 'object' && 'document' in window) {
|
||||
if (typeof FileReader === 'function') {
|
||||
let xhr = new XMLHttpRequest()
|
||||
xhr.open('GET', path, true)
|
||||
xhr.responseType = 'blob'
|
||||
xhr.onload = function() {
|
||||
if (this.status === 200) {
|
||||
let fileReader = new FileReader()
|
||||
fileReader.onload = function(e) {
|
||||
resolve(e.target.result)
|
||||
}
|
||||
fileReader.onerror = reject
|
||||
fileReader.readAsDataURL(this.response)
|
||||
}
|
||||
}
|
||||
xhr.onerror = reject
|
||||
xhr.send()
|
||||
return
|
||||
}
|
||||
let canvas = document.createElement('canvas')
|
||||
let c2x = canvas.getContext('2d')
|
||||
let img = new Image
|
||||
img.onload = function() {
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
c2x.drawImage(img, 0, 0)
|
||||
resolve(canvas.toDataURL())
|
||||
canvas.height = canvas.width = 0
|
||||
}
|
||||
img.onerror = reject
|
||||
img.src = path
|
||||
return
|
||||
}
|
||||
if (typeof plus === 'object') {
|
||||
plus.io.resolveLocalFileSystemURL(getLocalFilePath(path), function(entry) {
|
||||
entry.file(function(file) {
|
||||
let fileReader = new plus.io.FileReader()
|
||||
fileReader.onload = function(data) {
|
||||
resolve(data.target.result)
|
||||
}
|
||||
fileReader.onerror = function(error) {
|
||||
reject(error)
|
||||
}
|
||||
fileReader.readAsDataURL(file)
|
||||
}, function(error) {
|
||||
reject(error)
|
||||
})
|
||||
}, function(error) {
|
||||
reject(error)
|
||||
})
|
||||
return
|
||||
}
|
||||
if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
|
||||
wx.getFileSystemManager().readFile({
|
||||
filePath: path,
|
||||
encoding: 'base64',
|
||||
success: function(res) {
|
||||
resolve('data:image/png;base64,' + res.data)
|
||||
},
|
||||
fail: function(error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
reject(new Error('not support'))
|
||||
})
|
||||
}
|
||||
|
||||
export function base64ToPath(base64) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (typeof window === 'object' && 'document' in window) {
|
||||
base64 = base64.split(',')
|
||||
let type = base64[0].match(/:(.*?);/)[1]
|
||||
let str = atob(base64[1])
|
||||
let n = str.length
|
||||
let array = new Uint8Array(n)
|
||||
while (n--) {
|
||||
array[n] = str.charCodeAt(n)
|
||||
}
|
||||
return resolve((window.URL || window.webkitURL).createObjectURL(new Blob([array], {
|
||||
type: type
|
||||
})))
|
||||
}
|
||||
let extName = base64.split(',')[0].match(/data\:\S+\/(\S+);/)
|
||||
if (extName) {
|
||||
extName = extName[1]
|
||||
} else {
|
||||
reject(new Error('base64 error'))
|
||||
}
|
||||
let fileName = getNewFileId() + '.' + extName
|
||||
if (typeof plus === 'object') {
|
||||
let basePath = '_doc'
|
||||
let dirPath = 'uniapp_temp'
|
||||
let filePath = basePath + '/' + dirPath + '/' + fileName
|
||||
if (!biggerThan(plus.os.name === 'Android' ? '1.9.9.80627' : '1.9.9.80472', plus.runtime.innerVersion)) {
|
||||
plus.io.resolveLocalFileSystemURL(basePath, function(entry) {
|
||||
entry.getDirectory(dirPath, {
|
||||
create: true,
|
||||
exclusive: false,
|
||||
}, function(entry) {
|
||||
entry.getFile(fileName, {
|
||||
create: true,
|
||||
exclusive: false,
|
||||
}, function(entry) {
|
||||
entry.createWriter(function(writer) {
|
||||
writer.onwrite = function() {
|
||||
resolve(filePath)
|
||||
}
|
||||
writer.onerror = reject
|
||||
writer.seek(0)
|
||||
writer.writeAsBinary(dataUrlToBase64(base64))
|
||||
}, reject)
|
||||
}, reject)
|
||||
}, reject)
|
||||
}, reject)
|
||||
return
|
||||
}
|
||||
let bitmap = new plus.nativeObj.Bitmap(fileName)
|
||||
bitmap.loadBase64Data(base64, function() {
|
||||
bitmap.save(filePath, {}, function() {
|
||||
bitmap.clear()
|
||||
resolve(filePath)
|
||||
}, function(error) {
|
||||
bitmap.clear()
|
||||
reject(error)
|
||||
})
|
||||
}, function(error) {
|
||||
bitmap.clear()
|
||||
reject(error)
|
||||
})
|
||||
return
|
||||
}
|
||||
if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
|
||||
let filePath = wx.env.USER_DATA_PATH + '/' + fileName
|
||||
wx.getFileSystemManager().writeFile({
|
||||
filePath: filePath,
|
||||
data: dataUrlToBase64(base64),
|
||||
encoding: 'base64',
|
||||
success: function() {
|
||||
resolve(filePath)
|
||||
},
|
||||
fail: function(error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
reject(new Error('not support'))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 本方法为本人自己写的,建议还是使用上述的pathToBase64方法
|
||||
* @description 图片地址转换为base64格式图片
|
||||
* @param {string} url 图片地址 网络地址 本地相对路径
|
||||
* @param {string} type base64图片类型 默认png
|
||||
*/
|
||||
export function urlToBase64(url, type = 'png') {
|
||||
let promises
|
||||
|
||||
// 网络地址 或者h5端本地相对路径 可使用request方式
|
||||
promises = new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
responseType: 'arraybuffer',
|
||||
success: (res) => {
|
||||
const base64 = `data:image/${type};base64,${uni.arrayBufferToBase64(res.data)}`
|
||||
resolve(base64);
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// #ifdef APP
|
||||
if (!url.startsWith('http')) {
|
||||
// app真机本地相对路径
|
||||
promises = new Promise((resolve, reject) => {
|
||||
// 使用compressImage获取到安卓本地路径file:///...
|
||||
uni.compressImage({
|
||||
src: url,
|
||||
quality: 100,
|
||||
success: (res) => {
|
||||
const tempUrl = res.tempFilePath
|
||||
plus.io.resolveLocalFileSystemURL(tempUrl, (entry) => {
|
||||
entry.file((e) => {
|
||||
let fileReader = new plus.io.FileReader();
|
||||
fileReader.onload = (r) => {
|
||||
resolve(r.target.result)
|
||||
}
|
||||
fileReader.readAsDataURL(e)
|
||||
})
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
|
||||
return promises
|
||||
}
|
||||
188
uni_modules/sv-editor/components/common/parse.js
Normal file
@ -0,0 +1,188 @@
|
||||
/**
|
||||
* 富文本解析工具
|
||||
* @author sonve
|
||||
* @version 1.0.0
|
||||
* @date 2024-12-04
|
||||
*/
|
||||
|
||||
import config from './config.js'
|
||||
|
||||
/**
|
||||
* 将含有封面占位图形式的视频富文本转换成正常视频的富文本
|
||||
* @param {String} richText 要进行处理的富文本字符串
|
||||
* @returns {String} 返回处理结果
|
||||
*/
|
||||
export function parseHtmlWithVideo(richText) {
|
||||
// 正则表达式匹配<img>标签及其属性
|
||||
const imgRegex = /<img\s+([^>]+)>/gi;
|
||||
// 正则表达式匹配data-custom属性中的url值
|
||||
const customUrlRegex = /\bdata-custom="[^"]*url=([^&"]+)/i;
|
||||
|
||||
return richText.replace(imgRegex, (match, attrs) => {
|
||||
// 查找data-custom属性中的url值
|
||||
const urlMatch = attrs.match(customUrlRegex);
|
||||
if (urlMatch) {
|
||||
// 获取data-custom中的url
|
||||
const videoUrl = urlMatch[1];
|
||||
|
||||
// 解析出所有属性
|
||||
const attrArray = attrs.split(/\s+/).filter(attr => attr.trim() !== '');
|
||||
|
||||
// 过滤掉src属性和data-custom属性
|
||||
const newAttrs = attrArray.filter(attr => !attr.startsWith('src=') && !attr.startsWith('data-custom='))
|
||||
.join(' ');
|
||||
|
||||
// 构建新的video标签,保留原有的其他属性,但去除src和data-custom
|
||||
//return `<video controls ${newAttrs}><source src="${videoUrl}" /></video>`;
|
||||
return `<video controls ${newAttrs} src="${videoUrl}" width="100%" ></video>`;
|
||||
}
|
||||
// 如果没有匹配到data-custom中的url,则保持原样
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 带有视频的富文本逆向转换
|
||||
* @description 可自定义处理封面
|
||||
* @param {Promise} richText 要转换的富文本
|
||||
* @param {Function<Promise>} customCallback 自定义处理封面回调,需要return封面图片资源,自带参数为视频地址
|
||||
* @returns {Promise} 转换后的富文本 注意异步处理
|
||||
*/
|
||||
export async function replaceVideoWithImageRender(richText, customCallback) {
|
||||
console.log(1);
|
||||
// 正则表达式用于匹配 <video> 标签以及其内部的 <source> 标签
|
||||
const videoRegex = /<video\s+([^>]+)>(.*?)<\/video>/gi;
|
||||
console.log(2);
|
||||
// 找到所有的 <video> 标签
|
||||
const matches = [];
|
||||
let match;
|
||||
while ((match = videoRegex.exec(richText)) !== null) {
|
||||
matches.push(match);
|
||||
}
|
||||
console.log(3);
|
||||
// 并行处理每个 <video> 标签,生成对应的缩略图
|
||||
const replacements = await Promise.all(
|
||||
|
||||
matches.map(async (match) => {
|
||||
console.log(5);
|
||||
const [fullMatch, attributes, content] = match;
|
||||
|
||||
// 匹配 <source> 标签中的 src 属性
|
||||
const sourceRegex = /<source\s+[^>]*src="([^">]+)"/i;
|
||||
const matchSource = content.match(sourceRegex);
|
||||
|
||||
let videoUrl = '';
|
||||
if (matchSource && matchSource.length > 1) {
|
||||
videoUrl = matchSource[1];
|
||||
}
|
||||
|
||||
// 生成视频封面图
|
||||
console.log('5-1')
|
||||
let thumbnailRes
|
||||
//if (customCallback) thumbnailRes = await customCallback(videoUrl)
|
||||
console.log('5-2')
|
||||
// 自定义封面处理
|
||||
//if (!thumbnailRes) thumbnailRes = config.video_thumbnail // 无效值则默认封面处理
|
||||
console.log('5-3')
|
||||
thumbnailRes="https://caseplatform.oss-cn-beijing.aliyuncs.com/prod/static/shipinfengmian.jpg"
|
||||
// 过滤掉不需要的属性,例如 controls
|
||||
const filteredAttributes = attributes
|
||||
.split(/\s+/)
|
||||
.filter(attr => !attr.startsWith('controls'))
|
||||
.join(' ').replace('src=','').replace("").replaceAll('width="100%"','').replaceAll('"','').replace(/\s+/g, "");;
|
||||
console.log('5-4')
|
||||
// 构建新的 img 标签,继承 video 的属性(除了 controls)并添加 data-custom 属性
|
||||
const imgTag = `<img src="${thumbnailRes}" data-custom="url=${filteredAttributes}">`;
|
||||
console.log(6);
|
||||
return { fullMatch, imgTag };
|
||||
}));
|
||||
console.log(7);
|
||||
// 使用 replacements 替换原始的 <video> 标签
|
||||
let result = richText;
|
||||
for (const { fullMatch, imgTag } of replacements) {
|
||||
console.log(8);
|
||||
result = result.replace(fullMatch, imgTag);
|
||||
}
|
||||
console.log("打印结果2")
|
||||
console.log(result)
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析出富文本中的图片和视频
|
||||
* @param {String} richText 要解析的富文本
|
||||
* @returns {Array} 图片和视频数组
|
||||
*/
|
||||
export function parseImagesAndVideos(richText) {
|
||||
// 创建一个空数组用于存储图片和视频信息
|
||||
const result = [];
|
||||
|
||||
// 正则表达式匹配 <img> 标签及其属性
|
||||
const imgRegex = /<img\s+[^>]*>/gi;
|
||||
// 匹配属性名和值的正则表达式,改进后的版本可以处理属性名中包含连字符的情况
|
||||
const attrRegex = /(\w+(-\w+)*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'<>]+))/gi;
|
||||
|
||||
// 找到所有的 <img> 标签
|
||||
const matches = richText.match(imgRegex);
|
||||
// 如果没有找到任何 <img> 标签,返回空数组
|
||||
if (!matches) return [];
|
||||
|
||||
// 遍历所有的 <img> 标签
|
||||
matches.forEach(match => {
|
||||
// 创建一个对象用于存储单个图片或视频的信息
|
||||
const ivInfo = {};
|
||||
// 使用正则表达式匹配每个 <img> 标签的属性
|
||||
let attrsMatch;
|
||||
while ((attrsMatch = attrRegex.exec(match)) !== null) {
|
||||
// 属性名
|
||||
const name = attrsMatch[1].toLowerCase();
|
||||
// 属性值可能存在于第三、第四或第五个捕获组中
|
||||
let value = attrsMatch[3] || attrsMatch[4] || attrsMatch[5] || '';
|
||||
|
||||
// 去除属性值两端可能存在的引号
|
||||
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
||||
value = value.substring(1, value.length - 1);
|
||||
}
|
||||
|
||||
// 将属性名和值添加到 ivInfo 对象中
|
||||
ivInfo[name] = value;
|
||||
}
|
||||
// 将单个图片或视频信息添加到数组中
|
||||
result.push(ivInfo);
|
||||
});
|
||||
|
||||
// 返回包含所有图片和视频信息的数组
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析出富文本中的图片
|
||||
* @param {String} richText 要解析的富文本
|
||||
* @returns {Array} 图片数组
|
||||
*/
|
||||
export function parseImages(richText) {
|
||||
let result = []
|
||||
const ivList = parseImagesAndVideos(richText)
|
||||
ivList.forEach(item => {
|
||||
if (!item['data-custom'] || !item['data-custom'].startsWith('url')) {
|
||||
result.push(item)
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析出富文本中的视频
|
||||
* @param {String} richText 要解析的富文本
|
||||
* @returns {Array} 视频数组
|
||||
*/
|
||||
export function parseVideos(richText) {
|
||||
let result = []
|
||||
const ivList = parseImagesAndVideos(richText)
|
||||
ivList.forEach(item => {
|
||||
if (item['data-custom'] && item['data-custom'].startsWith('url')) {
|
||||
result.push(item)
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
101
uni_modules/sv-editor/components/common/store.js
Normal file
@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 插件内全局状态管理
|
||||
* @author sonve
|
||||
* @version 1.0.0
|
||||
* @date 2024-12-04
|
||||
*/
|
||||
|
||||
// #ifdef VUE3
|
||||
import { reactive } from 'vue';
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE2
|
||||
import Vue from 'vue';
|
||||
// #endif
|
||||
|
||||
// 定义state状态
|
||||
let state = null
|
||||
|
||||
// #ifdef VUE3
|
||||
// 定义响应式状态
|
||||
state = reactive({
|
||||
curEID: '',
|
||||
formats: {},
|
||||
isReadOnly: false,
|
||||
firstInstanceFlag: '' // 首次实例化标志,禁止手动更改
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE2
|
||||
// 定义响应式状态
|
||||
state = Vue.observable({
|
||||
curEID: '',
|
||||
formats: {},
|
||||
isReadOnly: false,
|
||||
firstInstanceFlag: '' // 首次实例化标志,禁止手动更改
|
||||
})
|
||||
// #endif
|
||||
|
||||
// 定义方法
|
||||
function getEditor(eid) {
|
||||
return state[`${eid}-ctx`];
|
||||
};
|
||||
|
||||
function setEditor(eid, ctx) {
|
||||
state[`${eid}-ctx`] = ctx
|
||||
// #ifdef MP-WEIXIN
|
||||
state[`${eid}-ctx`].id = eid
|
||||
// #endif
|
||||
}
|
||||
|
||||
function getEID() {
|
||||
return state.curEID
|
||||
};
|
||||
|
||||
function setEID(eid) {
|
||||
state.curEID = eid
|
||||
}
|
||||
|
||||
function getFormats() {
|
||||
return state.formats
|
||||
}
|
||||
|
||||
function setFormats(formats) {
|
||||
state.formats = formats
|
||||
}
|
||||
|
||||
function getReadOnly() {
|
||||
return state.isReadOnly
|
||||
}
|
||||
|
||||
function setReadOnly(readOnly) {
|
||||
state.isReadOnly = readOnly
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
// 重置所有状态
|
||||
state = {}
|
||||
state.curEID = ''
|
||||
state.formats = {}
|
||||
state.isReadOnly = false
|
||||
state.firstInstanceFlag = '' // 首次实例化标志,禁止手动更改
|
||||
}
|
||||
|
||||
// 定义options对象
|
||||
const options = {
|
||||
state,
|
||||
actions: {
|
||||
getEditor,
|
||||
setEditor,
|
||||
getEID,
|
||||
setEID,
|
||||
getFormats,
|
||||
setFormats,
|
||||
getReadOnly,
|
||||
setReadOnly,
|
||||
destroy
|
||||
}
|
||||
}
|
||||
|
||||
// 导出
|
||||
export default options
|
||||
208
uni_modules/sv-editor/components/common/tool-list.js
Normal file
@ -0,0 +1,208 @@
|
||||
/**
|
||||
* 工具栏
|
||||
* @author sonve
|
||||
* @version 1.0.0
|
||||
* @date 2024-12-04
|
||||
*/
|
||||
|
||||
export const toolList = [
|
||||
{ title: '样式', name: 'style', icon: 'icon-zitiyanse' },
|
||||
{ title: '表情', name: 'emoji', icon: 'icon-xiaolian' },
|
||||
{ title: '撤销', name: 'undo', icon: 'icon-shangyibu1' },
|
||||
{ title: '重做', name: 'redo', icon: 'icon-xiayibu1' },
|
||||
{ title: '更多', name: 'more', icon: 'icon-icon_tianjia' },
|
||||
{ title: '扩展', name: 'setting', icon: 'icon-bianji' },
|
||||
]
|
||||
|
||||
export const styleToolList = [
|
||||
{ title: '标题', name: 'header', value: 2, icon: 'icon-zitibiaoti' },
|
||||
{ title: '分割线', name: 'divider', icon: 'icon-fengexian' },
|
||||
{ title: '粗体', name: 'bold', icon: 'icon-zitijiacu' },
|
||||
{ title: '斜体', name: 'italic', icon: 'icon-zitixieti' },
|
||||
{ title: '下划线', name: 'underline', icon: 'icon-zitixiahuaxian' },
|
||||
{ title: '删除线', name: 'strike', icon: 'icon-zitishanchuxian' },
|
||||
{ title: '左对齐', name: 'align', value: 'left', icon: 'icon-zuoduiqi' },
|
||||
{ title: '居中', name: 'align', value: 'center', icon: 'icon-juzhongduiqi' },
|
||||
{ title: '右对齐', name: 'align', value: 'right', icon: 'icon-youduiqi' },
|
||||
{ title: '有序列表', name: 'list', value: 'ordered', icon: 'icon-youxupailie' },
|
||||
{ title: '无序列表', name: 'list', value: 'bullet', icon: 'icon-wuxupailie' },
|
||||
{ title: '上标', name: 'script', value: 'super', icon: 'icon-zitishangbiao' },
|
||||
{ title: '左缩进', name: 'indent', value: '+1', icon: 'icon-zuosuojin' },
|
||||
{ title: '右缩进', name: 'indent', value: '-1', icon: 'icon-yousuojin' },
|
||||
{ title: '下标', name: 'script', value: 'sub', icon: 'icon-ziti-xiabiao' },
|
||||
{ title: '文字颜色', name: 'color', icon: 'icon-wenziyanse' },
|
||||
{ title: '背景颜色', name: 'backgroundColor', icon: 'icon-beijingyanse' },
|
||||
{ title: '清除格式', name: 'removeformat', icon: 'icon-qingchugeshi' },
|
||||
]
|
||||
|
||||
export const moreToolList = [
|
||||
{ title: '添加图片', name: 'image', value: 'popup', icon: 'icon-charutupian' },
|
||||
{ title: '添加视频', name: 'video', value: 'popup', icon: 'icon-shexiangji' },
|
||||
{ title: '添加链接', name: 'link', value: 'popup', icon: 'icon-charulianjie' },
|
||||
{ title: '添加附件', name: 'attachment', value: 'popup', icon: 'icon-huixingzhen' },
|
||||
{ title: '提及', name: 'at', value: 'popup', icon: 'icon-at' },
|
||||
{ title: '话题', name: 'topic', value: 'popup', icon: 'icon-huati' },
|
||||
{ title: '清空', name: 'clear', value: 'button', icon: 'icon-shanchu' },
|
||||
]
|
||||
|
||||
export const emojiToolList = [
|
||||
'😊', // 笑笑
|
||||
'😃', // 大笑
|
||||
'😄', // 开心果
|
||||
'😁', // 嘲讽
|
||||
'😆', // 爆笑
|
||||
'😅', // 出汗笑
|
||||
'🤣', // 滚地大笑
|
||||
'😂', // 泪流满面
|
||||
'🙂', // 轻松愉快
|
||||
'🙃', // 上下翻白眼
|
||||
'😉', // 鬼鬼祟祟
|
||||
'😌', // 安慰
|
||||
'😍', // 心动
|
||||
'🥰', // 深情
|
||||
'😘', // 吻
|
||||
'😗', // 接吻
|
||||
'😙', // 亲吻
|
||||
'😚', // 亲吻
|
||||
'😋', // 哇塞
|
||||
'😛', // 舌头外伸
|
||||
'😝', // 舌头吐出
|
||||
'😜', // 顽皮
|
||||
'🤪', // 疯狂
|
||||
'😎', // 自豪
|
||||
'🤓', // 学究
|
||||
'🧐', // 思考
|
||||
'😏', // 狡猾
|
||||
'😒', // 不高兴
|
||||
'😞', // 不开心
|
||||
'😔', // 抒发情绪
|
||||
'😟', // 担忧
|
||||
'😕', // 困惑
|
||||
'🙁', // 小失望
|
||||
'☹️️', // 不好意思
|
||||
'😣', // 苦恼
|
||||
'😖', // 愤怒
|
||||
'😫', // 累
|
||||
'😩', // 悲伤
|
||||
'😤', // 生气
|
||||
'😠', // 生气
|
||||
'😡', // 极端愤怒
|
||||
'🤬', // 发飙
|
||||
'🤯', // 爆炸头脑
|
||||
'😳', // 吃惊
|
||||
'😱', // 惊吓
|
||||
'😨', // 恐惧
|
||||
'😰', // 慌张
|
||||
'😢', // 哭泣
|
||||
'😭', // 大哭
|
||||
'😓', // 受挫
|
||||
'🤗', // 给力
|
||||
'🤔', // 思考
|
||||
'🤭', // 戴口罩捂嘴笑
|
||||
'🤫', // 戴口罩做鬼脸
|
||||
'🤥', // 说谎
|
||||
'😬', // 格格不入
|
||||
'😴', // 睡觉
|
||||
'🤤', // 垂涎欲滴
|
||||
'🥳', // 庆祝
|
||||
'🥺', // 求求你
|
||||
'😈', // 恶魔
|
||||
'👿', // 恶灵
|
||||
'🤡', // 小丑
|
||||
'👻', // 鬼魂
|
||||
'👽', // 外星人
|
||||
'👾', // 游戏角色
|
||||
'🤖', // 机器人
|
||||
'😺', // 笑猫
|
||||
'😸', // 大笑猫
|
||||
'😹', // 开心猫
|
||||
'😻', // 心动猫
|
||||
'😼', // 傲娇猫
|
||||
'😽', // 亲吻猫
|
||||
'🙀', // 惊吓猫
|
||||
'😿', // 哭猫
|
||||
'😾' // 生气猫
|
||||
]
|
||||
|
||||
export const colorList = [
|
||||
'#000000',
|
||||
'#222222',
|
||||
'#444444',
|
||||
'#666666',
|
||||
'#999999',
|
||||
'#cccccc',
|
||||
'#eeeeee',
|
||||
'#ffffff',
|
||||
|
||||
'#c92a2a',
|
||||
'#e03131',
|
||||
'#f03e3e',
|
||||
'#fa5252',
|
||||
'#ff6b6b',
|
||||
'#ff8787',
|
||||
'#ffa8a8',
|
||||
'#ffc9c9',
|
||||
|
||||
'#a61e4d',
|
||||
'#c2255c',
|
||||
'#d6336c',
|
||||
'#e64980',
|
||||
'#f06595',
|
||||
'#f783ac',
|
||||
'#faa2c1',
|
||||
'#fcc2d7',
|
||||
|
||||
'#862e9c',
|
||||
'#9c36b5',
|
||||
'#ae3ec9',
|
||||
'#be4bdb',
|
||||
'#cc5de8',
|
||||
'#da77f2',
|
||||
'#e599f7',
|
||||
'#eebefa',
|
||||
|
||||
'#5f3dc4',
|
||||
'#6741d9',
|
||||
'#7048e8',
|
||||
'#7950f2',
|
||||
'#845ef7',
|
||||
'#9775fa',
|
||||
'#b197fc',
|
||||
'#d0bfff',
|
||||
|
||||
'#0b7285',
|
||||
'#0c8599',
|
||||
'#1098ad',
|
||||
'#15aabf',
|
||||
'#22b8cf',
|
||||
'#3bc9db',
|
||||
'#66d9e8',
|
||||
'#99e9f2',
|
||||
|
||||
'#087f5b',
|
||||
'#099268',
|
||||
'#0ca678',
|
||||
'#12b886',
|
||||
'#20c997',
|
||||
'#38d9a9',
|
||||
'#63e6be',
|
||||
'#96f2d7',
|
||||
|
||||
'#5c940d',
|
||||
'#66a80f',
|
||||
'#74b816',
|
||||
'#82c91e',
|
||||
'#94d82d',
|
||||
'#a9e34b',
|
||||
'#c0eb75',
|
||||
'#ffec99',
|
||||
|
||||
'#d9480f',
|
||||
'#e8590c',
|
||||
'#f76707',
|
||||
'#fd7e14',
|
||||
'#ff922b',
|
||||
'#ffa94d',
|
||||
'#ffc078',
|
||||
'#ffd8a8'
|
||||
]
|
||||
430
uni_modules/sv-editor/components/common/utils.js
Normal file
@ -0,0 +1,430 @@
|
||||
/**
|
||||
* 通用工具api
|
||||
* @author sonve
|
||||
* @version 1.0.0
|
||||
* @date 2024-12-04
|
||||
*/
|
||||
|
||||
import store from './store.js'
|
||||
export function addText(word){
|
||||
const eid = store.actions.getEID()
|
||||
const editorCtx = store.actions.getEditor(eid)
|
||||
// 取消键盘副作用
|
||||
noKeyboardEffect(() => {
|
||||
editorCtx.insertText({ text: '\n' })
|
||||
editorCtx.insertText({ text:'【'+word+'】:' })
|
||||
// 建议加个换行,虽然会导致input回调再次触发,不过问题不大
|
||||
editorCtx.insertText({ text: '\n' })
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加图片
|
||||
* @param {Function} uploadFunc 文件上传函数(开发者自行调用上传接口上传本地图片至服务器后获取服务器图片真实地址,需要return包含地址的数组)
|
||||
* @param {Object} options 图片配置项
|
||||
* @property {String} options.srcFiled 图片地址字段名,默认无时使用数组元素本身
|
||||
* @property {String} options.alt 图像无法显示时的替代文本
|
||||
* @property {String} options.width 图片宽度(pixels/百分比)为空时自适应图片本身宽度,默认空(不建议100%,预留一点空隙以便用户编辑)
|
||||
* @property {String} options.height 图片高度 (pixels/百分比)为空时自适应图片本身高度,默认空
|
||||
* @property {String} options.extClass 添加到图片 img 标签上的类名
|
||||
* @property {String} options.data 被序列化为 v1=1;v2=2 的格式挂在属性 data-custom 上
|
||||
* @returns {Array|Promise} 上传的文件数组
|
||||
*/
|
||||
export function addImage(imgs, options = {}) {
|
||||
const eid = store.actions.getEID()
|
||||
const editorCtx = store.actions.getEditor(eid)
|
||||
|
||||
// 服务器上传图片
|
||||
//if (!uploadFunc) return
|
||||
const upRes = imgs //await uploadFunc(editorCtx)
|
||||
console.log('upRes')
|
||||
console.log(upRes)
|
||||
console.log(upRes instanceof Array)
|
||||
console.log(upRes.length)
|
||||
if (!upRes || upRes.length==0) return
|
||||
|
||||
// 取消键盘副作用
|
||||
noKeyboardEffect(() => {
|
||||
//editorCtx.insertText({ text: '\n' })
|
||||
upRes?.forEach((item) => {
|
||||
console.log('item')
|
||||
console.log(item)
|
||||
editorCtx.insertImage({
|
||||
...options,
|
||||
src: options.srcFiled ? item[options.srcFiled] : item,
|
||||
})
|
||||
})
|
||||
// 建议加个换行,虽然会导致input回调再次触发,不过问题不大
|
||||
//editorCtx.insertText({ text: '\n' })
|
||||
})
|
||||
|
||||
return upRes
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加视频
|
||||
* @description uni-editor暂不支持插入视频,此处使用视频封面占位,将视频地址作为属性挂在至data-custom上,携带视频的富文本需要使用专用的api进行解析导出。注:建议后端配合返回视频封面图片地址,或者使用固定的网络图片作为封面。
|
||||
* @param {Function} uploadFunc 文件上传函数(开发者自行调用上传接口上传本地视频至服务器后获取服务器视频真实地址,需要return包含地址的数组)
|
||||
* @param {Object} options 视频封面图片配置项
|
||||
* @property {String} options.imageFiled 视频封面图片地址字段名,默认imagePath
|
||||
* @property {String} options.videoFiled 视频真实地址字段名,默认videoPath
|
||||
* @property {String} options.alt 视频封面图片无法显示时的替代文本
|
||||
* @property {String} options.width 视频封面图片宽度(pixels/百分比)默认空,但是要注意,不设置width的话,video标签默认宽度为300px
|
||||
* @property {String} options.height 视频封面图片高度 (pixels/百分比)默认空
|
||||
* @property {String} options.extClass 添加到视频封面图片 img 标签上的类名
|
||||
* @property {String} options.data 警告:视频地址已存入data-custom中,请勿使用此参数导致视频地址被覆盖
|
||||
* @returns {Array|Promise} 上传的文件数组
|
||||
*/
|
||||
export async function addVideo(uploadFunc, options = {}) {
|
||||
const eid = store.actions.getEID()
|
||||
const editorCtx = store.actions.getEditor(eid)
|
||||
|
||||
// 服务器上传视频
|
||||
if (!uploadFunc) return
|
||||
const upRes = await uploadFunc(editorCtx)
|
||||
console.log(upRes);
|
||||
if (!upRes || !upRes?.length) return
|
||||
|
||||
// 取消键盘副作用
|
||||
noKeyboardEffect(() => {
|
||||
//editorCtx.insertText({ text: '\n' })
|
||||
upRes?.forEach((item) => {
|
||||
editorCtx.insertImage({
|
||||
...options,
|
||||
src: item.videoImg,//item[options.imageFiled || 'imagePath'],
|
||||
data: { url: item.videoUrl },
|
||||
})
|
||||
})
|
||||
// 建议加个换行,虽然会导致input回调再次触发,不过问题不大
|
||||
//editorCtx.insertText({ text: '\n' })
|
||||
})
|
||||
|
||||
return upRes
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加链接
|
||||
* @param {Object} options 链接配置项
|
||||
* @property {String} options.link 链接地址
|
||||
* @property {String} options.text 链接文本 空缺时使用link
|
||||
* @property {String} options.textDecoration 下划线
|
||||
* @property {String} options.color 颜色 默认#007aff
|
||||
* @property {Object} options.style 其他样式,例如 { bold: true, italic: true } 等,详见:https://quilljs.com/docs/delta
|
||||
* @param {Function} callback 添加链接成功后回调
|
||||
* @returns {void}
|
||||
*/
|
||||
export async function addLink(options = {}, callback) {
|
||||
const eid = store.actions.getEID()
|
||||
const editorCtx = store.actions.getEditor(eid)
|
||||
|
||||
// 取消键盘副作用
|
||||
noKeyboardEffect(() => {
|
||||
insertLink(editorCtx, {
|
||||
...options,
|
||||
link: options.link,
|
||||
text: ` ${options.text || options.link} `, // 前后各加一个空格
|
||||
}, () => {
|
||||
editorCtx.changeInput() // 通知更新编辑器input事件
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加附件
|
||||
* @param {Function} uploadFunc 文件上传函数(开发者自行调用上传接口上传本地附件至服务器后获取服务器附件真实地址,需要return包含地址的对象)
|
||||
* @param {Object} options 附件配置项
|
||||
* @property {String} options.srcFiled 附件地址字段名,默认path
|
||||
* @property {String} options.link 附件地址 注:临时地址会自动转成about:blank导致无效
|
||||
* @property {String} options.text 附件文本 空缺时使用link
|
||||
* @property {String} options.textDecoration 下划线
|
||||
* @property {String} options.color 颜色 默认#34d19d
|
||||
* @property {Object} options.style 其他样式,例如 { bold: true, italic: true } 等,详见:https://quilljs.com/docs/delta
|
||||
* @param {Function} callback 添加附件成功后回调
|
||||
* @returns {Object|Promise} 上传的文件对象
|
||||
*/
|
||||
export async function addAttachment(uploadFunc, options = {}, callback) {
|
||||
const eid = store.actions.getEID()
|
||||
const editorCtx = store.actions.getEditor(eid)
|
||||
|
||||
// 服务器上传附件
|
||||
if (!uploadFunc) return
|
||||
const upRes = await uploadFunc(editorCtx)
|
||||
if (!upRes) return
|
||||
|
||||
const link = upRes[options.srcFiled || 'path'] || options.link
|
||||
if (!link) return
|
||||
const text = ` 📄${upRes.text || options.text || upRes.file?.name || link } ` // 加上附件图标前置,并前后各加一个空格
|
||||
// 取消键盘副作用
|
||||
noKeyboardEffect(() => {
|
||||
insertLink(editorCtx, {
|
||||
color: '#34d19d',
|
||||
...options,
|
||||
text,
|
||||
link,
|
||||
}, () => {
|
||||
editorCtx.changeInput() // 通知更新编辑器input事件
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
|
||||
return upRes
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加提及
|
||||
* @param {Object} options 提及配置项
|
||||
* @property {String} options.username 用户名称
|
||||
* @property {String} options.userid 用户id
|
||||
* @property {String} options.textDecoration 下划线
|
||||
* @property {String} options.color 颜色 默认#66ccff
|
||||
* @property {Object} options.style 其他样式,例如 { bold: true, italic: true } 等,详见:https://quilljs.com/docs/delta
|
||||
* @param {Function} callback 添加链接成功后回调
|
||||
*/
|
||||
export async function addAt(options = {}, callback) {
|
||||
const eid = store.actions.getEID()
|
||||
const editorCtx = store.actions.getEditor(eid)
|
||||
|
||||
// 取消键盘副作用
|
||||
noKeyboardEffect(() => {
|
||||
insertLink(editorCtx, {
|
||||
color: '#66ccff',
|
||||
...options,
|
||||
link: `@${options.userid}`, // 添加特殊前缀,后续便于解析标识
|
||||
text: ` @${options.username} `, // 前后各加一个空格
|
||||
}, () => {
|
||||
editorCtx.changeInput() // 通知更新编辑器input事件
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加话题
|
||||
* @param {Object} options 话题配置项
|
||||
* @property {String} options.link 话题链接
|
||||
* @property {String} options.topic 话题名称
|
||||
* @property {String} options.textDecoration 下划线
|
||||
* @property {String} options.color 颜色 默认#909399
|
||||
* @property {Object} options.style 其他样式,例如 { bold: true, italic: true } 等,详见:https://quilljs.com/docs/delta
|
||||
* @param {Function} callback 添加链接成功后回调
|
||||
*/
|
||||
export async function addTopic(options = {}, callback) {
|
||||
const eid = store.actions.getEID()
|
||||
const editorCtx = store.actions.getEditor(eid)
|
||||
|
||||
// 取消键盘副作用
|
||||
noKeyboardEffect(() => {
|
||||
insertLink(editorCtx, {
|
||||
color: '#909399',
|
||||
...options,
|
||||
link: `#${options.link}`, // 添加特殊前缀,后续便于解析标识
|
||||
text: ` #${options.topic}# `, // 前后各加一个空格
|
||||
}, () => {
|
||||
editorCtx.changeInput() // 通知更新编辑器input事件
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 标识必须独一无二 - 标识是为了使用insertText插入标识文本后,查找到标识所在delta位置的索引
|
||||
* 注:因为做了一次insertText操作,所有可能会有linkFlag标识字样闪一下的副作用,没有办法避免
|
||||
*/
|
||||
export const linkFlag = '🔗添加链接中, 请稍后...🔗' // 建议语义化该标识,否则闪烁的时候可能会有点尴尬
|
||||
/**
|
||||
* 插入链接
|
||||
* @description uni-editor暂不支持插入链接,此api使用delta替换链接本文标识方式实现,因硬性原因会导致光标失焦
|
||||
* @param {Object} editorCtx 编辑器上下文
|
||||
* @param {Object} attr 链接属性
|
||||
* @property {String} attr.link 链接地址 注:临时地址会自动转成about:blank导致无效
|
||||
* @property {String} attr.text 链接文本 空缺时使用link
|
||||
* @property {String} attr.textDecoration 下划线
|
||||
* @property {String} attr.color 颜色 默认#007aff
|
||||
* @property {Object} attr.style 其他样式,例如 { bold: true, italic: true } 等,详见:https://quilljs.com/docs/delta
|
||||
* @param {Object} callback 成功回调
|
||||
*/
|
||||
export function insertLink(editorCtx, attr, callback) {
|
||||
// 先插入一段文本内容
|
||||
editorCtx.insertText({ text: linkFlag })
|
||||
// 必须先失焦,否则光标会移至开始位置
|
||||
editorCtx.blur()
|
||||
// 获取全文delta内容
|
||||
editorCtx.getContents({
|
||||
success: (res) => {
|
||||
let options = res.delta.ops
|
||||
const findex = options.findIndex(item => {
|
||||
return item.insert && typeof item.insert !== 'object' && item.insert?.indexOf(linkFlag) !== -1
|
||||
})
|
||||
// 根据标识查找到插入的位置
|
||||
if (findex > -1) {
|
||||
const findOption = options[findex]
|
||||
const findAttributes = findOption.attributes
|
||||
// 将该findOption分成三部分:前内容 要插入的link 后内容
|
||||
const [prefix, suffix] = findOption.insert.split(linkFlag);
|
||||
const handleOps = []
|
||||
// 前内容
|
||||
if (prefix) {
|
||||
const prefixOps = findAttributes ? {
|
||||
insert: prefix,
|
||||
attributes: findAttributes
|
||||
} : {
|
||||
insert: prefix
|
||||
}
|
||||
handleOps.push(prefixOps)
|
||||
}
|
||||
// 插入的link
|
||||
const linkOps = {
|
||||
insert: attr.text || attr.link,
|
||||
attributes: {
|
||||
link: attr.link,
|
||||
textDecoration: attr.textDecoration || 'none', // 下划线
|
||||
color: attr.color || '#007aff',
|
||||
...attr.style
|
||||
}
|
||||
}
|
||||
handleOps.push(linkOps)
|
||||
// 后内容
|
||||
if (suffix) {
|
||||
const suffixOps = findAttributes ? {
|
||||
insert: suffix,
|
||||
attributes: findAttributes
|
||||
} : {
|
||||
insert: suffix
|
||||
}
|
||||
handleOps.push(suffixOps)
|
||||
}
|
||||
// 删除原options[findex]并在findex位置插入上述三个ops
|
||||
options.splice(findex, 1);
|
||||
options.splice(findex, 0, ...handleOps);
|
||||
// 最后重新初始化内容
|
||||
editorCtx.setContents({
|
||||
delta: {
|
||||
ops: options
|
||||
}
|
||||
})
|
||||
// 清除格式,以防残留超链接格式
|
||||
editorCtx.removeFormat()
|
||||
editorCtx.format('color', 'inherit')
|
||||
|
||||
// 后续回调操作
|
||||
if (callback) callback()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 尽量消除键盘带来的影响;重要:核心功能!!!
|
||||
* @param {Function} callback 回调
|
||||
* @param {Object} options 配置项
|
||||
* @property {String} options.mode 可选:setInputMode:通过控制ql-editor的inputmode属性控制键盘 [H5 APP] | loseFocus:通过blur失焦隐藏键盘 [MP-WEIXIN] | hideKeyboard:通过hideKeyboard隐藏键盘 | setReadOnly:通过控制读写隐藏键盘
|
||||
* @property {Number} options.delay 延时(毫秒)默认50
|
||||
*/
|
||||
export function noKeyboardEffect(callback, options) {
|
||||
let defaultOpt = { delay: 50 }
|
||||
|
||||
// #ifdef APP
|
||||
const isIOS = uni.getSystemInfoSync().platform == 'ios'
|
||||
defaultOpt.mode = isIOS ? 'loseFocus' : 'setInputMode' // iOS使用setInputMode无效
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
defaultOpt.mode = 'setInputMode'
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
defaultOpt.mode = 'loseFocus'
|
||||
// #endif
|
||||
|
||||
const opt = Object.assign(defaultOpt, options)
|
||||
|
||||
const eid = store.actions.getEID()
|
||||
const editorCtx = store.actions.getEditor(eid)
|
||||
|
||||
// 通过 uni.hideKeyboard() 隐藏键盘,但是会导致键盘闪烁
|
||||
// 微信小程序好像无法正常隐藏键盘
|
||||
if (opt.mode == 'hideKeyboard') {
|
||||
callback()
|
||||
setTimeout(() => {
|
||||
uni.hideKeyboard()
|
||||
}, opt.delay)
|
||||
}
|
||||
|
||||
// 通过控制编辑器失焦来隐藏键盘,但是会导致键盘闪烁
|
||||
// 只推荐微信小程序使用(也是无可奈何)
|
||||
if (opt.mode == 'loseFocus') {
|
||||
callback()
|
||||
editorCtx.blur()
|
||||
}
|
||||
|
||||
// 通过控制编辑器读写模式进行屏蔽焦点,虽然隐藏了键盘,但是也失焦了
|
||||
// 微信小程序中当只读时是无法使用api去修改内容的
|
||||
if (opt.mode == 'setReadOnly') {
|
||||
store.actions.setReadOnly(true)
|
||||
callback()
|
||||
setTimeout(() => {
|
||||
store.actions.setReadOnly(false)
|
||||
}, opt.delay)
|
||||
}
|
||||
|
||||
// 使用renderjs给ql-editor节点设置inputmode属性来控制键盘是否弹出
|
||||
// 设置none时将会阻止键盘弹出,设置remove将会恢复,完美适配H5、App(Android),但是不支持App(iOS)和微信小程序
|
||||
if (opt.mode == 'setInputMode') {
|
||||
// #ifdef APP || H5
|
||||
// 以下严格处理异步与延时操作,缺一不可
|
||||
editorCtx.changeInputMode('none')
|
||||
setTimeout(() => {
|
||||
callback()
|
||||
setTimeout(() => {
|
||||
editorCtx.changeInputMode('remove')
|
||||
}, opt.delay)
|
||||
}, opt.delay)
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 版权信息
|
||||
*/
|
||||
import packageConfig from '../../package.json'
|
||||
export function copyrightPrint() {
|
||||
/* 标题样式 */
|
||||
const styleTitle1 = `font-size:16px;font-weight:700;color:#ff4500;`
|
||||
const styleTitle2 = `font-style:oblique;font-size:14px;color:#fb7299;`
|
||||
const styleContent = `color:#66ccff;`
|
||||
/* 版权信息 */
|
||||
const title1 = ` 📝 sv-editor v${packageConfig.version} `
|
||||
const title2 = 'by Sonve'
|
||||
const content = `
|
||||
版权声明:
|
||||
1. 本插件免费开源,还望保留此版权声明在控制台输出
|
||||
2. 如需借鉴源码,还望注明出处
|
||||
3. 未经授权您不得以任何形式转载、售卖本插件,或以其他形式侵犯版权及附属权利
|
||||
4. 作者将保留对此插件版权信息的最终解释权
|
||||
🏠 地址: https://ext.dcloud.net.cn/plugin?id=21184
|
||||
😸 Gitee: https://gitee.com/Sonve/sv-editor
|
||||
💬 微信: s1051399604
|
||||
🐧 QQ群: ① 852637893 ② 816646292
|
||||
`
|
||||
console.log(`%c${title1}%c${title2}%c${content}`, styleTitle1, styleTitle2, styleContent)
|
||||
}
|
||||
|
||||
export function noAuthorization(name) {
|
||||
/* 标题样式 */
|
||||
const styleTitle1 = `font-size:16px;font-weight:700;color:#e6a23c;`
|
||||
const styleTitle2 = `font-style:oblique;font-size:14px;color:#fb7299;`
|
||||
const styleContent = `color:#f56c6c;`
|
||||
/* 授权信息 */
|
||||
const title1 = ` ⛔ sv-editor ${name} `
|
||||
const title2 = 'by Sonve'
|
||||
const content = `
|
||||
提示:您还未获取插件特殊扩展功能授权,可联系作者获取
|
||||
💬 微信: s1051399604 | 🐧 QQ群: ① 852637893 ② 816646292
|
||||
🏠 插件地址: https://ext.dcloud.net.cn/plugin?id=21184
|
||||
`
|
||||
console.log(`%c${title1}%c${title2}%c${content}`, styleTitle1, styleTitle2, styleContent)
|
||||
}
|
||||
233
uni_modules/sv-editor/components/icons/iconfont.css
Normal file
@ -0,0 +1,233 @@
|
||||
@font-face {
|
||||
font-family: "iconfont";
|
||||
/* 在vue2中直接使用 ./iconfont.ttf 会找不到文件,很坑,需要返回上一级再点回来 */
|
||||
src: url('../icons/iconfont.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-zitishangbiao:before {
|
||||
content: "\e8e5";
|
||||
}
|
||||
|
||||
.icon-ziti-xiabiao:before {
|
||||
content: "\e8ea";
|
||||
}
|
||||
|
||||
.icon-yousuojin:before {
|
||||
content: "\e6f0";
|
||||
}
|
||||
|
||||
.icon-zuosuojin:before {
|
||||
content: "\e6f1";
|
||||
}
|
||||
|
||||
.icon-wenziyanse:before {
|
||||
content: "\e60b";
|
||||
}
|
||||
|
||||
.icon-beijingyanse:before {
|
||||
content: "\e634";
|
||||
}
|
||||
|
||||
.icon-qingchugeshi:before {
|
||||
content: "\e6f5";
|
||||
}
|
||||
|
||||
.icon-tuige:before {
|
||||
content: "\e61a";
|
||||
}
|
||||
|
||||
.icon-xiajiantou:before {
|
||||
content: "\eb6d";
|
||||
}
|
||||
|
||||
.icon-shangjiantou:before {
|
||||
content: "\eb6e";
|
||||
}
|
||||
|
||||
.icon-huati:before {
|
||||
content: "\e659";
|
||||
}
|
||||
|
||||
.icon-video:before {
|
||||
content: "\e60a";
|
||||
}
|
||||
|
||||
.icon-jianpan:before {
|
||||
content: "\e636";
|
||||
}
|
||||
|
||||
.icon-at:before {
|
||||
content: "\e81e";
|
||||
}
|
||||
|
||||
.icon-bianji:before {
|
||||
content: "\eb61";
|
||||
}
|
||||
|
||||
.icon-icon_tianjia:before {
|
||||
content: "\eb89";
|
||||
}
|
||||
|
||||
.icon-xingzhuang-tupian:before {
|
||||
content: "\eb98";
|
||||
}
|
||||
|
||||
.icon-xingzhuang-wenzi:before {
|
||||
content: "\eb99";
|
||||
}
|
||||
|
||||
.icon-huixingzhen:before {
|
||||
content: "\ebe6";
|
||||
}
|
||||
|
||||
.icon-xiayibu:before {
|
||||
content: "\ebef";
|
||||
}
|
||||
|
||||
.icon-shangyibu:before {
|
||||
content: "\ebf0";
|
||||
}
|
||||
|
||||
.icon-baocun:before {
|
||||
content: "\ec09";
|
||||
}
|
||||
|
||||
.icon-xiayibu1:before {
|
||||
content: "\ec0a";
|
||||
}
|
||||
|
||||
.icon-shangyibu1:before {
|
||||
content: "\ec0b";
|
||||
}
|
||||
|
||||
.icon-weizhigeshi:before {
|
||||
content: "\ec1a";
|
||||
}
|
||||
|
||||
.icon-chehuisekuai:before {
|
||||
content: "\ec45";
|
||||
}
|
||||
|
||||
.icon-shexiangji:before {
|
||||
content: "\ec59";
|
||||
}
|
||||
|
||||
.icon-fuzhi:before {
|
||||
content: "\ec7a";
|
||||
}
|
||||
|
||||
.icon-shanchu:before {
|
||||
content: "\ec7b";
|
||||
}
|
||||
|
||||
.icon-bianjisekuai:before {
|
||||
content: "\ec7c";
|
||||
}
|
||||
|
||||
.icon-fengexian:before {
|
||||
content: "\ec7f";
|
||||
}
|
||||
|
||||
.icon-charulianjie:before {
|
||||
content: "\ec80";
|
||||
}
|
||||
|
||||
.icon-charutupian:before {
|
||||
content: "\ec81";
|
||||
}
|
||||
|
||||
.icon-quxiaolianjie:before {
|
||||
content: "\ec82";
|
||||
}
|
||||
|
||||
.icon-wuxupailie:before {
|
||||
content: "\ec83";
|
||||
}
|
||||
|
||||
.icon-juzhongduiqi:before {
|
||||
content: "\ec84";
|
||||
}
|
||||
|
||||
.icon-yinyong:before {
|
||||
content: "\ec85";
|
||||
}
|
||||
|
||||
.icon-youxupailie:before {
|
||||
content: "\ec86";
|
||||
}
|
||||
|
||||
.icon-youduiqi:before {
|
||||
content: "\ec87";
|
||||
}
|
||||
|
||||
.icon-zitidaima:before {
|
||||
content: "\ec88";
|
||||
}
|
||||
|
||||
.icon-xiaolian:before {
|
||||
content: "\ec89";
|
||||
}
|
||||
|
||||
.icon-zitijiacu:before {
|
||||
content: "\ec8a";
|
||||
}
|
||||
|
||||
.icon-zitishanchuxian:before {
|
||||
content: "\ec8b";
|
||||
}
|
||||
|
||||
.icon-zitibiaoti:before {
|
||||
content: "\ec8c";
|
||||
}
|
||||
|
||||
.icon-zitixiahuaxian:before {
|
||||
content: "\ec8d";
|
||||
}
|
||||
|
||||
.icon-zitixieti:before {
|
||||
content: "\ec8e";
|
||||
}
|
||||
|
||||
.icon-zitiyanse:before {
|
||||
content: "\ec8f";
|
||||
}
|
||||
|
||||
.icon-zuoduiqi:before {
|
||||
content: "\ec90";
|
||||
}
|
||||
|
||||
.icon-zuoyouduiqi:before {
|
||||
content: "\ec91";
|
||||
}
|
||||
|
||||
.icon-tianxie:before {
|
||||
content: "\ec92";
|
||||
}
|
||||
|
||||
.icon-kongxinwenhao:before {
|
||||
content: "\ed19";
|
||||
}
|
||||
|
||||
.icon-fangkuai:before {
|
||||
content: "\ed1a";
|
||||
}
|
||||
|
||||
.icon-jia_sekuai:before {
|
||||
content: "\ed21";
|
||||
}
|
||||
|
||||
.icon-jian_sekuai:before {
|
||||
content: "\ed22";
|
||||
}
|
||||
|
||||
.icon-fenxiangfangshi:before {
|
||||
content: "\ed2e";
|
||||
}
|
||||
BIN
uni_modules/sv-editor/components/icons/iconfont.ttf
Normal file
656
uni_modules/sv-editor/components/plugins/sv-editor-plugin.vue
Normal file
@ -0,0 +1,656 @@
|
||||
<template>
|
||||
<text
|
||||
:eid="eid"
|
||||
:change:eid="quillEditor.watchEID"
|
||||
:sid="sid"
|
||||
:change:sid="quillEditor.watchStartID"
|
||||
:video="videoUrl"
|
||||
:change:pastemode="quillEditor.watchPasteMode"
|
||||
:pastemode="pastemode"
|
||||
:change:video="quillEditor.watchVideoUrl"
|
||||
:cover="coverUrl"
|
||||
:change:cover="quillEditor.watchCoverUrl"
|
||||
:coverios="coverUrlIOS"
|
||||
:change:coverios="quillEditor.watchCoverUrlIOS"
|
||||
></text>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 富文本plugin特殊扩展
|
||||
* @author sonve
|
||||
* @version 1.0.0
|
||||
* @date 2024-12-14
|
||||
*/
|
||||
|
||||
export default {
|
||||
props: {
|
||||
sid: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
eid: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
videoUrl: '', // 视频地址
|
||||
coverUrl: '', // 封面地址
|
||||
coverUrlIOS: '', // ios端封面地址
|
||||
pastemode: 'text' // 粘贴模式 text | origin
|
||||
}
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
changePasteMode(e) {
|
||||
this.pastemode = e
|
||||
},
|
||||
editorPaste(e) {
|
||||
this.$emit('epaste', e)
|
||||
},
|
||||
createVideoThumbnail(url) {
|
||||
this.videoUrl = url
|
||||
},
|
||||
getVideoThumbnail(e) {
|
||||
// e: { video, cover }
|
||||
uni.$emit(`E_EDITOR_GET_VIDEO_THUMBNAIL_${e.video}`, e)
|
||||
},
|
||||
createCoverThumbnail(url) {
|
||||
// #ifdef H5
|
||||
this.coverUrl = url
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
const isIOS = uni.getSystemInfoSync().platform == 'ios'
|
||||
if (isIOS) {
|
||||
this.coverUrlIOS = url // iOS用不了OffscreenCanvas
|
||||
} else {
|
||||
this.coverUrl = url
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
getCoverThumbnail(e) {
|
||||
// e: { image, cover }
|
||||
uni.$emit(`E_EDITOR_GET_COVER_THUMBNAIL_${e.image}`, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script module="quillEditor" lang="renderjs">
|
||||
import config from '../common/config.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
editorID: '',
|
||||
idStack: [], // sid栈
|
||||
matcherMode: '' // 粘贴模式 text | origin
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 注意:watch频繁触发时需要异步修改,否则可能会导致监听不到
|
||||
*/
|
||||
watchPasteMode(newValue, oldValue, ownerInstance, instance) {
|
||||
if (newValue) {
|
||||
this.matcherMode = newValue
|
||||
}
|
||||
},
|
||||
watchStartID(newValue, oldValue, ownerInstance, instance) {
|
||||
if (newValue) {
|
||||
this.addMatcher(newValue)
|
||||
}
|
||||
},
|
||||
watchEID(newValue, oldValue, ownerInstance, instance) {
|
||||
if (newValue) {
|
||||
this.editorID = newValue
|
||||
}
|
||||
},
|
||||
watchVideoUrl(newValue, oldValue, ownerInstance, instance) {
|
||||
if (newValue) {
|
||||
this.generateVideoThumbnail(newValue).then((res) => {
|
||||
this.$ownerInstance.callMethod('getVideoThumbnail', {
|
||||
video: newValue,
|
||||
cover: res
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
watchCoverUrl(newValue, oldValue, ownerInstance, instance) {
|
||||
if (newValue) {
|
||||
this.generateCoverThumbnail(newValue).then((res) => {
|
||||
this.$ownerInstance.callMethod('getCoverThumbnail', {
|
||||
image: newValue,
|
||||
cover: res
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Only Apple Can Do !!!
|
||||
*/
|
||||
watchCoverUrlIOS(newValue, oldValue, ownerInstance, instance) {
|
||||
if (newValue) {
|
||||
this.generateCoverThumbnailIOS(newValue).then((res) => {
|
||||
this.$ownerInstance.callMethod('getCoverThumbnail', {
|
||||
image: newValue,
|
||||
cover: res
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 保留格式粘贴内容
|
||||
* @description 此方式尽可能保留原格式,易于再编辑,但是部分格式会丢失
|
||||
* @param {String} sid 当前编辑器id
|
||||
*/
|
||||
addMatcher(sid) {
|
||||
if(this.idStack.includes(sid)) return // 禁止重复添加Matcher
|
||||
this.idStack.push(sid)
|
||||
|
||||
const el = document.querySelector(`#${sid}`);
|
||||
const quill = Quill.find(el);
|
||||
|
||||
const getStyleAttributes = (node, style) => {
|
||||
let attributes = {}
|
||||
|
||||
// node属性
|
||||
const width = node.getAttribute('width');
|
||||
const height = node.getAttribute('height');
|
||||
if (width) attributes.width = width
|
||||
if (height) attributes.height = height
|
||||
const dataCustom = node.getAttribute('data-custom');
|
||||
if (dataCustom) attributes['data-custom'] = dataCustom;
|
||||
|
||||
// style样式
|
||||
if (style.textAlign) attributes.align = style.textAlign;
|
||||
if (style.fontWeight === 'bold' || style.fontWeight === '700') attributes.bold = true;
|
||||
if (style.fontStyle === 'italic') attributes.italic = true;
|
||||
if (style.textDecoration.includes('underline')) attributes.underline = true;
|
||||
if (style.textDecoration.includes('line-through')) attributes.strike = true;
|
||||
if (style.verticalAlign === 'super') attributes.script = 'super'
|
||||
if (style.verticalAlign === 'sub') attributes.script = 'sub'
|
||||
if (style.fontFamily) attributes.font = style.fontFamily;
|
||||
if (style.fontSize) attributes.size = parseFloat(style.fontSize);
|
||||
if (style.color) attributes.color = style.color;
|
||||
if (style.backgroundColor) attributes.background = style.backgroundColor;
|
||||
|
||||
return attributes
|
||||
}
|
||||
|
||||
const processNode = (node) => {
|
||||
let ops = [];
|
||||
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const computedStyle = document.defaultView.getComputedStyle(node);
|
||||
|
||||
// 处理 <img> 标签
|
||||
if (node.tagName === 'IMG') {
|
||||
const imgSrc = node.getAttribute('src');
|
||||
if (imgSrc) {
|
||||
ops.push({ insert: '\n' }); // 插入换行符,确保图片前有一个空行
|
||||
ops.push({
|
||||
insert: { image: imgSrc },
|
||||
attributes: getStyleAttributes(node, computedStyle)
|
||||
});
|
||||
ops.push({ insert: '\n' }); // 插入换行符,确保图片后有一个空行
|
||||
|
||||
return ops; // 不参与递归
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 <p> 和 <div> 标签
|
||||
else if (node.tagName === 'P' || node.tagName === 'DIV') {
|
||||
ops.push({ insert: '\n' }); // 插入换行符
|
||||
}
|
||||
|
||||
// 处理 <ol> 标签 有序
|
||||
else if (node.tagName === 'OL') {
|
||||
// ops.push({ insert: '\n', attributes: { list: 'ordered' } });
|
||||
}
|
||||
|
||||
// 处理 <ul> 标签 无序
|
||||
else if (node.tagName === 'UL') {
|
||||
// ops.push({ insert: '\n', attributes: { list: 'bullet' } });
|
||||
}
|
||||
|
||||
// 处理 <li> 标签
|
||||
else if (node.tagName === 'LI') {
|
||||
ops.push({ insert: '\n' });
|
||||
}
|
||||
|
||||
|
||||
// 处理 <hr> 标签
|
||||
else if (node.tagName === 'HR') {
|
||||
ops.push({ insert: '\n' }); // 插入换行符
|
||||
ops.push({ insert: { divider: true } });
|
||||
|
||||
return ops; // 不参与递归
|
||||
}
|
||||
|
||||
// 处理 <a> 标签
|
||||
else if (node.tagName === 'A') {
|
||||
const href = node.getAttribute('href');
|
||||
const textContent = node.textContent.trim();
|
||||
|
||||
if (href && textContent) {
|
||||
ops.push({
|
||||
insert: ' ' + textContent + ' ',
|
||||
attributes: {
|
||||
link: href,
|
||||
textDecoration: computedStyle.textDecoration,
|
||||
...getStyleAttributes(node, computedStyle)
|
||||
}
|
||||
});
|
||||
|
||||
return ops; // 不参与递归
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 <h1> 到 <h6> 标题
|
||||
else if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(node.tagName)) {
|
||||
// 处理 <h1> 到 <h6> 标题,生成 header 操作
|
||||
const headerLevel = parseInt(node.tagName.charAt(1), 10);
|
||||
const textContent = node.textContent.trim();
|
||||
|
||||
if (textContent) {
|
||||
ops.push({
|
||||
insert: textContent + '\n', // 必须要加上换行
|
||||
attributes: {
|
||||
header: headerLevel,
|
||||
...getStyleAttributes(node, computedStyle)
|
||||
}
|
||||
});
|
||||
|
||||
return ops; // 不参与递归
|
||||
}
|
||||
}
|
||||
|
||||
// 递归处理子节点
|
||||
for (let child of node.childNodes) {
|
||||
ops = ops.concat(processNode(child));
|
||||
}
|
||||
|
||||
} else if (node.nodeType === Node.TEXT_NODE) {
|
||||
const textContent = node.nodeValue.trim();
|
||||
if (textContent) {
|
||||
// 从父元素获取样式
|
||||
const parentNode = node.parentNode;
|
||||
if (parentNode) {
|
||||
const computedStyle = document.defaultView.getComputedStyle(parentNode);
|
||||
ops.push({
|
||||
insert: textContent,
|
||||
attributes: getStyleAttributes(parentNode, computedStyle)
|
||||
});
|
||||
} else {
|
||||
// 如果没有父元素,直接插入文本
|
||||
ops.push({ insert: textContent });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ops;
|
||||
}
|
||||
|
||||
quill.clipboard.addMatcher(Node.ELEMENT_NODE, (node, delta) => {
|
||||
if (this.matcherMode == 'origin') {
|
||||
let newOps = processNode(node);
|
||||
if (newOps.length > 0) {
|
||||
return { ops: newOps };
|
||||
}
|
||||
}
|
||||
return delta;
|
||||
})
|
||||
|
||||
const cleanClipboardHTML = (html, text) => {
|
||||
if(!html) return text
|
||||
// 使用正则表达式匹配 <!--StartFragment--> 和 <!--EndFragment--> 之间的内容
|
||||
const fragmentRegex = /<!--StartFragment-->([\s\S]*?)<!--EndFragment-->/;
|
||||
const match = html.match(fragmentRegex);
|
||||
if (match && match[1]) {
|
||||
// 返回匹配到的内容
|
||||
return match[1].trim();
|
||||
}
|
||||
// 如果没有匹配到片段内容,返回原始 HTML
|
||||
return html;
|
||||
}
|
||||
|
||||
el.addEventListener('paste', (e) => {
|
||||
let clipboardText = e.clipboardData.getData('text/plain'); // 获取剪切板中的纯文本内容
|
||||
let clipboardHtml = e.clipboardData.getData('text/html'); // 获取剪切板中的 HTML 内容(如果存在)
|
||||
clipboardHtml = cleanClipboardHTML(clipboardHtml, clipboardText)
|
||||
|
||||
setTimeout(() => {
|
||||
this.$ownerInstance.callMethod('editorPaste', {
|
||||
id: sid,
|
||||
text: clipboardText,
|
||||
html: clipboardHtml,
|
||||
range: quill.getSelection() // 获取当前光标位置
|
||||
})
|
||||
}, 100);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 保留格式粘贴内容
|
||||
* @description 此方式虽然可以极大程度保留格式,但是会导致粘贴下来的内容为一整个块,且不易再编辑
|
||||
* @param {String} sid 当前编辑器id
|
||||
*/
|
||||
/*
|
||||
addMatcher(sid) {
|
||||
if(this.idStack.includes(sid)) return // 禁止重复添加Matcher
|
||||
this.idStack.push(sid)
|
||||
|
||||
// 引入源码中的BlockEmbed
|
||||
const BlockEmbed = Quill.import('blots/block/embed');
|
||||
// 定义新的blot类型
|
||||
class AppPanelEmbed extends BlockEmbed {
|
||||
static create(value) {
|
||||
const node = super.create(value);
|
||||
node.setAttribute('width', '100%');
|
||||
// 设置自定义html
|
||||
node.innerHTML = this.transformValue(value)
|
||||
return node;
|
||||
}
|
||||
static transformValue(value) {
|
||||
let handleArr = value.split('\n')
|
||||
handleArr = handleArr.map(e => e.replace(/^[\s]+/, '').replace(/[\s]+$/, ''))
|
||||
return handleArr.join('')
|
||||
}
|
||||
// 返回节点自身的value值 用于撤销操作
|
||||
static value(node) {
|
||||
return node.innerHTML
|
||||
}
|
||||
}
|
||||
// blotName
|
||||
AppPanelEmbed.blotName = 'AppPanelEmbed';
|
||||
// 标签类型自定义
|
||||
AppPanelEmbed.tagName = 'p';
|
||||
Quill.register(AppPanelEmbed, true);
|
||||
|
||||
const el = document.querySelector(`#${sid}`);
|
||||
const quill = Quill.find(el);
|
||||
|
||||
const cleanClipboardHTML = (html, text) => {
|
||||
if(!html) return text
|
||||
// 使用正则表达式匹配 <!--StartFragment--> 和 <!--EndFragment--> 之间的内容
|
||||
const fragmentRegex = /<!--StartFragment-->([\s\S]*?)<!--EndFragment-->/;
|
||||
const match = html.match(fragmentRegex);
|
||||
|
||||
if (match && match[1]) {
|
||||
// 返回匹配到的内容
|
||||
return match[1].trim();
|
||||
}
|
||||
|
||||
// 如果没有匹配到片段内容,返回原始 HTML
|
||||
return html;
|
||||
}
|
||||
|
||||
el.addEventListener('paste', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let clipboardText = e.clipboardData.getData('text/plain'); // 获取剪切板中的纯文本内容
|
||||
let clipboardHtml = e.clipboardData.getData('text/html'); // 获取剪切板中的 HTML 内容(如果存在)
|
||||
clipboardHtml = cleanClipboardHTML(clipboardHtml, clipboardText)
|
||||
|
||||
this.$ownerInstance.callMethod('editorPaste', {
|
||||
id: sid,
|
||||
text: clipboardText,
|
||||
html: clipboardHtml
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
const range = quill.getSelection(); // 获取当前光标位置
|
||||
quill.insertEmbed(range.index, 'AppPanelEmbed', clipboardHtml);
|
||||
}, 100);
|
||||
});
|
||||
},
|
||||
*/
|
||||
/**
|
||||
* 生成视频封面图片(不兼容iOS)
|
||||
* @property {String} videoUrl 视频地址
|
||||
* @returns {Promise} 视频封面图片 注意异步处理
|
||||
*/
|
||||
async generateVideoThumbnail(videoUrl) {
|
||||
// 绘制播放按钮图标
|
||||
// @param {CanvasContext} context canvas上下文
|
||||
// @param {Canvas} canvas
|
||||
const drawPlayButton = (context, canvas) => {
|
||||
// 创建一个 <img> 元素来加载播放图标
|
||||
const img = new Image();
|
||||
img.src = config.video_playicon;
|
||||
|
||||
// 等待图像加载完成
|
||||
return new Promise((resolve, reject) => {
|
||||
img.onload = () => {
|
||||
// 计算播放按钮的位置和大小
|
||||
// const playButtonSize = Math.min(canvas.width, canvas.height) * 0.15;
|
||||
const playButtonSize = canvas.width * 0.15;
|
||||
const playButtonX = (canvas.width - playButtonSize) / 2;
|
||||
const playButtonY = (canvas.height - playButtonSize) / 2;
|
||||
|
||||
// 绘制播放按钮到 canvas
|
||||
context.drawImage(img, playButtonX, playButtonY, playButtonSize, playButtonSize);
|
||||
|
||||
resolve();
|
||||
};
|
||||
|
||||
img.onerror = (error) => {
|
||||
reject(new Error('Failed to load SVG image.'));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
// 创建一个新的 video 元素,并设置 crossOrigin 属性
|
||||
const video = document.createElement('video');
|
||||
video.crossOrigin = 'anonymous'; // 添加 crossOrigin 属性
|
||||
video.preload = 'metadata';
|
||||
video.src = videoUrl;
|
||||
|
||||
// 创建一个新的 canvas 元素
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
// 监听视频加载元数据完成
|
||||
video.onloadedmetadata = async () => {
|
||||
// 设置 canvas 尺寸与视频相同
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
|
||||
// 尝试绘制视频的第一帧到 canvas
|
||||
video.currentTime = 0; // 确保我们从视频的第一帧开始
|
||||
video.onseeked = async () => {
|
||||
try {
|
||||
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 绘制播放按钮
|
||||
await drawPlayButton(context, canvas);
|
||||
|
||||
// 将 canvas 内容转换为 Data URL
|
||||
// resolve(canvas.toDataURL('image/png')); // base64太长了,不建议使用
|
||||
|
||||
// 将 canvas 内容转换为 Blob 对象
|
||||
canvas.toBlob((blob) => {
|
||||
resolve(URL.createObjectURL(blob));
|
||||
}, 'image/png');
|
||||
|
||||
} catch (error) {
|
||||
reject(new Error('Failed to draw image to canvas.'));
|
||||
}
|
||||
};
|
||||
|
||||
// 如果 seek 操作没有成功,尝试直接绘制当前帧
|
||||
setTimeout(async () => {
|
||||
if (!video.seeking) {
|
||||
try {
|
||||
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
// 绘制播放按钮
|
||||
await drawPlayButton(context, canvas);
|
||||
|
||||
// 将 canvas 内容转换为 Data URL
|
||||
// resolve(canvas.toDataURL('image/png')); // base64太长了,不建议使用
|
||||
|
||||
// 将 canvas 内容转换为 Blob 对象
|
||||
canvas.toBlob((blob) => {
|
||||
resolve(URL.createObjectURL(blob));
|
||||
}, 'image/png');
|
||||
|
||||
} catch (error) {
|
||||
reject(new Error('Failed to draw image to canvas.'));
|
||||
}
|
||||
}
|
||||
}, 1000); // 等待1秒后尝试绘制,防止 seek 操作未完成
|
||||
};
|
||||
|
||||
// 监听视频加载错误
|
||||
video.onerror = (error) => {
|
||||
// reject(new Error('Failed to load video or get metadata. PS: Maybe the browser cannot play videos.'));
|
||||
|
||||
// 不直接抛出错误,而是抛出一个默认的封面图片,但是需要加以警告提示
|
||||
console.warn('Failed to load video or get metadata. PS: Maybe the browser cannot play videos.');
|
||||
resolve(config.video_thumbnail);
|
||||
};
|
||||
} catch (error) {
|
||||
// reject(error);
|
||||
// 不直接抛出错误,而是抛出一个默认的封面图片,但是需要加以警告提示
|
||||
console.warn(error)
|
||||
resolve(config.video_thumbnail);
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 生成封面图片,OffscreenCanvas方式(不兼容iOS)
|
||||
* @param {Object} coverUrl 封面图片地址
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async generateCoverThumbnail(coverUrl) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
// 内联 Worker 代码
|
||||
const workerCode = `
|
||||
self.onmessage = async function(e) {
|
||||
const { imageUrl, iconBase64 } = e.data;
|
||||
|
||||
try {
|
||||
// 加载图片并创建 ImageBitmap
|
||||
let imgResponse = await fetch(imageUrl);
|
||||
if (!imgResponse.ok) {
|
||||
throw new Error(\`Failed to load image from \${imageUrl}: \${imgResponse.statusText}\`);
|
||||
}
|
||||
let imgBlob = await imgResponse.blob();
|
||||
let imgBitmap = await createImageBitmap(imgBlob);
|
||||
|
||||
// 创建 OffscreenCanvas 并绘制图片
|
||||
const offscreen = new OffscreenCanvas(imgBitmap.width, imgBitmap.height);
|
||||
const ctx = offscreen.getContext('2d');
|
||||
ctx.drawImage(imgBitmap, 0, 0);
|
||||
|
||||
// 加载图标并创建 ImageBitmap
|
||||
let iconResponse = await fetch(iconBase64);
|
||||
if (!iconResponse.ok) {
|
||||
throw new Error(\`Failed to load icon from \${iconBase64}: \${iconResponse.statusText}\`);
|
||||
}
|
||||
let iconBlob = await iconResponse.blob();
|
||||
let iconBitmap = await createImageBitmap(iconBlob);
|
||||
|
||||
// 计算图标的中心位置并绘制
|
||||
const x = (imgBitmap.width - iconBitmap.width) / 2;
|
||||
const y = (imgBitmap.height - iconBitmap.height) / 2;
|
||||
ctx.drawImage(iconBitmap, x, y);
|
||||
|
||||
// 获取处理后的图像数据
|
||||
const result = await offscreen.convertToBlob();
|
||||
|
||||
// 发送结果回主线程
|
||||
self.postMessage(result);
|
||||
} catch (error) {
|
||||
console.error('Error processing image:', error.message);
|
||||
self.postMessage({ error: error.message });
|
||||
}
|
||||
};
|
||||
`
|
||||
|
||||
// 创建 Blob
|
||||
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
||||
|
||||
// 创建一个指向 Blob 的 URL
|
||||
const workerUrl = URL.createObjectURL(blob);
|
||||
|
||||
// 实例化 Worker
|
||||
const worker = new Worker(workerUrl);
|
||||
|
||||
// 监听来自 Worker 的消息
|
||||
worker.onmessage = (e) => {
|
||||
if (e.data instanceof Blob) {
|
||||
resolve(URL.createObjectURL(e.data));
|
||||
} else {
|
||||
console.warn(e.data.error);
|
||||
// 不直接抛出错误,而是抛出一个默认的封面图片,但是需要加以警告提示
|
||||
resolve(config.video_thumbnail);
|
||||
}
|
||||
worker.terminate(); // 处理完成后终止 worker
|
||||
};
|
||||
|
||||
// 向 Worker 发送消息
|
||||
worker.postMessage({ imageUrl: coverUrl, iconBase64: config.video_playicon });
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 生成封面图片,普通方式,可能影响性能(兼容iOS)
|
||||
* @param {Object} coverUrl 封面图片地址
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async generateCoverThumbnailIOS(coverUrl){
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
// 创建 Image 对象并加载封面图片
|
||||
const img = new Image();
|
||||
img.src = coverUrl;
|
||||
await new Promise(resolve => img.onload = resolve);
|
||||
|
||||
// 创建 Canvas 并绘制封面图片
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
// 加载播放按钮图标并绘制
|
||||
const icon = new Image();
|
||||
icon.src = config.video_playicon; // 假设这是播放按钮图标的 URL
|
||||
await new Promise(resolve => icon.onload = resolve);
|
||||
|
||||
// 计算播放按钮的位置和大小
|
||||
// const playButtonSize = Math.min(canvas.width, canvas.height) * 0.15;
|
||||
const playButtonSize = canvas.width * 0.15;
|
||||
const playButtonX = (canvas.width - playButtonSize) / 2;
|
||||
const playButtonY = (canvas.height - playButtonSize) / 2;
|
||||
|
||||
// 确保播放按钮图标按比例缩放
|
||||
const iconAspectRatio = icon.width / icon.height;
|
||||
const iconWidth = playButtonSize;
|
||||
const iconHeight = iconWidth / iconAspectRatio;
|
||||
|
||||
// 绘制播放按钮图标到 Canvas
|
||||
ctx.drawImage(icon, playButtonX, playButtonY, iconWidth, iconHeight);
|
||||
|
||||
// 将 canvas 内容转换为 Blob 对象
|
||||
canvas.toBlob((blob) => {
|
||||
resolve(URL.createObjectURL(blob));
|
||||
}, 'image/png');
|
||||
|
||||
} catch (error) {
|
||||
// iOS Safari 的安全策略通常比其他浏览器更严格,本地file://协议也会导致跨域
|
||||
console.warn('iOS createCoverThumbnail error :', error);
|
||||
// reject(error);
|
||||
// 不直接抛出错误,而是抛出一个默认的封面图片,但是需要加以警告提示
|
||||
resolve(config.video_thumbnail);
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
175
uni_modules/sv-editor/components/plugins/sv-editor-wxplugin.js
Normal file
@ -0,0 +1,175 @@
|
||||
/**
|
||||
* 富文本plugin微信小程序特殊扩展
|
||||
* @author sonve
|
||||
* @version 1.0.0
|
||||
* @date 2024-12-17
|
||||
*/
|
||||
import config from '../common/config.js'
|
||||
|
||||
/**
|
||||
* 微信小程序特有的OffscreenCanvas方法
|
||||
* @param {String} coverImageUrl 封面资源地址
|
||||
* @returns {Promise<String>} 处理后的封面图片的临时文件路径
|
||||
*/
|
||||
export function wxCreateCoverThumbnail(coverImageUrl) {
|
||||
const loadImage = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getImageInfo({
|
||||
src: coverImageUrl,
|
||||
success: (info) => {
|
||||
resolve(info)
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const imageInfo = await loadImage()
|
||||
|
||||
// 创建离屏 Canvas
|
||||
const canvas = uni.createOffscreenCanvas({
|
||||
type: '2d',
|
||||
width: imageInfo.width,
|
||||
height: imageInfo.height
|
||||
})
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
// 创建一个图片
|
||||
const coverImg = canvas.createImage()
|
||||
// 等待图片加载
|
||||
await new Promise((resolve) => {
|
||||
coverImg.onload = resolve
|
||||
coverImg.src = coverImageUrl // 要加载的图片 url
|
||||
})
|
||||
|
||||
// 绘制封面图片到离屏 Canvas
|
||||
ctx.drawImage(coverImg, 0, 0, canvas.width, canvas.height)
|
||||
|
||||
// 加载播放按钮图标
|
||||
const playIcon = canvas.createImage()
|
||||
// 等待图片加载
|
||||
await new Promise((resolve) => {
|
||||
playIcon.onload = resolve
|
||||
playIcon.src = config.video_playicon // 要加载的图片 url
|
||||
})
|
||||
|
||||
// 计算播放按钮的位置和大小
|
||||
// const playButtonSize = Math.min(canvas.width, canvas.height) * 0.15
|
||||
const playButtonSize = canvas.width * 0.15
|
||||
const playButtonX = (canvas.width - playButtonSize) / 2
|
||||
const playButtonY = (canvas.height - playButtonSize) / 2
|
||||
|
||||
// 确保播放按钮图标按比例缩放
|
||||
const iconAspectRatio = playIcon.width / playIcon.height
|
||||
const iconWidth = playButtonSize
|
||||
const iconHeight = iconWidth / iconAspectRatio
|
||||
|
||||
// 绘制播放按钮图标到离屏 Canvas
|
||||
ctx.drawImage(playIcon, playButtonX, playButtonY, iconWidth, iconHeight)
|
||||
|
||||
// 获取画完后的数据
|
||||
uni.canvasToTempFilePath({
|
||||
canvas: canvas,
|
||||
destWidth: canvas.width,
|
||||
destHeight: canvas.height,
|
||||
fileType: 'png',
|
||||
success: (res) => {
|
||||
resolve(res.tempFilePath)
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error('Failed to convert canvas to image.'))
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function wxCreateVideoThumbnail(coverImageUrl) {
|
||||
const loadVideo = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getVideoInfo({
|
||||
src: coverImageUrl,
|
||||
success: (info) => {
|
||||
resolve(info)
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const imageInfo = await loadVideo()
|
||||
|
||||
// 创建离屏 Canvas
|
||||
const canvas = uni.createOffscreenCanvas({
|
||||
type: '2d',
|
||||
width: imageInfo.width,
|
||||
height: imageInfo.height
|
||||
})
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
// 创建一个图片
|
||||
const coverImg = canvas.createImage()
|
||||
// 等待图片加载
|
||||
await new Promise((resolve) => {
|
||||
coverImg.onload = resolve
|
||||
coverImg.src = coverImageUrl // 要加载的图片 url
|
||||
})
|
||||
|
||||
// 绘制封面图片到离屏 Canvas
|
||||
ctx.drawImage(coverImg, 0, 0, canvas.width, canvas.height)
|
||||
|
||||
// 加载播放按钮图标
|
||||
const playIcon = canvas.createImage()
|
||||
// 等待图片加载
|
||||
await new Promise((resolve) => {
|
||||
playIcon.onload = resolve
|
||||
playIcon.src = config.video_playicon // 要加载的图片 url
|
||||
})
|
||||
|
||||
// 计算播放按钮的位置和大小
|
||||
// const playButtonSize = Math.min(canvas.width, canvas.height) * 0.15
|
||||
const playButtonSize = canvas.width * 0.15
|
||||
const playButtonX = (canvas.width - playButtonSize) / 2
|
||||
const playButtonY = (canvas.height - playButtonSize) / 2
|
||||
|
||||
// 确保播放按钮图标按比例缩放
|
||||
const iconAspectRatio = playIcon.width / playIcon.height
|
||||
const iconWidth = playButtonSize
|
||||
const iconHeight = iconWidth / iconAspectRatio
|
||||
|
||||
// 绘制播放按钮图标到离屏 Canvas
|
||||
ctx.drawImage(playIcon, playButtonX, playButtonY, iconWidth, iconHeight)
|
||||
|
||||
// 获取画完后的数据
|
||||
uni.canvasToTempFilePath({
|
||||
canvas: canvas,
|
||||
destWidth: canvas.width,
|
||||
destHeight: canvas.height,
|
||||
fileType: 'png',
|
||||
success: (res) => {
|
||||
resolve(res.tempFilePath)
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(new Error('Failed to convert canvas to image.'))
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
wxCreateCoverThumbnail,
|
||||
wxCreateVideoThumbnail
|
||||
}
|
||||
122
uni_modules/sv-editor/components/sv-editor/sv-choose-file.vue
Normal file
@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<text :data="flag" :props="config" :change:data="fileManager.watchData" :change:props="fileManager.watchProps"></text>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 文件选择 - APP端
|
||||
* @author sonve
|
||||
* @version 1.0.0
|
||||
* @date 2024-12-04
|
||||
*/
|
||||
|
||||
export default {
|
||||
props: {
|
||||
/**
|
||||
* 配置项
|
||||
* @tutorial https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input/file
|
||||
*/
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
accept: `.doc,.docx,.xls,.xlsx,.pdf,.zip,.rar,
|
||||
application/msword,
|
||||
application/vnd.openxmlformats-officedocument.wordprocessingml.document,
|
||||
application/vnd.ms-excel,
|
||||
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,
|
||||
application/pdf,
|
||||
application/zip,
|
||||
application/x-rar-compressed`,
|
||||
multiple: false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
flag: 0 // 监听标志
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
chooseFile() {
|
||||
this.flag++ // 修改监听标志
|
||||
},
|
||||
rawFile(file) {
|
||||
this.$emit('confirm', file)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script module="fileManager" lang="renderjs">
|
||||
import { base64ToPath } from '../common/file-handler.js';
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
configCopy: {}, // 跟随vue中props的配置
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
watchData(newValue, oldValue, ownerInstance, instance) {
|
||||
if (newValue) {
|
||||
this.openFileManager()
|
||||
}
|
||||
},
|
||||
watchProps(newValue, oldValue, ownerInstance, instance) {
|
||||
if (newValue) {
|
||||
this.configCopy = newValue
|
||||
}
|
||||
},
|
||||
openFileManager() {
|
||||
try {
|
||||
const { accept, multiple } = this.configCopy
|
||||
// 创建文件选择器input
|
||||
let fileInput = document.createElement('input')
|
||||
fileInput.setAttribute('type', 'file')
|
||||
fileInput.setAttribute('accept', accept)
|
||||
// 注:是否多选不要直接赋值multiple,应当是为false时不添加multiple属性
|
||||
if(multiple) fileInput.setAttribute('multiple', multiple)
|
||||
fileInput.click()
|
||||
|
||||
// 封装为Promise的FileReader读取文件
|
||||
const readFileAsDataURL = (file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
reader.onload = async (event) => {
|
||||
const base64 = event.target.result
|
||||
const path = await base64ToPath(base64)
|
||||
resolve({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
base64,
|
||||
path
|
||||
});
|
||||
};
|
||||
reader.onerror = (error) => {
|
||||
reject(error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
fileInput.addEventListener('change', async (e) => {
|
||||
let files = e.target.files // 注:此处为FileList对象,并非常规数组
|
||||
|
||||
let results = await Promise.all(
|
||||
// Array.from 方法可以将类数组对象转换为真正的数组
|
||||
Array.from(files).map(item => readFileAsDataURL(item))
|
||||
);
|
||||
|
||||
// callMethod不支持流数据,无法直接传递文件流对象
|
||||
this.$ownerInstance.callMethod('rawFile', results)
|
||||
})
|
||||
} catch (err) {
|
||||
console.warn('==== openFileManager catch error :', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<view @touchmove.stop.prevent="moveStop">
|
||||
<view class="sv-editor-colorpicker" v-if="showPicker">
|
||||
<view class="editor-popup-header">
|
||||
<!-- <view class="header-left" @click="cancel">取消</view> -->
|
||||
<view class="header-left" @click="reset">重置</view>
|
||||
<view class="header-title" :style="{ backgroundColor: selectColor }" v-if="selectColor">{{ selectColor }}</view>
|
||||
<view class="header-right" @click="confirm">确认</view>
|
||||
</view>
|
||||
<view class="sv-editor-colorpicker-container">
|
||||
<view
|
||||
v-for="item in allColors"
|
||||
:key="item"
|
||||
class="color-item"
|
||||
:style="{ backgroundColor: item }"
|
||||
@click="onSelect(item)"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mask" v-if="showPicker" @click.stop="onMask"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { colorList } from '../common/tool-list'
|
||||
|
||||
export default {
|
||||
name: 'sv-editor-colorpicker',
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'color'
|
||||
},
|
||||
// 点击遮罩层自动关闭弹窗
|
||||
maskClose: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ['update:show', 'open', 'close', 'onMask', 'cancel', 'confirm'],
|
||||
// #ifdef VUE2
|
||||
model: {
|
||||
prop: 'show',
|
||||
event: 'update:show'
|
||||
},
|
||||
// #endif
|
||||
data() {
|
||||
return {
|
||||
selectColor: this.color
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
color(newVal) {
|
||||
this.selectColor = newVal
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showPicker: {
|
||||
set(newVal) {
|
||||
this.$emit('update:show', newVal)
|
||||
},
|
||||
get() {
|
||||
return this.show
|
||||
}
|
||||
},
|
||||
allColors() {
|
||||
return colorList
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 阻止滑动穿透
|
||||
moveStop() {},
|
||||
open() {
|
||||
this.showPicker = true
|
||||
this.$emit('open')
|
||||
},
|
||||
close() {
|
||||
this.showPicker = false
|
||||
this.$emit('close')
|
||||
},
|
||||
onMask() {
|
||||
if (this.maskClose) this.close()
|
||||
this.$emit('onMask')
|
||||
},
|
||||
cancel() {
|
||||
this.$emit('cancel')
|
||||
this.close()
|
||||
},
|
||||
confirm() {
|
||||
this.$emit('confirm', this.selectColor, this.type)
|
||||
},
|
||||
reset() {
|
||||
this.selectColor = ''
|
||||
},
|
||||
onSelect(e) {
|
||||
this.selectColor = e
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sv-editor-colorpicker {
|
||||
--editor-colorpicker-bgcolor: #ffffff;
|
||||
--editor-colorpicker-radius: 30rpx 30rpx 0 0;
|
||||
--editor-colorpicker-confirm: #4d80f0;
|
||||
--editor-colorpicker-cancel: #fa4350;
|
||||
--editor-colorpicker-header-height: 50rpx;
|
||||
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
z-index: 10000;
|
||||
border-radius: var(--editor-colorpicker-radius);
|
||||
padding: 30rpx;
|
||||
padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
|
||||
background-color: var(--editor-colorpicker-bgcolor);
|
||||
box-sizing: border-box;
|
||||
|
||||
.editor-popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
box-sizing: border-box;
|
||||
height: var(--editor-colorpicker-header-height);
|
||||
|
||||
.header-left {
|
||||
color: var(--editor-colorpicker-cancel);
|
||||
}
|
||||
.header-title {
|
||||
color: #000000;
|
||||
text-shadow: 1rpx 1rpx #ffffff, -1rpx 1rpx #ffffff, 1rpx -1rpx #ffffff, -1rpx -1rpx #ffffff;
|
||||
padding: 4rpx 12rpx;
|
||||
box-shadow: 0 0 8rpx #cccccc;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
.header-right {
|
||||
color: var(--editor-colorpicker-confirm);
|
||||
}
|
||||
}
|
||||
|
||||
.sv-editor-colorpicker-container {
|
||||
// max-height: 40vh;
|
||||
overflow: auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
align-items: center; /* 垂直居中 */
|
||||
justify-items: center; /* 水平居中 */
|
||||
gap: 20rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
.color-item {
|
||||
width: 100%;
|
||||
height: 60rpx;
|
||||
box-shadow: 0 0 8rpx #ccc;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
.mask {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 9999;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,445 @@
|
||||
<template>
|
||||
<view @touchmove.stop.prevent="moveStop">
|
||||
<view class="sv-editor-popup" v-if="showPop">
|
||||
<view class="editor-popup-header">
|
||||
<view class="header-left" @click="cancel">取消</view>
|
||||
<view class="header-title">{{ title }}</view>
|
||||
<view class="header-right" @click="confirm">确认</view>
|
||||
</view>
|
||||
<view class="sv-editor-popup-container">
|
||||
<!-- 添加图片 -->
|
||||
<view class="popup-image" v-if="toolName == 'image'">
|
||||
<view class="popup-form-input">
|
||||
<text class="form-label">网络图片</text>
|
||||
<input v-model="imageForm.link" type="text" class="form-input" placeholder="请输入图片地址" />
|
||||
</view>
|
||||
<view class="popup-form-input">
|
||||
<text class="form-label">本地图片</text>
|
||||
<button size="mini" class="form-button" @click="selectImage">选择文件</button>
|
||||
<view class="form-thumbnail">
|
||||
<image
|
||||
class="form-thumbnail-item form-thumbnail-image"
|
||||
v-for="(item, index) in imageForm.file"
|
||||
:key="item.path"
|
||||
:src="item.path"
|
||||
@click="deleteImage(index)"
|
||||
></image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 添加视频 -->
|
||||
<view class="popup-video" v-if="toolName == 'video'">
|
||||
<view class="popup-form-input">
|
||||
<text class="form-label">网络视频</text>
|
||||
<input v-model="videoForm.link" type="text" class="form-input" placeholder="请输入视频地址" />
|
||||
</view>
|
||||
<view class="popup-form-input">
|
||||
<text class="form-label">本地视频</text>
|
||||
<button size="mini" class="form-button" @click="selectVideo">选择文件</button>
|
||||
<view class="form-thumbnail" v-if="videoForm.file.tempFilePath">
|
||||
<view class="form-thumbnail-item form-thumbnail-icon" @click="deleteVideo">
|
||||
<text class="iconfont icon-video"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 添加链接 -->
|
||||
<view class="popup-link" v-if="toolName == 'link'">
|
||||
<view class="popup-form-input">
|
||||
<text class="form-label">链接地址</text>
|
||||
<input v-model="linkForm.link" type="text" class="form-input" placeholder="请输入链接地址 (必填)" />
|
||||
</view>
|
||||
<view class="popup-form-input">
|
||||
<text class="form-label">链接文本</text>
|
||||
<input v-model="linkForm.text" type="text" class="form-input" placeholder="请输入链接文本 (可选)" />
|
||||
</view>
|
||||
</view>
|
||||
<!-- 添加附件 -->
|
||||
<view class="popup-attachment" v-if="toolName == 'attachment'">
|
||||
<view class="popup-form-input">
|
||||
<text class="form-label">附件地址</text>
|
||||
<input v-model="attachmentForm.link" type="text" class="form-input" placeholder="请输入附件地址" />
|
||||
</view>
|
||||
<view class="popup-form-input">
|
||||
<text class="form-label">附件描述</text>
|
||||
<input v-model="attachmentForm.text" type="text" class="form-input" placeholder="请输入附件描述" />
|
||||
</view>
|
||||
<view class="popup-form-input">
|
||||
<text class="form-label">本地文件</text>
|
||||
<button size="mini" class="form-button" @click="selectAttachment">选择文件</button>
|
||||
<view class="form-thumbnail" v-if="attachmentForm.file.path">
|
||||
<view class="form-thumbnail-item form-thumbnail-icon" @click="deleteAttachment">
|
||||
<text class="iconfont icon-huixingzhen"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 提及 -->
|
||||
<view class="popup-at" v-if="toolName == 'at'">
|
||||
<slot name="at"></slot>
|
||||
</view>
|
||||
<!-- 话题 -->
|
||||
<view class="popup-topic" v-if="toolName == 'topic'">
|
||||
<slot name="topic"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mask" v-if="showPop" @click.stop="onMask"></view>
|
||||
<!-- #ifdef APP -->
|
||||
<sv-choose-file ref="chooseFileRef" @confirm="selectAppFile"></sv-choose-file>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 扩展工具面板弹窗
|
||||
* @author sonve
|
||||
* @version 1.0.0
|
||||
* @date 2024-12-04
|
||||
*/
|
||||
|
||||
import { moreToolList } from '../common/tool-list.js'
|
||||
import SvChooseFile from './sv-choose-file.vue'
|
||||
|
||||
export default {
|
||||
name: 'sv-editor-popup-more',
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序特殊配置
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
virtualHost: true,
|
||||
styleIsolation: 'shared'
|
||||
},
|
||||
// #endif
|
||||
components: {
|
||||
SvChooseFile
|
||||
},
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
toolName: {
|
||||
type: [String, null],
|
||||
default: 'image'
|
||||
},
|
||||
// 点击遮罩层自动关闭弹窗
|
||||
maskClose: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ['update:show', 'open', 'close', 'onMask', 'cancel', 'confirm'],
|
||||
// #ifdef VUE2
|
||||
model: {
|
||||
prop: 'show',
|
||||
event: 'update:show'
|
||||
},
|
||||
// #endif
|
||||
data() {
|
||||
return {
|
||||
imageForm: {
|
||||
link: '',
|
||||
file: []
|
||||
},
|
||||
videoForm: {
|
||||
link: '',
|
||||
file: {}
|
||||
},
|
||||
linkForm: {
|
||||
link: '',
|
||||
text: ''
|
||||
},
|
||||
attachmentForm: {
|
||||
link: '',
|
||||
text: '',
|
||||
file: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showPop: {
|
||||
set(newVal) {
|
||||
this.$emit('update:show', newVal)
|
||||
},
|
||||
get() {
|
||||
return this.show
|
||||
}
|
||||
},
|
||||
title() {
|
||||
return moreToolList.find((item) => item.name == this.toolName)?.title
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 阻止滑动穿透
|
||||
moveStop() {},
|
||||
open() {
|
||||
this.showPop = true
|
||||
this.$emit('open')
|
||||
},
|
||||
close() {
|
||||
this.showPop = false
|
||||
this.$emit('close')
|
||||
},
|
||||
onMask() {
|
||||
if (this.maskClose) this.close()
|
||||
this.$emit('onMask')
|
||||
},
|
||||
cancel() {
|
||||
this.$emit('cancel')
|
||||
this.close()
|
||||
},
|
||||
confirm() {
|
||||
let params = {}
|
||||
params.name = this.toolName
|
||||
switch (this.toolName) {
|
||||
case 'image':
|
||||
Object.assign(params, this.imageForm)
|
||||
break
|
||||
case 'video':
|
||||
Object.assign(params, this.videoForm)
|
||||
break
|
||||
case 'link':
|
||||
Object.assign(params, this.linkForm)
|
||||
break
|
||||
case 'attachment':
|
||||
Object.assign(params, this.attachmentForm)
|
||||
break
|
||||
}
|
||||
this.$emit('confirm', params)
|
||||
},
|
||||
/**
|
||||
* 业务方法
|
||||
*/
|
||||
// 选择图片
|
||||
selectImage() {
|
||||
// #ifdef APP || H5
|
||||
uni.chooseImage({
|
||||
count: 5, // 默认9,此处限制为5
|
||||
success: (res) => {
|
||||
this.imageForm.file = res.tempFiles
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '未授权访问相册权限,请授权后使用',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.chooseMedia({
|
||||
count: 5, // 默认9,此处限制为5
|
||||
mediaType: ['image'],
|
||||
success: (res) => {
|
||||
this.imageForm.file = res.tempFiles
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '未授权访问相册权限,请授权后使用',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
// 删除指定图片
|
||||
deleteImage(index) {
|
||||
this.imageForm.file.splice(index, 1)
|
||||
},
|
||||
// 选择视频
|
||||
selectVideo() {
|
||||
uni.chooseVideo({
|
||||
sourceType: ['camera', 'album'],
|
||||
success: (res) => {
|
||||
this.videoForm.file = res
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '未授权访问媒体权限,请授权后使用',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
// 删除选择的本地视频
|
||||
deleteVideo() {
|
||||
this.videoForm.file = {}
|
||||
},
|
||||
// 选择附件
|
||||
selectAttachment() {
|
||||
// #ifdef H5
|
||||
uni.chooseFile({
|
||||
count: 1, // 默认100,此处限制为1
|
||||
extension: ['.doc', '.docx', '.xls', '.xlsx', '.pdf', '.zip', '.rar'],
|
||||
success: (res) => {
|
||||
this.attachmentForm.file = res.tempFiles[0]
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '未授权访问文件权限,请授权后使用',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef APP
|
||||
this.$refs.chooseFileRef.chooseFile()
|
||||
// 选择文件完成后触发selectAppFile方法
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
wx.chooseMessageFile({
|
||||
count: 1, // 最多可以选择的文件个数,可以 0~100,此处限制为1
|
||||
type: 'file', // 可以选择除了图片和视频之外的其它的文件
|
||||
extension: ['.doc', '.docx', '.xls', '.xlsx', '.pdf', '.zip', '.rar'],
|
||||
success: (res) => {
|
||||
this.attachmentForm.file = res.tempFiles[0]
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '未授权访问文件权限,请授权后使用',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
// 选择文件完成后触发
|
||||
selectAppFile(files) {
|
||||
this.attachmentForm.file = files[0]
|
||||
},
|
||||
// 删除指定附件
|
||||
deleteAttachment() {
|
||||
this.attachmentForm.file = {}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../icons/iconfont.css';
|
||||
|
||||
.sv-editor-popup {
|
||||
--editor-popup-radius: 30rpx 30rpx 0 0;
|
||||
--editor-popup-bgcolor: #ffffff;
|
||||
--editor-popup-confirm: #4d80f0;
|
||||
--editor-popup-cancel: #fa4350;
|
||||
--thumbnail-icon-bgcolor: #cccccc;
|
||||
--editor-popup-header-height: 50rpx;
|
||||
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
z-index: 10000;
|
||||
border-radius: var(--editor-popup-radius);
|
||||
padding: 30rpx;
|
||||
padding-bottom: calc(30rpx + env(safe-area-inset-bottom));
|
||||
background-color: var(--editor-popup-bgcolor);
|
||||
box-sizing: border-box;
|
||||
|
||||
.editor-popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
box-sizing: border-box;
|
||||
height: var(--editor-popup-header-height);
|
||||
|
||||
.header-left {
|
||||
color: var(--editor-popup-cancel);
|
||||
}
|
||||
.header-right {
|
||||
color: var(--editor-popup-confirm);
|
||||
}
|
||||
}
|
||||
|
||||
.sv-editor-popup-container {
|
||||
box-sizing: border-box;
|
||||
|
||||
.popup-form-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
.form-label {
|
||||
margin-right: 20rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
flex: 1;
|
||||
padding: 12rpx;
|
||||
border: 1rpx solid #eeeeee;
|
||||
border-radius: 8rpx;
|
||||
line-height: unset;
|
||||
height: unset;
|
||||
min-height: unset;
|
||||
box-sizing: border-box;
|
||||
|
||||
.uni-input-placeholder {
|
||||
color: #dddddd;
|
||||
}
|
||||
}
|
||||
|
||||
.form-button {
|
||||
margin-left: unset;
|
||||
margin-right: unset;
|
||||
}
|
||||
|
||||
.form-thumbnail {
|
||||
.form-thumbnail-item {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
margin-left: 20rpx;
|
||||
position: relative;
|
||||
border: 1rpx solid #eeeeee;
|
||||
|
||||
&:active {
|
||||
border-color: #d83b01;
|
||||
|
||||
&::after {
|
||||
content: 'X';
|
||||
font-size: 25px;
|
||||
font-weight: bold;
|
||||
color: #d83b01;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-thumbnail-image {
|
||||
vertical-align: bottom; // 取消image标签底部留白
|
||||
}
|
||||
|
||||
.form-thumbnail-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--form-thumbnail-icon-bgcolor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.mask {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 9999;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
</style>
|
||||
150
uni_modules/sv-editor/components/sv-editor/sv-editor-render.vue
Normal file
@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<text
|
||||
:eid="eid"
|
||||
:change:eid="quillEditor.watchEID"
|
||||
:mode="inputmode"
|
||||
:change:mode="quillEditor.watchInputMode"
|
||||
:focus="focusFlag"
|
||||
:change:focus="quillEditor.watchFocus"
|
||||
:backspace="backspaceFlag"
|
||||
:change:backspace="quillEditor.watchBackSpace"
|
||||
></text>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 富文本renderjs扩展
|
||||
* @author sonve
|
||||
* @version 1.0.0
|
||||
* @date 2024-12-04
|
||||
*/
|
||||
|
||||
export default {
|
||||
props: {
|
||||
eid: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inputmode: '', // none | remove
|
||||
focusFlag: 0, // 主动聚焦标志
|
||||
backspaceFlag: 0 // 主动删除标志
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeInputMode(mode) {
|
||||
this.inputmode = mode
|
||||
},
|
||||
focus() {
|
||||
this.focusFlag++
|
||||
},
|
||||
backspace() {
|
||||
this.backspaceFlag++
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script module="quillEditor" lang="renderjs">
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
editorID: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
watchEID(newValue, oldValue, ownerInstance, instance) {
|
||||
if (newValue) {
|
||||
this.editorID = newValue
|
||||
}
|
||||
},
|
||||
watchInputMode(newValue, oldValue, ownerInstance, instance) {
|
||||
if (newValue) {
|
||||
this.changeQuillInputMode(newValue)
|
||||
}
|
||||
},
|
||||
watchFocus(newValue, oldValue, ownerInstance, instance) {
|
||||
if (newValue) {
|
||||
this.changeFocus(newValue)
|
||||
}
|
||||
},
|
||||
watchBackSpace(newValue, oldValue, ownerInstance, instance) {
|
||||
if (newValue) {
|
||||
this.changeBackSpace(newValue)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 通过增加或移出inputmode属性来控制是否允许键盘弹出
|
||||
* @param {String} type none | remove
|
||||
* @tutorial https://ask.dcloud.net.cn/article/39915
|
||||
*/
|
||||
changeQuillInputMode(type) {
|
||||
try {
|
||||
// 要关闭软键盘的话,需要给inputmode属性设置none
|
||||
// 如果要打开软键盘的话,需要移出inputmode属性
|
||||
const el = document.querySelector(`#${this.editorID} .ql-editor`);
|
||||
if(!el) return console.warn('==== quill dom error ====');
|
||||
if(type == 'none') el.setAttribute('inputmode', 'none')
|
||||
if(type == 'remove') el.removeAttribute('inputmode')
|
||||
} catch (err) {
|
||||
console.warn('==== changeQuillInputMode catch error :', err);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 通过quill节点实例的focus方法来主动触发编辑器聚焦
|
||||
*/
|
||||
changeFocus() {
|
||||
try {
|
||||
const el = document.querySelector(`#${this.editorID} .ql-editor`);
|
||||
if(!el) return console.warn('==== quill dom error ====');
|
||||
el.focus()
|
||||
} catch (err) {
|
||||
console.warn('==== changeFocus catch error :', err);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 通过quill节点实例的deleteText方法来主动触发编辑器删除
|
||||
*/
|
||||
changeBackSpace() {
|
||||
try {
|
||||
const el = document.querySelector(`#${this.editorID}`);
|
||||
const quill = Quill.find(el);
|
||||
if(!el || !quill) return console.warn('==== quill dom error ====');
|
||||
|
||||
const range = quill.getSelection(); // 获取当前光标位置
|
||||
|
||||
if (range && range.length === 0) {
|
||||
// 如果没有选中文本且光标存在,则删除前一个字符或 emoji
|
||||
if (range.index > 0) {
|
||||
// 获取光标前的所有文本
|
||||
const text = quill.getText(0, range.index);
|
||||
// 规范化 Unicode 字符,确保正确处理组合字符和 emoji
|
||||
const normalizedText = text.normalize('NFC');
|
||||
// 将文本转换为字符数组,确保正确处理多字节字符
|
||||
const chars = Array.from(normalizedText);
|
||||
// 计算前一个字符的索引
|
||||
const lastCharIndex = chars.length - 1;
|
||||
if (lastCharIndex >= 0) {
|
||||
// 删除前一个字符(包括多字节字符)
|
||||
const lastChar = chars[lastCharIndex];
|
||||
const lastCharLength = text.slice(-lastChar.length).length;
|
||||
quill.deleteText(range.index - lastCharLength, lastCharLength);
|
||||
quill.setSelection(range.index - lastCharLength); // 更新光标位置
|
||||
}
|
||||
}
|
||||
} else if (range && range.length > 0) {
|
||||
// 如果有选中文本,则删除选中的文本
|
||||
quill.deleteText(range.index, range.length);
|
||||
quill.setSelection(range.index); // 更新光标位置
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.warn('==== changeBackSpace catch error :', err);
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
647
uni_modules/sv-editor/components/sv-editor/sv-editor-toolbar.vue
Normal file
@ -0,0 +1,647 @@
|
||||
<template>
|
||||
<view class="sv-editor-toolbar">
|
||||
<view class="editor-tools" @tap="onTool">
|
||||
<text
|
||||
v-for="item in allTools"
|
||||
:key="item.title"
|
||||
class="iconfont"
|
||||
:class="item.icon"
|
||||
:data-name="item.name"
|
||||
></text>
|
||||
<!-- [展开/折叠] 为固定工具 -->
|
||||
<text v-if="isShowPanel" class="iconfont icon-xiajiantou" data-name="fold" data-value="0"></text>
|
||||
<text v-else class="iconfont icon-shangjiantou" data-name="fold" data-value="1"></text>
|
||||
</view>
|
||||
<!-- 样式面板 不建议使用 :key="item.name" 因为 name 可能重复 -->
|
||||
<view class="tool-panel" v-if="curTool == 'style' && isShowPanel">
|
||||
<view class="panel-grid panel-style">
|
||||
<view
|
||||
class="panel-style-item"
|
||||
:class="[(item.value ? formats[item.name] === item.value : formats[item.name]) ? 'ql-active' : '']"
|
||||
:style="{ color: item.name == 'color' ? curTextColor : item.name == 'backgroundColor' ? curBgColor : '' }"
|
||||
v-for="item in allStyleTools"
|
||||
:key="item.title"
|
||||
:title="item.title"
|
||||
:data-name="item.name"
|
||||
:data-value="item.value"
|
||||
@tap="onToolStyleItem"
|
||||
>
|
||||
<text class="iconfont pointer-events-none" :class="item.icon"></text>
|
||||
<text class="tool-item-title pointer-events-none">{{ item.title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 表情面板 -->
|
||||
<view class="tool-panel" v-if="curTool == 'emoji' && isShowPanel">
|
||||
<view class="panel-grid panel-emoji">
|
||||
<view
|
||||
class="panel-emoji-item"
|
||||
v-for="item in allEmojiTools"
|
||||
:key="item"
|
||||
:data-name="item"
|
||||
@tap="onToolEmojiItem"
|
||||
>
|
||||
{{ item }}
|
||||
</view>
|
||||
</view>
|
||||
<!-- #ifdef H5 -->
|
||||
<view class="editor-backspace iconfont icon-tuige" @click="onBackSpace"></view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP -->
|
||||
<view v-if="!isIOS" class="editor-backspace iconfont icon-tuige" @click="onBackSpace"></view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
<!-- 更多功能面板 -->
|
||||
<view class="tool-panel" v-if="curTool == 'more' && isShowPanel">
|
||||
<view class="panel-grid panel-more">
|
||||
<view
|
||||
class="panel-more-item"
|
||||
v-for="item in allMoreTools"
|
||||
:key="item.title"
|
||||
:title="item.title"
|
||||
:data-name="item.name"
|
||||
:data-value="item.value"
|
||||
@tap="onToolMoreItem"
|
||||
>
|
||||
<view class="iconfont pointer-events-none" :class="item.icon"></view>
|
||||
<view class="panel-more-item-title pointer-events-none">{{ item.title }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 扩展面板 -->
|
||||
<view class="tool-panel" v-if="curTool == 'setting' && isShowPanel">
|
||||
<slot name="setting"></slot>
|
||||
</view>
|
||||
|
||||
<!-- 弹窗 因vue2/3的v-model写法有区别,故需要条件编译,我也是醉了 -->
|
||||
<!-- #ifdef VUE3 -->
|
||||
<sv-editor-popup-more v-model:show="showMorePop" :tool-name="curMoreTool" @confirm="moreItemConfirm">
|
||||
<!-- APP端不支持循环插槽,此处建议挨个写 -->
|
||||
<!-- <template v-for="(slot, name) in $slots" #[name]="scope">
|
||||
<slot :name="name" v-bind="scope"></slot>
|
||||
</template> -->
|
||||
<template #at>
|
||||
<slot name="at"></slot>
|
||||
</template>
|
||||
<template #topic>
|
||||
<slot name="topic"></slot>
|
||||
</template>
|
||||
</sv-editor-popup-more>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 弹窗 特别是微信小程序端的vue2,必须使用.sync,服 -->
|
||||
<!-- #ifdef VUE2 -->
|
||||
<sv-editor-popup-more :show.sync="showMorePop" :tool-name="curMoreTool" @confirm="moreItemConfirm">
|
||||
<template #at>
|
||||
<slot name="at"></slot>
|
||||
</template>
|
||||
<template #topic>
|
||||
<slot name="topic"></slot>
|
||||
</template>
|
||||
</sv-editor-popup-more>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 调色板 -->
|
||||
<!-- #ifdef VUE3 -->
|
||||
<sv-editor-colorpicker
|
||||
v-model:show="showColorPicker"
|
||||
:type="colorType"
|
||||
:color="curColor"
|
||||
@confirm="selectColor"
|
||||
></sv-editor-colorpicker>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef VUE2 -->
|
||||
<sv-editor-colorpicker
|
||||
:show.sync="showColorPicker"
|
||||
:type="colorType"
|
||||
:color="curColor"
|
||||
@confirm="selectColor"
|
||||
></sv-editor-colorpicker>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* sv-editor 默认工具栏
|
||||
* @author sonve
|
||||
* @version 1.0.0
|
||||
* @date 2024-12-04
|
||||
*/
|
||||
|
||||
import store from '../common/store.js'
|
||||
import { toolList, emojiToolList, styleToolList, moreToolList } from '../common/tool-list.js'
|
||||
import { noKeyboardEffect } from '../common/utils.js'
|
||||
import SvEditorPopupMore from './sv-editor-popup-more.vue'
|
||||
import SvEditorColorpicker from './sv-editor-colorpicker.vue'
|
||||
|
||||
export default {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序特殊配置
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
virtualHost: true,
|
||||
styleIsolation: 'shared'
|
||||
},
|
||||
// #endif
|
||||
components: {
|
||||
SvEditorPopupMore,
|
||||
SvEditorColorpicker
|
||||
},
|
||||
props: {
|
||||
// 工具栏列表
|
||||
tools: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [] // 空默认为全列表
|
||||
}
|
||||
},
|
||||
// 样式工具列表
|
||||
styleTools: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [] // 空默认为全列表
|
||||
}
|
||||
},
|
||||
// 更多功能列表
|
||||
moreTools: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [] // 空默认为全列表
|
||||
}
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'toolMoreItem',
|
||||
'moreItemConfirm',
|
||||
'keyboardChange',
|
||||
'changeMorePop',
|
||||
'changeTool',
|
||||
'tapTool',
|
||||
'tapStyle',
|
||||
'tapEmoji',
|
||||
'backspace'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
curTool: '', // 当前工具(头部工具栏)默认第一个
|
||||
showPanel: false, // 是否能显示工具面板,区别于isShowPanel
|
||||
showMorePop: false, // 是否弹出更多功能面板弹窗
|
||||
showColorPicker: false, // 是否弹出调色板
|
||||
curColor: '', // 当前颜色
|
||||
curTextColor: '', // 当前文字颜色暂存
|
||||
curBgColor: '', // 当前背景颜色暂存
|
||||
colorType: '', // 当前颜色模式,可选 color | backgroundColor
|
||||
curMoreTool: '', // 当前所选的更多功能项
|
||||
keyboardHeight: 0 // 键盘高度
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isIOS() {
|
||||
return uni.getSystemInfoSync().platform == 'ios'
|
||||
},
|
||||
allTools() {
|
||||
if (this.tools.length == 0) return toolList
|
||||
const indexMap = new Map(this.tools.map((item, index) => [item, index]))
|
||||
const filtered = toolList
|
||||
.filter((item) => indexMap.has(item.name)) // 过滤
|
||||
.sort((a, b) => indexMap.get(a.name) - indexMap.get(b.name)) // 排序
|
||||
return filtered
|
||||
},
|
||||
allStyleTools() {
|
||||
if (this.styleTools.length == 0) return styleToolList
|
||||
const indexMap = new Map(this.styleTools.map((item, index) => [item, index]))
|
||||
const filtered = styleToolList
|
||||
.filter((item) => indexMap.has(item.name)) // 过滤
|
||||
.sort((a, b) => indexMap.get(a.name) - indexMap.get(b.name)) // 排序
|
||||
return filtered
|
||||
},
|
||||
allEmojiTools() {
|
||||
return emojiToolList
|
||||
},
|
||||
allMoreTools() {
|
||||
if (this.moreTools.length == 0) return moreToolList
|
||||
const indexMap = new Map(this.moreTools.map((item, index) => [item, index]))
|
||||
const filtered = moreToolList
|
||||
.filter((item) => indexMap.has(item.name)) // 过滤
|
||||
.sort((a, b) => indexMap.get(a.name) - indexMap.get(b.name)) // 排序
|
||||
return filtered
|
||||
},
|
||||
/**
|
||||
* 在微信小程序端的vue2环境下无法直接使用计算属性读取editorCtx
|
||||
* 为了统一化,只在各自需要使用编辑器实例的地方,按需重新获取
|
||||
*/
|
||||
// editorCtx() {
|
||||
// const eid = store.actions.getEID()
|
||||
// return store.actions.getEditor(eid)
|
||||
// },
|
||||
formats() {
|
||||
return store.actions.getFormats()
|
||||
},
|
||||
isReadOnly: {
|
||||
set(newVal) {
|
||||
store.actions.setReadOnly(newVal)
|
||||
},
|
||||
get() {
|
||||
return store.actions.getReadOnly()
|
||||
}
|
||||
},
|
||||
isShowPanel() {
|
||||
let show = this.showPanel
|
||||
/**
|
||||
* 规则:
|
||||
* 1. 当键盘弹出时,必须折叠面板
|
||||
* 2. 当点击有面板的工具栏时,必须展开面板
|
||||
* 3. 展开工具栏时,可以点击fold进行展开折叠切换
|
||||
*/
|
||||
if (this.keyboardHeight !== 0) {
|
||||
show = this.showMorePop ? true : false
|
||||
} else {
|
||||
if (!this.curTool) {
|
||||
show = false
|
||||
}
|
||||
}
|
||||
return show
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
curTool(newVal) {
|
||||
this.$emit('changeTool', newVal)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.curTool = this.allTools[0].name // 当前工具(头部工具栏)默认第一个
|
||||
|
||||
uni.$on('E_EDITOR_STATUSCHANGE', (e) => {
|
||||
this.curTextColor = e.detail.color || ''
|
||||
this.curBgColor = e.detail.backgroundColor || ''
|
||||
})
|
||||
|
||||
// #ifndef H5
|
||||
uni.onKeyboardHeightChange(this.keyboardChange)
|
||||
// #endif
|
||||
},
|
||||
destroyed() {
|
||||
// #ifndef H5
|
||||
uni.offKeyboardHeightChange(this.keyboardChange)
|
||||
// #endif
|
||||
uni.$off('E_EDITOR_STATUSCHANGE')
|
||||
},
|
||||
unmounted() {
|
||||
// #ifndef H5
|
||||
uni.offKeyboardHeightChange(this.keyboardChange)
|
||||
// #endif
|
||||
uni.$off('E_EDITOR_STATUSCHANGE')
|
||||
},
|
||||
methods: {
|
||||
getEditorCtx() {
|
||||
const eid = store.actions.getEID()
|
||||
return store.actions.getEditor(eid)
|
||||
},
|
||||
onTool(e) {
|
||||
this.editorCtx = this.getEditorCtx() // 按需重新获取编辑器实例
|
||||
if (!this.editorCtx) return console.warn('editor is null')
|
||||
const { name, value } = e.target.dataset
|
||||
|
||||
this.$emit('tapTool', { name, value })
|
||||
|
||||
switch (name) {
|
||||
case 'style':
|
||||
case 'emoji':
|
||||
case 'more':
|
||||
case 'setting':
|
||||
this.curTool = name
|
||||
this.showPanel = true
|
||||
break
|
||||
case 'undo':
|
||||
noKeyboardEffect(() => {
|
||||
this.editorCtx.undo()
|
||||
})
|
||||
break
|
||||
case 'redo':
|
||||
noKeyboardEffect(() => {
|
||||
this.editorCtx.redo()
|
||||
})
|
||||
break
|
||||
case 'fold':
|
||||
this.showPanel = value == '1' ? true : false
|
||||
break
|
||||
}
|
||||
// 点击toolbar需要主动聚焦
|
||||
// #ifdef H5
|
||||
noKeyboardEffect(() => {
|
||||
this.editorCtx.focus()
|
||||
})
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
if (!this.isIOS) {
|
||||
noKeyboardEffect(() => {
|
||||
this.editorCtx.focus()
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
onToolStyleItem(e) {
|
||||
const { name, value } = e.target.dataset
|
||||
this.$emit('tapStyle', { name, value })
|
||||
|
||||
this.editorCtx = this.getEditorCtx() // 按需重新获取编辑器实例
|
||||
switch (name) {
|
||||
case 'divider':
|
||||
// 分割线单独使用insertDivider处理
|
||||
noKeyboardEffect(() => {
|
||||
this.editorCtx.insertDivider()
|
||||
})
|
||||
break
|
||||
case 'color':
|
||||
this.colorType = name
|
||||
this.curColor = this.curTextColor
|
||||
this.showColorPicker = true
|
||||
break
|
||||
case 'backgroundColor':
|
||||
this.colorType = name
|
||||
this.curColor = this.curBgColor
|
||||
this.showColorPicker = true
|
||||
break
|
||||
case 'removeformat':
|
||||
// 清除当前选区的样式
|
||||
uni.showModal({
|
||||
title: '系统提示',
|
||||
content: '是否清除当前选区样式',
|
||||
success: ({ confirm }) => {
|
||||
if (confirm) {
|
||||
noKeyboardEffect(() => {
|
||||
this.editorCtx.removeFormat()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
case 'bold':
|
||||
case 'italic':
|
||||
case 'underline':
|
||||
case 'strike':
|
||||
case 'script':
|
||||
// 部分格式需要弹出键盘,若禁止弹出键盘,则会使格式丢失
|
||||
this.editorCtx.format(name, value)
|
||||
break
|
||||
default:
|
||||
noKeyboardEffect(() => {
|
||||
this.editorCtx.format(name, value)
|
||||
})
|
||||
break
|
||||
}
|
||||
},
|
||||
onToolEmojiItem(e) {
|
||||
const { name, value } = e.target.dataset
|
||||
|
||||
this.$emit('tapEmoji', { name, value })
|
||||
|
||||
this.editorCtx = this.getEditorCtx() // 按需重新获取编辑器实例
|
||||
noKeyboardEffect(() => {
|
||||
this.editorCtx.insertText({
|
||||
text: name
|
||||
})
|
||||
})
|
||||
},
|
||||
onToolMoreItem(e) {
|
||||
const { name, value } = e.target.dataset
|
||||
this.curMoreTool = name
|
||||
if (value == 'popup') this.openMorePop()
|
||||
this.$emit('toolMoreItem', { name, value })
|
||||
},
|
||||
moreItemConfirm(e) {
|
||||
this.$emit('moreItemConfirm', e)
|
||||
},
|
||||
// 打开内置更多功能弹窗
|
||||
openMorePop() {
|
||||
this.showMorePop = true
|
||||
this.$emit('changeMorePop', this.showMorePop)
|
||||
},
|
||||
// 关闭内置更多功能弹窗
|
||||
closeMorePop() {
|
||||
this.showMorePop = false
|
||||
this.$emit('changeMorePop', this.showMorePop)
|
||||
},
|
||||
/**
|
||||
* 键盘相关方法
|
||||
*/
|
||||
keyboardChange(e) {
|
||||
this.keyboardHeight = e.height
|
||||
|
||||
this.$emit('keyboardChange', e)
|
||||
|
||||
if (this.showMorePop) return
|
||||
|
||||
// #ifdef H5
|
||||
if (this.keyboardHeight > 0) {
|
||||
this.showPanel = false
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 可能存在秒闪的情况, 因此需要短暂延后判断
|
||||
const timerHandler = () => {
|
||||
if (this.timer) {
|
||||
// 清除已有的计时器
|
||||
clearTimeout(this.timer)
|
||||
this.timer = null
|
||||
}
|
||||
this.timer = setTimeout(() => {
|
||||
if (this.keyboardHeight > 0) {
|
||||
this.showPanel = false
|
||||
}
|
||||
this.timer = null
|
||||
}, 50)
|
||||
}
|
||||
|
||||
// #ifdef APP
|
||||
if (this.isIOS) {
|
||||
timerHandler()
|
||||
} else {
|
||||
if (this.keyboardHeight > 0) {
|
||||
this.showPanel = false
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
timerHandler()
|
||||
// #endif
|
||||
},
|
||||
// 退格
|
||||
onBackSpace() {
|
||||
this.$emit('backspace')
|
||||
// #ifdef H5 || APP
|
||||
this.editorCtx = this.getEditorCtx() // 按需重新获取编辑器实例
|
||||
noKeyboardEffect(() => {
|
||||
this.editorCtx.backspace()
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
// 调色板确认
|
||||
selectColor(color, type) {
|
||||
this.curColor = color
|
||||
this.showColorPicker = false
|
||||
if (type == 'color') {
|
||||
this.curTextColor = color
|
||||
} else {
|
||||
this.curBgColor = color
|
||||
}
|
||||
// 确认颜色选择后不要noKeyboardEffect取消键盘,会造成颜色格式丢失
|
||||
this.editorCtx = this.getEditorCtx() // 按需重新获取编辑器实例
|
||||
this.editorCtx.format(type, color)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../icons/iconfont.css';
|
||||
|
||||
.sv-editor-toolbar {
|
||||
--editor-toolbar-height: 88rpx;
|
||||
--editor-toolbar-bgcolor: #ffffff;
|
||||
--editor-toolbar-bordercolor: #eeeeee;
|
||||
--editor-toolbar-iconsize: 32rpx;
|
||||
--tool-panel-height: auto;
|
||||
--tool-panel-bgcolor: #ffffff;
|
||||
--tool-panel-max-height: 400rpx;
|
||||
--tool-style-columns: 3;
|
||||
--tool-style-iconsize: 32rpx;
|
||||
--tool-style-titlesize: 28rpx;
|
||||
--tool-emoji-columns: 8;
|
||||
--tool-more-columns: 4;
|
||||
--tool-more-iconsize: 60rpx;
|
||||
--tool-more-titlesize: 24rpx;
|
||||
--tool-item-bgcolor: #f1f1f1;
|
||||
--editor-backspace-bgcolor: #ffffff;
|
||||
--editor-backspace-shadow: 0 0 8px 6px rgba(0, 0, 0, 0.08);
|
||||
|
||||
.editor-tools {
|
||||
width: 100%;
|
||||
height: var(--editor-toolbar-height);
|
||||
background-color: var(--editor-toolbar-bgcolor);
|
||||
border-top: 1rpx solid var(--editor-toolbar-bordercolor);
|
||||
border-bottom: 1rpx solid var(--editor-toolbar-bordercolor);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
box-sizing: border-box;
|
||||
|
||||
.iconfont {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: var(--editor-toolbar-iconsize);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-panel {
|
||||
height: var(--tool-panel-height);
|
||||
max-height: var(--tool-panel-max-height);
|
||||
overflow: auto;
|
||||
padding: 30rpx;
|
||||
box-sizing: border-box;
|
||||
// position: relative;
|
||||
background-color: var(--tool-panel-bgcolor);
|
||||
|
||||
.editor-backspace {
|
||||
width: 80rpx;
|
||||
height: 60rpx;
|
||||
position: absolute;
|
||||
bottom: 30rpx;
|
||||
right: 30rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 50rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: var(--editor-backspace-bgcolor);
|
||||
box-shadow: var(--editor-backspace-shadow);
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
bottom: 32rpx;
|
||||
right: 32rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-grid {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
align-items: center; /* 垂直居中 */
|
||||
justify-items: center; /* 水平居中 */
|
||||
gap: 30rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.panel-style {
|
||||
grid-template-columns: repeat(var(--tool-style-columns), 1fr);
|
||||
}
|
||||
|
||||
&.panel-emoji {
|
||||
grid-template-columns: repeat(var(--tool-emoji-columns), 1fr);
|
||||
}
|
||||
|
||||
&.panel-more {
|
||||
grid-template-columns: repeat(var(--tool-more-columns), 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.panel-style-item {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--tool-item-bgcolor);
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
.tool-item-title {
|
||||
font-size: var(--tool-style-titlesize);
|
||||
}
|
||||
.iconfont {
|
||||
font-size: var(--tool-style-iconsize);
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-emoji-item {
|
||||
}
|
||||
|
||||
.panel-more-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--tool-item-bgcolor);
|
||||
padding: 20rpx;
|
||||
border-radius: 20rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-size: var(--tool-more-iconsize);
|
||||
}
|
||||
.panel-more-item-title {
|
||||
font-size: var(--tool-more-titlesize);
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ql-active {
|
||||
color: #66ccff;
|
||||
}
|
||||
.pointer-events-none {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
467
uni_modules/sv-editor/components/sv-editor/sv-editor.vue
Normal file
@ -0,0 +1,467 @@
|
||||
<template>
|
||||
<view class="sv-editor-wrapper" @longpress="eLongpress">
|
||||
<slot name="header"></slot>
|
||||
<editor
|
||||
:id="eid"
|
||||
class="sv-editor-container"
|
||||
show-img-size
|
||||
show-img-toolbar
|
||||
show-img-resize
|
||||
:placeholder="placeholder"
|
||||
:read-only="isReadOnly"
|
||||
@statuschange="onStatusChange"
|
||||
@ready="onEditorReady"
|
||||
@input="onEditorInput"
|
||||
@focus="onEditorFocus"
|
||||
@blur="onEditorBlur"
|
||||
></editor>
|
||||
<view class="maxlength-tip" v-if="maxlength > 0 && !hideMax">{{ textlength }}/{{ maxlength }}</view>
|
||||
<slot name="footer"></slot>
|
||||
<!-- renderjs辅助插件 -->
|
||||
<!-- #ifdef APP || H5 -->
|
||||
<sv-editor-render ref="editorRenderRef" :eid="editorEID"></sv-editor-render>
|
||||
<sv-editor-plugin ref="editorPluginRef" :sid="startID" :eid="editorEID" @epaste="ePaste"></sv-editor-plugin>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* sv-editor
|
||||
* @author sonve
|
||||
* @version 1.0.0
|
||||
* @date 2024-12-04
|
||||
*/
|
||||
|
||||
import store from '../common/store.js'
|
||||
import { linkFlag, copyrightPrint } from '../common/utils.js'
|
||||
import { parseHtmlWithVideo, replaceVideoWithImageRender } from '../common/parse.js'
|
||||
import SvEditorRender from './sv-editor-render.vue'
|
||||
import SvEditorPlugin from '../plugins/sv-editor-plugin.vue'
|
||||
import wxplugin from '../plugins/sv-editor-wxplugin.js'
|
||||
|
||||
export default {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序特殊配置
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
virtualHost: true,
|
||||
styleIsolation: 'shared'
|
||||
},
|
||||
// #endif
|
||||
components: {
|
||||
SvEditorRender,
|
||||
SvEditorPlugin
|
||||
},
|
||||
props: {
|
||||
// 编辑器id可传入,以便循环组件使用,防止id重复
|
||||
eid: {
|
||||
type: String,
|
||||
default: 'sv-editor' // 唯一,禁止重复
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '写点什么吧 ~'
|
||||
},
|
||||
// 是否只读
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 最大字数限制,<=0时表示不限
|
||||
maxlength: {
|
||||
type: Number,
|
||||
default: -1
|
||||
},
|
||||
// 是否关闭最大字数显示
|
||||
hideMax: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 粘贴模式,可选 text 纯文本(默认) | origin 尽可能保持原格式
|
||||
pasteMode: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
}
|
||||
},
|
||||
emits: ['ready', 'input', 'statuschange', 'focus', 'blur', 'overmax', 'epaste'],
|
||||
data() {
|
||||
return {
|
||||
textlength: 0, // 当前字数统计
|
||||
startID: '',
|
||||
// #ifdef VUE2
|
||||
// #ifdef MP-WEIXIN
|
||||
editorIns: null // 仅vue2环境下的微信小程序需要声明实例变量,否则报错,属实逆天
|
||||
// #endif
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
editorEID: {
|
||||
set(newVal) {
|
||||
store.actions.setEID(newVal)
|
||||
},
|
||||
get() {
|
||||
return store.actions.getEID()
|
||||
}
|
||||
},
|
||||
editorCtx: {
|
||||
set(newVal) {
|
||||
store.actions.setEditor(newVal.eid, newVal.ctx)
|
||||
// #ifdef VUE2
|
||||
this.editorIns = newVal.ctx
|
||||
this.editorIns.id = newVal.eid
|
||||
// #endif
|
||||
},
|
||||
get() {
|
||||
let instance = store.actions.getEditor(this.eid)
|
||||
// #ifdef VUE2
|
||||
instance = store.actions.getEditor(this.eid) || this.editorIns
|
||||
// #endif
|
||||
return instance
|
||||
}
|
||||
},
|
||||
isReadOnly: {
|
||||
set(newVal) {
|
||||
store.actions.setReadOnly(newVal)
|
||||
},
|
||||
get() {
|
||||
return store.actions.getReadOnly()
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
readOnly(newVal) {
|
||||
this.isReadOnly = newVal
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 首个实例初始化时执行
|
||||
if (!store.state.firstInstanceFlag) {
|
||||
this.editorEID = this.eid
|
||||
store.state.firstInstanceFlag = this.eid
|
||||
copyrightPrint()
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
store.actions.destroy()
|
||||
},
|
||||
unmounted() {
|
||||
store.actions.destroy()
|
||||
},
|
||||
methods: {
|
||||
onEditorReady() {
|
||||
this.$nextTick(() => {
|
||||
uni
|
||||
.createSelectorQuery()
|
||||
.in(this)
|
||||
.select('#' + this.eid)
|
||||
.context((res) => {
|
||||
// 存储上下文
|
||||
this.editorCtx = { eid: this.eid, ctx: res.context }
|
||||
// 挂载实例api
|
||||
this.bindMethods()
|
||||
// 初始化完成
|
||||
this.$emit('ready', this.editorCtx)
|
||||
|
||||
// #ifdef APP || H5
|
||||
if (this.pasteMode == 'origin') this.editorCtx.changePasteMode('origin')
|
||||
// #endif
|
||||
})
|
||||
.exec()
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 挂载实例api
|
||||
*/
|
||||
bindMethods() {
|
||||
// ===== renderjs相关扩展api =====
|
||||
|
||||
// #ifdef APP || H5
|
||||
/**
|
||||
* 主动聚焦
|
||||
* @returns {void}
|
||||
*/
|
||||
//this.editorCtx.focus = this.$refs.editorRenderRef.focus
|
||||
|
||||
|
||||
/**
|
||||
* 退格
|
||||
* @returns {void}
|
||||
*/
|
||||
this.editorCtx.backspace = this.$refs.editorRenderRef.backspace
|
||||
|
||||
/**
|
||||
* 键盘输入模式
|
||||
* @param {String} type 模式,可选:none | remove
|
||||
* @returns {void}
|
||||
*/
|
||||
this.editorCtx.changeInputMode = this.$refs.editorRenderRef.changeInputMode
|
||||
|
||||
/**
|
||||
* 粘贴模式
|
||||
* @param {String} type 模式,可选:text纯文本(默认) | origin尽可能保持原格式
|
||||
* @returns {void}
|
||||
*/
|
||||
this.editorCtx.changePasteMode = (type) => {
|
||||
// 告知plugin启动
|
||||
this.startID = this.eid
|
||||
if (this.$refs.editorPluginRef?.changePasteMode) {
|
||||
this.$refs.editorPluginRef.changePasteMode(type)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成视频封面图
|
||||
* @param {String} url 封面图片地址
|
||||
* @returns {Promise} 携带播放图标的封面图地址
|
||||
*/
|
||||
this.editorCtx.createCoverThumbnail = (url) => {
|
||||
return new Promise((resolve) => {
|
||||
if (this.$refs.editorPluginRef?.createCoverThumbnail) {
|
||||
// 事件名必须唯一,否则会覆盖
|
||||
uni.$once(`E_EDITOR_GET_COVER_THUMBNAIL_${url}`, (res) => {
|
||||
resolve(res.cover)
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.$refs.editorPluginRef?.createCoverThumbnail(url)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成视频封面图
|
||||
* @param {String} url 视频地址
|
||||
* @returns {Promise} 封面图地址
|
||||
*/
|
||||
this.editorCtx.createVideoThumbnail = (url) => {
|
||||
return new Promise((resolve) => {
|
||||
if (this.$refs.editorPluginRef?.createVideoThumbnail) {
|
||||
// 事件名必须唯一,否则会覆盖
|
||||
uni.$once(`E_EDITOR_GET_VIDEO_THUMBNAIL_${url}`, (res) => {
|
||||
resolve(res.cover)
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.$refs.editorPluginRef?.createVideoThumbnail(url)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
// ===== 微信小程序扩展api =====
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
|
||||
/**
|
||||
* 生成视频封面图
|
||||
* @param {String} url 视频地址
|
||||
* @returns {Promise} 封面图地址
|
||||
*/
|
||||
this.editorCtx.createCoverThumbnail = wxplugin?.wxCreateCoverThumbnail
|
||||
//this.editorCtx.createVideoThumbnail = wxplugin?.wxCreateVideoThumbnail
|
||||
|
||||
// #endif
|
||||
|
||||
// ===== 通用扩展api =====
|
||||
|
||||
/**
|
||||
* 主动触发input回调事件
|
||||
* @returns {void}
|
||||
*/
|
||||
// this.editorCtx.blur = ()=>{
|
||||
// this.$refs.editorRenderRef.blur();
|
||||
// };
|
||||
this.editorCtx.changeInput = () => {
|
||||
this.editorCtx.getContents({
|
||||
success: (res) => {
|
||||
this.$emit('input', { ctx: this.editorCtx, html: res.html, text: res.text })
|
||||
}
|
||||
})
|
||||
}
|
||||
this.editorCtx.scrollView=()=>{
|
||||
this.editorCtx.scrollIntoView();
|
||||
}
|
||||
/**
|
||||
* 获取最新内容
|
||||
* @returns {Promise} 内容对象 { html, text... }
|
||||
*/
|
||||
this.editorCtx.getLastContent = async () => {
|
||||
return new Promise((resolve) => {
|
||||
this.editorCtx.getContents({
|
||||
success: (res) => {
|
||||
resolve(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 富文本内容初始化
|
||||
* 注意:微信小程序会导致聚焦滚动,建议先将编辑器v-show=false,待initHtml内容初始化完成后再true
|
||||
* 也正是因为微信小程序端会聚焦滚动,所以editorEID在初始阶段会默认保持最后一个实例eid,需要手动重新聚焦
|
||||
* @param {String} html 初始化的富文本
|
||||
* @param {Function<Promise>} customCallback 自定义处理封面回调,需要以Promise形式返回封面图片资源
|
||||
* @returns {void}
|
||||
*/
|
||||
this.editorCtx.initHtml = async (html, customCallback) => {
|
||||
console.log('打印html');
|
||||
console.log(html);
|
||||
let transHtml = await replaceVideoWithImageRender(html, customCallback)
|
||||
console.log('transHtml')
|
||||
console.log(transHtml);
|
||||
// #ifdef APP || H5
|
||||
this.editorCtx.changePasteMode('text') // text模式下可以防止初始化时对格式的影响
|
||||
// #endif
|
||||
setTimeout(() => {
|
||||
|
||||
this.editorCtx.setContents({
|
||||
html: transHtml,
|
||||
success: () => {
|
||||
console.log('成功了')
|
||||
// 主动触发一次input回调事件
|
||||
this.editorCtx.changeInput()
|
||||
// #ifdef APP || H5
|
||||
if (this.pasteMode == 'origin') this.editorCtx.changePasteMode('origin')
|
||||
// #endif
|
||||
this.editorCtx.blur();
|
||||
|
||||
},
|
||||
fail:(err)=>{
|
||||
console.log('失败了')
|
||||
uni.showToast({
|
||||
title:err,
|
||||
icon:'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出处理
|
||||
* @param {String} html 要导出的富文本
|
||||
* @returns {String} 处理后的富文本
|
||||
*/
|
||||
this.editorCtx.exportHtml = (html) => {
|
||||
return parseHtmlWithVideo(html)
|
||||
}
|
||||
},
|
||||
onEditorInput(e) {
|
||||
// 注意不要使用getContents获取html和text,会导致重复触发onStatusChange从而失去toolbar工具的高亮状态
|
||||
// 复制粘贴的时候detail会为空,此时应当直接return
|
||||
if (Object.keys(e.detail).length <= 0) return
|
||||
const { html, text } = e.detail
|
||||
|
||||
// 识别到链接特殊标识立即return
|
||||
if (text.indexOf(linkFlag) !== -1) return
|
||||
|
||||
/**
|
||||
* 因为uni-editor不提供最大字符限制,故需要手动进行以下特殊处理
|
||||
*/
|
||||
const maxlength = parseInt(this.maxlength)
|
||||
const textStr = text.replace(/[ \t\r\n]/g, '')
|
||||
this.textlength = textStr.length // 当前字符数
|
||||
if (this.textlength >= maxlength && maxlength > 0) {
|
||||
this.textlength = maxlength // 因为editor特性,需要手动赋阈值
|
||||
if (!this.lockHtmlFlag) {
|
||||
this.lockHtml = html // 锁定最后一次超出字数前的html
|
||||
this.lockHtmlFlag = true // 锁定标志
|
||||
// 首次到达最大限制时还需最后回调一次input事件
|
||||
this.$emit('input', { ctx: this.editorCtx, html, text })
|
||||
} else {
|
||||
// 在超过字数时锁定,若再编辑则抛出超出事件
|
||||
this.$emit('overmax', { ctx: this.editorCtx })
|
||||
}
|
||||
// 超过字数时,锁定最后一次超出字数前的html
|
||||
this.editorCtx.setContents({ html: this.lockHtml })
|
||||
} else {
|
||||
// 正常输入
|
||||
this.$emit('input', { ctx: this.editorCtx, html, text })
|
||||
this.lockHtmlFlag = false // 锁定标志
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 样式格式改变时触发
|
||||
* 注意:微信小程序端在多编辑器实例下切换编辑器后可能不会及时触发onStatusChange
|
||||
*/
|
||||
onStatusChange(e) {
|
||||
store.actions.setFormats(e.detail)
|
||||
this.$emit('statuschange', { ...e, ctx: this.editorCtx })
|
||||
uni.$emit('E_EDITOR_STATUSCHANGE', { ...e, ctx: this.editorCtx })
|
||||
},
|
||||
onEditorFocus(e) {
|
||||
this.editorEID = this.eid
|
||||
this.$emit('focus', { ...e, ctx: this.editorCtx })
|
||||
},
|
||||
onEditorBlur(e) {
|
||||
this.$emit('blur', { ...e, ctx: this.editorCtx })
|
||||
},
|
||||
ePaste(e) {
|
||||
this.$emit('epaste', { ...e, ctx: this.editorCtx })
|
||||
uni.$emit('E_EDITOR_PASTE', { ...e, ctx: this.editorCtx })
|
||||
},
|
||||
/**
|
||||
* 微信小程序官方editor的长按事件有bug,需要重写覆盖,不需做任何逻辑,可见下面小程序社区问题链接
|
||||
* @tutorial https://developers.weixin.qq.com/community/develop/doc/000c04b3e1c1006f660065e4f61000
|
||||
*/
|
||||
eLongpress() {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sv-editor-wrapper {
|
||||
--maxlength-text-color: #666666;
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.sv-editor-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.maxlength-tip {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
font-size: 24rpx;
|
||||
color: var(--maxlength-text-color);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
// placeholder字样
|
||||
.sv-editor-container ::v-deep .ql-blank::before {
|
||||
font-style: normal;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
// 图片工具样式
|
||||
::v-deep .ql-container {
|
||||
min-height: unset;
|
||||
|
||||
.ql-image-overlay {
|
||||
pointer-events: none;
|
||||
|
||||
.ql-image-size {
|
||||
right: 28px !important;
|
||||
}
|
||||
.ql-image-toolbar {
|
||||
// 删除按钮
|
||||
pointer-events: auto;
|
||||
}
|
||||
.ql-image-handle {
|
||||
// 四角缩放按钮
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
87
uni_modules/sv-editor/package.json
Normal file
@ -0,0 +1,87 @@
|
||||
{
|
||||
"id": "sv-editor",
|
||||
"displayName": "基于官方 uni-editor 的富文本编辑器",
|
||||
"version": "1.1.2",
|
||||
"description": "可插入图片、视频、链接、@提及、#话题、Emoji表情包,且优化了聚焦键盘闪烁等问题",
|
||||
"keywords": [
|
||||
"富文本",
|
||||
"编辑器",
|
||||
"editor",
|
||||
"html"
|
||||
],
|
||||
"repository": "https://gitee.com/Sonve/sv-editor",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.1.0"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "插件不采集任何数据",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "y",
|
||||
"aliyun": "y",
|
||||
"alipay": "y"
|
||||
},
|
||||
"client": {
|
||||
"Vue": {
|
||||
"vue2": "y",
|
||||
"vue3": "y"
|
||||
},
|
||||
"App": {
|
||||
"app-vue": "y",
|
||||
"app-nvue": "u",
|
||||
"app-uvue": "u",
|
||||
"app-harmony": "u"
|
||||
},
|
||||
"H5-mobile": {
|
||||
"Safari": "y",
|
||||
"Android Browser": "y",
|
||||
"微信浏览器(Android)": "y",
|
||||
"QQ浏览器(Android)": "y"
|
||||
},
|
||||
"H5-pc": {
|
||||
"Chrome": "y",
|
||||
"IE": "y",
|
||||
"Edge": "y",
|
||||
"Firefox": "y",
|
||||
"Safari": "y"
|
||||
},
|
||||
"小程序": {
|
||||
"微信": "y",
|
||||
"阿里": "u",
|
||||
"百度": "u",
|
||||
"字节跳动": "u",
|
||||
"QQ": "u",
|
||||
"钉钉": "u",
|
||||
"快手": "u",
|
||||
"飞书": "u",
|
||||
"京东": "u"
|
||||
},
|
||||
"快应用": {
|
||||
"华为": "u",
|
||||
"联盟": "u"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
333
uni_modules/sv-editor/readme.md
Normal file
@ -0,0 +1,333 @@
|
||||
## 基于官方 uni-editor 的富文本编辑器 [sv-editor]
|
||||
|
||||
### 一、前言
|
||||
首先,你需要了解 uni-editor 相关注意事项,以及api
|
||||
|
||||
传送门:
|
||||
|
||||
1. [editor 组件概况](https://uniapp.dcloud.net.cn/component/editor.html)
|
||||
2. [editorContext api详情](https://uniapp.dcloud.net.cn/api/media/editor-context.html)
|
||||
3. 仔细阅读 [HTML 标签和 style 内联样式支持情况](https://uniapp.dcloud.net.cn/component/editor.html#html-%E6%A0%87%E7%AD%BE%E5%92%8C-style-%E5%86%85%E8%81%94%E6%A0%B7%E5%BC%8F%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5)
|
||||
4. 仔细了解 [注意事项](https://uniapp.dcloud.net.cn/component/editor.html#%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9)
|
||||
|
||||
|
||||
### 二、本插件在官方 uni-editor 基础上做了什么
|
||||
|
||||
1. 提供插入视频的api
|
||||
2. 提供插入链接的api
|
||||
3. 在插入链接的基础上扩展了 @某人、#话题#、以及 添加附件 的api
|
||||
4. 支持插入emoji表情包,可自定义表情包面板
|
||||
5. 解决了在app端插入内容后,编辑器聚焦后自动弹出键盘的问题,提供api可在聚焦的同时取消键盘反复弹出带来的影响!!!
|
||||
6. 工具栏toolbar与编辑器editor分离式写法,让你的代码更加自由
|
||||
7. 插件内部大部分样式由css变量控制,更方便你使用样式穿透去自定义,对有暗黑主题的需求更加友好
|
||||
8. 所有组件添加了 styleIsolation: 'shared' 配置项,再也不用怕小程序端的样式隔离穿透不了
|
||||
9. 部分扩展基于renderjs,因此小程序端无法使用,可见下列关键功能概况详情
|
||||
10. App与H5端关键扩展api如下:
|
||||
- noKeyboardEffect:取消键盘影响,不想让富文本聚焦后总是自动弹出键盘?这个api可以完美解决你的问题
|
||||
- focus:主动聚焦,你可以直接通过 editorCtx 实例调用此api,以便直接主动使富文本聚焦
|
||||
- backspace:主动退格(删除),希望可以模拟键盘上的退格键?这个api如同键盘的 backspace 键一样,删除光标前一个单位,或者删除所选区域
|
||||
- 等等其他api,详见下文
|
||||
|
||||
### 三、兼容性
|
||||
|
||||
✅已兼容,❌未兼容
|
||||
|
||||
| VUE2 | VUE3 | APP(Android) | APP(iOS)| H5 | 微信小程序 | 其他小程序 |
|
||||
| :---:| :---:| :---: | :---: | :---: | :---: | :---: |
|
||||
| ✅ ️| ✅️ | ✅ | ✅ | ✅ ️️ | ✅️️ | ❌(没测过)️️ |
|
||||
|
||||
1. 实际请以真机效果为准,并不能保证所有机型都兼容,如遇到问题还请加群讨论
|
||||
2. 注意:因为部分api基于renderjs,而小程序无法使用renderjs,故部分api和功能并不适配小程序,更多详情会在各api中说明
|
||||
3. 特别注意:**在微信小程序中,生成的 a 标签的 href 属性会被自动抹去,因此在微信小程序中是无法点击超链接跳转的,这点目前微信小程序官方固件不支持,暂时也没啥好的办法**
|
||||
|
||||
### 四、关键功能概况
|
||||
|
||||
✅完美支持,☑可用但或有副作用,❌不支持
|
||||
|
||||
| 功能 | VUE2 | VUE3 | H5 | APP(Android) | APP(iOS) | 微信小程序 |
|
||||
| :---: | :---:| :---:| :---:| :---: | :---: | :---: |
|
||||
| 插入图片 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| 插入视频 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| 插入链接 | ✅ | ✅ | ✅ | ✅ | ✅ | ☑️ |
|
||||
| 插入提及 | ✅ | ✅ | ✅ | ✅ | ✅ | ☑️ |
|
||||
| 插入话题 | ✅ | ✅ | ✅ | ✅ | ✅ | ☑️ |
|
||||
| 插入附件 | ✅ | ✅ | ✅ | ✅ | ✅ | ☑️ |
|
||||
| 主动聚焦 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌️ |
|
||||
| 主动退格 | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| 多编辑器实例 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| 消除键盘影响 | ✅ | ✅ | ✅ | ✅ | ☑ | ☑️ |
|
||||
| 粘贴保留格式 <br/> [(*特殊扩展)](#特殊扩展) | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| 粘贴事件监听 <br/> [(*特殊扩展)](#特殊扩展) | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
| 视频截取封面 | ✅ | ✅ | ✅ | ✅ | ☑️ | ☑ |
|
||||
| 视频回显解析 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| 待补充 ... | | | | | | |
|
||||
|
||||
|
||||
### 五、开始
|
||||
|
||||
1. 因为本插件不依赖其他第三方插件,因此直接点击右上角 `下载并导入HBuilderX` 导入至你的项目中即可
|
||||
2. 强烈建议你先 `使用HBuilderX导入示例项目` ,跑一下示例看亿下先,部分写法可以直接抄示例
|
||||
3. 因为本插件提供除 [editorContext](https://uniapp.dcloud.net.cn/api/media/editor-context.html) 官方api外,额外扩展的api,需要你对js有着基本的掌握,特别是Promise和异步处理
|
||||
4. 本插件仅为富文本编辑器,如要解析回显还请自行寻找富文本解析插件(不推荐rich-text)
|
||||
|
||||
### 六、插件目录结构
|
||||
|
||||
```
|
||||
uni_modules
|
||||
└─ sv-editor
|
||||
├─ components
|
||||
│ ├─ common
|
||||
│ │ ├─ config.js // 配置文件
|
||||
│ │ ├─ file-handler.js // 文件处理方法
|
||||
│ │ ├─ parse.js // 富文本解析工具
|
||||
│ │ ├─ store.js // 插件内全局状态管理
|
||||
│ │ ├─ tool-list.js // 工具栏工具列表
|
||||
│ │ └─ utils.js // 通用工具api
|
||||
│ ├─ icons
|
||||
│ │ ├─ iconfont.css // 字体图标样式
|
||||
│ │ └─ iconfont.ttf // 字体图标
|
||||
│ └─ sv-editor
|
||||
│ ├─ sv-choose-file.vue // 文件选择器
|
||||
│ ├─ sv-editor-popup-more.vue // 更多工具弹窗面板
|
||||
│ ├─ sv-editor-render.vue // renderjs组件
|
||||
│ ├─ sv-editor-toolbar.vue // 内置工具栏
|
||||
│ └─ sv-editor.vue // 编辑器主体
|
||||
├─ changelog.md
|
||||
├─ package.json
|
||||
└─ readme.md
|
||||
```
|
||||
|
||||
### 七、基本使用
|
||||
|
||||
#### sv-editor 编辑器主体
|
||||
|
||||
`符合uni_modules规范,无需引入直接使用`
|
||||
|
||||
1. props
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| eid | String | 'sv-editor' | 编辑器id,唯一,禁止重复,多编辑器实例时必填 |
|
||||
| placeholder | String | '写点什么吧 ~' | 占位字样 |
|
||||
| readOnly | Boolean | false | 是否只读 |
|
||||
| maxlength | Number | -1 | 最大字数限制,<=0时表示不限 |
|
||||
| hideMax | Boolean | false | 是否关闭最大字数显示 |
|
||||
|
||||
2. emits
|
||||
|
||||
| 事件名 | 参数 | 说明 |兼容性 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| ready | ctx 当前编辑器上下文实例 | 编辑器初始化完成时触发 | 通用 |
|
||||
| input | { ctx, html, text } | 编辑器内容改变时触发 | 通用 |
|
||||
| focus | { ctx, event } | 编辑器聚焦时触发 | 通用 |
|
||||
| blur | { ctx, event } | 编辑器失去焦点时触发 | 通用 |
|
||||
| statuschange | { ctx, event } | 通过 Context 方法改变编辑器内样式时触发,返回选区已设置的样式 | 通用 |
|
||||
| overmax | { ctx } | 超过最大字数限制时回调 | 通用 |
|
||||
| epaste <br/> [(*特殊扩展)](#特殊扩展) | { ctx, id, text, html, range } | 粘贴回调事件 | H5、APP |
|
||||
|
||||
- statuschange 事件还提供 uni.$emit('E_EDITOR_STATUSCHANGE', { ctx, event }) 抛出,你可以通过 uni.$on('E_EDITOR_STATUSCHANGE') 进行监听,但是不要忘记在适当的地方off关掉
|
||||
- epaste [(*特殊扩展)](#特殊扩展) 事件还提供 uni.$emit('E_EDITOR_PASTE', { ctx, id, text, html, range }) 抛出,你可以通过 uni.$on('E_EDITOR_PASTE') 进行监听,但是不要忘记在适当的地方off关掉
|
||||
|
||||
#### sv-editor-toolbar 编辑器工具栏
|
||||
|
||||
`与编辑器本体分离,按需引入使用`
|
||||
|
||||
1. props
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| tools | Array | [] 默认空数组即为全工具,可选 [详见 toolList](#toolList) | 工具栏列表,例如 ['style', ...] |
|
||||
| styleTools| Array | [] 默认空数组即为全工具,可选 [详见 styleToolList](#styleToolList) | 样式工具列表,例如 ['header', ...] |
|
||||
| moreTools | Array | [] 默认空数组即为全工具,可选 [详见 moreToolList](#moreToolList) | 更多功能列表,例如 ['image', ...] |
|
||||
|
||||
注意:
|
||||
- 此处 toolList 等为全列表,详见 `uni_modules/sv-editor/components/common/tool-list.js` 文件。
|
||||
- 若只想使用部分工具以及修改顺序,则给组件对应的props属性例如 `:tools="['style', 'undo', 'redo']"` 即可只使用该三项工具且顺序以该数组顺序排序。
|
||||
- 关于图标,本插件内置了 [阿里巴巴矢量图标库](https://www.iconfont.cn/) 的字体图标,如需使用其他图标,请自行替换。
|
||||
|
||||
2. emits
|
||||
|
||||
| 事件名 | 参数 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| toolMoreItem | { name, value } | 点击更多功能面板子项 |
|
||||
| moreItemConfirm | { link, text, file } | 点击更多功能弹窗确认后回调 |
|
||||
| keyboardChange | { height } | 键盘高度变化 |
|
||||
| changeMorePop | true 打开 / false 关闭 | 更多功能弹窗打开/关闭 |
|
||||
| tapTool | { name, value } | 点击工具栏 |
|
||||
| changeTool | 工具name | 工具栏改变 |
|
||||
| tapStyle | { name, value } | 点击样式工具 |
|
||||
| tapEmoji | { name, value } | 点击Emoji表情 |
|
||||
| backspace | | 触发编辑器实例主动使用backspace后回调 |
|
||||
|
||||
##### toolList
|
||||
|
||||
| title | name | value | icon |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 样式 | style | | icon-zitiyanse |
|
||||
| 表情 | emoji | | icon-xiaolian |
|
||||
| 撤销 | undo | | icon-shangyibu1 |
|
||||
| 重做 | redo | | icon-xiayibu1 |
|
||||
| 更多 | more | | icon-icon_tianjia |
|
||||
| 扩展 | setting | | icon-bianji |
|
||||
|
||||
##### styleToolList
|
||||
|
||||
| title | name | value | icon |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 标题 | header | 2 | icon-zitibiaoti |
|
||||
| 分割线 | divider | | icon-fengexian |
|
||||
| 粗体 | bold | | icon-zitijiacu |
|
||||
| 斜体 | italic | | icon-zitixieti |
|
||||
| 下划线 | underline | | icon-zitixiahuaxian |
|
||||
| 删除线 | strike | | icon-zitishanchuxian|
|
||||
| 左对齐 | align | left | icon-zuoduiqi |
|
||||
| 居中 | align | center | icon-juzhongduiqi |
|
||||
| 右对齐 | align | right | icon-youduiqi |
|
||||
| 有序列表 | list | ordered | icon-youxupailie |
|
||||
| 无序列表 | list | bullet | icon-wuxupailie |
|
||||
| 上标 | script | super | icon-zitishangbiao |
|
||||
| 左缩进 | indent | +1 | icon-zuosuojin |
|
||||
| 右缩进 | indent | -1 | icon-yousuojin |
|
||||
| 下标 | script | sub | icon-ziti-xiabiao |
|
||||
| 文字颜色 | color | | icon-wenziyanse |
|
||||
| 背景颜色 | backgroundColor | | icon-beijingyanse' |
|
||||
| 清除格式 | removeformat | | icon-qingchugeshi |
|
||||
|
||||
- 以上为插件内置样式工具,更多详见 [支持设置的样式列表](https://uniapp.dcloud.net.cn/api/media/editor-context.html#editorcontext-format)
|
||||
- 缩进时,需要在解析插件(此处以mp-html为例)中添加如下缩进样式以供识别:
|
||||
```
|
||||
// uni_modules/mp-html/components/mp-html/node/node.vue
|
||||
|
||||
// 不要管插件内原始的样式代码
|
||||
<style>...</style>
|
||||
|
||||
// 直接在该vue文件最底下添加如下scss样式
|
||||
<style lang="scss">
|
||||
@for $i from 1 through 10 {
|
||||
.ql-indent-#{$i} {
|
||||
// 默认一个缩进为2个em单位,此处对应
|
||||
text-indent: #{$i * 2}em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
##### moreToolList
|
||||
|
||||
| title | name | value | icon |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 添加图片 | image | popup | icon-charutupian |
|
||||
| 添加视频 | video | popup | icon-shexiangji |
|
||||
| 添加链接 | link | popup | icon-charulianjie |
|
||||
| 添加附件 | attachment| popup | icon-huixingzhen |
|
||||
| 提及 | at | popup | icon-at |
|
||||
| 话题 | topic | popup | icon-huati |
|
||||
| 清空 | clear | button| icon-shanchu |
|
||||
|
||||
*在微信小程序中,生成的 a 标签的 href 属性会被自动抹去,因此在微信小程序中是无法点击超链接跳转的,这点目前微信小程序官方固件不支持,暂时也没啥好的办法*
|
||||
|
||||
|
||||
##### emojiToolList
|
||||
|
||||
emoji默认列表
|
||||
|
||||
##### colorList
|
||||
|
||||
调色板默认颜色列表
|
||||
|
||||
#### api 合集
|
||||
|
||||
1. [editorContext 官方api](https://uniapp.dcloud.net.cn/api/media/editor-context.html)
|
||||
|
||||
2. 本插件编辑器实例 `editorCtx` 中,你可以直接通过富文本实例调用
|
||||
|
||||
| 方法名 | 参数 | 返回值 | 说明 | 兼容性 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| focus | | | 主动聚焦 | H5、App |
|
||||
| backspace | | | 退格,会触发sv-editor-toolbar的backspace回调函数 | H5、App(Android) |
|
||||
| createVideoThumbnail <br/> [(*特殊扩展)](#特殊扩展)| url:string 视频地址 | 封面图地址 Promise | 以视频资源地址,直接生成视频封面图(需要保证视频资源正常可以播放) | H5、App(Android) |
|
||||
| createCoverThumbnail <br/> [(*特殊扩展)](#特殊扩展)| url:string 图片资源 | 封面图地址 Promise | 若后端返回视频封面但是没有播放图标,可以用此方法在图片中央叠加播放图标,用于作为视频封面 | 通用 |
|
||||
| changeInputMode | type:string 模式,可选:none/remove | | 修改输入模式,该api是取消键盘闪烁的关键,none时将禁止键盘弹出,remove时将移除该限制 | H5、App |
|
||||
| changeInput | | | 主动触发input回调事件 | 通用 |
|
||||
| getLastContent | | { html, text... } 内容对象 Promise | 获取富文本当前最新内容 | 通用 |
|
||||
| exportHtml | html:string 要导出的富文本 | 处理后的富文本 String | 富文本导出,若富文本携带视频,则会自动解析为video标签 | 通用 |
|
||||
| initHtml | html:string 初始化的富文本 <br/> customCallback 详见补充说明 | | 富文本内容初始化,若富文本携带video标签,将会自动进行解析转换 | 通用 |
|
||||
|
||||
- initHtml 在微信小程序端会导致聚焦滚动,建议先将编辑器 v-show=false,待 initHtml 内容初始化完成后再 true。也正是因为微信小程序端会聚焦滚动,所以 editorEID 在初始阶段会默认保持最后一个实例 eid,需要手动重新聚焦
|
||||
- initHtml 第二个参数 customCallback 和 api: replaceVideoWithImageRender 一致,customCallback 为自定义处理封面回调,自带参数为视频地址,需要return封面图片资源,若无有效返回则走默认封面处理,建议配合后端生成视频封面以兼容各端。
|
||||
|
||||
3. `uni_modules/sv-editor/components/common/store.js` 文件中,插件内全局状态仓库,你可以按需引入后通过 store.state 与 store.actions 来访问变量
|
||||
|
||||
| 方法名 | 参数 | 返回值 | 说明 | 兼容性 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| getEditor | eid | eid编辑器实例 | 获取指定eid的编辑器实例 | 通用 |
|
||||
| setEditor | eid, ctx | | 设置eid编辑器实例 | 通用 |
|
||||
| getEID | | 当前编辑器eid | 获取当前编辑器eid | 通用 |
|
||||
| setEID | 当前编辑器eid | | 设置当前编辑器eid | 通用 |
|
||||
| getFormats | | 编辑器样式格式 | 获取编辑器样式格式 | 通用 |
|
||||
| setFormats | 编辑器样式格式 | | 设置编辑器样式格式 | 通用 |
|
||||
| getReadOnly | | 是否只读 Boolean | 获取编辑器是否只读 | 通用 |
|
||||
| setReadOnly | 是否只读 Boolean | | 设置编辑器是否只读 | 通用 |
|
||||
|
||||
4. `uni_modules/sv-editor/components/common/utils.js` 文件中,需要按需引入,实用工具
|
||||
|
||||
| 方法名 | 参数 | 返回值 | 说明 | 兼容性 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| addImage | (uploadFunc必填, options) | Array/Promise 上传的文件 | 添加图片 | 通用 |
|
||||
| addVideo | (uploadFunc必填, options) | Array/Promise 上传的文件 | 添加视频 | 通用 |
|
||||
| addLink | (options, callback) | | 添加链接 | 通用 |
|
||||
| addAttachment | (uploadFunc必填, options, callback) | Object/Promise 上传的文件 | 添加附件 | 通用 |
|
||||
| addAt | (options, callback) | | 添加提及 | 通用 |
|
||||
| addTopic | (options, callback) | | 添加话题 | 通用 |
|
||||
| insertLink | (editorCtx必填, options, callback) | | 插入链接母本:添加链接、添加附件、添加提及、添加话题均基于此 | 通用 |
|
||||
| noKeyboardEffect | (callback必填, options) | | 核心:消除键盘影响,但是微信小程序只能通过编辑器失焦的方式关闭键盘(依然会闪一下) | 通用 |
|
||||
|
||||
5. `uni_modules/sv-editor/components/common/parse.js` 文件中,需要按需引入,正则解析工具
|
||||
|
||||
| 方法名 | 参数 | 返回值 | 说明 | 兼容性 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| replaceVideoWithImageRender| richText:string 要进行处理的富文本字符串 <br/> customCallback 自定义处理封面回调,需要return处理后的封面图片,自带参数为视频地址 | 处理结果 Promise | 带有视频的富文本逆向转换,可通过customCallback回调函数自定义处理封面 | 通用 |
|
||||
| parseHtmlWithVideo | richText:string 要进行处理的富文本字符串 | 处理结果 String | 将含有封面占位图形式的视频富文本转换成正常视频的富文本 | 通用 |
|
||||
| parseImagesAndVideos | richText:string 要进行处理的富文本字符串 | 处理结果 Array < Object >| 解析当前富文本中所有图片和视频 | 通用 |
|
||||
| parseImages | richText:string 要进行处理的富文本字符串 | 处理结果 Array < Object >| 解析当前富文本中所有图片 | 通用 |
|
||||
| parseVideos | richText:string 要进行处理的富文本字符串 | 处理结果 Array < Object >| 解析当前富文本中所有视频 | 通用 |
|
||||
|
||||
6. `uni_modules/sv-editor/components/common/config.js` 配置文件
|
||||
|
||||
| 参数 | 说明 |
|
||||
| :--- | :--- |
|
||||
| video_thumbnail | 视频默认封面,默认封面图可能会失效,原图在示例工程根目录下static文件夹中,建议自行替换 |
|
||||
| video_playicon | 视频封面播放图标(默认三角播放图标) |
|
||||
|
||||
7. 具体使用代码案例请 `使用HBuilderX导入示例项目` 导入示例工程参考
|
||||
|
||||
### 八、特殊扩展
|
||||
|
||||
**本插件提供部分额外特殊扩展功能,具体如下:**
|
||||
|
||||
| 功能 | 说明 | 类型 | 兼容 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 粘贴保留格式 | 粘贴时尽可能的保留原有格式(并非完全复制) | 固有功能 | H5、APP |
|
||||
| epaste | 粘贴回调事件 | 事件 | H5、APP |
|
||||
| createVideoThumbnail| 以视频资源地址,直接生成视频封面图(需要保证视频资源正常可以播放) | api | H5、APP |
|
||||
| createCoverThumbnail| 若后端返回视频封面但是没有播放图标,可以用此方法在图片中央叠加播放图标,用于作为视频封面 | api | 通用 |
|
||||
| 待补充 ... | | | |
|
||||
|
||||
- createCoverThumbnail 在iOS端可能会报 `the operation is insecure` 的错,这是iOS更加严格的安全策略导致的,本地file://协议也会导致跨域,从而污染了画布
|
||||
|
||||
制作不易,特殊扩展功能限时免费开放,感谢支持Thanks♪(・ω・)ノ
|
||||
使用方式:将插件内 backup 文件夹下的文件复制并粘贴进 plugins 文件夹下并覆盖原文件
|
||||
|
||||
### 九、结语
|
||||
|
||||
本插件免费开源(除特殊扩展外),如若借鉴源码还请注明出处,未经授权禁止转载售卖等侵犯版权行为,谢谢!
|
||||
|
||||
如若商用,望您可以联系作者本人,留下您的项目名,我希望能以方式此推广,谢谢!
|
||||
|
||||
感谢您使用本插件,如果在使用过程中遇到任何问题,欢迎在评论区留言,或在 [Gitee](https://gitee.com/Sonve/sv-editor) 上提交issue,我会尽快回复您。
|
||||
|
||||
制作不易,还望五星好评,若能在 [Gitee](https://gitee.com/Sonve/sv-editor) 上点个 ⭐star,不胜感激Thanks♪(・ω・)ノ
|
||||
|
||||
欢迎加群讨论,Q群:
|
||||
① [852637893](https://qm.qq.com/cgi-bin/qm/qr?k=R7DHSqqDI4-xRCfwdUB2e3NrTytHpcVe&jump_from=webapi&authKey=2IpufavBOSPOLdncCt7EFnbmbWrUHg1c8iqNEdTzG8zCvnKb8/0aaLXF4HJzlp2R)
|
||||
② [816646292](https://qm.qq.com/cgi-bin/qm/qr?k=ndZIUqx0xctbq8oDQVTiDir7AUO5jq9X&jump_from=webapi&authKey=fgk45wWObUUvig7FIuFUuM+0IFLvOJI7LMc1d4qNbWAIfehakai/ZfckYfAGLPne)
|
||||
41
uni_modules/z-paging/changelog.md
Normal file
@ -0,0 +1,41 @@
|
||||
## 2.8.6(2025-03-17)
|
||||
1.`新增` 聊天记录模式流式输出(类似chatGPT回答)演示demo。
|
||||
2.`新增` z-paging及其公共子组件支持`HBuilderX`代码文档提示。
|
||||
3.`新增` props:`virtual-in-swiper-slot`,用以解决vue3+(微信小程序或QQ小程序)中,使用非内置列表写法时,若z-paging在`swiper-item`中存在的无法获取slot插入的cell高度进而导致虚拟列表失败的问题。
|
||||
4.`新增` `@scrolltolower`和@`scrolltoupper`支持nvue。
|
||||
5.`修复` 由`v2.8.1`引出的方法`scrollIntoViewById`在微信小程序+vue3中无效的问题。
|
||||
6.`修复` 由`v2.8.1`引出的在子组件内使用z-paging虚拟列表无效的问题。
|
||||
7.`修复` 在微信小程序中基础库版本较高时`wx.getSystemInfoSync is deprecated`警告。
|
||||
8.`优化` 提升下拉刷新在鸿蒙Next中的性能。
|
||||
9.`优化` `@scrolltolower`和`@scrolltoupper`在倒置的聊天记录模式下的触发逻辑。
|
||||
10.`优化` 其他细节调整。
|
||||
## 2.8.5(2025-02-09)
|
||||
1.`新增` 方法`scrollToX`,支持控制x轴滚动到指定位置。
|
||||
2.`修复` 快手小程序中报错`await isn't allowed in non-async function`的问题。
|
||||
3.`修复` 在iOS+nvue中,设置了`:loading-more-enabled="false"`后,调用`scrollToBottom`无法滚动到底部的问题。
|
||||
4.`修复` 在支付宝小程序+页面滚动中,数据为空时空数据图未居中的问题。
|
||||
5.`优化` fetch types修改。
|
||||
## 2.8.4(2024-12-02)
|
||||
1.`修复` 在虚拟列表+vue2中,顶部占位采用transformY方案;在虚拟列表+vue3中,顶部占位采用view占位方案。以解决在vue2+微信小程序+安卓+兼容模式中,可能出现的虚拟列表闪动的问题。
|
||||
2.`修复` 在列表渲染时(尤其是在虚拟列表中)偶现的【点击加载更多】闪现的问题。
|
||||
3.`优化` 统一在RefresherStatus枚举中Loading取值。
|
||||
4.`优化` `defaultPageNo`&`defaultPageSize`修改为只允许number类型。
|
||||
5.`优化` 提升兼容性&细节优化。
|
||||
## 2.8.3(2024-11-27)
|
||||
1.`修复` `doInsertVirtualListItem`插入数据无效的问题。
|
||||
2.`优化` 提升兼容性&细节优化。
|
||||
## 2.8.2(2024-11-25)
|
||||
1.`优化` types中`ZPagingRef`和`ZPagingInstance`支持泛型。
|
||||
## 2.8.1(2024-11-24)
|
||||
1.`新增` 完整的`props`、`slots`、`methods`、`events`的typescript types声明,可在ts中获得绝佳的代码提示体验。
|
||||
2.`新增` `virtual-cell-id-prefix`:虚拟列表cell id的前缀,适用于一个页面有多个虚拟列表的情况,用以区分不同虚拟列表cell的id。
|
||||
3.`修复` 在vue3+(微信小程序或QQ小程序)中,使用非内置列表写法时,若`z-paging`在`swiper-item`标签内的情况下存在的无法获取slot插入的cell高度的问题。
|
||||
4.`修复` 在虚拟列表中分页数据小于1页时插入新数据,虚拟列表未生效的问题。
|
||||
5.`修复` 在虚拟列表中调用`refresh`时,cell的index计算不正确的问题。
|
||||
6.`修复` 在快手小程序中内容较少或空数据时`z-paging`未能铺满全屏的问题。
|
||||
7.`优化` `events`中的参数涉及枚举的部分,统一由之前的number类型修改为string类型,展示更直观!涉及的events:`@query`中的`from`参数;`@refresherStatusChange`中的`status`参数;`@loadingStatusChange`中的`status`参数;`slot=refresher`中的`refresherStatus`参数;`slot=chatLoading`中的`loadingMoreStatus`参数。更新版本请特别留意!
|
||||
## 2.8.0(2024-10-21)
|
||||
1.`新增` 全面支持鸿蒙Next。
|
||||
2.`修复` 设置了`refresher-complete-delay`后,在下拉刷新期间调用reload导致的无法再次下拉刷新的问题。
|
||||
3.`优化` 废弃虚拟列表transformY顶部占位方案,修改为空view占位。解决因使用旧方案导致的vue3中可能出现的虚拟列表闪动问题。提升虚拟列表的兼容性。
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
<!-- z-paging -->
|
||||
<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
|
||||
<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
|
||||
<!-- 反馈QQ群:790460711 -->
|
||||
|
||||
<!-- z-paging-cell,用于在nvue中使用cell包裹,vue中使用view包裹 -->
|
||||
<template>
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<cell :style="[cellStyle]" @touchstart="onTouchstart">
|
||||
<slot />
|
||||
</cell>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-NVUE -->
|
||||
<view :style="[cellStyle]" @touchstart="onTouchstart">
|
||||
<slot />
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* z-paging-cell 组件
|
||||
* @description 用于兼容 nvue 和 vue 中的 cell 渲染。因为在 nvue 中 z-paging 内置的是 list,因此列表 item 必须使用 cell 包住;在 vue 中不能使用 cell,否则会报组件找不到的错误。此子组件为了兼容这两种情况,内部作了条件编译处理。
|
||||
* @tutorial https://z-paging.zxlee.cn/api/sub-components/main.html#z-paging-cell配置
|
||||
* @notice 以下为 z-paging-cell 的配置项
|
||||
* @property {Object} cellStyle cell 样式,默认为 {}
|
||||
* @example <z-paging-cell :cellStyle="{ backgroundColor: '#f0f0f0' }"></z-paging-cell>
|
||||
*/
|
||||
export default {
|
||||
name: "z-paging-cell",
|
||||
props: {
|
||||
//cellStyle
|
||||
cellStyle: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onTouchstart(e) {
|
||||
this.$emit('touchstart', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -0,0 +1,209 @@
|
||||
<!-- z-paging -->
|
||||
<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
|
||||
<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
|
||||
<!-- 反馈QQ群:790460711 -->
|
||||
|
||||
<!-- 空数据占位view,此组件支持easycom规范,可以在项目中直接引用 -->
|
||||
<template>
|
||||
<view :class="{'zp-container':true,'zp-container-fixed':emptyViewFixed}" :style="[finalEmptyViewStyle]" @click="emptyViewClick">
|
||||
<view class="zp-main">
|
||||
<image v-if="!emptyViewImg.length" :class="{'zp-main-image-rpx':unit==='rpx','zp-main-image-px':unit==='px'}" :style="[emptyViewImgStyle]" :src="emptyImg" />
|
||||
<image v-else :class="{'zp-main-image-rpx':unit==='rpx','zp-main-image-px':unit==='px'}" mode="aspectFit" :style="[emptyViewImgStyle]" :src="emptyViewImg" />
|
||||
<text class="zp-main-title" :class="{'zp-main-title-rpx':unit==='rpx','zp-main-title-px':unit==='px'}" :style="[emptyViewTitleStyle]">{{emptyViewText}}</text>
|
||||
<text v-if="showEmptyViewReload" :class="{'zp-main-error-btn':true,'zp-main-error-btn-rpx':unit==='rpx','zp-main-error-btn-px':unit==='px'}" :style="[emptyViewReloadStyle]" @click.stop="reloadClick">{{emptyViewReloadText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import zStatic from '../z-paging/js/z-paging-static'
|
||||
|
||||
/**
|
||||
* z-paging-empty-view 空数据组件
|
||||
* @description 通用的 z-paging 空数据组件
|
||||
* @tutorial https://z-paging.zxlee.cn/api/sub-components/main.html#z-paging-empty-view配置
|
||||
* @property {Boolean} emptyViewFixed 空数据图片是否铺满 z-paging,默认为 false。若设置为 true,则为填充满 z-paging 的剩余部分
|
||||
* @property {String} emptyViewText 空数据图描述文字,默认为 '没有数据哦~'
|
||||
* @property {String} emptyViewImg 空数据图图片,默认使用 z-paging 内置的图片 (建议使用绝对路径,开头不要添加 "@",请以 "/" 开头)
|
||||
* @property {String} emptyViewReloadText 空数据图点击重新加载文字,默认为 '重新加载'
|
||||
* @property {Object} emptyViewStyle 空数据图样式,可设置空数据 view 的 top 等,如: empty-view-style="{'top':'100rpx'}" (如果空数据图不是 fixed 布局,则此处是 margin-top),默认为 {}
|
||||
* @property {Object} emptyViewImgStyle 空数据图 img 样式,默认为 {}
|
||||
* @property {Object} emptyViewTitleStyle 空数据图描述文字样式,默认为 {}
|
||||
* @property {Object} emptyViewReloadStyle 空数据图重新加载按钮样式,默认为 {}
|
||||
* @property {Boolean} showEmptyViewReload 是否显示空数据图重新加载按钮(无数据时),默认为 false
|
||||
* @property {Boolean} isLoadFailed 是否是加载失败,默认为 false
|
||||
* @property {String} unit 空数据图中布局的单位,默认为 'rpx'
|
||||
* @event {Function} reload 点击了重新加载按钮
|
||||
* @event {Function} viewClick 点击了空数据图 view
|
||||
* @example <z-paging-empty-view empty-view-text="暂无数据" />
|
||||
*/
|
||||
export default {
|
||||
name: "z-paging-empty-view",
|
||||
data() {
|
||||
return {
|
||||
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 空数据描述文字
|
||||
emptyViewText: {
|
||||
type: String,
|
||||
default: '没有数据哦~'
|
||||
},
|
||||
// 空数据图片
|
||||
emptyViewImg: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否显示空数据图重新加载按钮
|
||||
showEmptyViewReload: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 空数据点击重新加载文字
|
||||
emptyViewReloadText: {
|
||||
type: String,
|
||||
default: '重新加载'
|
||||
},
|
||||
// 是否是加载失败
|
||||
isLoadFailed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 空数据图样式
|
||||
emptyViewStyle: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 空数据图img样式
|
||||
emptyViewImgStyle: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 空数据图描述文字样式
|
||||
emptyViewTitleStyle: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 空数据图重新加载按钮样式
|
||||
emptyViewReloadStyle: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 空数据图z-index
|
||||
emptyViewZIndex: {
|
||||
type: Number,
|
||||
default: 9
|
||||
},
|
||||
// 空数据图片是否使用fixed布局并铺满z-paging
|
||||
emptyViewFixed: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 空数据图中布局的单位,默认为rpx
|
||||
unit: {
|
||||
type: String,
|
||||
default: 'rpx'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
emptyImg() {
|
||||
return this.isLoadFailed ? zStatic.base64Error : zStatic.base64Empty;
|
||||
},
|
||||
finalEmptyViewStyle(){
|
||||
this.emptyViewStyle['z-index'] = this.emptyViewZIndex;
|
||||
return this.emptyViewStyle;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 点击了reload按钮
|
||||
reloadClick() {
|
||||
this.$emit('reload');
|
||||
},
|
||||
// 点击了空数据view
|
||||
emptyViewClick() {
|
||||
this.$emit('viewClick');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.zp-container{
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.zp-container-fixed {
|
||||
/* #ifndef APP-NVUE */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
/* #ifdef APP-NVUE */
|
||||
flex: 1;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-main{
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 50rpx 0rpx;
|
||||
}
|
||||
|
||||
.zp-main-image-rpx {
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
}
|
||||
.zp-main-image-px {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.zp-main-title {
|
||||
color: #aaaaaa;
|
||||
text-align: center;
|
||||
}
|
||||
.zp-main-title-rpx {
|
||||
font-size: 28rpx;
|
||||
margin-top: 10rpx;
|
||||
padding: 0rpx 20rpx;
|
||||
}
|
||||
.zp-main-title-px {
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
padding: 0px 10px;
|
||||
}
|
||||
|
||||
.zp-main-error-btn {
|
||||
border: solid 1px #dddddd;
|
||||
color: #aaaaaa;
|
||||
}
|
||||
.zp-main-error-btn-rpx {
|
||||
font-size: 28rpx;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-top: 50rpx;
|
||||
}
|
||||
.zp-main-error-btn-px {
|
||||
font-size: 14px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 3px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,160 @@
|
||||
<!-- z-paging -->
|
||||
<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
|
||||
<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
|
||||
<!-- 反馈QQ群:790460711 -->
|
||||
|
||||
<!-- 滑动切换选项卡swiper-item,此组件支持easycom规范,可以在项目中直接引用 -->
|
||||
<template>
|
||||
<view class="zp-swiper-item-container">
|
||||
<z-paging ref="paging" :fixed="false"
|
||||
:auto="false" :useVirtualList="useVirtualList" :useInnerList="useInnerList" :cellKeyName="cellKeyName" :innerListStyle="innerListStyle"
|
||||
:preloadPage="preloadPage" :cellHeightMode="cellHeightMode" :virtualScrollFps="virtualScrollFps" :virtualListCol="virtualListCol"
|
||||
@query="_queryList" @listChange="_updateList">
|
||||
<slot />
|
||||
<template #header>
|
||||
<slot name="header"/>
|
||||
</template>
|
||||
<template #cell="{item,index}">
|
||||
<slot name="cell" :item="item" :index="index"/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<slot name="footer"/>
|
||||
</template>
|
||||
</z-paging>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import zPaging from '../z-paging/z-paging'
|
||||
/**
|
||||
* z-paging-swiper-item 组件
|
||||
* @description swiper+list极简写法中使用到,实际上就是对普通的swiper+list中swiper-item的包装封装,用以简化写法,但个性化配置局限较多
|
||||
* @tutorial https://z-paging.zxlee.cn/api/sub-components/main.html#z-paging-swiper-item配置
|
||||
* @notice 以下为 z-paging-swiper-item 的配置项
|
||||
* @property {Number} tabIndex 当前组件的 index,也就是当前组件是 swiper 中的第几个,默认为 0
|
||||
* @property {Number} currentIndex 当前 swiper 切换到第几个 index,默认为 0
|
||||
* @property {Boolean} useVirtualList 是否使用虚拟列表,默认为 false
|
||||
* @property {Boolean} useInnerList 是否在 z-paging 内部循环渲染列表(内置列表),默认为 false。若 useVirtualList 为 true,则此项恒为 true
|
||||
* @property {String} cellKeyName 内置列表 cell 的 key 名称,仅 nvue 有效,在 nvue 中开启 useInnerList 时必须填此项,默认为 ''
|
||||
* @property {Object} innerListStyle innerList 样式,默认为 {}
|
||||
* @property {Number|String} preloadPage 预加载的列表可视范围(列表高度)页数,默认为 12。此数值越大,则虚拟列表中加载的 dom 越多,内存消耗越大(会维持在一个稳定值),但增加预加载页面数量可缓解快速滚动短暂白屏问题
|
||||
* @property {String} cellHeightMode 虚拟列表 cell 高度模式,默认为 'fixed',也就是每个 cell 高度完全相同,将以第一个 cell 高度为准进行计算。可选值【dynamic】,即代表高度是动态非固定的,【dynamic】性能低于【fixed】
|
||||
* @property {Number|String} virtualListCol 虚拟列表列数,默认为 1。常用于每行有多列的情况,例如每行有 2 列数据,需要将此值设置为 2
|
||||
* @property {Number|String} virtualScrollFps 虚拟列表 scroll 取样帧率,默认为 60,过高可能出现卡顿等问题
|
||||
* @example <z-paging-swiper-item ref="swiperItem" :tabIndex="index" :currentIndex="current" @query="queryList" @updateList="updateList"></z-paging-swiper-item>
|
||||
*/
|
||||
export default {
|
||||
name: "z-paging-swiper-item",
|
||||
components: { zPaging },
|
||||
data() {
|
||||
return {
|
||||
firstLoaded: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
// 当前组件的index,也就是当前组件是swiper中的第几个
|
||||
tabIndex: {
|
||||
type: Number,
|
||||
default: function() {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
// 当前swiper切换到第几个index
|
||||
currentIndex: {
|
||||
type: Number,
|
||||
default: function() {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
// 是否使用虚拟列表,默认为否
|
||||
useVirtualList: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否在z-paging内部循环渲染列表(内置列表),默认为否。若use-virtual-list为true,则此项恒为true
|
||||
useInnerList: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 内置列表cell的key名称,仅nvue有效,在nvue中开启use-inner-list时必须填此项
|
||||
cellKeyName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// innerList样式
|
||||
innerListStyle: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
// 预加载的列表可视范围(列表高度)页数,默认为12,即预加载当前页及上下各12页的cell。此数值越大,则虚拟列表中加载的dom越多,内存消耗越大(会维持在一个稳定值),但增加预加载页面数量可缓解快速滚动短暂白屏问题
|
||||
preloadPage: {
|
||||
type: [Number, String],
|
||||
default: 12
|
||||
},
|
||||
// 虚拟列表cell高度模式,默认为fixed,也就是每个cell高度完全相同,将以第一个cell高度为准进行计算。可选值【dynamic】,即代表高度是动态非固定的,【dynamic】性能低于【fixed】。
|
||||
cellHeightMode: {
|
||||
type: String,
|
||||
default: 'fixed'
|
||||
},
|
||||
// 虚拟列表列数,默认为1。常用于每行有多列的情况,例如每行有2列数据,需要将此值设置为2
|
||||
virtualListCol: {
|
||||
type: [Number, String],
|
||||
default: 1
|
||||
},
|
||||
// 虚拟列表scroll取样帧率,默认为60,过高可能出现卡顿等问题
|
||||
virtualScrollFps: {
|
||||
type: [Number, String],
|
||||
default: 60
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentIndex: {
|
||||
handler(newVal, oldVal) {
|
||||
if (newVal === this.tabIndex) {
|
||||
// 懒加载,当滑动到当前的item时,才去加载
|
||||
if (!this.firstLoaded) {
|
||||
this.$nextTick(()=>{
|
||||
let delay = 5;
|
||||
// #ifdef MP-TOUTIAO
|
||||
delay = 100;
|
||||
// #endif
|
||||
setTimeout(() => {
|
||||
this.$refs.paging.reload().catch(() => {});
|
||||
}, delay);
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reload(data) {
|
||||
return this.$refs.paging.reload(data);
|
||||
},
|
||||
complete(data) {
|
||||
this.firstLoaded = true;
|
||||
return this.$refs.paging.complete(data);
|
||||
},
|
||||
_queryList(pageNo, pageSize, from) {
|
||||
this.$emit('query', pageNo, pageSize, from);
|
||||
},
|
||||
_updateList(list) {
|
||||
this.$emit('updateList', list);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.zp-swiper-item-container {
|
||||
/* #ifndef APP-NVUE */
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
/* #ifdef APP-NVUE */
|
||||
flex: 1;
|
||||
/* #endif */
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,176 @@
|
||||
<!-- z-paging -->
|
||||
<!-- github地址:https://github.com/SmileZXLee/uni-z-paging -->
|
||||
<!-- dcloud地址:https://ext.dcloud.net.cn/plugin?id=3935 -->
|
||||
<!-- 反馈QQ群:790460711 -->
|
||||
|
||||
<!-- 滑动切换选项卡swiper容器,此组件支持easycom规范,可以在项目中直接引用 -->
|
||||
<template>
|
||||
<view :class="fixed?'zp-swiper-container zp-swiper-container-fixed':'zp-swiper-container'" :style="[finalSwiperStyle]">
|
||||
<!-- #ifndef APP-PLUS -->
|
||||
<view v-if="cssSafeAreaInsetBottom===-1" class="zp-safe-area-inset-bottom"></view>
|
||||
<!-- #endif -->
|
||||
<slot v-if="zSlots.top" name="top" />
|
||||
<view class="zp-swiper-super">
|
||||
<view v-if="zSlots.left" :class="{'zp-swiper-left':true,'zp-absoulte':isOldWebView}">
|
||||
<slot name="left" />
|
||||
</view>
|
||||
<view :class="{'zp-swiper':true,'zp-absoulte':isOldWebView}" :style="[swiperContentStyle]">
|
||||
<slot />
|
||||
</view>
|
||||
<view v-if="zSlots.right" :class="{'zp-swiper-right':true,'zp-absoulte zp-right':isOldWebView}">
|
||||
<slot name="right" />
|
||||
</view>
|
||||
</view>
|
||||
<slot v-if="zSlots.bottom" name="bottom" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import commonLayoutModule from '../z-paging/js/modules/common-layout'
|
||||
|
||||
/**
|
||||
* z-paging-swiper 组件
|
||||
* @description 在 swiper 中使用 z-paging 时(左右滑动切换列表),在根节点使用 z-paging-swiper,其相当于一个 view 容器,默认铺满全屏,可免计算高度直接插入 swiper 的视图容器。
|
||||
* @tutorial https://z-paging.zxlee.cn/api/sub-components/main.html#z-paging-swiper配置
|
||||
* @property {Boolean} fixed 是否使用 fixed 布局,默认为 true
|
||||
* @property {Boolean} safeAreaInsetBottom 是否开启底部安全区域适配,默认为 false
|
||||
* @property {Object} swiperStyle z-paging-swiper 样式,默认为 {}
|
||||
* @example <z-paging-swiper :safeAreaInsetBottom="true"></z-paging-swiper>
|
||||
*/
|
||||
export default {
|
||||
name: "z-paging-swiper",
|
||||
mixins: [commonLayoutModule],
|
||||
data() {
|
||||
return {
|
||||
swiperContentStyle: {}
|
||||
};
|
||||
},
|
||||
props: {
|
||||
// 是否使用fixed布局,默认为是
|
||||
fixed: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否开启底部安全区域适配
|
||||
safeAreaInsetBottom: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// z-paging-swiper样式
|
||||
swiperStyle: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {};
|
||||
},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.systemInfo = this._getSystemInfoSync();
|
||||
setTimeout(this.updateFixedLayout, 100)
|
||||
})
|
||||
// #ifndef APP-PLUS
|
||||
this._getCssSafeAreaInsetBottom();
|
||||
// #endif
|
||||
this.updateLeftAndRightWidth();
|
||||
|
||||
this.swiperContentStyle = { 'flex': '1' };
|
||||
// #ifndef APP-NVUE
|
||||
this.swiperContentStyle = { width: '100%',height: '100%' };
|
||||
// #endif
|
||||
},
|
||||
computed: {
|
||||
finalSwiperStyle() {
|
||||
const swiperStyle = { ...this.swiperStyle };
|
||||
if (!this.systemInfo) return swiperStyle;
|
||||
const windowTop = this.windowTop;
|
||||
const windowBottom = this.systemInfo.windowBottom;
|
||||
if (this.fixed) {
|
||||
if (windowTop && !swiperStyle.top) {
|
||||
swiperStyle.top = windowTop + 'px';
|
||||
}
|
||||
if (!swiperStyle.bottom) {
|
||||
let bottom = windowBottom || 0;
|
||||
bottom += this.safeAreaInsetBottom ? this.safeAreaBottom : 0;
|
||||
if (bottom > 0) {
|
||||
swiperStyle.bottom = bottom + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
return swiperStyle;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 更新slot="left"和slot="right"宽度,当slot="left"或slot="right"宽度动态改变时调用
|
||||
updateLeftAndRightWidth() {
|
||||
if (!this.isOldWebView) return;
|
||||
this.$nextTick(() => this._updateLeftAndRightWidth(this.swiperContentStyle, 'zp-swiper'));
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.zp-swiper-container {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.zp-swiper-container-fixed {
|
||||
position: fixed;
|
||||
/* #ifndef APP-NVUE */
|
||||
height: auto;
|
||||
width: auto;
|
||||
/* #endif */
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.zp-safe-area-inset-bottom {
|
||||
position: absolute;
|
||||
/* #ifndef APP-PLUS */
|
||||
height: env(safe-area-inset-bottom);
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-swiper-super {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.zp-swiper-left,.zp-swiper-right{
|
||||
/* #ifndef APP-NVUE */
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-swiper {
|
||||
flex: 1;
|
||||
/* #ifndef APP-NVUE */
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-absoulte {
|
||||
/* #ifndef APP-NVUE */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: auto;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-swiper-item {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,182 @@
|
||||
<!-- [z-paging]上拉加载更多view -->
|
||||
<template>
|
||||
<view class="zp-l-container" :class="{'zp-l-container-rpx':c.unit==='rpx','zp-l-container-px':c.unit==='px'}" :style="[c.customStyle]" @click="doClick">
|
||||
<template v-if="!c.hideContent">
|
||||
<!-- 底部加载更多没有更多数据分割线 -->
|
||||
<text v-if="c.showNoMoreLine&&finalStatus===M.NoMore" :class="{'zp-l-line-rpx':c.unit==='rpx','zp-l-line-px':c.unit==='px'}" :style="[{backgroundColor:zTheme.line[ts]},c.noMoreLineCustomStyle]" />
|
||||
<!-- 底部加载更多loading -->
|
||||
<!-- #ifndef APP-NVUE -->
|
||||
<image v-if="finalStatus===M.Loading&&!!c.loadingIconCustomImage"
|
||||
:src="c.loadingIconCustomImage" :style="[c.iconCustomStyle]" :class="{'zp-l-line-loading-custom-image':true,'zp-l-line-loading-custom-image-animated':c.loadingAnimated,'zp-l-line-loading-custom-image-rpx':c.unit==='rpx','zp-l-line-loading-custom-image-px':c.unit==='px'}" />
|
||||
<image v-if="finalStatus===M.Loading&&finalLoadingIconType==='flower'&&!c.loadingIconCustomImage.length"
|
||||
:class="{'zp-line-loading-image':true,'zp-line-loading-image-rpx':c.unit==='rpx','zp-line-loading-image-px':c.unit==='px'}" :style="[c.iconCustomStyle]" :src="zTheme.flower[ts]" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<!-- 在nvue中底部加载更多loading使用系统自带的 -->
|
||||
<view>
|
||||
<loading-indicator v-if="finalStatus===M.Loading&&finalLoadingIconType!=='circle'" :class="{'zp-line-loading-image-rpx':c.unit==='rpx','zp-line-loading-image-px':c.unit==='px'}" :style="[{color:zTheme.indicator[ts]}]" :animating="true" />
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- 底部加载更多文字 -->
|
||||
<text v-if="finalStatus===M.Loading&&finalLoadingIconType==='circle'&&!c.loadingIconCustomImage.length"
|
||||
class="zp-l-circle-loading-view" :class="{'zp-l-circle-loading-view-rpx':c.unit==='rpx','zp-l-circle-loading-view-px':c.unit==='px'}" :style="[{borderColor:zTheme.circleBorder[ts],borderTopColor:zTheme.circleBorderTop[ts]},c.iconCustomStyle]" />
|
||||
<text v-if="!c.isChat||(!c.chatDefaultAsLoading&&finalStatus===M.Default)||finalStatus===M.Fail" :class="{'zp-l-text-rpx':c.unit==='rpx','zp-l-text-px':c.unit==='px'}" :style="[{color:zTheme.title[ts]},c.titleCustomStyle]">{{ownLoadingMoreText}}</text>
|
||||
<!-- 底部加载更多没有更多数据分割线 -->
|
||||
<text v-if="c.showNoMoreLine&&finalStatus===M.NoMore" :class="{'zp-l-line-rpx':c.unit==='rpx','zp-l-line-px':c.unit==='px'}" :style="[{backgroundColor:zTheme.line[ts]},c.noMoreLineCustomStyle]" />
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import zStatic from '../js/z-paging-static'
|
||||
import Enum from '../js/z-paging-enum'
|
||||
export default {
|
||||
name: 'z-paging-load-more',
|
||||
data() {
|
||||
return {
|
||||
M: Enum.More,
|
||||
zTheme: {
|
||||
title: { white: '#efefef', black: '#a4a4a4' },
|
||||
line: { white: '#efefef', black: '#eeeeee' },
|
||||
circleBorder: { white: '#aaaaaa', black: '#c8c8c8' },
|
||||
circleBorderTop: { white: '#ffffff', black: '#444444' },
|
||||
flower: { white: zStatic.base64FlowerWhite, black: zStatic.base64Flower },
|
||||
indicator: { white: '#eeeeee', black: '#777777' }
|
||||
}
|
||||
};
|
||||
},
|
||||
props: ['zConfig'],
|
||||
computed: {
|
||||
ts() {
|
||||
return this.c.defaultThemeStyle;
|
||||
},
|
||||
// 底部加载更多配置
|
||||
c() {
|
||||
return this.zConfig || {};
|
||||
},
|
||||
// 底部加载更多文字
|
||||
ownLoadingMoreText() {
|
||||
return {
|
||||
[this.M.Default]: this.c.defaultText,
|
||||
[this.M.Loading]: this.c.loadingText,
|
||||
[this.M.NoMore]: this.c.noMoreText,
|
||||
[this.M.Fail]: this.c.failText,
|
||||
}[this.finalStatus];
|
||||
},
|
||||
// 底部加载更多状态
|
||||
finalStatus() {
|
||||
if (this.c.defaultAsLoading && this.c.status === this.M.Default) return this.M.Loading;
|
||||
return this.c.status;
|
||||
},
|
||||
// 加载更多icon类型
|
||||
finalLoadingIconType() {
|
||||
// #ifdef APP-NVUE
|
||||
return 'flower';
|
||||
// #endif
|
||||
return this.c.loadingIconType;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 点击了加载更多
|
||||
doClick() {
|
||||
this.$emit('doClick');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import "../css/z-paging-static.css";
|
||||
|
||||
.zp-l-container {
|
||||
/* #ifndef APP-NVUE */
|
||||
clear: both;
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.zp-l-container-rpx {
|
||||
height: 80rpx;
|
||||
font-size: 27rpx;
|
||||
}
|
||||
.zp-l-container-px {
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.zp-l-line-loading-custom-image {
|
||||
color: #a4a4a4;
|
||||
}
|
||||
.zp-l-line-loading-custom-image-rpx {
|
||||
margin-right: 8rpx;
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
}
|
||||
.zp-l-line-loading-custom-image-px {
|
||||
margin-right: 4px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.zp-l-line-loading-custom-image-animated{
|
||||
/* #ifndef APP-NVUE */
|
||||
animation: loading-circle 1s linear infinite;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-l-circle-loading-view {
|
||||
border: 3rpx solid #dddddd;
|
||||
border-radius: 50%;
|
||||
/* #ifndef APP-NVUE */
|
||||
animation: loading-circle 1s linear infinite;
|
||||
/* #endif */
|
||||
/* #ifdef APP-NVUE */
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
/* #endif */
|
||||
}
|
||||
.zp-l-circle-loading-view-rpx {
|
||||
margin-right: 8rpx;
|
||||
width: 23rpx;
|
||||
height: 23rpx;
|
||||
}
|
||||
.zp-l-circle-loading-view-px {
|
||||
margin-right: 4px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.zp-l-text-rpx {
|
||||
font-size: 30rpx;
|
||||
margin: 0rpx 6rpx;
|
||||
}
|
||||
.zp-l-text-px {
|
||||
font-size: 15px;
|
||||
margin: 0px 3px;
|
||||
}
|
||||
|
||||
.zp-l-line-rpx {
|
||||
height: 1px;
|
||||
width: 100rpx;
|
||||
margin: 0rpx 10rpx;
|
||||
}
|
||||
.zp-l-line-px {
|
||||
height: 1px;
|
||||
width: 50px;
|
||||
margin: 0rpx 5px;
|
||||
}
|
||||
|
||||
/* #ifndef APP-NVUE */
|
||||
@keyframes loading-circle {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
/* #endif */
|
||||
</style>
|
||||
@ -0,0 +1,214 @@
|
||||
<!-- [z-paging]下拉刷新view -->
|
||||
<template>
|
||||
<view style="height: 100%;">
|
||||
<view :class="showUpdateTime?'zp-r-container zp-r-container-padding':'zp-r-container'">
|
||||
<view class="zp-r-left">
|
||||
<!-- 非加载中(继续下拉刷新、松手立即刷新状态图片) -->
|
||||
<image v-if="status!==R.Loading" :class="leftImageClass" :style="[leftImageStyle,imgStyle]" :src="leftImageSrc" />
|
||||
<!-- 加载状态图片 -->
|
||||
<!-- #ifndef APP-NVUE -->
|
||||
<image v-else :class="{'zp-line-loading-image':refreshingAnimated,'zp-r-left-image':true,'zp-r-left-image-pre-size-rpx':unit==='rpx','zp-r-left-image-pre-size-px':unit==='px'}" :style="[leftImageStyle,imgStyle]" :src="leftImageSrc" />
|
||||
<!-- #endif -->
|
||||
<!-- 在nvue中,加载状态loading使用系统loading -->
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<view v-else :style="[{'margin-right':showUpdateTime?addUnit(18,unit):addUnit(12, unit)}]">
|
||||
<loading-indicator :class="isIos?{'zp-loading-image-ios-rpx':unit==='rpx','zp-loading-image-ios-px':unit==='px'}:{'zp-loading-image-android-rpx':unit==='rpx','zp-loading-image-android-px':unit==='px'}"
|
||||
:style="[{color:zTheme.indicator[ts]},imgStyle]" :animating="true" />
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
<!-- 右侧文字内容 -->
|
||||
<view class="zp-r-right">
|
||||
<!-- 右侧下拉刷新状态文字 -->
|
||||
<text class="zp-r-right-text" :style="[rightTextStyle,titleStyle]">{{currentTitle}}</text>
|
||||
<!-- 右侧下拉刷新时间文字 -->
|
||||
<text v-if="showUpdateTime&&refresherTimeText.length" class="zp-r-right-text" :class="{'zp-r-right-time-text-rpx':unit==='rpx','zp-r-right-time-text-px':unit==='px'}" :style="[{color:zTheme.title[ts]},updateTimeStyle]">
|
||||
{{refresherTimeText}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import zStatic from '../js/z-paging-static'
|
||||
import u from '../js/z-paging-utils'
|
||||
import Enum from '../js/z-paging-enum'
|
||||
|
||||
export default {
|
||||
name: 'z-paging-refresh',
|
||||
data() {
|
||||
return {
|
||||
R: Enum.Refresher,
|
||||
refresherTimeText: '',
|
||||
zTheme: {
|
||||
title: { white: '#efefef', black: '#555555' },
|
||||
arrow: { white: zStatic.base64ArrowWhite, black: zStatic.base64Arrow },
|
||||
flower: { white: zStatic.base64FlowerWhite, black: zStatic.base64Flower },
|
||||
success: { white: zStatic.base64SuccessWhite, black: zStatic.base64Success },
|
||||
indicator: { white: '#eeeeee', black: '#777777' }
|
||||
}
|
||||
};
|
||||
},
|
||||
props: ['status', 'defaultThemeStyle', 'defaultText', 'pullingText', 'refreshingText', 'completeText', 'goF2Text', 'defaultImg', 'pullingImg',
|
||||
'refreshingImg', 'completeImg', 'refreshingAnimated', 'showUpdateTime', 'updateTimeKey', 'imgStyle', 'titleStyle', 'updateTimeStyle', 'updateTimeTextMap', 'unit', 'isIos'
|
||||
],
|
||||
computed: {
|
||||
ts() {
|
||||
return this.defaultThemeStyle;
|
||||
},
|
||||
// 当前状态Map
|
||||
statusTextMap() {
|
||||
this.updateTime();
|
||||
const { R, defaultText, pullingText, refreshingText, completeText, goF2Text } = this;
|
||||
return {
|
||||
[R.Default]: defaultText,
|
||||
[R.ReleaseToRefresh]: pullingText,
|
||||
[R.Loading]: refreshingText,
|
||||
[R.Complete]: completeText,
|
||||
[R.GoF2]: goF2Text,
|
||||
};
|
||||
},
|
||||
// 当前状态文字
|
||||
currentTitle() {
|
||||
return this.statusTextMap[this.status] || this.defaultText;
|
||||
},
|
||||
// 左侧图片class
|
||||
leftImageClass() {
|
||||
const preSizeClass = `zp-r-left-image-pre-size-${this.unit}`;
|
||||
if (this.status === this.R.Complete) return preSizeClass;
|
||||
return `zp-r-left-image ${preSizeClass} ${this.status === this.R.Default ? 'zp-r-arrow-down' : 'zp-r-arrow-top'}`;
|
||||
},
|
||||
// 左侧图片style
|
||||
leftImageStyle() {
|
||||
const showUpdateTime = this.showUpdateTime;
|
||||
const size = showUpdateTime ? u.addUnit(36, this.unit) : u.addUnit(34, this.unit);
|
||||
return {width: size,height: size,'margin-right': showUpdateTime ? u.addUnit(20, this.unit) : u.addUnit(9, this.unit)};
|
||||
},
|
||||
// 左侧图片src
|
||||
leftImageSrc() {
|
||||
const R = this.R;
|
||||
const status = this.status;
|
||||
if (status === R.Default) {
|
||||
if (!!this.defaultImg) return this.defaultImg;
|
||||
return this.zTheme.arrow[this.ts];
|
||||
} else if (status === R.ReleaseToRefresh) {
|
||||
if (!!this.pullingImg) return this.pullingImg;
|
||||
if (!!this.defaultImg) return this.defaultImg;
|
||||
return this.zTheme.arrow[this.ts];
|
||||
} else if (status === R.Loading) {
|
||||
if (!!this.refreshingImg) return this.refreshingImg;
|
||||
return this.zTheme.flower[this.ts];;
|
||||
} else if (status === R.Complete) {
|
||||
if (!!this.completeImg) return this.completeImg;
|
||||
return this.zTheme.success[this.ts];;
|
||||
} else if (status === R.GoF2) {
|
||||
return this.zTheme.arrow[this.ts];
|
||||
}
|
||||
return '';
|
||||
},
|
||||
// 右侧文字style
|
||||
rightTextStyle() {
|
||||
let stl = {};
|
||||
// #ifdef APP-NVUE
|
||||
const textHeight = this.showUpdateTime ? u.addUnit(40, this.unit) : u.addUnit(80, this.unit);
|
||||
stl = {'height': textHeight, 'line-height': textHeight}
|
||||
// #endif
|
||||
stl['color'] = this.zTheme.title[this.ts];
|
||||
stl['font-size'] = u.addUnit(30, this.unit);
|
||||
return stl;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 添加单位
|
||||
addUnit(value, unit) {
|
||||
return u.addUnit(value, unit);
|
||||
},
|
||||
// 更新下拉刷新时间
|
||||
updateTime() {
|
||||
if (this.showUpdateTime) {
|
||||
this.refresherTimeText = u.getRefesrherFormatTimeByKey(this.updateTimeKey, this.updateTimeTextMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import "../css/z-paging-static.css";
|
||||
|
||||
.zp-r-container {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.zp-r-container-padding {
|
||||
/* #ifdef APP-NVUE */
|
||||
padding: 7px 0rpx;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-r-left {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
/* #ifdef MP-ALIPAY */
|
||||
margin-top: -4rpx;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-r-left-image {
|
||||
transition-duration: .2s;
|
||||
transition-property: transform;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.zp-r-left-image-pre-size-rpx {
|
||||
/* #ifndef APP-NVUE */
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
overflow: hidden;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-r-left-image-pre-size-px {
|
||||
/* #ifndef APP-NVUE */
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
overflow: hidden;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-r-arrow-top {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.zp-r-arrow-down {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.zp-r-right {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.zp-r-right-time-text-rpx {
|
||||
margin-top: 10rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.zp-r-right-time-text-px {
|
||||
margin-top: 5px;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
3
uni_modules/z-paging/components/z-paging/config/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
// z-paging全局配置文件,注意避免更新时此文件被覆盖,若被覆盖,可在此文件中右键->点击本地历史记录,找回覆盖前的配置
|
||||
|
||||
export default {}
|
||||
241
uni_modules/z-paging/components/z-paging/css/z-paging-main.css
Normal file
@ -0,0 +1,241 @@
|
||||
/* [z-paging]公共css*/
|
||||
|
||||
.z-paging-content {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
/* #ifndef APP-NVUE */
|
||||
overflow: hidden;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.z-paging-content-full {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.z-paging-content-fixed, .zp-loading-fixed {
|
||||
position: fixed;
|
||||
/* #ifndef APP-NVUE */
|
||||
height: auto;
|
||||
width: auto;
|
||||
/* #endif */
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.zp-f2-content {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.zp-page-top, .zp-page-bottom {
|
||||
/* #ifndef APP-NVUE */
|
||||
width: auto;
|
||||
/* #endif */
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.zp-page-left, .zp-page-right {
|
||||
/* #ifndef APP-NVUE */
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-scroll-view-super {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.zp-view-super {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.zp-scroll-view-container, .zp-scroll-view {
|
||||
position: relative;
|
||||
/* #ifndef APP-NVUE */
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-absoulte {
|
||||
/* #ifndef APP-NVUE */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: auto;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-scroll-view-absolute {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* #ifndef APP-NVUE */
|
||||
.zp-scroll-view-hide-scrollbar ::-webkit-scrollbar {
|
||||
display: none;
|
||||
-webkit-appearance: none;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
background: transparent;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
.zp-paging-touch-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.zp-fixed-bac-view {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.zp-paging-main {
|
||||
height: 100%;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.zp-paging-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.zp-chat-record-loading-custom-image {
|
||||
width: 35rpx;
|
||||
height: 35rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
animation: loading-flower 1s linear infinite;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-page-bottom-keyboard-placeholder-animate {
|
||||
transition-property: height;
|
||||
transition-duration: 0.15s;
|
||||
/* #ifndef APP-NVUE */
|
||||
will-change: height;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-custom-refresher-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.zp-custom-refresher-refresh {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: block;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-back-to-top {
|
||||
z-index: 999;
|
||||
position: absolute;
|
||||
bottom: 0rpx;
|
||||
transition-duration: .3s;
|
||||
transition-property: opacity;
|
||||
}
|
||||
.zp-back-to-top-rpx {
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
bottom: 0rpx;
|
||||
right: 25rpx;
|
||||
}
|
||||
.zp-back-to-top-px {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
bottom: 0px;
|
||||
right: 13px;
|
||||
}
|
||||
|
||||
.zp-back-to-top-show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.zp-back-to-top-hide {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.zp-back-to-top-img {
|
||||
/* #ifndef APP-NVUE */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
/* #ifdef APP-NVUE */
|
||||
flex: 1;
|
||||
/* #endif */
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.zp-back-to-top-img-inversion {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.zp-empty-view {
|
||||
/* #ifdef APP-NVUE */
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.zp-empty-view-center {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.zp-loading-fixed {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.zp-safe-area-inset-bottom {
|
||||
position: absolute;
|
||||
/* #ifndef APP-PLUS */
|
||||
height: env(safe-area-inset-bottom);
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.zp-n-refresh-container {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
justify-content: center;
|
||||
width: 750rpx;
|
||||
}
|
||||
|
||||
.zp-n-list-container{
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
/* [z-paging]公用的静态css资源 */
|
||||
|
||||
.zp-line-loading-image {
|
||||
/* #ifndef APP-NVUE */
|
||||
animation: loading-flower 1s steps(12) infinite;
|
||||
/* #endif */
|
||||
color: #666666;
|
||||
}
|
||||
.zp-line-loading-image-rpx {
|
||||
margin-right: 8rpx;
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
}
|
||||
.zp-line-loading-image-px {
|
||||
margin-right: 4px;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
.zp-loading-image-ios-rpx {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
.zp-loading-image-ios-px {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.zp-loading-image-android-rpx {
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
}
|
||||
.zp-loading-image-android-px {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
/* #ifndef APP-NVUE */
|
||||
@keyframes loading-flower {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: rotate(1turn);
|
||||
transform: rotate(1turn);
|
||||
}
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
23
uni_modules/z-paging/components/z-paging/i18n/en.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"zp.refresher.default": "Pull down to refresh",
|
||||
"zp.refresher.pulling": "Release to refresh",
|
||||
"zp.refresher.refreshing": "Refreshing...",
|
||||
"zp.refresher.complete": "Refresh succeeded",
|
||||
"zp.refresher.f2": "Refresh to enter 2f",
|
||||
|
||||
"zp.loadingMore.default": "Click to load more",
|
||||
"zp.loadingMore.loading": "Loading...",
|
||||
"zp.loadingMore.noMore": "No more data",
|
||||
"zp.loadingMore.fail": "Load failed,click to reload",
|
||||
|
||||
"zp.emptyView.title": "No data",
|
||||
"zp.emptyView.reload": "Reload",
|
||||
"zp.emptyView.error": "Sorry,load failed",
|
||||
|
||||
"zp.refresherUpdateTime.title": "Last update: ",
|
||||
"zp.refresherUpdateTime.none": "None",
|
||||
"zp.refresherUpdateTime.today": "Today",
|
||||
"zp.refresherUpdateTime.yesterday": "Yesterday",
|
||||
|
||||
"zp.systemLoading.title": "Loading..."
|
||||
}
|
||||
8
uni_modules/z-paging/components/z-paging/i18n/index.js
Normal file
@ -0,0 +1,8 @@
|
||||
import en from './en.json'
|
||||
import zhHans from './zh-Hans.json'
|
||||
import zhHant from './zh-Hant.json'
|
||||
export default {
|
||||
en,
|
||||
'zh-Hans': zhHans,
|
||||
'zh-Hant': zhHant
|
||||
}
|
||||
23
uni_modules/z-paging/components/z-paging/i18n/zh-Hans.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"zp.refresher.default": "继续下拉刷新",
|
||||
"zp.refresher.pulling": "松开立即刷新",
|
||||
"zp.refresher.refreshing": "正在刷新...",
|
||||
"zp.refresher.complete": "刷新成功",
|
||||
"zp.refresher.f2": "松手进入二楼",
|
||||
|
||||
"zp.loadingMore.default": "点击加载更多",
|
||||
"zp.loadingMore.loading": "正在加载...",
|
||||
"zp.loadingMore.noMore": "没有更多了",
|
||||
"zp.loadingMore.fail": "加载失败,点击重新加载",
|
||||
|
||||
"zp.emptyView.title": "没有数据哦~",
|
||||
"zp.emptyView.reload": "重新加载",
|
||||
"zp.emptyView.error": "很抱歉,加载失败",
|
||||
|
||||
"zp.refresherUpdateTime.title": "最后更新:",
|
||||
"zp.refresherUpdateTime.none": "无",
|
||||
"zp.refresherUpdateTime.today": "今天",
|
||||
"zp.refresherUpdateTime.yesterday": "昨天",
|
||||
|
||||
"zp.systemLoading.title": "加载中..."
|
||||
}
|
||||
23
uni_modules/z-paging/components/z-paging/i18n/zh-Hant.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"zp.refresher.default": "繼續下拉重繪",
|
||||
"zp.refresher.pulling": "鬆開立即重繪",
|
||||
"zp.refresher.refreshing": "正在重繪...",
|
||||
"zp.refresher.complete": "重繪成功",
|
||||
"zp.refresher.f2": "鬆手進入二樓",
|
||||
|
||||
"zp.loadingMore.default": "點擊加載更多",
|
||||
"zp.loadingMore.loading": "正在加載...",
|
||||
"zp.loadingMore.noMore": "沒有更多了",
|
||||
"zp.loadingMore.fail": "加載失敗,點擊重新加載",
|
||||
|
||||
"zp.emptyView.title": "沒有數據哦~",
|
||||
"zp.emptyView.reload": "重新加載",
|
||||
"zp.emptyView.error": "很抱歉,加載失敗",
|
||||
|
||||
"zp.refresherUpdateTime.title": "最後更新:",
|
||||
"zp.refresherUpdateTime.none": "無",
|
||||
"zp.refresherUpdateTime.today": "今天",
|
||||
"zp.refresherUpdateTime.yesterday": "昨天",
|
||||
|
||||
"zp.systemLoading.title": "加載中..."
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
// [z-paging]useZPaging hooks
|
||||
|
||||
import { onPageScroll, onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app';
|
||||
|
||||
function useZPaging(paging) {
|
||||
const cPaging = !!paging ? paging.value || paging : null;
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
if (!cPaging || !cPaging.value) return;
|
||||
cPaging.value.reload().catch(() => {});
|
||||
})
|
||||
|
||||
onPageScroll(e => {
|
||||
if (!cPaging || !cPaging.value) return;
|
||||
cPaging.value.updatePageScrollTop(e.scrollTop);
|
||||
e.scrollTop < 10 && cPaging.value.doChatRecordLoadMore();
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!cPaging || !cPaging.value) return;
|
||||
cPaging.value.pageReachBottom();
|
||||
})
|
||||
}
|
||||
|
||||
export default useZPaging
|
||||
@ -0,0 +1,25 @@
|
||||
// [z-paging]useZPagingComp hooks
|
||||
|
||||
function useZPagingComp(paging) {
|
||||
const cPaging = !!paging ? paging.value || paging : null;
|
||||
|
||||
const reload = () => {
|
||||
if (!cPaging || !cPaging.value) return;
|
||||
cPaging.value.reload().catch(() => {});
|
||||
}
|
||||
const updatePageScrollTop = scrollTop => {
|
||||
if (!cPaging || !cPaging.value) return;
|
||||
cPaging.value.updatePageScrollTop(scrollTop);
|
||||
}
|
||||
const doChatRecordLoadMore = () => {
|
||||
if (!cPaging || !cPaging.value) return;
|
||||
cPaging.value.doChatRecordLoadMore();
|
||||
}
|
||||
const pageReachBottom = () => {
|
||||
if (!cPaging || !cPaging.value) return;
|
||||
cPaging.value.pageReachBottom();
|
||||
}
|
||||
return { reload, updatePageScrollTop, doChatRecordLoadMore, pageReachBottom };
|
||||
}
|
||||
|
||||
export default useZPagingComp
|
||||
@ -0,0 +1,125 @@
|
||||
// [z-paging]点击返回顶部view模块
|
||||
import u from '.././z-paging-utils'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
// 自动显示点击返回顶部按钮,默认为否
|
||||
autoShowBackToTop: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoShowBackToTop', false)
|
||||
},
|
||||
// 点击返回顶部按钮显示/隐藏的阈值(滚动距离),单位为px,默认为400rpx
|
||||
backToTopThreshold: {
|
||||
type: [Number, String],
|
||||
default: u.gc('backToTopThreshold', '400rpx')
|
||||
},
|
||||
// 点击返回顶部按钮的自定义图片地址,默认使用z-paging内置的图片
|
||||
backToTopImg: {
|
||||
type: String,
|
||||
default: u.gc('backToTopImg', '')
|
||||
},
|
||||
// 点击返回顶部按钮返回到顶部时是否展示过渡动画,默认为是
|
||||
backToTopWithAnimate: {
|
||||
type: Boolean,
|
||||
default: u.gc('backToTopWithAnimate', true)
|
||||
},
|
||||
// 点击返回顶部按钮与底部的距离,注意添加单位px或rpx,默认为160rpx
|
||||
backToTopBottom: {
|
||||
type: [Number, String],
|
||||
default: u.gc('backToTopBottom', '160rpx')
|
||||
},
|
||||
// 点击返回顶部按钮的自定义样式
|
||||
backToTopStyle: {
|
||||
type: Object,
|
||||
default: u.gc('backToTopStyle', {}),
|
||||
},
|
||||
// iOS点击顶部状态栏、安卓双击标题栏时,滚动条返回顶部,只支持竖向,默认为是
|
||||
enableBackToTop: {
|
||||
type: Boolean,
|
||||
default: u.gc('enableBackToTop', true)
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 点击返回顶部的class
|
||||
backToTopClass: 'zp-back-to-top zp-back-to-top-hide',
|
||||
// 上次点击返回顶部的时间
|
||||
lastBackToTopShowTime: 0,
|
||||
// 点击返回顶部显示的class是否在展示中,使得按钮展示/隐藏过度效果更自然
|
||||
showBackToTopClass: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
backToTopThresholdUnitConverted() {
|
||||
return u.addUnit(this.backToTopThreshold, this.unit);
|
||||
},
|
||||
backToTopBottomUnitConverted() {
|
||||
return u.addUnit(this.backToTopBottom, this.unit);
|
||||
},
|
||||
finalEnableBackToTop() {
|
||||
return this.usePageScroll ? false : this.enableBackToTop;
|
||||
},
|
||||
finalBackToTopThreshold() {
|
||||
return u.convertToPx(this.backToTopThresholdUnitConverted);
|
||||
},
|
||||
finalBackToTopStyle() {
|
||||
const backToTopStyle = this.backToTopStyle;
|
||||
if (!backToTopStyle.bottom) {
|
||||
backToTopStyle.bottom = this.windowBottom + u.convertToPx(this.backToTopBottomUnitConverted) + 'px';
|
||||
}
|
||||
if(!backToTopStyle.position){
|
||||
backToTopStyle.position = this.usePageScroll ? 'fixed': 'absolute';
|
||||
}
|
||||
return backToTopStyle;
|
||||
},
|
||||
finalBackToTopClass() {
|
||||
return `${this.backToTopClass} zp-back-to-top-${this.unit}`;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 点击了返回顶部
|
||||
_backToTopClick() {
|
||||
let callbacked = false;
|
||||
this.$emit('backToTopClick', toTop => {
|
||||
(toTop === undefined || toTop === true) && this._handleToTop();
|
||||
callbacked = true;
|
||||
});
|
||||
// 如果用户没有禁止默认的返回顶部事件,则触发滚动到顶部
|
||||
this.$nextTick(() => {
|
||||
!callbacked && this._handleToTop();
|
||||
})
|
||||
},
|
||||
// 处理滚动到顶部(聊天记录模式中为滚动到底部)
|
||||
_handleToTop() {
|
||||
!this.backToTopWithAnimate && this._checkShouldShowBackToTop(0);
|
||||
!this.useChatRecordMode ? this.scrollToTop(this.backToTopWithAnimate) : this.scrollToBottom(this.backToTopWithAnimate);
|
||||
},
|
||||
// 判断是否要显示返回顶部按钮
|
||||
_checkShouldShowBackToTop(scrollTop) {
|
||||
if (!this.autoShowBackToTop) {
|
||||
this.showBackToTopClass = false;
|
||||
return;
|
||||
}
|
||||
if (scrollTop > this.finalBackToTopThreshold) {
|
||||
if (!this.showBackToTopClass) {
|
||||
// 记录当前点击返回顶部按钮显示的class生效了
|
||||
this.showBackToTopClass = true;
|
||||
this.lastBackToTopShowTime = new Date().getTime();
|
||||
// 当滚动到需要展示返回顶部的阈值内,则延迟300毫秒展示返回到顶部按钮
|
||||
u.delay(() => {
|
||||
this.backToTopClass = 'zp-back-to-top zp-back-to-top-show';
|
||||
}, 300)
|
||||
}
|
||||
} else {
|
||||
// 如果当前点击返回顶部按钮显示的class是生效状态并且滚动小于触发阈值,则隐藏返回顶部按钮
|
||||
if (this.showBackToTopClass) {
|
||||
this.backToTopClass = 'zp-back-to-top zp-back-to-top-hide';
|
||||
u.delay(() => {
|
||||
this.showBackToTopClass = false;
|
||||
}, new Date().getTime() - this.lastBackToTopShowTime < 500 ? 0 : 300)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,149 @@
|
||||
// [z-paging]聊天记录模式模块
|
||||
import u from '.././z-paging-utils'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
// 使用聊天记录模式,默认为否
|
||||
useChatRecordMode: {
|
||||
type: Boolean,
|
||||
default: u.gc('useChatRecordMode', false)
|
||||
},
|
||||
// 使用聊天记录模式时滚动到顶部后,列表垂直移动偏移距离。默认0rpx。单位px(暂时无效)
|
||||
chatRecordMoreOffset: {
|
||||
type: [Number, String],
|
||||
default: u.gc('chatRecordMoreOffset', '0rpx')
|
||||
},
|
||||
// 使用聊天记录模式时是否自动隐藏键盘:在用户触摸列表时候自动隐藏键盘,默认为是
|
||||
autoHideKeyboardWhenChat: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoHideKeyboardWhenChat', true)
|
||||
},
|
||||
// 使用聊天记录模式中键盘弹出时是否自动调整slot="bottom"高度,默认为是
|
||||
autoAdjustPositionWhenChat: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoAdjustPositionWhenChat', true)
|
||||
},
|
||||
// 使用聊天记录模式中键盘弹出时占位高度偏移距离。默认0rpx。单位px
|
||||
chatAdjustPositionOffset: {
|
||||
type: [Number, String],
|
||||
default: u.gc('chatAdjustPositionOffset', '0rpx')
|
||||
},
|
||||
// 使用聊天记录模式中键盘弹出时是否自动滚动到底部,默认为否
|
||||
autoToBottomWhenChat: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoToBottomWhenChat', false)
|
||||
},
|
||||
// 使用聊天记录模式中reload时是否显示chatLoading,默认为否
|
||||
showChatLoadingWhenReload: {
|
||||
type: Boolean,
|
||||
default: u.gc('showChatLoadingWhenReload', false)
|
||||
},
|
||||
// 在聊天记录模式中滑动到顶部状态为默认状态时,以加载中的状态展示,默认为是。若设置为否,则默认会显示【点击加载更多】,然后才会显示loading
|
||||
chatLoadingMoreDefaultAsLoading: {
|
||||
type: Boolean,
|
||||
default: u.gc('chatLoadingMoreDefaultAsLoading', true)
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 键盘高度
|
||||
keyboardHeight: 0,
|
||||
// 键盘高度是否未改变,此时占位高度变化不需要动画效果
|
||||
isKeyboardHeightChanged: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
finalChatRecordMoreOffset() {
|
||||
return u.convertToPx(this.chatRecordMoreOffset);
|
||||
},
|
||||
finalChatAdjustPositionOffset() {
|
||||
return u.convertToPx(this.chatAdjustPositionOffset);
|
||||
},
|
||||
// 聊天记录模式旋转180度style
|
||||
chatRecordRotateStyle() {
|
||||
let cellStyle;
|
||||
// 在vue中,直接将列表倒置,因此在vue的cell中,也直接写style="transform: scaleY(-1)"转回来即可。
|
||||
// #ifndef APP-NVUE
|
||||
cellStyle = this.useChatRecordMode ? { transform: 'scaleY(-1)' } : {};
|
||||
// #endif
|
||||
|
||||
// 在nvue中,需要考虑数据量不满一页的情况,因为nvue中的list无法通过flex-end修改不满一页的起始位置,会导致不满一页时列表数据从底部开始,因此需要特别判断
|
||||
// 当数据不满一屏的时候,不进行列表倒置
|
||||
// #ifdef APP-NVUE
|
||||
cellStyle = this.useChatRecordMode ? { transform: this.isFirstPageAndNoMore ? 'scaleY(1)' : 'scaleY(-1)' } : {};
|
||||
// #endif
|
||||
|
||||
this.$emit('update:cellStyle', cellStyle);
|
||||
this.$emit('cellStyleChange', cellStyle);
|
||||
|
||||
// 在聊天记录模式中,如果列表没有倒置并且当前是第一页,则需要自动滚动到最底部
|
||||
this.$nextTick(() => {
|
||||
if (this.isFirstPage && this.isChatRecordModeAndNotInversion) {
|
||||
this.$nextTick(() => {
|
||||
// 这里多次触发滚动到底部是为了避免在某些情况下,即使是在nextTick但是cell未渲染完毕导致滚动到底部位置不正确的问题
|
||||
this._scrollToBottom(false);
|
||||
u.delay(() => {
|
||||
this._scrollToBottom(false);
|
||||
u.delay(() => {
|
||||
this._scrollToBottom(false);
|
||||
}, 50)
|
||||
}, 50)
|
||||
})
|
||||
}
|
||||
})
|
||||
return cellStyle;
|
||||
},
|
||||
// 是否是聊天记录列表并且有配置transform
|
||||
isChatRecordModeHasTransform() {
|
||||
return this.useChatRecordMode && this.chatRecordRotateStyle && this.chatRecordRotateStyle.transform;
|
||||
},
|
||||
// 是否是聊天记录列表并且列表未倒置
|
||||
isChatRecordModeAndNotInversion() {
|
||||
return this.isChatRecordModeHasTransform && this.chatRecordRotateStyle.transform === 'scaleY(1)';
|
||||
},
|
||||
// 是否是聊天记录列表并且列表倒置
|
||||
isChatRecordModeAndInversion() {
|
||||
return this.isChatRecordModeHasTransform && this.chatRecordRotateStyle.transform === 'scaleY(-1)';
|
||||
},
|
||||
// 最终的聊天记录模式中底部安全区域的高度,如果开启了底部安全区域并且键盘未弹出,则添加底部区域高度
|
||||
chatRecordModeSafeAreaBottom() {
|
||||
return this.safeAreaInsetBottom && !this.keyboardHeight ? this.safeAreaBottom : 0;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 监听键盘高度变化(H5、百度小程序、抖音小程序、飞书小程序不支持)
|
||||
// #ifndef H5 || MP-BAIDU || MP-TOUTIAO
|
||||
if (this.useChatRecordMode) {
|
||||
uni.onKeyboardHeightChange(this._handleKeyboardHeightChange);
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
methods: {
|
||||
// 添加聊天记录
|
||||
addChatRecordData(data, toBottom = true, toBottomWithAnimate = true) {
|
||||
if (!this.useChatRecordMode) return;
|
||||
this.isTotalChangeFromAddData = true;
|
||||
this.addDataFromTop(data, toBottom, toBottomWithAnimate);
|
||||
},
|
||||
// 手动触发滚动到顶部加载更多,聊天记录模式时有效
|
||||
doChatRecordLoadMore() {
|
||||
this.useChatRecordMode && this._onLoadingMore('click');
|
||||
},
|
||||
// 处理键盘高度变化
|
||||
_handleKeyboardHeightChange(res) {
|
||||
this.$emit('keyboardHeightChange', res);
|
||||
if (this.autoAdjustPositionWhenChat) {
|
||||
this.isKeyboardHeightChanged = true;
|
||||
this.keyboardHeight = res.height > 0 ? res.height + this.finalChatAdjustPositionOffset : res.height;
|
||||
}
|
||||
if (this.autoToBottomWhenChat && this.keyboardHeight > 0) {
|
||||
u.delay(() => {
|
||||
this.scrollToBottom(false);
|
||||
u.delay(() => {
|
||||
this.scrollToBottom(false);
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,152 @@
|
||||
// [z-paging]通用布局相关模块
|
||||
import u from '.././z-paging-utils'
|
||||
|
||||
// #ifdef APP-NVUE
|
||||
const weexDom = weex.requireModule('dom');
|
||||
// #endif
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
systemInfo: null,
|
||||
cssSafeAreaInsetBottom: -1,
|
||||
isReadyDestroy: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 顶部可用距离
|
||||
windowTop() {
|
||||
if (!this.systemInfo) return 0;
|
||||
// 暂时修复vue3中隐藏系统导航栏后windowTop获取不正确的问题,具体bug详见https://ask.dcloud.net.cn/question/141634
|
||||
// 感谢litangyu!!https://github.com/SmileZXLee/uni-z-paging/issues/25
|
||||
// #ifdef VUE3 && H5
|
||||
const pageHeadNode = document.getElementsByTagName("uni-page-head");
|
||||
if (!pageHeadNode.length) return 0;
|
||||
// #endif
|
||||
return this.systemInfo.windowTop || 0;
|
||||
},
|
||||
// 底部安全区域高度
|
||||
safeAreaBottom() {
|
||||
if (!this.systemInfo) return 0;
|
||||
let safeAreaBottom = 0;
|
||||
// #ifdef APP-PLUS
|
||||
safeAreaBottom = this.systemInfo.safeAreaInsets.bottom || 0 ;
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
safeAreaBottom = Math.max(this.cssSafeAreaInsetBottom, 0);
|
||||
// #endif
|
||||
return safeAreaBottom;
|
||||
},
|
||||
// 是否是比较老的webview,在一些老的webview中,需要进行一些特殊处理
|
||||
isOldWebView() {
|
||||
// #ifndef APP-NVUE || MP-KUAISHOU
|
||||
try {
|
||||
const systemInfos = u.getSystemInfoSync(true).system.split(' ');
|
||||
const deviceType = systemInfos[0];
|
||||
const version = parseInt(systemInfos[1]);
|
||||
if ((deviceType === 'iOS' && version <= 10) || (deviceType === 'Android' && version <= 6)) {
|
||||
return true;
|
||||
}
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
// #endif
|
||||
return false;
|
||||
},
|
||||
// 当前组件的$slots,兼容不同平台
|
||||
zSlots() {
|
||||
// #ifdef VUE2
|
||||
|
||||
// #ifdef MP-ALIPAY
|
||||
return this.$slots;
|
||||
// #endif
|
||||
|
||||
return this.$scopedSlots || this.$slots;
|
||||
// #endif
|
||||
|
||||
return this.$slots;
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.isReadyDestroy = true;
|
||||
},
|
||||
// #ifdef VUE3
|
||||
unmounted() {
|
||||
this.isReadyDestroy = true;
|
||||
},
|
||||
// #endif
|
||||
methods: {
|
||||
// 更新fixed模式下z-paging的布局
|
||||
updateFixedLayout() {
|
||||
this.fixed && this.$nextTick(() => {
|
||||
this.systemInfo = u.getSystemInfoSync();
|
||||
})
|
||||
},
|
||||
// 获取节点尺寸
|
||||
_getNodeClientRect(select, inDom = true, scrollOffset = false) {
|
||||
if (this.isReadyDestroy) {
|
||||
return Promise.resolve(false);
|
||||
};
|
||||
// nvue中获取节点信息
|
||||
// #ifdef APP-NVUE
|
||||
select = select.replace(/[.|#]/g, '');
|
||||
const ref = this.$refs[select];
|
||||
return new Promise((resolve, reject) => {
|
||||
if (ref) {
|
||||
weexDom.getComponentRect(ref, option => {
|
||||
resolve(option && option.result ? [option.size] : false);
|
||||
})
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
return;
|
||||
// #endif
|
||||
|
||||
// vue中获取节点信息
|
||||
//#ifdef MP-ALIPAY
|
||||
inDom = false;
|
||||
//#endif
|
||||
|
||||
/*
|
||||
inDom可能是true、false,也可能是具体的dom节点
|
||||
如果inDom不为false,则使用uni.createSelectorQuery().in()进行查询,如果inDom为true,则in中的是this,否则in中的为具体的dom
|
||||
如果inDom为false,则使用uni.createSelectorQuery()进行查询
|
||||
*/
|
||||
let res = !!inDom ? uni.createSelectorQuery().in(inDom === true ? this : inDom) : uni.createSelectorQuery();
|
||||
scrollOffset ? res.select(select).scrollOffset() : res.select(select).boundingClientRect();
|
||||
return new Promise((resolve, reject) => {
|
||||
res.exec(data => {
|
||||
resolve((data && data != '' && data != undefined && data.length) ? data : false);
|
||||
});
|
||||
});
|
||||
},
|
||||
// 获取slot="left"和slot="right"宽度并且更新布局
|
||||
_updateLeftAndRightWidth(targetStyle, parentNodePrefix) {
|
||||
this.$nextTick(() => {
|
||||
let delayTime = 0;
|
||||
// #ifdef MP-BAIDU
|
||||
delayTime = 10;
|
||||
// #endif
|
||||
setTimeout(() => {
|
||||
['left','right'].map(position => {
|
||||
this._getNodeClientRect(`.${parentNodePrefix}-${position}`).then(res => {
|
||||
this.$set(targetStyle, position, res ? res[0].width + 'px' : '0px');
|
||||
});
|
||||
})
|
||||
}, delayTime)
|
||||
})
|
||||
},
|
||||
// 通过获取css设置的底部安全区域占位view高度设置bottom距离(直接通过systemInfo在部分平台上无法获取到底部安全区域)
|
||||
_getCssSafeAreaInsetBottom(success) {
|
||||
this._getNodeClientRect('.zp-safe-area-inset-bottom').then(res => {
|
||||
this.cssSafeAreaInsetBottom = res ? res[0].height : -1;
|
||||
res && success && success();
|
||||
});
|
||||
},
|
||||
// 同步获取系统信息,兼容不同平台(供z-paging-swiper使用)
|
||||
_getSystemInfoSync(useCache = false) {
|
||||
return u.getSystemInfoSync(useCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,736 @@
|
||||
// [z-paging]数据处理模块
|
||||
import u from '.././z-paging-utils'
|
||||
import c from '.././z-paging-constant'
|
||||
import Enum from '.././z-paging-enum'
|
||||
import interceptor from '../z-paging-interceptor'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
// 自定义初始的pageNo,默认为1
|
||||
defaultPageNo: {
|
||||
type: Number,
|
||||
default: u.gc('defaultPageNo', 1),
|
||||
observer: function(newVal) {
|
||||
this.pageNo = newVal;
|
||||
},
|
||||
},
|
||||
// 自定义pageSize,默认为10
|
||||
defaultPageSize: {
|
||||
type: Number,
|
||||
default: u.gc('defaultPageSize', 10),
|
||||
validator: (value) => {
|
||||
if (value <= 0) u.consoleErr('default-page-size必须大于0!');
|
||||
return value > 0;
|
||||
}
|
||||
},
|
||||
// 为保证数据一致,设置当前tab切换时的标识key,并在complete中传递相同key,若二者不一致,则complete将不会生效
|
||||
dataKey: {
|
||||
type: [Number, String, Object],
|
||||
default: u.gc('dataKey', null),
|
||||
},
|
||||
// 使用缓存,若开启将自动缓存第一页的数据,默认为否。请注意,因考虑到切换tab时不同tab数据不同的情况,默认仅会缓存组件首次加载时第一次请求到的数据,后续的下拉刷新操作不会更新缓存。
|
||||
useCache: {
|
||||
type: Boolean,
|
||||
default: u.gc('useCache', false)
|
||||
},
|
||||
// 使用缓存时缓存的key,用于区分不同列表的缓存数据,useCache为true时必须设置,否则缓存无效
|
||||
cacheKey: {
|
||||
type: String,
|
||||
default: u.gc('cacheKey', null)
|
||||
},
|
||||
// 缓存模式,默认仅会缓存组件首次加载时第一次请求到的数据,可设置为always,即代表总是缓存,每次列表刷新(下拉刷新、调用reload等)都会更新缓存
|
||||
cacheMode: {
|
||||
type: String,
|
||||
default: u.gc('cacheMode', Enum.CacheMode.Default)
|
||||
},
|
||||
// 自动注入的list名,可自动修改父view(包含ref="paging")中对应name的list值
|
||||
autowireListName: {
|
||||
type: String,
|
||||
default: u.gc('autowireListName', '')
|
||||
},
|
||||
// 自动注入的query名,可自动调用父view(包含ref="paging")中的query方法
|
||||
autowireQueryName: {
|
||||
type: String,
|
||||
default: u.gc('autowireQueryName', '')
|
||||
},
|
||||
// 获取分页数据Function,功能与@query类似。若设置了fetch则@query将不再触发
|
||||
fetch: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
// fetch的附加参数,fetch配置后有效
|
||||
fetchParams: {
|
||||
type: Object,
|
||||
default: u.gc('fetchParams', null)
|
||||
},
|
||||
// z-paging mounted后自动调用reload方法(mounted后自动调用接口),默认为是
|
||||
auto: {
|
||||
type: Boolean,
|
||||
default: u.gc('auto', true)
|
||||
},
|
||||
// 用户下拉刷新时是否触发reload方法,默认为是
|
||||
reloadWhenRefresh: {
|
||||
type: Boolean,
|
||||
default: u.gc('reloadWhenRefresh', true)
|
||||
},
|
||||
// reload时自动滚动到顶部,默认为是
|
||||
autoScrollToTopWhenReload: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoScrollToTopWhenReload', true)
|
||||
},
|
||||
// reload时立即自动清空原list,默认为是,若立即自动清空,则在reload之后、请求回调之前页面是空白的
|
||||
autoCleanListWhenReload: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoCleanListWhenReload', true)
|
||||
},
|
||||
// 列表刷新时自动显示下拉刷新view,默认为否
|
||||
showRefresherWhenReload: {
|
||||
type: Boolean,
|
||||
default: u.gc('showRefresherWhenReload', false)
|
||||
},
|
||||
// 列表刷新时自动显示加载更多view,且为加载中状态,默认为否
|
||||
showLoadingMoreWhenReload: {
|
||||
type: Boolean,
|
||||
default: u.gc('showLoadingMoreWhenReload', false)
|
||||
},
|
||||
// 组件created时立即触发reload(可解决一些情况下先看到页面再看到loading的问题),auto为true时有效。为否时将在mounted+nextTick后触发reload,默认为否
|
||||
createdReload: {
|
||||
type: Boolean,
|
||||
default: u.gc('createdReload', false)
|
||||
},
|
||||
// 本地分页时上拉加载更多延迟时间,单位为毫秒,默认200毫秒
|
||||
localPagingLoadingTime: {
|
||||
type: [Number, String],
|
||||
default: u.gc('localPagingLoadingTime', 200)
|
||||
},
|
||||
// 自动拼接complete中传过来的数组(使用聊天记录模式时无效)
|
||||
concat: {
|
||||
type: Boolean,
|
||||
default: u.gc('concat', true)
|
||||
},
|
||||
// 请求失败是否触发reject,默认为是
|
||||
callNetworkReject: {
|
||||
type: Boolean,
|
||||
default: u.gc('callNetworkReject', true)
|
||||
},
|
||||
// 父组件v-model所绑定的list的值
|
||||
value: {
|
||||
type: Array,
|
||||
default: function() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
// #ifdef VUE3
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: function() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
data (){
|
||||
return {
|
||||
currentData: [],
|
||||
totalData: [],
|
||||
realTotalData: [],
|
||||
totalLocalPagingList: [],
|
||||
dataPromiseResultMap: {
|
||||
reload: null,
|
||||
complete: null,
|
||||
localPaging: null
|
||||
},
|
||||
isSettingCacheList: false,
|
||||
pageNo: 1,
|
||||
currentRefreshPageSize: 0,
|
||||
isLocalPaging: false,
|
||||
isAddedData: false,
|
||||
isTotalChangeFromAddData: false,
|
||||
privateConcat: true,
|
||||
myParentQuery: -1,
|
||||
firstPageLoaded: false,
|
||||
pagingLoaded: false,
|
||||
loaded: false,
|
||||
isUserReload: true,
|
||||
fromEmptyViewReload: false,
|
||||
queryFrom: '',
|
||||
listRendering: false,
|
||||
isHandlingRefreshToPage: false,
|
||||
isFirstPageAndNoMore: false,
|
||||
totalDataChangeThrow: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pageSize() {
|
||||
return this.defaultPageSize;
|
||||
},
|
||||
finalConcat() {
|
||||
return this.concat && this.privateConcat;
|
||||
},
|
||||
finalUseCache() {
|
||||
if (this.useCache && !this.cacheKey) {
|
||||
u.consoleErr('use-cache为true时,必须设置cache-key,否则缓存无效!');
|
||||
}
|
||||
return this.useCache && !!this.cacheKey;
|
||||
},
|
||||
finalCacheKey() {
|
||||
return this.cacheKey ? `${c.cachePrefixKey}-${this.cacheKey}` : null;
|
||||
},
|
||||
isFirstPage() {
|
||||
return this.pageNo === this.defaultPageNo;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
totalData(newVal, oldVal) {
|
||||
this._totalDataChange(newVal, oldVal, this.totalDataChangeThrow);
|
||||
this.totalDataChangeThrow = true;
|
||||
},
|
||||
currentData(newVal, oldVal) {
|
||||
this._currentDataChange(newVal, oldVal);
|
||||
},
|
||||
useChatRecordMode(newVal, oldVal) {
|
||||
if (newVal) {
|
||||
this.nLoadingMoreFixedHeight = false;
|
||||
}
|
||||
},
|
||||
value: {
|
||||
handler(newVal) {
|
||||
// 当v-model绑定的数据源被更改时,此时数据源改变不emit input事件,避免循环调用
|
||||
if (newVal !== this.totalData) {
|
||||
this.totalDataChangeThrow = false;
|
||||
this.totalData = newVal;
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
// #ifdef VUE3
|
||||
modelValue: {
|
||||
handler(newVal) {
|
||||
// 当v-model绑定的数据源被更改时,此时数据源改变不emit input事件,避免循环调用
|
||||
if (newVal !== this.totalData) {
|
||||
this.totalDataChangeThrow = false;
|
||||
this.totalData = newVal;
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
methods: {
|
||||
// 请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为是否成功(默认为是)
|
||||
complete(data, success = true) {
|
||||
this.customNoMore = -1;
|
||||
return this.addData(data, success);
|
||||
},
|
||||
//【保证数据一致】请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为dataKey,需与:data-key绑定的一致,第三个参数为是否成功(默认为是)
|
||||
completeByKey(data, dataKey = null, success = true) {
|
||||
if (dataKey !== null && this.dataKey !== null && dataKey !== this.dataKey) {
|
||||
this.isFirstPage && this.endRefresh();
|
||||
return new Promise(resolve => resolve());
|
||||
}
|
||||
this.customNoMore = -1;
|
||||
return this.addData(data, success);
|
||||
},
|
||||
//【通过total判断是否有更多数据】请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为total(列表总数),第三个参数为是否成功(默认为是)
|
||||
completeByTotal(data, total, success = true) {
|
||||
if (total == 'undefined') {
|
||||
this.customNoMore = -1;
|
||||
} else {
|
||||
const dataTypeRes = this._checkDataType(data, success, false);
|
||||
data = dataTypeRes.data;
|
||||
success = dataTypeRes.success;
|
||||
if (total >= 0 && success) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.$nextTick(() => {
|
||||
let nomore = false;
|
||||
const realTotalDataCount = this.pageNo == this.defaultPageNo ? 0 : this.realTotalData.length;
|
||||
const dataLength = this.privateConcat ? data.length : 0;
|
||||
let exceedCount = realTotalDataCount + dataLength - total;
|
||||
// 没有更多数据了
|
||||
if (exceedCount >= 0) {
|
||||
nomore = true;
|
||||
// 仅截取total内部分的数据
|
||||
exceedCount = this.defaultPageSize - exceedCount;
|
||||
if (this.privateConcat && exceedCount > 0 && exceedCount < data.length) {
|
||||
data = data.splice(0, exceedCount);
|
||||
}
|
||||
}
|
||||
this.completeByNoMore(data, nomore, success).then(res => resolve(res)).catch(() => reject());
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
return this.addData(data, success);
|
||||
},
|
||||
//【自行判断是否有更多数据】请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging处理,第一个参数为请求结果数组,第二个参数为是否没有更多数据,第三个参数为是否成功(默认是是)
|
||||
completeByNoMore(data, nomore, success = true) {
|
||||
if (nomore != 'undefined') {
|
||||
this.customNoMore = nomore == true ? 1 : 0;
|
||||
}
|
||||
return this.addData(data, success);
|
||||
},
|
||||
// 请求结束且请求失败时调用,支持传入请求失败原因
|
||||
completeByError(errorMsg) {
|
||||
this.customerEmptyViewErrorText = errorMsg;
|
||||
return this.complete(false);
|
||||
},
|
||||
// 与上方complete方法功能一致,新版本中设置服务端回调数组请使用complete方法
|
||||
addData(data, success = true) {
|
||||
if (!this.fromCompleteEmit) {
|
||||
this.disabledCompleteEmit = true;
|
||||
this.fromCompleteEmit = false;
|
||||
}
|
||||
const currentTimeStamp = u.getTime();
|
||||
const disTime = currentTimeStamp - this.requestTimeStamp;
|
||||
let minDelay = this.minDelay;
|
||||
if (this.isFirstPage && this.finalShowRefresherWhenReload) {
|
||||
minDelay = Math.max(400, minDelay);
|
||||
}
|
||||
const addDataDalay = (this.requestTimeStamp > 0 && disTime < minDelay) ? minDelay - disTime : 0;
|
||||
this.$nextTick(() => {
|
||||
u.delay(() => {
|
||||
this._addData(data, success, false);
|
||||
}, this.delay > 0 ? this.delay : addDataDalay)
|
||||
})
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.dataPromiseResultMap.complete = { resolve, reject };
|
||||
});
|
||||
},
|
||||
// 从顶部添加数据,不会影响分页的pageNo和pageSize
|
||||
addDataFromTop(data, toTop = true, toTopWithAnimate = true) {
|
||||
// 数据是否拼接到顶部,如果是聊天记录模式并且列表没有倒置,则应该拼接在底部
|
||||
let addFromTop = !this.isChatRecordModeAndNotInversion;
|
||||
data = Object.prototype.toString.call(data) !== '[object Array]' ? [data] : (addFromTop ? data.reverse() : data);
|
||||
// #ifndef APP-NVUE
|
||||
this.finalUseVirtualList && this._setCellIndex(data, 'top')
|
||||
// #endif
|
||||
|
||||
this.totalData = addFromTop ? [...data, ...this.totalData] : [...this.totalData, ...data];
|
||||
if (toTop) {
|
||||
u.delay(() => this.useChatRecordMode ? this.scrollToBottom(toTopWithAnimate) : this.scrollToTop(toTopWithAnimate));
|
||||
}
|
||||
},
|
||||
// 重新设置列表数据,调用此方法不会影响pageNo和pageSize,也不会触发请求。适用场景:当需要删除列表中某一项时,将删除对应项后的数组通过此方法传递给z-paging。(当出现类似的需要修改列表数组的场景时,请使用此方法,请勿直接修改page中:list.sync绑定的数组)
|
||||
resetTotalData(data) {
|
||||
this.isTotalChangeFromAddData = true;
|
||||
data = Object.prototype.toString.call(data) !== '[object Array]' ? [data] : data;
|
||||
this.totalData = data;
|
||||
},
|
||||
// 设置本地分页数据,请求结束(成功或者失败)调用此方法,将请求的结果传递给z-paging作分页处理(若调用了此方法,则上拉加载更多时内部会自动分页,不会触发@query所绑定的事件)
|
||||
setLocalPaging(data, success = true) {
|
||||
this.isLocalPaging = true;
|
||||
this.$nextTick(() => {
|
||||
this._addData(data, success, true);
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
this.dataPromiseResultMap.localPaging = { resolve, reject };
|
||||
});
|
||||
},
|
||||
// 重新加载分页数据,pageNo会恢复为默认值,相当于下拉刷新的效果(animate为true时会展示下拉刷新动画,默认为false)
|
||||
reload(animate = this.showRefresherWhenReload) {
|
||||
if (animate) {
|
||||
this.privateShowRefresherWhenReload = animate;
|
||||
this.isUserPullDown = true;
|
||||
}
|
||||
if (!this.showLoadingMoreWhenReload) {
|
||||
this.listRendering = true;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this._preReload(animate, false);
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
this.dataPromiseResultMap.reload = { resolve, reject };
|
||||
});
|
||||
},
|
||||
// 刷新列表数据,pageNo和pageSize不会重置,列表数据会重新从服务端获取。必须保证@query绑定的方法中的pageNo和pageSize和传给服务端的一致
|
||||
refresh() {
|
||||
return this._handleRefreshWithDisPageNo(this.pageNo - this.defaultPageNo + 1);
|
||||
},
|
||||
// 刷新列表数据至指定页,例如pageNo=5时则代表刷新列表至第5页,此时pageNo会变为5,列表会展示前5页的数据。必须保证@query绑定的方法中的pageNo和pageSize和传给服务端的一致
|
||||
refreshToPage(pageNo) {
|
||||
this.isHandlingRefreshToPage = true;
|
||||
return this._handleRefreshWithDisPageNo(pageNo + this.defaultPageNo - 1);
|
||||
},
|
||||
// 手动更新列表缓存数据,将自动截取v-model绑定的list中的前pageSize条覆盖缓存,请确保在list数据更新到预期结果后再调用此方法
|
||||
updateCache() {
|
||||
if (this.finalUseCache && this.totalData.length) {
|
||||
this._saveLocalCache(this.totalData.slice(0, Math.min(this.totalData.length, this.pageSize)));
|
||||
}
|
||||
},
|
||||
// 清空分页数据
|
||||
clean() {
|
||||
this._reload(true);
|
||||
this._addData([], true, false);
|
||||
},
|
||||
// 清空分页数据
|
||||
clear() {
|
||||
this.clean();
|
||||
},
|
||||
// reload之前的一些处理
|
||||
_preReload(animate = this.showRefresherWhenReload, isFromMounted = true, retryCount = 0) {
|
||||
const showRefresher = this.finalRefresherEnabled && this.useCustomRefresher;
|
||||
// #ifndef APP-NVUE
|
||||
// 如果获取slot="refresher"高度失败,则不触发reload,直到获取slot="refresher"高度成功
|
||||
if (this.customRefresherHeight === -1 && showRefresher) {
|
||||
u.delay(() => {
|
||||
retryCount ++;
|
||||
// 如果重试次数是10的倍数(也就是每500毫秒),尝试重新获取一下slot="refresher"高度
|
||||
// 此举是为了解决在某些特殊情况下,z-paging组件mounted了,但是未展示在用户面前,(比如在tabbar页面中,未切换到对应tabbar但是通过代码让z-paging展示了,此时控制台会报Error: Not Found:Page,因为这时候去获取dom节点信息获取不到)
|
||||
// 当用户在某个时刻让此z-paging展示在面前时,即可顺利获取到slot="refresher"高度,递归停止
|
||||
if (retryCount % 10 === 0) {
|
||||
this._updateCustomRefresherHeight();
|
||||
}
|
||||
this._preReload(animate, isFromMounted, retryCount);
|
||||
}, c.delayTime / 2);
|
||||
return;
|
||||
}
|
||||
// #endif
|
||||
this.isUserReload = true;
|
||||
this.loadingType = Enum.LoadingType.Refresher;
|
||||
if (animate) {
|
||||
this.privateShowRefresherWhenReload = animate;
|
||||
// #ifndef APP-NVUE
|
||||
if (this.useCustomRefresher) {
|
||||
this._doRefresherRefreshAnimate();
|
||||
} else {
|
||||
this.refresherTriggered = true;
|
||||
}
|
||||
// #endif
|
||||
// #ifdef APP-NVUE
|
||||
this.refresherStatus = Enum.Refresher.Loading;
|
||||
this.refresherRevealStackCount ++;
|
||||
u.delay(() => {
|
||||
this._getNodeClientRect('zp-n-refresh-container', false).then((node) => {
|
||||
if (node) {
|
||||
let nodeHeight = node[0].height;
|
||||
this.nShowRefresherReveal = true;
|
||||
this.nShowRefresherRevealHeight = nodeHeight;
|
||||
u.delay(() => {
|
||||
this._nDoRefresherEndAnimation(0, -nodeHeight, false, false);
|
||||
u.delay(() => {
|
||||
this._nDoRefresherEndAnimation(nodeHeight, 0);
|
||||
}, 10)
|
||||
}, 10)
|
||||
}
|
||||
this._reload(false, isFromMounted);
|
||||
this._doRefresherLoad(false);
|
||||
});
|
||||
}, this.pagingLoaded ? 10 : 100)
|
||||
return;
|
||||
// #endif
|
||||
} else {
|
||||
this._refresherEnd(false, false, false, false);
|
||||
}
|
||||
this._reload(false, isFromMounted);
|
||||
},
|
||||
// 重新加载分页数据
|
||||
_reload(isClean = false, isFromMounted = false, isUserPullDown = false) {
|
||||
this.isAddedData = false;
|
||||
this.insideOfPaging = -1;
|
||||
this.cacheScrollNodeHeight = -1;
|
||||
this.pageNo = this.defaultPageNo;
|
||||
this._cleanRefresherEndTimeout();
|
||||
!this.privateShowRefresherWhenReload && !isClean && this._startLoading(true);
|
||||
this.firstPageLoaded = true;
|
||||
this.isTotalChangeFromAddData = false;
|
||||
if (!this.isSettingCacheList) {
|
||||
this.totalData = [];
|
||||
}
|
||||
if (!isClean) {
|
||||
this._emitQuery(this.pageNo, this.defaultPageSize, isUserPullDown ? Enum.QueryFrom.UserPullDown : Enum.QueryFrom.Reload);
|
||||
let delay = 0;
|
||||
// #ifdef MP-TOUTIAO
|
||||
delay = 5;
|
||||
// #endif
|
||||
u.delay(this._callMyParentQuery, delay);
|
||||
if (!isFromMounted && this.autoScrollToTopWhenReload) {
|
||||
let checkedNRefresherLoading = true;
|
||||
// #ifdef APP-NVUE
|
||||
checkedNRefresherLoading = !this.nRefresherLoading;
|
||||
// #endif
|
||||
checkedNRefresherLoading && this._scrollToTop(false);
|
||||
}
|
||||
}
|
||||
// #ifdef APP-NVUE
|
||||
this.$nextTick(() => {
|
||||
this.nShowBottom = this.realTotalData.length > 0;
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
// 处理服务端返回的数组
|
||||
_addData(data, success, isLocal) {
|
||||
this.isAddedData = true;
|
||||
this.fromEmptyViewReload = false;
|
||||
this.isTotalChangeFromAddData = true;
|
||||
this.refresherTriggered = false;
|
||||
this._endSystemLoadingAndRefresh();
|
||||
const tempIsUserPullDown = this.isUserPullDown;
|
||||
if (this.showRefresherUpdateTime && this.isFirstPage) {
|
||||
u.setRefesrherTime(u.getTime(), this.refresherUpdateTimeKey);
|
||||
this.$refs.refresh && this.$refs.refresh.updateTime();
|
||||
}
|
||||
if (!isLocal && tempIsUserPullDown && this.isFirstPage) {
|
||||
this.isUserPullDown = false;
|
||||
}
|
||||
this.listRendering = true;
|
||||
this.$nextTick(() => {
|
||||
u.delay(() => this.listRendering = false);
|
||||
})
|
||||
let dataTypeRes = this._checkDataType(data, success, isLocal);
|
||||
data = dataTypeRes.data;
|
||||
success = dataTypeRes.success;
|
||||
let delayTime = c.delayTime;
|
||||
if (this.useChatRecordMode) delayTime = 0;
|
||||
this.loadingForNow = false;
|
||||
u.delay(() => {
|
||||
this.pagingLoaded = true;
|
||||
this.$nextTick(()=>{
|
||||
!isLocal && this._refresherEnd(delayTime > 0, true, tempIsUserPullDown);
|
||||
})
|
||||
})
|
||||
if (this.isFirstPage) {
|
||||
this.isLoadFailed = !success;
|
||||
this.$emit('isLoadFailedChange', this.isLoadFailed);
|
||||
if (this.finalUseCache && success && (this.cacheMode === Enum.CacheMode.Always ? true : this.isSettingCacheList)) {
|
||||
this._saveLocalCache(data);
|
||||
}
|
||||
}
|
||||
this.isSettingCacheList = false;
|
||||
if (success) {
|
||||
if (!(this.privateConcat === false && !this.isHandlingRefreshToPage && this.loadingStatus === Enum.More.NoMore)) {
|
||||
this.loadingStatus = Enum.More.Default;
|
||||
}
|
||||
if (isLocal) {
|
||||
// 如果当前是本地分页,则必然是由setLocalPaging方法触发,此时直接本地加载第一页数据即可。后续本地分页加载更多方法由滚动到底部加载更多事件处理
|
||||
this.totalLocalPagingList = data;
|
||||
const localPageNo = this.defaultPageNo;
|
||||
const localPageSize = this.queryFrom !== Enum.QueryFrom.Refresh ? this.defaultPageSize : this.currentRefreshPageSize;
|
||||
this._localPagingQueryList(localPageNo, localPageSize, 0, res => {
|
||||
u.delay(() => {
|
||||
this.completeByTotal(res, this.totalLocalPagingList.length);;
|
||||
}, 0)
|
||||
})
|
||||
} else {
|
||||
// 如果当前不是本地分页,则按照正常分页逻辑进行数据处理&emit数据
|
||||
let dataChangeDelayTime = 0;
|
||||
// #ifdef APP-NVUE
|
||||
if (this.privateShowRefresherWhenReload && this.finalNvueListIs === 'waterfall') {
|
||||
dataChangeDelayTime = 150;
|
||||
}
|
||||
// #endif
|
||||
u.delay(() => {
|
||||
this._currentDataChange(data, this.currentData);
|
||||
this._callDataPromise(true, this.totalData);
|
||||
}, dataChangeDelayTime)
|
||||
}
|
||||
if (this.isHandlingRefreshToPage) {
|
||||
this.isHandlingRefreshToPage = false;
|
||||
this.pageNo = this.defaultPageNo + Math.ceil(data.length / this.pageSize) - 1;
|
||||
if (data.length % this.pageSize !== 0) {
|
||||
this.customNoMore = 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._currentDataChange(data, this.currentData);
|
||||
this._callDataPromise(false);
|
||||
this.loadingStatus = Enum.More.Fail;
|
||||
this.isHandlingRefreshToPage = false;
|
||||
if (this.loadingType === Enum.LoadingType.LoadMore) {
|
||||
this.pageNo --;
|
||||
}
|
||||
}
|
||||
},
|
||||
// 所有数据改变时调用
|
||||
_totalDataChange(newVal, oldVal, eventThrow=true) {
|
||||
if ((!this.isUserReload || !this.autoCleanListWhenReload) && this.firstPageLoaded && !newVal.length && oldVal.length) {
|
||||
return;
|
||||
}
|
||||
this._doCheckScrollViewShouldFullHeight(newVal);
|
||||
if(!this.realTotalData.length && !newVal.length){
|
||||
eventThrow = false;
|
||||
}
|
||||
this.realTotalData = newVal;
|
||||
// emit列表更新事件
|
||||
if (eventThrow) {
|
||||
this.$emit('input', newVal);
|
||||
// #ifdef VUE3
|
||||
this.$emit('update:modelValue', newVal);
|
||||
// #endif
|
||||
this.$emit('update:list', newVal);
|
||||
this.$emit('listChange', newVal);
|
||||
this._callMyParentList(newVal);
|
||||
}
|
||||
this.firstPageLoaded = false;
|
||||
this.isTotalChangeFromAddData = false;
|
||||
this.$nextTick(() => {
|
||||
u.delay(()=>{
|
||||
// emit z-paging内容区域高度改变事件
|
||||
this._getNodeClientRect('.zp-paging-container-content').then(res => {
|
||||
res && this.$emit('contentHeightChanged', res[0].height);
|
||||
});
|
||||
}, c.delayTime * (this.isIos ? 1 : 3))
|
||||
// #ifdef APP-NVUE
|
||||
// 在nvue中延时600毫秒展示底部加载更多,避免底部加载更多太早加载闪一下的问题
|
||||
u.delay(() => {
|
||||
this.nShowBottom = true;
|
||||
}, c.delayTime * 6, 'nShowBottomDelay');
|
||||
// #endif
|
||||
})
|
||||
},
|
||||
// 当前数据改变时调用
|
||||
_currentDataChange(newVal, oldVal) {
|
||||
newVal = [...newVal];
|
||||
// #ifndef APP-NVUE
|
||||
this.finalUseVirtualList && this._setCellIndex(newVal, 'bottom');
|
||||
// #endif
|
||||
if (this.isFirstPage && this.finalConcat) {
|
||||
this.totalData = [];
|
||||
}
|
||||
// customNoMore:-1代表交由z-paging自行判断;1代表没有更多了;0代表还有更多数据
|
||||
if (this.customNoMore !== -1) {
|
||||
// 如果customNoMore等于1 或者 customNoMore不是0并且新增数组长度为0(也就是不是明确的还有更多数据并且新增的数组长度为0),则没有更多数据了
|
||||
if (this.customNoMore === 1 || (this.customNoMore !== 0 && !newVal.length)) {
|
||||
this.loadingStatus = Enum.More.NoMore;
|
||||
}
|
||||
} else {
|
||||
// 如果新增的数据数组长度为0 或者 新增的数组长度小于默认的pageSize,则没有更多数据了
|
||||
if (!newVal.length || (newVal.length && newVal.length < this.defaultPageSize)) {
|
||||
this.loadingStatus = Enum.More.NoMore;
|
||||
}
|
||||
}
|
||||
if (!this.totalData.length) {
|
||||
// #ifdef APP-NVUE
|
||||
// 如果在聊天记录模式+nvue中,并且数据不满一页时需要将列表倒序,因为此时没有将列表旋转180度,数组中第0条数据应当在最底下显示
|
||||
if (this.useChatRecordMode && this.finalConcat && this.isFirstPage && this.loadingStatus === Enum.More.NoMore) {
|
||||
newVal.reverse();
|
||||
}
|
||||
// #endif
|
||||
this.totalData = newVal;
|
||||
} else {
|
||||
if (this.finalConcat) {
|
||||
const currentScrollTop = this.oldScrollTop;
|
||||
this.totalData = [...this.totalData, ...newVal];
|
||||
// 此处是为了解决在微信小程序中,在某些情况下滚动到底部加载更多后滚动位置直接变为最底部的问题,因此需要通过代码强制滚动回加载更多前的位置
|
||||
// #ifdef MP-WEIXIN
|
||||
if (!this.isIos && !this.refresherOnly && !this.usePageScroll && newVal.length) {
|
||||
this.loadingMoreTimeStamp = u.getTime();
|
||||
this.$nextTick(() => {
|
||||
this.scrollToY(currentScrollTop);
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
} else {
|
||||
this.totalData = newVal;
|
||||
}
|
||||
}
|
||||
this.privateConcat = true;
|
||||
},
|
||||
// 根据pageNo处理refresh操作
|
||||
_handleRefreshWithDisPageNo(pageNo) {
|
||||
if (!this.isHandlingRefreshToPage && !this.realTotalData.length) return this.reload();
|
||||
if (pageNo >= 1) {
|
||||
this.loading = true;
|
||||
this.privateConcat = false;
|
||||
const totalPageSize = pageNo * this.pageSize;
|
||||
this.currentRefreshPageSize = totalPageSize;
|
||||
// 如果调用refresh时是本地分页,则在组件内部自己处理分页逻辑,不emit query相关事件
|
||||
if (this.isLocalPaging && this.isHandlingRefreshToPage) {
|
||||
this._localPagingQueryList(this.defaultPageNo, totalPageSize, 0, res => {
|
||||
this.complete(res);
|
||||
})
|
||||
} else {
|
||||
// emit query相关事件
|
||||
this._emitQuery(this.defaultPageNo, totalPageSize, Enum.QueryFrom.Refresh);
|
||||
this._callMyParentQuery(this.defaultPageNo, totalPageSize);
|
||||
}
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.dataPromiseResultMap.reload = { resolve, reject };
|
||||
});
|
||||
},
|
||||
// 本地分页请求
|
||||
_localPagingQueryList(pageNo, pageSize, localPagingLoadingTime, callback) {
|
||||
pageNo = Math.max(1, pageNo);
|
||||
pageSize = Math.max(1, pageSize);
|
||||
const totalPagingList = [...this.totalLocalPagingList];
|
||||
const pageNoIndex = (pageNo - 1) * pageSize;
|
||||
const finalPageNoIndex = Math.min(totalPagingList.length, pageNoIndex + pageSize);
|
||||
const resultPagingList = totalPagingList.splice(pageNoIndex, finalPageNoIndex - pageNoIndex);
|
||||
u.delay(() => callback(resultPagingList), localPagingLoadingTime)
|
||||
},
|
||||
// 存储列表缓存数据
|
||||
_saveLocalCache(data) {
|
||||
uni.setStorageSync(this.finalCacheKey, data);
|
||||
},
|
||||
// 通过缓存数据填充列表数据
|
||||
_setListByLocalCache() {
|
||||
this.totalData = uni.getStorageSync(this.finalCacheKey) || [];
|
||||
this.isSettingCacheList = true;
|
||||
},
|
||||
// 修改父view的list
|
||||
_callMyParentList(newVal) {
|
||||
if (this.autowireListName.length) {
|
||||
const myParent = u.getParent(this.$parent);
|
||||
if (myParent && myParent[this.autowireListName]) {
|
||||
myParent[this.autowireListName] = newVal;
|
||||
}
|
||||
}
|
||||
},
|
||||
// 调用父view的query
|
||||
_callMyParentQuery(customPageNo = 0, customPageSize = 0) {
|
||||
if (this.autowireQueryName) {
|
||||
if (this.myParentQuery === -1) {
|
||||
const myParent = u.getParent(this.$parent);
|
||||
if (myParent && myParent[this.autowireQueryName]) {
|
||||
this.myParentQuery = myParent[this.autowireQueryName];
|
||||
}
|
||||
}
|
||||
if (this.myParentQuery !== -1) {
|
||||
customPageSize > 0 ? this.myParentQuery(customPageNo, customPageSize) : this.myParentQuery(this.pageNo, this.defaultPageSize);
|
||||
}
|
||||
}
|
||||
},
|
||||
// emit query事件
|
||||
_emitQuery(pageNo, pageSize, from){
|
||||
this.queryFrom = from;
|
||||
this.requestTimeStamp = u.getTime();
|
||||
const [lastItem] = this.realTotalData.slice(-1);
|
||||
if (this.fetch) {
|
||||
const fetchParams = interceptor._handleFetchParams({pageNo, pageSize, from, lastItem: lastItem || null}, this.fetchParams);
|
||||
const fetchResult = this.fetch(fetchParams);
|
||||
if (!interceptor._handleFetchResult(fetchResult, this, fetchParams)) {
|
||||
u.isPromise(fetchResult) ? fetchResult.then(res => {
|
||||
this.complete(res);
|
||||
}).catch(err => {
|
||||
this.complete(false);
|
||||
}) : this.complete(fetchResult)
|
||||
}
|
||||
} else {
|
||||
this.$emit('query', ...interceptor._handleQuery(pageNo, pageSize, from, lastItem || null));
|
||||
}
|
||||
},
|
||||
// 触发数据改变promise
|
||||
_callDataPromise(success, totalList) {
|
||||
for (const key in this.dataPromiseResultMap) {
|
||||
const obj = this.dataPromiseResultMap[key];
|
||||
if (!obj) continue;
|
||||
success ? obj.resolve({ totalList, noMore: this.loadingStatus === Enum.More.NoMore }) : this.callNetworkReject && obj.reject(`z-paging-${key}-error`);
|
||||
}
|
||||
},
|
||||
// 检查complete data的类型
|
||||
_checkDataType(data, success, isLocal) {
|
||||
const dataType = Object.prototype.toString.call(data);
|
||||
if (dataType === '[object Boolean]') {
|
||||
success = data;
|
||||
data = [];
|
||||
} else if (dataType !== '[object Array]') {
|
||||
data = [];
|
||||
if (dataType !== '[object Undefined]' && dataType !== '[object Null]') {
|
||||
u.consoleErr(`${isLocal ? 'setLocalPaging' : 'complete'}参数类型不正确,第一个参数类型必须为Array!`);
|
||||
}
|
||||
}
|
||||
return { data, success };
|
||||
},
|
||||
}
|
||||
}
|
||||
144
uni_modules/z-paging/components/z-paging/js/modules/empty.js
Normal file
@ -0,0 +1,144 @@
|
||||
// [z-paging]空数据图view模块
|
||||
import u from '.././z-paging-utils'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
// 是否强制隐藏空数据图,默认为否
|
||||
hideEmptyView: {
|
||||
type: Boolean,
|
||||
default: u.gc('hideEmptyView', false)
|
||||
},
|
||||
// 空数据图描述文字,默认为“没有数据哦~”
|
||||
emptyViewText: {
|
||||
type: [String, Object],
|
||||
default: u.gc('emptyViewText', null)
|
||||
},
|
||||
// 是否显示空数据图重新加载按钮(无数据时),默认为否
|
||||
showEmptyViewReload: {
|
||||
type: Boolean,
|
||||
default: u.gc('showEmptyViewReload', false)
|
||||
},
|
||||
// 加载失败时是否显示空数据图重新加载按钮,默认为是
|
||||
showEmptyViewReloadWhenError: {
|
||||
type: Boolean,
|
||||
default: u.gc('showEmptyViewReloadWhenError', true)
|
||||
},
|
||||
// 空数据图点击重新加载文字,默认为“重新加载”
|
||||
emptyViewReloadText: {
|
||||
type: [String, Object],
|
||||
default: u.gc('emptyViewReloadText', null)
|
||||
},
|
||||
// 空数据图图片,默认使用z-paging内置的图片
|
||||
emptyViewImg: {
|
||||
type: String,
|
||||
default: u.gc('emptyViewImg', '')
|
||||
},
|
||||
// 空数据图“加载失败”描述文字,默认为“很抱歉,加载失败”
|
||||
emptyViewErrorText: {
|
||||
type: [String, Object],
|
||||
default: u.gc('emptyViewErrorText', null)
|
||||
},
|
||||
// 空数据图“加载失败”图片,默认使用z-paging内置的图片
|
||||
emptyViewErrorImg: {
|
||||
type: String,
|
||||
default: u.gc('emptyViewErrorImg', '')
|
||||
},
|
||||
// 空数据图样式
|
||||
emptyViewStyle: {
|
||||
type: Object,
|
||||
default: u.gc('emptyViewStyle', {})
|
||||
},
|
||||
// 空数据图容器样式
|
||||
emptyViewSuperStyle: {
|
||||
type: Object,
|
||||
default: u.gc('emptyViewSuperStyle', {})
|
||||
},
|
||||
// 空数据图img样式
|
||||
emptyViewImgStyle: {
|
||||
type: Object,
|
||||
default: u.gc('emptyViewImgStyle', {})
|
||||
},
|
||||
// 空数据图描述文字样式
|
||||
emptyViewTitleStyle: {
|
||||
type: Object,
|
||||
default: u.gc('emptyViewTitleStyle', {})
|
||||
},
|
||||
// 空数据图重新加载按钮样式
|
||||
emptyViewReloadStyle: {
|
||||
type: Object,
|
||||
default: u.gc('emptyViewReloadStyle', {})
|
||||
},
|
||||
// 空数据图片是否铺满z-paging,默认为否,即填充满z-paging内列表(滚动区域)部分。若设置为否,则为填铺满整个z-paging
|
||||
emptyViewFixed: {
|
||||
type: Boolean,
|
||||
default: u.gc('emptyViewFixed', false)
|
||||
},
|
||||
// 空数据图片是否垂直居中,默认为是,若设置为否即为从空数据容器顶部开始显示。emptyViewFixed为false时有效
|
||||
emptyViewCenter: {
|
||||
type: Boolean,
|
||||
default: u.gc('emptyViewCenter', true)
|
||||
},
|
||||
// 加载中时是否自动隐藏空数据图,默认为是
|
||||
autoHideEmptyViewWhenLoading: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoHideEmptyViewWhenLoading', true)
|
||||
},
|
||||
// 用户下拉列表触发下拉刷新加载中时是否自动隐藏空数据图,默认为是
|
||||
autoHideEmptyViewWhenPull: {
|
||||
type: Boolean,
|
||||
default: u.gc('autoHideEmptyViewWhenPull', true)
|
||||
},
|
||||
// 空数据view的z-index,默认为9
|
||||
emptyViewZIndex: {
|
||||
type: Number,
|
||||
default: u.gc('emptyViewZIndex', 9)
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
customerEmptyViewErrorText: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
finalEmptyViewImg() {
|
||||
return this.isLoadFailed ? this.emptyViewErrorImg : this.emptyViewImg;
|
||||
},
|
||||
finalShowEmptyViewReload() {
|
||||
return this.isLoadFailed ? this.showEmptyViewReloadWhenError : this.showEmptyViewReload;
|
||||
},
|
||||
// 是否展示空数据图
|
||||
showEmpty() {
|
||||
if (this.refresherOnly || this.hideEmptyView || this.realTotalData.length) return false;
|
||||
if (this.autoHideEmptyViewWhenLoading) {
|
||||
if (this.isAddedData && !this.firstPageLoaded && !this.loading) return true;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return !this.autoHideEmptyViewWhenPull && !this.isUserReload;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 点击了空数据view重新加载按钮
|
||||
_emptyViewReload() {
|
||||
let callbacked = false;
|
||||
this.$emit('emptyViewReload', reload => {
|
||||
if (reload === undefined || reload === true) {
|
||||
this.fromEmptyViewReload = true;
|
||||
this.reload().catch(() => {});
|
||||
}
|
||||
callbacked = true;
|
||||
});
|
||||
// 如果用户没有禁止默认的点击重新加载刷新列表事件,则触发列表重新刷新
|
||||
this.$nextTick(() => {
|
||||
if (!callbacked) {
|
||||
this.fromEmptyViewReload = true;
|
||||
this.reload().catch(() => {});
|
||||
}
|
||||
})
|
||||
},
|
||||
// 点击了空数据view
|
||||
_emptyViewClick() {
|
||||
this.$emit('emptyViewClick');
|
||||
},
|
||||
}
|
||||
}
|
||||
113
uni_modules/z-paging/components/z-paging/js/modules/i18n.js
Normal file
@ -0,0 +1,113 @@
|
||||
// [z-paging]i18n模块
|
||||
import { initVueI18n } from '@dcloudio/uni-i18n'
|
||||
import messages from '../../i18n/index.js'
|
||||
const { t } = initVueI18n(messages)
|
||||
|
||||
import u from '.././z-paging-utils'
|
||||
import c from '.././z-paging-constant'
|
||||
import interceptor from '../z-paging-interceptor'
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
finalLanguage() {
|
||||
try {
|
||||
const local = uni.getLocale();
|
||||
const language = this.systemInfo.appLanguage;
|
||||
return local === 'auto' ? interceptor._handleLanguage2Local(language, this._language2Local(language)) : local;
|
||||
} catch (e) {
|
||||
// 如果获取系统本地语言异常,则默认返回中文,uni.getLocale在部分低版本HX或者cli中可能报找不到的问题
|
||||
return 'zh-Hans';
|
||||
}
|
||||
},
|
||||
// 最终的下拉刷新默认状态的文字
|
||||
finalRefresherDefaultText() {
|
||||
return this._getI18nText('zp.refresher.default', this.refresherDefaultText);
|
||||
},
|
||||
// 最终的下拉刷新下拉中的文字
|
||||
finalRefresherPullingText() {
|
||||
return this._getI18nText('zp.refresher.pulling', this.refresherPullingText);
|
||||
},
|
||||
// 最终的下拉刷新中文字
|
||||
finalRefresherRefreshingText() {
|
||||
return this._getI18nText('zp.refresher.refreshing', this.refresherRefreshingText);
|
||||
},
|
||||
// 最终的下拉刷新完成文字
|
||||
finalRefresherCompleteText() {
|
||||
return this._getI18nText('zp.refresher.complete', this.refresherCompleteText);
|
||||
},
|
||||
// 最终的下拉刷新上次更新时间文字
|
||||
finalRefresherUpdateTimeTextMap() {
|
||||
return {
|
||||
title: t('zp.refresherUpdateTime.title'),
|
||||
none: t('zp.refresherUpdateTime.none'),
|
||||
today: t('zp.refresherUpdateTime.today'),
|
||||
yesterday: t('zp.refresherUpdateTime.yesterday')
|
||||
};
|
||||
},
|
||||
// 最终的继续下拉进入二楼文字
|
||||
finalRefresherGoF2Text() {
|
||||
return this._getI18nText('zp.refresher.f2', this.refresherGoF2Text);
|
||||
},
|
||||
// 最终的底部加载更多默认状态文字
|
||||
finalLoadingMoreDefaultText() {
|
||||
return this._getI18nText('zp.loadingMore.default', this.loadingMoreDefaultText);
|
||||
},
|
||||
// 最终的底部加载更多加载中文字
|
||||
finalLoadingMoreLoadingText() {
|
||||
return this._getI18nText('zp.loadingMore.loading', this.loadingMoreLoadingText);
|
||||
},
|
||||
// 最终的底部加载更多没有更多数据文字
|
||||
finalLoadingMoreNoMoreText() {
|
||||
return this._getI18nText('zp.loadingMore.noMore', this.loadingMoreNoMoreText);
|
||||
},
|
||||
// 最终的底部加载更多加载失败文字
|
||||
finalLoadingMoreFailText() {
|
||||
return this._getI18nText('zp.loadingMore.fail', this.loadingMoreFailText);
|
||||
},
|
||||
// 最终的空数据图title
|
||||
finalEmptyViewText() {
|
||||
return this.isLoadFailed ? this.finalEmptyViewErrorText : this._getI18nText('zp.emptyView.title', this.emptyViewText);
|
||||
},
|
||||
// 最终的空数据图reload title
|
||||
finalEmptyViewReloadText() {
|
||||
return this._getI18nText('zp.emptyView.reload', this.emptyViewReloadText);
|
||||
},
|
||||
// 最终的空数据图加载失败文字
|
||||
finalEmptyViewErrorText() {
|
||||
return this.customerEmptyViewErrorText || this._getI18nText('zp.emptyView.error', this.emptyViewErrorText);
|
||||
},
|
||||
// 最终的系统loading title
|
||||
finalSystemLoadingText() {
|
||||
return this._getI18nText('zp.systemLoading.title', this.systemLoadingText);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 获取当前z-paging的语言
|
||||
getLanguage() {
|
||||
return this.finalLanguage;
|
||||
},
|
||||
// 获取国际化转换后的文本
|
||||
_getI18nText(key, value) {
|
||||
const dataType = Object.prototype.toString.call(value);
|
||||
if (dataType === '[object Object]') {
|
||||
const nextValue = value[this.finalLanguage];
|
||||
if (nextValue) return nextValue;
|
||||
} else if (dataType === '[object String]') {
|
||||
return value;
|
||||
}
|
||||
return t(key);
|
||||
},
|
||||
// 系统language转i18n local
|
||||
_language2Local(language) {
|
||||
const formatedLanguage = language.toLowerCase().replace(new RegExp('_', ''), '-');
|
||||
if (formatedLanguage.indexOf('zh') !== -1) {
|
||||
if (formatedLanguage === 'zh' || formatedLanguage === 'zh-cn' || formatedLanguage.indexOf('zh-hans') !== -1) {
|
||||
return 'zh-Hans';
|
||||
}
|
||||
return 'zh-Hant';
|
||||
}
|
||||
if (formatedLanguage.indexOf('en') !== -1) return 'en';
|
||||
return language;
|
||||
}
|
||||
}
|
||||
}
|
||||
374
uni_modules/z-paging/components/z-paging/js/modules/load-more.js
Normal file
@ -0,0 +1,374 @@
|
||||
// [z-paging]滚动到底部加载更多模块
|
||||
import u from '.././z-paging-utils'
|
||||
import Enum from '.././z-paging-enum'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
// 自定义底部加载更多样式
|
||||
loadingMoreCustomStyle: {
|
||||
type: Object,
|
||||
default: u.gc('loadingMoreCustomStyle', {})
|
||||
},
|
||||
// 自定义底部加载更多文字样式
|
||||
loadingMoreTitleCustomStyle: {
|
||||
type: Object,
|
||||
default: u.gc('loadingMoreTitleCustomStyle', {})
|
||||
},
|
||||
// 自定义底部加载更多加载中动画样式
|
||||
loadingMoreLoadingIconCustomStyle: {
|
||||
type: Object,
|
||||
default: u.gc('loadingMoreLoadingIconCustomStyle', {})
|
||||
},
|
||||
// 自定义底部加载更多加载中动画图标类型,可选flower或circle,默认为flower
|
||||
loadingMoreLoadingIconType: {
|
||||
type: String,
|
||||
default: u.gc('loadingMoreLoadingIconType', 'flower')
|
||||
},
|
||||
// 自定义底部加载更多加载中动画图标图片
|
||||
loadingMoreLoadingIconCustomImage: {
|
||||
type: String,
|
||||
default: u.gc('loadingMoreLoadingIconCustomImage', '')
|
||||
},
|
||||
// 底部加载更多加载中view是否展示旋转动画,默认为是
|
||||
loadingMoreLoadingAnimated: {
|
||||
type: Boolean,
|
||||
default: u.gc('loadingMoreLoadingAnimated', true)
|
||||
},
|
||||
// 是否启用加载更多数据(含滑动到底部加载更多数据和点击加载更多数据),默认为是
|
||||
loadingMoreEnabled: {
|
||||
type: Boolean,
|
||||
default: u.gc('loadingMoreEnabled', true)
|
||||
},
|
||||
// 是否启用滑动到底部加载更多数据,默认为是
|
||||
toBottomLoadingMoreEnabled: {
|
||||
type: Boolean,
|
||||
default: u.gc('toBottomLoadingMoreEnabled', true)
|
||||
},
|
||||
// 滑动到底部状态为默认状态时,以加载中的状态展示,默认为否。若设置为是,可避免滚动到底部看到默认状态然后立刻变为加载中状态的问题,但分页数量未超过一屏时,不会显示【点击加载更多】
|
||||
loadingMoreDefaultAsLoading: {
|
||||
type: Boolean,
|
||||
default: u.gc('loadingMoreDefaultAsLoading', false)
|
||||
},
|
||||
// 滑动到底部"默认"文字,默认为【点击加载更多】
|
||||
loadingMoreDefaultText: {
|
||||
type: [String, Object],
|
||||
default: u.gc('loadingMoreDefaultText', null)
|
||||
},
|
||||
// 滑动到底部"加载中"文字,默认为【正在加载...】
|
||||
loadingMoreLoadingText: {
|
||||
type: [String, Object],
|
||||
default: u.gc('loadingMoreLoadingText', null)
|
||||
},
|
||||
// 滑动到底部"没有更多"文字,默认为【没有更多了】
|
||||
loadingMoreNoMoreText: {
|
||||
type: [String, Object],
|
||||
default: u.gc('loadingMoreNoMoreText', null)
|
||||
},
|
||||
// 滑动到底部"加载失败"文字,默认为【加载失败,点击重新加载】
|
||||
loadingMoreFailText: {
|
||||
type: [String, Object],
|
||||
default: u.gc('loadingMoreFailText', null)
|
||||
},
|
||||
// 当没有更多数据且分页内容未超出z-paging时是否隐藏没有更多数据的view,默认为否
|
||||
hideNoMoreInside: {
|
||||
type: Boolean,
|
||||
default: u.gc('hideNoMoreInside', false)
|
||||
},
|
||||
// 当没有更多数据且分页数组长度少于这个值时,隐藏没有更多数据的view,默认为0,代表不限制。
|
||||
hideNoMoreByLimit: {
|
||||
type: Number,
|
||||
default: u.gc('hideNoMoreByLimit', 0)
|
||||
},
|
||||
// 是否显示默认的加载更多text,默认为是
|
||||
showDefaultLoadingMoreText: {
|
||||
type: Boolean,
|
||||
default: u.gc('showDefaultLoadingMoreText', true)
|
||||
},
|
||||
// 是否显示没有更多数据的view
|
||||
showLoadingMoreNoMoreView: {
|
||||
type: Boolean,
|
||||
default: u.gc('showLoadingMoreNoMoreView', true)
|
||||
},
|
||||
// 是否显示没有更多数据的分割线,默认为是
|
||||
showLoadingMoreNoMoreLine: {
|
||||
type: Boolean,
|
||||
default: u.gc('showLoadingMoreNoMoreLine', true)
|
||||
},
|
||||
// 自定义底部没有更多数据的分割线样式
|
||||
loadingMoreNoMoreLineCustomStyle: {
|
||||
type: Object,
|
||||
default: u.gc('loadingMoreNoMoreLineCustomStyle', {})
|
||||
},
|
||||
// 当分页未满一屏时,是否自动加载更多,默认为否(nvue无效)
|
||||
insideMore: {
|
||||
type: Boolean,
|
||||
default: u.gc('insideMore', false)
|
||||
},
|
||||
// 距底部/右边多远时(单位px),触发 scrolltolower 事件,默认为100rpx
|
||||
lowerThreshold: {
|
||||
type: [Number, String],
|
||||
default: u.gc('lowerThreshold', '100rpx')
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
M: Enum.More,
|
||||
// 底部加载更多状态
|
||||
loadingStatus: Enum.More.Default,
|
||||
// 在渲染之后的底部加载更多状态
|
||||
loadingStatusAfterRender: Enum.More.Default,
|
||||
// 底部加载更多时间戳
|
||||
loadingMoreTimeStamp: 0,
|
||||
// 底部加载更多slot
|
||||
loadingMoreDefaultSlot: null,
|
||||
// 是否展示底部加载更多
|
||||
showLoadingMore: false,
|
||||
// 是否是开发者自定义的加载更多,-1代表交由z-paging自行判断;1代表没有更多了;0代表还有更多数据
|
||||
customNoMore: -1,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 底部加载更多配置
|
||||
zLoadMoreConfig() {
|
||||
return {
|
||||
status: this.loadingStatusAfterRender,
|
||||
defaultAsLoading: this.loadingMoreDefaultAsLoading || (this.useChatRecordMode && this.chatLoadingMoreDefaultAsLoading),
|
||||
defaultThemeStyle: this.finalLoadingMoreThemeStyle,
|
||||
customStyle: this.loadingMoreCustomStyle,
|
||||
titleCustomStyle: this.loadingMoreTitleCustomStyle,
|
||||
iconCustomStyle: this.loadingMoreLoadingIconCustomStyle,
|
||||
loadingIconType: this.loadingMoreLoadingIconType,
|
||||
loadingIconCustomImage: this.loadingMoreLoadingIconCustomImage,
|
||||
loadingAnimated: this.loadingMoreLoadingAnimated,
|
||||
showNoMoreLine: this.showLoadingMoreNoMoreLine,
|
||||
noMoreLineCustomStyle: this.loadingMoreNoMoreLineCustomStyle,
|
||||
defaultText: this.finalLoadingMoreDefaultText,
|
||||
loadingText: this.finalLoadingMoreLoadingText,
|
||||
noMoreText: this.finalLoadingMoreNoMoreText,
|
||||
failText: this.finalLoadingMoreFailText,
|
||||
hideContent: !this.loadingMoreDefaultAsLoading && this.listRendering,
|
||||
unit: this.unit,
|
||||
isChat: this.useChatRecordMode,
|
||||
chatDefaultAsLoading: this.chatLoadingMoreDefaultAsLoading
|
||||
};
|
||||
},
|
||||
// 最终的底部加载更多主题
|
||||
finalLoadingMoreThemeStyle() {
|
||||
return this.loadingMoreThemeStyle.length ? this.loadingMoreThemeStyle : this.defaultThemeStyle;
|
||||
},
|
||||
// 最终的底部加载更多触发阈值
|
||||
finalLowerThreshold() {
|
||||
return u.convertToPx(this.lowerThreshold);
|
||||
},
|
||||
// 是否显示默认状态下的底部加载更多
|
||||
showLoadingMoreDefault() {
|
||||
return this._showLoadingMore('Default');
|
||||
},
|
||||
// 是否显示加载中状态下的底部加载更多
|
||||
showLoadingMoreLoading() {
|
||||
return this._showLoadingMore('Loading');
|
||||
},
|
||||
// 是否显示没有更多了状态下的底部加载更多
|
||||
showLoadingMoreNoMore() {
|
||||
return this._showLoadingMore('NoMore');
|
||||
},
|
||||
// 是否显示加载失败状态下的底部加载更多
|
||||
showLoadingMoreFail() {
|
||||
return this._showLoadingMore('Fail');
|
||||
},
|
||||
// 是否显示自定义状态下的底部加载更多
|
||||
showLoadingMoreCustom() {
|
||||
return this._showLoadingMore('Custom');
|
||||
},
|
||||
// 底部加载更多固定高度
|
||||
loadingMoreFixedHeight() {
|
||||
return u.addUnit('80rpx', this.unit);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 页面滚动到底部时通知z-paging进行进一步处理
|
||||
pageReachBottom() {
|
||||
!this.useChatRecordMode && this.toBottomLoadingMoreEnabled && this._onLoadingMore('toBottom');
|
||||
},
|
||||
// 手动触发上拉加载更多(非必须,可依据具体需求使用)
|
||||
doLoadMore(type) {
|
||||
this._onLoadingMore(type);
|
||||
},
|
||||
// 通过@scroll事件检测是否滚动到了底部(顺带检测下是否滚动到了顶部)
|
||||
_checkScrolledToBottom(scrollDiff, checked = false) {
|
||||
// 如果当前scroll-view高度未获取,则获取其高度
|
||||
if (this.cacheScrollNodeHeight === -1) {
|
||||
// 获取当前scroll-view高度
|
||||
this._getNodeClientRect('.zp-scroll-view').then((res) => {
|
||||
if (res) {
|
||||
const scrollNodeHeight = res[0].height;
|
||||
// 缓存当前scroll-view高度,如果获取过了不再获取
|
||||
this.cacheScrollNodeHeight = scrollNodeHeight;
|
||||
// // scrollDiff - this.cacheScrollNodeHeight = 当前滚动区域的顶部与内容底部的距离 - scroll-view高度 = 当前滚动区域的底部与内容底部的距离(也就是最终的与底部的距离)
|
||||
if (scrollDiff - scrollNodeHeight <= this.finalLowerThreshold) {
|
||||
// 如果与底部的距离小于阈值,则判断为滚动到了底部,触发滚动到底部事件
|
||||
this._onLoadingMore('toBottom');
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// scrollDiff - this.cacheScrollNodeHeight = 当前滚动区域的顶部与内容底部的距离 - scroll-view高度 = 当前滚动区域的底部与内容底部的距离(也就是最终的与底部的距离)
|
||||
if (scrollDiff - this.cacheScrollNodeHeight <= this.finalLowerThreshold) {
|
||||
// 如果与底部的距离小于阈值,则判断为滚动到了底部,触发滚动到底部事件
|
||||
this._onLoadingMore('toBottom');
|
||||
} else if (scrollDiff - this.cacheScrollNodeHeight <= 500 && !checked) {
|
||||
// 如果与底部的距离小于500px,则获取当前滚动的位置,延迟150毫秒重复上述步骤再次检测(避免@scroll触发时获取的scrollTop不正确导致的其他问题,此时获取的scrollTop不一定可信)。防止因为部分性能较差安卓设备@scroll采样率过低导致的滚动到底部但是依然没有触发的问题
|
||||
u.delay(() => {
|
||||
this._getNodeClientRect('.zp-scroll-view', true, true).then((res) => {
|
||||
if (res) {
|
||||
this.oldScrollTop = res[0].scrollTop;
|
||||
const newScrollDiff = res[0].scrollHeight - this.oldScrollTop;
|
||||
this._checkScrolledToBottom(newScrollDiff, true);
|
||||
}
|
||||
})
|
||||
}, 150, 'checkScrolledToBottomDelay')
|
||||
}
|
||||
// 检测一下是否已经滚动到了顶部了,因为在安卓中滚动到顶部时scrollTop不一定为0(和滚动到底部一样的原因),所以需要在scrollTop小于150px时,通过获取.zp-scroll-view的scrollTop再判断一下
|
||||
if (this.oldScrollTop <= 150 && this.oldScrollTop !== 0) {
|
||||
u.delay(() => {
|
||||
// 这里再判断一下是否确实已经滚动到顶部了,如果已经滚动到顶部了,则不用再判断了,再次判断的原因是可能150毫秒之后oldScrollTop才是0
|
||||
if (this.oldScrollTop !== 0) {
|
||||
this._getNodeClientRect('.zp-scroll-view', true, true).then((res) => {
|
||||
// 如果150毫秒后.zp-scroll-view的scrollTop为0,则认为已经滚动到了顶部了
|
||||
if (res && res[0].scrollTop === 0 && this.oldScrollTop !== 0) {
|
||||
this._onScrollToUpper();
|
||||
}
|
||||
})
|
||||
}
|
||||
}, 150, 'checkScrolledToTopDelay')
|
||||
}
|
||||
}
|
||||
},
|
||||
// 触发加载更多时调用,from:toBottom-滑动到底部触发;click-点击加载更多触发
|
||||
_onLoadingMore(from = 'click') {
|
||||
// 如果是ios并且是滚动到底部的,则在滚动到底部时候尝试将列表设置为禁止滚动然后设置为允许滚动,以禁止底部bounce的效果
|
||||
if (this.isIos && from === 'toBottom' && !this.scrollToBottomBounceEnabled && this.scrollEnable) {
|
||||
this.scrollEnable = false;
|
||||
this.$nextTick(() => {
|
||||
this.scrollEnable = true;
|
||||
})
|
||||
}
|
||||
// emit scrolltolower
|
||||
this._emitScrollEvent('scrolltolower');
|
||||
// 如果是只使用下拉刷新 或者 禁用底部加载更多 或者 底部加载更多不是默认状态或加载失败状态 或者 是加载中状态 或者 空数据图已经展示了,则return,不触发内部加载更多逻辑
|
||||
if (this.refresherOnly || !this.loadingMoreEnabled || !(this.loadingStatus === Enum.More.Default || this.loadingStatus === Enum.More.Fail) || this.loading || this.showEmpty) return;
|
||||
// #ifdef MP-WEIXIN
|
||||
if (!this.isIos && !this.refresherOnly && !this.usePageScroll) {
|
||||
const currentTimestamp = u.getTime();
|
||||
// 在非ios平台+scroll-view中节流处理
|
||||
if (this.loadingMoreTimeStamp > 0 && currentTimestamp - this.loadingMoreTimeStamp < 100) {
|
||||
this.loadingMoreTimeStamp = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
// 处理加载更多数据
|
||||
this._doLoadingMore();
|
||||
},
|
||||
// 处理开始加载更多
|
||||
_doLoadingMore() {
|
||||
if (this.pageNo >= this.defaultPageNo && this.loadingStatus !== Enum.More.NoMore) {
|
||||
this.pageNo ++;
|
||||
this._startLoading(false);
|
||||
if (this.isLocalPaging) {
|
||||
// 如果是本地分页,则在组件内部对数据进行分页处理,不触发@query事件
|
||||
this._localPagingQueryList(this.pageNo, this.defaultPageSize, this.localPagingLoadingTime, res => {
|
||||
this.completeByTotal(res, this.totalLocalPagingList.length);
|
||||
this.queryFrom = Enum.QueryFrom.LoadMore;
|
||||
})
|
||||
} else {
|
||||
// emit @query相关加载更多事件
|
||||
this._emitQuery(this.pageNo, this.defaultPageSize, Enum.QueryFrom.LoadMore);
|
||||
this._callMyParentQuery();
|
||||
}
|
||||
// 设置当前加载状态为底部加载更多状态
|
||||
this.loadingType = Enum.LoadingType.LoadMore;
|
||||
}
|
||||
},
|
||||
// (预处理)判断当没有更多数据且分页内容未超出z-paging时是否显示没有更多数据的view
|
||||
_preCheckShowNoMoreInside(newVal, scrollViewNode, pagingContainerNode) {
|
||||
if (this.loadingStatus === Enum.More.NoMore && this.hideNoMoreByLimit > 0 && newVal.length) {
|
||||
this.showLoadingMore = newVal.length > this.hideNoMoreByLimit;
|
||||
} else if ((this.loadingStatus === Enum.More.NoMore && this.hideNoMoreInside && newVal.length) || (this.insideMore && this.insideOfPaging !== false && newVal.length)) {
|
||||
this.$nextTick(() => {
|
||||
this._checkShowNoMoreInside(newVal, scrollViewNode, pagingContainerNode);
|
||||
})
|
||||
if (this.insideMore && this.insideOfPaging !== false && newVal.length) {
|
||||
this.showLoadingMore = newVal.length;
|
||||
}
|
||||
} else {
|
||||
this.showLoadingMore = newVal.length;
|
||||
}
|
||||
},
|
||||
// 判断当没有更多数据且分页内容未超出z-paging时是否显示没有更多数据的view
|
||||
async _checkShowNoMoreInside(totalData, oldScrollViewNode, oldPagingContainerNode) {
|
||||
try {
|
||||
const scrollViewNode = oldScrollViewNode || await this._getNodeClientRect('.zp-scroll-view');
|
||||
// 在页面滚动模式下
|
||||
if (this.usePageScroll) {
|
||||
if (scrollViewNode) {
|
||||
// 获取滚动内容总高度
|
||||
const scrollViewTotalH = scrollViewNode[0].top + scrollViewNode[0].height;
|
||||
// 如果滚动内容总高度小于窗口高度,则认为内容未超出z-paging
|
||||
this.insideOfPaging = scrollViewTotalH < this.windowHeight;
|
||||
// 如果需要没有更多数据时,隐藏底部加载更多view,并且内容未超过z-paging,则隐藏底部加载更多
|
||||
if (this.hideNoMoreInside) {
|
||||
this.showLoadingMore = !this.insideOfPaging;
|
||||
}
|
||||
// 如果需要内容未超过z-paging时自动加载更多,则触发加载更多
|
||||
this._updateInsideOfPaging();
|
||||
}
|
||||
} else {
|
||||
// 在scroll-view滚动模式下
|
||||
const pagingContainerNode = oldPagingContainerNode || await this._getNodeClientRect('.zp-paging-container-content');
|
||||
// 获取滚动内容总高度
|
||||
const pagingContainerH = pagingContainerNode ? pagingContainerNode[0].height : 0;
|
||||
// 获取z-paging内置scroll-view高度
|
||||
const scrollViewH = scrollViewNode ? scrollViewNode[0].height : 0;
|
||||
// 如果滚动内容总高度小于z-paging内置scroll-view高度,则认为内容未超出z-paging
|
||||
this.insideOfPaging = pagingContainerH < scrollViewH;
|
||||
if (this.hideNoMoreInside) {
|
||||
this.showLoadingMore = !this.insideOfPaging;
|
||||
}
|
||||
// 如果需要内容未超过z-paging时自动加载更多,则触发加载更多
|
||||
this._updateInsideOfPaging();
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果发生了异常,判断totalData数组长度为0,则认为内容未超出z-paging
|
||||
this.insideOfPaging = !totalData.length;
|
||||
if (this.hideNoMoreInside) {
|
||||
this.showLoadingMore = !this.insideOfPaging;
|
||||
}
|
||||
// 如果需要内容未超过z-paging时自动加载更多,则触发加载更多
|
||||
this._updateInsideOfPaging();
|
||||
}
|
||||
},
|
||||
// 是否要展示上拉加载更多view
|
||||
_showLoadingMore(type) {
|
||||
if (!this.showLoadingMoreWhenReload && (!(this.loadingStatus === Enum.More.Default ? this.nShowBottom : true) || !this.realTotalData.length)) return false;
|
||||
if (((!this.showLoadingMoreWhenReload || this.isUserPullDown || this.loadingStatus !== Enum.More.Loading) && !this.showLoadingMore) ||
|
||||
(!this.loadingMoreEnabled && (!this.showLoadingMoreWhenReload || this.isUserPullDown || this.loadingStatus !== Enum.More.Loading)) || this.refresherOnly) {
|
||||
return false;
|
||||
}
|
||||
if (this.useChatRecordMode && type !== 'Loading') return false;
|
||||
if (!this.zSlots) return false;
|
||||
if (type === 'Custom') {
|
||||
return this.showDefaultLoadingMoreText && !(this.loadingStatus === Enum.More.NoMore && !this.showLoadingMoreNoMoreView);
|
||||
}
|
||||
const res = this.loadingStatus === Enum.More[type] && this.zSlots[`loadingMore${type}`] && (type === 'NoMore' ? this.showLoadingMoreNoMoreView : true);
|
||||
if (res) {
|
||||
// #ifdef APP-NVUE
|
||||
if (!this.isIos) {
|
||||
this.nLoadingMoreFixedHeight = false;
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
return res;
|
||||
},
|
||||
}
|
||||
}
|
||||