我的页和设置页完成

This commit is contained in:
田岩
2025-06-27 17:12:26 +08:00
37 changed files with 3096 additions and 141 deletions
+6
View File
@@ -12,9 +12,15 @@ ENV = 'development'
VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7001'
app入口
<<<<<<< HEAD
# VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001'
# VITE_APP_BASE_API_Url = 'http://25.16.122.65:7001'
# VITE_APP_BASE_API_Url = 'http://25.18.122.66:9786'
=======
# VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001'
# VITE_APP_BASE_API_Url = 'http://25.16.122.65:7001'
# VITE_APP_BASE_API_Url = 'http://25.18.122.66:9786'
>>>>>>> cea58939be69e08335a0aaf3f8fef73c5fdb914a
Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 36 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+48 -1
View File
@@ -62,7 +62,54 @@ export function downloadFile(id) {
});
});
}
// 上传音频 翻译文本
export const uploadVoiceFile = (file) => {
const url = '/trastudy/traStdyInfo/audioTranscriptions';
const base_url = get_base_url(url);
const token = getToken();
const header = {
"Content-Type": "multipart/form-data; charset=UTF-8",
};
if (token)
header['summary'] = token
return new Promise((resolve, reject) => {
uni.uploadFile({
url: `${base_url}${url}`,
header: header,
filePath: file,
name: 'voice',
success(result) {
resolve(result)
},
fail(error) {
reject(new Error('Upload failed'));
}
});
});
};
// 获取音频 通用
export function downloadVoiceFile(url) {
const token = getToken();
const header = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
};
if (token)
header['summary'] = token
let baseUrl = get_base_url();
let _params = new FormData()
return new Promise((resolve, reject) => {
uni.downloadFile({
url: `${baseUrl}${url}`,
header,
success(result) {
resolve(result)
},
fail(error) {
reject(new Error(error));
}
});
});
}
// 对象存储预览 /traoss/tras3/show/{文件ID}
export function previewFile(id, responseType = 'arraybuffer') {
const token = getToken();
+26 -1
View File
@@ -2,11 +2,36 @@ import request from '@/api/request'
const base_url = '/trastudy'
// 获取课程分类树接口
// 获取课程学习信息接口
export const getCourseStudyInfoApi = (data) => {
return request({
url: base_url + '/traStdyInfo/queryTraStdyProgressAndTeacher',
method: 'post',
data
});
};
// 获取课程学习 知识点 接口
export const queryTraStdyBatchWaitParagraphInfoApi = (data) => {
return request({
url: base_url + '/traStdyInfo/queryTraStdyBatchWaitParagraphInfo',
method: 'post',
data
});
};
// 学习段落完成
export const studyParagraphOverApi = (data) => {
return request({
url: base_url + '/traStdyInfo/studyParagraphOver',
method: 'post',
data
});
};
// 上传语音翻译i文本 /traStdyInfo/audioTranscriptions
export const audioTranscriptionsApi = (data) => {
return request({
url: base_url + '/traStdyInfo/audioTranscriptions',
method: 'post',
data
});
};
+9
View File
@@ -36,6 +36,15 @@
}
}
},
{
"path": "pages/welcome/index",
"style": {
"navigationBarTitleText": "欢迎",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/study/index",
"style": {
+245
View File
@@ -0,0 +1,245 @@
<template>
<view class="chart-box">
<view class="cont-box">
<view v-for="width in 3" :class="`percent-${width * 25}`"></view>
<view class="data-box">
<view class="line-box">
<view v-for="width in 3" :class="`percent-${width * 25}`"></view>
</view>
</view>
</view>
<view class="title">
<img class="title-icon" src="@/static/images/course/edit.png" alt="" />
改进建议
</view>
<view class="total-score">
<view class="score-num">57</view>
<img class="total-score-icon" src="@/static/images/course/score.png" alt="" />
</view>
<view v-for="item,index in 4" :class="['score-item', 'score-item'+(index + 1)]">
<view class="score-item-name">词命中率</view>
<view class="score-item-score">98</view>
</view>
</view>
</template>
<script setup>
import { ref, computed} from "vue"
const props = defineProps({
data:{
default:() => [60,70,80,90],
type: Array
},
size:{
default: 262,
type: [Number,String]
}
})
const chartWidth = computed(() => props.size + 'rpx')
const chartHeight = computed(() => props.size + 'rpx')
const scoreMarginX = computed(() => {
return props.size/2 + 'rpx'
})
const scoreMarginY = computed(() => {
return (props.size/2 + 30) + 'rpx'
})
const scoreMarginAbsX = computed(() => {
return '-' + (props.size/2 ) + 'rpx'
})
const scoreMarginAbsY = computed(() => {
return '-' + (props.size/2 - 25) + 'rpx'
})
const styleFour = ref('polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)')
const styleFourResult = computed(()=>{
let score = props.data
let _tx = 50
let _ty = 50 - Math.floor((score[0]/100)*50)
let _rx = 50 + Math.floor((score[1]/100)*50)
let _ry = 50
let _bx = 50
let _by = 50 + Math.floor((score[2]/100)*50)
let _lx = 50 - Math.floor((score[3]/100)*50)
let _ly = 50
return `polygon(${_tx}% ${_ty}%, ${_rx}% ${_ry}%, ${_bx}% ${_by}%, ${_lx}% ${_ly}%)`
})
</script>
<style lang="scss" scoped>
.chart-box{
width: 100%;
height: 580rpx;
background:linear-gradient( 270deg, #4a74ff 0%, #6fa7fe 100%);
overflow: hidden;
position: relative;
border-radius: 16rpx;
margin-bottom: 20rpx;
.cont-box{
background-color: #87abfe;
width: v-bind(chartWidth);
height: v-bind(chartHeight);
clip-path: v-bind(styleFour);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin-top: 25rpx;
&::before{
content: '';
display: block;
width: 100%;
height: 3rpx;
background-color: rgba(255, 255, 255, .3);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 20;
}
&::after{
content: '';
display: block;
height: 100%;
width: 3rpx;
background-color: rgba(255, 255, 255, .3);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 20;
}
@for $i from 1 through 3 {
.percent-#{$i*25}{
width: $i * 25%;
height: $i * 25%;
clip-path: v-bind(styleFour);
background-color: rgba(255, 255, 255, .3);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: (4 - $i) * 2;
&::before{
content: '';
display: block;
width: calc(100% - 10rpx);
height: calc(100% - 10rpx);
background-color: #87abfe;
clip-path: v-bind(styleFour);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}
.data-box{
width: 100%;
height: 100%;
clip-path: v-bind(styleFourResult);
position: absolute;
background-color: #fff;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 12;
overflow: hidden;
.line-box{
display: block;
width: calc(100% - 24rpx);
height: calc(100% - 24rpx);
// background-color: #6691fe;
background-color: #6691fe;
clip-path: v-bind(styleFourResult);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 14;
}
@for $i from 1 through 3 {
.percent-#{$i*25}{
background-color: rgba(255, 255, 255, .1);
z-index: (4 - $i) * 2 + 10;
&::before{
background-color: #6692ff;
}
}
}
}
}
.title{
padding: 24rpx;
line-height: 46rpx;
font-size: 32rpx;
color: #fff;
.title-icon{
width: 27rpx;
height: 27rpx;
}
}
.total-score{
position: absolute;
right: 30rpx;
top:30rpx;
width: 100rpx;
height: 80rpx;
line-height: 70rpx;
font-size: 30rpx;
color: #fff;
white-space: nowrap;
text-shadow: 0 4px 12px rgba(0, 0, 0, .33);
.score-num{
display: inline-block;
font-size: 60rpx;
line-height: 70rpx;
font-weight: bold;
}
.total-score-icon{
width: 100%;
height: 16rpx;
position: absolute;
bottom:0;
left:0;
}
}
.score-item{
position: absolute;
color: #fff;
width: 120rpx;
text-align: center;
.score-item-name{
font-size: 28rpx;
line-height: 36rpx;
margin-bottom: 4rpx;
}
.score-item-score{
font-size: 38rpx;
line-height: 46rpx;
font-weight: bold;
}
&.score-item1{
left: 50%;
margin-left: -60rpx;
bottom: 50%;
transform: translateY(v-bind(scoreMarginAbsY));
}
&.score-item2{
left: 0%;
top: 50%;
transform: translate(calc(v-bind(scoreMarginX) - 60rpx), calc(-50% + 25rpx));
}
&.score-item3{
left: 50%;
margin-left: -60rpx;
top: 50%;
transform: translateY(v-bind(scoreMarginY));
}
&.score-item4{
right: 0%;
top: 50%;
transform: translate(calc(v-bind(scoreMarginAbsX) + 60rpx), calc(-50% + 25rpx));
}
}
}
</style>
+251
View File
@@ -0,0 +1,251 @@
<template>
<view class="chart-box">
<view class="cont-box">
<view class="total-line total-line1"></view>
<view class="total-line total-line2"></view>
<view class="total-line total-line3"></view>
<view v-for="width in 3" :class="`percent-${width * 25}`"></view>
<view class="data-box">
<view class="line-box">
<view v-for="width in 3" :class="`percent-${width * 25}`"></view>
</view>
</view>
</view>
<view class="title">
<img class="title-icon" src="@/static/images/course/edit.png" alt="" />
改进建议
</view>
<view class="total-score">
<view class="score-num">57</view>
<img class="total-score-icon" src="@/static/images/course/score.png" alt="" />
</view>
<view v-for="item,index in 3" :class="['score-item', 'score-item'+(index + 1)]">
<view class="score-item-name">词命中率</view>
<view class="score-item-score">98</view>
</view>
</view>
</template>
<script setup>
import { ref, computed} from "vue"
const props = defineProps({
data:{
default:() => [60,70,80,90],
type: Array
},
size:{
default: 262,
type: [Number,String]
}
})
let _scale = .9
const chartWidth = computed(() => props.size / _scale + 'rpx')
const chartHeight = computed(() => props.size / _scale + 'rpx')
const scoreMarginX = computed(() => {
return (props.size/2 + 20) + 'rpx'
})
const scoreMarginAbsY = computed(() => {
return '-' + (props.size/2 - 40) + 'rpx'
})
const scoreMarginPercentY = computed(() => {
let score = props.data
return setPercent(score[2], Math.round(100 * 1.732 / 6 * _scale * 100)/100) + '%'
})
const styleFour = computed(()=>{
// let _blank = (50 - Math.round(100 * 1.732 / 3))/2
// let _margin = _blank +
let _tx = 50
let _ty = 50 - Math.round(100 * 1.732 / 3)* _scale
let _rx = 50 + 50 *_scale
let _ry = 50 + Math.round(100 * 1.732 / 6)* _scale
let _bx = 50 - 50 *_scale
let _by = 50 + Math.round(100 * 1.732 / 6)* _scale
return `polygon(${_tx}% ${_ty}%, ${_rx}% ${_ry}%, ${_bx}% ${_by}%)`
})
const styleFourResult = computed(()=>{
let score = props.data
let _tx = 50
let _ty = 50 - setPercent(score[0], Math.round(100 * 1.732 / 3 * _scale * 100)/100)
let _rx = 50 + setPercent(score[1], Math.round(50 *_scale * 100)/100)
let _ry = 50 + setPercent(score[1], Math.round(100 * 1.732 / 6 * _scale * 100)/100)
let _bx = 50 - setPercent(score[2], Math.round(50 *_scale * 100)/100)
let _by = 50 + setPercent(score[2], Math.round(100 * 1.732 / 6 * _scale * 100)/100)
return `polygon(${_tx}% ${_ty}%, ${_rx}% ${_ry}%, ${_bx}% ${_by}%)`
})
const setPercent = (precent, show) => {
return Math.floor((precent/100) * show)
}
</script>
<style lang="scss" scoped>
.chart-box{
width: 100%;
height: 580rpx;
background:linear-gradient( 270deg, #4a74ff 0%, #6fa7fe 100%);
overflow: hidden;
position: relative;
border-radius: 16rpx;
margin-bottom: 20rpx;
.cont-box{
background-color: #87abfe;
width: v-bind(chartWidth);
height: v-bind(chartHeight);
clip-path: v-bind(styleFour);
position: absolute;
top: calc(60% + 20rpx);
left: 50%;
transform: translate(-50%, -50%);
.total-line{
width: 57.7%;
height: 3rpx;
background-color: rgba(255, 255, 255, .3);
position: absolute;
overflow: hidden;
z-index: 20;
&.total-line1{
top: 64.5%;
left: 46.5%;
transform: rotateZ(30deg);
}
&.total-line2{
top: 64.5%;
left:-4.5%;
transform: rotateZ(150deg);
}
&.total-line3{
top: 21.5%;
left: 21%;
transform: rotateZ(-90deg);
}
}
@for $i from 1 through 3 {
.percent-#{$i*25}{
width: $i * 25%;
height: $i * 25%;
clip-path: v-bind(styleFour);
background-color: rgba(255, 255, 255, .3);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: (4 - $i) * 2;
&::before{
content: '';
display: block;
width: calc(100% - 10rpx);
height: calc(100% - 10rpx);
background-color: #87abfe;
clip-path: v-bind(styleFour);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}
.data-box{
width: 100%;
height: 100%;
clip-path: v-bind(styleFourResult);
position: absolute;
background-color: #fff;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 12;
overflow: hidden;
.line-box{
display: block;
width: calc(100% - 24rpx);
height: calc(100% - 24rpx);
// background-color: #6691fe;
background-color: #6691fe;
clip-path: v-bind(styleFourResult);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 14;
}
@for $i from 1 through 3 {
.percent-#{$i*25}{
background-color: rgba(255, 255, 255, .1);
z-index: (4 - $i) * 2 + 10;
&::before{
background-color: #6692ff;
}
}
}
}
}
.title{
padding: 24rpx;
line-height: 46rpx;
font-size: 32rpx;
color: #fff;
.title-icon{
width: 27rpx;
height: 27rpx;
}
}
.total-score{
position: absolute;
right: 30rpx;
top:30rpx;
width: 100rpx;
height: 80rpx;
line-height: 70rpx;
font-size: 30rpx;
color: #fff;
white-space: nowrap;
text-shadow: 0 4px 12px rgba(0, 0, 0, .33);
.score-num{
display: inline-block;
font-size: 60rpx;
line-height: 70rpx;
font-weight: bold;
}
.total-score-icon{
width: 100%;
height: 16rpx;
position: absolute;
bottom:0;
left:0;
}
}
.score-item{
position: absolute;
color: #fff;
width: 120rpx;
text-align: center;
.score-item-name{
font-size: 28rpx;
line-height: 36rpx;
margin-bottom: 4rpx;
}
.score-item-score{
font-size: 38rpx;
line-height: 46rpx;
font-weight: bold;
}
&.score-item1{
left: 50%;
margin-left: -60rpx;
bottom: 50%;
transform: translateY(v-bind(scoreMarginAbsY));
}
&.score-item2{
left: calc( 50% + v-bind(scoreMarginX));
bottom: calc(v-bind(scoreMarginPercentY) - 30rpx);
}
&.score-item3{
right: calc( 50% + v-bind(scoreMarginX));
bottom: calc(v-bind(scoreMarginPercentY) - 30rpx);
}
}
}
</style>
+142 -25
View File
@@ -2,39 +2,54 @@
<view class="ai-answer" v-if="type === 0">
<view class="user-info">
<view class="avatar-box">
<ImagePreview class="img-box" :imgId="''"></ImagePreview>
<ImagePreview class="img-box" :imgId="dataInfo?.userInfo?.imgAddr"></ImagePreview>
</view>
<view class="ai-name">
梁安娜
<view class="ai-name-desc">AI课程教练</view>
{{ dataInfo?.userInfo?.teacherName }}
<view class="ai-name-desc">{{dataInfo?.userInfo?.teacherDesc}}</view>
</view>
</view>
<view class="talking-info">
<view class="talking-gif is-play"></view>
<view :class="['talking-gif', activeVoiceId === dataInfo.id?'is-play':'']"></view>
<view class="talking-cont">
<view class="talking-text">
{{ dataInfo.pgrphContent }}
</view>
<ImagePreview class="cont-img-box" v-if="dataInfo.imageAddrs.length>0" :imgId="dataInfo.imageAddrs[0]"></ImagePreview>
<VideoPreview class="cont-img-box" v-if="dataInfo.vidoAddrs.length>0" :imgId="dataInfo.vidoAddrs[0]"></VideoPreview>
<ChartFour/>
<ChartThree/>
<!-- <ChartFour/>
<ChartThree/> -->
</view>
<!-- <view class="talking-cont">
<view class="talking-text">
阿列克山大是快乐的你艾德卡省的mask安东拉森扣篮大赛了快递那里是啊克里斯蒂娜老师的阿萨是看
</view>
<!-- <ImagePreview class="cont-img-box" :imgId="''"></ImagePreview>
<VideoPreview class="cont-img-box" :imgId="''"></VideoPreview> -->
</view>
<view class="talking-cont">
<view class="talking-text">
阿列克山大是快乐的你艾德卡省的mask安东拉森扣篮大赛了快递那里是啊克里斯蒂娜老师的阿萨是看
</view>
</view>
</view> -->
<view class="btns-cont">
<view class="replay-btn">
<view class="replay-btn" @click="restartPlay">
<image class="icon" src="@/static/images/course/replay.png" style="width: 100%;height: 100%;"></image>
</view>
<view class="play-btn">
<image class="icon" v-if="false" src="@/static/images/course/play.png" style="width: 100%;height: 100%;"></image>
<image class="icon" src="@/static/images/course/pause.png" style="width: 100%;height: 100%;"></image>
<image
class="icon"
src="@/static/images/course/play.png"
style="width: 100%;height: 100%;"
@click="playVoice"
v-show="props.activeVoiceId !== dataInfo.id"
></image>
<image
class="icon"
v-show="props.activeVoiceId === dataInfo.id"
src="@/static/images/course/pause.png"
style="width: 100%;height: 100%;"
@click="pauseVoice"
></image>
</view>
<view class="next-learn-btn">继续学<image class="icon icon-iamge" src="@/static/images/course/goon.png" ></image></view>
<!-- <view class="replay-learn-btn">重新学<image class="icon icon-iamge" src="@/static/images/course/restart.png" ></image></view> -->
<view class="replay-study-btn"><image class="icon icon-iamge" src="@/static/images/course/restudy.png" ></image></view>
<view class="next-btn">继续
<!-- <view class="next-learn-btn">继续学<image class="icon icon-iamge" src="@/static/images/course/goon.png" ></image></view>
<view class="replay-study-btn"><image class="icon icon-iamge" src="@/static/images/course/restudy.png" ></image></view> -->
<view class="next-btn" @click="goOnGetQues" v-if="showNextBtn">继续
<image class="icon icon-iamge" src="@/static/images/course/goon.png" ></image></view>
</view>
</view>
@@ -42,10 +57,11 @@
<view class="user-answer" v-else-if="type === 1">
<view class="user-info">
<view class="avatar-box">
<ImagePreview class="img-box" :imgId="''"></ImagePreview>
<!-- <ImagePreview class="img-box" :imgId="''"></ImagePreview> -->
<img class="img-box" :src="userInfo.imageAddr" alt="" fit="contain" style="width: 100%; height: 100%;"/>
</view>
<view class="ai-name">
玛卡巴卡
{{ userInfo.userName }}
</view>
</view>
@@ -53,7 +69,7 @@
<view class="talking-gif"></view>
<view class="talking-cont">
<view class="talking-text">
阿列克山大是快乐的你艾德卡省的mask安东拉森扣篮大赛了快递那里是啊克里斯蒂娜老师的阿萨是看
{{ dataInfo.talkingContent }}
</view>
</view>
<view class="btns-cont">
@@ -67,18 +83,112 @@
</view>
</view>
</view>
<view class="user-answer" v-else-if="type===2">
<view class="user-info">
<view class="avatar-box">
<!-- <ImagePreview class="img-box" :imgId="''"></ImagePreview> -->
<img class="img-box" :src="userInfo.imageAddr" alt="" fit="contain" style="width: 100%; height: 100%;"/>
</view>
<view class="ai-name">
{{ userInfo.userName }}
</view>
</view>
<view class="talking-info-less">
<view class="talking-text">
{{ dataInfo.talkingContent }}
</view>
</view>
</view>
<view v-else></view>
</template>
<script setup>
import ImagePreview from '@/components/image-preview/image-preview.vue'
import VideoPreview from '@/components/image-preview/video-preview.vue'
import { watch, unref, ref, toRefs, computed } from 'vue'
import ImagePreview from '@/components/image-preview/image-preview.vue'
import VideoPreview from '@/components/image-preview/video-preview.vue'
import { downloadVoiceFile } from "@/api/common.js"
import { studyParagraphOverApi } from "@/api/study.js"
import ChartFour from "./chartFour.vue"
import ChartThree from "./chartThree.vue"
import { getUserInfo } from '@/common/common';
const props = defineProps({
type:{
default: 0,// 1 userAnswer 0: AIAnswer
type:[Number,String]
},
dataInfo: {
default: () => ({}),
type: Object
},
activeVoiceId:{
default:'',
type: String
}
})
const { type, dataInfo, activeVoiceId } = toRefs(props)
const emit = defineEmits(['changePlay', 'nextQuestion'])
const talkVoiceObj = uni.createInnerAudioContext()
const userInfo = computed(() => getUserInfo())
watch(() => props.activeVoiceId,(val) => {
if(val && (val !== unref(dataInfo).id)){
talkVoiceObj?.stop()
}
},{
deep: true,
immediate: true
})
talkVoiceObj.onEnded(()=>{
// talkVoiceObj.src = ''
emit('changePlay',unref(dataInfo)?.id)
})
const pauseVoice = () => {
if(!talkVoiceObj) return
talkVoiceObj.pause()
emit('changePlay',unref(dataInfo)?.id)
}
const restartPlay = () => {
console.log(userInfo)
if(talkVoiceObj.src){
talkVoiceObj.seek(0)
talkVoiceObj.play()
}else{
playVoice()
}
}
const playVoice = (readType = 'pgth') =>{
// readType 阅读内容类型pgrh段落/qns问题
emit('changePlay', unref(dataInfo)?.id)
if(talkVoiceObj.src){
talkVoiceObj.play()
}else{
let _params = {
crsId: unref(dataInfo)?.crsId || '',
pgrphId: unref(dataInfo)?.pgrphId || '',
qnsId: unref(dataInfo)?.qnsId || '',
readType: readType,
teachNo: unref(dataInfo)?.userInfo?.teacherNo, // 必填
content: unref(dataInfo)?.pgrphContent,
}
let _url = '/trastudy/traStdyInfo/readContent?' + Object.keys(_params).map(t=>{
return encodeURIComponent(t) + '=' + encodeURIComponent(_params[t])
}).join('&')
downloadVoiceFile(_url).then(res=>{
talkVoiceObj.src = res.tempFilePath
talkVoiceObj.play()
})
}
}
const showNextBtn = ref(true)
const goOnGetQues = () => {
studyParagraphOverApi({
pgrphId:unref(dataInfo)?.pgrphId
}).then(res=>{
showNextBtn.value = false
emit('nextQuestion', unref(dataInfo))
})
}
</script>
<style lang="scss">
@@ -239,7 +349,7 @@
}
}
.user-answer{
overflow: hidden;
.user-info{
overflow: hidden;
.avatar-box{
@@ -265,6 +375,13 @@
}
}
.talking-info-less{
padding: 20rpx;
float: right;
border-radius: 15rpx;
background-color: #06f;
color: #fff;
}
.talking-info{
padding: 20rpx;
border-radius: 15rpx;
+183 -80
View File
@@ -7,7 +7,6 @@
<uv-icon class="arrow-icon" name="arrow-left" @click="common.navigateBack()"/>
语言选择开启学习之旅
</view>
<!-- <video v-if="videoUrlShow" :src="videoUrlShow" controls></video> -->
<view class="voice-list">
<uv-radio-group v-model="choiceTeacher">
<view class="voice-item" v-for="item in teacherList" :key="item.teacherNo" @click="choiceTeacher = item.teacherNo">
@@ -18,12 +17,11 @@
<view class="right-name">{{item.teacherName}}</view>
<view class="right-desc">描述{{item.voiceDesc || '--'}}</view>
<view class="audio-box">
<view class="talking-gif"></view>
<view class="play-btn">
<image class="icon" v-if="false" src="@/static/images/course/play.png" style="width: 100%;height: 100%;"></image>
<image class="icon" src="@/static/images/course/pause-focus.png" style="width: 100%;height: 100%;"></image>
<view :class="['talking-gif', activeVoiceTeachNo === item.teacherNo ? 'is-play':'']"></view>
<view class="play-btn" @click="playTeacherVoice(item)">
<image class="icon" v-if="activeVoiceTeachNo !== item.teacherNo" src="@/static/images/course/play-blue.png" style="width: 100%;height: 100%;"></image>
<image class="icon" v-else src="@/static/images/course/stop-blue.png" style="width: 100%;height: 100%;"></image>
</view>
<!-- <view class="talking-time">00:26</view> -->
</view>
</view>
<uv-radio :name="item.teacherNo" class="radio-box" @click.stop/>
@@ -31,7 +29,6 @@
</uv-radio-group>
<view class="start-btns" @click="nextStep">
开启学习之旅
<!-- <view class="touch-btn">开启学习之旅</view> -->
</view>
</view>
</view>
@@ -41,8 +38,19 @@
课程学习
</view>
<view class="talking-list">
<view class="study-num">1/20</view>
<TalkingItem class="talking-item" v-for="item in 8" :type="item%2"></TalkingItem>
<view class="study-num">
<view class="color-blue">1</view> / 20
</view>
<TalkingItem
class="talking-item"
v-for="item in talkingList"
:type="item.type"
:dataInfo="item"
:activeVoiceId="activeVoiceTalkingItemId"
@changePlay="changePlayItemId"
@nextQuestion="nextQuestionSuccess"
></TalkingItem>
<!-- <video v-if="videoUrlShow" :src="videoUrlShow" controls></video> -->
</view>
<view class="talk-btns">
<view
@@ -50,7 +58,9 @@
@touchstart="showOverlayTalking"
@touchend="hideOverlayTalking"
@touchmove="handleMove"
>按住说话</view>
>按住说话
<image class="talk-btn-icon" src="/src/static/images/course/jianpan.png" mode=""></image>
</view>
</view>
</view>
<uv-overlay :show="showTalkingModel" opacity=".9" @click="showTalkingModel = false">
@@ -66,9 +76,6 @@
<image class="btn-box" v-if="!isOut" src="/src/static/images/course/close.png" mode=""></image>
<image class="btn-box" v-else src="/src/static/images/course/close-active.png" mode=""></image>
</view>
<!-- <view class="translate-btn">转文字 发送
<image class="btn-box" src="/src/static/images/course/to-text.png" mode=""></image>
</view> -->
</view>
<!-- <view class="talk-dialog">
<view class="voice-gif"></view>
@@ -81,10 +88,10 @@
<script setup>
import { reactive, ref, computed, unref, onMounted, onUnmounted } from 'vue';
import { onLoad,onShow,onHide } from '@dcloudio/uni-app';
import { previewFile, downloadFile } from "@/api/common.js"
import { previewFile, downloadFile, downloadVoiceFile, uploadVoiceFile } from "@/api/common.js"
import { getToken } from '@/common/common.js'
import { get_base_url } from '@/api/request'
import { getCourseStudyInfoApi } from "@/api/study.js"
import { getCourseStudyInfoApi, queryTraStdyBatchWaitParagraphInfoApi,audioTranscriptionsApi } from "@/api/study.js"
import common from '@/common/common';
import TalkingItem from '@/pages/course/components/talkingItem.vue'
import ImagePreview from '@/components/image-preview/image-preview.vue'
@@ -94,102 +101,174 @@ const step = ref(0)
const teacherList = ref([])
const choiceTeacher = ref('')
const videoUrlShow = ref('')
const talkingList = ref([])
const activeVoiceTalkingItemId = ref('')
const getData = async () => {
try{
const { body } = await getCourseStudyInfoApi({crsId: crsId.value})
if(body.stdyProgressFlag === 'N'){
step.value = 1
teacherList.value = body.teacheList
teacherList.value = body.teacheList.map(t=>({
...t,
voicePath: '',
voiceObj: null
}))
initsocketTask(1)
}else{
step.value = 2
choiceTeacher.value = body.choiceTeacher
initWS(2)
initsocketTask(2)
}
}catch{
}
// downloadFile('S3F0250181220722025062409312400000331622').then(res=>{
// console.log(res)
// audioObj.value = uni.createInnerAudioContext()
// audioObj.value.src = res.tempFilePath
// })
// downloadFile('S3F0250640161332025061618052000000267276').then(res=>{
// videoUrlShow.value = res.tempFilePath
// })
};
var WS = null
// 关闭播放状态
const changePlayItemId = (id) => {
if(activeVoiceTalkingItemId.value === id){
activeVoiceTalkingItemId.value = ''
}else{
activeVoiceTalkingItemId.value = id
}
}
const choiceTeacherInfo = computed(()=>{
return teacherList.value.find(t=> t.teacherNo === choiceTeacher.value)
})
const activeVoiceTeachNo = ref('')
const teacherVoiceObj = uni.createInnerAudioContext()
teacherVoiceObj.onEnded(()=>{
activeVoiceTeachNo.value = ''
teacherVoiceObj.src = ''
})
const playTeacherVoice = (item) =>{
teacherVoiceObj.stop()
teacherVoiceObj.src = ''
let _isStop = item.teacherNo === activeVoiceTeachNo.value
activeVoiceTeachNo.value = _isStop ? '' : item.teacherNo
if(_isStop) return
if(item.voicePath){
teacherVoiceObj.src = item.voicePath
teacherVoiceObj.play()
}else{
downloadVoiceFile('/trastudy/traStdyInfo/trialListening?teachNo='+item.teacherNo).then(res=>{
item.voicePath = res.tempFilePath
teacherVoiceObj.src = item.voicePath
teacherVoiceObj.play()
})
}
}
var socketTask = null
const nextStep = () => {
if(choiceTeacher.value){
initWS(1)
teacherVoiceObj.destroy()
console.log(111)
if( socketTask && socketTask.readyState === 1){
sendMessage({
"commond" : 'open',
"body" : {
crsId: crsId.value,
teacherNo: choiceTeacher.value
}
})
step.value = 2
getGraphInfoUnStudy()
}
}else{
uni.showToast({
title: '请选择一位教师!',
icon: "none",
duration
duration:1000
});
}
}
const reconnectTimer = null // 重连timer
const isManualClose = false // 是否手动关闭
const initWS = ($step) => {
WS = uni.connectSocket({
url: get_base_url() + '/trastudy/trasoket/open?summary=' + getToken(),
complete: function(cv){
console.log(cv)
}
const getGraphInfoUnStudy = () => {
queryTraStdyBatchWaitParagraphInfoApi({crsId: crsId.value}).then(res=>{
console.log(res)
let _id = new Date().getTime() + 'id'
talkingList.value.push({
...res.body,
type: 0,
userInfo: {...choiceTeacherInfo.value},
id: _id
})
})
WS.onMessage = (event) => {
console.log(event,111)
}
WS.onSocketMessage = (event) => {
console.log(event,222)
}
WS.onOpen = () =>{
console.log('建立链接')
}
WS.onError = (event) => {
console.log('error',event)
}
// WS.onclose = () => {
}
// 继续 执行方法
const nextQuestionSuccess = (info) => {
let _id = new Date().getTime() + 'id'
talkingList.value.push({
crsId: info.crsId,
pgrphId: info.pgrphId,
talkingContent: '继续',
type: 2,
id: _id
})
getGraphInfoUnStudy()
}
const reconnectTimer = null // 重连timer
const isManualClose = ref(false) // 是否手动关闭
const initsocketTask = ($step) => {
socketTask = uni.connectSocket({
url: get_base_url() + '/trastudy/trasoket/open?summary=' + getToken(),
complete: function(){}
})
uni.onSocketMessage((res)=>{
let _data = JSON.parse(res.data)
console.log(_data)
})
uni.onSocketOpen((e)=>{
console.log('链接成功!')
})
uni.onSocketError((e)=>{
console.log(e,111)
})
uni.onSocketClose((e)=>{
// 重连
reconnect()
})
// socketTask.onMessage = (event) => {
// console.log(event,111)
// }
// socketTask.onOpen = () =>{
// console.log('建立链接')
// }
// socketTask.onError = (event) => {
// console.log('error',event)
// }
// socketTask.onclose = () => {
// // 重连
// // initWS()
// // initsocketTask()
// }
setTimeout(()=>{
console.log(WS , WS.readyState)
if( WS && WS.readyState === 1){
let _params = $step === 1 ?{
"commond" : 'open',
"body" : {
crsId: crsId.value,
teacherNo: choiceTeacher.value
}
}:{}
sendMessage(_params)
step.value = 2
}
},100)
}
const reconnect = ()=> {
if(reconnectTimer || isManualClose) return
if(reconnectTimer || isManualClose.value) return
reconnectTimer = setTimeout(()=>{
initWS(2)
initsocketTask(2)
},3000)
}
const sendMessage = (data) => {
WS.send({
const sendMessage = (data) => {
socketTask.send({
data: JSON.stringify(data)
})
}
const closeWebSocket = () => {
isManualClose = true
if(WS){
WS.close()
WS = null
isManualClose.value = true
if(socketTask){
// socketTask.close()
socketTask = null
uni.closeSocket({})
}
}
onLoad((params) => {
crsId.value = params.crsId||'';
crsNum.value = params.crsNum||'';
@@ -207,32 +286,29 @@ const initRecord = () => {
})
}
const startRecord = () => {
unref(recordObj).start()
unref(recordObj).start({format:'mp3'})
}
const stopRecord = () => {
unref(recordObj).stop()
}
// onMounted(()=>{
// initRecord()
// })
// #endif
//#ifndef APP-PLUS
const recordObj = {}
const initRecord = () => {}
// #endif
onMounted(()=>{
getData();
// getData();
initRecord?.()
})
onShow(()=>{
getData();
})
onHide(()=>{
closeWebSocket()
})
onUnmounted(()=>{
onHide(()=>{
closeWebSocket()
})
// onUnmounted(()=>{
// closeWebSocket()
// })
const playRecord = () => {
audioObj.src = audioPath.value
unref(audioObj).play()
@@ -255,7 +331,9 @@ const showOverlayTalking = (obj) => {
touchX.value = obj.changedTouches[0].pageX
touchY.value = obj.changedTouches[0].pageY
baseY.value = obj.currentTarget.offsetTop
//#ifdef APP-PLUS
startRecord()
// #endif
}
const hideOverlayTalking = (obj) => {
showTalkingModel.value = false
@@ -264,6 +342,17 @@ const hideOverlayTalking = (obj) => {
// #endif
if(!isOut.value){
console.log('发送')
// let formData = new FormData()
// formData.append('voice',)
console.log(audioPath.value)
setTimeout(()=>{
uploadVoiceFile(audioPath.value,{}).then(res=>{
console.log(JSON.parse(res.data))
}).catch(err=>{
console.log(err)
})
},1000)
}
// else{
// console.log('取消发送')
@@ -333,6 +422,10 @@ const handleMove = (obj) => {
line-height: 36rpx;
text-align: center;
margin-bottom: 15rpx;
.color-blue{
color: #06f;
display: inline;
}
}
.talking-item{
margin-bottom: 46rpx;
@@ -349,6 +442,15 @@ const handleMove = (obj) => {
line-height: 92rpx;
color: #000;
font-weight: 400;
position: relative;
.talk-btn-icon{
width: 40rpx;
height: 40rpx;
position: absolute;
right: 40rpx;
top: 50%;
margin-top: -20rpx
}
}
}
.voice-list{
@@ -528,4 +630,5 @@ const handleMove = (obj) => {
background-color: #06f;
}
}
</style>
+26
View File
@@ -40,6 +40,7 @@
</template>
<script setup>
<<<<<<< HEAD
import {
reactive,
ref,
@@ -138,6 +139,31 @@
userInfo.value = getUserInfo()
console.log(userInfo.value);
});
=======
import { reactive, ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import common from '@/common/common';
const index = ref(0);
const form = ref({
gender: 'Y'
});
const sexOptions = [
{ value: 'Y', label: '男' },
{ value: 'M', label: '女' }
];
const cellCLick = (value) => {
sexOptions.length = 0;
console.log(sexOptions,'sisisis');
};
const sexChange = (e) => {
console.log(e);
console.log(sexOptions[e.detail.value].value, 'sexOptions[e.detail.value].value');
form.value.gender = sexOptions[e.detail.value].value;
console.log(form.value.gender, 'iiii');
};
onLoad((params) => {});
>>>>>>> cea58939be69e08335a0aaf3f8fef73c5fdb914a
</script>
<style lang="scss" scoped>
+92
View File
@@ -0,0 +1,92 @@
<template>
<view class="main_div max_page">
<view class="swiper-box">
<uv-swiper
height="100%"
indicator-active-color="#0066ff"
indicator-inactive-color="#D7E1F0"
:list="imgList"
indicator
indicatorMode="dot"
imgMode="scaleToFill"
:autoplay="false"
:current="indexNow"
@change="swiperChange"
@transition="swiperTransition"
@animationfinish="animationfinish"
></uv-swiper>
</view>
<view class="text-div">
<view class="title font_pf">{{ textList[indexNow].title }}</view>
<view class="tips font_pf">{{ textList[indexNow].tips }}</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import common from '@/common/common.js'
const imgList = ['/static/images/welcome/01.png', '/static/images/welcome/03.png', '/static/images/welcome/02.png'];
const textList = [
{ title: '学霸已就位', tips: '遇见AI学习搭子,难题随时有解!' },
{ title: '碎片时间逆袭', tips: '碎片时间全利用,定制课程随身学,成长不停歇!' },
{ title: '闯关式学习超上头', tips: '闯关升级,模拟实战,行业精英非你莫属!' }
];
const indexNow = ref(0);
const indexNowNow = ref(0)
const swiperChange = (index) => {
indexNow.value = index.current;
};
const animationfinish = () => {
indexNowNow.value = indexNow.value
}
const swiperTransition = (e) => {
if (e.dx > 30 && indexNowNow.value == 2) {
common.navigateBack()
}
}
</script>
<style scoped lang="scss">
.main_div {
background-color: #ffffff;
height: 100vh;
// background-color: red;
}
.swiper-box {
width: 100%;
height: 80%;
:deep(.uv-swiper) {
overflow: unset;
}
:deep(.uv-swiper__indicator) {
bottom: -40rpx;
z-index: 9999;
}
:deep(.uv-swiper-indicator__wrapper__dot) {
width: 15rpx;
height: 15rpx;
}
:deep(.uv-swiper-indicator__wrapper__dot--active) {
width: 54rpx;
height: 15rpx;
}
}
.text-div {
width: 100%;
height: 20%;
padding-top: 104rpx;
display: flex;
flex-direction: column;
align-items: center;
// justify-content: center;
.title {
font-weight: 600;
color: #000000;
font-size: 46rpx;
margin-bottom: 10rpx;
}
.tips {
font-size: 30rpx;
color: #999999;
}
}
</style>
+43 -34
View File
@@ -1,41 +1,50 @@
{
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"app-plus": {
"titleNView": false
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/courseDetail/index",
"style": {
"navigationBarTitleText": "课程详情",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/courseDetail/submit",
"style": {
"navigationBarTitleText": "提交评价",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/setting/index",
"style": {
"navigationBarTitleText": "设置",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/welcome/index",
"style": {
"navigationBarTitleText": "欢迎",
"app-plus": {
"titleNView": false
}
}
}
},
{
"path": "pages/courseDetail/index",
"style": {
"navigationBarTitleText": "课程详情",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/courseDetail/submit",
"style": {
"navigationBarTitleText": "提交评价",
"app-plus": {
"titleNView": false
}
}
},
{
"path": "pages/setting/index",
"style": {
"navigationBarTitleText": "设置",
"app-plus": {
"titleNView": false
}
}
}
],
"tabBar": {
"list": [{
Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

@@ -0,0 +1,11 @@
## 1.0.42023-10-13
1. 优化
## 1.0.32023-10-13
1. unmounted兼容vue3
## 1.0.22023-05-27
1. 不支持抖音小程序说明
## 1.0.12023-05-16
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
2. 优化部分功能
## 1.0.02023-05-10
uv-swipe-action 滑动单元格
@@ -0,0 +1,256 @@
/**
* 此为wxs模块,只支持APP-VUE,微信和QQ小程序以及H5平台
* wxs内部不支持es6语法,变量只能使用var定义,无法使用解构,箭头函数等特性
*/
// 开始触摸
function touchstart(event, ownerInstance) {
// 触发事件的组件的ComponentDescriptor实例
var instance = event.instance
// wxs内的局部变量快照,此快照是属于整个组件的,在touchstart和touchmove事件中都能获取到相同的结果
var state = instance.getState()
if (state.disable) return
var touches = event.touches
// 如果进行的是多指触控,不允许进行操作
if (touches && touches.length > 1) return
// 标识当前为滑动中状态
state.moving = true
// 记录触摸开始点的坐标值
state.startX = touches[0].pageX
state.startY = touches[0].pageY
}
// 触摸滑动
function touchmove(event, ownerInstance) {
// 触发事件的组件的ComponentDescriptor实例
var instance = event.instance
// wxs内的局部变量快照
var state = instance.getState()
if (state.disabled || !state.moving) return
var touches = event.touches
var pageX = touches[0].pageX
var pageY = touches[0].pageY
var moveX = pageX - state.startX
var moveY = pageY - state.startY
var buttonsWidth = state.buttonsWidth
// 移动的X轴距离大于Y轴距离,也即终点与起点位置连线,与X轴夹角小于45度时,禁止页面滚动
if (Math.abs(moveX) > Math.abs(moveY) || Math.abs(moveX) > state.threshold) {
event.preventDefault()
event.stopPropagation()
}
// 如果移动的X轴距离小于Y轴距离,也即终点位置与起点位置连线,与Y轴夹角小于45度时,认为是页面上下滑动,而不是左右滑动单元格
if (Math.abs(moveX) < Math.abs(moveY)) return
// 限制右滑的距离,不允许内容部分往右偏移,右滑会导致X轴偏移值大于0,以此做判断
// 此处不能直接return,因为滑动过程中会缺失某些关键点坐标,会导致错乱,最好的办法就是
// 在超出后,设置为0
if (state.status === 'open') {
// 在开启状态下,向左滑动,需忽略
if (moveX < 0) moveX = 0
// 想要收起菜单,最大能移动的距离为按钮的总宽度
if (moveX > buttonsWidth) moveX = buttonsWidth
// 如果是已经打开了的状态,向左滑动时,移动收起菜单
moveSwipeAction(-buttonsWidth + moveX, instance, ownerInstance)
} else {
// 关闭状态下,右滑动需忽略
if (moveX > 0) moveX = 0
// 滑动的距离不允许超过所有按钮的总宽度,此时只能是左滑,最终设置按钮的总宽度,同时为负数
if (Math.abs(moveX) > buttonsWidth) moveX = -buttonsWidth
// 只要是在滑过程中,就不断移动菜单的内容部分,从而使隐藏的菜单显示出来
moveSwipeAction(moveX, instance, ownerInstance)
}
}
// 触摸结束
function touchend(event, ownerInstance) {
// 触发事件的组件的ComponentDescriptor实例
var instance = event.instance
// wxs内的局部变量快照
var state = instance.getState()
if (!state.moving || state.disabled) return
var touches = event.changedTouches ? event.changedTouches[0] : {}
var pageX = touches.pageX
var pageY = touches.pageY
var moveX = pageX - state.startX
if (state.status === 'open') {
// 在展开的状态下,继续左滑,无需操作
if (moveX < 0) return
// 在开启状态下,点击一下内容区域,moveX为0,也即没有进行移动,这时执行收起菜单逻辑
if (moveX === 0) {
return closeSwipeAction(instance, ownerInstance)
}
// 在开启状态下,滑动距离小于阈值,则默认为不关闭,同时恢复原来的打开状态
if (Math.abs(moveX) < state.threshold) {
openSwipeAction(instance, ownerInstance)
} else {
// 如果滑动距离大于阈值,则执行收起逻辑
closeSwipeAction(instance, ownerInstance)
}
} else {
// 在关闭的状态下,右滑,无需操作
if (moveX > 0) return
// 理由同上
if (Math.abs(moveX) < state.threshold) {
closeSwipeAction(instance, ownerInstance)
} else {
openSwipeAction(instance, ownerInstance)
}
}
}
// 获取过渡时间
function getDuration(value) {
if (value.toString().indexOf('s') >= 0) return value
return value > 30 ? value + 'ms' : value + 's'
}
// 滑动结束时判断滑动的方向
function getMoveDirection(instance, ownerInstance) {
var state = instance.getState()
}
// 移动滑动选择器内容区域,同时显示出其隐藏的菜单
function moveSwipeAction(moveX, instance, ownerInstance) {
var state = instance.getState()
// 获取所有按钮的实例,需要通过它去设置按钮的位移
var buttons = ownerInstance.selectAllComponents('.uv-swipe-action-item__right__button')
var len = buttons.length
var previewButtonsMoveX = 0
// 设置菜单内容部分的偏移
instance.requestAnimationFrame(function() {
instance.setStyle({
// 设置translateX的值
'transition': 'none',
transform: 'translateX(' + moveX + 'px)',
'-webkit-transform': 'translateX(' + moveX + 'px)'
})
// 折叠按钮动画
for (var i = len - 1; i >= 0; i--) {
// 通过比例,得出元素自身该移动的距离
var translateX = state.buttons[i].width / state.buttonsWidth * moveX
// 最终移动的距离,是通过自身比例算出的距离,再加上在它之前所有按钮移动的距离之和
var realTranslateX = translateX + previewButtonsMoveX
buttons[i].setStyle({
// 在移动期间,不能使用过渡效果,否则会造成卡顿,本质原因是每次移动一点,就要花一定时间去过渡这个过程
'transition': 'none',
'transform': 'translateX(' + realTranslateX + 'px)',
'-webkit-transform': 'translateX(' + realTranslateX + 'px)'
})
// 记录本按钮之前的所有按钮的移动距离之和
previewButtonsMoveX += translateX
}
})
}
// 一次性展开滑动菜单
function openSwipeAction(instance, ownerInstance) {
var state = instance.getState()
// 获取所有按钮的实例,需要通过它去设置按钮的位移
var buttons = ownerInstance.selectAllComponents('.uv-swipe-action-item__right__button')
var len = buttons.length
// 处理duration单位问题
const duration = getDuration(state.duration)
// 展开过程中,是向左移动,所以X的偏移应该为负值
var buttonsWidth = -state.buttonsWidth
var previewButtonsMoveX = 0
instance.requestAnimationFrame(function() {
// 设置菜单主体内容
instance.setStyle({
'transition': 'transform ' + duration,
'transform': 'translateX(' + buttonsWidth + 'px)',
'-webkit-transform': 'translateX(' + buttonsWidth + 'px)',
})
// 设置各个隐藏的按钮为展开的状态
for (var i = len - 1; i >= 0; i--) {
// 通过比例,得出元素自身该移动的距离
var translateX = state.buttons[i].width / state.buttonsWidth * buttonsWidth
// 最终移动的距离,是通过自身比例算出的距离,再加上在它之前所有按钮移动的距离之和
var realTranslateX = translateX + previewButtonsMoveX
buttons[i].setStyle({
// 在移动期间,需要加上动画效果
'transition': 'transform ' + duration,
'transform': 'translateX(' + realTranslateX + 'px)',
'-webkit-transform': 'translateX(' + realTranslateX + 'px)'
})
// 记录本按钮之前的所有按钮的移动距离之和
previewButtonsMoveX += translateX
}
})
setStatus('open', instance)
}
// 标记菜单的当前状态,open-已经打开,close-已经关闭
function setStatus(status, instance) {
var state = instance.getState()
state.status = status
}
// 一次性收起滑动菜单
function closeSwipeAction(instance, ownerInstance) {
var state = instance.getState()
// 获取所有按钮的实例,需要通过它去设置按钮的位移
var buttons = ownerInstance.selectAllComponents('.uv-swipe-action-item__right__button')
var len = buttons.length
// 处理duration单位问题
const duration = getDuration(state.duration)
instance.requestAnimationFrame(function() {
// 设置菜单主体内容
instance.setStyle({
'transition': 'transform ' + duration,
'transform': 'translateX(0px)',
'-webkit-transform': 'translateX(0px)'
})
// 设置各个隐藏的按钮为收起的状态
for (var i = len - 1; i >= 0; i--) {
buttons[i].setStyle({
'transition': 'transform ' + duration,
'transform': 'translateX(0px)',
'-webkit-transform': 'translateX(0px)'
})
}
})
setStatus('close', instance)
}
// show的状态发生变化
function showChange(newValue, oldValue, ownerInstance, instance) {
var state = instance.getState()
if (state.disabled) return
// 打开或关闭单元格
if (newValue) {
openSwipeAction(instance, ownerInstance)
} else {
closeSwipeAction(instance, ownerInstance)
}
}
// 菜单尺寸发生变化
function sizeChange(newValue, oldValue, ownerInstance, instance) {
// wxs内的局部变量快照
var state = instance.getState()
state.disabled = newValue.disabled
state.duration = newValue.duration
state.show = newValue.show
state.threshold = newValue.threshold
state.buttons = newValue.buttons
var len = state.buttons.length
if (len) {
var buttonsWidth = 0
var buttons = newValue.buttons
for (var i = 0; i < len; i++) {
buttonsWidth += buttons[i].width
}
}
state.buttonsWidth = buttonsWidth
}
module.exports = {
touchstart: touchstart,
touchmove: touchmove,
touchend: touchend,
sizeChange: sizeChange
}
@@ -0,0 +1,225 @@
/**
* 此为wxs模块,只支持APP-VUE,微信和QQ小程序以及H5平台
* wxs内部不支持es6语法,变量只能使用var定义,无法使用解构,箭头函数等特性
*/
// 开始触摸
function touchstart(event, ownerInstance) {
// 触发事件的组件的ComponentDescriptor实例
var instance = event.instance
// wxs内的局部变量快照,此快照是属于整个组件的,在touchstart和touchmove事件中都能获取到相同的结果
var state = instance.getState()
if (state.disabled) return
var touches = event.touches
// 如果进行的是多指触控,不允许进行操作
if (touches && touches.length > 1) return
// 标识当前为滑动中状态
state.moving = true
// 记录触摸开始点的坐标值
state.startX = touches[0].pageX
state.startY = touches[0].pageY
ownerInstance.callMethod('closeOther')
}
// 触摸滑动
function touchmove(event, ownerInstance) {
// 触发事件的组件的ComponentDescriptor实例
var instance = event.instance
// wxs内的局部变量快照
var state = instance.getState()
if (state.disabled || !state.moving) return
var touches = event.touches
var pageX = touches[0].pageX
var pageY = touches[0].pageY
var moveX = pageX - state.startX
var moveY = pageY - state.startY
var buttonsWidth = state.buttonsWidth
// 移动的X轴距离大于Y轴距离,也即终点与起点位置连线,与X轴夹角小于45度时,禁止页面滚动
if (Math.abs(moveX) > Math.abs(moveY) || Math.abs(moveX) > state.threshold) {
event.preventDefault && event.preventDefault()
event.stopPropagation && event.stopPropagation()
}
// 如果移动的X轴距离小于Y轴距离,也即终点位置与起点位置连线,与Y轴夹角小于45度时,认为是页面上下滑动,而不是左右滑动单元格
if (Math.abs(moveX) < Math.abs(moveY)) return
// 限制右滑的距离,不允许内容部分往右偏移,右滑会导致X轴偏移值大于0,以此做判断
// 此处不能直接return,因为滑动过程中会缺失某些关键点坐标,会导致错乱,最好的办法就是
// 在超出后,设置为0
if (state.status === 'open') {
// 在开启状态下,向左滑动,需忽略
if (moveX < 0) moveX = 0
// 想要收起菜单,最大能移动的距离为按钮的总宽度
if (moveX > buttonsWidth) moveX = buttonsWidth
// 如果是已经打开了的状态,向左滑动时,移动收起菜单
moveSwipeAction(-buttonsWidth + moveX, instance, ownerInstance)
} else {
// 关闭状态下,右滑动需忽略
if (moveX > 0) moveX = 0
// 滑动的距离不允许超过所有按钮的总宽度,此时只能是左滑,最终设置按钮的总宽度,同时为负数
if (Math.abs(moveX) > buttonsWidth) moveX = -buttonsWidth
// 只要是在滑过程中,就不断移动单元格内容部分,从而使隐藏的菜单显示出来
moveSwipeAction(moveX, instance, ownerInstance)
}
}
// 触摸结束
function touchend(event, ownerInstance) {
// 触发事件的组件的ComponentDescriptor实例
var instance = event.instance
// wxs内的局部变量快照
var state = instance.getState()
if (!state.moving || state.disabled) return
var touches = event.changedTouches ? event.changedTouches[0] : {}
var pageX = touches.pageX
var pageY = touches.pageY
var moveX = pageX - state.startX
if (state.status === 'open') {
// 在展开的状态下,继续左滑,无需操作
if (moveX < 0) return
// 在开启状态下,点击一下内容区域,moveX为0,也即没有进行移动,这时执行收起菜单逻辑
if (moveX === 0) {
return closeSwipeAction(instance, ownerInstance)
}
// 在开启状态下,滑动距离小于阈值,则默认为不关闭,同时恢复原来的打开状态
if (Math.abs(moveX) < state.threshold) {
openSwipeAction(instance, ownerInstance)
} else {
// 如果滑动距离大于阈值,则执行收起逻辑
closeSwipeAction(instance, ownerInstance)
}
} else {
// 在关闭的状态下,右滑,无需操作
if (moveX > 0) return
// 理由同上
if (Math.abs(moveX) < state.threshold) {
closeSwipeAction(instance, ownerInstance)
} else {
openSwipeAction(instance, ownerInstance)
}
}
}
// 获取过渡时间
function getDuration(value) {
if (value.toString().indexOf('s') >= 0) return value
return value > 30 ? value + 'ms' : value + 's'
}
// 滑动结束时判断滑动的方向
function getMoveDirection(instance, ownerInstance) {
var state = instance.getState()
}
// 移动滑动选择器内容区域,同时显示出其隐藏的菜单
function moveSwipeAction(moveX, instance, ownerInstance) {
var state = instance.getState()
// 获取所有按钮的实例,需要通过它去设置按钮的位移
var buttons = ownerInstance.selectAllComponents('.uv-swipe-action-item__right__button')
// 设置菜单内容部分的偏移
instance.requestAnimationFrame(function() {
instance.setStyle({
// 设置translateX的值
'transition': 'none',
transform: 'translateX(' + moveX + 'px)',
'-webkit-transform': 'translateX(' + moveX + 'px)'
})
})
}
// 一次性展开滑动菜单
function openSwipeAction(instance, ownerInstance) {
var state = instance.getState()
// 获取所有按钮的实例,需要通过它去设置按钮的位移
var buttons = ownerInstance.selectAllComponents('.uv-swipe-action-item__right__button')
// 处理duration单位问题
var duration = getDuration(state.duration)
// 展开过程中,是向左移动,所以X的偏移应该为负值
var buttonsWidth = -state.buttonsWidth
instance.requestAnimationFrame(function() {
// 设置菜单主体内容
instance.setStyle({
'transition': 'transform ' + duration,
'transform': 'translateX(' + buttonsWidth + 'px)',
'-webkit-transform': 'translateX(' + buttonsWidth + 'px)',
})
})
setStatus('open', instance, ownerInstance)
}
// 标记菜单的当前状态,open-已经打开,close-已经关闭
function setStatus(status, instance, ownerInstance) {
var state = instance.getState()
state.status = status
ownerInstance.callMethod('setState', status)
}
// 一次性收起滑动菜单
function closeSwipeAction(instance, ownerInstance) {
var state = instance.getState()
// 获取所有按钮的实例,需要通过它去设置按钮的位移
var buttons = ownerInstance.selectAllComponents('.uv-swipe-action-item__right__button')
var len = buttons.length
// 处理duration单位问题
var duration = getDuration(state.duration)
instance.requestAnimationFrame(function() {
// 设置菜单主体内容
instance.setStyle({
'transition': 'transform ' + duration,
'transform': 'translateX(0px)',
'-webkit-transform': 'translateX(0px)'
})
// 设置各个隐藏的按钮为收起的状态
for (var i = len - 1; i >= 0; i--) {
buttons[i].setStyle({
'transition': 'transform ' + duration,
'transform': 'translateX(0px)',
'-webkit-transform': 'translateX(0px)'
})
}
})
setStatus('close', instance, ownerInstance)
}
// status的状态发生变化
function statusChange(newValue, oldValue, ownerInstance, instance) {
var state = instance.getState()
if (state.disabled) return
// 打开或关闭单元格
if (newValue === 'close' && state.status === 'open') {
closeSwipeAction(instance, ownerInstance)
} else if(newValue === 'open' && state.status === 'close') {
openSwipeAction(instance, ownerInstance)
}
}
// 菜单尺寸发生变化
function sizeChange(newValue, oldValue, ownerInstance, instance) {
// wxs内的局部变量快照
var state = instance.getState()
state.disabled = newValue.disabled
state.duration = newValue.duration
state.show = newValue.show
state.threshold = newValue.threshold
state.buttons = newValue.buttons
if (state.buttons) {
var len = state.buttons.length
var buttonsWidth = 0
var buttons = newValue.buttons
for (var i = 0; i < len; i++) {
buttonsWidth += buttons[i].width
}
}
state.buttonsWidth = buttonsWidth
}
module.exports = {
touchstart: touchstart,
touchmove: touchmove,
touchend: touchend,
sizeChange: sizeChange,
statusChange: statusChange
}
@@ -0,0 +1,264 @@
// nvue操作dom的库,用于获取dom的尺寸信息
const dom = uni.requireNativePlugin('dom')
// nvue中用于操作元素动画的库,类似于uni.animation,只不过uni.animation不能用于nvue
const animation = uni.requireNativePlugin('animation')
import { sleep } from '@/uni_modules/uv-ui-tools/libs/function/index.js'
export default {
data() {
return {
// 是否滑动中
moving: false,
// 状态,open-打开状态,close-关闭状态
status: 'close',
// 开始触摸点的X和Y轴坐标
startX: 0,
startY: 0,
// 所有隐藏按钮的尺寸信息数组
buttons: [],
// 所有按钮的总宽度
buttonsWidth: 0,
// 记录上一次移动的位置值
moveX: 0,
// 记录上一次滑动的位置,用于前后两次做对比,如果移动的距离小于某一阈值,则认为前后之间没有移动,为了解决可能存在的通信阻塞问题
lastX: 0
}
},
computed: {
// 获取过渡时间
getDuratin() {
let duration = String(this.duration)
// 如果ms为单位,返回ms的数值部分
if (duration.indexOf('ms') >= 0) return parseInt(duration)
// 如果s为单位,为了得到ms的数值,需要乘以1000
if (duration.indexOf('s') >= 0) return parseInt(duration) * 1000
// 如果值传了数值,且小于30,认为是s单位
duration = Number(duration)
return duration < 30 ? duration * 1000 : duration
}
},
watch: {
show: {
immediate: true,
handler(n) {
}
}
},
mounted() {
sleep(20).then(() => {
this.queryRect()
})
},
methods: {
close() {
this.closeSwipeAction()
},
// 触摸单元格
touchstart(event) {
if (this.disabled) return
this.closeOther()
const { touches } = event
// 记录触摸开始点的坐标值
this.startX = touches[0].pageX
this.startY = touches[0].pageY
},
// // 触摸滑动
touchmove(event) {
if (this.disabled) return
const { touches } = event
const { pageX } = touches[0]
const { pageY } = touches[0]
let moveX = pageX - this.startX
const moveY = pageY - this.startY
const { buttonsWidth } = this
const len = this.buttons.length
// 判断前后两次的移动距离,如果小于一定值,则不进行移动处理
if (Math.abs(pageX - this.lastX) < 0.3) return
this.lastX = pageX
// 移动的X轴距离大于Y轴距离,也即终点与起点位置连线,与X轴夹角小于45度时,禁止页面滚动
if (Math.abs(moveX) > Math.abs(moveY) || Math.abs(moveX) > this.threshold) {
event.stopPropagation()
}
// 如果移动的X轴距离小于Y轴距离,也即终点位置与起点位置连线,与Y轴夹角小于45度时,认为是页面上下滑动,而不是左右滑动单元格
if (Math.abs(moveX) < Math.abs(moveY)) return
// 限制右滑的距离,不允许内容部分往右偏移,右滑会导致X轴偏移值大于0,以此做判断
// 此处不能直接return,因为滑动过程中会缺失某些关键点坐标,会导致错乱,最好的办法就是
// 在超出后,设置为0
if (this.status === 'open') {
// 在开启状态下,向左滑动,需忽略
if (moveX < 0) moveX = 0
// 想要收起菜单,最大能移动的距离为按钮的总宽度
if (moveX > buttonsWidth) moveX = buttonsWidth
// 如果是已经打开了的状态,向左滑动时,移动收起菜单
this.moveSwipeAction(-buttonsWidth + moveX)
} else {
// 关闭状态下,右滑动需忽略
if (moveX > 0) moveX = 0
// 滑动的距离不允许超过所有按钮的总宽度,此时只能是左滑,最终设置按钮的总宽度,同时为负数
if (Math.abs(moveX) > buttonsWidth) moveX = -buttonsWidth
// 只要是在滑过程中,就不断移动菜单的内容部分,从而使隐藏的菜单显示出来
this.moveSwipeAction(moveX)
}
},
// 单元格结束触摸
touchend(event) {
if (this.disabled) return
const touches = event.changedTouches ? event.changedTouches[0] : {}
const { pageX } = touches
const { pageY } = touches
const { buttonsWidth } = this
this.moveX = pageX - this.startX
if (this.status === 'open') {
// 在展开的状态下,继续左滑,无需操作
if (this.moveX < 0) this.moveX = 0
if (this.moveX > buttonsWidth) this.moveX = buttonsWidth
// 在开启状态下,点击一下内容区域,moveX为0,也即没有进行移动,这时执行收起菜单逻辑
if (this.moveX === 0) {
return this.closeSwipeAction()
}
// 在开启状态下,滑动距离小于阈值,则默认为不关闭,同时恢复原来的打开状态
if (Math.abs(this.moveX) < this.threshold) {
this.openSwipeAction()
} else {
// 如果滑动距离大于阈值,则执行收起逻辑
this.closeSwipeAction()
}
} else {
// 在关闭的状态下,右滑,无需操作
if (this.moveX >= 0) this.moveX = 0
if (this.moveX <= -buttonsWidth) this.moveX = -buttonsWidth
// 理由同上
if (Math.abs(this.moveX) < this.threshold) {
this.closeSwipeAction()
} else {
this.openSwipeAction()
}
}
},
// 移动滑动选择器内容区域,同时显示出其隐藏的菜单
moveSwipeAction(moveX) {
if (this.moving) return
this.moving = true
let previewButtonsMoveX = 0
const len = this.buttons.length
animation.transition(this.$refs['uv-swipe-action-item__content'].ref, {
styles: {
transform: `translateX(${moveX}px)`
},
timingFunction: 'linear'
}, () => {
this.moving = false
})
// 按钮的组的长度
for (let i = len - 1; i >= 0; i--) {
const buttonRef = this.$refs[`uv-swipe-action-item__right__button-${i}`][0].ref
// 通过比例,得出元素自身该移动的距离
const translateX = this.buttons[i].width / this.buttonsWidth * moveX
// 最终移动的距离,是通过自身比例算出的距离,再加上在它之前所有按钮移动的距离之和
const realTranslateX = translateX + previewButtonsMoveX
animation.transition(buttonRef, {
styles: {
transform: `translateX(${realTranslateX}px)`
},
duration: 0,
delay: 0,
timingFunction: 'linear'
}, () => {})
// 记录本按钮之前的所有按钮的移动距离之和
previewButtonsMoveX += translateX
}
},
// 关闭菜单
closeSwipeAction() {
if (this.status === 'close') return
this.moving = true
const { buttonsWidth } = this
animation.transition(this.$refs['uv-swipe-action-item__content'].ref, {
styles: {
transform: 'translateX(0px)'
},
duration: this.getDuratin,
timingFunction: 'ease-in-out'
}, () => {
this.status = 'close'
this.moving = false
this.closeHandler()
})
// 按钮的组的长度
const len = this.buttons.length
for (let i = len - 1; i >= 0; i--) {
const buttonRef = this.$refs[`uv-swipe-action-item__right__button-${i}`][0].ref
// 如果不满足边界条件,返回
if (this.buttons.length === 0 || !this.buttons[i] || !this.buttons[i].width) return
animation.transition(buttonRef, {
styles: {
transform: 'translateX(0px)'
},
duration: this.getDuratin,
timingFunction: 'ease-in-out'
}, () => {})
}
},
// 打开菜单
openSwipeAction() {
if (this.status === 'open') return
this.moving = true
const buttonsWidth = -this.buttonsWidth
let previewButtonsMoveX = 0
animation.transition(this.$refs['uv-swipe-action-item__content'].ref, {
styles: {
transform: `translateX(${buttonsWidth}px)`
},
duration: this.getDuratin,
timingFunction: 'ease-in-out'
}, () => {
this.status = 'open'
this.moving = false
this.openHandler()
})
// 按钮的组的长度
const len = this.buttons.length
for (let i = len - 1; i >= 0; i--) {
const buttonRef = this.$refs[`uv-swipe-action-item__right__button-${i}`][0].ref
// 如果不满足边界条件,返回
if (this.buttons.length === 0 || !this.buttons[i] || !this.buttons[i].width) return
// 通过比例,得出元素自身该移动的距离
const translateX = this.buttons[i].width / this.buttonsWidth * buttonsWidth
// 最终移动的距离,是通过自身比例算出的距离,再加上在它之前所有按钮移动的距离之和
const realTranslateX = translateX + previewButtonsMoveX
animation.transition(buttonRef, {
styles: {
transform: `translateX(${realTranslateX}px)`
},
duration: this.getDuratin,
timingFunction: 'ease-in-out'
}, () => {})
previewButtonsMoveX += translateX
}
},
// 查询按钮节点信息
queryRect() {
// 历遍所有按钮数组,通过getRectByDom返回一个promise
const promiseAll = this.rightOptions.map((item, index) => this.getRectByDom(this.$refs[`uv-swipe-action-item__right__button-${index}`][0]))
// 通过promise.all方法,让所有按钮的查询结果返回一个数组的形式
Promise.all(promiseAll).then((sizes) => {
this.buttons = sizes
// 计算所有按钮总宽度
this.buttonsWidth = sizes.reduce((sum, cur) => sum + cur.width, 0)
})
},
// 通过nvue的dom模块,查询节点信息
getRectByDom(ref) {
return new Promise((resolve) => {
dom.getComponentRect(ref, (res) => {
resolve(res.size)
})
})
}
}
}
@@ -0,0 +1,182 @@
// nvue操作dom的库,用于获取dom的尺寸信息
const dom = uni.requireNativePlugin('dom');
const bindingX = uni.requireNativePlugin('bindingx');
const animation = uni.requireNativePlugin('animation');
import { getDuration, getPx } from '@/uni_modules/uv-ui-tools/libs/function/index.js'
export default {
data() {
return {
// 所有按钮的总宽度
buttonsWidth: 0,
// 是否正在移动中
moving: false
}
},
computed: {
// 获取过渡时间
getDuratin() {
let duration = String(this.duration)
// 如果ms为单位,返回ms的数值部分
if (duration.indexOf('ms') >= 0) return parseInt(duration)
// 如果s为单位,为了得到ms的数值,需要乘以1000
if (duration.indexOf('s') >= 0) return parseInt(duration) * 1000
// 如果值传了数值,且小于30,认为是s单位
duration = Number(duration)
return duration < 30 ? duration * 1000 : duration
}
},
watch: {
show(n) {
if(n) {
this.moveCellByAnimation('open')
} else {
this.moveCellByAnimation('close')
}
}
},
mounted() {
setTimeout(()=>{
this.initialize()
},20)
},
methods: {
initialize() {
this.queryRect()
},
// 关闭单元格,用于打开一个,自动关闭其他单元格的场景
closeHandler() {
if(this.status === 'open') {
// 如果在打开状态下,进行点击的话,直接关闭单元格
return this.moveCellByAnimation('close') && this.unbindBindingX()
}
},
// 点击单元格
clickHandler() {
// 如果在移动中被点击,进行忽略
if(this.moving) return
// 尝试关闭其他打开的单元格
this.parent && this.parent.closeOther(this)
if(this.status === 'open') {
// 如果在打开状态下,进行点击的话,直接关闭单元格
return this.moveCellByAnimation('close') && this.unbindBindingX()
}
},
// 滑动单元格
onTouchstart(e) {
// 如果当前正在移动中,或者disabled状态,则返回
if(this.moving || this.disabled) {
return this.unbindBindingX()
}
if(this.status === 'open') {
// 如果在打开状态下,进行点击的话,直接关闭单元格
return this.moveCellByAnimation('close') && this.unbindBindingX()
}
// 特殊情况下,e可能不为一个对象
e?.stopPropagation && e.stopPropagation()
e?.preventDefault && e.preventDefault()
this.moving = true
// 获取元素ref
const content = this.getContentRef()
let expression = `min(max(${-this.buttonsWidth}, x), 0)`
// 尝试关闭其他打开的单元格
this.parent && this.parent.closeOther(this)
// 阿里为了KPI而开源的BindingX
this.panEvent = bindingX.bind({
anchor: content,
eventType: 'pan',
props: [{
element: content,
// 绑定width属性,设置其宽度值
property: 'transform.translateX',
expression
}]
}, (res) => {
this.moving = false
if (res.state === 'end' || res.state === 'exit') {
const deltaX = res.deltaX
if(deltaX <= -this.buttonsWidth || deltaX >= 0) {
// 如果触摸滑动的过程中,大于单元格的总宽度,或者大于0,意味着已经动过滑动达到了打开或者关闭的状态
// 这里直接进行状态的标记
this.$nextTick(() => {
this.status = deltaX <= -this.buttonsWidth ? 'open' : 'close'
})
} else if(Math.abs(deltaX) > getPx(this.threshold)) {
// 在移动大于阈值、并且小于总按钮宽度时,进行自动打开或者关闭
// 移动距离大于0时,意味着需要关闭状态
if(Math.abs(deltaX) < this.buttonsWidth) {
this.moveCellByAnimation(deltaX > 0 ? 'close' : 'open')
}
} else {
// 在小于阈值时,进行关闭操作(如果在打开状态下,将不会执行bindingX)
this.moveCellByAnimation('close')
}
}
})
},
// 释放bindingX
unbindBindingX() {
// 释放上一次的资源
if (this?.panEvent?.token != 0) {
bindingX.unbind({
token: this.panEvent?.token,
// pan为手势事件
eventType: 'pan'
})
}
},
// 查询按钮节点信息
queryRect() {
// 历遍所有按钮数组,通过getRectByDom返回一个promise
const promiseAll = this.options.map(async(item, index) => {
return await this.getRectByDom(this.$refs[`uv-swipe-action-item__right__button-${index}`][0])
})
// 通过promise.all方法,让所有按钮的查询结果返回一个数组的形式
Promise.all(promiseAll).then(sizes => {
this.buttons = sizes
// 计算所有按钮总宽度
this.buttonsWidth = sizes.reduce((sum, cur) => sum + cur.width, 0)
})
},
// 通过nvue的dom模块,查询节点信息
getRectByDom(ref) {
return new Promise(resolve => {
dom.getComponentRect(ref, res => {
resolve(res.size)
})
})
},
// 移动单元格到左边或者右边尽头
moveCellByAnimation(status = 'open') {
if(this.moving) return
// 标识当前状态
this.moveing = true
const content = this.getContentRef()
const x = status === 'open' ? -this.buttonsWidth : 0
animation.transition(content, {
styles: {
transform: `translateX(${x}px)`,
},
duration: getDuration(this.duration, false),
timingFunction: 'ease-in-out'
}, () => {
this.moving = false
this.status = status
this.unbindBindingX()
})
},
// 获取元素ref
getContentRef() {
return this.$refs['uv-swipe-action-item__content'].ref
}
},
// #ifdef VUE2
beforeDestroy() {
this.unbindBindingX()
},
// #endif
// #ifdef VUE3
unmounted() {
this.unbindBindingX()
}
// #endif
}
@@ -0,0 +1,40 @@
export default {
props: {
// 控制打开或者关闭
show: {
type: Boolean,
default: false
},
// 标识符,如果是v-for,可用index索引值
name: {
type: [String, Number],
default: ''
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 是否自动关闭其他swipe按钮组
autoClose: {
type: Boolean,
default: true
},
// 滑动距离阈值,只有大于此值,才被认为是要打开菜单
threshold: {
type: Number,
default: 20
},
// 右侧按钮内容
options: {
type: Array,
default: () => []
},
// 动画过渡时间,单位ms
duration: {
type: [String, Number],
default: 300
},
...uni.$uv?.props?.swipeActionItem
}
}
@@ -0,0 +1,200 @@
<template>
<view class="uv-swipe-action-item" ref="uv-swipe-action-item">
<view class="uv-swipe-action-item__right">
<slot name="button">
<view v-for="(item,index) in options" :key="index" class="uv-swipe-action-item__right__button"
:ref="`uv-swipe-action-item__right__button-${index}`" :style="[{
alignItems: item.style && item.style.borderRadius ? 'center' : 'stretch'
}]" @tap="buttonClickHandler(item, index)">
<view class="uv-swipe-action-item__right__button__wrapper" :style="[{
backgroundColor: item.style && item.style.backgroundColor ? item.style.backgroundColor : '#C7C6CD',
borderRadius: item.style && item.style.borderRadius ? item.style.borderRadius : '0',
padding: item.style && item.style.borderRadius ? '0' : '0 15px',
}, item.style]">
<uv-icon v-if="item.icon" :name="item.icon"
:color="item.style && item.style.color ? item.style.color : '#ffffff'"
:size="item.iconSize ? $uv.addUnit(item.iconSize) : item.style && item.style.fontSize ? $uv.getPx(item.style.fontSize) * 1.2 : 17"
:customStyle="{
marginRight: item.text ? '2px' : 0
}"></uv-icon>
<text v-if="item.text" class="uv-swipe-action-item__right__button__wrapper__text uv-line-1"
:style="[{
color: item.style && item.style.color ? item.style.color : '#ffffff',
fontSize: item.style && item.style.fontSize ? item.style.fontSize : '16px',
lineHeight: item.style && item.style.fontSize ? item.style.fontSize : '16px',
}]">{{ item.text }}</text>
</view>
</view>
</slot>
</view>
<!-- #ifndef APP-NVUE -->
<view class="uv-swipe-action-item__content" @touchstart="wxs.touchstart" @touchmove="wxs.touchmove"
@touchend="wxs.touchend" :status="status" :change:status="wxs.statusChange" :size="size"
:change:size="wxs.sizeChange">
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<view class="uv-swipe-action-item__content" ref="uv-swipe-action-item__content" @panstart="onTouchstart"
@tap="clickHandler">
<!-- #endif -->
<slot />
</view>
</view>
</template>
<!-- #ifdef APP-VUE || MP-WEIXIN || H5 || MP-QQ -->
<script src="./index.wxs" module="wxs" lang="wxs"></script>
<!-- #endif -->
<script>
import touch from '@/uni_modules/uv-ui-tools/libs/mixin/touch.js'
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
import props from './props.js';
// #ifdef APP-NVUE
import nvue from './nvue.js';
// #endif
// #ifdef APP-VUE || MP-WEIXIN || H5 || MP-QQ
import wxs from './wxs.js';
// #endif
/**
* SwipeActionItem 滑动单元格子组件
* @description 该组件一般用于左滑唤出操作菜单的场景,用的最多的是左滑删除操作
* @tutorial https://www.uvui.cn/components/swipeAction.html
* @property {Boolean} show 控制打开或者关闭(默认 false )
* @property {String | Number} index 标识符,如果是v-for,可用index索引
* @property {Boolean} disabled 是否禁用(默认 false
* @property {Boolean} autoClose 是否自动关闭其他swipe按钮组(默认 true )
* @property {Number} threshold 滑动距离阈值,只有大于此值,才被认为是要打开菜单(默认 30 )
* @property {Array} options 右侧按钮内容
* @property {String | Number} duration 动画过渡时间,单位ms(默认 350 )
* @event {Function(index)} open 组件打开时触发
* @event {Function(index)} close 组件关闭时触发
* @example <uv-swipe-action><uv-swipe-action-item :options="options1" ></uv-swipe-action-item></uv-swipe-action>
*/
export default {
name: 'uv-swipe-action-item',
emits: ['click'],
// #ifndef APP-NVUE
mixins: [mpMixin, mixin, props, touch],
// #endif
// #ifdef APP-NVUE
mixins: [mpMixin, mixin, props, nvue , touch],
// #endif
// #ifdef APP-VUE || MP-WEIXIN || H5 || MP-QQ
mixins: [mpMixin, mixin, props, touch, wxs],
// #endif
data() {
return {
// 按钮的尺寸信息
size: {},
// 父组件uv-swipe-action的参数
parentData: {
autoClose: true,
},
// 当前状态,open-打开,close-关闭
status: 'close',
}
},
watch: {
// 由于wxs无法直接读取外部的值,需要在外部值变化时,重新执行赋值逻辑
wxsInit(newValue, oldValue) {
this.queryRect()
}
},
computed: {
wxsInit() {
return [this.disabled, this.autoClose, this.threshold, this.options, this.duration]
}
},
mounted() {
// #ifdef MP-TOUTIAO
this.$uv.error('抖音小程序暂不支持wxs,故该组件暂不支持抖音小程序');
// #endif
this.init()
},
methods: {
init() {
// 初始化父组件数据
this.updateParentData()
// #ifndef APP-NVUE
this.$uv.sleep().then(() => {
this.queryRect()
})
// #endif
},
updateParentData() {
// 此方法在mixin中
this.getParentData('uv-swipe-action')
},
// #ifndef APP-NVUE
// 查询节点
queryRect() {
this.$uvGetRect('.uv-swipe-action-item__right__button', true).then(buttons => {
this.size = {
buttons,
show: this.show,
disabled: this.disabled,
threshold: this.threshold,
duration: this.duration
}
})
},
// #endif
// 按钮被点击
buttonClickHandler(item, index) {
this.$emit('click', {
index,
name: this.name
})
}
},
}
</script>
<style lang="scss" scoped>
$show-lines: 1;
@import '@/uni_modules/uv-ui-tools/libs/css/variable.scss';
@import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
.uv-swipe-action-item {
position: relative;
overflow: hidden;
/* #ifndef APP-NVUE || MP-WEIXIN */
touch-action: pan-y;
/* #endif */
&__content {
background-color: #FFFFFF;
z-index: 10;
}
&__right {
position: absolute;
top: 0;
bottom: 0;
right: 0;
@include flex;
&__button {
@include flex;
justify-content: center;
overflow: hidden;
align-items: center;
&__wrapper {
@include flex;
align-items: center;
justify-content: center;
padding: 0 15px;
&__text {
@include flex;
align-items: center;
color: #FFFFFF;
font-size: 15px;
text-align: center;
justify-content: center;
}
}
}
}
}
</style>
@@ -0,0 +1,15 @@
export default {
methods: {
// 关闭时执行
closeHandler() {
this.status = 'close'
},
setState(status) {
this.status = status
},
closeOther() {
// 尝试关闭其他打开的单元格
this.parent && this.parent.closeOther(this)
}
}
}
@@ -0,0 +1,10 @@
export default {
props: {
// 是否自动关闭其他swipe按钮组
autoClose: {
type: Boolean,
default: true
},
...uni.$uv?.props?.swipeAction
}
}
@@ -0,0 +1,65 @@
<template>
<view class="uv-swipe-action">
<slot></slot>
</view>
</template>
<script>
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
import props from './props.js';
/**
* SwipeAction 滑动单元格
* @description 该组件一般用于左滑唤出操作菜单的场景,用的最多的是左滑删除操作
* @tutorial https://www.uvui.cn/components/swipeAction.html
* @property {Boolean} autoClose 是否自动关闭其他swipe按钮组
* @event {Function(index)} click 点击组件时触发
* @example <uv-swipe-action><uv-swipe-action-item :rightOptions="options1" ></uv-swipe-action-item></uv-swipe-action>
*/
export default {
name: 'uv-swipe-action',
mixins: [mpMixin, mixin, props],
data() {
return {}
},
provide() {
return {
swipeAction: this
}
},
computed: {
// 这里computed的变量,都是子组件uv-swipe-action-item需要用到的,由于头条小程序的兼容性差异,子组件无法实时监听父组件参数的变化
// 所以需要手动通知子组件,这里返回一个parentData变量,供watch监听,在其中去通知每一个子组件重新从父组件(uv-swipe-action-item)
// 拉取父组件新的变化后的参数
parentData() {
return [this.autoClose]
}
},
watch: {
// 当父组件需要子组件需要共享的参数发生了变化,手动通知子组件
parentData() {
if (this.children.length) {
this.children.map(child => {
// 判断子组件(uv-swipe-action-item)如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
typeof(child.updateParentData) === 'function' && child.updateParentData()
})
}
},
},
created() {
this.children = []
},
methods: {
closeOther(child) {
if (this.autoClose) {
// 历遍所有的单元格,找出非当前操作中的单元格,进行关闭
this.children.map((item, index) => {
if (child !== item) {
item.closeHandler()
}
})
}
}
}
}
</script>
@@ -0,0 +1,88 @@
{
"id": "uv-swipe-action",
"displayName": "uv-swipe-action 滑动单元格 全面兼容小程序、nvue、vue2、vue3等多端",
"version": "1.0.4",
"description": "滑动单元格组件一般用于左滑唤出操作菜单的场景,用的最多的是左滑删除操作。",
"keywords": [
"uv-swipe-action",
"uvui",
"uv-ui",
"swipe-action",
"滑动单元格"
],
"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": [
"uv-ui-tools",
"uv-icon"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"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",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
## SwipeAction 滑动单元格
> **组件名:uv-swipe-action**
该组件一般用于左滑唤出操作菜单的场景,用的最多的是左滑删除操作。
### <a href="https://www.uvui.cn/components/swipeAction.html" target="_blank">查看文档</a>
### [完整示例项目下载 | 关注更多组件](https://ext.dcloud.net.cn/plugin?name=uv-ui)
#### 如使用过程中有任何问题,或者您对uv-ui有一些好的建议,欢迎加入 uv-ui 交流群:<a href="https://ext.dcloud.net.cn/plugin?id=12287" target="_blank">uv-ui</a>、<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>
+16
View File
@@ -0,0 +1,16 @@
## 1.0.62023-12-20
1. 优化
## 1.0.52023-08-24
1. 修复标题文字过多未隐藏掉的BUG
## 1.0.42023-07-24
1. 增加 滑动方向是否为纵向 属性vertical
## 1.0.32023-06-27
1. 增加titleStyle属性,方便修改标题样式
2. 标题上去掉是否是图片的判断,避免无后缀的图片不显示
## 1.0.22023-06-01
1. 修复点击触发两次事件的BUG
## 1.0.12023-05-16
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
2. 优化部分功能
## 1.0.02023-05-10
uv-swiper 轮播图,走马灯
@@ -0,0 +1,30 @@
export default {
props: {
// 轮播的长度
length: {
type: [String, Number],
default: 0
},
// 当前处于活动状态的轮播的索引
current: {
type: [String, Number],
default: 0
},
// 指示器非激活颜色
indicatorActiveColor: {
type: String,
default: ''
},
// 指示器的激活颜色
indicatorInactiveColor: {
type: String,
default: ''
},
// 指示器模式,line-线型,dot-点型
indicatorMode: {
type: String,
default: ''
},
...uni.$uv?.props?.swiperIndicator
}
}
@@ -0,0 +1,112 @@
<template>
<view class="uv-swiper-indicator">
<view
class="uv-swiper-indicator__wrapper"
v-if="indicatorMode === 'line'"
:class="[`uv-swiper-indicator__wrapper--${indicatorMode}`]"
:style="{
width: $uv.addUnit(lineWidth * length),
backgroundColor: indicatorInactiveColor
}"
>
<view
class="uv-swiper-indicator__wrapper--line__bar"
:style="[lineStyle]"
></view>
</view>
<view
class="uv-swiper-indicator__wrapper"
v-if="indicatorMode === 'dot'"
>
<view
class="uv-swiper-indicator__wrapper__dot"
v-for="(item, index) in length"
:key="index"
:class="[index === current && 'uv-swiper-indicator__wrapper__dot--active']"
:style="[dotStyle(index)]"
>
</view>
</view>
</view>
</template>
<script>
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
import props from './props.js';
/**
* SwiperIndicator 轮播图指示器
* @description 该组件一般用于导航轮播,广告展示等场景,可开箱即用,
* @tutorial https://www.uvui.cn/components/swiper.html
* @property {String | Number} length 轮播的长度(默认 0
* @property {String | Number} current 当前处于活动状态的轮播的索引(默认 0 )
* @property {String} indicatorActiveColor 指示器非激活颜色
* @property {String} indicatorInactiveColor 指示器的激活颜色
* @property {String} indicatorMode 指示器模式(默认 'line'
* @example <uv-swiper :list="list4" indicator keyName="url" :autoplay="false"></uv-swiper>
*/
export default {
name: 'uv-swiper-indicator',
mixins: [mpMixin, mixin, props],
data() {
return {
lineWidth: 22
}
},
computed: {
// 指示器为线型的样式
lineStyle() {
let style = {}
style.width = this.$uv.addUnit(this.lineWidth)
style.transform = `translateX(${ this.$uv.addUnit(this.current * this.lineWidth) })`
style.backgroundColor = this.indicatorActiveColor
return style
},
// 指示器为点型的样式
dotStyle() {
return index => {
let style = {}
style.backgroundColor = index === this.current ? this.indicatorActiveColor : this.indicatorInactiveColor
return style
}
}
},
}
</script>
<style lang="scss" scoped>
@import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
.uv-swiper-indicator {
&__wrapper {
@include flex;
&--line {
border-radius: 100px;
height: 4px;
&__bar {
width: 22px;
height: 4px;
border-radius: 100px;
background-color: #FFFFFF;
transition: transform 0.3s;
}
}
&__dot {
width: 5px;
height: 5px;
border-radius: 100px;
margin: 0 4px;
&--active {
width: 12px;
}
}
}
}
</style>
@@ -0,0 +1,136 @@
export default {
props: {
// 列表数组,元素可为字符串,如为对象可通过keyName指定目标属性名
list: {
type: Array,
default: () => []
},
// 是否显示面板指示器
indicator: {
type: Boolean,
default: false
},
// 指示器非激活颜色
indicatorActiveColor: {
type: String,
default: '#fff'
},
// 指示器的激活颜色
indicatorInactiveColor: {
type: String,
default: 'rgba(255, 255, 255, 0.35)'
},
// 指示器样式,可通过bottomleftright进行定位
indicatorStyle: {
type: [String, Object],
default: ''
},
// 指示器模式,line-线型,dot-点型
indicatorMode: {
type: String,
default: 'line'
},
// 是否自动切换
autoplay: {
type: Boolean,
default: true
},
// 当前所在滑块的 index
current: {
type: [String, Number],
default: 0
},
// 当前所在滑块的 item-id ,不能与 current 被同时指定
currentItemId: {
type: String,
default: ''
},
// 滑块自动切换时间间隔
interval: {
type: [String, Number],
default: 3000
},
// 滑块切换过程所需时间
duration: {
type: [String, Number],
default: 300
},
// 播放到末尾后是否重新回到开头
circular: {
type: Boolean,
default: false
},
// 滑动方向是否为纵向
vertical: {
type: Boolean,
default: false
},
// 前边距,可用于露出前一项的一小部分,nvue和支付宝不支持
previousMargin: {
type: [String, Number],
default: 0
},
// 后边距,可用于露出后一项的一小部分,nvue和支付宝不支持
nextMargin: {
type: [String, Number],
default: 0
},
// 当开启时,会根据滑动速度,连续滑动多屏,支付宝不支持
acceleration: {
type: Boolean,
default: false
},
// 同时显示的滑块数量,nvue、支付宝小程序不支持
displayMultipleItems: {
type: Number,
default: 1
},
// 指定swiper切换缓动动画类型,有效值:default、linear、easeInCubic、easeOutCubic、easeInOutCubic
// 只对微信小程序有效
easingFunction: {
type: String,
default: 'default'
},
// list数组中指定对象的目标属性名
keyName: {
type: String,
default: 'url'
},
// 图片的裁剪模式
imgMode: {
type: String,
default: 'aspectFill'
},
// 组件高度
height: {
type: [String, Number],
default: 130
},
// 背景颜色
bgColor: {
type: String,
default: '#f3f4f6'
},
// 组件圆角,数值或带单位的字符串
radius: {
type: [String, Number],
default: 4
},
// 是否加载中
loading: {
type: Boolean,
default: false
},
// 是否显示标题,要求数组对象中有title属性
showTitle: {
type: Boolean,
default: false
},
// 显示的标题样式
titleStyle: {
type: [Object, String],
default: ''
},
...uni.$uv?.props?.swiper
}
}
@@ -0,0 +1,257 @@
<template>
<view
class="uv-swiper"
:style="{
backgroundColor: bgColor,
height: $uv.addUnit(height),
borderRadius: $uv.addUnit(radius)
}"
>
<view class="uv-swiper__loading" v-if="loading">
<uv-loading-icon mode="circle"></uv-loading-icon>
</view>
<swiper
v-else
class="uv-swiper__wrapper"
:style="{
height: $uv.addUnit(height),
flex: 1
}"
@change="change"
@transition="transition"
@animationfinish="animationfinish"
:circular="circular"
:vertical="vertical"
:interval="interval"
:duration="duration"
:autoplay="autoplay"
:current="current"
:currentItemId="currentItemId"
:previousMargin="$uv.addUnit(previousMargin)"
:nextMargin="$uv.addUnit(nextMargin)"
:acceleration="acceleration"
:displayMultipleItems="displayMultipleItems"
:easingFunction="easingFunction"
>
<swiper-item class="uv-swiper__wrapper__item" v-for="(item, index) in list" :key="index">
<view class="uv-swiper__wrapper__item__wrapper" :style="[itemStyle(index)]">
<!-- 在nvue中image图片的宽度默认为屏幕宽度需要通过flex:1撑开另外必须设置高度才能显示图片 -->
<image
class="uv-swiper__wrapper__item__wrapper__image"
v-if="getItemType(item) === 'image'"
:src="getSource(item)"
:mode="imgMode"
@tap="clickHandler(index)"
:style="{
height: $uv.addUnit(height),
borderRadius: $uv.addUnit(radius)
}"
></image>
<video
class="uv-swiper__wrapper__item__wrapper__video"
v-if="getItemType(item) === 'video'"
:id="`video-${index}`"
:enable-progress-gesture="false"
:src="getSource(item)"
:poster="getPoster(item)"
:title="showTitle && $uv.test.object(item) && item.title ? item.title : ''"
:style="{
height: $uv.addUnit(height)
}"
controls
@tap="clickHandler(index)"
></video>
<text v-if="showTitle && $uv.test.object(item) && item.title" class="uv-swiper__wrapper__item__wrapper__title uv-line-1" :style="[$uv.addStyle(titleStyle)]">
{{ item.title }}
</text>
</view>
</swiper-item>
</swiper>
<view class="uv-swiper__indicator" :style="[$uv.addStyle(indicatorStyle)]">
<slot name="indicator">
<uv-swiper-indicator
v-if="!loading && indicator && !showTitle"
:indicatorActiveColor="indicatorActiveColor"
:indicatorInactiveColor="indicatorInactiveColor"
:length="list.length"
:current="currentIndex"
:indicatorMode="indicatorMode"
></uv-swiper-indicator>
</slot>
</view>
</view>
</template>
<script>
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js';
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js';
import props from './props.js';
/**
* Swiper 轮播图
* @description 该组件一般用于导航轮播,广告展示等场景,可开箱即用,
* @tutorial https://www.uvui.cn/components/swiper.html
* @property {Array} list 轮播图数据
* @property {Boolean} indicator 是否显示面板指示器(默认 false )
* @property {String} indicatorActiveColor 指示器非激活颜色(默认 '#FFFFFF'
* @property {String} indicatorInactiveColor 指示器的激活颜色(默认 'rgba(255, 255, 255, 0.35)'
* @property {String | Object} indicatorStyle 指示器样式,可通过bottomleftright进行定位
* @property {String} indicatorMode 指示器模式(默认 'line'
* @property {Boolean} autoplay 是否自动切换(默认 true )
* @property {String | Number} current 当前所在滑块的 index(默认 0 )
* @property {String} currentItemId 当前所在滑块的 item-id ,不能与 current 被同时指定
* @property {String | Number} interval 滑块自动切换时间间隔(ms)(默认 3000 )
* @property {String | Number} duration 滑块切换过程所需时间(ms)(默认 300 )
* @property {Boolean} circular 播放到末尾后是否重新回到开头(默认 false )
* @property {String | Number} previousMargin 前边距,可用于露出前一项的一小部分,nvue和支付宝不支持(默认 0 )
* @property {String | Number} nextMargin 后边距,可用于露出后一项的一小部分,nvue和支付宝不支持(默认 0 )
* @property {Boolean} acceleration 当开启时,会根据滑动速度,连续滑动多屏,支付宝不支持(默认 false )
* @property {Number} displayMultipleItems 同时显示的滑块数量,nvue、支付宝小程序不支持(默认 1 )
* @property {String} easingFunction 指定swiper切换缓动动画类型, 只对微信小程序有效(默认 'default'
* @property {String} keyName list数组中指定对象的目标属性名(默认 'url'
* @property {String} imgMode 图片的裁剪模式(默认 'aspectFill'
* @property {String | Number} height 组件高度(默认 130
* @property {String} bgColor 背景颜色(默认 '#f3f4f6'
* @property {String | Number} radius 组件圆角,数值或带单位的字符串(默认 4 )
* @property {Boolean} loading 是否加载中(默认 false
* @property {Boolean} showTitle 是否显示标题,要求数组对象中有title属性(默认 false
* @event {Function(index)} click 点击轮播图时触发 index:点击了第几张图片,从0开始
* @event {Function(index)} change 轮播图切换时触发(自动或者手动切换) index:切换到了第几张图片,从0开始
* @example <uv-swiper :list="list4" keyName="url" :autoplay="false"></uv-swiper>
*/
export default {
name: 'uv-swiper',
mixins: [mpMixin, mixin, props],
emits: ['click', 'change'],
data() {
return {
currentIndex: 0
};
},
watch: {
current(val, preVal) {
if (val === preVal) return;
this.currentIndex = val; // 和上游数据关联上
}
},
computed: {
itemStyle() {
return (index) => {
const style = {};
// #ifndef APP-NVUE || MP-TOUTIAO
// 左右流出空间的写法不支持nvue和头条
// 只有配置了此二值,才加上对应的圆角,以及缩放
if (this.nextMargin && this.previousMargin) {
style.borderRadius = this.$uv.addUnit(this.radius);
if (index !== this.currentIndex) style.transform = 'scale(0.92)';
}
// #endif
return style;
};
}
},
methods: {
getItemType(item) {
if (typeof item === 'string') return this.$uv.test.video(this.getSource(item)) ? 'video' : 'image';
if (typeof item === 'object' && this.keyName) {
if (!item.type) return this.$uv.test.video(this.getSource(item)) ? 'video' : 'image';
if (item.type === 'image') return 'image';
if (item.type === 'video') return 'video';
return 'image';
}
},
// 获取目标路径,可能数组中为字符串,对象的形式,额外可指定对象的目标属性名keyName
getSource(item) {
if (typeof item === 'string') return item;
if (typeof item === 'object' && this.keyName) return item[this.keyName];
else this.$uv.error('请按格式传递列表参数');
return '';
},
// 轮播切换事件
change(e) {
// 当前的激活索引
const { current } = e.detail;
this.pauseVideo(this.currentIndex);
this.currentIndex = current;
this.$emit('change', e.detail);
},
transition(e) {
this.$emit('transition', e.detail);
},
animationfinish(e) {
this.$emit('animationfinish', e.detail);
},
// 切换轮播时,暂停视频播放
pauseVideo(index) {
const lastItem = this.getSource(this.list[index]);
if (this.$uv.test.video(lastItem)) {
// 当视频隐藏时,暂停播放
const video = uni.createVideoContext(`video-${index}`, this);
video.pause();
}
},
// 当一个轮播item为视频时,获取它的视频海报
getPoster(item) {
return typeof item === 'object' && item.poster ? item.poster : '';
},
// 点击某个item
clickHandler(index) {
this.$emit('click', index);
}
}
};
</script>
<style lang="scss" scoped>
$show-lines: 1;
@import '@/uni_modules/uv-ui-tools/libs/css/variable.scss';
@import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
.uv-swiper {
@include flex;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
&__wrapper {
flex: 1;
&__item {
flex: 1;
&__wrapper {
@include flex;
position: relative;
overflow: hidden;
transition: transform 0.3s;
flex: 1;
&__image {
flex: 1;
}
&__video {
flex: 1;
}
&__title {
position: absolute;
background-color: rgba(0, 0, 0, 0.3);
bottom: 0;
left: 0;
right: 0;
font-size: 28rpx;
height: 60rpx;
line-height: 60rpx;
color: #ffffff;
flex: 1;
}
}
}
}
&__indicator {
position: absolute;
bottom: 10px;
}
}
</style>
+88
View File
@@ -0,0 +1,88 @@
{
"id": "uv-swiper",
"displayName": "uv-swiper 轮播图 全面兼容vue3+2、app、h5、小程序等多端",
"version": "1.0.6",
"description": "该组件为轮播、跑马灯,支持卡片式。一般用于导航轮播,广告展示等场景,开箱即用",
"keywords": [
"swiper",
"uvui",
"uv-ui",
"轮播图",
"走马灯"
],
"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": [
"uv-ui-tools",
"uv-loading-icon"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"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",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}
+19
View File
@@ -0,0 +1,19 @@
## Swiper 轮播图
> **组件名:uv-swiper**
该组件为轮播、跑马灯,支持卡片式。一般用于导航轮播,广告展示等场景,开箱即用。
# <a href="https://www.uvui.cn/components/swiper.html" target="_blank">查看文档</a>
## [下载完整示例项目](https://ext.dcloud.net.cn/plugin?name=uv-ui) <small>(请不要 下载插件ZIP</small>
### [更多插件,请关注uv-ui组件库](https://ext.dcloud.net.cn/plugin?name=uv-ui)
<a href="https://ext.dcloud.net.cn/plugin?name=uv-ui" target="_blank">
![image](https://mp-a667b617-c5f1-4a2d-9a54-683a67cff588.cdn.bspapp.com/uv-ui/banner.png)
</a>
#### 如使用过程中有任何问题反馈,或者您对uv-ui有一些好的建议,欢迎加入uv-ui官方交流群:<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>