| @ -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| 翻页按钮右侧图标 | | |||||