课程详情页面

This commit is contained in:
李泉德
2025-06-20 08:44:23 +08:00
16 changed files with 1184 additions and 79 deletions
+3 -1
View File
@@ -3,4 +3,6 @@ ENV = 'development'
# 'development'
# base api
VITE_APP_BASE_API_Url = 'http://25.18.122.66:9786'
# VITE_APP_BASE_API_Url = 'http://25.18.122.66:9786'
# 苗扬
VITE_APP_BASE_API_Url = 'http://25.64.16.133:9786'
+8 -6
View File
@@ -7,8 +7,6 @@ import {
export const uploadFile = (file, data) => {
const url = '/plimg/fileUpload/stuMgrUploadImg';
const base_url = get_base_url(url);
const token = getToken();
@@ -43,19 +41,23 @@ export const uploadFile = (file, data) => {
// 对象存储预览 /traoss/tras3/show/{文件ID}
export function previewFile(id,responseType = 'blob') {
export function previewFile(id,responseType = 'arraybuffer') {
const token = getToken();
const header = {};
const header = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
};
if (token)
header['summary'] = token
let baseUrl = get_base_url();
return new Promise((resolve, reject) => {
uni.request({
url: `/traoss/tras3/show/${id}`,
url: `${baseUrl}/traoss/tras3/show/${id}`,
header,
responseType,
success(result) {
resolve(result.data)
let _blob = new Blob([result.data],{type:result.header['content-type'].split(';')[0]})
resolve(_blob)
},
fail(error) {
reject(new Error('Upload failed'));
+57 -16
View File
@@ -1,27 +1,68 @@
import request from '@/api/request'
const base_url = '/traapp'
// 获取课程分类树接口
export const getCourseTypeListApi = (data) => {
return request({
url: '/app/traCrsCatalogInfo/courseQueryTraCrsCatalogInfoList',
method: 'post',
data
});
return request({
url: base_url + '/app/traCrsCatalogInfo/courseQueryTraCrsCatalogInfoList',
method: 'post',
data
});
};
// 获取生产线分类树接口
export const getPrdLineTypeListApi = (data) => {
return request({
url: '/app/traPrdLineInfo/courseQueryTraPrdLineInfoList',
method: 'post',
data
});
return request({
url: base_url + '/app/traPrdLineInfo/courseQueryTraPrdLineInfoList',
method: 'post',
data
});
};
// 获取标签分类树 接口
export const getTraTagListApi = (data) => {
return request({
url: '/app/traTagCatalogInfo/queryTraTagCatalogInfoList',
method: 'post',
data
});
};
return request({
url: base_url + '/app/traTagCatalogInfo/queryTraTagCatalogInfoList',
method: 'post',
data
});
};
// 全部模块
export const queryCrsInfoByCatalogApi = (data) => {
return request({
url: base_url + '/crsInfo/queryCrsInfoByCatalog',
method: 'post',
data
});
};
// 生产线模块-
export const queryCrsInfoByPrdLineApi = (data) => {
return request({
url: base_url + '/crsInfo/queryCrsInfoByPrdLine',
method: 'post',
data
});
};
// 最新
export const queryCrsInfoByLastnewApi = (data) => {
return request({
url: base_url + '/crsInfo/queryCrsInfoByLastnew',
method: 'post',
data
});
};
// 最热
export const queryCrsInfoByPopularApi = (data) => {
return request({
url: base_url + '/crsInfo/queryCrsInfoByPopular',
method: 'post',
data
});
};
// 推荐
export const queryCrsInfoByRecmdApi = (data) => {
return request({
url: base_url + '/crsInfo/queryCrsInfoByRecmd',
method: 'post',
data
});
};
+4 -4
View File
@@ -6,7 +6,7 @@ const setUserInfo = data => uni.setStorageSync('userInfo', data)
// 账号登录接口
export const useAccountLoginApp = (data) => request({
url: '/access/userLogin',
url: '/traapp/access/userLogin',
method: 'post',
data
}).then(async({ rtnCode, body }) => {
@@ -29,21 +29,21 @@ export const useAccountLoginApp = (data) => request({
// 获取个人信息
export const queryCurrentUser = (data) => request({
url: '/access/queryCurrentUser',
url: '/traapp/access/queryCurrentUser',
method: 'post',
data
})
//获取隐私协议
export const getYsxy = (data) => request({
url: '/dfAgtInfo/queryValidDfAgt',
url: '/traapp/dfAgtInfo/queryValidDfAgt',
method: 'post',
data
})
// 获取验证码(作废)
export const getLoginAppCheckCode = (data) => request({
url: '/access/checkCode',
url: '/traapp/access/checkCode',
method: 'get',
data
})
+8 -6
View File
@@ -8,10 +8,10 @@ const ENV = import.meta.env
export const get_base_url = url => {
if (process.env.NODE_ENV === 'development') {
// 开发环境
return ENV.VITE_APP_BASE_API_Url + "/traapp"
return ENV.VITE_APP_BASE_API_Url
} else if (process.env.NODE_ENV === 'production') {
// 生产环境
return ENV.VITE_APP_BASE_API_Url + "/traapp"
return ENV.VITE_APP_BASE_API_Url
}
}
@@ -21,17 +21,19 @@ export default function request({
data,
meta,
isStream,
suppressErrors
suppressErrors,
header={}
}) {
const base_url = get_base_url(url);
const header = {
const headers_base = {
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
...(header||{})
}
let token = getToken()
if (token) {
header['summary'] = token // 让每个请求携带令牌
headers_base['summary'] = token // 让每个请求携带令牌
}
let that = this
return new Promise((resolve, reject) => {
@@ -39,7 +41,7 @@ export default function request({
url: `${base_url}${url}`,
data: data,
method: method,
header: header,
header: headers_base,
success: (response) => {
if (isStream) { // 流式传输,直接返回
return resolve(response);
+112 -4
View File
@@ -1,20 +1,128 @@
<template>
<view>
<view class="course-item-box" @click="clickItem">
<view class="img-box-view">
<ImagePreview class="img-box" :imgId="itemInfo.imgId"></ImagePreview>
<view class="item-grade">{{itemInfo.evalGrade}}</view>
</view>
<view class="cont-box">
<view class="item-title">{{itemInfo.crsName}}</view>
<uv-scroll-list class="item-tags" :indicator="false">
<view class="item-tag-view">
<view class="item-tag" v-for="item in itemInfo.tagCataList">{{item.tagName}}</view>
</view>
</uv-scroll-list>
<view class="item-publish">发布{{itemInfo.issuTm}}</view>
<view class="item-num">已学{{itemInfo.accmStdyCnt}}人次</view>
</view>
</view>
</template>
<script>
<script >
import ImagePreview from '@/components/image-preview/image-preview.vue'
export default {
name:"course-item",
components:{
ImagePreview
},
props:{
itemInfo:{
type:Object,
default:()=>({
crsName: '银行核心业务全流程实践哒哒哒哒',
tagsList:[{tagName:'公斤业务'},{tagName:'初级'}],
publicTime:"2025-01-03",
learnedNum:'2222222'
})
}
},
data() {
return {
};
},
methods:{
clickItem(){
this.$emit('clickItem', this.itemInfo)
}
}
}
</script>
<style lang="scss">
.course-item-box{
padding: 15rpx;
overflow: hidden;
.img-box-view{
width: 170rpx;
height: 170rpx;
float: left;
position: relative;
border-radius: 20rpx;
overflow: hidden;
.img-box{
width: 170rpx;
height: 170rpx;
}
.item-grade{
position: absolute;
right: 0;
width: 60rpx;
height: 46rpx;
background-color: #ff581c;
top:20rpx;
border-radius: 8rpx 0 0 8rpx;
color:#fff;
text-align: center;
// padding-right: 10rpx;
font-size: 28rpx;
line-height: 46rpx;
}
}
.cont-box{
width: calc(100% - 170rpx);
box-sizing: border-box;
padding: 0 10rpx 0 20rpx;
float: left;
.item-title{
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 44rpx;
font-size: 29rpx;
font-weight: 500;
margin-bottom: 10rpx;
}
.item-tags{
padding-bottom: 0 !important;
.item-tag-view{
white-space: nowrap;
height: 34rpx;
.item-tag{
height: 32rpx;
line-height: 32rpx;
font-size: 20rpx;
background-color: #eee;
color: rgb(51, 51, 51);
padding: 0 20rpx;
border-radius: 16rpx;
margin-right: 20rpx;
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200rpx;
box-sizing: border-box;
}
}
}
.item-publish,
.item-num{
height: 32rpx;
line-height: 32rpx;
font-size: 20rpx;
color: rgb(102,102,102);
margin-top: 6rpx;
}
}
}
</style>
+13 -10
View File
@@ -1,27 +1,29 @@
<template>
<image :src="imgUrl"></image>
<image :src="imgUrlShow" fit="cover"></image>
<!-- <image class="img" src="@/static/images/test/1.png"></image> -->
</template>
<script>
import previewFile from'@/api/common.js'
import {previewFile} from'@/api/common.js'
export default {
name:"image-preview",
props:{
modelValue:{
imgId:{
default:'',
type:String
}
},
data() {
return {
imgUrl
imgUrlShow:'',
watching:false
};
},
watch: {
modelValue: {
imgId: {
handler(newval, oldval) {
if (newval) {
this.getPreviewUrlBlob(newval);
if (newval && this.watching) {
this.getPreviewUrlBlob(newval);
}
},
immediate: true,
@@ -39,9 +41,9 @@
},
methods: {
getPreviewUrlBlob(id) {
// this.imgUrlShow = '@/static/images/index/message_b.png'
previewFile(id).then((res) => {
this.imgUrlShow = URL.createObjectURL(res);
// const reader = new FileReader();
// reader.readAsDataURL(res)
// reader.onloadend = () => {
@@ -51,9 +53,10 @@
},
},
mounted() {
if (this.modelValue) {
this.getPreviewUrlBlob(this.modelValue);
if (this.imgId) {
this.getPreviewUrlBlob(this.imgId);
}
this.watching = true
},
beforeDestroy() {
URL.revokeObjectURL(this.imgUrlShow);
+15
View File
@@ -10,6 +10,7 @@
}
},
{
<<<<<<< HEAD
"path": "pages/courseDetail/index",
"style": {
"navigationBarTitleText": "课程详情",
@@ -19,6 +20,8 @@
}
},
{
=======
>>>>>>> f0015d5ea46c4904ce35c5ec4a03028d9ec6d5b4
"path": "pages/study/index",
"style": {
"navigationBarTitleText": "学习",
@@ -90,6 +93,18 @@
"titleNView": false
}
}
<<<<<<< HEAD
=======
},
{
"path": "pages/course/index",
"style": {
"navigationBarTitleText": "课程中心",
"app-plus": {
"titleNView": false
}
}
>>>>>>> f0015d5ea46c4904ce35c5ec4a03028d9ec6d5b4
}
],
"tabBar": {
+352 -31
View File
@@ -7,15 +7,15 @@
<uv-icon class="arrow-icon" name="arrow-left" @click="common.navigateBack()"/>
课程中心
</view>
<view class="input-view">
<uv-input shape="circle"
placeholderStyle='color:#999999;font-size:28rpx'
placeholder="请输入课程名"
suffixIcon="search"
:customStyle="customStyle"
:suffixIconStyle="suffixIconStyle"
:readonly="true">
</uv-input>
<view class="input-view" @click="toSearchPage">
<uv-input shape="circle"
placeholderStyle='color:#999999;font-size:28rpx'
placeholder="请输入课程名"
suffixIcon="search"
:customStyle="customStyle"
:suffixIconStyle="suffixIconStyle"
:readonly="true">
</uv-input>
</view>
<view class="tabs-view">
<uv-tabs :list="tabList" @click="changeTab" :scrollable="false"
@@ -24,17 +24,87 @@
></uv-tabs>
</view>
<view class="list-view">
<view class="filter-btn" @click="showFilterList = true">筛选
<uv-icon class="filter-icon" name="list"/>
</view>
<view :class="['filter-list',showFilterList?'show':'']">
<view class="filter-type" v-for="item in tagsTree.filter(t=>(t.children||[]).length>0)" :key="item.value">
<view class="filter-type-title">{{item.label}}</view>
<view class="filter-type-list">
<view :class="['filter-type-item', activeTag===itemT.value?'is-active':'']" v-for="itemT in (item.children||[])" @click="chooseTag(itemT)">
{{itemT.label}}
</view>
</view>
</view>
</view>
<uv-scroll-list class="tags-view" :indicator="false">
<uv-tags text="全部" shape="circle" plain class="tags-item" size="mini"></uv-tags>
<uv-tags text="全部" v-for="item in 15" shape="circle" plain type="info" class="tags-item" size="mini"></uv-tags>
<uv-tags text="全部"
shape="circle"
plain
class="tags-item"
size="mini"
:type="activeTag === ''?'primary':'info'"
@click="chooseTag({value:''})"
></uv-tags>
<uv-tags :text="item.label"
v-for="item in tagsList"
:key="item.value"
shape="circle"
plain
:type="activeTag === item.value?'primary':'info'"
@click="chooseTag(item)"
class="tags-item" size="mini"
></uv-tags>
</uv-scroll-list>
<view class="course-list">
<view class="left-type-list">
<view v-for="item in 6" :class="['type-item', item == 3 ?'is-active':'']">
课程分类
<view class="left-type-list" v-if="'全部' === activeTab">
<view
v-for="item in courseTypeList"
:key="item.value"
:class="['type-item',activeCourseType == item.value ?'is-active':'']"
@click="changeCourseType(item)"
>
{{item.label}}
</view>
</view>
<view class="left-type-list" v-if="'生产线'===activeTab">
<view
v-for="item in coursePrdlineList"
:key="item.value"
:class="['type-item', activePrdlineType == item.value ?'is-active':'']"
@click="changePrdLine(item)"
>
{{item.label}}
</view>
</view>
<view class="right-course-list">
<view
class="type-list"
v-for="item in courseListTreeShow"
:key="item.value"
>
<view
class="type-title"
v-if="item.courseList.length > 0 && showTypeName"
>
<!-- v-if="itemT.courseList.length>0 && " -->
<view class="left-color"></view>
{{item.label}}
</view>
<CourseItem v-for="itemC in item.courseList" :key="itemC.crsNum" :itemInfo="itemC" @clickItem="toCourseDetail(itemC)"/>
<view
class="type-list"
v-for="itemT in item.children"
:key="itemT.value"
>
<view class="type-title" v-if="itemT.courseList.length>0">
<view class="left-color"></view>
{{itemT.label}}
</view>
<CourseItem v-for="itemTC in itemT.courseList" :key="itemTC.crsNum" :itemInfo="itemTC" @clickItem="toCourseDetail(itemTC)"/>
</view>
</view>
</view>
<view class="right-course-list"></view>
</view>
</view>
</view>
@@ -42,13 +112,19 @@
</template>
<script setup>
import CourseItem from "@/components/course-item/course-item.vue"
import common from '@/common/common'
import {
getPrdLineTypeListApi,
getTraTagListApi,
getCourseTypeListApi
getCourseTypeListApi,
queryCrsInfoByCatalogApi,
queryCrsInfoByPrdLineApi,
queryCrsInfoByLastnewApi,
queryCrsInfoByPopularApi,
queryCrsInfoByRecmdApi
} from "@/api/course.js"
import { onMounted } from 'vue'
import { onMounted, ref, computed } from 'vue'
const customStyle = {
backgroundColor: "#ffffff",
paddingLeft: "40rpx",
@@ -63,24 +139,156 @@ import { onMounted } from 'vue'
}
const tabsCustomStyle ={
backgroundColor: "#ffffff",
marginTop: '23rpx',
borderRadius: '23rpx',
}
const tabList = [
{ name:'全部'},
{ name:'热门'},
{ name:'最新'},
{ name:'推荐'},
{ name:'生产线'},
]
const changeTab = (item) => {
console.log(item)
{ name:'全部'},
{ name:'热门'},
{ name:'最新'},
{ name:'推荐'},
{ name:'生产线'},
]
const activeTab = ref('全部')
const activeCourseType = ref('')
const activePrdlineType = ref('')
const activeTag = ref('')
const courseTypeList = ref([])
const coursePrdlineList = ref([])
const tagsTree = ref([])
const showFilterList = ref(false)
// 点击标签
const chooseTag = (item)=>{
activeTag.value = item.value
showFilterList.value = false
}
// 点击生产线分类
const changePrdLine = (item)=>{
activePrdlineType.value = item.value
}
// 点击课程分类
const changeCourseType = (item)=>{
activeCourseType.value = item.value
setTimeout(()=>{
console.log(courseListTreeShow.value)
})
}
const showTypeName = computed(()=>{
return (activeTab.value === '全部' && activeCourseType.value === '') || (activeTab.value === '生产线' && activePrdlineType.value === '')
})
const tagsList = computed(()=>{
let _arr = []
tagsTree.value.forEach(t=>{
let _tarr = t.children || []
_arr = _arr.concat(_tarr)
})
return _arr.sort((a,b)=>{
a.required == "Y" && b.required === 'N'
})
})
const courseListBase = ref([])
const courseList = computed(()=>{
return activeTag.value
?courseListBase.value.filter(t=>t.tagCataList.map(k=>k.tagId).indexOf(activeTag.value)>=0)
:courseListBase.value
})
// 整理树形
const courseListTree = computed(()=>{
let _arr = activeTab.value === '全部'? courseTypeList.value
: activeTab.value === '生产线'? coursePrdlineList.value: []
let _idKey = activeTab.value === '全部'? 'tagId': 'prdLineId'
let _tree = _arr.map(t=>{
let _children = t.children||[]
return {
label: t.label,
required: t.required,
value:t.value,
courseList: courseList.value.filter(k=>k[_idKey] === t.value),
children: _children.map(k=>{
return {
...k,
courseList: courseList.value.filter(m=>m[_idKey] === k.value),
}
})
}
})
return _tree
})
// 过滤树形
const courseListTreeShow = computed(()=>{
let _type = activeTab.value === '全部'? activeCourseType.value : activePrdlineType.value
if(activeTab.value === '全部'|| activeTab.value === '生产线'){
if(_type){
return courseListTree.value.filter(t=> t.value === _type)
}else{
return courseListTree.value
}
}else{
return [{
children: [],
courseList: courseList.value,
label: "",
value: "333"
}]
}
})
const changeTab = (item) => {
if(!item) return
activeTab.value = item.name
activeTag.value = ''
activeCourseType.value = ''
activePrdlineType.value = ''
switch (item.name){
case '全部':
queryCrsInfoByCatalogApi().then(res=>{
courseListBase.value = res.body || []
})
break;
case '热门':
queryCrsInfoByPopularApi().then(res=>{
courseListBase.value = res.body || []
})
break;
case '最新':
queryCrsInfoByLastnewApi().then(res=>{
courseListBase.value = res.body || []
})
break;
case '推荐':
queryCrsInfoByRecmdApi().then(res=>{
courseListBase.value = res.body || []
})
break;
case '生产线':
queryCrsInfoByPrdLineApi().then(res=>{
courseListBase.value = res.body || []
})
break;
}
}
// 跳转搜索页
const toSearchPage = () => {
common.navigateTo('/pages/search/search')
}
// 跳转 课程详情
const toCourseDetail = (item) => {
common.navigateTo('/pages/courseDetail/index?crsNum=' + item.crsNum)
}
onMounted(()=>{
// getPrdLineTypeListApi().then(res=>{
// console.log(res)
// })
getCourseTypeListApi().then(res=>{
courseTypeList.value = res.body||[]
})
getPrdLineTypeListApi().then(res=>{
coursePrdlineList.value = res.body||[]
})
getTraTagListApi().then(res=>{
tagsTree.value = res.body||[]
})
changeTab({name:'全部'})
})
</script>
@@ -102,6 +310,7 @@ import { onMounted } from 'vue'
}
.course-center-body{
position: absolute;
overflow: hidden;
left:0;
top: 0;
height: 100%;
@@ -120,6 +329,7 @@ import { onMounted } from 'vue'
font-weight: 500;
color:#000;
position: relative;
// margin-bottom: 23rpx;
.arrow-icon{
position: absolute;
left:36rpx;
@@ -139,15 +349,99 @@ import { onMounted } from 'vue'
}
.list-view{
flex:1;
overflow: hidden;
margin-bottom: 80rpx;
background: #ffffff;
border-radius: 23rpx;
display: flex;
flex-direction: column;
position: relative;
.filter-list{
position: absolute;
top: 84rpx;
left:0;
width: 100%;
height: 508rpx;
overflow-y: auto;
z-index: 2;
background-color: #f9f9f9;
box-shadow: 0rpx 3.85rpx 38rpx rgba(0,0,0,.05);
padding: 0rpx 25rpx;
box-sizing: border-box;
.filter-type{
padding: 10rpx 0;
box-sizing: border-box;
.filter-type-title{
font-size: 29rpx;
line-height: 40rpx;
margin-bottom: 15rpx;
color: #000;
}
.filter-type-list{
overflow: hidden;
.filter-type-item{
width: calc(25% - 18rpx);
font-size: 29rpx;
box-sizing: border-box;
padding:0 20rpx;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
text-align: center;
float: left;
margin-right: 24rpx;
height: 65rpx;
line-height: 65rpx;
background-color: #f1f3f8;
border-radius: 6rpx;
margin-bottom: 20rpx;
&:nth-child(4n){
margin-right: 0;
}
&.is-active{
color: #06f;
}
}
}
&+.filter-type{
margin-top: 20rpx;
}
}
}
.filter-list{
display: none;
&.show{
display: block;
}
}
.filter-btn{
position: absolute;
right: 0;
top: 0;
height: 83rpx;
width: 120rpx;
line-height: 83rpx;
font-size: 26rpx;
background: #fff;
z-index: 2;
text-align: center;
box-shadow: -4rpx 0 6rpx rgba(0,0,0,.05);
white-space: nowrap;
padding-right: 30rpx;
box-sizing: border-box;
.filter-icon{
// display: inline-block;
font-size: 26rpx;
position: absolute;
right: 15rpx;
top: 50%;
transform: translateY(-50%);
}
}
.tags-view{
height: 84rpx;
display: block;
padding: 20rpx;
padding: 20rpx 120rpx 20rpx 20rpx;
box-sizing: border-box;
white-space: nowrap;
border-bottom: 2rpx solid #eee;
@@ -158,16 +452,21 @@ import { onMounted } from 'vue'
padding: 0 20rpx;
.uv-tags__text{
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200rpx;
}
}
}
}
.course-list{
display: flex;
flex:1;
flex:1;
overflow: hidden;
.left-type-list{
width: 152rpx;
border-right: 2rpx solid #eee;
overflow-y: auto;
.type-item{
height: 90rpx;
line-height: 90rpx;
@@ -185,6 +484,28 @@ import { onMounted } from 'vue'
}
.right-course-list{
flex:1;
overflow-y: auto;
.type-list{
.type-title{
font-size: 33rpx;
line-height: 43rpx;
margin: 20rpx 15rpx 5rpx;
height: 43rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 600;
.left-color{
width: 8rpx;
height: 33rpx;
margin-top: 5rpx;
float: left;
overflow: hidden;
background-color: #06f;
margin-right: 8rpx;
}
}
}
}
}
}
+9 -1
View File
@@ -1,5 +1,13 @@
{
"pages": [
{
"path": "pages/course/index",
"style": {
"navigationBarTitleText": "课程中心",
"app-plus": {
"titleNView": false
}
}
}
]
}
@@ -0,0 +1,19 @@
## 1.0.82023-10-18
1. 修复在APP上不能正常显示的BUG
## 1.0.72023-10-12
1. 修复部分情况,修改某属性自动关闭的BUG
## 1.0.62023-07-24
1. 优化 nvue模式下增加cellChild参数,是否在list中cell节点下,nvue中cell下建议设置成true
## 1.0.52023-07-02
修改VUE3模式下可能存在的BUG
## 1.0.42023-07-02
uv-transition 动画组件,代码重构优化,性能更加友好,增加自定义动画功能。详情参考文档:https://www.uvui.cn/components/transition.html
## 1.0.32023-06-12
1. 恢复this.$nextTick的使用,经过测试百度等平台无问题
## 1.0.22023-05-23
1. 百度小程序等平台不支持this.$nextick,修改成延时
## 1.0.12023-05-16
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
2. 优化部分功能
## 1.0.02023-05-10
1. 新增动画组件
@@ -0,0 +1,131 @@
// const defaultOption = {
// duration: 300,
// timingFunction: 'linear',
// delay: 0,
// transformOrigin: '50% 50% 0'
// }
// #ifdef APP-NVUE
const nvueAnimation = uni.requireNativePlugin('animation')
// #endif
class MPAnimation {
constructor(options, _this) {
this.options = options
// 在iOS10+QQ小程序平台下,传给原生的对象一定是个普通对象而不是Proxy对象,否则会报parameter should be Object instead of ProxyObject的错误
this.animation = uni.createAnimation({
...options
})
this.currentStepAnimates = {}
this.next = 0
this.$ = _this
}
_nvuePushAnimates(type, args) {
let aniObj = this.currentStepAnimates[this.next]
let styles = {}
if (!aniObj) {
styles = {
styles: {},
config: {}
}
} else {
styles = aniObj
}
if (animateTypes1.includes(type)) {
if (!styles.styles.transform) {
styles.styles.transform = ''
}
let unit = ''
if(type === 'rotate'){
unit = 'deg'
}
styles.styles.transform += `${type}(${args+unit}) `
} else {
styles.styles[type] = `${args}`
}
this.currentStepAnimates[this.next] = styles
}
_animateRun(styles = {}, config = {}) {
let ref = this.$.$refs['ani'].ref
if (!ref) return
return new Promise((resolve, reject) => {
nvueAnimation.transition(ref, {
styles,
...config
}, res => {
resolve()
})
})
}
_nvueNextAnimate(animates, step = 0, fn) {
let obj = animates[step]
if (obj) {
let {
styles,
config
} = obj
this._animateRun(styles, config).then(() => {
step += 1
this._nvueNextAnimate(animates, step, fn)
})
} else {
this.currentStepAnimates = {}
typeof fn === 'function' && fn()
this.isEnd = true
}
}
step(config = {}) {
// #ifndef APP-NVUE
this.animation.step(config)
// #endif
// #ifdef APP-NVUE
this.currentStepAnimates[this.next].config = Object.assign({}, this.options, config)
this.currentStepAnimates[this.next].styles.transformOrigin = this.currentStepAnimates[this.next].config.transformOrigin
this.next++
// #endif
return this
}
run(fn) {
// #ifndef APP-NVUE
this.$.animationData = this.animation.export()
this.$.timer = setTimeout(() => {
typeof fn === 'function' && fn()
}, this.$.durationTime)
// #endif
// #ifdef APP-NVUE
this.isEnd = false
let ref = this.$.$refs['ani'] && this.$.$refs['ani'].ref
if(!ref) return
this._nvueNextAnimate(this.currentStepAnimates, 0, fn)
this.next = 0
// #endif
}
}
const animateTypes1 = ['matrix', 'matrix3d', 'rotate', 'rotate3d', 'rotateX', 'rotateY', 'rotateZ', 'scale', 'scale3d',
'scaleX', 'scaleY', 'scaleZ', 'skew', 'skewX', 'skewY', 'translate', 'translate3d', 'translateX', 'translateY',
'translateZ'
]
const animateTypes2 = ['opacity', 'backgroundColor']
const animateTypes3 = ['width', 'height', 'left', 'right', 'top', 'bottom']
animateTypes1.concat(animateTypes2, animateTypes3).forEach(type => {
MPAnimation.prototype[type] = function(...args) {
// #ifndef APP-NVUE
this.animation[type](...args)
// #endif
// #ifdef APP-NVUE
this._nvuePushAnimates(type, args)
// #endif
return this
}
})
export function createAnimation(option, _this) {
if(!_this) return
clearTimeout(_this.timer)
return new MPAnimation(option, _this)
}
@@ -0,0 +1,31 @@
export default {
props: {
// 是否展示组件
show: {
type: Boolean,
default: false
},
// 使用的动画模式
mode: {
type: [Array, String, null],
default() {
return 'fade'
}
},
// 动画的执行时间,单位ms
duration: {
type: [String, Number],
default: 300
},
// 使用的动画过渡函数
timingFunction: {
type: String,
default: 'ease-out'
},
customClass: {
type: String,
default: ''
},
...uni.$uv?.props?.transition
}
}
@@ -0,0 +1,320 @@
<template>
<!-- #ifndef APP-NVUE -->
<view
v-if="isShow"
ref="ani"
:animation="animationData"
:class="customClass"
:style="transformStyles"
@click="onClick">
<slot></slot>
</view>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<view
v-if="isShow"
ref="ani"
:animation="animationData"
:class="customClass"
:style="transformStyles"
@click="onClick">
<slot></slot>
</view>
<!-- #endif -->
</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 { createAnimation } from './createAnimation'
/**
* transition 动画组件
* @description
* @tutorial
* @property {Boolean} show 控制组件显示或关闭 (默认 false )
* @property {Array | String } mode 内置过渡动画类型 (默认 'fade'
* @value fade 渐隐渐出过渡
* @value slide-top 由上至下过渡
* @value slide-bottom 由下至上过渡
* @value slide-left 由左至右过渡
* @value slide-right 由右至左过渡
* @value zoom-in 由小到大过渡
* @value zoom-out 由大到小过渡
* @property {String | Number} duration 动画的执行时间,单位ms (默认 300 )
* @property {String} timingFunction 使用的动画过渡函数 (默认 'ease-out'
* @property {Object} customStyle 自定义样式
* @property {String} customClass 自定义类名
* @event {Function} click 点击组件触发
* @event {Function} change 过渡动画结束时触发
* @example
*/
export default {
name: 'uv-transition',
mixins: [mpMixin,mixin],
emits:['click','change'],
props: {
// 是否展示组件
show: {
type: Boolean,
default: false
},
// 使用的动画模式
mode: {
type: [Array, String, null],
default() {
return 'fade'
}
},
// 动画的执行时间,单位ms
duration: {
type: [String, Number],
default: 300
},
// 使用的动画过渡函数
timingFunction: {
type: String,
default: 'ease-out'
},
customClass: {
type: String,
default: ''
},
// nvue模式下 是否直接显示,在uv-list等cell下面使用就需要设置
cellChild: {
type: Boolean,
default: false
}
},
data(){
return {
isShow: false,
transform: '',
opacity: 1,
animationData: {},
durationTime: 300,
config: {}
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.open();
} else {
// 避免上来就执行 close,导致动画错乱
if (this.isShow) {
this.close();
}
}
},
immediate: true
}
},
computed: {
// 初始化动画条件
transformStyles() {
const style = {
transform: this.transform,
opacity: this.opacity,
...this.$uv.addStyle(this.customStyle),
'transition-duration': `${this.duration / 1000}s`
};
return this.$uv.addStyle(style,'string');
}
},
created() {
// 动画默认配置
this.config = {
duration: this.duration,
timingFunction: this.timingFunction,
transformOrigin: '50% 50%',
delay: 0
};
this.durationTime = this.duration;
},
methods: {
/**
* ref 触发 初始化动画
*/
init(obj = {}) {
if (obj.duration) {
this.durationTime = obj.duration;
}
this.animation = createAnimation(Object.assign(this.config, obj),this);
},
/**
* 点击组件触发回调
*/
onClick() {
this.$emit('click', {
detail: this.isShow
})
},
/**
* ref 触发 动画分组
* @param {Object} obj
*/
step(obj, config = {}) {
if (!this.animation) return;
for (let i in obj) {
try {
if(typeof obj[i] === 'object'){
this.animation[i](...obj[i]);
}else{
this.animation[i](obj[i]);
}
} catch (e) {
console.error(`方法 ${i} 不存在`);
}
}
this.animation.step(config);
return this;
},
/**
* ref 触发 执行动画
*/
run(fn) {
if (!this.animation) return;
this.animation.run(fn);
},
// 开始过度动画
open() {
clearTimeout(this.timer);
this.transform = '';
this.isShow = true;
let { opacity, transform } = this.styleInit(false);
if (typeof opacity !== 'undefined') {
this.opacity = opacity;
}
this.transform = transform;
// 确保动态样式已经生效后,执行动画,如果不加 nextTick ,会导致 wx 动画执行异常
this.$nextTick(() => {
// TODO 定时器保证动画完全执行,目前有些问题,后面会取消定时器
this.timer = setTimeout(() => {
this.animation = createAnimation(this.config, this);
this.tranfromInit(false).step();
// #ifdef APP-NVUE
if(this.cellChild) {
this.opacity = 1;
} else{
this.animation.run();
}
// #endif
// #ifndef APP-NVUE
this.animation.run();
// #endif
// #ifdef VUE3
// #ifdef H5
this.opacity = 1;
// #endif
// #endif
this.$emit('change', {
detail: this.isShow
})
// #ifdef H5
// #ifdef VUE3
this.transform = '';
// #endif
// #endif
}, 20);
})
},
// 关闭过渡动画
close(type) {
if (!this.animation) return;
this.tranfromInit(true)
.step()
.run(() => {
this.isShow = false;
this.animationData = null;
this.animation = null;
let { opacity, transform } = this.styleInit(false);
this.opacity = opacity || 1;
this.transform = transform;
this.$emit('change', {
detail: this.isShow
});
})
},
// 处理动画开始前的默认样式
styleInit(type) {
let styles = {
transform: ''
};
let buildStyle = (type, mode) => {
if (mode === 'fade') {
styles.opacity = this.animationType(type)[mode];
} else {
styles.transform += this.animationType(type)[mode] + ' ';
}
}
if (typeof this.mode === 'string') {
buildStyle(type, this.mode);
} else {
this.mode.forEach(mode => {
buildStyle(type, mode)
})
}
return styles
},
// 处理内置组合动画
tranfromInit(type) {
let buildTranfrom = (type, mode) => {
let aniNum = null;
if (mode === 'fade') {
aniNum = type ? 0 : 1;
} else {
aniNum = type ? '-100%' : '0';
if (mode === 'zoom-in') {
aniNum = type ? 0.8 : 1
}
if (mode === 'zoom-out') {
aniNum = type ? 1.2 : 1
}
if (mode === 'slide-right') {
aniNum = type ? '100%' : '0'
}
if (mode === 'slide-bottom') {
aniNum = type ? '100%' : '0'
}
}
this.animation[this.animationMode()[mode]](aniNum)
}
if (typeof this.mode === 'string') {
buildTranfrom(type, this.mode)
} else {
this.mode.forEach(mode => {
buildTranfrom(type, mode)
})
}
return this.animation;
},
animationType(type) {
return {
fade: type ? 1 : 0,
'slide-top': `translateY(${type ? '0' : '-100%'})`,
'slide-right': `translateX(${type ? '0' : '100%'})`,
'slide-bottom': `translateY(${type ? '0' : '100%'})`,
'slide-left': `translateX(${type ? '0' : '-100%'})`,
'zoom-in': `scaleX(${type ? 1 : 0.8}) scaleY(${type ? 1 : 0.8})`,
'zoom-out': `scaleX(${type ? 1 : 1.2}) scaleY(${type ? 1 : 1.2})`
}
},
// 内置动画类型与实际动画对应字典
animationMode() {
return {
fade: 'opacity',
'slide-top': 'translateY',
'slide-right': 'translateX',
'slide-bottom': 'translateY',
'slide-left': 'translateX',
'zoom-in': 'scale',
'zoom-out': 'scale'
}
},
// 驼峰转中横线
toLine(name) {
return name.replace(/([A-Z])/g, '-$1').toLowerCase()
}
}
}
</script>
@@ -0,0 +1,87 @@
{
"id": "uv-transition",
"displayName": "uv-transition 动画 全面兼容vue3+2、app、h5、小程序等多端",
"version": "1.0.8",
"description": "transition 该组件用于组件的动画过渡效果。",
"keywords": [
"uv-transition",
"uvui",
"uv-ui",
"transition",
"动画"
],
"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"
],
"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"
}
}
}
}
}
+15
View File
@@ -0,0 +1,15 @@
## Transition 动画
> **组件名:uv-transition**
该组件用于组件的动画过渡效果,支持自定义动画,开箱即用。
# <a href="https://www.uvui.cn/components/transition.html" target="_blank">查看文档</a>
## [下载完整示例项目](https://ext.dcloud.net.cn/plugin?name=uv-ui)
### [更多插件,请关注uv-ui组件库](https://ext.dcloud.net.cn/plugin?name=uv-ui)
![image](https://mp-a667b617-c5f1-4a2d-9a54-683a67cff588.cdn.bspapp.com/uv-ui/banner.png)
#### 如使用过程中有任何问题反馈,或者您对uv-ui有一些好的建议,欢迎加入uv-ui官方交流群:<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>