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