364 changed files with 57803 additions and 0 deletions
@ -0,0 +1 @@ |
|||||
|
/unpackage |
||||
@ -0,0 +1,75 @@ |
|||||
|
<script> |
||||
|
export default { |
||||
|
|
||||
|
/** |
||||
|
* 全局变量 |
||||
|
*/ |
||||
|
globalData: { |
||||
|
|
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 初始化完成时触发 |
||||
|
*/ |
||||
|
onLaunch() { |
||||
|
// 小程序主动更新 |
||||
|
this.updateManager() |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
|
||||
|
/** |
||||
|
* 小程序主动更新 |
||||
|
*/ |
||||
|
updateManager() { |
||||
|
const updateManager = uni.getUpdateManager(); |
||||
|
updateManager.onCheckForUpdate(res => { |
||||
|
// 请求完新版本信息的回调 |
||||
|
// console.log(res.hasUpdate) |
||||
|
}) |
||||
|
updateManager.onUpdateReady(() => { |
||||
|
uni.showModal({ |
||||
|
title: '更新提示', |
||||
|
content: '新版本已经准备好,即将重启应用', |
||||
|
showCancel: false, |
||||
|
success(res) { |
||||
|
if (res.confirm) { |
||||
|
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启 |
||||
|
updateManager.applyUpdate() |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
updateManager.onUpdateFailed(() => { |
||||
|
// 新的版本下载失败 |
||||
|
uni.showModal({ |
||||
|
title: '更新提示', |
||||
|
content: '新版本下载失败', |
||||
|
showCancel: false |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss"> |
||||
|
/* 引入uView库样式 */ |
||||
|
@import "uview-ui/index.scss"; |
||||
|
</style> |
||||
|
|
||||
|
<style> |
||||
|
/* 项目基础样式 */ |
||||
|
@import "./app.scss"; |
||||
|
|
||||
|
.uni-app--showlayout+uni-tabbar.uni-tabbar-bottom, |
||||
|
.uni-app--showlayout+uni-tabbar.uni-tabbar-bottom .uni-tabbar, |
||||
|
.uni-app--showlayout+uni-tabbar.uni-tabbar-top, |
||||
|
.uni-app--showlayout+uni-tabbar.uni-tabbar-top .uni-tabbar { |
||||
|
left: var(--window-left); |
||||
|
right: var(--window-right); |
||||
|
} |
||||
|
|
||||
|
</style> |
||||
@ -0,0 +1,191 @@ |
|||||
|
Apache License |
||||
|
Version 2.0, January 2004 |
||||
|
http://www.apache.org/licenses/ |
||||
|
|
||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
||||
|
|
||||
|
1. Definitions. |
||||
|
|
||||
|
"License" shall mean the terms and conditions for use, reproduction, and |
||||
|
distribution as defined by Sections 1 through 9 of this document. |
||||
|
|
||||
|
"Licensor" shall mean the copyright owner or entity authorized by the copyright |
||||
|
owner that is granting the License. |
||||
|
|
||||
|
"Legal Entity" shall mean the union of the acting entity and all other entities |
||||
|
that control, are controlled by, or are under common control with that entity. |
||||
|
For the purposes of this definition, "control" means (i) the power, direct or |
||||
|
indirect, to cause the direction or management of such entity, whether by |
||||
|
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the |
||||
|
outstanding shares, or (iii) beneficial ownership of such entity. |
||||
|
|
||||
|
"You" (or "Your") shall mean an individual or Legal Entity exercising |
||||
|
permissions granted by this License. |
||||
|
|
||||
|
"Source" form shall mean the preferred form for making modifications, including |
||||
|
but not limited to software source code, documentation source, and configuration |
||||
|
files. |
||||
|
|
||||
|
"Object" form shall mean any form resulting from mechanical transformation or |
||||
|
translation of a Source form, including but not limited to compiled object code, |
||||
|
generated documentation, and conversions to other media types. |
||||
|
|
||||
|
"Work" shall mean the work of authorship, whether in Source or Object form, made |
||||
|
available under the License, as indicated by a copyright notice that is included |
||||
|
in or attached to the work (an example is provided in the Appendix below). |
||||
|
|
||||
|
"Derivative Works" shall mean any work, whether in Source or Object form, that |
||||
|
is based on (or derived from) the Work and for which the editorial revisions, |
||||
|
annotations, elaborations, or other modifications represent, as a whole, an |
||||
|
original work of authorship. For the purposes of this License, Derivative Works |
||||
|
shall not include works that remain separable from, or merely link (or bind by |
||||
|
name) to the interfaces of, the Work and Derivative Works thereof. |
||||
|
|
||||
|
"Contribution" shall mean any work of authorship, including the original version |
||||
|
of the Work and any modifications or additions to that Work or Derivative Works |
||||
|
thereof, that is intentionally submitted to Licensor for inclusion in the Work |
||||
|
by the copyright owner or by an individual or Legal Entity authorized to submit |
||||
|
on behalf of the copyright owner. For the purposes of this definition, |
||||
|
"submitted" means any form of electronic, verbal, or written communication sent |
||||
|
to the Licensor or its representatives, including but not limited to |
||||
|
communication on electronic mailing lists, source code control systems, and |
||||
|
issue tracking systems that are managed by, or on behalf of, the Licensor for |
||||
|
the purpose of discussing and improving the Work, but excluding communication |
||||
|
that is conspicuously marked or otherwise designated in writing by the copyright |
||||
|
owner as "Not a Contribution." |
||||
|
|
||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf |
||||
|
of whom a Contribution has been received by Licensor and subsequently |
||||
|
incorporated within the Work. |
||||
|
|
||||
|
2. Grant of Copyright License. |
||||
|
|
||||
|
Subject to the terms and conditions of this License, each Contributor hereby |
||||
|
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, |
||||
|
irrevocable copyright license to reproduce, prepare Derivative Works of, |
||||
|
publicly display, publicly perform, sublicense, and distribute the Work and such |
||||
|
Derivative Works in Source or Object form. |
||||
|
|
||||
|
3. Grant of Patent License. |
||||
|
|
||||
|
Subject to the terms and conditions of this License, each Contributor hereby |
||||
|
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, |
||||
|
irrevocable (except as stated in this section) patent license to make, have |
||||
|
made, use, offer to sell, sell, import, and otherwise transfer the Work, where |
||||
|
such license applies only to those patent claims licensable by such Contributor |
||||
|
that are necessarily infringed by their Contribution(s) alone or by combination |
||||
|
of their Contribution(s) with the Work to which such Contribution(s) was |
||||
|
submitted. If You institute patent litigation against any entity (including a |
||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work or a |
||||
|
Contribution incorporated within the Work constitutes direct or contributory |
||||
|
patent infringement, then any patent licenses granted to You under this License |
||||
|
for that Work shall terminate as of the date such litigation is filed. |
||||
|
|
||||
|
4. Redistribution. |
||||
|
|
||||
|
You may reproduce and distribute copies of the Work or Derivative Works thereof |
||||
|
in any medium, with or without modifications, and in Source or Object form, |
||||
|
provided that You meet the following conditions: |
||||
|
|
||||
|
You must give any other recipients of the Work or Derivative Works a copy of |
||||
|
this License; and |
||||
|
You must cause any modified files to carry prominent notices stating that You |
||||
|
changed the files; and |
||||
|
You must retain, in the Source form of any Derivative Works that You distribute, |
||||
|
all copyright, patent, trademark, and attribution notices from the Source form |
||||
|
of the Work, excluding those notices that do not pertain to any part of the |
||||
|
Derivative Works; and |
||||
|
If the Work includes a "NOTICE" text file as part of its distribution, then any |
||||
|
Derivative Works that You distribute must include a readable copy of the |
||||
|
attribution notices contained within such NOTICE file, excluding those notices |
||||
|
that do not pertain to any part of the Derivative Works, in at least one of the |
||||
|
following places: within a NOTICE text file distributed as part of the |
||||
|
Derivative Works; within the Source form or documentation, if provided along |
||||
|
with the Derivative Works; or, within a display generated by the Derivative |
||||
|
Works, if and wherever such third-party notices normally appear. The contents of |
||||
|
the NOTICE file are for informational purposes only and do not modify the |
||||
|
License. You may add Your own attribution notices within Derivative Works that |
||||
|
You distribute, alongside or as an addendum to the NOTICE text from the Work, |
||||
|
provided that such additional attribution notices cannot be construed as |
||||
|
modifying the License. |
||||
|
You may add Your own copyright statement to Your modifications and may provide |
||||
|
additional or different license terms and conditions for use, reproduction, or |
||||
|
distribution of Your modifications, or for any such Derivative Works as a whole, |
||||
|
provided Your use, reproduction, and distribution of the Work otherwise complies |
||||
|
with the conditions stated in this License. |
||||
|
|
||||
|
5. Submission of Contributions. |
||||
|
|
||||
|
Unless You explicitly state otherwise, any Contribution intentionally submitted |
||||
|
for inclusion in the Work by You to the Licensor shall be under the terms and |
||||
|
conditions of this License, without any additional terms or conditions. |
||||
|
Notwithstanding the above, nothing herein shall supersede or modify the terms of |
||||
|
any separate license agreement you may have executed with Licensor regarding |
||||
|
such Contributions. |
||||
|
|
||||
|
6. Trademarks. |
||||
|
|
||||
|
This License does not grant permission to use the trade names, trademarks, |
||||
|
service marks, or product names of the Licensor, except as required for |
||||
|
reasonable and customary use in describing the origin of the Work and |
||||
|
reproducing the content of the NOTICE file. |
||||
|
|
||||
|
7. Disclaimer of Warranty. |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, Licensor provides the |
||||
|
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, |
||||
|
including, without limitation, any warranties or conditions of TITLE, |
||||
|
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are |
||||
|
solely responsible for determining the appropriateness of using or |
||||
|
redistributing the Work and assume any risks associated with Your exercise of |
||||
|
permissions under this License. |
||||
|
|
||||
|
8. Limitation of Liability. |
||||
|
|
||||
|
In no event and under no legal theory, whether in tort (including negligence), |
||||
|
contract, or otherwise, unless required by applicable law (such as deliberate |
||||
|
and grossly negligent acts) or agreed to in writing, shall any Contributor be |
||||
|
liable to You for damages, including any direct, indirect, special, incidental, |
||||
|
or consequential damages of any character arising as a result of this License or |
||||
|
out of the use or inability to use the Work (including but not limited to |
||||
|
damages for loss of goodwill, work stoppage, computer failure or malfunction, or |
||||
|
any and all other commercial damages or losses), even if such Contributor has |
||||
|
been advised of the possibility of such damages. |
||||
|
|
||||
|
9. Accepting Warranty or Additional Liability. |
||||
|
|
||||
|
While redistributing the Work or Derivative Works thereof, You may choose to |
||||
|
offer, and charge a fee for, acceptance of support, warranty, indemnity, or |
||||
|
other liability obligations and/or rights consistent with this License. However, |
||||
|
in accepting such obligations, You may act only on Your own behalf and on Your |
||||
|
sole responsibility, not on behalf of any other Contributor, and only if You |
||||
|
agree to indemnify, defend, and hold each Contributor harmless for any liability |
||||
|
incurred by, or claims asserted against, such Contributor by reason of your |
||||
|
accepting any such warranty or additional liability. |
||||
|
|
||||
|
END OF TERMS AND CONDITIONS |
||||
|
|
||||
|
APPENDIX: How to apply the Apache License to your work |
||||
|
|
||||
|
To apply the Apache License to your work, attach the following boilerplate |
||||
|
notice, with the fields enclosed by brackets "{}" replaced with your own |
||||
|
identifying information. (Don't include the brackets!) The text should be |
||||
|
enclosed in the appropriate comment syntax for the file format. We also |
||||
|
recommend that a file or class name and description of purpose be included on |
||||
|
the same "printed page" as the copyright notice for easier identification within |
||||
|
third-party archives. |
||||
|
|
||||
|
Copyright 2018 萤火科技 |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
@ -0,0 +1,29 @@ |
|||||
|
# 文化云 [uni-app端] |
||||
|
|
||||
|
#### 如何使用uni-app端 |
||||
|
|
||||
|
##### 一、导入uniapp项目 |
||||
|
|
||||
|
1. 首先下载HBuilderX并安装,地址:https://www.dcloud.io/hbuilderx.html |
||||
|
2. 打开HBuilderX -> 顶部菜单栏 -> 文件 -> 导入 -> 从本地目录导入 -> 选择uniapp端项目目录 |
||||
|
3. 找到config.js文件,找到里面的apiUrl项,填入已搭建的后端url地址 |
||||
|
4. 打开manifest.json文件,选择微信小程序配置,填写小程序的appid |
||||
|
|
||||
|
##### 二、本地调试 |
||||
|
|
||||
|
1. 打开HBuilderX -> 顶部菜单栏 -> 运行 -> 运行到浏览器 -> Chrome |
||||
|
2. 如果请求后端api时 提示跨域错误,可安装Chrome插件:【Allow CORS: Access-Control-Allow-Origin】,地址:https://chrome.google.com/webstore/detail/allow-cors-access-control/lhobafahddgcelffkeicbaginigeejlf |
||||
|
|
||||
|
##### 三、打包发行(H5) |
||||
|
|
||||
|
1. 打开HBuilderX -> 顶部菜单栏 -> 发行 -> 网站H5-手机版 |
||||
|
2. 打包后的文件路径:/unpackage/dist/build/h5 |
||||
|
3. 将打包完成的所有文件 复制到商城后端/pulic目录下,全部替换 |
||||
|
|
||||
|
##### 四、打包发行(微信小程序) |
||||
|
|
||||
|
1. 下载微信开发者工具并安装,地址:https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html |
||||
|
2. 打开HBuilderX -> 顶部菜单栏 -> 发行 -> 小程序-微信 |
||||
|
3. 打包后的文件路径:/unpackage/dist/build/mp-weixin |
||||
|
5. 打开微信开发者工具 导入 打包完成的项目 |
||||
|
6. 检查没有运行错误,在右上方上传小程序 |
||||
@ -0,0 +1,14 @@ |
|||||
|
{ |
||||
|
"version" : "1", |
||||
|
"prompt" : "template", |
||||
|
"title" : "服务协议和隐私政策", |
||||
|
"message" : " 请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/> 你可阅读<a href=\"static/protocol.html\">《服务协议》</a>和<a href=\"static/privacy.html\">《隐私政策》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。", |
||||
|
"buttonAccept" : "同意并接受", |
||||
|
"buttonRefuse" : "暂不同意", |
||||
|
"second" : { |
||||
|
"title" : "确认提示", |
||||
|
"message" : " 进入应用前,你需先同意<a href=\"static/protocol.html\">《服务协议》</a>和<a href=\"static/privacy.html\">《隐私政策》</a>,否则将退出应用。", |
||||
|
"buttonAccept" : "同意并继续", |
||||
|
"buttonRefuse" : "退出应用" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
list: 'address/list', |
||||
|
defaultId: 'address/defaultId', |
||||
|
detail: 'address/detail', |
||||
|
add: 'address/add', |
||||
|
edit: 'address/edit', |
||||
|
setDefault: 'address/setDefault', |
||||
|
remove: 'address/remove' |
||||
|
} |
||||
|
|
||||
|
// 收货地址列表
|
||||
|
export const list = (param) => { |
||||
|
return request.get(api.list, param) |
||||
|
} |
||||
|
|
||||
|
// 默认收货地址ID
|
||||
|
export const defaultId = (param) => { |
||||
|
return request.get(api.defaultId, param) |
||||
|
} |
||||
|
|
||||
|
// 收货地址详情
|
||||
|
export const detail = (addressId) => { |
||||
|
return request.get(api.detail, { addressId }) |
||||
|
} |
||||
|
|
||||
|
// 新增收货地址
|
||||
|
export const add = (data) => { |
||||
|
return request.post(api.add, { form: data }) |
||||
|
} |
||||
|
|
||||
|
// 编辑收货地址
|
||||
|
export const edit = (addressId, data) => { |
||||
|
return request.post(api.edit, { addressId, form: data }) |
||||
|
} |
||||
|
|
||||
|
// 设置默认收货地址
|
||||
|
export const setDefault = (addressId) => { |
||||
|
return request.post(api.setDefault, { addressId }) |
||||
|
} |
||||
|
|
||||
|
// 删除收货地址
|
||||
|
export const remove = (addressId) => { |
||||
|
return request.post(api.remove, { addressId }) |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
list: 'article.category/list' |
||||
|
} |
||||
|
|
||||
|
// 页面数据
|
||||
|
export function list() { |
||||
|
return request.get(api.list) |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
list: 'article/list', |
||||
|
detail: 'article/detail' |
||||
|
} |
||||
|
|
||||
|
// 文章列表
|
||||
|
export function list(param, option) { |
||||
|
return request.get(api.list, param, option) |
||||
|
} |
||||
|
|
||||
|
// 文章详情
|
||||
|
export function detail(articleId) { |
||||
|
return request.get(api.detail, { articleId }) |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
list: 'balance.log/list' |
||||
|
} |
||||
|
|
||||
|
// 余额账单明细列表
|
||||
|
export const list = (param) => { |
||||
|
return request.get(api.list, param) |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
list: 'balance.log/list' |
||||
|
} |
||||
|
|
||||
|
// 余额账单明细
|
||||
|
export const list = (param) => { |
||||
|
return request.get(api.list, param) |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
image: 'captcha/image', |
||||
|
sendSmsCaptcha: 'captcha/sendSmsCaptcha' |
||||
|
} |
||||
|
|
||||
|
// 图形验证码
|
||||
|
export function image() { |
||||
|
return request.get(api.image, {}, { load: false }) |
||||
|
} |
||||
|
|
||||
|
// 发送短信验证码
|
||||
|
export function sendSmsCaptcha(data) { |
||||
|
return request.post(api.sendSmsCaptcha, data, { load: false }) |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
list: 'cart/list', |
||||
|
total: 'cart/total', |
||||
|
add: 'cart/add', |
||||
|
update: 'cart/update', |
||||
|
clear: 'cart/clear' |
||||
|
} |
||||
|
|
||||
|
// 购物车列表
|
||||
|
export const list = () => { |
||||
|
return request.get(api.list, {}, { load: false }) |
||||
|
} |
||||
|
|
||||
|
// 购物车商品总数量
|
||||
|
export const total = () => { |
||||
|
return request.get(api.total, {}, { load: false }) |
||||
|
} |
||||
|
|
||||
|
// 加入购物车
|
||||
|
export const add = (goodsId, goodsSkuId, goodsNum) => { |
||||
|
return request.post(api.add, { goodsId, goodsSkuId, goodsNum }) |
||||
|
} |
||||
|
|
||||
|
// 更新购物车商品数量
|
||||
|
export const update = (goodsId, goodsSkuId, goodsNum) => { |
||||
|
return request.post(api.update, { goodsId, goodsSkuId, goodsNum }, { isPrompt: false }) |
||||
|
} |
||||
|
|
||||
|
// 删除购物车中指定记录
|
||||
|
export const clear = (cartIds = []) => { |
||||
|
return request.post(api.clear, { cartIds }) |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
list: 'category/list' |
||||
|
} |
||||
|
|
||||
|
// 页面数据
|
||||
|
export function list() { |
||||
|
return request.get(api.list) |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
order: 'checkout/order', |
||||
|
submit: 'checkout/submit', |
||||
|
} |
||||
|
|
||||
|
// mode: 结算模式 (buyNow立即购买 cart购物车)
|
||||
|
|
||||
|
// 结算台订单信息
|
||||
|
export const order = (mode, param) => { |
||||
|
return request.get(api.order, { mode, ...param }) |
||||
|
} |
||||
|
|
||||
|
// 结算台订单提交
|
||||
|
export const submit = (mode, data) => { |
||||
|
return request.post(api.submit, { mode, ...data }) |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
list: 'comment/list', |
||||
|
listRows: 'comment/listRows', |
||||
|
total: 'comment/total' |
||||
|
} |
||||
|
|
||||
|
// 商品评价列表
|
||||
|
export const list = (goodsId, param, option) => { |
||||
|
return request.get(api.list, { ...param, goodsId }, option) |
||||
|
} |
||||
|
|
||||
|
// 商品评价列表 (限制数量, 用于商品详情页展示)
|
||||
|
export const listRows = (goodsId, limit = 5) => { |
||||
|
return request.get(api.listRows, { goodsId, limit }) |
||||
|
} |
||||
|
|
||||
|
// 商品评分总数
|
||||
|
export const total = (goodsId) => { |
||||
|
return request.get(api.total, { goodsId }) |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
list: 'coupon/list' |
||||
|
} |
||||
|
|
||||
|
// 优惠券列表
|
||||
|
export const list = (param, option) => { |
||||
|
const options = { |
||||
|
isPrompt: true, //(默认 true 说明:本接口抛出的错误是否提示)
|
||||
|
load: true, //(默认 true 说明:本接口是否提示加载动画)
|
||||
|
...option |
||||
|
} |
||||
|
return request.get(api.list, param, options) |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
list: 'express/list' |
||||
|
} |
||||
|
|
||||
|
// 物流公司列表
|
||||
|
export const list = (param) => { |
||||
|
return request.get(api.list, param) |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
list: 'goods/list', |
||||
|
detail: 'goods/detail', |
||||
|
specData: 'goods/specData' |
||||
|
} |
||||
|
|
||||
|
// 商品列表
|
||||
|
export const list = param => { |
||||
|
return request.get(api.list, param) |
||||
|
} |
||||
|
|
||||
|
// 商品详情
|
||||
|
export const detail = goodsId => { |
||||
|
return request.get(api.detail, { goodsId }) |
||||
|
} |
||||
|
|
||||
|
// 获取商品规格数据
|
||||
|
export const specData = (goodsId) => { |
||||
|
return request.get(api.specData, { goodsId }) |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
list: 'goods.service/list' |
||||
|
} |
||||
|
|
||||
|
// 商品评价列表
|
||||
|
export function list(goodsId) { |
||||
|
return request.get(api.list, { goodsId }) |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
list: 'help/list' |
||||
|
} |
||||
|
|
||||
|
// 帮助中心列表
|
||||
|
export const list = (param) => { |
||||
|
return request.get(api.list, param) |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
login: 'passport/login', |
||||
|
loginMpWx: 'passport/loginMpWx', |
||||
|
loginMpWxMobile: 'passport/loginMpWxMobile', |
||||
|
} |
||||
|
|
||||
|
// 用户登录(手机号+验证码)
|
||||
|
export function login(data) { |
||||
|
return request.post(api.login, data) |
||||
|
} |
||||
|
|
||||
|
// 微信小程序快捷登录(获取微信用户基本信息)
|
||||
|
export function loginMpWx(data, option) { |
||||
|
return request.post(api.loginMpWx, data, option) |
||||
|
} |
||||
|
|
||||
|
// 微信小程序快捷登录(授权手机号)
|
||||
|
export function loginMpWxMobile(data, option) { |
||||
|
return request.post(api.loginMpWxMobile, data, option) |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
list: 'myCoupon/list', |
||||
|
receive: 'myCoupon/receive' |
||||
|
} |
||||
|
|
||||
|
// 我的优惠券列表
|
||||
|
export const list = (param, option) => { |
||||
|
const options = { |
||||
|
isPrompt: true, //(默认 true 说明:本接口抛出的错误是否提示)
|
||||
|
load: true, //(默认 true 说明:本接口是否提示加载动画)
|
||||
|
...option |
||||
|
} |
||||
|
return request.get(api.list, param, options) |
||||
|
} |
||||
|
|
||||
|
// 领取优惠券
|
||||
|
export const receive = (couponId, data) => { |
||||
|
return request.post(api.receive, { couponId, ...couponId, data }) |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
todoCounts: 'order/todoCounts', |
||||
|
list: 'order/list', |
||||
|
detail: 'order/detail', |
||||
|
express: 'order/express', |
||||
|
cancel: 'order/cancel', |
||||
|
receipt: 'order/receipt', |
||||
|
pay: 'order/pay' |
||||
|
} |
||||
|
|
||||
|
// 当前用户待处理的订单数量
|
||||
|
export function todoCounts(param, option) { |
||||
|
return request.get(api.todoCounts, param, option) |
||||
|
} |
||||
|
|
||||
|
// 我的订单列表
|
||||
|
export function list(param, option) { |
||||
|
return request.get(api.list, param, option) |
||||
|
} |
||||
|
|
||||
|
// 订单详情
|
||||
|
export function detail(orderId, param) { |
||||
|
return request.get(api.detail, { orderId, ...param }) |
||||
|
} |
||||
|
|
||||
|
// 获取物流信息
|
||||
|
export function express(orderId, param) { |
||||
|
return request.get(api.express, { orderId, ...param }) |
||||
|
} |
||||
|
|
||||
|
// 取消订单
|
||||
|
export function cancel(orderId, data) { |
||||
|
return request.post(api.cancel, { orderId, ...data }) |
||||
|
} |
||||
|
|
||||
|
// 确认收货
|
||||
|
export function receipt(orderId, data) { |
||||
|
return request.post(api.receipt, { orderId, ...data }) |
||||
|
} |
||||
|
|
||||
|
// 立即支付
|
||||
|
export function pay(orderId, payType, param) { |
||||
|
return request.get(api.pay, { orderId, payType, ...param }) |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
list: 'order.comment/list', |
||||
|
submit: 'order.comment/submit' |
||||
|
} |
||||
|
|
||||
|
// 待评价订单商品列表
|
||||
|
export const list = (orderId, param) => { |
||||
|
return request.get(api.list, { orderId, ...param }) |
||||
|
} |
||||
|
|
||||
|
// 创建商品评价
|
||||
|
export const submit = (orderId, data) => { |
||||
|
return request.post(api.submit, { orderId, form: data }) |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const apiUri = { |
||||
|
detail: 'page/detail' |
||||
|
} |
||||
|
|
||||
|
// 页面数据
|
||||
|
export function detail(pageId) { |
||||
|
return request.get(apiUri.detail, { |
||||
|
pageId |
||||
|
}) |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
list: 'points.log/list' |
||||
|
} |
||||
|
|
||||
|
// 积分明细列表
|
||||
|
export const list = (param) => { |
||||
|
return request.get(api.list, param) |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
submit: 'recharge/submit' |
||||
|
} |
||||
|
|
||||
|
// 积分明细列表
|
||||
|
export const submit = (data) => { |
||||
|
return request.post(api.submit, data) |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
list: 'recharge.order/list' |
||||
|
} |
||||
|
|
||||
|
// 我的充值记录列表
|
||||
|
export const list = (param) => { |
||||
|
return request.get(api.list, param) |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
list: 'recharge.plan/list' |
||||
|
} |
||||
|
|
||||
|
// 充值套餐列表
|
||||
|
export const list = (param) => { |
||||
|
return request.get(api.list, param) |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
list: 'refund/list', |
||||
|
goods: 'refund/goods', |
||||
|
apply: 'refund/apply', |
||||
|
detail: 'refund/detail', |
||||
|
delivery: 'refund/delivery' |
||||
|
} |
||||
|
|
||||
|
// 售后单列表
|
||||
|
export const list = (param, option) => { |
||||
|
return request.get(api.list, param, option) |
||||
|
} |
||||
|
|
||||
|
// 订单商品详情
|
||||
|
export const goods = (orderGoodsId, param) => { |
||||
|
return request.get(api.goods, { orderGoodsId, ...param }) |
||||
|
} |
||||
|
|
||||
|
// 申请售后
|
||||
|
export const apply = (orderGoodsId, data) => { |
||||
|
return request.post(api.apply, { orderGoodsId, form: data }) |
||||
|
} |
||||
|
|
||||
|
// 售后单详情
|
||||
|
export const detail = (orderRefundId, param) => { |
||||
|
return request.get(api.detail, { orderRefundId, ...param }) |
||||
|
} |
||||
|
|
||||
|
// 用户发货
|
||||
|
export const delivery = (orderRefundId, data) => { |
||||
|
return request.post(api.delivery, { orderRefundId, form: data }) |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
all: 'region/all', |
||||
|
tree: 'region/tree' |
||||
|
} |
||||
|
|
||||
|
// 获取所有地区
|
||||
|
export const all = (param) => { |
||||
|
return request.get(api.all, param) |
||||
|
} |
||||
|
|
||||
|
// 获取所有地区(树状)
|
||||
|
export const tree = (param) => { |
||||
|
return request.get(api.tree, param) |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
data: 'setting/data' |
||||
|
} |
||||
|
|
||||
|
// 设置项详情
|
||||
|
export function data() { |
||||
|
return request.get(api.data) |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
image: 'upload/image' |
||||
|
} |
||||
|
|
||||
|
// 图片上传
|
||||
|
export const image = files => { |
||||
|
// 文件上传大小, 2M
|
||||
|
const maxSize = 1024 * 1024 * 2 |
||||
|
// 执行上传
|
||||
|
return new Promise((resolve, reject) => { |
||||
|
request.urlFileUpload({ files, maxSize }) |
||||
|
.then(result => resolve(result.map(item => item.data.fileInfo.file_id), result)) |
||||
|
.catch(reject) |
||||
|
}) |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
userInfo: 'user/info', |
||||
|
assets: 'user/assets', |
||||
|
bindMobile: 'user/bindMobile', |
||||
|
personal: 'user/personal' |
||||
|
} |
||||
|
|
||||
|
// 当前登录的用户信息
|
||||
|
export const info = (param, option) => { |
||||
|
const options = { |
||||
|
isPrompt: true, //(默认 true 说明:本接口抛出的错误是否提示)
|
||||
|
load: true, //(默认 true 说明:本接口是否提示加载动画)
|
||||
|
...option |
||||
|
} |
||||
|
return request.get(api.userInfo, param, options) |
||||
|
} |
||||
|
|
||||
|
// 账户资产
|
||||
|
export const assets = (param, option) => { |
||||
|
return request.get(api.assets, param, option) |
||||
|
} |
||||
|
|
||||
|
// 绑定手机号
|
||||
|
export const bindMobile = (data, option) => { |
||||
|
return request.post(api.bindMobile, data, option) |
||||
|
} |
||||
|
|
||||
|
// 修改个人信息(头像昵称)
|
||||
|
export const personal = (data, option) => { |
||||
|
return request.post(api.personal, data, option) |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// api地址
|
||||
|
const api = { |
||||
|
receive: 'user.coupon/receive' |
||||
|
} |
||||
|
|
||||
|
// 优惠券列表
|
||||
|
export const receive = (data) => { |
||||
|
return request.post(api.receive, data) |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
/* utils.scss */ |
||||
|
@import "/utils/utils.scss"; |
||||
|
|
||||
|
page { |
||||
|
background: #fafafa; |
||||
|
} |
||||
|
|
||||
|
@-webkit-keyframes rotate { |
||||
|
0% { |
||||
|
transform: rotate(0deg) scale(1); |
||||
|
} |
||||
|
|
||||
|
100% { |
||||
|
transform: rotate(360deg) scale(1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@keyframes rotate { |
||||
|
0% { |
||||
|
transform: rotate(0deg) scale(1); |
||||
|
} |
||||
|
|
||||
|
100% { |
||||
|
transform: rotate(360deg) scale(1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* #ifdef H5*/ |
||||
|
|
||||
|
uni-page { |
||||
|
box-shadow: 0 1px 22px rgb(169, 169, 169, .3); |
||||
|
} |
||||
|
|
||||
|
.footer-fixed { |
||||
|
left: var(--window-left) !important; |
||||
|
right: var(--window-right) !important; |
||||
|
} |
||||
|
|
||||
|
.u-mask,.u-drawer { |
||||
|
left: var(--window-left) !important; |
||||
|
right: var(--window-right) !important; |
||||
|
} |
||||
|
|
||||
|
/* #endif */ |
||||
@ -0,0 +1,3 @@ |
|||||
|
import paginate from './paginate' |
||||
|
|
||||
|
export { paginate } |
||||
@ -0,0 +1,7 @@ |
|||||
|
export default { |
||||
|
data: [], // 列表数据
|
||||
|
current_page: 1, // 当前页码
|
||||
|
last_page: 1, // 最大页码
|
||||
|
per_page: 15, // 每页记录数
|
||||
|
total: 0, // 总记录数
|
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
import Enum from '../enum' |
||||
|
|
||||
|
/** |
||||
|
* 枚举类:优惠券适用范围 |
||||
|
* ApplyRangeEnum |
||||
|
*/ |
||||
|
export default new Enum([ |
||||
|
{ key: 'ALL', name: '全部商品', value: 10 }, |
||||
|
{ key: 'SOME_GOODS', name: '指定商品', value: 20 } |
||||
|
]) |
||||
@ -0,0 +1,10 @@ |
|||||
|
import Enum from '../enum' |
||||
|
|
||||
|
/** |
||||
|
* 枚举类:优惠券类型 |
||||
|
* CouponTypeEnum |
||||
|
*/ |
||||
|
export default new Enum([ |
||||
|
{ key: 'FULL_DISCOUNT', name: '满减券', value: 10 }, |
||||
|
{ key: 'DISCOUNT', name: '折扣券', value: 20 } |
||||
|
]) |
||||
@ -0,0 +1,10 @@ |
|||||
|
import Enum from '../enum' |
||||
|
|
||||
|
/** |
||||
|
* 枚举类:优惠券到期类型 |
||||
|
* ExpireTypeEnum |
||||
|
*/ |
||||
|
export default new Enum([ |
||||
|
{ key: 'RECEIVE', name: '领取后', value: 10 }, |
||||
|
{ key: 'FIXED_TIME', name: '固定时间', value: 20 } |
||||
|
]) |
||||
@ -0,0 +1,5 @@ |
|||||
|
import ApplyRangeEnum from './ApplyRange' |
||||
|
import ExpireTypeEnum from './ExpireType' |
||||
|
import CouponTypeEnum from './CouponType' |
||||
|
|
||||
|
export { ApplyRangeEnum, CouponTypeEnum, ExpireTypeEnum } |
||||
@ -0,0 +1,85 @@ |
|||||
|
/** |
||||
|
* 枚举类 |
||||
|
* Enum.IMAGE.name => "图片" |
||||
|
* Enum.getNameByKey('IMAGE') => "图片" |
||||
|
* Enum.getValueByKey('IMAGE') => 10 |
||||
|
* Enum.getNameByValue(10) => "图片" |
||||
|
* Enum.getData() => [{key: "IMAGE", name: "图片", value: 10}] |
||||
|
*/ |
||||
|
class Enum { |
||||
|
constructor (param) { |
||||
|
const keyArr = [] |
||||
|
const valueArr = [] |
||||
|
|
||||
|
if (!Array.isArray(param)) { |
||||
|
throw new Error('param is not an array!') |
||||
|
} |
||||
|
|
||||
|
param.map(element => { |
||||
|
if (!element.key || !element.name) { |
||||
|
return |
||||
|
} |
||||
|
// 保存key值组成的数组,方便A.getName(name)类型的调用
|
||||
|
keyArr.push(element.key) |
||||
|
valueArr.push(element.value) |
||||
|
// 根据key生成不同属性值,以便A.B.name类型的调用
|
||||
|
this[element.key] = element |
||||
|
if (element.key !== element.value) { |
||||
|
this[element.value] = element |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
|
||||
|
// 保存源数组
|
||||
|
this.data = param |
||||
|
this.keyArr = keyArr |
||||
|
this.valueArr = valueArr |
||||
|
|
||||
|
// 防止被修改
|
||||
|
// Object.freeze(this)
|
||||
|
} |
||||
|
|
||||
|
// 根据key得到对象
|
||||
|
keyOf (key) { |
||||
|
return this.data[this.keyArr.indexOf(key)] |
||||
|
} |
||||
|
|
||||
|
// 根据key得到对象
|
||||
|
valueOf (key) { |
||||
|
return this.data[this.valueArr.indexOf(key)] |
||||
|
} |
||||
|
|
||||
|
// 根据key获取name值
|
||||
|
getNameByKey (key) { |
||||
|
const prop = this.keyOf(key) |
||||
|
if (!prop) { |
||||
|
throw new Error('No enum constant' + key) |
||||
|
} |
||||
|
return prop.name |
||||
|
} |
||||
|
|
||||
|
// 根据value获取name值
|
||||
|
getNameByValue (value) { |
||||
|
const prop = this.valueOf(value) |
||||
|
if (!prop) { |
||||
|
throw new Error('No enum constant' + value) |
||||
|
} |
||||
|
return prop.name |
||||
|
} |
||||
|
|
||||
|
// 根据key获取value值
|
||||
|
getValueByKey (key) { |
||||
|
const prop = this.keyOf(key) |
||||
|
if (!prop) { |
||||
|
throw new Error('No enum constant' + key) |
||||
|
} |
||||
|
return prop.key |
||||
|
} |
||||
|
|
||||
|
// 返回源数组
|
||||
|
getData () { |
||||
|
return this.data |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default Enum |
||||
@ -0,0 +1,10 @@ |
|||||
|
import Enum from '../enum' |
||||
|
|
||||
|
/** |
||||
|
* 枚举类:商品规格类型 |
||||
|
* SpecTypeEnum |
||||
|
*/ |
||||
|
export default new Enum([ |
||||
|
{ key: 'SINGLE', name: '单规格', value: 10 }, |
||||
|
{ key: 'MULTI', name: '多规格', value: 20 } |
||||
|
]) |
||||
@ -0,0 +1,3 @@ |
|||||
|
import SpecTypeEnum from './SpecType' |
||||
|
|
||||
|
export { SpecTypeEnum } |
||||
@ -0,0 +1,10 @@ |
|||||
|
import Enum from '../enum' |
||||
|
|
||||
|
/** |
||||
|
* 枚举类:订单发货状态 |
||||
|
* DeliveryStatusEnum |
||||
|
*/ |
||||
|
export default new Enum([ |
||||
|
{ key: 'NOT_DELIVERED', name: '未发货', value: 10 }, |
||||
|
{ key: 'DELIVERED', name: '已发货', value: 20 } |
||||
|
]) |
||||
@ -0,0 +1,9 @@ |
|||||
|
import Enum from '../enum' |
||||
|
|
||||
|
/** |
||||
|
* 枚举类:配送方式 |
||||
|
* DeliveryTypeEnum |
||||
|
*/ |
||||
|
export default new Enum([ |
||||
|
{ key: 'EXPRESS', name: '快递配送', value: 10 } |
||||
|
]) |
||||
@ -0,0 +1,11 @@ |
|||||
|
import Enum from '../enum' |
||||
|
|
||||
|
/** |
||||
|
* 枚举类:订单来源 |
||||
|
* OrderSourceEnum |
||||
|
*/ |
||||
|
export default new Enum([ |
||||
|
{ key: 'MASTER', name: '普通订单', value: 10 }, |
||||
|
{ key: 'BARGAIN', name: '砍价订单', value: 20 }, |
||||
|
{ key: 'SHARP', name: '秒杀订单', value: 30 } |
||||
|
]) |
||||
@ -0,0 +1,12 @@ |
|||||
|
import Enum from '../enum' |
||||
|
|
||||
|
/** |
||||
|
* 枚举类:订单状态 |
||||
|
* OrderStatusEnum |
||||
|
*/ |
||||
|
export default new Enum([ |
||||
|
{ key: 'NORMAL', name: '进行中', value: 10 }, |
||||
|
{ key: 'CANCELLED', name: '已取消', value: 20 }, |
||||
|
{ key: 'APPLY_CANCEL', name: '待取消', value: 21 }, |
||||
|
{ key: 'COMPLETED', name: '已完成', value: 30 } |
||||
|
]) |
||||
@ -0,0 +1,10 @@ |
|||||
|
import Enum from '../enum' |
||||
|
|
||||
|
/** |
||||
|
* 枚举类:订单支付状态 |
||||
|
* PayStatusEnum |
||||
|
*/ |
||||
|
export default new Enum([ |
||||
|
{ key: 'PENDING', name: '待支付', value: 10 }, |
||||
|
{ key: 'SUCCESS', name: '已支付', value: 20 } |
||||
|
]) |
||||
@ -0,0 +1,10 @@ |
|||||
|
import Enum from '../enum' |
||||
|
|
||||
|
/** |
||||
|
* 枚举类:订单支付方式 |
||||
|
* PayTypeEnum |
||||
|
*/ |
||||
|
export default new Enum([ |
||||
|
{ key: 'BALANCE', name: '余额支付', value: 10 }, |
||||
|
{ key: 'WECHAT', name: '微信支付', value: 20 } |
||||
|
]) |
||||
@ -0,0 +1,10 @@ |
|||||
|
import Enum from '../enum' |
||||
|
|
||||
|
/** |
||||
|
* 枚举类:订单收货状态 |
||||
|
* ReceiptStatusEnum |
||||
|
*/ |
||||
|
export default new Enum([ |
||||
|
{ key: 'NOT_RECEIVED', name: '未收货', value: 10 }, |
||||
|
{ key: 'RECEIVED', name: '已收货', value: 20 } |
||||
|
]) |
||||
@ -0,0 +1,17 @@ |
|||||
|
import DeliveryStatusEnum from './DeliveryStatus' |
||||
|
import DeliveryTypeEnum from './DeliveryType' |
||||
|
import OrderSourceEnum from './OrderSource' |
||||
|
import OrderStatusEnum from './OrderStatus' |
||||
|
import PayStatusEnum from './PayStatus' |
||||
|
import PayTypeEnum from './PayType' |
||||
|
import ReceiptStatusEnum from './ReceiptStatus' |
||||
|
|
||||
|
export { |
||||
|
DeliveryStatusEnum, |
||||
|
DeliveryTypeEnum, |
||||
|
OrderSourceEnum, |
||||
|
OrderStatusEnum, |
||||
|
PayStatusEnum, |
||||
|
PayTypeEnum, |
||||
|
ReceiptStatusEnum |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
import Enum from '../../enum' |
||||
|
|
||||
|
/** |
||||
|
* 枚举类:商家审核状态 |
||||
|
* AuditStatusEnum |
||||
|
*/ |
||||
|
export default new Enum([ |
||||
|
{ key: 'WAIT', name: '待审核', value: 0 }, |
||||
|
{ key: 'REVIEWED', name: '已同意', value: 10 }, |
||||
|
{ key: 'REJECTED', name: '已拒绝', value: 20 } |
||||
|
]) |
||||
@ -0,0 +1,12 @@ |
|||||
|
import Enum from '../../enum' |
||||
|
|
||||
|
/** |
||||
|
* 枚举类:售后单状态 |
||||
|
* RefundStatusEnum |
||||
|
*/ |
||||
|
export default new Enum([ |
||||
|
{ key: 'NORMAL', name: '进行中', value: 0 }, |
||||
|
{ key: 'REJECTED', name: '已拒绝', value: 10 }, |
||||
|
{ key: 'COMPLETED', name: '已完成', value: 20 }, |
||||
|
{ key: 'CANCELLED', name: '已取消', value: 30 } |
||||
|
]) |
||||
@ -0,0 +1,10 @@ |
|||||
|
import Enum from '../../enum' |
||||
|
|
||||
|
/** |
||||
|
* 枚举类:售后类型 |
||||
|
* RefundTypeEnum |
||||
|
*/ |
||||
|
export default new Enum([ |
||||
|
{ key: 'RETURN', name: '退货退款', value: 10 }, |
||||
|
{ key: 'EXCHANGE', name: '换货', value: 20 } |
||||
|
]) |
||||
@ -0,0 +1,9 @@ |
|||||
|
import AuditStatusEnum from './AuditStatus' |
||||
|
import RefundStatusEnum from './RefundStatus' |
||||
|
import RefundTypeEnum from './RefundType' |
||||
|
|
||||
|
export { |
||||
|
AuditStatusEnum, |
||||
|
RefundStatusEnum, |
||||
|
RefundTypeEnum |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
import Enum from '../enum' |
||||
|
|
||||
|
/** |
||||
|
* 枚举类:设置项索引 |
||||
|
* SettingKeyEnum |
||||
|
*/ |
||||
|
export default new Enum([{ |
||||
|
key: 'REGISTER', |
||||
|
name: '账户注册设置', |
||||
|
value: 'register' |
||||
|
}, |
||||
|
{ |
||||
|
key: 'PAGE_CATEGORY_TEMPLATE', |
||||
|
name: '分类页模板', |
||||
|
value: 'page_category_template' |
||||
|
}, |
||||
|
{ |
||||
|
key: 'POINTS', |
||||
|
name: '积分设置', |
||||
|
value: 'points' |
||||
|
}, |
||||
|
{ |
||||
|
key: 'RECHARGE', |
||||
|
name: '充值设置', |
||||
|
value: 'recharge' |
||||
|
} |
||||
|
]) |
||||
@ -0,0 +1,12 @@ |
|||||
|
import Enum from '../../../enum' |
||||
|
|
||||
|
/** |
||||
|
* 枚举类:地址类型 |
||||
|
* PageCategoryStyleEnum |
||||
|
*/ |
||||
|
export default new Enum([ |
||||
|
{ key: 'ONE_LEVEL_BIG', name: '一级分类[大图]', value: 10 }, |
||||
|
{ key: 'ONE_LEVEL_SMALL', name: '一级分类[小图]', value: 11 }, |
||||
|
{ key: 'TWO_LEVEL', name: '二级分类', value: 20 }, |
||||
|
{ key: 'COMMODITY', name: '一级分类+商品', value: 30 } |
||||
|
]) |
||||
@ -0,0 +1,3 @@ |
|||||
|
import PageCategoryStyleEnum from './Style' |
||||
|
|
||||
|
export { PageCategoryStyleEnum } |
||||
@ -0,0 +1,57 @@ |
|||||
|
import * as Api from '@/api/region' |
||||
|
import storage from '@/utils/storage' |
||||
|
|
||||
|
const REGION_TREE = 'region_tree' |
||||
|
|
||||
|
/** |
||||
|
* 商品分类 model类 |
||||
|
* RegionModel |
||||
|
*/ |
||||
|
export default { |
||||
|
|
||||
|
// 从服务端获取全部地区数据(树状)
|
||||
|
getTreeDataFromApi () { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
Api.tree().then(result => resolve(result.data.list)) |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
// 获取所有地区(树状)
|
||||
|
getTreeData () { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
// 判断缓存中是否存在
|
||||
|
const data = storage.get(REGION_TREE) |
||||
|
// 从服务端获取全部地区数据
|
||||
|
if (data) { |
||||
|
resolve(data) |
||||
|
} else { |
||||
|
this.getTreeDataFromApi().then(list => { |
||||
|
// 缓存24小时
|
||||
|
storage.set(REGION_TREE, list, 24 * 60 * 60) |
||||
|
resolve(list) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
// 获取所有地区的总数
|
||||
|
getCitysCount () { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
// 获取所有地区(树状)
|
||||
|
this.getTreeData().then(data => { |
||||
|
const cityIds = [] |
||||
|
// 遍历省份
|
||||
|
for (const pidx in data) { |
||||
|
const province = data[pidx] |
||||
|
// 遍历城市
|
||||
|
for (const cidx in province.city) { |
||||
|
const cityItem = province.city[cidx] |
||||
|
cityIds.push(cityItem.id) |
||||
|
} |
||||
|
} |
||||
|
resolve(cityIds.length) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
import Config from '@/core/config' |
||||
|
import storage from '@/utils/storage' |
||||
|
import * as SettingApi from '@/api/setting' |
||||
|
|
||||
|
const CACHE_KEY = 'Setting' |
||||
|
const OTHER = '_other' |
||||
|
|
||||
|
// 写入缓存, 到期时间10分钟
|
||||
|
const setStorage = (data) => { |
||||
|
const expireTime = 10 * 60 |
||||
|
storage.set(CACHE_KEY, data, expireTime) |
||||
|
} |
||||
|
|
||||
|
// 获取缓存中的数据
|
||||
|
const getStorage = () => { |
||||
|
return storage.get(CACHE_KEY) |
||||
|
} |
||||
|
|
||||
|
// 获取后端接口商城设置 (最新)
|
||||
|
const getApiData = () => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
SettingApi.data() |
||||
|
.then(result => { |
||||
|
resolve(result.data.setting) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取商城设置 |
||||
|
* 有缓存的情况下返回缓存, 没有缓存从后端api获取 |
||||
|
* @param {bool} isCache 是否从缓存中获取 [优点不用每次请求后端api 缺点后台更新设置后需等待时效性] |
||||
|
*/ |
||||
|
const data = isCache => { |
||||
|
if (isCache == undefined) { |
||||
|
isCache = Config.get('enabledSettingCache') |
||||
|
} |
||||
|
return new Promise((resolve, reject) => { |
||||
|
const cacheData = getStorage() |
||||
|
if (isCache && cacheData) { |
||||
|
resolve(cacheData) |
||||
|
} else { |
||||
|
getApiData() |
||||
|
.then(data => { |
||||
|
setStorage(data) |
||||
|
resolve(data) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 获取商城设置(指定项)
|
||||
|
const item = (key, isCache) => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
data(isCache) |
||||
|
.then(setting => { |
||||
|
resolve(setting[key]) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 获取H5端访问地址
|
||||
|
const h5Url = (isCache = false) => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
data(isCache) |
||||
|
.then(setting => { |
||||
|
const h5Url = setting[OTHER]['h5Url'] |
||||
|
resolve(h5Url) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
export default { |
||||
|
data, |
||||
|
item, |
||||
|
h5Url |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
<template> |
||||
|
<view class="add-cart" @click.stop="handleAddCart"> |
||||
|
<text class="icon iconfont" :class="[`icon-jiagou${btnStyle}`]"></text> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
props: { |
||||
|
// 购物车按钮样式 1 2 3 |
||||
|
btnStyle: { |
||||
|
type: Number, |
||||
|
default: 1 |
||||
|
}, |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
value: false, |
||||
|
goodsInfo: {} |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
handleAddCart() { |
||||
|
this.$emit('click') |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.add-cart { |
||||
|
font-size: 38rpx; |
||||
|
padding: 0 20rpx; |
||||
|
color: #fa2209; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,173 @@ |
|||||
|
<template> |
||||
|
<goods-sku-popup :value="visible" @input="onChangeValue" border-radius="20" :localdata="localdata" :mode="2" :maskCloseAble="true" |
||||
|
@add-cart="addCart" @buy-now="buyNow" buyNowText="立即购买" /> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { setCartTotalNum } from '@/core/app' |
||||
|
import { SpecTypeEnum } from '@/common/enum/goods' |
||||
|
import * as CartApi from '@/api/cart' |
||||
|
import * as GoodsApi from '@/api/goods' |
||||
|
import GoodsSkuPopup from '@/components/goods-sku-popup' |
||||
|
|
||||
|
export default { |
||||
|
components: { |
||||
|
GoodsSkuPopup |
||||
|
}, |
||||
|
props: { |
||||
|
// 购物车按钮样式 1 2 3 |
||||
|
btnStyle: { |
||||
|
type: Number, |
||||
|
default: 1 |
||||
|
}, |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
// 是否显示 |
||||
|
visible: false, |
||||
|
// 主商品信息 |
||||
|
goods: {}, |
||||
|
// SKU商品信息 |
||||
|
localdata: {} |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
|
||||
|
// 加入购物车事件 |
||||
|
async handle(goods) { |
||||
|
this.goods = goods |
||||
|
if (goods.spec_type == SpecTypeEnum.SINGLE.value) { |
||||
|
this.singleEvent() |
||||
|
} |
||||
|
if (goods.spec_type == SpecTypeEnum.MULTI.value) { |
||||
|
this.multiEvent() |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 单规格商品事件 |
||||
|
singleEvent() { |
||||
|
const { goods } = this |
||||
|
this.addCart({ |
||||
|
goods_id: goods.goods_id, |
||||
|
goods_sku_id: '0', |
||||
|
buy_num: 1 |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
// 多规格商品事件 |
||||
|
async multiEvent() { |
||||
|
const app = this |
||||
|
const { goods } = app |
||||
|
// 获取商品的规格信息 |
||||
|
const { data: { specData } } = await GoodsApi.specData(goods.goods_id) |
||||
|
goods.skuList = specData.skuList |
||||
|
goods.specList = specData.specList |
||||
|
// 整理SKU商品信息 |
||||
|
app.localdata = { |
||||
|
_id: goods.goods_id, |
||||
|
name: goods.goods_name, |
||||
|
goods_thumb: goods.goods_image, |
||||
|
sku_list: app.getSkuList(), |
||||
|
spec_list: app.getSpecList() |
||||
|
} |
||||
|
this.visible = true |
||||
|
}, |
||||
|
|
||||
|
// 监听组件显示隐藏 |
||||
|
onChangeValue(val) { |
||||
|
this.visible = val |
||||
|
}, |
||||
|
|
||||
|
// 整理商品SKU列表 (多规格) |
||||
|
getSkuList() { |
||||
|
const app = this |
||||
|
const { goods: { goods_name, goods_image, skuList } } = app |
||||
|
const skuData = [] |
||||
|
skuList.forEach(item => { |
||||
|
skuData.push({ |
||||
|
_id: item.id, |
||||
|
goods_sku_id: item.goods_sku_id, |
||||
|
goods_id: item.goods_id, |
||||
|
goods_name: goods_name, |
||||
|
image: item.image_url ? item.image_url : goods_image, |
||||
|
price: item.goods_price * 100, |
||||
|
stock: item.stock_num, |
||||
|
spec_value_ids: item.spec_value_ids, |
||||
|
sku_name_arr: app.getSkuNameArr(item.spec_value_ids) |
||||
|
}) |
||||
|
}) |
||||
|
return skuData |
||||
|
}, |
||||
|
|
||||
|
// 获取sku记录的规格值列表 |
||||
|
getSkuNameArr(specValueIds) { |
||||
|
const app = this |
||||
|
const defaultData = ['默认'] |
||||
|
const skuNameArr = [] |
||||
|
if (specValueIds) { |
||||
|
specValueIds.forEach((valueId, groupIndex) => { |
||||
|
const specValueName = app.getSpecValueName(valueId, groupIndex) |
||||
|
skuNameArr.push(specValueName) |
||||
|
}) |
||||
|
} |
||||
|
return skuNameArr.length ? skuNameArr : defaultData |
||||
|
}, |
||||
|
|
||||
|
// 获取指定的规格值名称 |
||||
|
getSpecValueName(valueId, groupIndex) { |
||||
|
const app = this |
||||
|
const { goods: { specList } } = app |
||||
|
const res = specList[groupIndex].valueList.find(specValue => { |
||||
|
return specValue.spec_value_id == valueId |
||||
|
}) |
||||
|
return res.spec_value |
||||
|
}, |
||||
|
|
||||
|
// 整理规格数据 (多规格) |
||||
|
getSpecList() { |
||||
|
const { goods: { specList } } = this |
||||
|
const defaultData = [{ name: '默认', list: [{ name: '默认' }] }] |
||||
|
const specData = [] |
||||
|
specList.forEach(group => { |
||||
|
const children = [] |
||||
|
group.valueList.forEach(specValue => { |
||||
|
children.push({ name: specValue.spec_value }) |
||||
|
}) |
||||
|
specData.push({ |
||||
|
name: group.spec_name, |
||||
|
list: children |
||||
|
}) |
||||
|
}) |
||||
|
return specData.length ? specData : defaultData |
||||
|
}, |
||||
|
|
||||
|
// 加入购物车按钮 |
||||
|
addCart(selectShop) { |
||||
|
const app = this |
||||
|
const { goods_id, goods_sku_id, buy_num } = selectShop |
||||
|
CartApi.add(goods_id, goods_sku_id, buy_num) |
||||
|
.then(result => { |
||||
|
// 显示成功 |
||||
|
app.$toast(result.message, 1000, false) |
||||
|
// 隐藏当前弹窗 |
||||
|
app.onChangeValue(false) |
||||
|
// 购物车商品总数量 |
||||
|
const cartTotal = result.data.cartTotal |
||||
|
// 缓存购物车数量 |
||||
|
setCartTotalNum(cartTotal) |
||||
|
// 传递给父级 |
||||
|
app.$emit('addCart', cartTotal) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.add-cart { |
||||
|
font-size: 38rpx; |
||||
|
padding: 0 20rpx; |
||||
|
color: #fa2209; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,57 @@ |
|||||
|
<template> |
||||
|
<view class="avatar-image"> |
||||
|
<image class="image" |
||||
|
:style="{ width: `${width}rpx`, height: `${width}rpx`, borderWidth: `${borderWidth}rpx`, borderColor: borderColor }" |
||||
|
:src="url ? url : '/static/default-avatar.png'"></image> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
|
||||
|
/** |
||||
|
* 组件的属性列表 |
||||
|
* 用于组件自定义设置 |
||||
|
*/ |
||||
|
props: { |
||||
|
url: { |
||||
|
type: String, |
||||
|
default: '' |
||||
|
}, |
||||
|
width: { |
||||
|
type: Number, |
||||
|
default: 90 |
||||
|
}, |
||||
|
borderWidth: { |
||||
|
type: Number, |
||||
|
default: 0 |
||||
|
}, |
||||
|
borderColor: { |
||||
|
type: String, |
||||
|
default: '#000000' |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
data() { |
||||
|
return { |
||||
|
|
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.avatar-image { |
||||
|
.image { |
||||
|
display: block; |
||||
|
width: 60rpx; |
||||
|
height: 60rpx; |
||||
|
border-radius: 50%; |
||||
|
border-style: solid; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,66 @@ |
|||||
|
<template> |
||||
|
<view v-if="!isLoading" class="empty-content" :style="customStyle"> |
||||
|
<view class="empty-icon"> |
||||
|
<image class="image" src="/static/empty.png" mode="widthFix"></image> |
||||
|
</view> |
||||
|
<view class="tips">{{ tips }}</view> |
||||
|
<slot name="slot"></slot> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
|
||||
|
/** |
||||
|
* 组件的属性列表 |
||||
|
* 用于组件自定义设置 |
||||
|
*/ |
||||
|
props: { |
||||
|
// 正在加载 |
||||
|
isLoading: { |
||||
|
type: Boolean, |
||||
|
default: true |
||||
|
}, |
||||
|
// 自定义样式 |
||||
|
customStyle: { |
||||
|
type: Object, |
||||
|
default () { |
||||
|
return {} |
||||
|
} |
||||
|
}, |
||||
|
// 提示的问题 |
||||
|
tips: { |
||||
|
type: String, |
||||
|
default: '亲,暂无相关数据' |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
data() { |
||||
|
return {} |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.empty-content { |
||||
|
box-sizing: border-box; |
||||
|
width: 100%; |
||||
|
padding: 140rpx 50rpx; |
||||
|
text-align: center; |
||||
|
|
||||
|
.tips { |
||||
|
font-size: 28rpx; |
||||
|
color: gray; |
||||
|
margin: 50rpx 0; |
||||
|
} |
||||
|
|
||||
|
.empty-icon .image { |
||||
|
width: 280rpx; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
</style> |
||||
File diff suppressed because it is too large
@ -0,0 +1,450 @@ |
|||||
|
<!-- 步进器 --> |
||||
|
<template> |
||||
|
<view class="number-box"> |
||||
|
<view class="u-icon-minus" @touchstart.prevent="btnTouchStart('minus')" @touchend.stop.prevent="clearTimer" :class="{ 'u-icon-disabled': disabled || inputVal <= min }" |
||||
|
:style="{ |
||||
|
background: bgColor, |
||||
|
height: inputHeight + 'rpx', |
||||
|
color: color, |
||||
|
fontSize: size + 'rpx', |
||||
|
minHeight: '1.4em' |
||||
|
}"> |
||||
|
<view :style="'font-size:'+(Number(size)+10)+'rpx'" class="num-btn">-</view> |
||||
|
</view> |
||||
|
<input :disabled="disabledInput || disabled" :cursor-spacing="getCursorSpacing" :class="{ 'u-input-disabled': disabled }" |
||||
|
v-model="inputVal" class="u-number-input" @blur="onBlur" |
||||
|
type="number" :style="{ |
||||
|
color: color, |
||||
|
fontSize: size + 'rpx', |
||||
|
background: bgColor, |
||||
|
height: inputHeight + 'rpx', |
||||
|
width: inputWidth + 'rpx', |
||||
|
}" /> |
||||
|
<view class="u-icon-plus" @touchstart.prevent="btnTouchStart('plus')" @touchend.stop.prevent="clearTimer" :class="{ 'u-icon-disabled': disabled || inputVal >= max }" |
||||
|
:style="{ |
||||
|
background: bgColor, |
||||
|
height: inputHeight + 'rpx', |
||||
|
color: color, |
||||
|
fontSize: size + 'rpx', |
||||
|
minHeight: '1.4em', |
||||
|
}"> |
||||
|
<view :style="'font-size:'+(Number(size)+10)+'rpx'" class="num-btn">+</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
<script> |
||||
|
/** |
||||
|
* numberBox 步进器 |
||||
|
* @description 该组件一般用于商城购物选择物品数量的场景。注意:该输入框只能输入大于或等于0的整数,不支持小数输入 |
||||
|
* @tutorial https://www.uviewui.com/components/numberBox.html |
||||
|
* @property {Number} value 输入框初始值(默认1) |
||||
|
* @property {String} bg-color 输入框和按钮的背景颜色(默认#F2F3F5) |
||||
|
* @property {Number} min 用户可输入的最小值(默认0) |
||||
|
* @property {Number} max 用户可输入的最大值(默认99999) |
||||
|
* @property {Number} step 步长,每次加或减的值(默认1) |
||||
|
* @property {Number} stepFirst 步进值,首次增加或最后减的值(默认step值和一致) |
||||
|
* @property {Boolean} disabled 是否禁用操作,禁用后无法加减或手动修改输入框的值(默认false) |
||||
|
* @property {Boolean} disabled-input 是否禁止输入框手动输入值(默认false) |
||||
|
* @property {Boolean} positive-integer 是否只能输入正整数(默认true) |
||||
|
* @property {String | Number} size 输入框文字和按钮字体大小,单位rpx(默认26) |
||||
|
* @property {String} color 输入框文字和加减按钮图标的颜色(默认#323233) |
||||
|
* @property {String | Number} input-width 输入框宽度,单位rpx(默认80) |
||||
|
* @property {String | Number} input-height 输入框和按钮的高度,单位rpx(默认50) |
||||
|
* @property {String | Number} index 事件回调时用以区分当前发生变化的是哪个输入框 |
||||
|
* @property {Boolean} long-press 是否开启长按连续递增或递减(默认true) |
||||
|
* @property {String | Number} press-time 开启长按触发后,每触发一次需要多久,单位ms(默认250) |
||||
|
* @property {String | Number} cursor-spacing 指定光标于键盘的距离,避免键盘遮挡输入框,单位rpx(默认200) |
||||
|
* @event {Function} change 输入框内容发生变化时触发,对象形式 |
||||
|
* @event {Function} blur 输入框失去焦点时触发,对象形式 |
||||
|
* @event {Function} minus 点击减少按钮时触发(按钮可点击情况下),对象形式 |
||||
|
* @event {Function} plus 点击增加按钮时触发(按钮可点击情况下),对象形式 |
||||
|
* @example <number-box :min="1" :max="100"></number-box> |
||||
|
*/ |
||||
|
export default { |
||||
|
name: "NumberBox", |
||||
|
emits: ["update:modelValue", "input", "change", "blur", "plus", "minus"], |
||||
|
props: { |
||||
|
// 预显示的数字 |
||||
|
value: { |
||||
|
type: Number, |
||||
|
default: 1 |
||||
|
}, |
||||
|
modelValue: { |
||||
|
type: Number, |
||||
|
default: 1 |
||||
|
}, |
||||
|
// 背景颜色 |
||||
|
bgColor: { |
||||
|
type: String, |
||||
|
default: '#F2F3F5' |
||||
|
}, |
||||
|
// 最小值 |
||||
|
min: { |
||||
|
type: Number, |
||||
|
default: 0 |
||||
|
}, |
||||
|
// 最大值 |
||||
|
max: { |
||||
|
type: Number, |
||||
|
default: 99999 |
||||
|
}, |
||||
|
// 步进值,每次加或减的值 |
||||
|
step: { |
||||
|
type: Number, |
||||
|
default: 1 |
||||
|
}, |
||||
|
// 步进值,首次增加或最后减的值 |
||||
|
stepFirst: { |
||||
|
type: Number, |
||||
|
default: 0 |
||||
|
}, |
||||
|
// 是否只能输入 step 的倍数 |
||||
|
stepStrictly: { |
||||
|
type: Boolean, |
||||
|
default: false |
||||
|
}, |
||||
|
// 是否禁用加减操作 |
||||
|
disabled: { |
||||
|
type: Boolean, |
||||
|
default: false |
||||
|
}, |
||||
|
// input的字体大小,单位rpx |
||||
|
size: { |
||||
|
type: [Number, String], |
||||
|
default: 26 |
||||
|
}, |
||||
|
// 加减图标的颜色 |
||||
|
color: { |
||||
|
type: String, |
||||
|
default: '#323233' |
||||
|
}, |
||||
|
// input宽度,单位rpx |
||||
|
inputWidth: { |
||||
|
type: [Number, String], |
||||
|
default: 80 |
||||
|
}, |
||||
|
// input高度,单位rpx |
||||
|
inputHeight: { |
||||
|
type: [Number, String], |
||||
|
default: 50 |
||||
|
}, |
||||
|
// index索引,用于列表中使用,让用户知道是哪个numberbox发生了变化,一般使用for循环出来的index值即可 |
||||
|
index: { |
||||
|
type: [Number, String], |
||||
|
default: '' |
||||
|
}, |
||||
|
// 是否禁用输入框,与disabled作用于输入框时,为OR的关系,即想要禁用输入框,又可以加减的话 |
||||
|
// 设置disabled为false,disabledInput为true即可 |
||||
|
disabledInput: { |
||||
|
type: Boolean, |
||||
|
default: false |
||||
|
}, |
||||
|
// 输入框于键盘之间的距离 |
||||
|
cursorSpacing: { |
||||
|
type: [Number, String], |
||||
|
default: 100 |
||||
|
}, |
||||
|
// 是否开启长按连续递增或递减 |
||||
|
longPress: { |
||||
|
type: Boolean, |
||||
|
default: true |
||||
|
}, |
||||
|
// 开启长按触发后,每触发一次需要多久 |
||||
|
pressTime: { |
||||
|
type: [Number, String], |
||||
|
default: 250 |
||||
|
}, |
||||
|
// 是否只能输入大于或等于0的整数(正整数) |
||||
|
positiveInteger: { |
||||
|
type: Boolean, |
||||
|
default: true |
||||
|
} |
||||
|
}, |
||||
|
watch: { |
||||
|
value(v1, v2) { |
||||
|
// 只有value的改变是来自外部的时候,才去同步inputVal的值,否则会造成循环错误 |
||||
|
if(!this.changeFromInner) { |
||||
|
this.inputVal = v1; |
||||
|
// 因为inputVal变化后,会触发this.handleChange(),在其中changeFromInner会再次被设置为true, |
||||
|
// 造成外面修改值,也导致被认为是内部修改的混乱,这里进行this.$nextTick延时,保证在运行周期的最后处 |
||||
|
// 将changeFromInner设置为false |
||||
|
this.$nextTick(function(){ |
||||
|
this.changeFromInner = false; |
||||
|
}) |
||||
|
} |
||||
|
}, |
||||
|
modelValue(v1, v2) { |
||||
|
// 只有value的改变是来自外部的时候,才去同步inputVal的值,否则会造成循环错误 |
||||
|
if(!this.changeFromInner) { |
||||
|
this.inputVal = v1; |
||||
|
// 因为inputVal变化后,会触发this.handleChange(),在其中changeFromInner会再次被设置为true, |
||||
|
// 造成外面修改值,也导致被认为是内部修改的混乱,这里进行this.$nextTick延时,保证在运行周期的最后处 |
||||
|
// 将changeFromInner设置为false |
||||
|
this.$nextTick(function(){ |
||||
|
this.changeFromInner = false; |
||||
|
}) |
||||
|
} |
||||
|
}, |
||||
|
inputVal(v1, v2) { |
||||
|
// 为了让用户能够删除所有输入值,重新输入内容,删除所有值后,内容为空字符串 |
||||
|
if (v1 == '') return; |
||||
|
let value = 0; |
||||
|
// 首先判断是否数值,并且在min和max之间,如果不是,使用原来值 |
||||
|
let tmp = this.isNumber(v1); |
||||
|
if (tmp && v1 >= this.min && v1 <= this.max) value = v1; |
||||
|
else value = v2; |
||||
|
// 判断是否只能输入大于等于0的整数 |
||||
|
if(this.positiveInteger) { |
||||
|
// 小于0,或者带有小数点, |
||||
|
if(v1 < 0 || String(v1).indexOf('.') !== -1) { |
||||
|
value = v2; |
||||
|
// 双向绑定input的值,必须要使用$nextTick修改显示的值 |
||||
|
this.$nextTick(() => { |
||||
|
this.inputVal = v2; |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
// 发出change事件 |
||||
|
this.handleChange(value, 'change'); |
||||
|
}, |
||||
|
min(v1){ |
||||
|
if(v1 !== undefined && v1!="" && this.getValue() < v1){ |
||||
|
this.$emit("input",v1); |
||||
|
} |
||||
|
}, |
||||
|
max(v1){ |
||||
|
if(v1 !== undefined && v1!="" && this.getValue() > v1){ |
||||
|
this.$emit("input",v1); |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
inputVal: 1, // 输入框中的值,不能直接使用props中的value,因为应该改变props的状态 |
||||
|
timer: null, // 用作长按的定时器 |
||||
|
changeFromInner: false, // 值发生变化,是来自内部还是外部 |
||||
|
innerChangeTimer: null, // 内部定时器 |
||||
|
}; |
||||
|
}, |
||||
|
created() { |
||||
|
this.inputVal = Number(this.getValue()); |
||||
|
}, |
||||
|
computed: { |
||||
|
getCursorSpacing() { |
||||
|
// 先将值转为px单位,再转为数值 |
||||
|
return Number(uni.upx2px(this.cursorSpacing)); |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
getValue(){ |
||||
|
// #ifndef VUE3 |
||||
|
return this.value; |
||||
|
// #endif |
||||
|
|
||||
|
// #ifdef VUE3 |
||||
|
return this.modelValue; |
||||
|
// #endif |
||||
|
}, |
||||
|
// 点击退格键 |
||||
|
btnTouchStart(callback) { |
||||
|
// 先执行一遍方法,否则会造成松开手时,就执行了clearTimer,导致无法实现功能 |
||||
|
this[callback](); |
||||
|
// 如果没开启长按功能,直接返回 |
||||
|
if (!this.longPress) return; |
||||
|
clearInterval(this.timer); //再次清空定时器,防止重复注册定时器 |
||||
|
this.timer = null; |
||||
|
this.timer = setInterval(() => { |
||||
|
// 执行加或减函数 |
||||
|
this[callback](); |
||||
|
}, this.pressTime); |
||||
|
}, |
||||
|
clearTimer() { |
||||
|
this.$nextTick(() => { |
||||
|
clearInterval(this.timer); |
||||
|
this.timer = null; |
||||
|
}) |
||||
|
}, |
||||
|
minus() { |
||||
|
this.computeVal('minus'); |
||||
|
}, |
||||
|
plus() { |
||||
|
this.computeVal('plus'); |
||||
|
}, |
||||
|
// 为了保证小数相加减出现精度溢出的问题 |
||||
|
calcPlus(num1, num2) { |
||||
|
let baseNum, baseNum1, baseNum2; |
||||
|
try { |
||||
|
baseNum1 = num1.toString().split('.')[1].length; |
||||
|
} catch (e) { |
||||
|
baseNum1 = 0; |
||||
|
} |
||||
|
try { |
||||
|
baseNum2 = num2.toString().split('.')[1].length; |
||||
|
} catch (e) { |
||||
|
baseNum2 = 0; |
||||
|
} |
||||
|
baseNum = Math.pow(10, Math.max(baseNum1, baseNum2)); |
||||
|
let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2; //精度 |
||||
|
return ((num1 * baseNum + num2 * baseNum) / baseNum).toFixed(precision); |
||||
|
}, |
||||
|
// 为了保证小数相加减出现精度溢出的问题 |
||||
|
calcMinus(num1, num2) { |
||||
|
let baseNum, baseNum1, baseNum2; |
||||
|
try { |
||||
|
baseNum1 = num1.toString().split('.')[1].length; |
||||
|
} catch (e) { |
||||
|
baseNum1 = 0; |
||||
|
} |
||||
|
try { |
||||
|
baseNum2 = num2.toString().split('.')[1].length; |
||||
|
} catch (e) { |
||||
|
baseNum2 = 0; |
||||
|
} |
||||
|
baseNum = Math.pow(10, Math.max(baseNum1, baseNum2)); |
||||
|
let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2; |
||||
|
return ((num1 * baseNum - num2 * baseNum) / baseNum).toFixed(precision); |
||||
|
}, |
||||
|
computeVal(type) { |
||||
|
uni.hideKeyboard(); |
||||
|
if (this.disabled) return; |
||||
|
let value = 0; |
||||
|
// 新增stepFirst开始 |
||||
|
// 减 |
||||
|
if (type === 'minus') { |
||||
|
if(this.stepFirst > 0 && this.inputVal == this.stepFirst){ |
||||
|
value = this.min; |
||||
|
}else{ |
||||
|
value = this.calcMinus(this.inputVal, this.step); |
||||
|
} |
||||
|
} else if (type === 'plus') { |
||||
|
if(this.stepFirst > 0 && this.inputVal < this.stepFirst){ |
||||
|
value = this.stepFirst; |
||||
|
}else{ |
||||
|
value = this.calcPlus(this.inputVal, this.step); |
||||
|
} |
||||
|
} |
||||
|
if(this.stepStrictly){ |
||||
|
let strictly = value % this.step; |
||||
|
if(strictly > 0){ |
||||
|
value -= strictly; |
||||
|
} |
||||
|
} |
||||
|
if (value > this.max ) { |
||||
|
value = this.max; |
||||
|
}else if (value < this.min) { |
||||
|
value = this.min; |
||||
|
} |
||||
|
// 新增stepFirst结束 |
||||
|
this.inputVal = value; |
||||
|
this.handleChange(value, type); |
||||
|
}, |
||||
|
// 处理用户手动输入的情况 |
||||
|
onBlur(event) { |
||||
|
let val = 0; |
||||
|
let value = event.detail.value; |
||||
|
// 如果为非0-9数字组成,或者其第一位数值为0,直接让其等于min值 |
||||
|
// 这里不直接判断是否正整数,是因为用户传递的props min值可能为0 |
||||
|
if (!/(^\d+$)/.test(value) || value[0] == 0) val = this.min; |
||||
|
val = +value; |
||||
|
|
||||
|
// 新增stepFirst开始 |
||||
|
if(this.stepFirst > 0 && this.inputVal < this.stepFirst && this.inputVal>0){ |
||||
|
val = this.stepFirst; |
||||
|
} |
||||
|
// 新增stepFirst结束 |
||||
|
if(this.stepStrictly){ |
||||
|
let strictly = val % this.step; |
||||
|
if(strictly > 0){ |
||||
|
val -= strictly; |
||||
|
} |
||||
|
} |
||||
|
if (val > this.max) { |
||||
|
val = this.max; |
||||
|
} else if (val < this.min) { |
||||
|
val = this.min; |
||||
|
} |
||||
|
this.$nextTick(() => { |
||||
|
this.inputVal = val; |
||||
|
}) |
||||
|
this.handleChange(val, 'blur'); |
||||
|
}, |
||||
|
handleChange(value, type) { |
||||
|
if (this.disabled) return; |
||||
|
// 清除定时器,避免造成混乱 |
||||
|
if(this.innerChangeTimer) { |
||||
|
clearTimeout(this.innerChangeTimer); |
||||
|
this.innerChangeTimer = null; |
||||
|
} |
||||
|
// 发出input事件,修改通过v-model绑定的值,达到双向绑定的效果 |
||||
|
this.changeFromInner = true; |
||||
|
// 一定时间内,清除changeFromInner标记,否则内部值改变后 |
||||
|
// 外部通过程序修改value值,将会无效 |
||||
|
this.innerChangeTimer = setTimeout(() => { |
||||
|
this.changeFromInner = false; |
||||
|
}, 150); |
||||
|
this.$emit('input', Number(value)); |
||||
|
this.$emit("update:modelValue", Number(value)); |
||||
|
this.$emit(type, { |
||||
|
// 转为Number类型 |
||||
|
value: Number(value), |
||||
|
index: this.index |
||||
|
}) |
||||
|
}, |
||||
|
/** |
||||
|
* 验证十进制数字 |
||||
|
*/ |
||||
|
isNumber(value) { |
||||
|
return /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value) |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.number-box { |
||||
|
display: inline-flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.u-number-input { |
||||
|
position: relative; |
||||
|
text-align: center; |
||||
|
padding: 0; |
||||
|
margin: 0 6rpx; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
.u-icon-plus, |
||||
|
.u-icon-minus { |
||||
|
width: 60rpx; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.u-icon-plus { |
||||
|
border-radius: 0 8rpx 8rpx 0; |
||||
|
} |
||||
|
|
||||
|
.u-icon-minus { |
||||
|
border-radius: 8rpx 0 0 8rpx; |
||||
|
} |
||||
|
|
||||
|
.u-icon-disabled { |
||||
|
color: #c8c9cc !important; |
||||
|
background: #f7f8fa !important; |
||||
|
} |
||||
|
|
||||
|
.u-input-disabled { |
||||
|
color: #c8c9cc !important; |
||||
|
background-color: #f2f3f5 !important; |
||||
|
} |
||||
|
.num-btn{ |
||||
|
font-weight:550; |
||||
|
position: relative; |
||||
|
top:-4rpx; |
||||
|
} |
||||
|
|
||||
|
</style> |
||||
@ -0,0 +1,55 @@ |
|||||
|
/* 下拉刷新区域 */ |
||||
|
.mescroll-downwarp { |
||||
|
position: absolute; |
||||
|
top: -100%; |
||||
|
left: 0; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
/* 下拉刷新--内容区,定位于区域底部 */ |
||||
|
.mescroll-downwarp .downwarp-content { |
||||
|
position: absolute; |
||||
|
left: 0; |
||||
|
bottom: 0; |
||||
|
width: 100%; |
||||
|
min-height: 60rpx; |
||||
|
padding: 20rpx 0; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
/* 下拉刷新--提示文本 */ |
||||
|
.mescroll-downwarp .downwarp-tip { |
||||
|
display: inline-block; |
||||
|
font-size: 28rpx; |
||||
|
vertical-align: middle; |
||||
|
margin-left: 16rpx; |
||||
|
/* color: gray; 已在style设置color,此处删去*/ |
||||
|
} |
||||
|
|
||||
|
/* 下拉刷新--旋转进度条 */ |
||||
|
.mescroll-downwarp .downwarp-progress { |
||||
|
display: inline-block; |
||||
|
width: 32rpx; |
||||
|
height: 32rpx; |
||||
|
border-radius: 50%; |
||||
|
border: 2rpx solid gray; |
||||
|
border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/ |
||||
|
vertical-align: middle; |
||||
|
} |
||||
|
|
||||
|
/* 旋转动画 */ |
||||
|
.mescroll-downwarp .mescroll-rotate { |
||||
|
animation: mescrollDownRotate 0.6s linear infinite; |
||||
|
} |
||||
|
|
||||
|
@keyframes mescrollDownRotate { |
||||
|
0% { |
||||
|
transform: rotate(0deg); |
||||
|
} |
||||
|
|
||||
|
100% { |
||||
|
transform: rotate(360deg); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
<!-- 下拉刷新区域 --> |
||||
|
<template> |
||||
|
<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}"> |
||||
|
<view class="downwarp-content"> |
||||
|
<view class="downwarp-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mOption.textColor, 'transform':downRotate}"></view> |
||||
|
<view class="downwarp-tip">{{downText}}</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
props: { |
||||
|
option: Object , // down的配置项 |
||||
|
type: Number, // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4) |
||||
|
rate: Number // 下拉比率 (inOffset: rate<1; outOffset: rate>=1) |
||||
|
}, |
||||
|
computed: { |
||||
|
// 支付宝小程序需写成计算属性,prop定义default仍报错 |
||||
|
mOption(){ |
||||
|
return this.option || {} |
||||
|
}, |
||||
|
// 是否在加载中 |
||||
|
isDownLoading(){ |
||||
|
return this.type === 3 |
||||
|
}, |
||||
|
// 旋转的角度 |
||||
|
downRotate(){ |
||||
|
return 'rotate(' + 360 * this.rate + 'deg)' |
||||
|
}, |
||||
|
// 文本提示 |
||||
|
downText(){ |
||||
|
switch (this.type){ |
||||
|
case 1: return this.mOption.textInOffset; |
||||
|
case 2: return this.mOption.textOutOffset; |
||||
|
case 3: return this.mOption.textLoading; |
||||
|
case 4: return this.mOption.textLoading; |
||||
|
default: return this.mOption.textInOffset; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style> |
||||
|
@import "./mescroll-down.css"; |
||||
|
</style> |
||||
@ -0,0 +1,90 @@ |
|||||
|
<!--空布局 |
||||
|
|
||||
|
可作为独立的组件, 不使用mescroll的页面也能单独引入, 以便APP全局统一管理: |
||||
|
import MescrollEmpty from '@/components/mescroll-uni/components/mescroll-empty.vue'; |
||||
|
<mescroll-empty v-if="isShowEmpty" :option="optEmpty" @emptyclick="emptyClick"></mescroll-empty> |
||||
|
|
||||
|
--> |
||||
|
<template> |
||||
|
<view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }" :style="{ 'z-index': option.zIndex, top: option.top }"> |
||||
|
<view> <image v-if="icon" class="empty-icon" :src="icon" mode="widthFix" /> </view> |
||||
|
<view v-if="tip" class="empty-tip">{{ tip }}</view> |
||||
|
<view v-if="option.btnText" class="empty-btn" @click="emptyClick">{{ option.btnText }}</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
// 引入全局配置 |
||||
|
import GlobalOption from './../mescroll-uni-option.js'; |
||||
|
export default { |
||||
|
props: { |
||||
|
// empty的配置项: 默认为GlobalOption.up.empty |
||||
|
option: { |
||||
|
type: Object, |
||||
|
default() { |
||||
|
return {}; |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
// 使用computed获取配置,用于支持option的动态配置 |
||||
|
computed: { |
||||
|
// 图标 |
||||
|
icon() { |
||||
|
return this.option.icon == null ? GlobalOption.up.empty.icon : this.option.icon; // 此处不使用短路求值, 用于支持传空串不显示图标 |
||||
|
}, |
||||
|
// 文本提示 |
||||
|
tip() { |
||||
|
return this.option.tip == null ? GlobalOption.up.empty.tip : this.option.tip; // 此处不使用短路求值, 用于支持传空串不显示文本提示 |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
// 点击按钮 |
||||
|
emptyClick() { |
||||
|
this.$emit('emptyclick'); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style> |
||||
|
/* 无任何数据的空布局 */ |
||||
|
.mescroll-empty { |
||||
|
box-sizing: border-box; |
||||
|
width: 100%; |
||||
|
padding: 100rpx 50rpx; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.mescroll-empty.empty-fixed { |
||||
|
z-index: 99; |
||||
|
position: absolute; /*transform会使fixed失效,最终会降级为absolute */ |
||||
|
top: 100rpx; |
||||
|
left: 0; |
||||
|
} |
||||
|
|
||||
|
.mescroll-empty .empty-icon { |
||||
|
width: 280rpx; |
||||
|
height: 280rpx; |
||||
|
} |
||||
|
|
||||
|
.mescroll-empty .empty-tip { |
||||
|
margin-top: 40rpx; |
||||
|
font-size: 28rpx; |
||||
|
color: gray; |
||||
|
} |
||||
|
|
||||
|
.mescroll-empty .empty-btn { |
||||
|
display: inline-block; |
||||
|
margin-top: 40rpx; |
||||
|
min-width: 200rpx; |
||||
|
padding: 18rpx; |
||||
|
font-size: 28rpx; |
||||
|
border: 1rpx solid #e04b28; |
||||
|
border-radius: 60rpx; |
||||
|
color: #e04b28; |
||||
|
} |
||||
|
|
||||
|
.mescroll-empty .empty-btn:active { |
||||
|
opacity: 0.75; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,83 @@ |
|||||
|
<!-- 回到顶部的按钮 --> |
||||
|
<template> |
||||
|
<image |
||||
|
v-if="mOption.src" |
||||
|
class="mescroll-totop" |
||||
|
:class="[value ? 'mescroll-totop-in' : 'mescroll-totop-out', {'mescroll-totop-safearea': mOption.safearea}]" |
||||
|
:style="{'z-index':mOption.zIndex, 'left': left, 'right': right, 'bottom':addUnit(mOption.bottom), 'width':addUnit(mOption.width), 'border-radius':addUnit(mOption.radius)}" |
||||
|
:src="mOption.src" |
||||
|
mode="widthFix" |
||||
|
@click="toTopClick" |
||||
|
/> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
props: { |
||||
|
// up.toTop的配置项 |
||||
|
option: Object, |
||||
|
// 是否显示 |
||||
|
value: false |
||||
|
}, |
||||
|
computed: { |
||||
|
// 支付宝小程序需写成计算属性,prop定义default仍报错 |
||||
|
mOption(){ |
||||
|
return this.option || {} |
||||
|
}, |
||||
|
// 优先显示左边 |
||||
|
left(){ |
||||
|
return this.mOption.left ? this.addUnit(this.mOption.left) : 'auto'; |
||||
|
}, |
||||
|
// 右边距离 (优先显示左边) |
||||
|
right() { |
||||
|
return this.mOption.left ? 'auto' : this.addUnit(this.mOption.right); |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
addUnit(num){ |
||||
|
if(!num) return 0; |
||||
|
if(typeof num === 'number') return num + 'rpx'; |
||||
|
return num |
||||
|
}, |
||||
|
toTopClick() { |
||||
|
this.$emit('input', false); // 使v-model生效 |
||||
|
this.$emit('click'); // 派发点击事件 |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style> |
||||
|
/* 回到顶部的按钮 */ |
||||
|
.mescroll-totop { |
||||
|
z-index: 9990; |
||||
|
position: fixed !important; /* 加上important避免编译到H5,在多mescroll中定位失效 */ |
||||
|
right: 20rpx; |
||||
|
bottom: 120rpx; |
||||
|
width: 72rpx; |
||||
|
height: auto; |
||||
|
border-radius: 50%; |
||||
|
opacity: 0; |
||||
|
transition: opacity 0.5s; /* 过渡 */ |
||||
|
margin-bottom: var(--window-bottom); /* css变量 */ |
||||
|
} |
||||
|
|
||||
|
/* 适配 iPhoneX */ |
||||
|
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) { |
||||
|
.mescroll-totop-safearea { |
||||
|
margin-bottom: calc(var(--window-bottom) + constant(safe-area-inset-bottom)); /* window-bottom + 适配 iPhoneX */ |
||||
|
margin-bottom: calc(var(--window-bottom) + env(safe-area-inset-bottom)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 显示 -- 淡入 */ |
||||
|
.mescroll-totop-in { |
||||
|
opacity: 1; |
||||
|
} |
||||
|
|
||||
|
/* 隐藏 -- 淡出且不接收事件*/ |
||||
|
.mescroll-totop-out { |
||||
|
opacity: 0; |
||||
|
pointer-events: none; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,47 @@ |
|||||
|
/* 上拉加载区域 */ |
||||
|
.mescroll-upwarp { |
||||
|
box-sizing: border-box; |
||||
|
min-height: 110rpx; |
||||
|
padding: 30rpx 0; |
||||
|
text-align: center; |
||||
|
clear: both; |
||||
|
} |
||||
|
|
||||
|
/*提示文本 */ |
||||
|
.mescroll-upwarp .upwarp-tip, |
||||
|
.mescroll-upwarp .upwarp-nodata { |
||||
|
display: inline-block; |
||||
|
font-size: 28rpx; |
||||
|
vertical-align: middle; |
||||
|
/* color: gray; 已在style设置color,此处删去*/ |
||||
|
} |
||||
|
|
||||
|
.mescroll-upwarp .upwarp-tip { |
||||
|
margin-left: 16rpx; |
||||
|
} |
||||
|
|
||||
|
/*旋转进度条 */ |
||||
|
.mescroll-upwarp .upwarp-progress { |
||||
|
display: inline-block; |
||||
|
width: 32rpx; |
||||
|
height: 32rpx; |
||||
|
border-radius: 50%; |
||||
|
border: 2rpx solid gray; |
||||
|
border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/ |
||||
|
vertical-align: middle; |
||||
|
} |
||||
|
|
||||
|
/* 旋转动画 */ |
||||
|
.mescroll-upwarp .mescroll-rotate { |
||||
|
animation: mescrollUpRotate 0.6s linear infinite; |
||||
|
} |
||||
|
|
||||
|
@keyframes mescrollUpRotate { |
||||
|
0% { |
||||
|
transform: rotate(0deg); |
||||
|
} |
||||
|
|
||||
|
100% { |
||||
|
transform: rotate(360deg); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
<!-- 上拉加载区域 --> |
||||
|
<template> |
||||
|
<view class="mescroll-upwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}"> |
||||
|
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) --> |
||||
|
<view v-show="isUpLoading"> |
||||
|
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mOption.textColor}"></view> |
||||
|
<view class="upwarp-tip">{{ mOption.textLoading }}</view> |
||||
|
</view> |
||||
|
<!-- 无数据 --> |
||||
|
<view v-if="isUpNoMore" class="upwarp-nodata">{{ mOption.textNoMore }}</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
props: { |
||||
|
option: Object, // up的配置项 |
||||
|
type: Number // 上拉加载的状态:0(loading前),1(loading中),2(没有更多了) |
||||
|
}, |
||||
|
computed: { |
||||
|
// 支付宝小程序需写成计算属性,prop定义default仍报错 |
||||
|
mOption() { |
||||
|
return this.option || {}; |
||||
|
}, |
||||
|
// 加载中 |
||||
|
isUpLoading() { |
||||
|
return this.type === 1; |
||||
|
}, |
||||
|
// 没有更多了 |
||||
|
isUpNoMore() { |
||||
|
return this.type === 2; |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style> |
||||
|
@import './mescroll-up.css'; |
||||
|
</style> |
||||
@ -0,0 +1,19 @@ |
|||||
|
.mescroll-body { |
||||
|
position: relative; /* 下拉刷新区域相对自身定位 */ |
||||
|
height: auto; /* 不可固定高度,否则overflow:hidden导致无法滑动; 同时使设置的最小高生效,实现列表不满屏仍可下拉*/ |
||||
|
overflow: hidden; /* 当有元素写在mescroll-body标签前面时,可遮住下拉刷新区域 */ |
||||
|
box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */ |
||||
|
} |
||||
|
|
||||
|
/* 使sticky生效: 父元素不能overflow:hidden或者overflow:auto属性 */ |
||||
|
.mescroll-body.mescorll-sticky{ |
||||
|
overflow: unset !important |
||||
|
} |
||||
|
|
||||
|
/* 适配 iPhoneX */ |
||||
|
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) { |
||||
|
.mescroll-safearea { |
||||
|
padding-bottom: constant(safe-area-inset-bottom); |
||||
|
padding-bottom: env(safe-area-inset-bottom); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,352 @@ |
|||||
|
<template> |
||||
|
<view |
||||
|
class="mescroll-body mescroll-render-touch" |
||||
|
:class="{'mescorll-sticky': sticky}" |
||||
|
:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" |
||||
|
@touchstart="wxsBiz.touchstartEvent" |
||||
|
@touchmove="wxsBiz.touchmoveEvent" |
||||
|
@touchend="wxsBiz.touchendEvent" |
||||
|
@touchcancel="wxsBiz.touchendEvent" |
||||
|
:change:prop="wxsBiz.propObserver" |
||||
|
:prop="wxsProp" |
||||
|
> |
||||
|
<!-- 状态栏 --> |
||||
|
<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view> |
||||
|
|
||||
|
<view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp"> |
||||
|
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)--> |
||||
|
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> --> |
||||
|
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}"> |
||||
|
<view class="downwarp-content"> |
||||
|
<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view> |
||||
|
<view class="downwarp-tip">{{downText}}</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 列表内容 --> |
||||
|
<slot></slot> |
||||
|
|
||||
|
<!-- 空布局 --> |
||||
|
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty> |
||||
|
|
||||
|
|
||||
|
<view class="mescroll-upwarp--container"> |
||||
|
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)--> |
||||
|
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> --> |
||||
|
<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}"> |
||||
|
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) --> |
||||
|
<view v-show="upLoadType===1"> |
||||
|
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view> |
||||
|
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view> |
||||
|
</view> |
||||
|
<!-- 无数据 --> |
||||
|
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
</view> |
||||
|
|
||||
|
<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) --> |
||||
|
<!-- #ifdef H5 --> |
||||
|
<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view> |
||||
|
<!-- #endif --> |
||||
|
|
||||
|
<!-- 适配iPhoneX --> |
||||
|
<view v-if="safearea" class="mescroll-safearea"></view> |
||||
|
|
||||
|
<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)--> |
||||
|
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top> |
||||
|
|
||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||
|
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 --> |
||||
|
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view> |
||||
|
<!-- #endif --> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<!-- 微信小程序, QQ小程序, app, h5使用wxs --> |
||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||
|
<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script> |
||||
|
<!-- #endif --> |
||||
|
|
||||
|
<!-- app, h5使用renderjs --> |
||||
|
<!-- #ifdef APP-PLUS || H5 --> |
||||
|
<script module="renderBiz" lang="renderjs"> |
||||
|
import renderBiz from './wxs/renderjs.js'; |
||||
|
export default { |
||||
|
mixins: [renderBiz] |
||||
|
} |
||||
|
</script> |
||||
|
<!-- #endif --> |
||||
|
|
||||
|
<script> |
||||
|
// 引入mescroll-uni.js,处理核心逻辑 |
||||
|
import MeScroll from './mescroll-uni.js'; |
||||
|
// 引入全局配置 |
||||
|
import GlobalOption from './mescroll-uni-option.js'; |
||||
|
// 引入空布局组件 |
||||
|
import MescrollEmpty from './components/mescroll-empty.vue'; |
||||
|
// 引入回到顶部组件 |
||||
|
import MescrollTop from './components/mescroll-top.vue'; |
||||
|
// 引入兼容wxs(含renderjs)写法的mixins |
||||
|
import WxsMixin from './wxs/mixins.js'; |
||||
|
|
||||
|
export default { |
||||
|
mixins: [WxsMixin], |
||||
|
components: { |
||||
|
MescrollEmpty, |
||||
|
MescrollTop |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
mescroll: {optDown:{},optUp:{}}, // mescroll实例 |
||||
|
downHight: 0, //下拉刷新: 容器高度 |
||||
|
downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1) |
||||
|
downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll) |
||||
|
upLoadType: 0, // 上拉加载状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示) |
||||
|
isShowEmpty: false, // 是否显示空布局 |
||||
|
isShowToTop: false, // 是否显示回到顶部按钮 |
||||
|
windowHeight: 0, // 可使用窗口的高度 |
||||
|
windowBottom: 0, // 可使用窗口的底部位置 |
||||
|
statusBarHeight: 0 // 状态栏高度 |
||||
|
}; |
||||
|
}, |
||||
|
props: { |
||||
|
down: Object, // 下拉刷新的参数配置 |
||||
|
up: Object, // 上拉加载的参数配置 |
||||
|
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||
|
topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变) |
||||
|
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||
|
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用) |
||||
|
height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉 |
||||
|
bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) |
||||
|
type: Boolean, |
||||
|
default: true |
||||
|
}, |
||||
|
sticky: Boolean // 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法会隐藏 |
||||
|
}, |
||||
|
computed: { |
||||
|
// mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉 |
||||
|
minHeight(){ |
||||
|
return this.toPx(this.height || '100%') + 'px' |
||||
|
}, |
||||
|
// 下拉布局往下偏移的距离 (px) |
||||
|
numTop() { |
||||
|
return this.toPx(this.top) |
||||
|
}, |
||||
|
padTop() { |
||||
|
return this.numTop + 'px'; |
||||
|
}, |
||||
|
// 上拉布局往上偏移 (px) |
||||
|
numBottom() { |
||||
|
return this.toPx(this.bottom); |
||||
|
}, |
||||
|
padBottom() { |
||||
|
return this.numBottom + 'px'; |
||||
|
}, |
||||
|
// 是否为重置下拉的状态 |
||||
|
isDownReset() { |
||||
|
return this.downLoadType === 3 || this.downLoadType === 4; |
||||
|
}, |
||||
|
// 过渡 |
||||
|
transition() { |
||||
|
return this.isDownReset ? 'transform 300ms' : ''; |
||||
|
}, |
||||
|
translateY() { |
||||
|
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外 |
||||
|
}, |
||||
|
// 是否在加载中 |
||||
|
isDownLoading(){ |
||||
|
return this.downLoadType === 3 |
||||
|
}, |
||||
|
// 旋转的角度 |
||||
|
downRotate(){ |
||||
|
return 'rotate(' + 360 * this.downRate + 'deg)' |
||||
|
}, |
||||
|
// 文本提示 |
||||
|
downText(){ |
||||
|
if(!this.mescroll) return ""; // 避免头条小程序初始化时报错 |
||||
|
switch (this.downLoadType){ |
||||
|
case 1: return this.mescroll.optDown.textInOffset; |
||||
|
case 2: return this.mescroll.optDown.textOutOffset; |
||||
|
case 3: return this.mescroll.optDown.textLoading; |
||||
|
case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset; |
||||
|
default: return this.mescroll.optDown.textInOffset; |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
//number,rpx,upx,px,% --> px的数值 |
||||
|
toPx(num) { |
||||
|
if (typeof num === 'string') { |
||||
|
if (num.indexOf('px') !== -1) { |
||||
|
if (num.indexOf('rpx') !== -1) { |
||||
|
// "10rpx" |
||||
|
num = num.replace('rpx', ''); |
||||
|
} else if (num.indexOf('upx') !== -1) { |
||||
|
// "10upx" |
||||
|
num = num.replace('upx', ''); |
||||
|
} else { |
||||
|
// "10px" |
||||
|
return Number(num.replace('px', '')); |
||||
|
} |
||||
|
} else if (num.indexOf('%') !== -1) { |
||||
|
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10% |
||||
|
let rate = Number(num.replace('%', '')) / 100; |
||||
|
return this.windowHeight * rate; |
||||
|
} |
||||
|
} |
||||
|
return num ? uni.upx2px(Number(num)) : 0; |
||||
|
}, |
||||
|
// 点击空布局的按钮回调 |
||||
|
emptyClick() { |
||||
|
this.$emit('emptyclick', this.mescroll); |
||||
|
}, |
||||
|
// 点击回到顶部的按钮回调 |
||||
|
toTopClick() { |
||||
|
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部 |
||||
|
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调 |
||||
|
} |
||||
|
}, |
||||
|
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效 |
||||
|
created() { |
||||
|
let vm = this; |
||||
|
|
||||
|
let diyOption = { |
||||
|
// 下拉刷新的配置 |
||||
|
down: { |
||||
|
inOffset() { |
||||
|
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||
|
}, |
||||
|
outOffset() { |
||||
|
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||
|
}, |
||||
|
onMoving(mescroll, rate, downHight) { |
||||
|
// 下拉过程中的回调,滑动过程一直在执行; |
||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||
|
vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1) |
||||
|
}, |
||||
|
showLoading(mescroll, downHight) { |
||||
|
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删) |
||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||
|
}, |
||||
|
beforeEndDownScroll(mescroll){ |
||||
|
vm.downLoadType = 4; |
||||
|
return mescroll.optDown.beforeEndDelay // 延时结束的时长 |
||||
|
}, |
||||
|
endDownScroll() { |
||||
|
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删) |
||||
|
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||
|
if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} // 移除重置倒计时 |
||||
|
vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,避免下次inOffset不及时显示textInOffset |
||||
|
if(vm.downLoadType === 4) vm.downLoadType = 0 |
||||
|
},300) |
||||
|
}, |
||||
|
// 派发下拉刷新的回调 |
||||
|
callback: function(mescroll) { |
||||
|
vm.$emit('down', mescroll); |
||||
|
} |
||||
|
}, |
||||
|
// 上拉加载的配置 |
||||
|
up: { |
||||
|
// 显示加载中的回调 |
||||
|
showLoading() { |
||||
|
vm.upLoadType = 1; |
||||
|
}, |
||||
|
// 显示无更多数据的回调 |
||||
|
showNoMore() { |
||||
|
vm.upLoadType = 2; |
||||
|
}, |
||||
|
// 隐藏上拉加载的回调 |
||||
|
hideUpScroll(mescroll) { |
||||
|
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3; |
||||
|
}, |
||||
|
// 空布局 |
||||
|
empty: { |
||||
|
onShow(isShow) { |
||||
|
// 显示隐藏的回调 |
||||
|
vm.isShowEmpty = isShow; |
||||
|
} |
||||
|
}, |
||||
|
// 回到顶部 |
||||
|
toTop: { |
||||
|
onShow(isShow) { |
||||
|
// 显示隐藏的回调 |
||||
|
vm.isShowToTop = isShow; |
||||
|
} |
||||
|
}, |
||||
|
// 派发上拉加载的回调 |
||||
|
callback: function(mescroll) { |
||||
|
vm.$emit('up', mescroll); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置 |
||||
|
let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // 深拷贝,避免对props的影响 |
||||
|
MeScroll.extend(myOption, diyOption); // 混入具体界面的配置 |
||||
|
|
||||
|
// 初始化MeScroll对象 |
||||
|
vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域 |
||||
|
// init回调mescroll对象 |
||||
|
vm.$emit('init', vm.mescroll); |
||||
|
|
||||
|
// 设置高度 |
||||
|
const sys = uni.getSystemInfoSync(); |
||||
|
if (sys.windowHeight) vm.windowHeight = sys.windowHeight; |
||||
|
if (sys.windowBottom) vm.windowBottom = sys.windowBottom; |
||||
|
if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight; |
||||
|
// 使down的bottomOffset生效 |
||||
|
vm.mescroll.setBodyHeight(sys.windowHeight); |
||||
|
|
||||
|
// 因为使用的是page的scroll,这里需自定义scrollTo |
||||
|
vm.mescroll.resetScrollTo((y, t) => { |
||||
|
if(typeof y === 'string'){ |
||||
|
// 滚动到指定view (y为css选择器) |
||||
|
setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick |
||||
|
let selector; |
||||
|
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){ |
||||
|
selector = '#'+y // 不带#和. 则默认为id选择器 |
||||
|
}else{ |
||||
|
selector = y |
||||
|
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK |
||||
|
if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询) |
||||
|
selector = y.split('>>>')[1].trim() |
||||
|
} |
||||
|
// #endif |
||||
|
} |
||||
|
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){ |
||||
|
if (rect) { |
||||
|
let top = rect.top |
||||
|
top += vm.mescroll.getScrollTop() |
||||
|
uni.pageScrollTo({ |
||||
|
scrollTop: top, |
||||
|
duration: t |
||||
|
}) |
||||
|
} else{ |
||||
|
console.error(selector + ' does not exist'); |
||||
|
} |
||||
|
}).exec() |
||||
|
},30) |
||||
|
} else{ |
||||
|
// 滚动到指定位置 (y必须为数字) |
||||
|
uni.pageScrollTo({ |
||||
|
scrollTop: y, |
||||
|
duration: t |
||||
|
}) |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值 |
||||
|
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else { |
||||
|
vm.mescroll.optUp.toTop.safearea = vm.safearea; |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
@import "./mescroll-body.css"; |
||||
|
@import "./components/mescroll-down.css"; |
||||
|
@import './components/mescroll-up.css'; |
||||
|
</style> |
||||
@ -0,0 +1,65 @@ |
|||||
|
// mescroll-body 和 mescroll-uni 通用
|
||||
|
|
||||
|
// import MescrollUni from "./mescroll-uni.vue";
|
||||
|
// import MescrollBody from "./mescroll-body.vue";
|
||||
|
|
||||
|
const MescrollMixin = { |
||||
|
// components: { // 非H5端无法通过mixin注册组件, 只能在main.js中注册全局组件或具体界面中注册
|
||||
|
// MescrollUni,
|
||||
|
// MescrollBody
|
||||
|
// },
|
||||
|
data() { |
||||
|
return { |
||||
|
mescroll: null //mescroll实例对象
|
||||
|
} |
||||
|
}, |
||||
|
// 注册系统自带的下拉刷新 (配置down.native为true时生效, 还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
|
||||
|
onPullDownRefresh(){ |
||||
|
this.mescroll && this.mescroll.onPullDownRefresh(); |
||||
|
}, |
||||
|
// 注册列表滚动事件,用于判定在顶部可下拉刷新,在指定位置可显示隐藏回到顶部按钮 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
|
||||
|
onPageScroll(e) { |
||||
|
this.mescroll && this.mescroll.onPageScroll(e); |
||||
|
}, |
||||
|
// 注册滚动到底部的事件,用于上拉加载 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
|
||||
|
onReachBottom() { |
||||
|
this.mescroll && this.mescroll.onReachBottom(); |
||||
|
}, |
||||
|
methods: { |
||||
|
// mescroll组件初始化的回调,可获取到mescroll对象
|
||||
|
mescrollInit(mescroll) { |
||||
|
this.mescroll = mescroll; |
||||
|
this.mescrollInitByRef(); // 兼容字节跳动小程序
|
||||
|
}, |
||||
|
// 以ref的方式初始化mescroll对象 (兼容字节跳动小程序)
|
||||
|
mescrollInitByRef() { |
||||
|
if(!this.mescroll || !this.mescroll.resetUpScroll){ |
||||
|
let mescrollRef = this.$refs.mescrollRef; |
||||
|
if(mescrollRef) this.mescroll = mescrollRef.mescroll |
||||
|
} |
||||
|
}, |
||||
|
// 下拉刷新的回调 (mixin默认resetUpScroll)
|
||||
|
downCallback() { |
||||
|
if(this.mescroll.optUp.use){ |
||||
|
this.mescroll.resetUpScroll() |
||||
|
}else{ |
||||
|
setTimeout(()=>{ |
||||
|
this.mescroll.endSuccess(); |
||||
|
}, 500) |
||||
|
} |
||||
|
}, |
||||
|
// 上拉加载的回调
|
||||
|
upCallback() { |
||||
|
// mixin默认延时500自动结束加载
|
||||
|
setTimeout(()=>{ |
||||
|
this.mescroll.endErr(); |
||||
|
}, 500) |
||||
|
} |
||||
|
}, |
||||
|
mounted() { |
||||
|
this.mescrollInitByRef(); // 兼容字节跳动小程序, 避免未设置@init或@init此时未能取到ref的情况
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
export default MescrollMixin; |
||||
@ -0,0 +1,37 @@ |
|||||
|
// 全局配置
|
||||
|
// mescroll-body 和 mescroll-uni 通用
|
||||
|
const GlobalOption = { |
||||
|
down: { |
||||
|
// 其他down的配置参数也可以写,这里只展示了常用的配置:
|
||||
|
textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
|
||||
|
textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
|
||||
|
textLoading: '加载中 ...', // 加载中的提示文本
|
||||
|
textSuccess: '加载成功', // 加载成功的文本
|
||||
|
textErr: '加载失败', // 加载失败的文本
|
||||
|
beforeEndDelay: 100, // 延时结束的时长 (显示加载成功/失败的时长)
|
||||
|
offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
|
||||
|
native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
|
||||
|
}, |
||||
|
up: { |
||||
|
// 其他up的配置参数也可以写,这里只展示了常用的配置:
|
||||
|
textLoading: '加载中 ...', // 加载中的提示文本
|
||||
|
textNoMore: '亲, 没有更多了', // 没有更多数据的提示文本
|
||||
|
offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
|
||||
|
toTop: { |
||||
|
// 回到顶部按钮,需配置src才显示
|
||||
|
src: "https://www.mescroll.com/img/mescroll-totop.png", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
|
||||
|
offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
|
||||
|
right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||
|
bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||
|
width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||
|
}, |
||||
|
empty: { |
||||
|
use: true, // 是否显示空布局
|
||||
|
// icon: "https://www.mescroll.com/img/mescroll-empty.png", // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
|
||||
|
icon: '/static/empty.png', |
||||
|
tip: '亲,暂无相关数据' // 提示
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default GlobalOption |
||||
@ -0,0 +1,36 @@ |
|||||
|
.mescroll-uni-warp{ |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.mescroll-uni-content{ |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.mescroll-uni { |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
min-height: 200rpx; |
||||
|
overflow-y: auto; |
||||
|
box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */ |
||||
|
} |
||||
|
|
||||
|
/* 定位的方式固定高度 */ |
||||
|
.mescroll-uni-fixed{ |
||||
|
z-index: 1; |
||||
|
position: fixed; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
width: auto; /* 使right生效 */ |
||||
|
height: auto; /* 使bottom生效 */ |
||||
|
} |
||||
|
|
||||
|
/* 适配 iPhoneX */ |
||||
|
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) { |
||||
|
.mescroll-safearea { |
||||
|
padding-bottom: constant(safe-area-inset-bottom); |
||||
|
padding-bottom: env(safe-area-inset-bottom); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,799 @@ |
|||||
|
/* mescroll |
||||
|
* version 1.3.3 |
||||
|
* 2020-09-15 wenju |
||||
|
* https://www.mescroll.com
|
||||
|
*/ |
||||
|
|
||||
|
export default function MeScroll(options, isScrollBody) { |
||||
|
let me = this; |
||||
|
me.version = '1.3.3'; // mescroll版本号
|
||||
|
me.options = options || {}; // 配置
|
||||
|
me.isScrollBody = isScrollBody || false; // 滚动区域是否为原生页面滚动; 默认为scroll-view
|
||||
|
|
||||
|
me.isDownScrolling = false; // 是否在执行下拉刷新的回调
|
||||
|
me.isUpScrolling = false; // 是否在执行上拉加载的回调
|
||||
|
let hasDownCallback = me.options.down && me.options.down.callback; // 是否配置了down的callback
|
||||
|
|
||||
|
// 初始化下拉刷新
|
||||
|
me.initDownScroll(); |
||||
|
// 初始化上拉加载,则初始化
|
||||
|
me.initUpScroll(); |
||||
|
|
||||
|
// 自动加载
|
||||
|
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
|
||||
|
// 自动触发下拉刷新 (只有配置了down的callback才自动触发下拉刷新)
|
||||
|
if ((me.optDown.use || me.optDown.native) && me.optDown.auto && hasDownCallback) { |
||||
|
if (me.optDown.autoShowLoading) { |
||||
|
me.triggerDownScroll(); // 显示下拉进度,执行下拉回调
|
||||
|
} else { |
||||
|
me.optDown.callback && me.optDown.callback(me); // 不显示下拉进度,直接执行下拉回调
|
||||
|
} |
||||
|
} |
||||
|
// 自动触发上拉加载
|
||||
|
if(!me.isUpAutoLoad){ // 部分小程序(头条小程序)emit是异步, 会导致isUpAutoLoad判断有误, 先延时确保先执行down的callback,再执行up的callback
|
||||
|
setTimeout(function(){ |
||||
|
me.optUp.use && me.optUp.auto && !me.isUpAutoLoad && me.triggerUpScroll(); |
||||
|
},100) |
||||
|
} |
||||
|
}, 30); // 需让me.optDown.inited和me.optUp.inited先执行
|
||||
|
} |
||||
|
|
||||
|
/* 配置参数:下拉刷新 */ |
||||
|
MeScroll.prototype.extendDownScroll = function(optDown) { |
||||
|
// 下拉刷新的配置
|
||||
|
MeScroll.extend(optDown, { |
||||
|
use: true, // 是否启用下拉刷新; 默认true
|
||||
|
auto: true, // 是否在初始化完毕之后自动执行下拉刷新的回调; 默认true
|
||||
|
native: false, // 是否使用系统自带的下拉刷新; 默认false; 仅mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
|
||||
|
autoShowLoading: false, // 如果设置auto=true(在初始化完毕之后自动执行下拉刷新的回调),那么是否显示下拉刷新的进度; 默认false
|
||||
|
isLock: false, // 是否锁定下拉刷新,默认false;
|
||||
|
offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
|
||||
|
startTop: 100, // scroll-view快速滚动到顶部时,此时的scroll-top可能大于0, 此值用于控制最大的误差
|
||||
|
inOffsetRate: 1, // 在列表顶部,下拉的距离小于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
|
||||
|
outOffsetRate: 0.2, // 在列表顶部,下拉的距离大于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
|
||||
|
bottomOffset: 20, // 当手指touchmove位置在距离body底部20px范围内的时候结束上拉刷新,避免Webview嵌套导致touchend事件不执行
|
||||
|
minAngle: 45, // 向下滑动最少偏移的角度,取值区间 [0,90];默认45度,即向下滑动的角度大于45度则触发下拉;而小于45度,将不触发下拉,避免与左右滑动的轮播等组件冲突;
|
||||
|
textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
|
||||
|
textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
|
||||
|
textLoading: '加载中 ...', // 加载中的提示文本
|
||||
|
textSuccess: '加载成功', // 加载成功的文本
|
||||
|
textErr: '加载失败', // 加载失败的文本
|
||||
|
beforeEndDelay: 100, // 延时结束的时长 (显示加载成功/失败的时长)
|
||||
|
bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorTop)
|
||||
|
textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
|
||||
|
inited: null, // 下拉刷新初始化完毕的回调
|
||||
|
inOffset: null, // 下拉的距离进入offset范围内那一刻的回调
|
||||
|
outOffset: null, // 下拉的距离大于offset那一刻的回调
|
||||
|
onMoving: null, // 下拉过程中的回调,滑动过程一直在执行; rate下拉区域当前高度与指定距离的比值(inOffset: rate<1; outOffset: rate>=1); downHight当前下拉区域的高度
|
||||
|
beforeLoading: null, // 准备触发下拉刷新的回调: 如果return true,将不触发showLoading和callback回调; 常用来完全自定义下拉刷新, 参考案例【淘宝 v6.8.0】
|
||||
|
showLoading: null, // 显示下拉刷新进度的回调
|
||||
|
afterLoading: null, // 显示下拉刷新进度的回调之后,马上要执行的代码 (如: 在wxs中使用)
|
||||
|
beforeEndDownScroll: null, // 准备结束下拉的回调. 返回结束下拉的延时执行时间,默认0ms; 常用于结束下拉之前再显示另外一小段动画,才去隐藏下拉刷新的场景, 参考案例【dotJump】
|
||||
|
endDownScroll: null, // 结束下拉刷新的回调
|
||||
|
afterEndDownScroll: null, // 结束下拉刷新的回调,马上要执行的代码 (如: 在wxs中使用)
|
||||
|
callback: function(mescroll) { |
||||
|
// 下拉刷新的回调;默认重置上拉加载列表为第一页
|
||||
|
mescroll.resetUpScroll(); |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
/* 配置参数:上拉加载 */ |
||||
|
MeScroll.prototype.extendUpScroll = function(optUp) { |
||||
|
// 上拉加载的配置
|
||||
|
MeScroll.extend(optUp, { |
||||
|
use: true, // 是否启用上拉加载; 默认true
|
||||
|
auto: true, // 是否在初始化完毕之后自动执行上拉加载的回调; 默认true
|
||||
|
isLock: false, // 是否锁定上拉加载,默认false;
|
||||
|
isBoth: true, // 上拉加载时,如果滑动到列表顶部是否可以同时触发下拉刷新;默认true,两者可同时触发;
|
||||
|
callback: null, // 上拉加载的回调;function(page,mescroll){ }
|
||||
|
page: { |
||||
|
num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
|
||||
|
size: 10, // 每页数据的数量
|
||||
|
time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
|
||||
|
}, |
||||
|
noMoreSize: 5, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
|
||||
|
offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
|
||||
|
textLoading: '加载中 ...', // 加载中的提示文本
|
||||
|
textNoMore: '-- END --', // 没有更多数据的提示文本
|
||||
|
bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorBottom)
|
||||
|
textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
|
||||
|
inited: null, // 初始化完毕的回调
|
||||
|
showLoading: null, // 显示加载中的回调
|
||||
|
showNoMore: null, // 显示无更多数据的回调
|
||||
|
hideUpScroll: null, // 隐藏上拉加载的回调
|
||||
|
errDistance: 60, // endErr的时候需往上滑动一段距离,使其往下滑动时再次触发onReachBottom,仅mescroll-body生效
|
||||
|
toTop: { |
||||
|
// 回到顶部按钮,需配置src才显示
|
||||
|
src: null, // 图片路径,默认null (绝对路径或网络图)
|
||||
|
offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000
|
||||
|
duration: 300, // 回到顶部的动画时长,默认300ms (当值为0或300则使用系统自带回到顶部,更流畅; 其他值则通过step模拟,部分机型可能不够流畅,所以非特殊情况不建议修改此项)
|
||||
|
btnClick: null, // 点击按钮的回调
|
||||
|
onShow: null, // 是否显示的回调
|
||||
|
zIndex: 9990, // fixed定位z-index值
|
||||
|
left: null, // 到左边的距离, 默认null. 此项有值时,right不生效. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||
|
right: 20, // 到右边的距离, 默认20 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||
|
bottom: 120, // 到底部的距离, 默认120 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||
|
safearea: false, // bottom的偏移量是否加上底部安全区的距离, 默认false, 需要适配iPhoneX时使用 (具体的界面如果不配置此项,则取本vue的safearea值)
|
||||
|
width: 72, // 回到顶部图标的宽度, 默认72 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||
|
radius: "50%" // 圆角, 默认"50%" (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||
|
}, |
||||
|
empty: { |
||||
|
use: true, // 是否显示空布局
|
||||
|
icon: null, // 图标路径
|
||||
|
tip: '~ 暂无相关数据 ~', // 提示
|
||||
|
btnText: '', // 按钮
|
||||
|
btnClick: null, // 点击按钮的回调
|
||||
|
onShow: null, // 是否显示的回调
|
||||
|
fixed: false, // 是否使用fixed定位,默认false; 配置fixed为true,以下的top和zIndex才生效 (transform会使fixed失效,最终会降级为absolute)
|
||||
|
top: "100rpx", // fixed定位的top值 (完整的单位值,如 "10%"; "100rpx")
|
||||
|
zIndex: 99 // fixed定位z-index值
|
||||
|
}, |
||||
|
onScroll: false // 是否监听滚动事件
|
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
/* 配置参数 */ |
||||
|
MeScroll.extend = function(userOption, defaultOption) { |
||||
|
if (!userOption) return defaultOption; |
||||
|
for (let key in defaultOption) { |
||||
|
if (userOption[key] == null) { |
||||
|
let def = defaultOption[key]; |
||||
|
if (def != null && typeof def === 'object') { |
||||
|
userOption[key] = MeScroll.extend({}, def); // 深度匹配
|
||||
|
} else { |
||||
|
userOption[key] = def; |
||||
|
} |
||||
|
} else if (typeof userOption[key] === 'object') { |
||||
|
MeScroll.extend(userOption[key], defaultOption[key]); // 深度匹配
|
||||
|
} |
||||
|
} |
||||
|
return userOption; |
||||
|
} |
||||
|
|
||||
|
/* 简单判断是否配置了颜色 (非透明,非白色) */ |
||||
|
MeScroll.prototype.hasColor = function(color) { |
||||
|
if(!color) return false; |
||||
|
let c = color.toLowerCase(); |
||||
|
return c != "#fff" && c != "#ffffff" && c != "transparent" && c != "white" |
||||
|
} |
||||
|
|
||||
|
/* -------初始化下拉刷新------- */ |
||||
|
MeScroll.prototype.initDownScroll = function() { |
||||
|
let me = this; |
||||
|
// 配置参数
|
||||
|
me.optDown = me.options.down || {}; |
||||
|
if(!me.optDown.textColor && me.hasColor(me.optDown.bgColor)) me.optDown.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
|
||||
|
me.extendDownScroll(me.optDown); |
||||
|
|
||||
|
// 如果是mescroll-body且配置了native,则禁止自定义的下拉刷新
|
||||
|
if(me.isScrollBody && me.optDown.native){ |
||||
|
me.optDown.use = false |
||||
|
}else{ |
||||
|
me.optDown.native = false // 仅mescroll-body支持,mescroll-uni不支持
|
||||
|
} |
||||
|
|
||||
|
me.downHight = 0; // 下拉区域的高度
|
||||
|
|
||||
|
// 在页面中加入下拉布局
|
||||
|
if (me.optDown.use && me.optDown.inited) { |
||||
|
// 初始化完毕的回调
|
||||
|
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
|
||||
|
me.optDown.inited(me); |
||||
|
}, 0) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 列表touchstart事件 */ |
||||
|
MeScroll.prototype.touchstartEvent = function(e) { |
||||
|
if (!this.optDown.use) return; |
||||
|
|
||||
|
this.startPoint = this.getPoint(e); // 记录起点
|
||||
|
this.startTop = this.getScrollTop(); // 记录此时的滚动条位置
|
||||
|
this.startAngle = 0; // 初始角度
|
||||
|
this.lastPoint = this.startPoint; // 重置上次move的点
|
||||
|
this.maxTouchmoveY = this.getBodyHeight() - this.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
|
||||
|
this.inTouchend = false; // 标记不是touchend
|
||||
|
} |
||||
|
|
||||
|
/* 列表touchmove事件 */ |
||||
|
MeScroll.prototype.touchmoveEvent = function(e) { |
||||
|
if (!this.optDown.use) return; |
||||
|
let me = this; |
||||
|
|
||||
|
let scrollTop = me.getScrollTop(); // 当前滚动条的距离
|
||||
|
let curPoint = me.getPoint(e); // 当前点
|
||||
|
|
||||
|
let moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
|
||||
|
|
||||
|
// 向下拉 && 在顶部
|
||||
|
// mescroll-body,直接判定在顶部即可
|
||||
|
// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
|
||||
|
// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
|
||||
|
if (moveY > 0 && ( |
||||
|
(me.isScrollBody && scrollTop <= 0) |
||||
|
|| |
||||
|
(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) ) |
||||
|
)) { |
||||
|
// 可下拉的条件
|
||||
|
if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling && |
||||
|
me.optUp.isBoth))) { |
||||
|
|
||||
|
// 下拉的初始角度是否在配置的范围内
|
||||
|
if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
|
||||
|
if (me.startAngle < me.optDown.minAngle) return; // 如果小于配置的角度,则不往下执行下拉刷新
|
||||
|
|
||||
|
// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
|
||||
|
if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) { |
||||
|
me.inTouchend = true; // 标记执行touchend
|
||||
|
me.touchendEvent(); // 提前触发touchend
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
me.preventDefault(e); // 阻止默认事件
|
||||
|
|
||||
|
let diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
|
||||
|
|
||||
|
// 下拉距离 < 指定距离
|
||||
|
if (me.downHight < me.optDown.offset) { |
||||
|
if (me.movetype !== 1) { |
||||
|
me.movetype = 1; // 加入标记,保证只执行一次
|
||||
|
me.isDownEndSuccess = null; // 重置是否加载成功的状态 (wxs执行的是wxs.wxs)
|
||||
|
me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
|
||||
|
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
|
||||
|
} |
||||
|
me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
|
||||
|
|
||||
|
// 指定距离 <= 下拉距离
|
||||
|
} else { |
||||
|
if (me.movetype !== 2) { |
||||
|
me.movetype = 2; // 加入标记,保证只执行一次
|
||||
|
me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
|
||||
|
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
|
||||
|
} |
||||
|
if (diff > 0) { // 向下拉
|
||||
|
me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
|
||||
|
} else { // 向上收
|
||||
|
me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
me.downHight = Math.round(me.downHight) // 取整
|
||||
|
let rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
|
||||
|
me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
me.lastPoint = curPoint; // 记录本次移动的点
|
||||
|
} |
||||
|
|
||||
|
/* 列表touchend事件 */ |
||||
|
MeScroll.prototype.touchendEvent = function(e) { |
||||
|
if (!this.optDown.use) return; |
||||
|
// 如果下拉区域高度已改变,则需重置回来
|
||||
|
if (this.isMoveDown) { |
||||
|
if (this.downHight >= this.optDown.offset) { |
||||
|
// 符合触发刷新的条件
|
||||
|
this.triggerDownScroll(); |
||||
|
} else { |
||||
|
// 不符合的话 则重置
|
||||
|
this.downHight = 0; |
||||
|
this.endDownScrollCall(this); |
||||
|
} |
||||
|
this.movetype = 0; |
||||
|
this.isMoveDown = false; |
||||
|
} else if (!this.isScrollBody && this.getScrollTop() === this.startTop) { // scroll-view到顶/左/右/底的滑动事件
|
||||
|
let isScrollUp = this.getPoint(e).y - this.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
|
||||
|
// 上滑
|
||||
|
if (isScrollUp) { |
||||
|
// 需检查滑动的角度
|
||||
|
let angle = this.getAngle(this.getPoint(e), this.startPoint); // 两点之间的角度,区间 [0,90]
|
||||
|
if (angle > 80) { |
||||
|
// 检查并触发上拉
|
||||
|
this.triggerUpScroll(true); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 根据点击滑动事件获取第一个手指的坐标 */ |
||||
|
MeScroll.prototype.getPoint = function(e) { |
||||
|
if (!e) { |
||||
|
return { |
||||
|
x: 0, |
||||
|
y: 0 |
||||
|
} |
||||
|
} |
||||
|
if (e.touches && e.touches[0]) { |
||||
|
return { |
||||
|
x: e.touches[0].pageX, |
||||
|
y: e.touches[0].pageY |
||||
|
} |
||||
|
} else if (e.changedTouches && e.changedTouches[0]) { |
||||
|
return { |
||||
|
x: e.changedTouches[0].pageX, |
||||
|
y: e.changedTouches[0].pageY |
||||
|
} |
||||
|
} else { |
||||
|
return { |
||||
|
x: e.clientX, |
||||
|
y: e.clientY |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 计算两点之间的角度: 区间 [0,90]*/ |
||||
|
MeScroll.prototype.getAngle = function(p1, p2) { |
||||
|
let x = Math.abs(p1.x - p2.x); |
||||
|
let y = Math.abs(p1.y - p2.y); |
||||
|
let z = Math.sqrt(x * x + y * y); |
||||
|
let angle = 0; |
||||
|
if (z !== 0) { |
||||
|
angle = Math.asin(y / z) / Math.PI * 180; |
||||
|
} |
||||
|
return angle |
||||
|
} |
||||
|
|
||||
|
/* 触发下拉刷新 */ |
||||
|
MeScroll.prototype.triggerDownScroll = function() { |
||||
|
if (this.optDown.beforeLoading && this.optDown.beforeLoading(this)) { |
||||
|
//return true则处于完全自定义状态
|
||||
|
} else { |
||||
|
this.showDownScroll(); // 下拉刷新中...
|
||||
|
!this.optDown.native && this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 显示下拉进度布局 */ |
||||
|
MeScroll.prototype.showDownScroll = function() { |
||||
|
this.isDownScrolling = true; // 标记下拉中
|
||||
|
if (this.optDown.native) { |
||||
|
uni.startPullDownRefresh(); // 系统自带的下拉刷新
|
||||
|
this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
|
||||
|
} else{ |
||||
|
this.downHight = this.optDown.offset; // 更新下拉区域高度
|
||||
|
this.showDownLoadingCall(this.downHight); // 下拉刷新中...
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
MeScroll.prototype.showDownLoadingCall = function(downHight) { |
||||
|
this.optDown.showLoading && this.optDown.showLoading(this, downHight); // 下拉刷新中...
|
||||
|
this.optDown.afterLoading && this.optDown.afterLoading(this, downHight); // 下拉刷新中...触发之后马上要执行的代码
|
||||
|
} |
||||
|
|
||||
|
/* 显示系统自带的下拉刷新时需要处理的业务 */ |
||||
|
MeScroll.prototype.onPullDownRefresh = function() { |
||||
|
this.isDownScrolling = true; // 标记下拉中
|
||||
|
this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
|
||||
|
this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
|
||||
|
} |
||||
|
|
||||
|
/* 结束下拉刷新 */ |
||||
|
MeScroll.prototype.endDownScroll = function() { |
||||
|
if (this.optDown.native) { // 结束原生下拉刷新
|
||||
|
this.isDownScrolling = false; |
||||
|
this.endDownScrollCall(this); |
||||
|
uni.stopPullDownRefresh(); |
||||
|
return |
||||
|
} |
||||
|
let me = this; |
||||
|
// 结束下拉刷新的方法
|
||||
|
let endScroll = function() { |
||||
|
me.downHight = 0; |
||||
|
me.isDownScrolling = false; |
||||
|
me.endDownScrollCall(me); |
||||
|
if(!me.isScrollBody){ |
||||
|
me.setScrollHeight(0) // scroll-view重置滚动区域,使数据不满屏时仍可检查触发翻页
|
||||
|
me.scrollTo(0,0) // scroll-view需重置滚动条到顶部,避免startTop大于0时,对下拉刷新的影响
|
||||
|
} |
||||
|
} |
||||
|
// 结束下拉刷新时的回调
|
||||
|
let delay = 0; |
||||
|
if (me.optDown.beforeEndDownScroll) { |
||||
|
delay = me.optDown.beforeEndDownScroll(me); // 结束下拉刷新的延时,单位ms
|
||||
|
if(me.isDownEndSuccess == null) delay = 0; // 没有执行加载中,则不延时
|
||||
|
} |
||||
|
if (typeof delay === 'number' && delay > 0) { |
||||
|
setTimeout(endScroll, delay); |
||||
|
} else { |
||||
|
endScroll(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
MeScroll.prototype.endDownScrollCall = function() { |
||||
|
this.optDown.endDownScroll && this.optDown.endDownScroll(this); |
||||
|
this.optDown.afterEndDownScroll && this.optDown.afterEndDownScroll(this); |
||||
|
} |
||||
|
|
||||
|
/* 锁定下拉刷新:isLock=ture,null锁定;isLock=false解锁 */ |
||||
|
MeScroll.prototype.lockDownScroll = function(isLock) { |
||||
|
if (isLock == null) isLock = true; |
||||
|
this.optDown.isLock = isLock; |
||||
|
} |
||||
|
|
||||
|
/* 锁定上拉加载:isLock=ture,null锁定;isLock=false解锁 */ |
||||
|
MeScroll.prototype.lockUpScroll = function(isLock) { |
||||
|
if (isLock == null) isLock = true; |
||||
|
this.optUp.isLock = isLock; |
||||
|
} |
||||
|
|
||||
|
/* -------初始化上拉加载------- */ |
||||
|
MeScroll.prototype.initUpScroll = function() { |
||||
|
let me = this; |
||||
|
// 配置参数
|
||||
|
me.optUp = me.options.up || {use: false} |
||||
|
if(!me.optUp.textColor && me.hasColor(me.optUp.bgColor)) me.optUp.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
|
||||
|
me.extendUpScroll(me.optUp); |
||||
|
|
||||
|
if (me.optUp.use === false) return; // 配置不使用上拉加载时,则不初始化上拉布局
|
||||
|
me.optUp.hasNext = true; // 如果使用上拉,则默认有下一页
|
||||
|
me.startNum = me.optUp.page.num + 1; // 记录page开始的页码
|
||||
|
|
||||
|
// 初始化完毕的回调
|
||||
|
if (me.optUp.inited) { |
||||
|
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
|
||||
|
me.optUp.inited(me); |
||||
|
}, 0) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/*滚动到底部的事件 (仅mescroll-body生效)*/ |
||||
|
MeScroll.prototype.onReachBottom = function() { |
||||
|
if (this.isScrollBody && !this.isUpScrolling) { // 只能支持下拉刷新的时候同时可以触发上拉加载,否则滚动到底部就需要上滑一点才能触发onReachBottom
|
||||
|
if (!this.optUp.isLock && this.optUp.hasNext) { |
||||
|
this.triggerUpScroll(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/*列表滚动事件 (仅mescroll-body生效)*/ |
||||
|
MeScroll.prototype.onPageScroll = function(e) { |
||||
|
if (!this.isScrollBody) return; |
||||
|
|
||||
|
// 更新滚动条的位置 (主要用于判断下拉刷新时,滚动条是否在顶部)
|
||||
|
this.setScrollTop(e.scrollTop); |
||||
|
|
||||
|
// 顶部按钮的显示隐藏
|
||||
|
if (e.scrollTop >= this.optUp.toTop.offset) { |
||||
|
this.showTopBtn(); |
||||
|
} else { |
||||
|
this.hideTopBtn(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/*列表滚动事件*/ |
||||
|
MeScroll.prototype.scroll = function(e, onScroll) { |
||||
|
// 更新滚动条的位置
|
||||
|
this.setScrollTop(e.scrollTop); |
||||
|
// 更新滚动内容高度
|
||||
|
this.setScrollHeight(e.scrollHeight); |
||||
|
|
||||
|
// 向上滑还是向下滑动
|
||||
|
if (this.preScrollY == null) this.preScrollY = 0; |
||||
|
this.isScrollUp = e.scrollTop - this.preScrollY > 0; |
||||
|
this.preScrollY = e.scrollTop; |
||||
|
|
||||
|
// 上滑 && 检查并触发上拉
|
||||
|
this.isScrollUp && this.triggerUpScroll(true); |
||||
|
|
||||
|
// 顶部按钮的显示隐藏
|
||||
|
if (e.scrollTop >= this.optUp.toTop.offset) { |
||||
|
this.showTopBtn(); |
||||
|
} else { |
||||
|
this.hideTopBtn(); |
||||
|
} |
||||
|
|
||||
|
// 滑动监听
|
||||
|
this.optUp.onScroll && onScroll && onScroll() |
||||
|
} |
||||
|
|
||||
|
/* 触发上拉加载 */ |
||||
|
MeScroll.prototype.triggerUpScroll = function(isCheck) { |
||||
|
if (!this.isUpScrolling && this.optUp.use && this.optUp.callback) { |
||||
|
// 是否校验在底部; 默认不校验
|
||||
|
if (isCheck === true) { |
||||
|
let canUp = false; |
||||
|
// 还有下一页 && 没有锁定 && 不在下拉中
|
||||
|
if (this.optUp.hasNext && !this.optUp.isLock && !this.isDownScrolling) { |
||||
|
if (this.getScrollBottom() <= this.optUp.offset) { // 到底部
|
||||
|
canUp = true; // 标记可上拉
|
||||
|
} |
||||
|
} |
||||
|
if (canUp === false) return; |
||||
|
} |
||||
|
this.showUpScroll(); // 上拉加载中...
|
||||
|
this.optUp.page.num++; // 预先加一页,如果失败则减回
|
||||
|
this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
|
||||
|
this.num = this.optUp.page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
|
||||
|
this.size = this.optUp.page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
|
||||
|
this.time = this.optUp.page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
|
||||
|
this.optUp.callback(this); // 执行回调,联网加载数据
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 显示上拉加载中 */ |
||||
|
MeScroll.prototype.showUpScroll = function() { |
||||
|
this.isUpScrolling = true; // 标记上拉加载中
|
||||
|
this.optUp.showLoading && this.optUp.showLoading(this); // 回调
|
||||
|
} |
||||
|
|
||||
|
/* 显示上拉无更多数据 */ |
||||
|
MeScroll.prototype.showNoMore = function() { |
||||
|
this.optUp.hasNext = false; // 标记无更多数据
|
||||
|
this.optUp.showNoMore && this.optUp.showNoMore(this); // 回调
|
||||
|
} |
||||
|
|
||||
|
/* 隐藏上拉区域**/ |
||||
|
MeScroll.prototype.hideUpScroll = function() { |
||||
|
this.optUp.hideUpScroll && this.optUp.hideUpScroll(this); // 回调
|
||||
|
} |
||||
|
|
||||
|
/* 结束上拉加载 */ |
||||
|
MeScroll.prototype.endUpScroll = function(isShowNoMore) { |
||||
|
if (isShowNoMore != null) { // isShowNoMore=null,不处理下拉状态,下拉刷新的时候调用
|
||||
|
if (isShowNoMore) { |
||||
|
this.showNoMore(); // isShowNoMore=true,显示无更多数据
|
||||
|
} else { |
||||
|
this.hideUpScroll(); // isShowNoMore=false,隐藏上拉加载
|
||||
|
} |
||||
|
} |
||||
|
this.isUpScrolling = false; // 标记结束上拉加载
|
||||
|
} |
||||
|
|
||||
|
/* 重置上拉加载列表为第一页 |
||||
|
*isShowLoading 是否显示进度布局; |
||||
|
* 1.默认null,不传参,则显示上拉加载的进度布局 |
||||
|
* 2.传参true, 则显示下拉刷新的进度布局 |
||||
|
* 3.传参false,则不显示上拉和下拉的进度 (常用于静默更新列表数据) |
||||
|
*/ |
||||
|
MeScroll.prototype.resetUpScroll = function(isShowLoading) { |
||||
|
if (this.optUp && this.optUp.use) { |
||||
|
let page = this.optUp.page; |
||||
|
this.prePageNum = page.num; // 缓存重置前的页码,加载失败可退回
|
||||
|
this.prePageTime = page.time; // 缓存重置前的时间,加载失败可退回
|
||||
|
page.num = this.startNum; // 重置为第一页
|
||||
|
page.time = null; // 重置时间为空
|
||||
|
if (!this.isDownScrolling && isShowLoading !== false) { // 如果不是下拉刷新触发的resetUpScroll并且不配置列表静默更新,则显示进度;
|
||||
|
if (isShowLoading == null) { |
||||
|
this.removeEmpty(); // 移除空布局
|
||||
|
this.showUpScroll(); // 不传参,默认显示上拉加载的进度布局
|
||||
|
} else { |
||||
|
this.showDownScroll(); // 传true,显示下拉刷新的进度布局,不清空列表
|
||||
|
} |
||||
|
} |
||||
|
this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
|
||||
|
this.num = page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
|
||||
|
this.size = page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
|
||||
|
this.time = page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
|
||||
|
this.optUp.callback && this.optUp.callback(this); // 执行上拉回调
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 设置page.num的值 */ |
||||
|
MeScroll.prototype.setPageNum = function(num) { |
||||
|
this.optUp.page.num = num - 1; |
||||
|
} |
||||
|
|
||||
|
/* 设置page.size的值 */ |
||||
|
MeScroll.prototype.setPageSize = function(size) { |
||||
|
this.optUp.page.size = size; |
||||
|
} |
||||
|
|
||||
|
/* 联网回调成功,结束下拉刷新和上拉加载 |
||||
|
* dataSize: 当前页的数据量(必传) |
||||
|
* totalPage: 总页数(必传) |
||||
|
* systime: 服务器时间 (可空) |
||||
|
*/ |
||||
|
MeScroll.prototype.endByPage = function(dataSize, totalPage, systime) { |
||||
|
let hasNext; |
||||
|
if (this.optUp.use && totalPage != null) hasNext = this.optUp.page.num < totalPage; // 是否还有下一页
|
||||
|
this.endSuccess(dataSize, hasNext, systime); |
||||
|
} |
||||
|
|
||||
|
/* 联网回调成功,结束下拉刷新和上拉加载 |
||||
|
* dataSize: 当前页的数据量(必传) |
||||
|
* totalSize: 列表所有数据总数量(必传) |
||||
|
* systime: 服务器时间 (可空) |
||||
|
*/ |
||||
|
MeScroll.prototype.endBySize = function(dataSize, totalSize, systime) { |
||||
|
let hasNext; |
||||
|
if (this.optUp.use && totalSize != null) { |
||||
|
let loadSize = (this.optUp.page.num - 1) * this.optUp.page.size + dataSize; // 已加载的数据总数
|
||||
|
hasNext = loadSize < totalSize; // 是否还有下一页
|
||||
|
} |
||||
|
this.endSuccess(dataSize, hasNext, systime); |
||||
|
} |
||||
|
|
||||
|
/* 联网回调成功,结束下拉刷新和上拉加载 |
||||
|
* dataSize: 当前页的数据个数(不是所有页的数据总和),用于上拉加载判断是否还有下一页.如果不传,则会判断还有下一页 |
||||
|
* hasNext: 是否还有下一页,布尔类型;用来解决这个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据dataSize判断,则需翻到第三页才会知道无更多数据,如果传了hasNext,则翻到第二页即可显示无更多数据. |
||||
|
* systime: 服务器时间(可空);用来解决这个小问题:当准备翻下一页时,数据库新增了几条记录,此时翻下一页,前面的几条数据会和上一页的重复;这里传入了systime,那么upCallback的page.time就会有值,把page.time传给服务器,让后台过滤新加入的那几条记录 |
||||
|
*/ |
||||
|
MeScroll.prototype.endSuccess = function(dataSize, hasNext, systime) { |
||||
|
let me = this; |
||||
|
// 结束下拉刷新
|
||||
|
if (me.isDownScrolling) { |
||||
|
me.isDownEndSuccess = true |
||||
|
me.endDownScroll(); |
||||
|
} |
||||
|
|
||||
|
// 结束上拉加载
|
||||
|
if (me.optUp.use) { |
||||
|
let isShowNoMore; // 是否已无更多数据
|
||||
|
if (dataSize != null) { |
||||
|
let pageNum = me.optUp.page.num; // 当前页码
|
||||
|
let pageSize = me.optUp.page.size; // 每页长度
|
||||
|
// 如果是第一页
|
||||
|
if (pageNum === 1) { |
||||
|
if (systime) me.optUp.page.time = systime; // 设置加载列表数据第一页的时间
|
||||
|
} |
||||
|
if (dataSize < pageSize || hasNext === false) { |
||||
|
// 返回的数据不满一页时,则说明已无更多数据
|
||||
|
me.optUp.hasNext = false; |
||||
|
if (dataSize === 0 && pageNum === 1) { |
||||
|
// 如果第一页无任何数据且配置了空布局
|
||||
|
isShowNoMore = false; |
||||
|
me.showEmpty(); |
||||
|
} else { |
||||
|
// 总列表数少于配置的数量,则不显示无更多数据
|
||||
|
let allDataSize = (pageNum - 1) * pageSize + dataSize; |
||||
|
if (allDataSize < me.optUp.noMoreSize) { |
||||
|
isShowNoMore = false; |
||||
|
} else { |
||||
|
isShowNoMore = true; |
||||
|
} |
||||
|
me.removeEmpty(); // 移除空布局
|
||||
|
} |
||||
|
} else { |
||||
|
// 还有下一页
|
||||
|
isShowNoMore = false; |
||||
|
me.optUp.hasNext = true; |
||||
|
me.removeEmpty(); // 移除空布局
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 隐藏上拉
|
||||
|
me.endUpScroll(isShowNoMore); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 回调失败,结束下拉刷新和上拉加载 */ |
||||
|
MeScroll.prototype.endErr = function(errDistance) { |
||||
|
// 结束下拉,回调失败重置回原来的页码和时间
|
||||
|
if (this.isDownScrolling) { |
||||
|
this.isDownEndSuccess = false |
||||
|
let page = this.optUp.page; |
||||
|
if (page && this.prePageNum) { |
||||
|
page.num = this.prePageNum; |
||||
|
page.time = this.prePageTime; |
||||
|
} |
||||
|
this.endDownScroll(); |
||||
|
} |
||||
|
// 结束上拉,回调失败重置回原来的页码
|
||||
|
if (this.isUpScrolling) { |
||||
|
this.optUp.page.num--; |
||||
|
this.endUpScroll(false); |
||||
|
// 如果是mescroll-body,则需往回滚一定距离
|
||||
|
if(this.isScrollBody && errDistance !== 0){ // 不处理0
|
||||
|
if(!errDistance) errDistance = this.optUp.errDistance; // 不传,则取默认
|
||||
|
this.scrollTo(this.getScrollTop() - errDistance, 0) // 往上回滚的距离
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 显示空布局 */ |
||||
|
MeScroll.prototype.showEmpty = function() { |
||||
|
this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(true) |
||||
|
} |
||||
|
|
||||
|
/* 移除空布局 */ |
||||
|
MeScroll.prototype.removeEmpty = function() { |
||||
|
this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(false) |
||||
|
} |
||||
|
|
||||
|
/* 显示回到顶部的按钮 */ |
||||
|
MeScroll.prototype.showTopBtn = function() { |
||||
|
if (!this.topBtnShow) { |
||||
|
this.topBtnShow = true; |
||||
|
this.optUp.toTop.onShow && this.optUp.toTop.onShow(true); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 隐藏回到顶部的按钮 */ |
||||
|
MeScroll.prototype.hideTopBtn = function() { |
||||
|
if (this.topBtnShow) { |
||||
|
this.topBtnShow = false; |
||||
|
this.optUp.toTop.onShow && this.optUp.toTop.onShow(false); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 获取滚动条的位置 */ |
||||
|
MeScroll.prototype.getScrollTop = function() { |
||||
|
return this.scrollTop || 0 |
||||
|
} |
||||
|
|
||||
|
/* 记录滚动条的位置 */ |
||||
|
MeScroll.prototype.setScrollTop = function(y) { |
||||
|
this.scrollTop = y; |
||||
|
} |
||||
|
|
||||
|
/* 滚动到指定位置 */ |
||||
|
MeScroll.prototype.scrollTo = function(y, t) { |
||||
|
this.myScrollTo && this.myScrollTo(y, t) // scrollview需自定义回到顶部方法
|
||||
|
} |
||||
|
|
||||
|
/* 自定义scrollTo */ |
||||
|
MeScroll.prototype.resetScrollTo = function(myScrollTo) { |
||||
|
this.myScrollTo = myScrollTo |
||||
|
} |
||||
|
|
||||
|
/* 滚动条到底部的距离 */ |
||||
|
MeScroll.prototype.getScrollBottom = function() { |
||||
|
return this.getScrollHeight() - this.getClientHeight() - this.getScrollTop() |
||||
|
} |
||||
|
|
||||
|
/* 计步器 |
||||
|
star: 开始值 |
||||
|
end: 结束值 |
||||
|
callback(step,timer): 回调step值,计步器timer,可自行通过window.clearInterval(timer)结束计步器; |
||||
|
t: 计步时长,传0则直接回调end值;不传则默认300ms |
||||
|
rate: 周期;不传则默认30ms计步一次 |
||||
|
* */ |
||||
|
MeScroll.prototype.getStep = function(star, end, callback, t, rate) { |
||||
|
let diff = end - star; // 差值
|
||||
|
if (t === 0 || diff === 0) { |
||||
|
callback && callback(end); |
||||
|
return; |
||||
|
} |
||||
|
t = t || 300; // 时长 300ms
|
||||
|
rate = rate || 30; // 周期 30ms
|
||||
|
let count = t / rate; // 次数
|
||||
|
let step = diff / count; // 步长
|
||||
|
let i = 0; // 计数
|
||||
|
let timer = setInterval(function() { |
||||
|
if (i < count - 1) { |
||||
|
star += step; |
||||
|
callback && callback(star, timer); |
||||
|
i++; |
||||
|
} else { |
||||
|
callback && callback(end, timer); // 最后一次直接设置end,避免计算误差
|
||||
|
clearInterval(timer); |
||||
|
} |
||||
|
}, rate); |
||||
|
} |
||||
|
|
||||
|
/* 滚动容器的高度 */ |
||||
|
MeScroll.prototype.getClientHeight = function(isReal) { |
||||
|
let h = this.clientHeight || 0 |
||||
|
if (h === 0 && isReal !== true) { // 未获取到容器的高度,可临时取body的高度 (可能会有误差)
|
||||
|
h = this.getBodyHeight() |
||||
|
} |
||||
|
return h |
||||
|
} |
||||
|
MeScroll.prototype.setClientHeight = function(h) { |
||||
|
this.clientHeight = h; |
||||
|
} |
||||
|
|
||||
|
/* 滚动内容的高度 */ |
||||
|
MeScroll.prototype.getScrollHeight = function() { |
||||
|
return this.scrollHeight || 0; |
||||
|
} |
||||
|
MeScroll.prototype.setScrollHeight = function(h) { |
||||
|
this.scrollHeight = h; |
||||
|
} |
||||
|
|
||||
|
/* body的高度 */ |
||||
|
MeScroll.prototype.getBodyHeight = function() { |
||||
|
return this.bodyHeight || 0; |
||||
|
} |
||||
|
MeScroll.prototype.setBodyHeight = function(h) { |
||||
|
this.bodyHeight = h; |
||||
|
} |
||||
|
|
||||
|
/* 阻止浏览器默认滚动事件 */ |
||||
|
MeScroll.prototype.preventDefault = function(e) { |
||||
|
// 小程序不支持e.preventDefault, 已在wxs中禁止
|
||||
|
// app的bounce只能通过配置pages.json的style.app-plus.bounce为"none"来禁止, 或使用renderjs禁止
|
||||
|
// cancelable:是否可以被禁用; defaultPrevented:是否已经被禁用
|
||||
|
if (e && e.cancelable && !e.defaultPrevented) e.preventDefault() |
||||
|
} |
||||
@ -0,0 +1,424 @@ |
|||||
|
<template> |
||||
|
<view class="mescroll-uni-warp"> |
||||
|
<scroll-view :id="viewId" class="mescroll-uni" :class="{'mescroll-uni-fixed':isFixed}" :style="{'height':scrollHeight,'padding-top':padTop,'padding-bottom':padBottom,'top':fixedTop,'bottom':fixedBottom}" :scroll-top="scrollTop" :scroll-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true" :throttle="false"> |
||||
|
<view class="mescroll-uni-content mescroll-render-touch" |
||||
|
@touchstart="wxsBiz.touchstartEvent" |
||||
|
@touchmove="wxsBiz.touchmoveEvent" |
||||
|
@touchend="wxsBiz.touchendEvent" |
||||
|
@touchcancel="wxsBiz.touchendEvent" |
||||
|
:change:prop="wxsBiz.propObserver" |
||||
|
:prop="wxsProp"> |
||||
|
<!-- 状态栏 --> |
||||
|
<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view> |
||||
|
|
||||
|
<view class="mescroll-wxs-content" :style="{'transform': translateY, 'transition': transition}" :change:prop="wxsBiz.callObserver" :prop="callProp"> |
||||
|
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)--> |
||||
|
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> --> |
||||
|
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}"> |
||||
|
<view class="downwarp-content"> |
||||
|
<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view> |
||||
|
<view class="downwarp-tip">{{downText}}</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 列表内容 --> |
||||
|
<slot></slot> |
||||
|
|
||||
|
<!-- 空布局 --> |
||||
|
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty> |
||||
|
|
||||
|
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)--> |
||||
|
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> --> |
||||
|
<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}"> |
||||
|
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) --> |
||||
|
<view v-show="upLoadType===1"> |
||||
|
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view> |
||||
|
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view> |
||||
|
</view> |
||||
|
<!-- 无数据 --> |
||||
|
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
|
||||
|
<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) --> |
||||
|
<!-- #ifdef H5 --> |
||||
|
<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view> |
||||
|
<!-- #endif --> |
||||
|
|
||||
|
<!-- 适配iPhoneX --> |
||||
|
<view v-if="safearea" class="mescroll-safearea"></view> |
||||
|
</view> |
||||
|
</scroll-view> |
||||
|
|
||||
|
<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)--> |
||||
|
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top> |
||||
|
|
||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||
|
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 --> |
||||
|
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view> |
||||
|
<!-- #endif --> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<!-- 微信小程序, QQ小程序, app, h5使用wxs --> |
||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||
|
<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script> |
||||
|
<!-- #endif --> |
||||
|
|
||||
|
<!-- app, h5使用renderjs --> |
||||
|
<!-- #ifdef APP-PLUS || H5 --> |
||||
|
<script module="renderBiz" lang="renderjs"> |
||||
|
import renderBiz from './wxs/renderjs.js'; |
||||
|
export default { |
||||
|
mixins:[renderBiz] |
||||
|
} |
||||
|
</script> |
||||
|
<!-- #endif --> |
||||
|
|
||||
|
<script> |
||||
|
// 引入mescroll-uni.js,处理核心逻辑 |
||||
|
import MeScroll from './mescroll-uni.js'; |
||||
|
// 引入全局配置 |
||||
|
import GlobalOption from './mescroll-uni-option.js'; |
||||
|
// 引入空布局组件 |
||||
|
import MescrollEmpty from './components/mescroll-empty.vue'; |
||||
|
// 引入回到顶部组件 |
||||
|
import MescrollTop from './components/mescroll-top.vue'; |
||||
|
// 引入兼容wxs(含renderjs)写法的mixins |
||||
|
import WxsMixin from './wxs/mixins.js'; |
||||
|
|
||||
|
export default { |
||||
|
mixins: [WxsMixin], |
||||
|
components: { |
||||
|
MescrollEmpty, |
||||
|
MescrollTop |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
mescroll: {optDown:{},optUp:{}}, // mescroll实例 |
||||
|
viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素) |
||||
|
downHight: 0, //下拉刷新: 容器高度 |
||||
|
downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1) |
||||
|
downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll) |
||||
|
upLoadType: 0, // 上拉加载状态: 0(loading前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示) |
||||
|
isShowEmpty: false, // 是否显示空布局 |
||||
|
isShowToTop: false, // 是否显示回到顶部按钮 |
||||
|
scrollTop: 0, // 滚动条的位置 |
||||
|
scrollAnim: false, // 是否开启滚动动画 |
||||
|
windowTop: 0, // 可使用窗口的顶部位置 |
||||
|
windowBottom: 0, // 可使用窗口的底部位置 |
||||
|
windowHeight: 0, // 可使用窗口的高度 |
||||
|
statusBarHeight: 0 // 状态栏高度 |
||||
|
} |
||||
|
}, |
||||
|
props: { |
||||
|
down: Object, // 下拉刷新的参数配置 |
||||
|
up: Object, // 上拉加载的参数配置 |
||||
|
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||
|
topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变) |
||||
|
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||
|
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用) |
||||
|
fixed: { // 是否通过fixed固定mescroll的高度, 默认true |
||||
|
type: Boolean, |
||||
|
default: true |
||||
|
}, |
||||
|
height: [String, Number], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||
|
bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) |
||||
|
type: Boolean, |
||||
|
default: true |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
// 是否使用fixed定位 (当height有值,则不使用) |
||||
|
isFixed(){ |
||||
|
return !this.height && this.fixed |
||||
|
}, |
||||
|
// mescroll的高度 |
||||
|
scrollHeight(){ |
||||
|
if (this.isFixed) { |
||||
|
return "auto" |
||||
|
} else if(this.height){ |
||||
|
return this.toPx(this.height) + 'px' |
||||
|
}else{ |
||||
|
return "100%" |
||||
|
} |
||||
|
}, |
||||
|
// 下拉布局往下偏移的距离 (px) |
||||
|
numTop() { |
||||
|
return this.toPx(this.top) |
||||
|
}, |
||||
|
fixedTop() { |
||||
|
return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0 |
||||
|
}, |
||||
|
padTop() { |
||||
|
return !this.isFixed ? this.numTop + 'px' : 0 |
||||
|
}, |
||||
|
// 上拉布局往上偏移 (px) |
||||
|
numBottom() { |
||||
|
return this.toPx(this.bottom) |
||||
|
}, |
||||
|
fixedBottom() { |
||||
|
return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0 |
||||
|
}, |
||||
|
padBottom() { |
||||
|
return !this.isFixed ? this.numBottom + 'px' : 0 |
||||
|
}, |
||||
|
// 是否为重置下拉的状态 |
||||
|
isDownReset(){ |
||||
|
return this.downLoadType===3 || this.downLoadType===4 |
||||
|
}, |
||||
|
// 过渡 |
||||
|
transition() { |
||||
|
return this.isDownReset ? 'transform 300ms' : ''; |
||||
|
}, |
||||
|
translateY() { |
||||
|
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外 |
||||
|
}, |
||||
|
// 列表是否可滑动 |
||||
|
scrollable(){ |
||||
|
return this.downLoadType===0 || this.isDownReset |
||||
|
}, |
||||
|
// 是否在加载中 |
||||
|
isDownLoading(){ |
||||
|
return this.downLoadType === 3 |
||||
|
}, |
||||
|
// 旋转的角度 |
||||
|
downRotate(){ |
||||
|
return 'rotate(' + 360 * this.downRate + 'deg)' |
||||
|
}, |
||||
|
// 文本提示 |
||||
|
downText(){ |
||||
|
if(!this.mescroll) return ""; // 避免头条小程序初始化时报错 |
||||
|
switch (this.downLoadType){ |
||||
|
case 1: return this.mescroll.optDown.textInOffset; |
||||
|
case 2: return this.mescroll.optDown.textOutOffset; |
||||
|
case 3: return this.mescroll.optDown.textLoading; |
||||
|
case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset; |
||||
|
default: return this.mescroll.optDown.textInOffset; |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
//number,rpx,upx,px,% --> px的数值 |
||||
|
toPx(num){ |
||||
|
if(typeof num === "string"){ |
||||
|
if (num.indexOf('px') !== -1) { |
||||
|
if(num.indexOf('rpx') !== -1) { // "10rpx" |
||||
|
num = num.replace('rpx', ''); |
||||
|
} else if(num.indexOf('upx') !== -1) { // "10upx" |
||||
|
num = num.replace('upx', ''); |
||||
|
} else { // "10px" |
||||
|
return Number(num.replace('px', '')) |
||||
|
} |
||||
|
}else if (num.indexOf('%') !== -1){ |
||||
|
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10% |
||||
|
let rate = Number(num.replace("%","")) / 100 |
||||
|
return this.windowHeight * rate |
||||
|
} |
||||
|
} |
||||
|
return num ? uni.upx2px(Number(num)) : 0 |
||||
|
}, |
||||
|
//注册列表滚动事件,用于下拉刷新和上拉加载 |
||||
|
scroll(e) { |
||||
|
this.mescroll.scroll(e.detail, () => { |
||||
|
this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动 |
||||
|
}) |
||||
|
}, |
||||
|
// 点击空布局的按钮回调 |
||||
|
emptyClick() { |
||||
|
this.$emit('emptyclick', this.mescroll) |
||||
|
}, |
||||
|
// 点击回到顶部的按钮回调 |
||||
|
toTopClick() { |
||||
|
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部 |
||||
|
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调 |
||||
|
}, |
||||
|
// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页) |
||||
|
setClientHeight() { |
||||
|
if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) { |
||||
|
this.isExec = true; // 避免多次获取 |
||||
|
this.$nextTick(() => { // 确保dom已渲染 |
||||
|
this.getClientInfo(data=>{ |
||||
|
this.isExec = false; |
||||
|
if (data) { |
||||
|
this.mescroll.setClientHeight(data.height); |
||||
|
} else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次 |
||||
|
this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1; |
||||
|
setTimeout(() => { |
||||
|
this.setClientHeight() |
||||
|
}, this.clientNum * 100) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
}, |
||||
|
// 获取滚动区域的信息 |
||||
|
getClientInfo(success){ |
||||
|
let query = uni.createSelectorQuery(); |
||||
|
// #ifndef MP-ALIPAY || MP-DINGTALK |
||||
|
query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值 |
||||
|
// #endif |
||||
|
let view = query.select('#' + this.viewId); |
||||
|
view.boundingClientRect(data => { |
||||
|
success(data) |
||||
|
}).exec(); |
||||
|
} |
||||
|
}, |
||||
|
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效 |
||||
|
created() { |
||||
|
let vm = this; |
||||
|
|
||||
|
let diyOption = { |
||||
|
// 下拉刷新的配置 |
||||
|
down: { |
||||
|
inOffset() { |
||||
|
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||
|
}, |
||||
|
outOffset() { |
||||
|
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||
|
}, |
||||
|
onMoving(mescroll, rate, downHight) { |
||||
|
// 下拉过程中的回调,滑动过程一直在执行; |
||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||
|
vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1) |
||||
|
}, |
||||
|
showLoading(mescroll, downHight) { |
||||
|
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删) |
||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||
|
}, |
||||
|
beforeEndDownScroll(mescroll){ |
||||
|
vm.downLoadType = 4; |
||||
|
return mescroll.optDown.beforeEndDelay // 延时结束的时长 |
||||
|
}, |
||||
|
endDownScroll() { |
||||
|
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删) |
||||
|
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||
|
vm.downResetTimer && clearTimeout(vm.downResetTimer) |
||||
|
vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整 |
||||
|
if(vm.downLoadType===4) vm.downLoadType = 0 |
||||
|
},300) |
||||
|
}, |
||||
|
// 派发下拉刷新的回调 |
||||
|
callback: function(mescroll) { |
||||
|
vm.$emit('down', mescroll) |
||||
|
} |
||||
|
}, |
||||
|
// 上拉加载的配置 |
||||
|
up: { |
||||
|
// 显示加载中的回调 |
||||
|
showLoading() { |
||||
|
vm.upLoadType = 1; |
||||
|
}, |
||||
|
// 显示无更多数据的回调 |
||||
|
showNoMore() { |
||||
|
vm.upLoadType = 2; |
||||
|
}, |
||||
|
// 隐藏上拉加载的回调 |
||||
|
hideUpScroll(mescroll) { |
||||
|
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3; |
||||
|
}, |
||||
|
// 空布局 |
||||
|
empty: { |
||||
|
onShow(isShow) { // 显示隐藏的回调 |
||||
|
vm.isShowEmpty = isShow; |
||||
|
} |
||||
|
}, |
||||
|
// 回到顶部 |
||||
|
toTop: { |
||||
|
onShow(isShow) { // 显示隐藏的回调 |
||||
|
vm.isShowToTop = isShow; |
||||
|
} |
||||
|
}, |
||||
|
// 派发上拉加载的回调 |
||||
|
callback: function(mescroll) { |
||||
|
vm.$emit('up', mescroll); |
||||
|
// 更新容器的高度 (多mescroll的情况) |
||||
|
vm.setClientHeight() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置 |
||||
|
let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // 深拷贝,避免对props的影响 |
||||
|
MeScroll.extend(myOption, diyOption); // 混入具体界面的配置 |
||||
|
|
||||
|
// 初始化MeScroll对象 |
||||
|
vm.mescroll = new MeScroll(myOption); |
||||
|
vm.mescroll.viewId = vm.viewId; // 附带id |
||||
|
// init回调mescroll对象 |
||||
|
vm.$emit('init', vm.mescroll); |
||||
|
|
||||
|
// 设置高度 |
||||
|
const sys = uni.getSystemInfoSync(); |
||||
|
if(sys.windowTop) vm.windowTop = sys.windowTop; |
||||
|
if(sys.windowBottom) vm.windowBottom = sys.windowBottom; |
||||
|
if(sys.windowHeight) vm.windowHeight = sys.windowHeight; |
||||
|
if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight; |
||||
|
// 使down的bottomOffset生效 |
||||
|
vm.mescroll.setBodyHeight(sys.windowHeight); |
||||
|
|
||||
|
// 因为使用的是scrollview,这里需自定义scrollTo |
||||
|
vm.mescroll.resetScrollTo((y, t) => { |
||||
|
vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡 |
||||
|
if(typeof y === 'string'){ |
||||
|
// 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现 |
||||
|
vm.getClientInfo(function(rect){ |
||||
|
let mescrollTop = rect.top // mescroll到顶部的距离 |
||||
|
let selector; |
||||
|
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){ |
||||
|
selector = '#'+y // 不带#和. 则默认为id选择器 |
||||
|
}else{ |
||||
|
selector = y |
||||
|
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK |
||||
|
if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询) |
||||
|
selector = y.split('>>>')[1].trim() |
||||
|
} |
||||
|
// #endif |
||||
|
} |
||||
|
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){ |
||||
|
if (rect) { |
||||
|
let curY = vm.mescroll.getScrollTop() |
||||
|
let top = rect.top - mescrollTop |
||||
|
top += curY |
||||
|
if(!vm.isFixed) top -= vm.numTop |
||||
|
vm.scrollTop = curY; |
||||
|
vm.$nextTick(function() { |
||||
|
vm.scrollTop = top |
||||
|
}) |
||||
|
} else{ |
||||
|
console.error(selector + ' does not exist'); |
||||
|
} |
||||
|
}).exec() |
||||
|
}) |
||||
|
return; |
||||
|
} |
||||
|
let curY = vm.mescroll.getScrollTop() |
||||
|
if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡 |
||||
|
vm.scrollTop = curY; |
||||
|
vm.$nextTick(function() { |
||||
|
vm.scrollTop = y |
||||
|
}) |
||||
|
} else { |
||||
|
vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t |
||||
|
vm.scrollTop = step |
||||
|
}, t) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值 |
||||
|
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else { |
||||
|
vm.mescroll.optUp.toTop.safearea = vm.safearea; |
||||
|
} |
||||
|
}, |
||||
|
mounted() { |
||||
|
// 设置容器的高度 |
||||
|
this.setClientHeight() |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style> |
||||
|
@import "./mescroll-uni.css"; |
||||
|
@import "./components/mescroll-down.css"; |
||||
|
@import './components/mescroll-up.css'; |
||||
|
</style> |
||||
@ -0,0 +1,48 @@ |
|||||
|
/** |
||||
|
* mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期 |
||||
|
*/ |
||||
|
const MescrollCompMixin = { |
||||
|
// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件 (一级)
|
||||
|
onPageScroll(e) { |
||||
|
this.handlePageScroll(e) |
||||
|
}, |
||||
|
onReachBottom() { |
||||
|
this.handleReachBottom() |
||||
|
}, |
||||
|
// 当down的native: true时, 还需传递此方法进到子组件
|
||||
|
onPullDownRefresh(){ |
||||
|
this.handlePullDownRefresh() |
||||
|
}, |
||||
|
// mescroll-body写在子子子...组件的情况 (多级)
|
||||
|
data() { |
||||
|
return { |
||||
|
mescroll: { |
||||
|
onPageScroll: e=>{ |
||||
|
this.handlePageScroll(e) |
||||
|
}, |
||||
|
onReachBottom: ()=>{ |
||||
|
this.handleReachBottom() |
||||
|
}, |
||||
|
onPullDownRefresh: ()=>{ |
||||
|
this.handlePullDownRefresh() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
methods:{ |
||||
|
handlePageScroll(e){ |
||||
|
let item = this.$refs["mescrollItem"]; |
||||
|
if(item && item.mescroll) item.mescroll.onPageScroll(e); |
||||
|
}, |
||||
|
handleReachBottom(){ |
||||
|
let item = this.$refs["mescrollItem"]; |
||||
|
if(item && item.mescroll) item.mescroll.onReachBottom(); |
||||
|
}, |
||||
|
handlePullDownRefresh(){ |
||||
|
let item = this.$refs["mescrollItem"]; |
||||
|
if(item && item.mescroll) item.mescroll.onPullDownRefresh(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default MescrollCompMixin; |
||||
@ -0,0 +1,59 @@ |
|||||
|
/** |
||||
|
* mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例) |
||||
|
*/ |
||||
|
const MescrollMoreItemMixin = { |
||||
|
// 支付宝小程序不支持props的mixin,需写在具体的页面中
|
||||
|
// #ifndef MP-ALIPAY || MP-DINGTALK
|
||||
|
props:{ |
||||
|
i: Number, // 每个tab页的专属下标
|
||||
|
index: { // 当前tab的下标
|
||||
|
type: Number, |
||||
|
default(){ |
||||
|
return 0 |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
// #endif
|
||||
|
data() { |
||||
|
return { |
||||
|
downOption:{ |
||||
|
auto:false // 不自动加载
|
||||
|
}, |
||||
|
upOption:{ |
||||
|
auto:false // 不自动加载
|
||||
|
}, |
||||
|
isInit: false // 当前tab是否已初始化
|
||||
|
} |
||||
|
}, |
||||
|
watch:{ |
||||
|
// 监听下标的变化
|
||||
|
index(val){ |
||||
|
if (this.i === val && !this.isInit) { |
||||
|
this.isInit = true; // 标记为true
|
||||
|
this.mescroll && this.mescroll.triggerDownScroll(); |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
// 以ref的方式初始化mescroll对象 (兼容字节跳动小程序)
|
||||
|
mescrollInitByRef() { |
||||
|
if(!this.mescroll || !this.mescroll.resetUpScroll){ |
||||
|
// 字节跳动小程序编辑器不支持一个页面存在相同的ref, 多mescroll的ref需动态生成, 格式为'mescrollRef下标'
|
||||
|
let mescrollRef = this.$refs.mescrollRef || this.$refs['mescrollRef'+this.i]; |
||||
|
if(mescrollRef) this.mescroll = mescrollRef.mescroll |
||||
|
} |
||||
|
}, |
||||
|
// mescroll组件初始化的回调,可获取到mescroll对象 (覆盖mescroll-mixins.js的mescrollInit, 为了标记isInit)
|
||||
|
mescrollInit(mescroll) { |
||||
|
this.mescroll = mescroll; |
||||
|
this.mescrollInitByRef && this.mescrollInitByRef(); // 兼容字节跳动小程序
|
||||
|
// 自动加载当前tab的数据
|
||||
|
if(this.i === this.index){ |
||||
|
this.isInit = true; // 标记为true
|
||||
|
this.mescroll.triggerDownScroll(); |
||||
|
} |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default MescrollMoreItemMixin; |
||||
@ -0,0 +1,74 @@ |
|||||
|
/** |
||||
|
* mescroll-body写在子组件时, 需通过mescroll的mixins补充子组件缺少的生命周期 |
||||
|
*/ |
||||
|
const MescrollMoreMixin = { |
||||
|
data() { |
||||
|
return { |
||||
|
tabIndex: 0, // 当前tab下标
|
||||
|
mescroll: { |
||||
|
onPageScroll: e=>{ |
||||
|
this.handlePageScroll(e) |
||||
|
}, |
||||
|
onReachBottom: ()=>{ |
||||
|
this.handleReachBottom() |
||||
|
}, |
||||
|
onPullDownRefresh: ()=>{ |
||||
|
this.handlePullDownRefresh() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件
|
||||
|
onPageScroll(e) { |
||||
|
this.handlePageScroll(e) |
||||
|
}, |
||||
|
onReachBottom() { |
||||
|
this.handleReachBottom() |
||||
|
}, |
||||
|
// 当down的native: true时, 还需传递此方法进到子组件
|
||||
|
onPullDownRefresh(){ |
||||
|
this.handlePullDownRefresh() |
||||
|
}, |
||||
|
methods:{ |
||||
|
handlePageScroll(e){ |
||||
|
let mescroll = this.getMescroll(this.tabIndex); |
||||
|
mescroll && mescroll.onPageScroll(e); |
||||
|
}, |
||||
|
handleReachBottom(){ |
||||
|
let mescroll = this.getMescroll(this.tabIndex); |
||||
|
mescroll && mescroll.onReachBottom(); |
||||
|
}, |
||||
|
handlePullDownRefresh(){ |
||||
|
let mescroll = this.getMescroll(this.tabIndex); |
||||
|
mescroll && mescroll.onPullDownRefresh(); |
||||
|
}, |
||||
|
// 根据下标获取对应子组件的mescroll
|
||||
|
getMescroll(i){ |
||||
|
if(!this.mescrollItems) this.mescrollItems = []; |
||||
|
if(!this.mescrollItems[i]) { |
||||
|
// v-for中的refs
|
||||
|
let vForItem = this.$refs["mescrollItem"]; |
||||
|
if(vForItem){ |
||||
|
this.mescrollItems[i] = vForItem[i] |
||||
|
}else{ |
||||
|
// 普通的refs,不可重复
|
||||
|
this.mescrollItems[i] = this.$refs["mescrollItem"+i]; |
||||
|
} |
||||
|
} |
||||
|
let item = this.mescrollItems[i] |
||||
|
return item ? item.mescroll : null |
||||
|
}, |
||||
|
// 切换tab,恢复滚动条位置
|
||||
|
tabChange(i){ |
||||
|
let mescroll = this.getMescroll(i); |
||||
|
if(mescroll){ |
||||
|
// 延时(比$nextTick靠谱一些),确保元素已渲染
|
||||
|
setTimeout(()=>{ |
||||
|
mescroll.scrollTo(mescroll.getScrollTop(),0) |
||||
|
},30) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default MescrollMoreMixin; |
||||
@ -0,0 +1,109 @@ |
|||||
|
// 定义在wxs (含renderjs) 逻辑层的数据和方法, 与视图层相互通信
|
||||
|
const WxsMixin = { |
||||
|
data() { |
||||
|
return { |
||||
|
// 传入wxs视图层的数据 (响应式)
|
||||
|
wxsProp: { |
||||
|
optDown:{}, // 下拉刷新的配置
|
||||
|
scrollTop:0, // 滚动条的距离
|
||||
|
bodyHeight:0, // body的高度
|
||||
|
isDownScrolling:false, // 是否正在下拉刷新中
|
||||
|
isUpScrolling:false, // 是否正在上拉加载中
|
||||
|
isScrollBody:true, // 是否为mescroll-body滚动
|
||||
|
isUpBoth:true, // 上拉加载时,是否同时可以下拉刷新
|
||||
|
t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
|
||||
|
}, |
||||
|
|
||||
|
// 标记调用wxs视图层的方法
|
||||
|
callProp: { |
||||
|
callType: '', // 方法名
|
||||
|
t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
|
||||
|
}, |
||||
|
|
||||
|
// 不用wxs的平台使用此处的wxsBiz对象,抹平wxs的写法 (微信小程序和APP使用的wxsBiz对象是./wxs/wxs.wxs)
|
||||
|
// #ifndef MP-WEIXIN || MP-QQ || APP-PLUS || H5
|
||||
|
wxsBiz: { |
||||
|
//注册列表touchstart事件,用于下拉刷新
|
||||
|
touchstartEvent: e=> { |
||||
|
this.mescroll.touchstartEvent(e); |
||||
|
}, |
||||
|
//注册列表touchmove事件,用于下拉刷新
|
||||
|
touchmoveEvent: e=> { |
||||
|
this.mescroll.touchmoveEvent(e); |
||||
|
}, |
||||
|
//注册列表touchend事件,用于下拉刷新
|
||||
|
touchendEvent: e=> { |
||||
|
this.mescroll.touchendEvent(e); |
||||
|
}, |
||||
|
propObserver(){}, // 抹平wxs的写法
|
||||
|
callObserver(){} // 抹平wxs的写法
|
||||
|
}, |
||||
|
// #endif
|
||||
|
|
||||
|
// 不用renderjs的平台使用此处的renderBiz对象,抹平renderjs的写法 (app 和 h5 使用的renderBiz对象是./wxs/renderjs.js)
|
||||
|
// #ifndef APP-PLUS || H5
|
||||
|
renderBiz: { |
||||
|
propObserver(){} // 抹平renderjs的写法
|
||||
|
} |
||||
|
// #endif
|
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
// wxs视图层调用逻辑层的回调
|
||||
|
wxsCall(msg){ |
||||
|
if(msg.type === 'setWxsProp'){ |
||||
|
// 更新wxsProp数据 (值改变才触发更新)
|
||||
|
this.wxsProp = { |
||||
|
optDown: this.mescroll.optDown, |
||||
|
scrollTop: this.mescroll.getScrollTop(), |
||||
|
bodyHeight: this.mescroll.getBodyHeight(), |
||||
|
isDownScrolling: this.mescroll.isDownScrolling, |
||||
|
isUpScrolling: this.mescroll.isUpScrolling, |
||||
|
isUpBoth: this.mescroll.optUp.isBoth, |
||||
|
isScrollBody:this.mescroll.isScrollBody, |
||||
|
t: Date.now() |
||||
|
} |
||||
|
}else if(msg.type === 'setLoadType'){ |
||||
|
// 设置inOffset,outOffset的状态
|
||||
|
this.downLoadType = msg.downLoadType |
||||
|
// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
|
||||
|
this.$set(this.mescroll, 'downLoadType', this.downLoadType) |
||||
|
// 重置是否加载成功的状态
|
||||
|
this.$set(this.mescroll, 'isDownEndSuccess', null) |
||||
|
}else if(msg.type === 'triggerDownScroll'){ |
||||
|
// 主动触发下拉刷新
|
||||
|
this.mescroll.triggerDownScroll(); |
||||
|
}else if(msg.type === 'endDownScroll'){ |
||||
|
// 结束下拉刷新
|
||||
|
this.mescroll.endDownScroll(); |
||||
|
}else if(msg.type === 'triggerUpScroll'){ |
||||
|
// 主动触发上拉加载
|
||||
|
this.mescroll.triggerUpScroll(true); |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
mounted() { |
||||
|
// #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5
|
||||
|
// 配置主动触发wxs显示加载进度的回调
|
||||
|
this.mescroll.optDown.afterLoading = ()=>{ |
||||
|
this.callProp = {callType: "showLoading", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
|
||||
|
} |
||||
|
// 配置主动触发wxs隐藏加载进度的回调
|
||||
|
this.mescroll.optDown.afterEndDownScroll = ()=>{ |
||||
|
this.callProp = {callType: "endDownScroll", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
|
||||
|
let delay = 300 + (this.mescroll.optDown.beforeEndDelay || 0) |
||||
|
setTimeout(()=>{ |
||||
|
if(this.downLoadType === 4 || this.downLoadType === 0){ |
||||
|
this.callProp = {callType: "clearTransform", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
|
||||
|
} |
||||
|
// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
|
||||
|
this.$set(this.mescroll, 'downLoadType', this.downLoadType) |
||||
|
}, delay) |
||||
|
} |
||||
|
// 初始化wxs的数据
|
||||
|
this.wxsCall({type: 'setWxsProp'}) |
||||
|
// #endif
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default WxsMixin; |
||||
@ -0,0 +1,92 @@ |
|||||
|
// 使用renderjs直接操作window对象,实现动态控制app和h5的bounce
|
||||
|
// bounce: iOS橡皮筋,Android半月弧,h5浏览器下拉背景等效果 (下拉刷新时禁止)
|
||||
|
// https://uniapp.dcloud.io/frame?id=renderjs
|
||||
|
|
||||
|
// 与wxs的me实例一致
|
||||
|
var me = {} |
||||
|
|
||||
|
// 初始化window对象的touch事件 (仅初始化一次)
|
||||
|
if(window && !window.$mescrollRenderInit){ |
||||
|
window.$mescrollRenderInit = true |
||||
|
|
||||
|
|
||||
|
window.addEventListener('touchstart', function(e){ |
||||
|
if (me.disabled()) return; |
||||
|
me.startPoint = me.getPoint(e); // 记录起点
|
||||
|
}, {passive: true}) |
||||
|
|
||||
|
|
||||
|
window.addEventListener('touchmove', function(e){ |
||||
|
if (me.disabled()) return; |
||||
|
if (me.getScrollTop() > 0) return; // 需在顶部下拉,才禁止bounce
|
||||
|
|
||||
|
var curPoint = me.getPoint(e); // 当前点
|
||||
|
var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
|
||||
|
// 向下拉
|
||||
|
if (moveY > 0) { |
||||
|
// 可下拉的条件
|
||||
|
if (!me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling && me.isUpBoth))) { |
||||
|
|
||||
|
// 只有touch在mescroll的view上面,才禁止bounce
|
||||
|
var el = e.target; |
||||
|
var isMescrollTouch = false; |
||||
|
while (el && el.tagName && el.tagName !== 'UNI-PAGE-BODY' && el.tagName != "BODY") { |
||||
|
var cls = el.classList; |
||||
|
if (cls && cls.contains('mescroll-render-touch')) { |
||||
|
isMescrollTouch = true |
||||
|
break; |
||||
|
} |
||||
|
el = el.parentNode; // 继续检查其父元素
|
||||
|
} |
||||
|
// 禁止bounce (不会对swiper和iOS侧滑返回造成影响)
|
||||
|
if (isMescrollTouch && e.cancelable && !e.defaultPrevented) e.preventDefault(); |
||||
|
} |
||||
|
} |
||||
|
}, {passive: false}) |
||||
|
} |
||||
|
|
||||
|
/* 获取滚动条的位置 */ |
||||
|
me.getScrollTop = function() { |
||||
|
return me.scrollTop || 0 |
||||
|
} |
||||
|
|
||||
|
/* 是否禁用下拉刷新 */ |
||||
|
me.disabled = function(){ |
||||
|
return !me.optDown || !me.optDown.use || me.optDown.native |
||||
|
} |
||||
|
|
||||
|
/* 根据点击滑动事件获取第一个手指的坐标 */ |
||||
|
me.getPoint = function(e) { |
||||
|
if (!e) { |
||||
|
return {x: 0,y: 0} |
||||
|
} |
||||
|
if (e.touches && e.touches[0]) { |
||||
|
return {x: e.touches[0].pageX,y: e.touches[0].pageY} |
||||
|
} else if (e.changedTouches && e.changedTouches[0]) { |
||||
|
return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY} |
||||
|
} else { |
||||
|
return {x: e.clientX,y: e.clientY} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 监听逻辑层数据的变化 (实时更新数据) |
||||
|
*/ |
||||
|
function propObserver(wxsProp) { |
||||
|
me.optDown = wxsProp.optDown |
||||
|
me.scrollTop = wxsProp.scrollTop |
||||
|
me.isDownScrolling = wxsProp.isDownScrolling |
||||
|
me.isUpScrolling = wxsProp.isUpScrolling |
||||
|
me.isUpBoth = wxsProp.isUpBoth |
||||
|
} |
||||
|
|
||||
|
/* 导出模块 */ |
||||
|
const renderBiz = { |
||||
|
data() { |
||||
|
return { |
||||
|
propObserver: propObserver, |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default renderBiz; |
||||
@ -0,0 +1,268 @@ |
|||||
|
// 使用wxs处理交互动画, 提高性能, 同时避免小程序bounce对下拉刷新的影响 |
||||
|
// https://uniapp.dcloud.io/frame?id=wxs |
||||
|
// https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html |
||||
|
|
||||
|
// 模拟mescroll实例, 与mescroll.js的写法尽量保持一致 |
||||
|
var me = {} |
||||
|
|
||||
|
// ------ 自定义下拉刷新动画 start ------ |
||||
|
|
||||
|
/* 下拉过程中的回调,滑动过程一直在执行 (rate<1为inOffset; rate>1为outOffset) */ |
||||
|
me.onMoving = function (ins, rate, downHight){ |
||||
|
ins.requestAnimationFrame(function () { |
||||
|
ins.selectComponent('.mescroll-wxs-content').setStyle({ |
||||
|
'will-change': 'transform', // 可解决下拉过程中, image和swiper脱离文档流的问题 |
||||
|
'transform': 'translateY(' + downHight + 'px)', |
||||
|
'transition': '' |
||||
|
}) |
||||
|
// 环形进度条 |
||||
|
var progress = ins.selectComponent('.mescroll-wxs-progress') |
||||
|
progress && progress.setStyle({transform: 'rotate(' + 360 * rate + 'deg)'}) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
/* 显示下拉刷新进度 */ |
||||
|
me.showLoading = function (ins){ |
||||
|
me.downHight = me.optDown.offset |
||||
|
ins.requestAnimationFrame(function () { |
||||
|
ins.selectComponent('.mescroll-wxs-content').setStyle({ |
||||
|
'will-change': 'auto', |
||||
|
'transform': 'translateY(' + me.downHight + 'px)', |
||||
|
'transition': 'transform 300ms' |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
/* 结束下拉 */ |
||||
|
me.endDownScroll = function (ins){ |
||||
|
me.downHight = 0; |
||||
|
me.isDownScrolling = false; |
||||
|
ins.requestAnimationFrame(function () { |
||||
|
ins.selectComponent('.mescroll-wxs-content').setStyle({ |
||||
|
'will-change': 'auto', |
||||
|
'transform': 'translateY(0)', // 不可以写空串,否则scroll-view渲染不完整 (延时350ms会调clearTransform置空) |
||||
|
'transition': 'transform 300ms' |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
/* 结束下拉动画执行完毕后, 清除transform和transition, 避免对列表内容样式造成影响, 如: h5的list-msg示例下拉进度条漏出来等 */ |
||||
|
me.clearTransform = function (ins){ |
||||
|
ins.requestAnimationFrame(function () { |
||||
|
ins.selectComponent('.mescroll-wxs-content').setStyle({ |
||||
|
'will-change': '', |
||||
|
'transform': '', |
||||
|
'transition': '' |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// ------ 自定义下拉刷新动画 end ------ |
||||
|
|
||||
|
/** |
||||
|
* 监听逻辑层数据的变化 (实时更新数据) |
||||
|
*/ |
||||
|
function propObserver(wxsProp) { |
||||
|
me.optDown = wxsProp.optDown |
||||
|
me.scrollTop = wxsProp.scrollTop |
||||
|
me.bodyHeight = wxsProp.bodyHeight |
||||
|
me.isDownScrolling = wxsProp.isDownScrolling |
||||
|
me.isUpScrolling = wxsProp.isUpScrolling |
||||
|
me.isUpBoth = wxsProp.isUpBoth |
||||
|
me.isScrollBody = wxsProp.isScrollBody |
||||
|
me.startTop = wxsProp.scrollTop // 及时更新touchstart触发的startTop, 避免scroll-view快速惯性滚动到顶部取值不准确 |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 监听逻辑层数据的变化 (调用wxs的方法) |
||||
|
*/ |
||||
|
function callObserver(callProp, oldValue, ins) { |
||||
|
if (me.disabled()) return; |
||||
|
if(callProp.callType){ |
||||
|
// 逻辑层(App Service)的style已失效,需在视图层(Webview)设置style |
||||
|
if(callProp.callType === 'showLoading'){ |
||||
|
me.showLoading(ins) |
||||
|
}else if(callProp.callType === 'endDownScroll'){ |
||||
|
me.endDownScroll(ins) |
||||
|
}else if(callProp.callType === 'clearTransform'){ |
||||
|
me.clearTransform(ins) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* touch事件 |
||||
|
*/ |
||||
|
function touchstartEvent(e, ins) { |
||||
|
me.downHight = 0; // 下拉的距离 |
||||
|
me.startPoint = me.getPoint(e); // 记录起点 |
||||
|
me.startTop = me.getScrollTop(); // 记录此时的滚动条位置 |
||||
|
me.startAngle = 0; // 初始角度 |
||||
|
me.lastPoint = me.startPoint; // 重置上次move的点 |
||||
|
me.maxTouchmoveY = me.getBodyHeight() - me.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况) |
||||
|
me.inTouchend = false; // 标记不是touchend |
||||
|
|
||||
|
me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步) |
||||
|
} |
||||
|
|
||||
|
function touchmoveEvent(e, ins) { |
||||
|
var isPrevent = true // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效) |
||||
|
|
||||
|
if (me.disabled()) return isPrevent; |
||||
|
|
||||
|
var scrollTop = me.getScrollTop(); // 当前滚动条的距离 |
||||
|
var curPoint = me.getPoint(e); // 当前点 |
||||
|
|
||||
|
var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉 |
||||
|
|
||||
|
// 向下拉 && 在顶部 |
||||
|
// mescroll-body,直接判定在顶部即可 |
||||
|
// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove |
||||
|
// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等 |
||||
|
if (moveY > 0 && ( |
||||
|
(me.isScrollBody && scrollTop <= 0) |
||||
|
|| |
||||
|
(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) ) |
||||
|
)) { |
||||
|
// 可下拉的条件 |
||||
|
if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling && |
||||
|
me.isUpBoth))) { |
||||
|
|
||||
|
// 下拉的角度是否在配置的范围内 |
||||
|
if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90] |
||||
|
if (me.startAngle < me.optDown.minAngle) return isPrevent; // 如果小于配置的角度,则不往下执行下拉刷新 |
||||
|
|
||||
|
// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发 |
||||
|
if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) { |
||||
|
me.inTouchend = true; // 标记执行touchend |
||||
|
touchendEvent(e, ins); // 提前触发touchend |
||||
|
return isPrevent; |
||||
|
} |
||||
|
|
||||
|
isPrevent = false // 小程序是return false |
||||
|
|
||||
|
var diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上) |
||||
|
|
||||
|
// 下拉距离 < 指定距离 |
||||
|
if (me.downHight < me.optDown.offset) { |
||||
|
if (me.movetype !== 1) { |
||||
|
me.movetype = 1; // 加入标记,保证只执行一次 |
||||
|
// me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次 |
||||
|
me.callMethod(ins, {type: 'setLoadType', downLoadType: 1}) |
||||
|
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来 |
||||
|
} |
||||
|
me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小 |
||||
|
|
||||
|
// 指定距离 <= 下拉距离 |
||||
|
} else { |
||||
|
if (me.movetype !== 2) { |
||||
|
me.movetype = 2; // 加入标记,保证只执行一次 |
||||
|
// me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次 |
||||
|
me.callMethod(ins, {type: 'setLoadType', downLoadType: 2}) |
||||
|
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来 |
||||
|
} |
||||
|
if (diff > 0) { // 向下拉 |
||||
|
me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小 |
||||
|
} else { // 向上收 |
||||
|
me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
me.downHight = Math.round(me.downHight) // 取整 |
||||
|
var rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值 |
||||
|
// me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行 |
||||
|
me.onMoving(ins, rate, me.downHight) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
me.lastPoint = curPoint; // 记录本次移动的点 |
||||
|
|
||||
|
return isPrevent // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效) |
||||
|
} |
||||
|
|
||||
|
function touchendEvent(e, ins) { |
||||
|
// 如果下拉区域高度已改变,则需重置回来 |
||||
|
if (me.isMoveDown) { |
||||
|
if (me.downHight >= me.optDown.offset) { |
||||
|
// 符合触发刷新的条件 |
||||
|
me.downHight = me.optDown.offset; // 更新下拉区域高度 |
||||
|
// me.triggerDownScroll(); |
||||
|
me.callMethod(ins, {type: 'triggerDownScroll'}) |
||||
|
} else { |
||||
|
// 不符合的话 则重置 |
||||
|
me.downHight = 0; |
||||
|
// me.optDown.endDownScroll && me.optDown.endDownScroll(me); |
||||
|
me.callMethod(ins, {type: 'endDownScroll'}) |
||||
|
} |
||||
|
me.movetype = 0; |
||||
|
me.isMoveDown = false; |
||||
|
} else if (!me.isScrollBody && me.getScrollTop() === me.startTop) { // scroll-view到顶/左/右/底的滑动事件 |
||||
|
var isScrollUp = me.getPoint(e).y - me.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉 |
||||
|
// 上滑 |
||||
|
if (isScrollUp) { |
||||
|
// 需检查滑动的角度 |
||||
|
var angle = me.getAngle(me.getPoint(e), me.startPoint); // 两点之间的角度,区间 [0,90] |
||||
|
if (angle > 80) { |
||||
|
// 检查并触发上拉 |
||||
|
// me.triggerUpScroll(true); |
||||
|
me.callMethod(ins, {type: 'triggerUpScroll'}) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步) |
||||
|
} |
||||
|
|
||||
|
/* 是否禁用下拉刷新 */ |
||||
|
me.disabled = function(){ |
||||
|
return !me.optDown || !me.optDown.use || me.optDown.native |
||||
|
} |
||||
|
|
||||
|
/* 根据点击滑动事件获取第一个手指的坐标 */ |
||||
|
me.getPoint = function(e) { |
||||
|
if (!e) { |
||||
|
return {x: 0,y: 0} |
||||
|
} |
||||
|
if (e.touches && e.touches[0]) { |
||||
|
return {x: e.touches[0].pageX,y: e.touches[0].pageY} |
||||
|
} else if (e.changedTouches && e.changedTouches[0]) { |
||||
|
return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY} |
||||
|
} else { |
||||
|
return {x: e.clientX,y: e.clientY} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 计算两点之间的角度: 区间 [0,90]*/ |
||||
|
me.getAngle = function (p1, p2) { |
||||
|
var x = Math.abs(p1.x - p2.x); |
||||
|
var y = Math.abs(p1.y - p2.y); |
||||
|
var z = Math.sqrt(x * x + y * y); |
||||
|
var angle = 0; |
||||
|
if (z !== 0) { |
||||
|
angle = Math.asin(y / z) / Math.PI * 180; |
||||
|
} |
||||
|
return angle |
||||
|
} |
||||
|
|
||||
|
/* 获取滚动条的位置 */ |
||||
|
me.getScrollTop = function() { |
||||
|
return me.scrollTop || 0 |
||||
|
} |
||||
|
|
||||
|
/* 获取body的高度 */ |
||||
|
me.getBodyHeight = function() { |
||||
|
return me.bodyHeight || 0; |
||||
|
} |
||||
|
|
||||
|
/* 调用逻辑层的方法 */ |
||||
|
me.callMethod = function(ins, param) { |
||||
|
if(ins) ins.callMethod('wxsCall', param) |
||||
|
} |
||||
|
|
||||
|
/* 导出模块 */ |
||||
|
module.exports = { |
||||
|
propObserver: propObserver, |
||||
|
callObserver: callObserver, |
||||
|
touchstartEvent: touchstartEvent, |
||||
|
touchmoveEvent: touchmoveEvent, |
||||
|
touchendEvent: touchendEvent |
||||
|
} |
||||
@ -0,0 +1,123 @@ |
|||||
|
<template> |
||||
|
<!-- 文章组 --> |
||||
|
<view class="diy-article"> |
||||
|
<view class="article-item" :class="[`show-type__${item.show_type}`]" v-for="(item, index) in dataList" :key="index" |
||||
|
@click="onTargetDetail(item.article_id)"> |
||||
|
<!-- 小图模式 --> |
||||
|
<block v-if="item.show_type == 10"> |
||||
|
<view class="article-item__left flex-box"> |
||||
|
<view class="article-item__title"> |
||||
|
<text class="twoline-hide">{{ item.title }}</text> |
||||
|
</view> |
||||
|
<view class="article-item__footer m-top10"> |
||||
|
<text class="article-views f-24 col-8">{{ item.show_views }}次浏览</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
<view class="article-item__image"> |
||||
|
<image class="image" mode="widthFix" :src="item.image_url"></image> |
||||
|
</view> |
||||
|
</block> |
||||
|
<!-- 大图模式 --> |
||||
|
<block v-if="item.show_type == 20"> |
||||
|
<view class="article-item__title"> |
||||
|
<text class="twoline-hide">{{ item.title }}</text> |
||||
|
</view> |
||||
|
<view class="article-item__image m-top20"> |
||||
|
<image class="image" mode="widthFix" :src="item.image_url"></image> |
||||
|
</view> |
||||
|
<view class="article-item__footer m-top10"> |
||||
|
<text class="article-views f-24 col-8">{{ item.show_views }}次浏览</text> |
||||
|
</view> |
||||
|
</block> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: "Article", |
||||
|
/** |
||||
|
* 组件的属性列表 |
||||
|
* 用于组件自定义设置 |
||||
|
*/ |
||||
|
props: { |
||||
|
itemIndex: String, |
||||
|
params: Object, |
||||
|
dataList: Array |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 组件的方法列表 |
||||
|
* 更新属性和数据的方法与更新页面数据的方法类似 |
||||
|
*/ |
||||
|
methods: { |
||||
|
|
||||
|
/** |
||||
|
* 跳转文章详情页 |
||||
|
*/ |
||||
|
onTargetDetail(id) { |
||||
|
uni.navigateTo({ |
||||
|
url: '/pages/article/detail?articleId=' + id |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.diy-article { |
||||
|
background: #f7f7f7; |
||||
|
|
||||
|
.article-item { |
||||
|
margin-bottom: 20rpx; |
||||
|
padding: 30rpx; |
||||
|
background: #fff; |
||||
|
|
||||
|
&:last-child { |
||||
|
margin-bottom: 0; |
||||
|
} |
||||
|
|
||||
|
.article-item__title { |
||||
|
max-height: 74rpx; |
||||
|
font-size: 28rpx; |
||||
|
line-height: 38rpx; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.article-item__image .image { |
||||
|
display: block; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
} |
||||
|
|
||||
|
/* 小图模式 */ |
||||
|
|
||||
|
.show-type__10 { |
||||
|
display: flex; |
||||
|
|
||||
|
.article-item__left { |
||||
|
padding-right: 20rpx; |
||||
|
} |
||||
|
|
||||
|
.article-item__title { |
||||
|
// min-height: 72rpx; |
||||
|
} |
||||
|
|
||||
|
.article-item__image .image { |
||||
|
width: 240rpx; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
/* 大图模式 */ |
||||
|
|
||||
|
.show-type__20 .article-item__image .image { |
||||
|
width: 100%; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,154 @@ |
|||||
|
<template> |
||||
|
<view class="diy-banner" :style="{ height: `${imgHeights[imgCurrent]}px` }"> |
||||
|
<!-- 图片轮播 --> |
||||
|
<swiper class="swiper-box" :autoplay="autoplay" :duration="duration" :circular="true" :interval="itemStyle.interval * 1000" @change="_bindChange"> |
||||
|
<swiper-item v-for="(dataItem, index) in dataList" :key="index"> |
||||
|
<image mode="widthFix" class="slide-image" :src="dataItem.imgUrl" @click="onLink(dataItem.link)" @load="_imagesHeight" /> |
||||
|
</swiper-item> |
||||
|
</swiper> |
||||
|
<!-- 指示点 --> |
||||
|
<view class="indicator-dots" :class="itemStyle.btnShape"> |
||||
|
<view class="dots-item" :class="{ active: imgCurrent == index }" :style="{ backgroundColor: itemStyle.btnColor }" |
||||
|
v-for="(dataItem, index) in dataList" :key="index"></view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import mixin from '../mixin'; |
||||
|
|
||||
|
export default { |
||||
|
name: 'Banner', |
||||
|
|
||||
|
/** |
||||
|
* 组件的属性列表 |
||||
|
* 用于组件自定义设置 |
||||
|
*/ |
||||
|
props: { |
||||
|
itemIndex: String, |
||||
|
itemStyle: Object, |
||||
|
params: Object, |
||||
|
dataList: Array |
||||
|
}, |
||||
|
|
||||
|
mixins: [mixin], |
||||
|
|
||||
|
/** |
||||
|
* 私有数据,组件的初始数据 |
||||
|
* 可用于模版渲染 |
||||
|
*/ |
||||
|
data() { |
||||
|
return { |
||||
|
windowWidth: 750, |
||||
|
indicatorDots: false, // 是否显示面板指示点 |
||||
|
autoplay: true, // 是否自动切换 |
||||
|
duration: 800, // 滑动动画时长 |
||||
|
imgHeights: [], // 图片的高度 |
||||
|
imgCurrent: 0 // 当前banne所在滑块指针 |
||||
|
}; |
||||
|
}, |
||||
|
|
||||
|
created() { |
||||
|
const app = this; |
||||
|
uni.getSystemInfo({ |
||||
|
success({ windowWidth }) { |
||||
|
app.windowWidth = windowWidth > 750 ? 750 : windowWidth; |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 组件的方法列表 |
||||
|
* 更新属性和数据的方法与更新页面数据的方法类似 |
||||
|
*/ |
||||
|
methods: { |
||||
|
/** |
||||
|
* 计算图片高度 |
||||
|
*/ |
||||
|
_imagesHeight({ detail }) { |
||||
|
const app = this; |
||||
|
// 获取图片真实宽度 |
||||
|
const { width, height } = detail; |
||||
|
// 宽高比 |
||||
|
const ratio = width / height; |
||||
|
// 计算的高度值 |
||||
|
const viewHeight = app.windowWidth / ratio; |
||||
|
// 把每一张图片的高度记录到数组里 |
||||
|
app.imgHeights.push(viewHeight); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 记录当前指针 |
||||
|
*/ |
||||
|
_bindChange(e) { |
||||
|
this.imgCurrent = e.detail.current; |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.diy-banner { |
||||
|
position: relative; |
||||
|
|
||||
|
// swiper组件 |
||||
|
.swiper-box { |
||||
|
height: 100%; |
||||
|
|
||||
|
.slide-image { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
margin: 0 auto; |
||||
|
display: block; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* 指示点 */ |
||||
|
.indicator-dots { |
||||
|
width: 100%; |
||||
|
height: 28rpx; |
||||
|
padding: 0 20rpx; |
||||
|
position: absolute; |
||||
|
left: 0; |
||||
|
right: 0; |
||||
|
bottom: 20rpx; |
||||
|
opacity: 0.8; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
|
||||
|
.dots-item { |
||||
|
width: 16rpx; |
||||
|
height: 16rpx; |
||||
|
margin-right: 8rpx; |
||||
|
background-color: #fff; |
||||
|
|
||||
|
&:last-child { |
||||
|
margin-right: 0; |
||||
|
} |
||||
|
|
||||
|
&.active { |
||||
|
background-color: #313131 !important; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 圆形 |
||||
|
&.round .dots-item { |
||||
|
width: 16rpx; |
||||
|
height: 16rpx; |
||||
|
border-radius: 20rpx; |
||||
|
} |
||||
|
|
||||
|
// 正方形 |
||||
|
&.square .dots-item { |
||||
|
width: 16rpx; |
||||
|
height: 16rpx; |
||||
|
} |
||||
|
|
||||
|
// 长方形 |
||||
|
&.rectangle .dots-item { |
||||
|
width: 22rpx; |
||||
|
height: 14rpx; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,31 @@ |
|||||
|
<template> |
||||
|
<!-- 辅助空白 --> |
||||
|
<view class="diy-blank" :style="{ height: `${itemStyle.height}px`, background: itemStyle.background }"> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
|
||||
|
/** |
||||
|
* 组件的属性列表 |
||||
|
* 用于组件自定义设置 |
||||
|
*/ |
||||
|
props: { |
||||
|
itemStyle: Object |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 组件的方法列表 |
||||
|
* 更新属性和数据的方法与更新页面数据的方法类似 |
||||
|
*/ |
||||
|
methods: { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
|
||||
|
</style> |
||||
@ -0,0 +1,256 @@ |
|||||
|
<template> |
||||
|
<!-- 商品组 --> |
||||
|
<view class="diy-goods" :style="{ background: itemStyle.background }"> |
||||
|
<view class="goods-list" :class="[`display__${itemStyle.display}`, `column__${itemStyle.column}`]"> |
||||
|
<scroll-view :scroll-x="itemStyle.display === 'slide'"> |
||||
|
<view class="goods-item" v-for="(dataItem, index) in dataList" :key="index" @click="onTargetGoods(dataItem.goods_id)"> |
||||
|
|
||||
|
<!-- 单列商品 --> |
||||
|
<block v-if="itemStyle.column === 1"> |
||||
|
<view class="dis-flex"> |
||||
|
<!-- 商品图片 --> |
||||
|
<view class="goods-item_left"> |
||||
|
<image class="image" :src="dataItem.goods_image"></image> |
||||
|
</view> |
||||
|
<view class="goods-item_right"> |
||||
|
<!-- 商品名称 --> |
||||
|
<view v-if="itemStyle.show.includes('goodsName')" class="goods-name"> |
||||
|
<text class="twoline-hide">{{ dataItem.goods_name }}</text> |
||||
|
</view> |
||||
|
<view class="goods-item_desc"> |
||||
|
<!-- 商品卖点 --> |
||||
|
<view v-if="itemStyle.show.includes('sellingPoint')" class="desc-selling_point dis-flex"> |
||||
|
<text class="oneline-hide">{{ dataItem.selling_point }}</text> |
||||
|
</view> |
||||
|
<!-- 商品销量 --> |
||||
|
<view v-if="itemStyle.show.includes('goodsSales')" class="desc-goods_sales dis-flex"> |
||||
|
<text>已售{{ dataItem.goods_sales }}件</text> |
||||
|
</view> |
||||
|
<!-- 商品价格 --> |
||||
|
<view class="desc_footer"> |
||||
|
<text v-if="itemStyle.show.includes('goodsPrice')" class="price_x">¥{{ dataItem.goods_price_min }}</text> |
||||
|
<text class="price_y col-9" |
||||
|
v-if="itemStyle.show.includes('linePrice') && dataItem.line_price_min > 0">¥{{ dataItem.line_price_min }}</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</block> |
||||
|
<!-- 多列商品 --> |
||||
|
<block v-else> |
||||
|
<!-- 商品图片 --> |
||||
|
<view class="goods-image"> |
||||
|
<image class="image" mode="aspectFill" :src="dataItem.goods_image"></image> |
||||
|
</view> |
||||
|
<view class="detail"> |
||||
|
<!-- 商品标题 --> |
||||
|
<view v-if="itemStyle.show.includes('goodsName')" class="goods-name"> |
||||
|
<text class="twoline-hide">{{ dataItem.goods_name }}</text> |
||||
|
</view> |
||||
|
<!-- 商品价格 --> |
||||
|
<view class="detail-price oneline-hide"> |
||||
|
<text v-if="itemStyle.show.includes('goodsPrice')" class="goods-price f-30 col-m">¥{{ dataItem.goods_price_min }}</text> |
||||
|
<text v-if="itemStyle.show.includes('linePrice') && dataItem.line_price_min > 0" |
||||
|
class="line-price col-9 f-24">¥{{ dataItem.line_price_min }}</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</block> |
||||
|
|
||||
|
</view> |
||||
|
</scroll-view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
name: "Goods", |
||||
|
/** |
||||
|
* 组件的属性列表 |
||||
|
* 用于组件自定义设置 |
||||
|
*/ |
||||
|
props: { |
||||
|
itemIndex: String, |
||||
|
itemStyle: Object, |
||||
|
params: Object, |
||||
|
dataList: Array |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 组件的方法列表 |
||||
|
* 更新属性和数据的方法与更新页面数据的方法类似 |
||||
|
*/ |
||||
|
methods: { |
||||
|
|
||||
|
/** |
||||
|
* 跳转商品详情页 |
||||
|
*/ |
||||
|
onTargetGoods(goodsId) { |
||||
|
this.$navTo(`pages/goods/detail`, { goodsId }) |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.diy-goods { |
||||
|
.goods-list { |
||||
|
padding: 4rpx; |
||||
|
box-sizing: border-box; |
||||
|
|
||||
|
.goods-item { |
||||
|
box-sizing: border-box; |
||||
|
padding: 6rpx; |
||||
|
|
||||
|
.goods-image { |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
height: 0; |
||||
|
padding-bottom: 100%; |
||||
|
overflow: hidden; |
||||
|
background: #fff; |
||||
|
|
||||
|
&:after { |
||||
|
content: ''; |
||||
|
display: block; |
||||
|
margin-top: 100%; |
||||
|
} |
||||
|
|
||||
|
.image { |
||||
|
position: absolute; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
-o-object-fit: cover; |
||||
|
object-fit: cover; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.detail { |
||||
|
padding: 8rpx; |
||||
|
background: #fff; |
||||
|
|
||||
|
.goods-name { |
||||
|
min-height: 68rpx; |
||||
|
line-height: 1.3; |
||||
|
white-space: normal; |
||||
|
color: #484848; |
||||
|
font-size: 26rpx; |
||||
|
} |
||||
|
|
||||
|
.detail-price { |
||||
|
.goods-price { |
||||
|
margin-right: 8rpx; |
||||
|
} |
||||
|
|
||||
|
.line-price { |
||||
|
text-decoration: line-through; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.display__slide { |
||||
|
white-space: nowrap; |
||||
|
font-size: 0; |
||||
|
|
||||
|
.goods-item { |
||||
|
display: inline-block; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.display__list { |
||||
|
.goods-item { |
||||
|
float: left; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.column__2 { |
||||
|
.goods-item { |
||||
|
width: 50%; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.column__3 { |
||||
|
.goods-item { |
||||
|
width: 33.33333%; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.column__1 { |
||||
|
.goods-item { |
||||
|
width: 100%; |
||||
|
height: 280rpx; |
||||
|
margin-bottom: 12rpx; |
||||
|
padding: 20rpx; |
||||
|
box-sizing: border-box; |
||||
|
background: #fff; |
||||
|
line-height: 1.6; |
||||
|
|
||||
|
&:last-child { |
||||
|
margin-bottom: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.goods-item_left { |
||||
|
display: flex; |
||||
|
width: 40%; |
||||
|
background: #fff; |
||||
|
align-items: center; |
||||
|
|
||||
|
.image { |
||||
|
display: block; |
||||
|
width: 240rpx; |
||||
|
height: 240rpx; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.goods-item_right { |
||||
|
position: relative; |
||||
|
width: 60%; |
||||
|
|
||||
|
.goods-name { |
||||
|
margin-top: 20rpx; |
||||
|
min-height: 68rpx; |
||||
|
line-height: 1.3; |
||||
|
white-space: normal; |
||||
|
color: #484848; |
||||
|
font-size: 26rpx; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.goods-item_desc { |
||||
|
margin-top: 8rpx; |
||||
|
} |
||||
|
|
||||
|
.desc-selling_point { |
||||
|
width: 400rpx; |
||||
|
font-size: 24rpx; |
||||
|
color: #e49a3d; |
||||
|
} |
||||
|
|
||||
|
.desc-goods_sales { |
||||
|
color: #999; |
||||
|
font-size: 24rpx; |
||||
|
} |
||||
|
|
||||
|
.desc_footer { |
||||
|
font-size: 24rpx; |
||||
|
|
||||
|
.price_x { |
||||
|
margin-right: 16rpx; |
||||
|
color: #f03c3c; |
||||
|
font-size: 30rpx; |
||||
|
} |
||||
|
|
||||
|
.price_y { |
||||
|
text-decoration: line-through; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,36 @@ |
|||||
|
<template> |
||||
|
<!-- 辅助线 --> |
||||
|
<view class="diy-guide" :style="{ padding: `${itemStyle.paddingTop}px 0`, background: itemStyle.background }"> |
||||
|
<view class="line" :style="{ borderTop: `${itemStyle.lineHeight}px ${itemStyle.lineStyle} ${itemStyle.lineColor}` }"> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
|
||||
|
export default { |
||||
|
|
||||
|
/** |
||||
|
* 组件的属性列表 |
||||
|
* 用于组件自定义设置 |
||||
|
*/ |
||||
|
props: { |
||||
|
itemStyle: Object |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 组件的方法列表 |
||||
|
* 更新属性和数据的方法与更新页面数据的方法类似 |
||||
|
*/ |
||||
|
methods: { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.diy-guide .line { |
||||
|
width: 100%; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,47 @@ |
|||||
|
<template> |
||||
|
<!-- 单图组 --> |
||||
|
<view class="diy-imageSingle" :style="{ paddingBottom: `${itemStyle.paddingTop}px`, background: itemStyle.background }"> |
||||
|
<view class="item-image" v-for="(dataItem, index) in dataList" :key="index" :style="{ padding: `${itemStyle.paddingTop}px ${itemStyle.paddingLeft}px 0` }"> |
||||
|
<view class="nav-to" @click="onLink(dataItem.link)"> |
||||
|
<image class="image" :src="dataItem.imgUrl" mode="widthFix"></image> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import mixin from '../mixin' |
||||
|
|
||||
|
export default { |
||||
|
name: "Images", |
||||
|
|
||||
|
/** |
||||
|
* 组件的属性列表 |
||||
|
* 用于组件自定义设置 |
||||
|
*/ |
||||
|
props: { |
||||
|
itemIndex: String, |
||||
|
itemStyle: Object, |
||||
|
params: Object, |
||||
|
dataList: Array |
||||
|
}, |
||||
|
|
||||
|
mixins: [mixin], |
||||
|
|
||||
|
/** |
||||
|
* 组件的方法列表 |
||||
|
* 更新属性和数据的方法与更新页面数据的方法类似 |
||||
|
*/ |
||||
|
methods: { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.diy-imageSingle .item-image .image { |
||||
|
display: block; |
||||
|
width: 100%; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,108 @@ |
|||||
|
<template> |
||||
|
<view class="page-items"> |
||||
|
<block v-for="(item, index) in items" :key="index"> |
||||
|
<!-- 搜索框 --> |
||||
|
<block v-if="item.type === 'search'"> |
||||
|
<Search :itemStyle="item.style" :params="item.params" /> |
||||
|
</block> |
||||
|
<!-- 图片组 --> |
||||
|
<block v-if="item.type === 'image'"> |
||||
|
<Images :itemStyle="item.style" :params="item.params" :dataList="item.data" /> |
||||
|
</block> |
||||
|
<!-- 轮播图 --> |
||||
|
<block v-if="item.type === 'banner'"> |
||||
|
<Banner :itemStyle="item.style" :params="item.params" :dataList="item.data" /> |
||||
|
</block> |
||||
|
<!-- 图片橱窗 --> |
||||
|
<block v-if="item.type === 'window'"> |
||||
|
<Window :itemStyle="item.style" :params="item.params" :dataList="item.data" /> |
||||
|
</block> |
||||
|
<!-- 视频 --> |
||||
|
<block v-if="item.type === 'video'"> |
||||
|
<Videos :itemStyle="item.style" :params="item.params" /> |
||||
|
</block> |
||||
|
<!-- 文章组 --> |
||||
|
<block v-if="item.type === 'article'"> |
||||
|
<Article :params="item.params" :dataList="item.data" /> |
||||
|
</block> |
||||
|
<!-- 店铺公告 --> |
||||
|
<block v-if="item.type === 'notice'"> |
||||
|
<Notice :itemStyle="item.style" :params="item.params" /> |
||||
|
</block> |
||||
|
<!-- 导航 --> |
||||
|
<block v-if="item.type === 'navBar'"> |
||||
|
<NavBar :itemStyle="item.style" :params="item.params" :dataList="item.data" /> |
||||
|
</block> |
||||
|
<!-- 商品 --> |
||||
|
<block v-if="item.type === 'goods'"> |
||||
|
<Goods :itemStyle="item.style" :params="item.params" :dataList="item.data" /> |
||||
|
</block> |
||||
|
<!-- 在线客服 --> |
||||
|
<block v-if="item.type === 'service'"> |
||||
|
<Service :itemStyle="item.style" :params="item.params" /> |
||||
|
</block> |
||||
|
<!-- 辅助空白 --> |
||||
|
<block v-if="item.type === 'blank'"> |
||||
|
<Blank :itemStyle="item.style" /> |
||||
|
</block> |
||||
|
<!-- 辅助线 --> |
||||
|
<block v-if="item.type === 'guide'"> |
||||
|
<Guide :itemStyle="item.style" /> |
||||
|
</block> |
||||
|
<!-- 富文本 --> |
||||
|
<block v-if="item.type === 'richText'"> |
||||
|
<RichText :itemStyle="item.style" :params="item.params" /> |
||||
|
</block> |
||||
|
</block> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import Search from './search' |
||||
|
import Images from './image' |
||||
|
import Banner from './banner' |
||||
|
import Window from './window' |
||||
|
import Videos from './video' |
||||
|
import Article from './article' |
||||
|
import Notice from './notice' |
||||
|
import NavBar from './navBar' |
||||
|
import Goods from './goods' |
||||
|
import Service from './service' |
||||
|
import Blank from './blank' |
||||
|
import Guide from './guide' |
||||
|
import RichText from './richText' |
||||
|
|
||||
|
export default { |
||||
|
name: "Page", |
||||
|
components: { |
||||
|
Search, |
||||
|
Images, |
||||
|
Banner, |
||||
|
Window, |
||||
|
Videos, |
||||
|
Article, |
||||
|
Notice, |
||||
|
NavBar, |
||||
|
Goods, |
||||
|
Service, |
||||
|
Blank, |
||||
|
Guide, |
||||
|
RichText |
||||
|
}, |
||||
|
/** |
||||
|
* 组件的属性列表 |
||||
|
* 用于组件自定义设置 |
||||
|
*/ |
||||
|
props: { |
||||
|
items: { |
||||
|
type: Array, |
||||
|
default () { |
||||
|
return [] |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
} |
||||
|
</script> |
||||
|
<style lang="scss"> |
||||
|
// 组件样式 |
||||
|
</style> |
||||
@ -0,0 +1,23 @@ |
|||||
|
import util from '@/utils/util' |
||||
|
|
||||
|
export default { |
||||
|
data() { |
||||
|
return {} |
||||
|
}, |
||||
|
methods: { |
||||
|
|
||||
|
/** |
||||
|
* link对象点击事件 |
||||
|
* 支持tabBar页面 |
||||
|
*/ |
||||
|
onLink(linkObj) { |
||||
|
if (!linkObj) return false |
||||
|
// 跳转到指定页面
|
||||
|
if (linkObj.type === 'PAGE') { |
||||
|
this.$navTo(linkObj.param.path, linkObj.param.query) |
||||
|
} |
||||
|
return true |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,87 @@ |
|||||
|
<template> |
||||
|
<!-- 导航组 --> |
||||
|
<view class="diy-navBar" :style="{ background: itemStyle.background, color: itemStyle.textColor }"> |
||||
|
<view class="data-list" :class="[`avg-sm-${itemStyle.rowsNum}`]"> |
||||
|
<view class="item-nav" v-for="(dataItem, index) in dataList" :key="index"> |
||||
|
<view class="nav-to" @click="onLink(dataItem.link)"> |
||||
|
<view class="item-image"> |
||||
|
<image class="image" mode="widthFix" :src="dataItem.imgUrl"></image> |
||||
|
</view> |
||||
|
<view class="item-text oneline-hide">{{ dataItem.text }}</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import mixin from '../mixin' |
||||
|
|
||||
|
export default { |
||||
|
name: "NavBar", |
||||
|
/** |
||||
|
* 组件的属性列表 |
||||
|
* 用于组件自定义设置 |
||||
|
*/ |
||||
|
props: { |
||||
|
itemIndex: String, |
||||
|
itemStyle: Object, |
||||
|
params: Object, |
||||
|
dataList: Array |
||||
|
}, |
||||
|
|
||||
|
mixins: [mixin], |
||||
|
|
||||
|
/** |
||||
|
* 组件的方法列表 |
||||
|
* 更新属性和数据的方法与更新页面数据的方法类似 |
||||
|
*/ |
||||
|
methods: { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.diy-navBar .data-list::after { |
||||
|
clear: both; |
||||
|
content: " "; |
||||
|
display: table; |
||||
|
} |
||||
|
|
||||
|
.item-nav { |
||||
|
float: left; |
||||
|
margin: 10px 0; |
||||
|
text-align: center; |
||||
|
|
||||
|
.item-text { |
||||
|
font-size: 26rpx; |
||||
|
} |
||||
|
|
||||
|
.item-image { |
||||
|
margin-bottom: 4px; |
||||
|
font-size: 0; |
||||
|
} |
||||
|
|
||||
|
.item-image .image { |
||||
|
width: 88rpx; |
||||
|
height: 88rpx; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
/* 分列布局 */ |
||||
|
|
||||
|
.diy-navBar .avg-sm-3>.item-nav { |
||||
|
width: 33.33333333%; |
||||
|
} |
||||
|
|
||||
|
.diy-navBar .avg-sm-4>.item-nav { |
||||
|
width: 25%; |
||||
|
} |
||||
|
|
||||
|
.diy-navBar .avg-sm-5>.item-nav { |
||||
|
width: 20%; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,40 @@ |
|||||
|
<template> |
||||
|
<!-- 店铺公告 --> |
||||
|
<view class="diy-notice" :style="{ paddingTop: `${itemStyle.paddingTop}px`, paddingBottom: `${itemStyle.paddingTop}px` }" |
||||
|
@click="onLink(params.link)"> |
||||
|
<u-notice-bar padding="10rpx 24rpx" :volume-icon="params.showIcon" :autoplay="params.scrollable" |
||||
|
:bg-color="itemStyle.background" :color="itemStyle.textColor" :list="[params.text]"></u-notice-bar> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import mixin from '../mixin' |
||||
|
|
||||
|
export default { |
||||
|
|
||||
|
/** |
||||
|
* 组件的属性列表 |
||||
|
* 用于组件自定义设置 |
||||
|
*/ |
||||
|
props: { |
||||
|
itemStyle: Object, |
||||
|
params: Object |
||||
|
}, |
||||
|
|
||||
|
mixins: [mixin], |
||||
|
|
||||
|
/** |
||||
|
* 组件的方法列表 |
||||
|
* 更新属性和数据的方法与更新页面数据的方法类似 |
||||
|
*/ |
||||
|
methods: { |
||||
|
|
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
|
||||
|
</style> |
||||
@ -0,0 +1,33 @@ |
|||||
|
<template> |
||||
|
<!-- 富文本 --> |
||||
|
<view class="diy-richText" |
||||
|
:style="{ padding: `${itemStyle.paddingTop}px ${itemStyle.paddingLeft}px`, background: itemStyle.background }"> |
||||
|
<mp-html :content="params.content" /> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
/** |
||||
|
* 组件的属性列表 |
||||
|
* 用于组件自定义设置 |
||||
|
*/ |
||||
|
props: { |
||||
|
itemStyle: Object, |
||||
|
params: Object |
||||
|
}, |
||||
|
/** |
||||
|
* 组件的方法列表 |
||||
|
* 更新属性和数据的方法与更新页面数据的方法类似 |
||||
|
*/ |
||||
|
methods: { |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.diy-richText { |
||||
|
font-size: 28rpx; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,75 @@ |
|||||
|
<template> |
||||
|
<!-- 搜索框 --> |
||||
|
<view class="diy-search"> |
||||
|
<view class="inner" :class="itemStyle.searchStyle" @click="onTargetSearch"> |
||||
|
<view class="search-input" :style="{ textAlign: itemStyle.textAlign }"> |
||||
|
<text class="search-icon iconfont icon-search"></text> |
||||
|
<text> {{ params.placeholder }}</text> |
||||
|
</view> |
||||
|
</view> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
export default { |
||||
|
|
||||
|
/** |
||||
|
* 组件的属性列表 |
||||
|
* 用于组件自定义设置 |
||||
|
*/ |
||||
|
props: { |
||||
|
itemIndex: String, |
||||
|
itemStyle: Object, |
||||
|
params: Object |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 组件的方法列表 |
||||
|
* 更新属性和数据的方法与更新页面数据的方法类似 |
||||
|
*/ |
||||
|
methods: { |
||||
|
|
||||
|
/** |
||||
|
* 跳转到搜索页面 |
||||
|
*/ |
||||
|
onTargetSearch() { |
||||
|
this.$navTo('pages/search/index') |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.diy-search { |
||||
|
background: #f1f1f2; |
||||
|
padding: 20rpx 20rpx; |
||||
|
font-size: 26rpx; |
||||
|
} |
||||
|
|
||||
|
.inner { |
||||
|
height: 60rpx; |
||||
|
background: #fff; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
&.radius { |
||||
|
border-radius: 10rpx; |
||||
|
} |
||||
|
|
||||
|
&.round { |
||||
|
border-radius: 60rpx; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.search-input { |
||||
|
height: 60rpx; |
||||
|
line-height: 60rpx; |
||||
|
color: #999; |
||||
|
padding: 0 20rpx; |
||||
|
|
||||
|
.search-icon { |
||||
|
margin-right: 8rpx; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,102 @@ |
|||||
|
<template> |
||||
|
<!-- 在线客服 --> |
||||
|
<view v-if="!(params.type === 'chat' && !isMpWeiXin)" class="diy-service" :style="{ '--right': `${right}px`, '--bottom': `${bottom}px` }"> |
||||
|
<!-- 拨打电话 --> |
||||
|
<block v-if="params.type === 'phone'"> |
||||
|
<view class="service-icon" @click="onMakePhoneCall"> |
||||
|
<image class="image" :src="params.image"></image> |
||||
|
</view> |
||||
|
</block> |
||||
|
<!-- 在线聊天 --> |
||||
|
<block v-else-if="params.type == 'chat'"> |
||||
|
<button open-type="contact" class="btn-normal"> |
||||
|
<view class="service-icon"> |
||||
|
<image class="image" :src="params.image"></image> |
||||
|
</view> |
||||
|
</button> |
||||
|
</block> |
||||
|
</view> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { rpx2px } from '@/utils/util' |
||||
|
|
||||
|
export default { |
||||
|
|
||||
|
/** |
||||
|
* 组件的属性列表 |
||||
|
* 用于组件自定义设置 |
||||
|
*/ |
||||
|
props: { |
||||
|
itemStyle: Object, |
||||
|
params: Object |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 私有数据,组件的初始数据 |
||||
|
* 可用于模版渲染 |
||||
|
*/ |
||||
|
data() { |
||||
|
return { |
||||
|
isMpWeiXin: false, |
||||
|
isShow: true |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
right() { |
||||
|
return rpx2px(2 * this.itemStyle.right) |
||||
|
}, |
||||
|
bottom() { |
||||
|
return rpx2px(2 * this.itemStyle.bottom) |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
created() { |
||||
|
// #ifdef MP-WEIXIN |
||||
|
this.isMpWeiXin = true |
||||
|
// #endif |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* 组件的方法列表 |
||||
|
* 更新属性和数据的方法与更新页面数据的方法类似 |
||||
|
*/ |
||||
|
methods: { |
||||
|
|
||||
|
/** |
||||
|
* 点击拨打电话 |
||||
|
*/ |
||||
|
onMakePhoneCall(e) { |
||||
|
uni.makePhoneCall({ |
||||
|
phoneNumber: this.params.tel |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.diy-service { |
||||
|
position: fixed; |
||||
|
z-index: 999; |
||||
|
right: calc(var(--window-right) + var(--right)); |
||||
|
// 设置ios刘海屏底部横线安全区域 |
||||
|
bottom: calc(constant(safe-area-inset-bottom) + var(--window-bottom) + var(--bottom)); |
||||
|
bottom: calc(env(safe-area-inset-bottom) + var(--window-bottom) + var(--bottom)); |
||||
|
|
||||
|
.service-icon { |
||||
|
padding: 10rpx; |
||||
|
|
||||
|
.image { |
||||
|
display: block; |
||||
|
width: 90rpx; |
||||
|
height: 90rpx; |
||||
|
border-radius: 50%; |
||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
</style> |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue