| @ -0,0 +1,165 @@ | |||
| <template> | |||
| <view class="page__view highlight"> | |||
| <!-- 导航栏 --> | |||
| <navbar :title="title" leftClick @leftClick="$utils.navigateBack" bgColor="transparent" color="#191919" /> | |||
| <!-- 搜索栏 --> | |||
| <view :class="['flex', 'search', isFocusSearch ? 'is-focus' : '']" > | |||
| <uv-search | |||
| v-model="keyword" | |||
| placeholder="请输入要查询的内容" | |||
| color="#181818" | |||
| bgColor="transparent" | |||
| :showAction="isFocusSearch" | |||
| @custom="search" | |||
| @search="search" | |||
| @focus="isFocusSearch = true" | |||
| @blur="isFocusSearch = false" | |||
| > | |||
| <template #prefix> | |||
| <image class="search-icon" src="/static/image/icon-search-dark.png" mode="widthFix"></image> | |||
| </template> | |||
| </uv-search> | |||
| </view> | |||
| <view v-if="list.length" class="list"> | |||
| <view class="list-item" v-for="item in list" :key="item.id"> | |||
| <view class="cover"> | |||
| <image class="img" :src="item.image" mode="aspectFill"></image> | |||
| </view> | |||
| <view class="info"> | |||
| <view class="title text-ellipsis-2">{{ item.title }}</view> | |||
| <view class="desc">{{ item.createTime }}</view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <template v-else> | |||
| <uv-empty mode="list"></uv-empty> | |||
| </template> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import mixinsList from '@/mixins/list.js' | |||
| import sortBar from './sortBar.vue' | |||
| import recordsView from '@/components/growing/recordsView.vue' | |||
| export default { | |||
| mixins: [mixinsList], | |||
| components: { | |||
| sortBar, | |||
| recordsView, | |||
| }, | |||
| data() { | |||
| return { | |||
| title: '搜索', | |||
| keyword: '', | |||
| isFocusSearch: false, | |||
| queryParams: { | |||
| pageNo: 1, | |||
| pageSize: 10, | |||
| title: '', | |||
| }, | |||
| // todo | |||
| mixinsListApi: '', | |||
| } | |||
| }, | |||
| onLoad({ title, api }) { | |||
| this.title = title | |||
| this.mixinsListApi = api | |||
| this.getData() | |||
| }, | |||
| methods: { | |||
| search() { | |||
| this.queryParams.pageNo = 1 | |||
| this.queryParams.pageSize = 10 | |||
| this.queryParams.title = this.keyword | |||
| this.getData() | |||
| }, | |||
| }, | |||
| } | |||
| </script> | |||
| <style scoped lang="scss"> | |||
| .search { | |||
| $h: 64rpx; | |||
| $radius: 32rpx; | |||
| $borderWidth: 4rpx; | |||
| margin: 24rpx 32rpx 0 32rpx; | |||
| width: calc(100% - 32rpx * 2); | |||
| height: $h; | |||
| position: relative; | |||
| border-radius: $radius; | |||
| &-icon { | |||
| margin: 0 13rpx 0 26rpx; | |||
| width: 30rpx; | |||
| height: auto; | |||
| } | |||
| &.is-focus { | |||
| /deep/ .uv-search__action { | |||
| padding: 19rpx 24rpx; | |||
| font-size: 26rpx; | |||
| font-weight: 500; | |||
| line-height: 1; | |||
| color: #FFFFFF; | |||
| background: #00A9FF; | |||
| border-radius: 32rpx; | |||
| } | |||
| } | |||
| } | |||
| .list { | |||
| margin-top: 24rpx; | |||
| padding: 12rpx 40rpx; | |||
| &-item { | |||
| column-gap: 16rpx; | |||
| & + & { | |||
| margin-top: 40rpx; | |||
| padding: 24rpx; | |||
| background: #F9F9F9; | |||
| border: 2rpx solid #FFFFFF; | |||
| border-radius: 24rpx; | |||
| } | |||
| .cover { | |||
| width: 152rpx; | |||
| height: 152rpx; | |||
| border-radius: 8rpx; | |||
| overflow: hidden; | |||
| border: 2rpx solid #E6E6E6; | |||
| .img { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| } | |||
| .info { | |||
| flex: 1; | |||
| .title { | |||
| font-size: 28rpx; | |||
| font-weight: 500; | |||
| color: #000000; | |||
| } | |||
| .desc { | |||
| margin-top: 8rpx; | |||
| font-size: 24rpx; | |||
| color: #8B8B8B; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,120 @@ | |||
| <template> | |||
| <view class="page__view highlight"> | |||
| <navbar title="标记有我" leftClick @leftClick="$utils.navigateBack" /> | |||
| <view class="main"> | |||
| <view class="section" v-for="item in list" :key="item.id"> | |||
| <view class="section-header"> | |||
| <view class="flex title">{{ item.title }}</view> | |||
| <button class="flex btn btn-all" @click="jumpToGrowing(item.id)"> | |||
| <view>查看成长档案</view> | |||
| <image class="icon" src="@/static/image/icon-arrow-right.png" mode="widthFix"></image> | |||
| </button> | |||
| </view> | |||
| <view class="section-content record"> | |||
| <view class="record-item" v-for="(image, imgIdx) in item.images" :key="imgIdx"> | |||
| <image class="img" :src="image" mode="scaleToFill"></image> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import mixinsList from '@/mixins/list.js' | |||
| export default { | |||
| mixins: [mixinsList], | |||
| data() { | |||
| return { | |||
| // todo | |||
| mixinsListApi: '', | |||
| } | |||
| }, | |||
| methods: { | |||
| jumpToGrowing(id) { | |||
| // todo | |||
| this.$utils.navigateTo(`/pages/index/growing`) | |||
| }, | |||
| }, | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .swiper { | |||
| margin-top: 40rpx; | |||
| .title { | |||
| margin-top: 12rpx; | |||
| font-size: 28rpx; | |||
| font-weight: 600; | |||
| color: #252545; | |||
| } | |||
| .tag { | |||
| margin-top: 4rpx; | |||
| padding: 2rpx 8rpx; | |||
| font-size: 24rpx; | |||
| color: #00A9FF; | |||
| background: #E0F5FF; | |||
| border-radius: 8rpx; | |||
| } | |||
| } | |||
| .main { | |||
| padding: 0 40rpx; | |||
| } | |||
| .section { | |||
| margin-top: 64rpx; | |||
| &-header { | |||
| font-size: 36rpx; | |||
| font-weight: 500; | |||
| color: #191919; | |||
| .title { | |||
| flex: 1; | |||
| justify-content: flex-start; | |||
| column-gap: 8rpx; | |||
| } | |||
| .btn-all { | |||
| column-gap: 4rpx; | |||
| font-size: 24rpx; | |||
| line-height: 1.4; | |||
| color: #8B8B8B; | |||
| .icon { | |||
| width: 32rpx; | |||
| height: auto; | |||
| } | |||
| } | |||
| } | |||
| &-content { | |||
| margin-top: 24rpx; | |||
| } | |||
| } | |||
| .record { | |||
| display: grid; | |||
| grid-template-columns: repeat(3, 1fr); | |||
| gap: 16rpx; | |||
| &-item { | |||
| border: 2rpx solid #CDCDCD; | |||
| border-radius: 12rpx; | |||
| overflow: hidden; | |||
| .img { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,262 @@ | |||
| <template> | |||
| <view> | |||
| <uv-popup ref="popup" mode="bottom" bgColor="none" > | |||
| <view class="popup__view"> | |||
| <view class="flex header"> | |||
| <view class="title">新增回顾</view> | |||
| <button class="btn" @click="close">关闭</button> | |||
| </view> | |||
| <view class="form"> | |||
| <uv-form | |||
| ref="form" | |||
| :model="form" | |||
| :rules="rules" | |||
| errorType="toast" | |||
| > | |||
| <view class="form-item"> | |||
| <uv-form-item prop="image" :customStyle="formItemStyle"> | |||
| <view class="form-item-label"> | |||
| <image class="icon" src="@/static/image/icon-require.png" mode="widthFix"></image> | |||
| 上传图片 | |||
| </view> | |||
| <view class="form-item-content"> | |||
| <button class="flex btn"> | |||
| <view v-if="form.image" class="avatar"> | |||
| <image class="img" :src="form.image" mode="aspectFill"></image> | |||
| <view class="flex mask"> | |||
| <image class="icon" src="@/static/image/icon-change.png" mode="widthFix" /> | |||
| </view> | |||
| </view> | |||
| <view v-else class="flex avatar is-empty"> | |||
| <image class="icon" src="@/static/image/icon-plus.png" mode="widthFix" /> | |||
| </view> | |||
| </button> | |||
| </view> | |||
| </uv-form-item> | |||
| </view> | |||
| </uv-form> | |||
| </view> | |||
| <view class="footer"> | |||
| <button class="flex btn" @click="onPublish">发布</button> | |||
| </view> | |||
| </view> | |||
| </uv-popup> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| export default { | |||
| data() { | |||
| return { | |||
| form: { | |||
| image: null, | |||
| }, | |||
| rules: { | |||
| 'image': { | |||
| type: 'string', | |||
| required: true, | |||
| message: '请上传图片', | |||
| }, | |||
| }, | |||
| formItemStyle: { padding: 0 }, | |||
| } | |||
| }, | |||
| methods: { | |||
| onUpload() { | |||
| uni.chooseImage({ | |||
| count: 1, | |||
| sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有 | |||
| success: res => { | |||
| let image = res.tempFilePaths[0] // 将选择的图片赋值给我们定义的cover | |||
| this.$Oss.ossUpload(image) | |||
| .then(url => { | |||
| this.form.image = url | |||
| }) | |||
| } | |||
| }); | |||
| }, | |||
| async onPublish() { | |||
| try { | |||
| await this.$refs.form.validate() | |||
| const { | |||
| } = this.form | |||
| const params = { | |||
| } | |||
| // todo: fetch | |||
| // await this.$fetch('updateAddress', params) | |||
| uni.showToast({ | |||
| icon: 'success', | |||
| title: '发布成功', | |||
| }); | |||
| this.$emit('submitted') | |||
| this.close() | |||
| } catch (err) { | |||
| console.log('onSave err', err) | |||
| } | |||
| }, | |||
| }, | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .popup__view { | |||
| width: 100vw; | |||
| display: flex; | |||
| flex-direction: column; | |||
| box-sizing: border-box; | |||
| background: #FFFFFF; | |||
| border-top-left-radius: 32rpx; | |||
| border-top-right-radius: 32rpx; | |||
| } | |||
| .header { | |||
| position: relative; | |||
| width: 100%; | |||
| padding: 24rpx 0; | |||
| box-sizing: border-box; | |||
| border-bottom: 2rpx solid #EEEEEE; | |||
| .title { | |||
| font-family: PingFang SC; | |||
| font-weight: 500; | |||
| font-size: 34rpx; | |||
| line-height: 1.4; | |||
| color: #181818; | |||
| } | |||
| .btn { | |||
| font-family: PingFang SC; | |||
| font-weight: 500; | |||
| font-size: 32rpx; | |||
| line-height: 1.4; | |||
| color: #8B8B8B; | |||
| position: absolute; | |||
| top: 26rpx; | |||
| left: 40rpx; | |||
| } | |||
| } | |||
| .form { | |||
| max-height: 75vh; | |||
| padding: 32rpx 40rpx; | |||
| box-sizing: border-box; | |||
| overflow-y: auto; | |||
| &-item { | |||
| padding: 8rpx 0 6rpx 0; | |||
| & + & { | |||
| padding-top: 24rpx; | |||
| border-top: 2rpx solid #EEEEEE; | |||
| } | |||
| &-label { | |||
| margin-bottom: 14rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| font-family: PingFang SC; | |||
| font-weight: 400; | |||
| font-size: 26rpx; | |||
| line-height: 1.4; | |||
| color: #181818; | |||
| .icon { | |||
| margin-right: 8rpx; | |||
| width: 16rpx; | |||
| height: auto; | |||
| } | |||
| } | |||
| &-content { | |||
| .text { | |||
| padding: 2rpx 0; | |||
| font-family: PingFang SC; | |||
| font-weight: 400; | |||
| font-size: 32rpx; | |||
| line-height: 1.4; | |||
| &.placeholder { | |||
| color: #C6C6C6; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .footer { | |||
| width: 100%; | |||
| padding: 32rpx 40rpx; | |||
| box-sizing: border-box; | |||
| border-top: 2rpx solid #F1F1F1; | |||
| .btn { | |||
| width: 100%; | |||
| padding: 14rpx 0; | |||
| box-sizing: border-box; | |||
| font-family: PingFang SC; | |||
| font-weight: 500; | |||
| font-size: 36rpx; | |||
| line-height: 1.4; | |||
| color: #FFFFFF; | |||
| background-image: linear-gradient(to right, #21FEEC, #019AF9); | |||
| border: 2rpx solid #00A9FF; | |||
| border-radius: 41rpx; | |||
| } | |||
| } | |||
| .btn-avatar { | |||
| display: inline-block; | |||
| width: auto; | |||
| border: none; | |||
| } | |||
| .avatar { | |||
| position: relative; | |||
| width: 200rpx; | |||
| height: 200rpx; | |||
| border-radius: 24rpx; | |||
| overflow: hidden; | |||
| .img { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| .mask { | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| width: 100%; | |||
| height: 100%; | |||
| background: #00000080; | |||
| border-radius: 24rpx; | |||
| .icon { | |||
| width: 64rpx; | |||
| height: 64rpx; | |||
| } | |||
| } | |||
| &.is-empty { | |||
| background: #F3F2F7; | |||
| .icon { | |||
| width: 61rpx; | |||
| height: auto; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,361 @@ | |||
| <template> | |||
| <view> | |||
| <uv-popup ref="popup" mode="bottom" bgColor="none" > | |||
| <view class="popup__view"> | |||
| <view class="flex header"> | |||
| <view class="title">新增记录</view> | |||
| <button class="btn" @click="close">关闭</button> | |||
| </view> | |||
| <view class="form"> | |||
| <uv-form | |||
| ref="form" | |||
| :model="form" | |||
| :rules="rules" | |||
| errorType="toast" | |||
| > | |||
| <view class="form-item"> | |||
| <uv-form-item prop="project" :customStyle="formItemStyle"> | |||
| <view class="form-item-label"> | |||
| <image class="icon" src="@/static/image/icon-require.png" mode="widthFix"></image> | |||
| 关联项目 | |||
| </view> | |||
| <view class="form-item-content"> | |||
| <view class="flex row" @click="openRelatePojectPicker"> | |||
| <view v-if="form.project" class="text">{{ projectDesc }}</view> | |||
| <view v-else class="text placeholder">请选择关联项目</view> | |||
| <uv-icon name="arrow-right" color="#C6C6C6" size="32rpx"></uv-icon> | |||
| </view> | |||
| <reloateProjectPopup ref="reloateProjectPopup" :options="projects" @confirm="onRelateProjectChange"></reloateProjectPopup> | |||
| </view> | |||
| </uv-form-item> | |||
| </view> | |||
| <view class="form-item"> | |||
| <uv-form-item prop="area" :customStyle="formItemStyle"> | |||
| <view class="form-item-label">选择地址</view> | |||
| <view class="form-item-content"> | |||
| <view class="flex row" @click="selectAddr"> | |||
| <view v-if="form.area" class="text">{{ form.area }}</view> | |||
| <view v-else class="text placeholder">请选择地址</view> | |||
| <uv-icon name="arrow-right" color="#C6C6C6" size="32rpx"></uv-icon> | |||
| </view> | |||
| </view> | |||
| </uv-form-item> | |||
| </view> | |||
| <view class="form-item"> | |||
| <uv-form-item prop="images" :customStyle="formItemStyle"> | |||
| <view class="form-item-label">上传图片</view> | |||
| <view class="form-item-content"> | |||
| <formUpload v-model="form.images"></formUpload> | |||
| </view> | |||
| </uv-form-item> | |||
| </view> | |||
| </uv-form> | |||
| </view> | |||
| <view class="footer"> | |||
| <button class="flex btn" @click="onPublish">发布</button> | |||
| </view> | |||
| </view> | |||
| </uv-popup> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import Position from '@/utils/position.js' | |||
| import reloateProjectPopup from '@/pages_order/components/reloateProjectPopup.vue' | |||
| import formTextarea from '@/pages_order/components/formTextarea.vue' | |||
| import formUpload from '@/pages_order/components/formUpload.vue' | |||
| import formRate from '@/pages_order/components/formRate.vue' | |||
| export default { | |||
| components: { | |||
| reloateProjectPopup, | |||
| formTextarea, | |||
| formUpload, | |||
| formRate, | |||
| }, | |||
| data() { | |||
| return { | |||
| form: { | |||
| project: null, | |||
| area: null, | |||
| latitude: null, | |||
| longitude: null, | |||
| images: [], | |||
| }, | |||
| rules: { | |||
| 'project': { | |||
| type: 'string', | |||
| required: true, | |||
| message: '请选择关联项目', | |||
| }, | |||
| 'area': { | |||
| type: 'string', | |||
| required: true, | |||
| message: '请选择地址', | |||
| }, | |||
| 'images': { | |||
| type: 'array', | |||
| required: true, | |||
| message: '请上传图片', | |||
| }, | |||
| }, | |||
| projects: [], | |||
| questions: [], | |||
| } | |||
| }, | |||
| computed: { | |||
| projectDesc() { | |||
| const { project } = this.form | |||
| const target = this.projects?.find?.(item => item.id === project) | |||
| return target?.name || '' | |||
| }, | |||
| }, | |||
| methods: { | |||
| getData() { | |||
| // todo | |||
| this.projects = [ | |||
| { | |||
| id: '001', | |||
| name: '亲子•坝上双草原6日 |乌兰布统+锡林郭勒+长城', | |||
| }, | |||
| { | |||
| id: '002', | |||
| name: '青青草原•云中岭 |5-10公里AB线强度可选', | |||
| }, | |||
| { | |||
| id: '003', | |||
| name: '新疆天山行7/9日丨醉美伊犁&吐鲁番双套餐', | |||
| }, | |||
| { | |||
| id: '004', | |||
| name: '九色甘南|人间净土6日/7日深度游', | |||
| }, | |||
| { | |||
| id: '005', | |||
| name: '北疆全景12日| 入疆首推!阿勒泰+伊犁+吐鲁番', | |||
| }, | |||
| { | |||
| id: '006', | |||
| name: '塞上江南•神奇宁夏5日|穿越大漠与历史对话', | |||
| }, | |||
| { | |||
| id: '007', | |||
| name: '尊享•天山环线9日| 伊犁全景+独库,头等舱大巴', | |||
| }, | |||
| ] | |||
| this.questions = [ | |||
| { | |||
| id: '001', | |||
| label: '这次研学之旅,整体给你留下了怎样的印象?用几个词或几句话简单概括一下', | |||
| }, | |||
| { | |||
| id: '002', | |||
| label: '在整个行程中,你最喜欢的部分是哪里?为什么?', | |||
| }, | |||
| { | |||
| id: '003', | |||
| label: '你觉得这次研学的行程安排是否合理?有没有哪些地方让你觉得特别满意或需要改进的?', | |||
| }, | |||
| ] | |||
| }, | |||
| async open(id) { | |||
| // todo: auto bind project by id? | |||
| await this.getData() | |||
| const texts = this.questions.map(() => '') | |||
| this.form = { | |||
| project: null, | |||
| tripNum: null, | |||
| spotNum: null, | |||
| mentorNum: null, | |||
| images: [], | |||
| texts, | |||
| } | |||
| this.$refs.popup.open() | |||
| }, | |||
| close() { | |||
| this.$refs.popup.close() | |||
| }, | |||
| openRelatePojectPicker() { | |||
| this.$refs.reloateProjectPopup.open(this.form.project?.id || null) | |||
| }, | |||
| onRelateProjectChange(id) { | |||
| this.form.project = id | |||
| }, | |||
| //地图上选择地址 | |||
| selectAddr() { | |||
| // Position.getLocation(res => { | |||
| Position.selectAddress(0, 0, success => { | |||
| this.setAddress(success) | |||
| }) | |||
| // }) | |||
| }, | |||
| //提取用户选择的地址信息复制给表单数据 | |||
| setAddress(res) { | |||
| //经纬度信息 | |||
| this.form.latitude = res.latitude | |||
| this.form.longitude = res.longitude | |||
| if (!res.address && res.name) { //用户直接选择城市的逻辑 | |||
| return this.form.area = res.name | |||
| } | |||
| if (res.address || res.name) { | |||
| return this.form.area = res.address + res.name | |||
| } | |||
| this.form.area = '' //用户啥都没选就点击勾选 | |||
| }, | |||
| async onPublish() { | |||
| try { | |||
| await this.$refs.form.validate() | |||
| const { | |||
| } = this.form | |||
| const params = { | |||
| } | |||
| // todo: fetch | |||
| // await this.$fetch('updateAddress', params) | |||
| uni.showToast({ | |||
| icon: 'success', | |||
| title: '发布成功', | |||
| }); | |||
| this.$emit('submitted') | |||
| this.close() | |||
| } catch (err) { | |||
| console.log('onSave err', err) | |||
| } | |||
| }, | |||
| }, | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .popup__view { | |||
| width: 100vw; | |||
| display: flex; | |||
| flex-direction: column; | |||
| box-sizing: border-box; | |||
| background: #FFFFFF; | |||
| border-top-left-radius: 32rpx; | |||
| border-top-right-radius: 32rpx; | |||
| } | |||
| .header { | |||
| position: relative; | |||
| width: 100%; | |||
| padding: 24rpx 0; | |||
| box-sizing: border-box; | |||
| border-bottom: 2rpx solid #EEEEEE; | |||
| .title { | |||
| font-family: PingFang SC; | |||
| font-weight: 500; | |||
| font-size: 34rpx; | |||
| line-height: 1.4; | |||
| color: #181818; | |||
| } | |||
| .btn { | |||
| font-family: PingFang SC; | |||
| font-weight: 500; | |||
| font-size: 32rpx; | |||
| line-height: 1.4; | |||
| color: #8B8B8B; | |||
| position: absolute; | |||
| top: 26rpx; | |||
| left: 40rpx; | |||
| } | |||
| } | |||
| .form { | |||
| max-height: 75vh; | |||
| padding: 32rpx 40rpx; | |||
| box-sizing: border-box; | |||
| overflow-y: auto; | |||
| &-item { | |||
| padding: 8rpx 0 6rpx 0; | |||
| & + & { | |||
| padding-top: 24rpx; | |||
| border-top: 2rpx solid #EEEEEE; | |||
| } | |||
| &-label { | |||
| margin-bottom: 14rpx; | |||
| display: flex; | |||
| align-items: center; | |||
| font-family: PingFang SC; | |||
| font-weight: 400; | |||
| font-size: 26rpx; | |||
| line-height: 1.4; | |||
| color: #181818; | |||
| .icon { | |||
| margin-right: 8rpx; | |||
| width: 16rpx; | |||
| height: auto; | |||
| } | |||
| } | |||
| &-content { | |||
| .text { | |||
| padding: 2rpx 0; | |||
| font-family: PingFang SC; | |||
| font-weight: 400; | |||
| font-size: 32rpx; | |||
| line-height: 1.4; | |||
| &.placeholder { | |||
| color: #C6C6C6; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .row { | |||
| justify-content: space-between; | |||
| .form-label { | |||
| margin: 0; | |||
| } | |||
| } | |||
| .footer { | |||
| width: 100%; | |||
| padding: 32rpx 40rpx; | |||
| box-sizing: border-box; | |||
| border-top: 2rpx solid #F1F1F1; | |||
| .btn { | |||
| width: 100%; | |||
| padding: 14rpx 0; | |||
| box-sizing: border-box; | |||
| font-family: PingFang SC; | |||
| font-weight: 500; | |||
| font-size: 36rpx; | |||
| line-height: 1.4; | |||
| color: #FFFFFF; | |||
| background-image: linear-gradient(to right, #21FEEC, #019AF9); | |||
| border: 2rpx solid #00A9FF; | |||
| border-radius: 41rpx; | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,241 @@ | |||
| <template> | |||
| <view class="page__view highlight"> | |||
| <navbar title="图片直播" leftClick @leftClick="$utils.navigateBack" /> | |||
| <view class="header"> | |||
| <image class="icon" :src="detail.image" mode="widthFix"></image> | |||
| <view class="flex"> | |||
| <view class="flex"> | |||
| <view class="title">{{ detail.title }}</view> | |||
| <view class="tag">{{ detail.createTime }}</view> | |||
| </view> | |||
| <view v-if="isManager" class="flex operate"> | |||
| <view class="btn btn-add" @click="onAdd">新增记录</view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <view class="main"> | |||
| <view class="section" v-for="item in list" :key="item.id"> | |||
| <view class="section-header"> | |||
| <view class="avatar"> | |||
| <!-- todo: check key --> | |||
| <image class="avatar-img" src="@/static/image/temp-30.png" mode="scaleToFill"></image> | |||
| </view> | |||
| <view class="info"> | |||
| <view class="flex title"> | |||
| <view>{{ item.name }}</view> | |||
| <image class="icon" src="@/static/image/icon-location.png" mode="widthFix"></image> | |||
| <view class="address text-ellipsis">{{ item.address }}</view> | |||
| </view> | |||
| <view class="desc">{{ item.createTime }}</view> | |||
| </view> | |||
| </view> | |||
| <view class="section-content record"> | |||
| <view class="record-item" v-for="(image, imgIdx) in item.images" :key="imgIdx"> | |||
| <image class="img" :src="image" mode="scaleToFill"></image> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <formPopup ref="formPopup" @submitted="getData"></formPopup> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import mixinsList from '@/mixins/list.js' | |||
| import SYStackedCarousel from '@/uni_modules/SY-StackedCarousel/components/SY-StackedCarousel/SY-StackedCarousel.vue' | |||
| import formPopup from './formPopup.vue' | |||
| export default { | |||
| mixins: [mixinsList], | |||
| components: { | |||
| SYStackedCarousel, | |||
| formPopup, | |||
| }, | |||
| data() { | |||
| return { | |||
| id: null, | |||
| detail: {}, | |||
| bannerList: [], | |||
| current: 0, | |||
| queryParams: { | |||
| pageNo: 1, | |||
| pageSize: 10, | |||
| id: '', | |||
| }, | |||
| // todo | |||
| mixinsListApi: '', | |||
| // todo: fetch | |||
| isManager: true, | |||
| } | |||
| }, | |||
| computed: { | |||
| swiperCurrent() { | |||
| return this.bannerList[this.current] | |||
| }, | |||
| }, | |||
| onLoad(arg) { | |||
| const { id } = arg | |||
| this.id = id | |||
| this.fetchDetail() | |||
| this.queryParams.id = id | |||
| this.getData() | |||
| }, | |||
| methods: { | |||
| async fetchDetail() { | |||
| this.bannerList = [ | |||
| { | |||
| url: '/static/image/temp-23.png', | |||
| title: '趣玩新加坡', | |||
| createTime: '2025-04-18', | |||
| }, | |||
| { | |||
| url: '/static/image/temp-24.png', | |||
| title: '坝上双草原', | |||
| createTime: '2025-04-18', | |||
| }, | |||
| { | |||
| url: '/static/image/temp-25.png', | |||
| title: '牛湖线', | |||
| createTime: '2025-04-18', | |||
| }, | |||
| ] | |||
| }, | |||
| clickHandler(item, index) { | |||
| console.log("item: ", item); | |||
| console.log("index: ", index); | |||
| this.current = index | |||
| }, | |||
| changeHandler(index) { | |||
| console.log("当前触发change事件,返回索引: ", index); | |||
| }, | |||
| onAdd() { | |||
| this.$refs.formPopup.open() | |||
| }, | |||
| }, | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .header { | |||
| margin-top: 40rpx; | |||
| .title { | |||
| margin-top: 12rpx; | |||
| font-size: 28rpx; | |||
| font-weight: 600; | |||
| color: #252545; | |||
| } | |||
| .tag { | |||
| margin-top: 4rpx; | |||
| padding: 2rpx 8rpx; | |||
| font-size: 24rpx; | |||
| color: #00A9FF; | |||
| background: #E0F5FF; | |||
| border-radius: 8rpx; | |||
| } | |||
| .operate { | |||
| flex: 1; | |||
| justify-content: flex-end; | |||
| .btn-add { | |||
| padding: 6rpx 22rpx; | |||
| font-family: PingFang SC; | |||
| font-size: 28rpx; | |||
| font-weight: 500; | |||
| line-height: 1.5; | |||
| color: #FFFFFF; | |||
| background: linear-gradient(to right, #21FEEC, #019AF9); | |||
| border: 2rpx solid #00A9FF; | |||
| border-radius: 30rpx; | |||
| } | |||
| } | |||
| } | |||
| .main { | |||
| padding: 0 40rpx; | |||
| } | |||
| .section { | |||
| margin-top: 40rpx; | |||
| &-header { | |||
| .avatar { | |||
| flex: none; | |||
| width: 100rpx; | |||
| height: 100rpx; | |||
| border: 4rpx solid #FFFFFF; | |||
| border-radius: 50%; | |||
| overflow: hidden; | |||
| &-img { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| } | |||
| .info { | |||
| flex: 1; | |||
| column-gap: 24rpx; | |||
| .title { | |||
| column-gap: 8rpx; | |||
| white-space: nowrap; | |||
| font-size: 26rpx; | |||
| font-weight: 600; | |||
| color: #252545; | |||
| .icon { | |||
| width: 32rpx; | |||
| height: 32rpx; | |||
| } | |||
| .address { | |||
| flex: 1; | |||
| font-size: 24rpx; | |||
| color: #00A9FF; | |||
| } | |||
| } | |||
| .desc { | |||
| margin-top: 8rpx; | |||
| } | |||
| } | |||
| } | |||
| &-content { | |||
| margin-top: 24rpx; | |||
| } | |||
| } | |||
| .record { | |||
| display: grid; | |||
| grid-template-columns: repeat(3, 1fr); | |||
| gap: 16rpx; | |||
| &-item { | |||
| border: 2rpx solid #CDCDCD; | |||
| border-radius: 12rpx; | |||
| overflow: hidden; | |||
| .img { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,235 @@ | |||
| <template> | |||
| <view class="page__view highlight"> | |||
| <navbar title="图片直播" leftClick @leftClick="$utils.navigateBack" /> | |||
| <view class="swiper"> | |||
| <SYStackedCarousel | |||
| height="536rpx" | |||
| :images="bannerList" | |||
| :current="current" | |||
| :autoplay="true" | |||
| horizontalMargin="25" | |||
| baseOpacity="0.5" | |||
| bgColor="transparent" | |||
| padding="0" | |||
| @click="clickHandler" | |||
| @change="changeHandler" | |||
| > | |||
| </SYStackedCarousel> | |||
| <view class="flex" v-if="swiperCurrent"> | |||
| <view class="title">{{ swiperCurrent.title }}</view> | |||
| <view class="tag">{{ swiperCurrent.createTime }}</view> | |||
| </view> | |||
| </view> | |||
| <view class="main"> | |||
| <view class="flex header"> | |||
| <view>图片直播</view> | |||
| <view class="btn btn-mark" @click="onMark">标记有我</view> | |||
| </view> | |||
| <view class="section" v-for="item in list" :key="item.id"> | |||
| <view class="section-header"> | |||
| <view class="flex title"> | |||
| <view>{{ item.title }}</view> | |||
| <view v-if="isManager" class="btn btn-add" @click="onAdd(item.id)">新增记录</view> | |||
| </view> | |||
| <button class="flex btn btn-all" @click="showAll"> | |||
| <view>查看全部</view> | |||
| <image class="icon" src="@/static/image/icon-arrow-right.png" mode="widthFix"></image> | |||
| </button> | |||
| </view> | |||
| <view class="section-content record"> | |||
| <view class="record-item" v-for="(image, imgIdx) in item.images" :key="imgIdx"> | |||
| <image class="img" :src="image" mode="scaleToFill"></image> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <markPopup ref="markPopup"></markPopup> | |||
| <formPopup ref="formPopup" @submitted="getData"></formPopup> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import mixinsList from '@/mixins/list.js' | |||
| import SYStackedCarousel from '@/uni_modules/SY-StackedCarousel/components/SY-StackedCarousel/SY-StackedCarousel.vue' | |||
| import markPopup from '@/pages_order/growing/activity/markPopup.vue' | |||
| import formPopup from './formPopup.vue' | |||
| export default { | |||
| mixins: [mixinsList], | |||
| components: { | |||
| SYStackedCarousel, | |||
| markPopup, | |||
| formPopup, | |||
| }, | |||
| data() { | |||
| return { | |||
| bannerList: [], | |||
| current: 0, | |||
| queryParams: { | |||
| pageNo: 1, | |||
| pageSize: 10, | |||
| id: '', | |||
| }, | |||
| // todo | |||
| mixinsListApi: '', | |||
| // todo: fetch | |||
| isManager: true, | |||
| } | |||
| }, | |||
| computed: { | |||
| swiperCurrent() { | |||
| return this.bannerList[this.current] | |||
| }, | |||
| }, | |||
| onLoad() { | |||
| this.fetchBanner() | |||
| }, | |||
| methods: { | |||
| async fetchBanner() { | |||
| this.bannerList = [ | |||
| { | |||
| url: '/static/image/temp-23.png', | |||
| title: '趣玩新加坡', | |||
| createTime: '2025-04-18', | |||
| }, | |||
| { | |||
| url: '/static/image/temp-24.png', | |||
| title: '坝上双草原', | |||
| createTime: '2025-04-18', | |||
| }, | |||
| { | |||
| url: '/static/image/temp-25.png', | |||
| title: '牛湖线', | |||
| createTime: '2025-04-18', | |||
| }, | |||
| ] | |||
| }, | |||
| clickHandler(item, index) { | |||
| console.log("item: ", item); | |||
| console.log("index: ", index); | |||
| this.current = index | |||
| }, | |||
| changeHandler(index) { | |||
| console.log("当前触发change事件,返回索引: ", index); | |||
| }, | |||
| onMark() { | |||
| this.$refs.markPopup.open(this.id) | |||
| }, | |||
| onAdd() { | |||
| this.$refs.formPopup.open() | |||
| }, | |||
| }, | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .swiper { | |||
| margin-top: 40rpx; | |||
| .title { | |||
| margin-top: 12rpx; | |||
| font-size: 28rpx; | |||
| font-weight: 600; | |||
| color: #252545; | |||
| } | |||
| .tag { | |||
| margin-top: 4rpx; | |||
| padding: 2rpx 8rpx; | |||
| font-size: 24rpx; | |||
| color: #00A9FF; | |||
| background: #E0F5FF; | |||
| border-radius: 8rpx; | |||
| } | |||
| } | |||
| .main { | |||
| padding: 0 40rpx; | |||
| } | |||
| .header { | |||
| justify-content: space-between; | |||
| font-size: 40rpx; | |||
| font-weight: 600; | |||
| color: #252545; | |||
| .btn-mark { | |||
| padding: 6rpx 22rpx; | |||
| font-family: PingFang SC; | |||
| font-size: 28rpx; | |||
| font-weight: 500; | |||
| line-height: 1.5; | |||
| color: #FFFFFF; | |||
| background: linear-gradient(to right, #21FEEC, #019AF9); | |||
| border: 2rpx solid #00A9FF; | |||
| border-radius: 30rpx; | |||
| } | |||
| } | |||
| .section { | |||
| margin-top: 64rpx; | |||
| &-header { | |||
| font-size: 36rpx; | |||
| font-weight: 500; | |||
| color: #191919; | |||
| .title { | |||
| flex: 1; | |||
| justify-content: flex-start; | |||
| column-gap: 8rpx; | |||
| .btn-add { | |||
| padding: 8rpx 24rpx; | |||
| font-size: 28rpx; | |||
| font-weight: 500; | |||
| line-height: 1.5; | |||
| color: #252545; | |||
| border: 2rpx solid #252545; | |||
| border-radius: 30rpx; | |||
| } | |||
| } | |||
| .btn-all { | |||
| column-gap: 4rpx; | |||
| font-size: 24rpx; | |||
| line-height: 1.4; | |||
| color: #8B8B8B; | |||
| .icon { | |||
| width: 32rpx; | |||
| height: auto; | |||
| } | |||
| } | |||
| } | |||
| &-content { | |||
| margin-top: 24rpx; | |||
| } | |||
| } | |||
| .record { | |||
| display: grid; | |||
| grid-template-columns: repeat(3, 1fr); | |||
| gap: 16rpx; | |||
| &-item { | |||
| border: 2rpx solid #CDCDCD; | |||
| border-radius: 12rpx; | |||
| overflow: hidden; | |||
| .img { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,4 @@ | |||
| ## 1.0.0(2024-08-18) | |||
| **初始发布** | |||
| - | |||
| # 1.0.0 (2024-08-16) | |||
| @ -0,0 +1,421 @@ | |||
| <template> | |||
| <view class="sy-swiper" :style="{ | |||
| backgroundColor: bgColor, | |||
| height: addUnit(height), | |||
| padding: padding, | |||
| borderRadius: addUnit(radius) | |||
| }"> | |||
| <view v-if="showButton" class="sy-swiper__operation left" @click="slideLeft" :style="{ | |||
| width: addUnit(buttonSize), | |||
| height: addUnit(buttonSize), | |||
| background: buttonBgColor, | |||
| fontSize: addUnit(buttonSize) | |||
| }"> | |||
| <slot name="left-button"></slot> | |||
| </view> | |||
| <view class="sy-swiper__panel" @touchstart="onTouchStart" @touchend="onTouchEnd"> | |||
| <view class="sy-swiper__panel-item" v-for="(item, index) in images" :key="index" :style="{ | |||
| transform: itemStyles[index].transform, | |||
| zIndex: itemStyles[index].zIndex, | |||
| opacity: itemStyles[index].opacity, | |||
| transitionDuration: millisecondsToSeconds(duration) + 's', | |||
| transitionTimingFunction: easing | |||
| }"> | |||
| <view class="sy-swiper__panel-item__content" @click="$emit('click', item, index)"> | |||
| <template v-if="showFirstImageOnly"> | |||
| <image class="slide" :src="item.url" :mode="imgMode" v-if="itemStyles[index].isTop" :style="{ | |||
| borderRadius: addUnit(slideRadius) | |||
| }"></image> | |||
| <view class="slide-mask" v-else :style="{ background: maskBgColor, borderRadius: addUnit(slideRadius) }"></view> | |||
| </template> | |||
| <template v-else> | |||
| <image class="slide" :src="item.url" :mode="imgMode" :style="{ borderRadius: addUnit(slideRadius) }"> | |||
| </image> | |||
| </template> | |||
| <text class="desc" :class="{ 'text': descNoWrap }" v-if="showDesc && item.desc" :style="{ background: descBgColor, color: descColor, fontSize: addUnit(descSize), borderRadius: `0 0 ${addUnit(slideRadius)} ${addUnit(slideRadius)}` }">{{ item.desc }}</text> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| <view v-if="showButton" class="sy-swiper__operation right" @click="slideRight" :style="{ | |||
| width: addUnit(buttonSize), | |||
| height: addUnit(buttonSize), | |||
| background: buttonBgColor, | |||
| fontSize: addUnit(buttonSize) | |||
| }"> | |||
| <slot name="right-button"></slot> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import { | |||
| addUnit, | |||
| millisecondsToSeconds | |||
| } from "./util.js"; | |||
| export default { | |||
| props: { | |||
| // 图片列表 | |||
| images: { | |||
| type: Array, | |||
| default: () => [] | |||
| }, | |||
| padding: { | |||
| type: String, | |||
| default: "10px" | |||
| }, | |||
| // 是否自动切换 | |||
| autoplay: { | |||
| type: Boolean, | |||
| default: false | |||
| }, | |||
| // 动画类型 | |||
| easing: { | |||
| type: String, | |||
| default: "ease-in-out" | |||
| }, | |||
| // 是否启用不透明度设定 | |||
| enableOpacity: { | |||
| type: Boolean, | |||
| default: true | |||
| }, | |||
| // 最顶层图片的不透明度 | |||
| baseOpacity: { | |||
| type: Number, | |||
| default: 0.8 | |||
| }, | |||
| // 当前所在滑块的 index | |||
| current: { | |||
| type: Number, | |||
| default: 0 | |||
| }, | |||
| // 滑块自动切换时间间隔(ms) | |||
| interval: { | |||
| type: Number, | |||
| default: 2000 | |||
| }, | |||
| // 滑块切换过程所需时间(ms) | |||
| duration: { | |||
| type: Number, | |||
| default: 500 | |||
| }, | |||
| // 左右边距 | |||
| horizontalMargin: { | |||
| type: [String, Number], | |||
| default: 10 | |||
| }, | |||
| // 上下边距 | |||
| verticalMargin: { | |||
| type: [String, Number], | |||
| default: 10 | |||
| }, | |||
| // 组件高度 | |||
| height: { | |||
| type: [String, Number], | |||
| default: 130 | |||
| }, | |||
| // 是否显示操作按钮 | |||
| showButton: { | |||
| type: Boolean, | |||
| default: false | |||
| }, | |||
| // 按钮字体大小 | |||
| buttonSize: { | |||
| type: [String, Number], | |||
| default: 24 | |||
| }, | |||
| // 操作按钮背景颜色 | |||
| buttonBgColor: { | |||
| type: String, | |||
| default: "rgba(0, 0, 0, 0.26)" | |||
| }, | |||
| // 图片裁剪模式,详情见微信原生imageMode | |||
| imgMode: { | |||
| type: String, | |||
| default: "aspectFill" | |||
| }, | |||
| // 是否只显示第一张图片,其他图片被蒙版遮住 | |||
| showFirstImageOnly: { | |||
| type: Boolean, | |||
| default: false | |||
| }, | |||
| // 遮住蒙版的颜色 | |||
| maskBgColor: { | |||
| type: String, | |||
| default: "#ffffff" | |||
| }, | |||
| // 是否显示图片描述(需要images传递的数据中存在desc属性) | |||
| showDesc: { | |||
| type: Boolean, | |||
| default: true, | |||
| }, | |||
| // 底部描述的背景 | |||
| descBgColor: { | |||
| type: String, | |||
| default: "rgba(0, 0, 0, 0.5)" | |||
| }, | |||
| // 底部描述字体颜色 | |||
| descColor: { | |||
| type: String, | |||
| default: "#ffffff" | |||
| }, | |||
| // 底部描述字体大小 | |||
| descSize: { | |||
| type: [Number, String], | |||
| default: 10 | |||
| }, | |||
| // 描述是否不换行,超出后用...省略 | |||
| descNoWrap: { | |||
| type: Boolean, | |||
| default: false | |||
| }, | |||
| // 轮播图片的圆角值 | |||
| radius: { | |||
| type: [String, Number], | |||
| default: 4 | |||
| }, | |||
| // 容器背景色 | |||
| bgColor: { | |||
| type: String, | |||
| default: "#ffffff" | |||
| }, | |||
| slideRadius: { | |||
| type: [String, Number], | |||
| default: 10 | |||
| } | |||
| }, | |||
| // 切换 | |||
| emits: ["click", "change"], | |||
| data() { | |||
| return { | |||
| slideNote: { | |||
| x: 0, | |||
| y: 0 | |||
| }, | |||
| itemStyles: [], | |||
| screenWidth: 0, | |||
| currentIndex: 0, | |||
| autoSlideInterval: null, | |||
| autoSlideTimeout: null | |||
| } | |||
| }, | |||
| mounted() { | |||
| this.autoplay && this.doSomething(); | |||
| }, | |||
| onUnload() { | |||
| this.autoSlideInterval = null; | |||
| this.autoSlideTimeout = null; | |||
| }, | |||
| beforeDestroy() { | |||
| clearInterval(this.autoSlideInterval); | |||
| clearTimeout(this.autoSlideTimeout); | |||
| this.autoSlideInterval = null; | |||
| this.autoSlideTimeout = null; | |||
| }, | |||
| watch: { | |||
| images: { | |||
| handler(newImages) { | |||
| var macInfo = uni.getSystemInfoSync(); | |||
| this.screenWidth = macInfo.screenWidth; | |||
| this.itemStyles = newImages.map((_, index) => (this.getStyle(index))); | |||
| }, | |||
| deep: true, | |||
| immediate: true | |||
| }, | |||
| current: { | |||
| handler(newCurrentIndex) { | |||
| this.scrollToCurrent(newCurrentIndex); | |||
| }, | |||
| immediate: true | |||
| } | |||
| }, | |||
| methods: { | |||
| addUnit, | |||
| millisecondsToSeconds, | |||
| doSomething() { | |||
| this.$nextTick(() => { | |||
| this.autoSlideInterval = setInterval(() => { | |||
| this.slideRight(); | |||
| }, this.interval) | |||
| }) | |||
| }, | |||
| getStyle(eIndex) { | |||
| if (eIndex > this.images.length / 2) { | |||
| var right = this.images.length - eIndex; | |||
| return { | |||
| transform: "scale(" + (1 - right / this.verticalMargin) + ") translate(-" + right * this | |||
| .horizontalMargin + "%, 0px)", | |||
| zIndex: 100 - right, | |||
| opacity: this.enableOpacity ? this.baseOpacity / right : 1, | |||
| isTop: eIndex === 0 | |||
| } | |||
| } else { | |||
| return { | |||
| transform: "scale(" + (1 - eIndex / this.verticalMargin) + ") translate(" + eIndex * this | |||
| .horizontalMargin + "%, 0px)", | |||
| zIndex: 100 - eIndex, | |||
| opacity: this.enableOpacity ? this.baseOpacity / eIndex : 1, | |||
| isTop: eIndex === 0 | |||
| } | |||
| } | |||
| }, | |||
| restartTimer() { | |||
| clearInterval(this.autoSlideInterval); | |||
| this.autoSlideInterval = null; | |||
| }, | |||
| scrollToCurrent(currentIndex) { | |||
| // 清除现有的定时器,避免干扰 | |||
| this.restartTimer(); | |||
| // 这里选择用转动的方式,更改图片顺序会导致整个轮播的顺序错落 | |||
| for (let i = 0; i < currentIndex; i++) { | |||
| this.slideRight(); | |||
| } | |||
| }, | |||
| simulateStartMove(x) { | |||
| this.slideNote.x = x; | |||
| }, | |||
| onTouchStart(e) { | |||
| this.restartTimer(); | |||
| this.slideNote.x = e.changedTouches[0] ? e.changedTouches[0].pageX : 0; | |||
| this.slideNote.y = e.changedTouches[0] ? e.changedTouches[0].pageY : 0; | |||
| }, | |||
| onTouchEnd(e) { | |||
| var newList = JSON.parse(JSON.stringify(this.itemStyles)); | |||
| if (e.changedTouches[0].pageX - this.slideNote.x < -10) { | |||
| // 向左滑动 | |||
| var last = [newList.pop()]; | |||
| newList = last.concat(newList); | |||
| this.currentIndex = (this.currentIndex + 1) % this.images.length; | |||
| } else if (e.changedTouches[0].pageX - this.slideNote.x >= 10) { | |||
| // 向右滑动 | |||
| newList.push(newList[0]); | |||
| newList.splice(0, 1); | |||
| this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length; | |||
| } | |||
| this.itemStyles = newList; | |||
| if (this.autoplay) { | |||
| clearInterval(this.autoSlideInterval); | |||
| clearTimeout(this.autoSlideTimeout); | |||
| // 设置新的定时器,使用 this.interval 值作为间隔时间 | |||
| this.autoSlideTimeout = setTimeout(() => { | |||
| this.slideRight(); | |||
| }, this.interval); | |||
| } | |||
| }, | |||
| slideLeft() { | |||
| this.restartTimer(); | |||
| this.simulateStartMove(this.slideNote.x); // 模拟 touchstart,确保 slideNote.x 更新 | |||
| this.onTouchEnd({ | |||
| changedTouches: [{ | |||
| pageX: this.slideNote.x + 20 | |||
| }] | |||
| }); | |||
| // 触发 change 事件并传递当前的索引 | |||
| this.$emit('change', this.currentIndex); | |||
| }, | |||
| slideRight() { | |||
| this.restartTimer(); | |||
| this.simulateStartMove(this.slideNote.x); // 模拟 touchstart,确保 slideNote.x 更新 | |||
| this.onTouchEnd({ | |||
| changedTouches: [{ | |||
| pageX: this.slideNote.x - 20 | |||
| }] | |||
| }); | |||
| // 触发change 事件并传递当前的索引 | |||
| this.$emit('change', this.currentIndex); | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .sy-swiper { | |||
| position: relative; | |||
| box-sizing: border-box; | |||
| .sy-swiper__operation { | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| position: absolute; | |||
| border-radius: 50%; | |||
| background: rgba(0, 0, 0, 0.26); | |||
| z-index: 999; | |||
| &:active { | |||
| filter: brightness(70%); | |||
| } | |||
| } | |||
| .sy-swiper__operation.left { | |||
| top: 50%; | |||
| left: 10px; | |||
| transform: translateY(-50%); | |||
| } | |||
| .sy-swiper__operation.right { | |||
| bottom: 50%; | |||
| right: 10px; | |||
| transform: translateY(50%); | |||
| } | |||
| .sy-swiper__panel { | |||
| width: 100%; | |||
| height: 100%; | |||
| overflow: hidden; | |||
| position: relative; | |||
| .sy-swiper__panel-item { | |||
| height: 100%; | |||
| width: 100%; | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| transition-property: transform, opacity; | |||
| // transition-duration: 0.4s; | |||
| // transition-timing-function: ease-in-out; | |||
| &__content { | |||
| height: 100%; | |||
| width: 330rpx; | |||
| margin: 2rpx auto; | |||
| position: relative; | |||
| .slide { | |||
| height: 100%; | |||
| width: 100%; | |||
| } | |||
| .slide-mask { | |||
| position: relative; | |||
| height: 100%; | |||
| width: 100%; | |||
| z-index: 1; | |||
| } | |||
| .desc { | |||
| position: absolute; | |||
| width: 100%; | |||
| bottom: 0; | |||
| left: 0; | |||
| padding: 10rpx 20rpx; | |||
| box-sizing: border-box; | |||
| box-shadow: 0rpx 4rpx 21rpx 0rpx rgba(0, 0, 0, 0.07); | |||
| // border-radius: 0 0 20px 20px; | |||
| font-family: PingFang SC; | |||
| font-weight: 400; | |||
| line-height: 32rpx; | |||
| text-transform: none; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .text { | |||
| overflow: hidden; | |||
| white-space: nowrap; | |||
| text-overflow: ellipsis; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,22 @@ | |||
| /** | |||
| * 验证十进制数字 | |||
| */ | |||
| export function isNumber(value) { | |||
| return /^[\+-]?(\d+\.?\d*|\.\d+|\d\.\d+e\+\d+)$/.test(value) | |||
| } | |||
| export function addUnit(value = 'auto', unit = '') { | |||
| if (!unit) { | |||
| unit = 'px' | |||
| } | |||
| if (unit == 'rpx') { | |||
| value = value * 2 | |||
| } | |||
| value = String(value) | |||
| // 用内置验证规则中的number判断是否为数值 | |||
| return isNumber(value) ? `${value}${unit}` : value | |||
| } | |||
| export function millisecondsToSeconds(milliseconds) { | |||
| return Math.round(milliseconds / 1000 * 100) / 100; // 将结果四舍五入到两位小数 | |||
| } | |||
| @ -0,0 +1,85 @@ | |||
| { | |||
| "id": "SY-StackedCarousel", | |||
| "displayName": "堆叠轮播图、3D轮播图", | |||
| "version": "1.0.0", | |||
| "description": "堆叠(层叠)式轮播图组件,3D轮播图,微信小程序通用。让您不再为特殊轮播需求而烦恼。", | |||
| "keywords": [ | |||
| "堆叠轮播图", | |||
| "层叠轮播图", | |||
| "3D轮播图", | |||
| "轮播图", | |||
| "轮播" | |||
| ], | |||
| "repository": "", | |||
| "engines": { | |||
| "HBuilderX": "^3.7.12" | |||
| }, | |||
| "dcloudext": { | |||
| "type": "component-vue", | |||
| "sale": { | |||
| "regular": { | |||
| "price": "0.00" | |||
| }, | |||
| "sourcecode": { | |||
| "price": "0.00" | |||
| } | |||
| }, | |||
| "contact": { | |||
| "qq": "" | |||
| }, | |||
| "declaration": { | |||
| "ads": "无", | |||
| "data": "无", | |||
| "permissions": "无" | |||
| }, | |||
| "npmurl": "" | |||
| }, | |||
| "uni_modules": { | |||
| "dependencies": [], | |||
| "encrypt": [], | |||
| "platforms": { | |||
| "cloud": { | |||
| "tcb": "y", | |||
| "aliyun": "y" | |||
| }, | |||
| "client": { | |||
| "Vue": { | |||
| "vue2": "y", | |||
| "vue3": "y" | |||
| }, | |||
| "App": { | |||
| "app-vue": "u", | |||
| "app-nvue": "u" | |||
| }, | |||
| "H5-mobile": { | |||
| "Safari": "u", | |||
| "Android Browser": "u", | |||
| "微信浏览器(Android)": "u", | |||
| "QQ浏览器(Android)": "u" | |||
| }, | |||
| "H5-pc": { | |||
| "Chrome": "y", | |||
| "IE": "y", | |||
| "Edge": "y", | |||
| "Firefox": "u", | |||
| "Safari": "u" | |||
| }, | |||
| "小程序": { | |||
| "微信": "y", | |||
| "阿里": "u", | |||
| "百度": "u", | |||
| "字节跳动": "u", | |||
| "QQ": "u", | |||
| "钉钉": "u", | |||
| "快手": "u", | |||
| "飞书": "u", | |||
| "京东": "u" | |||
| }, | |||
| "快应用": { | |||
| "华为": "u", | |||
| "联盟": "u" | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @ -0,0 +1,117 @@ | |||
| ## SY-StackedCarousel | |||
| 该组件一般用于导航轮播,广告展示等场景,可开箱即用,具有如下特点: | |||
| - 3D 轮播图效果,满足不同的开发需求 | |||
| - 可配置显示标题,涵盖不同的应用场景 | |||
| - 功能属性齐全丰富 | |||
| - 轻量级,支持小程序、H5 等平台(支持 vue2、vue3) | |||
| ### 使用方式 | |||
| ```html | |||
| <template> | |||
| <view class="container"> | |||
| <SYStackedCarousel | |||
| height="480rpx" | |||
| autoplay | |||
| :images="images" | |||
| :current="3" | |||
| :interval="2000" | |||
| @click="clickHandler" | |||
| @change="changeHandler" | |||
| > | |||
| </SYStackedCarousel> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import SYStackedCarousel from "@/uni_modules/SY-StackedCarousel/components/SY-StackedCarousel.vue"; | |||
| export default { | |||
| components: { | |||
| SYStackedCarousel, | |||
| }, | |||
| data() { | |||
| return { | |||
| images: [ | |||
| { | |||
| url: "../../static/images/1.jpg", | |||
| desc: "星辰大海任我行,风雨兼程志更坚。星辰大海任我行,风雨兼程志更坚。星辰大海任我行,风雨兼程志更坚。星辰大海任我行,风雨兼程志更坚。", | |||
| }, | |||
| { | |||
| url: "../../static/images/2.jpg", | |||
| desc: "晨曦微露破晓时,梦想起航正当时。", | |||
| }, | |||
| { | |||
| url: "../../static/images/3.jpg", | |||
| desc: "千锤百炼钢更强,逆境之中显真章。", | |||
| }, | |||
| { | |||
| url: "../../static/images/4.jpg", | |||
| desc: "心有猛虎细嗅蔷薇,勇者无畏亦柔情。", | |||
| }, | |||
| { | |||
| url: "../../static/images/5.jpg", | |||
| desc: "高山仰止景行行,志士仁人共长天。", | |||
| }, | |||
| ], | |||
| }; | |||
| }, | |||
| onLoad() {}, | |||
| methods: { | |||
| clickHandler(item, index) { | |||
| console.log("item: ", item); | |||
| console.log("index: ", index); | |||
| }, | |||
| changeHandler(index) { | |||
| console.log("当前触发change事件,返回索引: ", index); | |||
| }, | |||
| }, | |||
| }; | |||
| </script> | |||
| ``` | |||
| ### 属性说明 | |||
| | 属性名 | 类型 | 默认值 | 说明 | | |||
| | :-----------------| :---------------| :------------------ | :--------------------------------------------------------- | | |||
| | images | Array | [] | 图片数组 | | |||
| | height | [String, Number]| 300rpx | 轮播图高度 | | |||
| | autoplay | Boolean | true | 是否自动播放 | | |||
| | padding | String | 10px | 轮播内边距 | | |||
| | radius | [String, Number]| 4 | 组件圆角值 | | |||
| | bgColor | String | #ffffff | 容器背景色 | | |||
| | interval | Number | 2000 | 滑块自动切换时间间隔(ms) | | |||
| | current | Number | 0 | 当前显示的图片索引 | | |||
| | easing | String | ease-in-out | 动画类型 | | |||
| | enableOpacity | Boolean | true | 是否启用不透明度设定(堆叠每一层逐渐变透明) | | |||
| | baseOpacity | Number | 0.8 | 最顶层图片的不透明度,每一层会根据这个值自动缩小 opacity值 | | |||
| | duration | Number | 500 | 滑块切换过程所需时间(ms) | | |||
| | horizontalMargin | [String, Number]| 10 | 横向间距 | | |||
| | verticalMargin | [String, Number]| 10 | 纵向间距 | | |||
| | showButton | Boolean | false | 是否显示操作按钮 | | |||
| | buttonSize | [String, Number]| 24 | 按钮大小以及按钮图标大小 | | |||
| | buttonBgColor | String | rgba(0, 0, 0, 0.26) | 按钮背景颜色 | | |||
| | imgMode | String | aspectFill | 图片裁剪模式,详情见微信原生 imageMode | | |||
| | showFirstImageOnly| Boolean | false | 是否只显示第一张图片,其他图片被蒙版遮住 | | |||
| | maskBgColor | String | #ffffff | 遮住蒙版的颜色 | | |||
| | showDesc | Boolean | true | 是否显示图片描述(需要 images 传递的数据中存在 desc 属性) | | |||
| | descBgColor | String | rgba(0, 0, 0, 0.5) | 底部描述的背景颜色 | | |||
| | descColor | String | #ffffff | 底部描述字体颜色 | | |||
| | descSize | [String, Number]| 10 | 底部描述字体大小 | | |||
| | descNoWrap | Boolean | false | 描述是否不换行,超出后用...省略 | | |||
| | slideRadius | [String, Number]| 10 | 图片的圆角值 | | |||
| ### 事件说明 | |||
| | 事件名 | 说明 | 回调参数 | | |||
| | :----- | :----------------------------------- | :--------------------------------------------------------- | | |||
| | click | 点击图片触发 | item:当前图片对象信息, index:当前是第几张图片,从 0 开始 | | |||
| | change | 轮播图切换时触发(自动或者手动切换) | index:当前是第几张图片,从 0 开始 | | |||
| ### 插槽 | |||
| | 插槽名称 | 说明 | | |||
| | :----- | :-----------------------------------| | |||
| | left-button | 翻页按钮左侧图标 | | |||
| | right-button| 翻页按钮右侧图标 | | |||