This commit is contained in:
砂糖
2026-02-07 18:01:13 +08:00
commit 8015759c65
2110 changed files with 269866 additions and 0 deletions

2
fuintUniapp/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/unpackage
/utils/request-1.js

View File

@@ -0,0 +1,20 @@
{ // launch.json 配置了启动调试时相关设置configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
// launchtype项可配置值为local或remote, local代表前端连本地云函数remote代表前端连云端云函数
"version": "0.0",
"configurations": [{
"default" :
{
"launchtype" : "remote"
},
"h5" :
{
"launchtype" : "remote"
},
"mp-weixin" :
{
"launchtype" : "remote"
},
"type" : "uniCloud"
}
]
}

68
fuintUniapp/App.vue Normal file
View File

@@ -0,0 +1,68 @@
<script>
export default {
/**
* 全局变量
*/
globalData: {
},
/**
* 初始化完成时触发
*/
onLaunch(options) {
// 小程序主动更新
this.updateManager()
if (options.query.spm) {
uni.setStorageSync('shareId', options.query.spm);
}
},
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";
</style>

191
fuintUniapp/LICENSE Normal file
View File

@@ -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 2021 延禾信息
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.

122
fuintUniapp/README.md Normal file
View File

@@ -0,0 +1,122 @@
# fuint会员营销系统介绍
#### 介绍
fuint会员营销系统是一套开源的实体店铺会员管理和营销系统。系统基于前后端分离的架构后端采用<b>Java SpringBoot</b> + <b>Mysql</b>,前端基于当前流行的<b>Uniapp</b><b>Element UI</b>支持小程序、h5。主要功能包含电子优惠券、储值卡、实体卡、集次卡计次卡、短信发送、储值卡、会员积分、会员等级权益体系支付收款等会员日常营销工具。本系统适用于各类实体店铺如零售超市、酒吧、酒店、汽车4S店、鲜花店、甜品店、餐饮店、农家乐等是实体店铺会员营销必备的一款利器。
以下是前台的页面展示:
<p><img src="https://fuint-cn.oss-cn-shenzhen.aliyuncs.com/screenshots/g1.png?v=1" alt="前台页面1"></p>
<p><img src="https://fuint-cn.oss-cn-shenzhen.aliyuncs.com/screenshots/g2.png?v=2" alt="前台页面2"></p>
<p><img src="https://fuint-cn.oss-cn-shenzhen.aliyuncs.com/screenshots/g3.png?v=2" alt="前台页面3"></p>
<p><img src="https://fuint-cn.oss-cn-shenzhen.aliyuncs.com/screenshots/g4.png?v=1" alt="前台页面4"></p>
<p><img src="https://fuint-cn.oss-cn-shenzhen.aliyuncs.com/screenshots/g5.png?v=1" alt="前台页面4"></p>
fuint侧重于线下实体店的私域流量的运营同时提供会员端小程序和收银系统的线上线下统一渠道帮助商户降低获客成本。顾客通过扫码支付成为私域流量支付即可成为会员。积分和卡券功能建立起会员等级体系通过消息推送和短信营销方便触达用户。
<p>1、会员运营自动化商家通过日常活动设置如开卡礼设置沉睡唤醒等成为会员后自动给顾客送优惠券让顾客更有黏性提升会员运营效率。</p>
<p>2、打通收银系统和会员营销的壁垒代客下单收银支付即成为会员。</p>
<p>3、会员体系完整化积分兑换、积分转赠、会员等级权益、积分加速、买单折扣。</p>
<p>4、会员卡券齐全储值卡、电子券、优惠券、集次卡、计次卡、实体卡购买并兑换、会员充值、余额支付。</p>
<p>5、线上代客下单收银系统后台管理员可帮助临柜的会员下单、扫码支付。</p>
<p>6、支持手机短信、站内弹框消息、微信订阅消息支持包括发货消息、卡券到期提醒、活动提醒、会员到期提醒、积分余额变动提醒等消息。</p>
<p>小程序前端仓库https://gitee.com/fuint/fuint-uniapp</p>
<b>扫码小程序演示:</b><br>
<p><img src="https://fuint-cn.oss-cn-shenzhen.aliyuncs.com/screenshots/miniapp.png" alt="小程序演示"></p>
<br>
<b>官网演示地址:</b><br>
<p>
1、官网<a target="_blank" href="https://www.fuint.cn">https://www.fuint.cn</a> 点击 -> 系统演示演示账号fuint / 123456<br>
2、swagger接口文档<a target="_blank" href="https://www.fuint.cn/fuint-application/swagger-ui.html">https://www.fuint.cn/fuint-application/swagger-ui.html</a>
</p>
#### 软件架构
后端JAVA SpringBoot + MYSQL Mybatis Plus + Redis
前端采用基于Vue的Uniapp、Element UI前后端分离支持微信小程序、h5等
<p>后台截图:</p>
<p><img src="https://fuint-cn.oss-cn-shenzhen.aliyuncs.com/screenshots/login.png?v=fuint" alt="登录界面"></p>
<p><img src="https://fuint-cn.oss-cn-shenzhen.aliyuncs.com/screenshots/homeV2.png?v=fuint" alt="首页"></p>
前端使用技术<br>
2.1 Vue2<br>
2.2 Uniapp<br>
2.3 Element UI
后端使用技术<br>
1.1 SpringBoot 2.5<br>
1.2 Mybatis Plus<br>
1.3 Maven<br>
1.4 SpringSecurity<br>
1.5 Druid<br>
1.6 Slf4j<br>
1.7 Fastjson<br>
1.8 JWT<br>
1.9 Redis<br>
1.10 Quartz<br>
1.11 Mysql 5.8<br>
1.12 Swagger UI<br>
#### 安装步骤
推荐软件环境版本jdk 1.8、mysql 5.8
1. 导入db目录下的数据库文件。
2. 修改config目录下的配置文件。
3. 将工程打包把jar包上传并执行。
<p>提示无后端和linux基础的朋友可以使用<b>宝塔</b>部署,非常方便简单。</p>
#### 前台使用说明
1. 会员登录,登录成功后可看到会员的卡券列表。
2. 卡券领取和购买,预存券的充值等。
3. 核销卡券,会员在前台出示二维码,管理员用微信扫一扫即可核销。
4. 卡券转赠,会员可将自己的卡券转赠给其他用户,输入对方的手机号即可完成转赠,获赠的好友会收到卡券赠送的短信。
<p><img src="https://fuint-cn.oss-cn-shenzhen.aliyuncs.com/screenshots/create.png?v=fuint" alt="卡券创建界面"></p>
<p><img src="https://fuint-cn.oss-cn-shenzhen.aliyuncs.com/screenshots/member.png?v=fuint" alt="卡券创建界面"></p>
#### 后台使用
1. 会员管理:会员新增、导入、禁用等。
2. 内容管理:焦点图管理、文章管理等。
3. 卡券管理电子券管理为2层结构即电子券组和电子券。
4. 会员积分:会员积分管理,会员积分的操作,会员积分明细查看。
5. 转赠管理:卡券转赠记录。
6. 短信管理:短信营销功能,已发送的短信列表。
7. 系统配置:配置系统管理员权限等。
8. 店铺管理:支持多店铺模式。
9. 核销管理员:核销人员管理主要包含3个功能核销人员列表、核销人员审核、核销人员信息编辑。
10. 短信模板管理:可配置不同场景和业务的短信内容。
11. 卡券发放:单独发放、批量发放,发放成功后给会员发送短信通知
12. 操作日志主要针对电子券系统后台的一些关键操作进行日志记录,方便排查相关操作人的行为等问题。
13. 发券记录主要根据发券的实际操作情况来记录,分为单用户发券和批量发券,同时可针对该次发券记录进行作废操作。
14. 代客下单、收银功能。
<p>卡券营销:</p>
<p><img src="https://fuint-cn.oss-cn-shenzhen.aliyuncs.com/screenshots/coupon-list.png?v=fuint" alt="卡券列表"></p>
<p>收银代客下单功能:店员角色登录后台,从首页的“下单首页”菜单可进入代客收银下单界面,完成代客下单收银的流程。</p>
<p><img src="https://fuint-cn.oss-cn-shenzhen.aliyuncs.com/screenshots/cashier.png?v=fuint3.0.6" alt="收银界面"></p>
<p>发起结算:</p>
<p><img src="https://fuint-cn.oss-cn-shenzhen.aliyuncs.com/screenshots/cashier-1.png?v=fuint3.0.6" alt="收银结算"></p>
#### 开发计划
1. 完善的报表统计;
2. 分享助力、分享领券、分享获得积分;
3. 员工提成、分销功能;
4. 店铺结算功能;
5. 更多营销工具,比如签到等。
#### 允许使用范围:
1. 允许个人学习使用
2. 允许用于毕业设计、论文参考代码
3. 推荐Watch、Star项目获取项目第一时间更新同时也是对项目最好的支持
4. 希望大家多多支持原创软件
5. 请勿去除版权标签,要商用请购买源码授权(非常便宜),感谢理解!
不足和待完善之处请谅解!源码仅供学习交流,更多功能欢迎进群咨询讨论,或需安装帮助请联系我们(<b>麻烦先点star</b>)。<br>
官方网站https://www.fuint.cn <br>
开源不易,感谢支持!<br>
<b>作者wxfsq_better</b><br>
<p><img src="https://fuint-cn.oss-cn-shenzhen.aliyuncs.com/screenshots/qr.png" alt="公众号二维码"></p>
特别鸣谢:<br>
Mybaits Plus: https://github.com/baomidou/mybatis-plus<br>
Vue: https://github.com/vuejs/vue<br>
Element UI: https://element.eleme.cn

View File

@@ -0,0 +1,33 @@
import request from '@/utils/request'
// api地址
const api = {
list: 'clientApi/address/list',
detail: 'clientApi/address/detail',
save: 'clientApi/address/save'
}
// 个人收货地址列表
export const list = (param) => {
return request.get(api.list, param)
}
// 收货地址详情
export const detail = (addressId) => {
return request.post(api.detail, { addressId })
}
// 保存收货地址
export const save = (name, mobile, provinceId, cityId, regionId, detail, status, addressId) => {
return request.post(api.save, { name, mobile, provinceId, cityId, regionId, detail, status, addressId })
}
// 设置默认收货地址
export const setDefault = (addressId, isDefault) => {
return request.post(api.save, { addressId, isDefault })
}
// 删除收货地址
export const remove = (addressId, status) => {
return request.post(api.save, { addressId, status })
}

View File

@@ -0,0 +1,23 @@
import request from '@/utils/request'
// api地址
const api = {
list: 'clientApi/article/list',
detail: 'clientApi/article/detail',
cate: 'clientApi/article/cateList',
}
// 文章列表
export const list = (param) => {
return request.post(api.list, param)
}
// 文章详情
export const detail = (articleId) => {
return request.post(api.detail, { articleId })
}
// 文章分类列表
export const cateList = (param) => {
return request.post(api.cate, param)
}

View File

@@ -0,0 +1,23 @@
import request from '@/utils/request'
// api地址
const api = {
list: 'clientApi/balance/list',
setting: 'clientApi/balance/setting',
doRecharge: 'clientApi/balance/doRecharge',
}
// 余额设置
export const setting = (param) => {
return request.get(api.setting, param)
}
// 余额明细
export const list = (param) => {
return request.post(api.list, param)
}
// 提交充值
export const doRecharge = (rechargeAmount, customAmount) => {
return request.post(api.doRecharge, { rechargeAmount, customAmount })
}

53
fuintUniapp/api/book.js Normal file
View File

@@ -0,0 +1,53 @@
import request from '@/utils/request'
// api地址
const api = {
list: 'clientApi/book/list',
detail: 'clientApi/book/detail',
cate: 'clientApi/book/cateList',
submit: 'clientApi/book/submit',
bookable: 'clientApi/book/bookable',
myBookList: 'clientApi/book/myBook',
myBookDetail: 'clientApi/book/myBookDetail',
cancel: 'clientApi/book/cancel',
}
// 预约项目列表
export const list = (param) => {
return request.post(api.list, param)
}
// 预约详情
export const detail = (bookId) => {
return request.post(api.detail, { bookId })
}
// 预约分类列表
export const cateList = (param) => {
return request.get(api.cate, param)
}
// 提交预约
export const submit = (data) => {
return request.post(api.submit, data)
}
// 是否可预约
export const bookable = (data) => {
return request.post(api.bookable, data)
}
// 我的预约列表
export const myBookList = (param) => {
return request.get(api.myBookList, param)
}
// 我的预约详情
export const myBookDetail = (bookId) => {
return request.post(api.myBookDetail, { bookId })
}
// 取消预约
export function cancel(bookId, data) {
return request.get(api.cancel, { bookId, ...data })
}

23
fuintUniapp/api/cart.js Normal file
View File

@@ -0,0 +1,23 @@
import request from '@/utils/request'
// api地址
const api = {
list: 'clientApi/cart/list',
save: 'clientApi/cart/save',
clear: 'clientApi/cart/clear',
}
// 购物车列表
export const list = (cartIds, goodsId, skuId, buyNum, couponId, point, orderMode) => {
return request.post(api.list, { cartIds, goodsId, skuId, buyNum, couponId, point, orderMode })
}
// 更新购物车
export const save = (goodsId, action, skuId, buyNum) => {
return request.post(api.save, { goodsId, action, skuId, buyNum })
}
// 删除购物车商品
export const clear = (cartId) => {
return request.post(api.clear, { cartId })
}

View File

@@ -0,0 +1,11 @@
import request from '@/utils/request'
// api地址
const api = {
doConfirm: 'clientApi/confirm/doConfirm',
}
// 确定核销
export function doConfirm(code, amount, remark) {
return request.post(api.doConfirm, { code, amount, remark })
}

28
fuintUniapp/api/coupon.js Normal file
View File

@@ -0,0 +1,28 @@
import request from '@/utils/request'
// api地址
const api = {
list: 'clientApi/coupon/list',
receive: 'clientApi/coupon/receive',
detail: 'clientApi/coupon/detail'
}
// 卡券列表
export const list = (param, option) => {
const options = {
isPrompt: true, //(默认 true 说明:本接口抛出的错误是否提示)
load: true, //(默认 true 说明:本接口是否提示加载动画)
...option
}
return request.post(api.list, param, options)
}
// 领券接口
export const receive = (param) => {
return request.post(api.receive, param)
}
// 会员卡券详情
export function detail(couponId) {
return request.post(api.detail, { couponId })
}

17
fuintUniapp/api/give.js Normal file
View File

@@ -0,0 +1,17 @@
import request from '@/utils/request'
// api地址
const api = {
doGive: 'clientApi/give/doGive',
giveLog: 'clientApi/give/giveLog'
}
// 转赠
export const doGive = (data) => {
return request.post(api.doGive, data)
}
// 转赠记录
export const giveLog = (param, option) => {
return request.post(api.giveLog, param)
}

29
fuintUniapp/api/goods.js Normal file
View File

@@ -0,0 +1,29 @@
import request from '@/utils/request'
// api地址
const api = {
cateList: 'clientApi/goodsApi/cateList',
list: 'clientApi/goodsApi/list',
search: 'clientApi/goodsApi/search',
detail: 'clientApi/goodsApi/detail'
}
// 商品分类列表
export const cateList = param => {
return request.get(api.cateList, param)
}
// 商品列表
export const list = param => {
return request.get(api.list, param)
}
// 商品搜索
export const search = param => {
return request.post(api.search, param)
}
// 商品详情
export const detail = goodsId => {
return request.post(api.detail, { goodsId })
}

View File

@@ -0,0 +1,11 @@
import request from '@/utils/request'
// api地址
const api = {
list: 'clientApi/goods/service/list'
}
// 商品评价列表
export function list(goodsId) {
return;
}

11
fuintUniapp/api/help.js Normal file
View File

@@ -0,0 +1,11 @@
import request from '@/utils/request'
// api地址
const api = {
list: 'clientApi/article/list'
}
// 帮助中心列表
export const list = (param) => {
return request.post(api.list, param)
}

View File

@@ -0,0 +1,47 @@
import request from '@/utils/request'
// api地址
const api = {
register: 'clientApi/sign/register',
login: 'clientApi/sign/signIn',
mpWxLogin: 'clientApi/sign/mpWxLogin',
mpWxAuth: 'clientApi/sign/mpWxAuth',
captcha: 'clientApi/captcha/getCode',
sendSmsCaptcha: 'clientApi/sms/sendVerifyCode',
authLoginConfig: 'clientApi/sign/authLoginConfig'
}
// 用户注册
export function register(data) {
return request.post(api.register, data)
}
// 用户登录
export function login(data) {
return request.post(api.login, data)
}
// 微信小程序快捷登录
export function mpWxLogin(data, option) {
return request.post(api.mpWxLogin, data, option)
}
// 微信公众号授权
export function mpWxAuth(data, option) {
return request.post(api.mpWxAuth, data, option)
}
// 图形验证码
export function captcha() {
return request.get(api.captcha)
}
// 发送短信验证码
export function sendSmsCaptcha(data) {
return request.post(api.sendSmsCaptcha, data)
}
// 获取授权登录配置
export function authLoginConfig() {
return request.get(api.authLoginConfig)
}

View File

@@ -0,0 +1,16 @@
import request from '@/utils/request'
// api地址
const api = {
merchantInfo: 'merchantApi/merchant/info',
}
// 当前商户信息
export const info = (param, option) => {
const options = {
isPrompt: true, //(默认 true 说明:本接口抛出的错误是否提示)
load: true, //(默认 true 说明:本接口是否提示加载动画)
...option
}
return request.get(api.merchantInfo, param, options)
}

View File

@@ -0,0 +1,17 @@
import request from '@/utils/request'
// api地址
const api = {
sendCoupon: 'merchantApi/coupon/sendCoupon',
search: 'clientApi/coupon/list'
}
// 查询接口
export const search = (param) => {
return request.post(api.search, param)
}
// 发放卡券
export const sendCoupon = (param) => {
return request.post(api.sendCoupon, param)
}

View File

@@ -0,0 +1,23 @@
import request from '@/utils/request'
// api地址
const api = {
info: 'merchantApi/member/info',
list: 'merchantApi/member/list',
save: 'merchantApi/member/save',
}
// 会员详情
export function detail(memberId, param) {
return request.post(api.info, { memberId, ...param })
}
// 会员列表
export function list(param, option) {
return request.post(api.list, param, option)
}
// 保存会员信息
export const save = (param, option) => {
return request.post(api.save, param)
}

View File

@@ -0,0 +1,18 @@
import request from '@/utils/request'
// api地址
const api = {
list: 'merchantApi/order/list',
detail: 'merchantApi/order/detail'
}
// 订单列表
export function list(param, option) {
return request.post(api.list, param, option)
}
// 订单详情
export function detail(orderId, param) {
return request.get(api.detail, { orderId, ...param })
}

View File

@@ -0,0 +1,11 @@
import request from '@/utils/request'
// api地址
const api = {
doRecharge: 'merchantApi/balance/doRecharge',
}
// 提交充值
export const doRecharge = (rechargeAmount, customAmount, memberId) => {
return request.post(api.doRecharge, { rechargeAmount, customAmount, memberId })
}

View File

@@ -0,0 +1,24 @@
import request from '@/utils/request'
// api地址
const api = {
getOne: 'clientApi/message/getOne',
readed: 'clientApi/message/readed',
getSubTemplate: 'clientApi/message/getSubTemplate'
}
// 读取最新的一条消息
export const getOne = (param, option) => {
return request.get(api.getOne, param)
}
// 将消息置为已读
export const readed = (param) => {
return request.get(api.readed, param)
}
// 获取订阅消息模板
export const getSubTemplate = (param, option) => {
return request.get(api.getSubTemplate, param)
}

View File

@@ -0,0 +1,39 @@
import request from '@/utils/request'
// api地址
const api = {
list: 'clientApi/myCoupon/list',
remove: 'clientApi/myCoupon/remove',
mydetail: 'clientApi/userCouponApi/detail',
detail: 'clientApi/coupon/detail'
}
// 会员卡券列表
export const list = (param, option) => {
const options = {
isPrompt: true, //(默认 true 说明:本接口抛出的错误是否提示)
load: true, //(默认 true 说明:本接口是否提示加载动画)
...option
}
return request.get(api.list, param, options)
}
// 卡券详情
export function detail(couponId, userCouponId, userCouponCode) {
if (parseInt(userCouponId) > 0 || userCouponCode.length > 0) {
if (userCouponId == undefined) {
userCouponId = 0
}
if (userCouponCode == undefined) {
userCouponCode = ""
}
return request.get(api.mydetail, { userCouponId, userCouponCode })
} else {
return request.post(api.detail, { couponId })
}
}
// 删除卡券
export function remove(userCouponId) {
return request.post(api.remove, { userCouponId })
}

41
fuintUniapp/api/order.js Normal file
View File

@@ -0,0 +1,41 @@
import request from '@/utils/request'
// api地址
const api = {
todoCounts: 'clientApi/order/todoCounts',
list: 'clientApi/order/list',
detail: 'clientApi/order/detail',
cancel: 'clientApi/order/cancel',
pay: 'clientApi/pay/doPay',
receipt: 'clientApi/order/receipt',
}
// 当前用户待处理的订单数量
export function todoCounts(param) {
return request.get(api.todoCounts, param)
}
// 我的订单列表
export function list(param, option) {
return request.post(api.list, param, option)
}
// 订单详情
export function detail(orderId, param) {
return request.get(api.detail, { orderId, ...param })
}
// 取消订单
export function cancel(orderId, data) {
return request.get(api.cancel, { orderId, ...data })
}
// 立即支付
export function pay(orderId, payType, param) {
return request.get(api.pay, { orderId, payType, ...param })
}
// 确认收货
export function receipt(orderId, data) {
return request.get(api.receipt, { orderId, ...data })
}

11
fuintUniapp/api/page.js Normal file
View File

@@ -0,0 +1,11 @@
import request from '@/utils/request'
// api地址
const apiUri = {
home: 'clientApi/page/home'
}
// 页面数据
export function home() {
return request.get(apiUri.home)
}

View File

@@ -0,0 +1,17 @@
import request from '@/utils/request'
// api地址
const api = {
list: 'clientApi/points/list',
gift: 'clientApi/points/doGive'
}
// 积分明细列表
export const list = (param) => {
return request.get(api.list, param)
}
// 积分转赠
export const gift = (param) => {
return request.post(api.gift, param)
}

30
fuintUniapp/api/refund.js Normal file
View File

@@ -0,0 +1,30 @@
import request from '@/utils/request'
// api地址
const api = {
list: 'clientApi/refund/list',
goods: 'clientApi/refund/goods',
apply: 'clientApi/refund/submit',
detail: 'clientApi/refund/detail',
delivery: 'clientApi/refund/delivery'
}
// 售后单列表
export const list = (param, option) => {
return request.get(api.list, param, option)
}
// 申请售后
export const apply = (orderId, data) => {
return request.post(api.apply, { orderId, type: data.type, remark: data.content, images: data.images })
}
// 售后单详情
export const detail = (refundId, param) => {
return request.get(api.detail, { refundId, ...param })
}
// 用户发货
export const delivery = (refundId, expressName, expressNo) => {
return request.post(api.delivery, { refundId, expressName, expressNo })
}

17
fuintUniapp/api/region.js Normal file
View File

@@ -0,0 +1,17 @@
import request from '@/utils/request'
// api地址
const api = {
all: 'clientApi/region/all',
tree: 'clientApi/region/tree'
}
// 获取所有地区
export const all = (param) => {
return request.get(api.all, param)
}
// 获取所有地区(树状)
export const tree = (param) => {
return request.get(api.tree, param)
}

View File

@@ -0,0 +1,29 @@
import request from '@/utils/request'
// api地址
const api = {
recharge: 'clientApi/system/recharge',
system: 'clientApi/system/config',
storeList: 'clientApi/store/list',
storeDetail: 'clientApi/store/detail',
}
// 充值配置
export function recharge() {
return request.get(api.recharge)
}
// 系统配置
export function systemConfig() {
return request.get(api.system)
}
// 店铺列表
export const storeList = (keyword) => {
return request.post(api.storeList, { keyword })
}
// 店铺详情
export function storeDetail() {
return request.get(api.storeDetail)
}

View File

@@ -0,0 +1,22 @@
import request from '@/utils/request'
// api地址
const api = {
submit: 'clientApi/settlement/submit',
prePay: 'clientApi/pay/prePay',
}
// 结算台订单提交
export const submit = (targetId, selectNum, type, remark, payAmount, usePoint, couponId, cartIds, goodsId, skuId, buyNum, orderMode, payType) => {
return request.post(api.submit, { targetId, selectNum, type, remark, payAmount, usePoint, couponId, cartIds, goodsId, skuId, buyNum, orderMode, payType})
}
// 支付前查询
export const prePay = (param) => {
return request.get(api.prePay, param)
}
// 发起收款
export const doCashier = (param) => {
return request.post(api.submit, param)
}

17
fuintUniapp/api/share.js Normal file
View File

@@ -0,0 +1,17 @@
import request from '@/utils/request'
// api地址
const api = {
list: 'clientApi/share/list',
getMiniAppLink: 'clientApi/share/getMiniAppLink'
}
// 分享列表
export const list = (param) => {
return request.post(api.list, param)
}
// 生成小程序链接
export const getMiniAppLink = (param) => {
return request.post(api.getMiniAppLink, param)
}

23
fuintUniapp/api/upload.js Normal file
View File

@@ -0,0 +1,23 @@
import request from '@/utils/request'
// api地址
const api = {
uploadUrl: 'clientApi/file/upload'
}
// 图片上传
export const image = (files) => {
// 文件上传大小, 2M
const maxSize = 1024 * 1024 * 2;
// 执行上传
return new Promise((resolve, reject) => {
request.urlFileUpload({ files, maxSize })
.then(result => {
const fileIds = result.map(item => {
return item.data;
})
resolve(fileIds, result)
})
.catch(err => reject(err))
})
}

51
fuintUniapp/api/user.js Normal file
View File

@@ -0,0 +1,51 @@
import request from '@/utils/request'
// api地址
const api = {
userInfo: 'clientApi/user/info',
qrCode: 'clientApi/user/qrCode',
assets: 'clientApi/user/asset',
setting: 'clientApi/user/setting',
defaultStore: 'clientApi/user/defaultStore',
save: 'clientApi/user/saveInfo'
}
// 当前登录的用户信息
export const info = (param, option) => {
const options = {
isPrompt: true, //(默认 true 说明:本接口抛出的错误是否提示)
load: true, //(默认 true 说明:本接口是否提示加载动画)
...option
}
return request.get(api.userInfo, param, options)
}
// 当前登录的会员码信息
export const qrCode = (param, option) => {
const options = {
isPrompt: true,
load: true,
...option
}
return request.get(api.qrCode, param, options)
}
// 账户资产
export const assets = (param, option) => {
return request.get(api.assets, param)
}
// 获取会员设置
export const setting = (param, option) => {
return request.get(api.setting, param)
}
// 设置会员的默认店铺
export const defaultStore = (storeId) => {
return request.get(api.defaultStore, { storeId })
}
// 保存会员信息
export const save = (param, option) => {
return request.post(api.save, param)
}

View File

@@ -0,0 +1,11 @@
import request from '@/utils/request'
// api地址
const api = {
receive: 'clientApi/user/coupon/receive'
}
// 优惠券列表
export const receive = (data) => {
return request.post(api.receive, data)
}

30
fuintUniapp/app.scss Normal file
View File

@@ -0,0 +1,30 @@
/* utils.scss */
@import "/utils/utils.scss";
page {
background: #fafafa;
}
uni-modal {
z-index: 99999999999999 !important;
}
@-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);
}
}

View File

@@ -0,0 +1,3 @@
import paginate from './paginate'
export { paginate }

View File

@@ -0,0 +1,7 @@
export default {
content: [], // 列表数据
currentPage: 1, // 当前页码
totalPages: 1, // 最大页码
pageSize: 15, // 每页记录数
totalElements: 0, // 总记录数
}

View File

@@ -0,0 +1,10 @@
import Enum from '../enum'
/**
* 枚举类:优惠券适用范围
* ApplyRangeEnum
*/
export default new Enum([
{ key: 'ALL', name: '全部商品', value: 10 },
{ key: 'SOME_GOODS', name: '指定商品', value: 20 }
])

View File

@@ -0,0 +1,11 @@
import Enum from '../enum'
/**
* 枚举类:卡券类型
* CouponTypeEnum
*/
export default new Enum([
{ key: 'C', name: '优惠券', value: 1000 },
{ key: 'P', name: '储值卡', value: 2000 },
{ key: 'T', name: '计次卡', value: 3000 }
])

View File

@@ -0,0 +1,10 @@
import Enum from '../enum'
/**
* 枚举类:优惠券到期类型
* ExpireTypeEnum
*/
export default new Enum([
{ key: 'RECEIVE', name: '领取后', value: 10 },
{ key: 'FIXED_TIME', name: '固定时间', value: 20 }
])

View File

@@ -0,0 +1,5 @@
import ApplyRangeEnum from './ApplyRange'
import ExpireTypeEnum from './ExpireType'
import CouponTypeEnum from './CouponType'
export { ApplyRangeEnum, CouponTypeEnum, ExpireTypeEnum }

View File

@@ -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

View File

@@ -0,0 +1,10 @@
import Enum from '../enum'
/**
* 枚举类:订单发货状态
* DeliveryStatusEnum
*/
export default new Enum([
{ key: 'NOT_DELIVERED', name: '未发货', value: 10 },
{ key: 'DELIVERED', name: '已发货', value: 20 }
])

View File

@@ -0,0 +1,9 @@
import Enum from '../enum'
/**
* 枚举类:配送方式
* DeliveryTypeEnum
*/
export default new Enum([
{ key: 'EXPRESS', name: '快递配送', value: 10 }
])

View File

@@ -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 }
])

View File

@@ -0,0 +1,16 @@
import Enum from '../enum'
/**
* 枚举类:订单状态
* OrderStatusEnum
*/
export default new Enum([
{ key: 'CREATED', name: '待支付', value: 'A' },
{ key: 'PAID', name: '已支付', value: 'B' },
{ key: 'CANCEL', name: '已取消', value: 'C' },
{ key: 'DELIVERY', name: '待发货', value: 'D' },
{ key: 'DELIVERED', name: '已发货', value: 'E' },
{ key: 'RECEIVED', name: '已收货', value: 'F' },
{ key: 'DELETED', name: '已删除', value: 'G' },
{ key: 'REFUND', name: '已退款', value: 'H' },
])

View File

@@ -0,0 +1,10 @@
import Enum from '../enum'
/**
* 枚举类:订单支付状态
* PayStatusEnum
*/
export default new Enum([
{ key: 'PENDING', name: '待支付', value: 10 },
{ key: 'SUCCESS', name: '已支付', value: 20 }
])

View File

@@ -0,0 +1,14 @@
import Enum from '../enum'
/**
* 枚举类:订单支付方式
* PayTypeEnum
*/
export default new Enum([
{ key: 'BALANCE', name: '余额支付', value: 'BALANCE' },
{ key: 'WECHAT', name: '微信支付', value: 'JSAPI' },
{ key: 'WECHAT_H5', name: '微信H5支付', value: 'WECHAT_H5' },
{ key: 'CASH', name: '余额支付', value: 'CASH' },
{ key: 'MICROPAY', name: '微信扫码支付', value: 'MICROPAY' },
{ key: 'ALISCAN', name: '支付宝支付', value: 'ALISCAN' },
])

View File

@@ -0,0 +1,10 @@
import Enum from '../enum'
/**
* 枚举类:订单收货状态
* ReceiptStatusEnum
*/
export default new Enum([
{ key: 'NOT_RECEIVED', name: '未收货', value: 10 },
{ key: 'RECEIVED', name: '已收货', value: 20 }
])

View File

@@ -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
}

View File

@@ -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 }
])

View File

@@ -0,0 +1,13 @@
import Enum from '../../enum'
/**
* 枚举类:售后单状态
* RefundStatusEnum
*/
export default new Enum([
{ key: 'A', name: '待审核' },
{ key: 'B', name: '已同意' },
{ key: 'C', name: '已拒绝' },
{ key: 'D', name: '已取消' },
{ key: 'E', name: '已完成' }
])

View File

@@ -0,0 +1,10 @@
import Enum from '../../enum'
/**
* 枚举类:售后类型
* RefundTypeEnum
*/
export default new Enum([
{ key: 'RETURN', name: '退货退款', value: 'return' },
{ key: 'EXCHANGE', name: '换货', value: 'exchange' }
])

View File

@@ -0,0 +1,9 @@
import AuditStatusEnum from './AuditStatus'
import RefundStatusEnum from './RefundStatus'
import RefundTypeEnum from './RefundType'
export {
AuditStatusEnum,
RefundStatusEnum,
RefundTypeEnum
}

View File

@@ -0,0 +1,10 @@
import Enum from '../enum'
/**
* 枚举类:优惠券到期类型
* ExpireTypeEnum
*/
export default new Enum([
{ key: 'privacy', name: '《用户隐私保护指引》', value: '本指引是小程序开发者为处理你的个人信息而制定。本协议主要内容有1. 开发者处理的信息,根据法律规定开发者仅处理实现小程序功能所必要的信息。为了获取配送地址信息开发者将在获取你的明示同意后收集你的位置信息。为了识别用户唯一性实现网上购物所必须的功能及保证保障交易安全所必须的功能。例如账户注册、登录与验证、下单、配送服务、客服及售后等功能开发者将在获取你的明示同意后收集你的手机号。为了进行商品评价开发者将在获取你的明示同意后使用你的相册仅写入权限。为了账户登录开发者将在获取你的明示同意后收集你的微信昵称、头像。2. 第三方插件信息/SDK信息,为实现特定功能开发者可能会接入由第三方提供的插件、SDK。第三方插件、SDK的个人信息处理规则请以其公示的官方说明为准。本小程序为了分享海报保存到本地开发者将在获得你的明示同意后使用你的相册仅写入权限。为了与主播进行视频连麦开发者将在获得你的明示同意后访问你的摄像头。为了商品或中奖礼品发货开发者将在获得你的明示同意后收集你的位置信息。为了与主播进行视频或语音连麦开发者将在获得你的明示同意后访问你的麦克风。为了发送评论时显示头像和昵称开发者将在获得你的明示同意后收集你的微信昵称、头像。3. 你的权益,关于收集你的位置信息、使用你的相册(仅写入)权限,你可以通过以下路径:小程序主页右上角“…”—“设置”—点击特定信息—点击“不允许”,撤回对开发者的授权。关于收集你的手机号,你可以通过以下路径:小程序主页右上角“...” — “设置” — “小程序已获取的信息” — 点击特定信息 — 点击“通知开发者删除”开发者承诺收到通知后将删除信息。法律法规另有规定的开发者承诺将停止除存储和采取必要的安全保护措施之外的处理。关于你的个人信息你可以通过以下方式与开发者联系行使查阅、复制、更正、删除等法定权利。若你在小程序中注册了账号你可以通过以下方式与开发者联系申请注销你在小程序中使用的账号。在受理你的申请后开发者承诺在十五个工作日内完成核查和处理并按照法律法规要求处理你的相关信息。4. 开发者对信息的存储,开发者承诺除法律法规另有规定外开发者对你的信息的保存期限应当为实现处理目的所必要的最短时间。5. 信息的使用规则,开发者将会在本指引所明示的用途内使用收集的信息如开发者使用你的信息超出本指引目的或合理范围开发者必须在变更使用目的或范围前再次以邮箱方式告知并征得你的明示同意。6. 信息对外提供,开发者承诺不会主动共享或转让你的信息至任何第三方如存在确需共享或转让时开发者应当直接征得或确认第三方征得你的单独同意。开发者承诺不会对外公开披露你的信息如必须公开披露时开发者应当向你告知公开披露的目的、披露信息的类型及可能涉及的信息并征得你的单独同意。7. 你认为开发者未遵守上述约定,或有其他的投诉建议、或未成年人个人信息保护相关问题,可通过以下方式与开发者联系;或者向微信进行投诉。' },
{ key: 'member', name: '《用户使用协议》', value: '请您在注册、登录、使用平台之前仔细阅读并理解本协议的全部内容。一旦您点击同意、注册、登录或以任何方式使用平台服务即表示您已充分阅读、理解并接受本协议的全部条款并同意受其约束。一、协议范围本协议适用于您对XX商城平台的所有访问、注册、登录、浏览、购买商品、服务、发布信息、参与活动、评价商品、服务等行为。平台有权随时修改本协议内容并通过平台公告或其他方式通知您。修改后的协议一经发布即有效若您继续使用平台服务则视为您已接受修改后的协议。二、用户注册与账户管理用户注册时应提供真实、准确、完整的个人信息并保证在使用平台服务期间及时更新。若您提供的信息不真实、不准确、不完整或存在误导性平台有权暂停或终止您的账户并保留追究相关责任的权利。用户应妥善保管账户密码及个人信息不得将账户转让、出借或授权他人使用。因用户自身原因导致账户被盗用、密码泄露等安全问题用户应自行承担责任。用户如发现账户异常或被盗用应立即通知平台并配合平台进行调查处理。三、商品、服务购买与使用用户在平台上购买商品、服务时应仔细阅读商品、服务详情、价格、退换货政策等信息并确认订单无误后再进行支付。支付成功后订单即告成立用户需按照订单约定的时间、地点等要求接收商品、服务。用户应遵守平台及商家关于商品、服务使用的相关规定不得进行转售、转赠、非法用途等行为。如遇商品、服务质量问题或发货延迟等情况用户可通过平台提供的客服渠道与商家协商解决。若协商无果可申请平台介入处理。四、知识产权平台上展示的所有内容包括但不限于文字、图片、音频、视频、软件、商标、专利、标识等均受知识产权法律法规保护归平台或相关权利人所有。用户未经授权不得擅自复制、传播、修改、出售或以其他方式使用上述内容。用户在使用平台服务过程中产生的信息如用户评价、晒单等在遵守相关法律法规及平台规则的前提下用户享有相应的知识产权。但用户同意授权平台在全球范围内免费、非独家、可转授权的方式使用这些信息以促进平台的发展和服务质量的提升。五、用户行为规范用户在使用平台服务时应遵守国家法律法规、社会公德及平台规则不得进行任何违法、违规、不道德或损害平台及他人利益的行为。用户不得利用平台发布虚假信息、进行欺诈行为、侵犯他人权益或从事其他违法活动。用户应尊重他人知识产权不得在平台上发布侵犯他人版权、商标权、专利权等知识产权的内容。六、责任与免责平台仅为用户提供商品、服务展示、交易撮合等技术服务不对商品、服务的真实性、合法性、质量及售后服务等承担任何责任。用户因购买商品、服务产生的任何争议或损失应自行与商家协商解决或依法追究商家责任。在法律规定的范围内平台对因不可抗力、技术故障、第三方原因等非平台过错导致的服务中断、数据丢失或其他损失不承担责任。七、争议解决双方因执行本协议发生的或与本协议有关的一切争议应首先通过友好协商解决协商不成时任何一方均可向平台运营方所在地人民法院提起诉讼。八、其他本协议自您点击同意之日起生效至您注销账户或平台终止服务时终止。本协议的解释、效力及争议解决均适用中华人民共和国法律。本协议一式两份用户与平台各执一份具有同等法律效力。请您在仔细阅读并充分理解本协议内容后再决定是否使用XX商城平台及相关服务。如您有任何疑问或建议请随时联系平台客服。感谢您的理解与支持' }
])

View File

@@ -0,0 +1,19 @@
import Enum from '../enum'
/**
* 枚举类:设置项索引
* SettingKeyEnum
*/
export default new Enum([{
key: 'PAGE_CATEGORY_TEMPLATE',
name: '分类页模板',
value: 'page_category_template'
}, {
key: 'POINTS',
name: '积分设置',
value: 'points'
}, {
key: 'RECHARGE',
name: '充值设置',
value: 'recharge'
}])

View File

@@ -0,0 +1,11 @@
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 }
])

View File

@@ -0,0 +1,3 @@
import PageCategoryStyleEnum from './Style'
export { PageCategoryStyleEnum }

View File

@@ -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.data))
})
},
// 获取所有地区(树状)
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, 1 * 24 * 60 * 60 * 1000)
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)
})
})
}
}

View File

@@ -0,0 +1,58 @@
import * as SettingApi from '@/api/setting'
import storage from '@/utils/storage'
const CACHE_KEY = 'Setting'
// 写入缓存, 到期时间30分钟
const setStorage = (data) => {
const expireTime = 30 * 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 是否从缓存中获取
*/
const data = (isCache = true) => {
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 = true) => {
return new Promise((resolve, reject) => {
data(isCache).then(setting => {
resolve(setting[key])
})
})
}
export default {
data,
item
}

View File

@@ -0,0 +1,42 @@
'use strict';
Component({
externalClasses: ['mask-class', 'container-class'],
properties: {
actions: {
type: Array,
value: []
},
show: {
type: Boolean,
value: false
},
cancelWithMask: {
type: Boolean,
value: true
},
cancelText: {
type: String,
value: ''
}
},
methods: {
onMaskClick: function onMaskClick() {
if (this.data.cancelWithMask) {
this.cancelClick();
}
},
cancelClick: function cancelClick() {
this.triggerEvent('cancel');
},
handleBtnClick: function handleBtnClick(_ref) {
var _ref$currentTarget = _ref.currentTarget,
currentTarget = _ref$currentTarget === undefined ? {} : _ref$currentTarget;
var dataset = currentTarget.dataset || {};
var index = dataset.index;
this.triggerEvent('actionclick', { index: index });
}
}
});

View File

@@ -0,0 +1,6 @@
{
"component": true,
"usingComponents": {
"zan-btn": "../btn/index"
}
}

View File

@@ -0,0 +1,39 @@
<view class="zan-actionsheet {{ show ? 'zan-actionsheet--show' : '' }}">
<view
class="mask-class zan-actionsheet__mask"
bindtap="onMaskClick"
></view>
<view class="container-class zan-actionsheet__container">
<!-- 选项按钮 -->
<zan-btn
wx:for="{{ actions }}"
wx:key="this"
bind:btnclick="handleBtnClick"
data-index="{{ index }}"
open-type="{{ item.openType }}"
custom-class="zan-actionsheet__btn"
loading="{{ item.loading }}"
>
<!-- 自定义组件控制 slot 样式有问题,故在 slot 容器上传入 loading 信息 -->
<view class="zan-actionsheet__btn-content {{ item.loading ? 'zan-actionsheet__btn--loading' : '' }}">
<view class="zan-actionsheet__name">{{ item.name }}</view>
<view
wx:if="{{ item.subname }}"
class="zan-actionsheet__subname">
{{ item.subname }}
</view>
</view>
</zan-btn>
<!-- 关闭按钮 -->
<view
wx:if="{{ cancelText }}"
class="zan-actionsheet__footer"
>
<zan-btn
custom-class="zan-actionsheet__btn"
catchtap="cancelClick"
>{{ cancelText }}</zan-btn>
</view>
</view>
</view>

View File

@@ -0,0 +1,86 @@
.zan-actionsheet {
background-color: #f8f8f8;
}
.zan-actionsheet__mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10;
background: rgba(0, 0, 0, 0.7);
display: none;
}
.zan-actionsheet__container {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #f8f8f8;
-webkit-transform: translate3d(0, 50%, 0);
transform: translate3d(0, 50%, 0);
-webkit-transform-origin: center;
transform-origin: center;
-webkit-transition: all 0.2s ease;
transition: all 0.2s ease;
z-index: 11;
opacity: 0;
visibility: hidden;
}
.zan-actionsheet__btn {
margin-bottom: 0 !important;
}
.zan-actionsheet__footer .zan-actionsheet__btn {
background: #fff;
}
.zan-actionsheet__btn-content {
display: -webkit-box;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
flex-direction: row;
-webkit-box-pack: center;
justify-content: center;
}
.zan-actionsheet__subname {
color: #999;
}
.zan-actionsheet__name, .zan-actionsheet__subname {
height: 45px;
line-height: 45px;
}
.zan-actionsheet__btn.zan-btn:last-child::after {
border-bottom-width: 0;
}
.zan-actionsheet__subname {
margin-left: 2px;
font-size: 12px;
}
.zan-actionsheet__footer {
margin-top: 10px;
}
.zan-actionsheet__btn--loading .zan-actionsheet__subname {
color: transparent;
}
.zan-actionsheet--show .zan-actionsheet__container {
opacity: 1;
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
visibility: visible;
}
.zan-actionsheet--show .zan-actionsheet__mask {
display: block;
}

View File

@@ -0,0 +1,61 @@
'use strict';
var nativeButtonBehavior = require('./native-button-behaviors');
Component({
externalClasses: ['custom-class', 'theme-class'],
behaviors: [nativeButtonBehavior],
relations: {
'../btn-group/index': {
type: 'parent',
linked: function linked() {
this.setData({ inGroup: true });
},
unlinked: function unlinked() {
this.setData({ inGroup: false });
}
}
},
properties: {
type: {
type: String,
value: ''
},
size: {
type: String,
value: ''
},
plain: {
type: Boolean,
value: false
},
disabled: {
type: Boolean,
value: false
},
loading: {
type: Boolean,
value: false
}
},
data: {
inGroup: false,
isLast: false
},
methods: {
handleTap: function handleTap() {
if (this.data.disabled) {
this.triggerEvent('disabledclick');
return;
}
this.triggerEvent('btnclick');
},
switchLastButtonStatus: function switchLastButtonStatus() {
var isLast = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
this.setData({ isLast: isLast });
}
}
});

View File

@@ -0,0 +1,3 @@
{
"component": true
}

View File

@@ -0,0 +1,24 @@
<button
class="custom-class theme-class zan-btn {{ inGroup ? 'zan-btn--group' : '' }} {{ isLast ? 'zan-btn--last' : '' }} {{size ? 'zan-btn--'+size : ''}} {{size === 'mini' ? 'zan-btn--plain' : ''}} {{plain ? 'zan-btn--plain' : ''}} {{type ? 'zan-btn--'+type : ''}} {{loading ? 'zan-btn--loading' : ''}} {{disabled ? 'zan-btn--disabled' : ''}}"
disabled="{{ disabled }}"
hover-class="button-hover"
open-type="{{ openType }}"
app-parameter="{{ appParameter }}"
hover-stop-propagation="{{ hoverStopPropagation }}"
hover-start-time="{{ hoverStartTime }}"
hover-stay-time="{{ hoverStayTime }}"
lang="{{ lang }}"
session-from="{{ sessionFrom }}"
send-message-title="{{ sendMessageTitle }}"
send-message-path="{{ sendMessagePath }}"
send-message-img="{{ sendMessageImg }}"
show-message-card="{{ showMessageCard }}"
bindtap="handleTap"
bindcontact="bindcontact"
bindgetuserinfo="bindgetuserinfo"
bindgetphonenumber="bindgetphonenumber"
binderror="binderror"
bindopensetting="bindopensetting"
>
<slot></slot>
</button>

View File

@@ -0,0 +1 @@
.zan-btn{position:relative;color:#333;background-color:#fff;padding-left:15px;padding-right:15px;border-radius:2px;font-size:16px;line-height:45px;height:45px;box-sizing:border-box;text-decoration:none;text-align:center;vertical-align:middle;overflow:visible}.zan-btn--group{margin-bottom:10px}.zan-btn::after{content:'';position:absolute;top:0;left:0;width:200%;height:200%;-webkit-transform:scale(.5);transform:scale(.5);-webkit-transform-origin:0 0;transform-origin:0 0;pointer-events:none;box-sizing:border-box;border:0 solid #e5e5e5;border-width:1px;border-radius:4px}.zan-btn--primary{color:#fff;background-color:#4b0}.zan-btn--primary::after{border-color:#0a0}.zan-btn--warn{color:#fff;background-color:#f85}.zan-btn--warn::after{border-color:#f85}.zan-btn--danger{color:#fff;background-color:#f44}.zan-btn--danger::after{border-color:#e33}.zan-btn--small{display:inline-block;height:30px;line-height:30px;font-size:12px}.zan-btn--small.zan-btn--group{margin-bottom:0;margin-right:5px}.zan-btn--mini{display:inline-block;line-height:21px;height:22px;font-size:10px;padding-left:5px;padding-right:5px}.zan-btn--mini.zan-btn--group{margin-bottom:0;margin-right:5px}.zan-btn--large{border-radius:0;border:none;line-height:50px;height:50px}.zan-btn--large.zan-btn--group{margin-bottom:0}.zan-btn--plain.zan-btn{background-color:transparent}.zan-btn--plain.zan-btn--primary{color:#06bf04}.zan-btn--plain.zan-btn--warn{color:#f60}.zan-btn--plain.zan-btn--danger{color:#f44}.button-hover{opacity:.9}.zan-btn--loading{color:transparent;opacity:1}.zan-btn--loading::before{position:absolute;left:50%;top:50%;content:' ';width:16px;height:16px;margin-left:-8px;margin-top:-8px;border:3px solid #e5e5e5;border-color:#666 #e5e5e5 #e5e5e5 #e5e5e5;border-radius:8px;box-sizing:border-box;-webkit-animation:btn-spin .6s linear;animation:btn-spin .6s linear;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.zan-btn--danger.zan-btn--loading::before,.zan-btn--primary.zan-btn--loading::before,.zan-btn--warn.zan-btn--loading::before{border-color:#fff rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.1)}@-webkit-keyframes btn-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes btn-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.zan-btn.zan-btn--disabled{color:#999!important;background:#f8f8f8!important;border-color:#e5e5e5!important;cursor:not-allowed!important;opacity:1!important}.zan-btn.zan-btn--disabled::after{border-color:#e5e5e5!important}.zan-btn--group.zan-btn--last{margin-bottom:0;margin-right:0}

View File

@@ -0,0 +1,74 @@
'use strict';
module.exports = Behavior({
properties: {
loading: Boolean,
// 在自定义组件中,无法与外界的 form 组件联动,暂时不开放
// formType: String,
openType: String,
appParameter: String,
// 暂时不开放,直接传入无法设置样式
// hoverClass: {
// type: String,
// value: 'button-hover'
// },
hoverStopPropagation: Boolean,
hoverStartTime: {
type: Number,
value: 20
},
hoverStayTime: {
type: Number,
value: 70
},
lang: {
type: String,
value: 'en'
},
sessionFrom: {
type: String,
value: ''
},
sendMessageTitle: String,
sendMessagePath: String,
sendMessageImg: String,
showMessageCard: String
},
methods: {
bindgetuserinfo: function bindgetuserinfo() {
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
_ref$detail = _ref.detail,
detail = _ref$detail === undefined ? {} : _ref$detail;
this.triggerEvent('getuserinfo', detail);
},
bindcontact: function bindcontact() {
var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
_ref2$detail = _ref2.detail,
detail = _ref2$detail === undefined ? {} : _ref2$detail;
this.triggerEvent('contact', detail);
},
bindgetphonenumber: function bindgetphonenumber() {
var _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
_ref3$detail = _ref3.detail,
detail = _ref3$detail === undefined ? {} : _ref3$detail;
this.triggerEvent('getphonenumber', detail);
},
bindopensetting: function bindopensetting() {
var _ref4 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
_ref4$detail = _ref4.detail,
detail = _ref4$detail === undefined ? {} : _ref4$detail;
this.triggerEvent('opensetting', detail);
},
binderror: function binderror() {
var _ref5 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
_ref5$detail = _ref5.detail,
detail = _ref5$detail === undefined ? {} : _ref5$detail;
this.triggerEvent('error', detail);
}
}
});

View File

@@ -0,0 +1,3 @@
export const RED = '#f44';
export const BLUE = '#1989fa';
export const GREEN = '#07c160';

View File

@@ -0,0 +1,48 @@
import { basic } from '../mixins/basic';
import { observe } from '../mixins/observer/index';
function mapKeys(source, target, map) {
Object.keys(map).forEach(key => {
if (source[key]) {
target[map[key]] = source[key];
}
});
}
function VantComponent(vantOptions = {}) {
const options = {};
mapKeys(vantOptions, options, {
data: 'data',
props: 'properties',
mixins: 'behaviors',
methods: 'methods',
beforeCreate: 'created',
created: 'attached',
mounted: 'ready',
relations: 'relations',
destroyed: 'detached',
classes: 'externalClasses'
});
const { relation } = vantOptions;
if (relation) {
options.relations = Object.assign(options.relations || {}, {
[`../${relation.name}/index`]: relation
});
}
// add default externalClasses
options.externalClasses = options.externalClasses || [];
options.externalClasses.push('custom-class');
// add default behaviors
options.behaviors = options.behaviors || [];
options.behaviors.push(basic);
// map field to form-field behavior
if (vantOptions.field) {
options.behaviors.push('wx://form-field');
}
// add default options
options.options = {
multipleSlots: true,
addGlobalClass: true
};
observe(vantOptions, options);
Component(options);
}
export { VantComponent };

View File

@@ -0,0 +1 @@
.van-ellipsis{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.van-multi-ellipsis--l2{overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.van-multi-ellipsis--l3{overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical}.van-clearfix::after{content:'';display:table;clear:both}.van-hairline,.van-hairline--bottom,.van-hairline--left,.van-hairline--right,.van-hairline--surround,.van-hairline--top,.van-hairline--top-bottom{position:relative}.van-hairline--bottom::after,.van-hairline--left::after,.van-hairline--right::after,.van-hairline--surround::after,.van-hairline--top-bottom::after,.van-hairline--top::after,.van-hairline::after{content:' ';position:absolute;pointer-events:none;box-sizing:border-box;-webkit-transform-origin:center;transform-origin:center;top:-50%;left:-50%;right:-50%;bottom:-50%;-webkit-transform:scale(.5);transform:scale(.5);border:0 solid #eee}.van-hairline--top::after{border-top-width:1px}.van-hairline--left::after{border-left-width:1px}.van-hairline--right::after{border-right-width:1px}.van-hairline--bottom::after{border-bottom-width:1px}.van-hairline--top-bottom::after{border-width:1px 0}.van-hairline--surround::after{border-width:1px}

View File

@@ -0,0 +1 @@
.van-clearfix::after{content:'';display:table;clear:both}

View File

@@ -0,0 +1 @@
.van-ellipsis{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.van-multi-ellipsis--l2{overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.van-multi-ellipsis--l3{overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical}

View File

@@ -0,0 +1 @@
.van-hairline,.van-hairline--bottom,.van-hairline--left,.van-hairline--right,.van-hairline--surround,.van-hairline--top,.van-hairline--top-bottom{position:relative}.van-hairline--bottom::after,.van-hairline--left::after,.van-hairline--right::after,.van-hairline--surround::after,.van-hairline--top-bottom::after,.van-hairline--top::after,.van-hairline::after{content:' ';position:absolute;pointer-events:none;box-sizing:border-box;-webkit-transform-origin:center;transform-origin:center;top:-50%;left:-50%;right:-50%;bottom:-50%;-webkit-transform:scale(.5);transform:scale(.5);border:0 solid #eee}.van-hairline--top::after{border-top-width:1px}.van-hairline--left::after{border-left-width:1px}.van-hairline--right::after{border-right-width:1px}.van-hairline--bottom::after{border-bottom-width:1px}.van-hairline--top-bottom::after{border-width:1px 0}.van-hairline--surround::after{border-width:1px}

View File

@@ -0,0 +1,14 @@
function isDef(value) {
return value !== undefined && value !== null;
}
function isObj(x) {
const type = typeof x;
return x !== null && (type === 'object' || type === 'function');
}
function isNumber(value) {
return /^\d+$/.test(value);
}
function range(num, min, max) {
return Math.min(Math.max(num, min), max);
}
export { isObj, isDef, isNumber, range };

View File

@@ -0,0 +1,26 @@
'use strict';
module.exports = {
// 标题
title: '',
// 内容
message: ' ',
// 选择节点
selector: '#zan-dialog',
// 按钮是否展示为纵向
buttonsShowVertical: false,
// 是否展示确定
showConfirmButton: true,
// 确认按钮文案
confirmButtonText: '确定',
// 确认按钮颜色
confirmButtonColor: '#3CC51F',
// 是否展示取消
showCancelButton: false,
// 取消按钮文案
cancelButtonText: '取消',
// 取消按钮颜色
cancelButtonColor: '#333',
// 点击按钮自动关闭 dialog
autoClose: true
};

View File

@@ -0,0 +1,104 @@
'use strict';
var defaultData = require('./data');
function getDialogCtx(_ref) {
var selector = _ref.selector,
pageCtx = _ref.pageCtx;
var ctx = pageCtx;
if (!ctx) {
var pages = getCurrentPages();
ctx = pages[pages.length - 1];
}
return ctx.selectComponent(selector);
}
function getParsedOptions() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return Object.assign({
// 自定义 btn 列表
// { type: 按钮类型回调时以此作为区分依据text: 按钮文案, color: 按钮文字颜色 }
buttons: []
}, defaultData, options);
}
// options 使用参数
// pageCtx 页面 page 上下文
function Dialog(options, pageCtx) {
var parsedOptions = getParsedOptions(options);
var dialogCtx = getDialogCtx({
selector: parsedOptions.selector,
pageCtx: pageCtx
});
if (!dialogCtx) {
console.error('无法找到对应的dialog组件请于页面中注册并在 wxml 中声明 dialog 自定义组件');
return Promise.reject({ type: 'component error' });
}
// 处理默认按钮的展示
// 纵向排布确认按钮在上方
var _parsedOptions$button = parsedOptions.buttons,
buttons = _parsedOptions$button === undefined ? [] : _parsedOptions$button;
var showCustomBtns = false;
if (buttons.length === 0) {
if (parsedOptions.showConfirmButton) {
buttons.push({
type: 'confirm',
text: parsedOptions.confirmButtonText,
color: parsedOptions.confirmButtonColor
});
}
if (parsedOptions.showCancelButton) {
var cancelButton = {
type: 'cancel',
text: parsedOptions.cancelButtonText,
color: parsedOptions.cancelButtonColor
};
if (parsedOptions.buttonsShowVertical) {
buttons.push(cancelButton);
} else {
buttons.unshift(cancelButton);
}
}
} else {
showCustomBtns = true;
}
return new Promise(function (resolve, reject) {
dialogCtx.setData(Object.assign({}, parsedOptions, {
buttons: buttons,
showCustomBtns: showCustomBtns,
key: '' + new Date().getTime(),
show: true,
promiseFunc: { resolve: resolve, reject: reject },
openTypePromiseFunc: null
}));
});
}
Dialog.close = function (options, pageCtx) {
var parsedOptions = getParsedOptions(options);
var dialogCtx = getDialogCtx({
selector: parsedOptions.selector,
pageCtx: pageCtx
});
if (!dialogCtx) {
return;
}
dialogCtx.setData({
show: false,
promiseFunc: null,
openTypePromiseFunc: null
});
};
module.exports = Dialog;

View File

@@ -0,0 +1,148 @@
'use strict';
var _f = function _f() {};
var needResponseOpenTypes = ['getUserInfo', 'getPhoneNumber', 'openSetting'];
Component({
properties: {},
data: {
// 标题
title: '',
// 自定义 btn 列表
// { type: 按钮类型回调时以此作为区分依据text: 按钮文案, color: 按钮文字颜色, openType: 微信开放能力 }
buttons: [],
// 内容
message: ' ',
// 选择节点
selector: '#zan-dialog',
// 是否允许滚动
isScroll: false,
// 按钮是否展示为纵向
buttonsShowVertical: false,
// 是否展示确定
showConfirmButton: true,
// 确认按钮文案
confirmButtonText: '确定',
// 确认按钮颜色
confirmButtonColor: '#3CC51F',
// 是否展示取消
showCancelButton: false,
// 取消按钮文案
cancelButtonText: '取消',
// 取消按钮颜色
cancelButtonColor: '#333',
key: '',
autoClose: true,
show: false,
showCustomBtns: false,
promiseFunc: {},
openTypePromiseFunc: {}
},
methods: {
handleButtonClick: function handleButtonClick(e) {
var _this = this;
var _e$currentTarget = e.currentTarget,
currentTarget = _e$currentTarget === undefined ? {} : _e$currentTarget;
var _currentTarget$datase = currentTarget.dataset,
dataset = _currentTarget$datase === undefined ? {} : _currentTarget$datase;
// 获取当次弹出框的信息
var _ref = this.data.promiseFunc || {},
_ref$resolve = _ref.resolve,
resolve = _ref$resolve === undefined ? _f : _ref$resolve,
_ref$reject = _ref.reject,
reject = _ref$reject === undefined ? _f : _ref$reject;
// 重置展示
if (this.data.autoClose) {
this.setData({
show: false
});
}
// 自定义按钮,全部 resolve 形式返回,根据 type 区分点击按钮
if (this.data.showCustomBtns) {
var isNeedOpenDataButton = needResponseOpenTypes.indexOf(dataset.openType) > -1;
var resolveData = {
type: dataset.type
};
// 如果需要 openData就额外返回一个 promise用于后续 open 数据返回
if (isNeedOpenDataButton) {
resolveData.openDataPromise = new Promise(function(resolve, reject) {
_this.setData({
openTypePromiseFunc: {
resolve: resolve,
reject: reject
}
});
});
resolveData.hasOpenDataPromise = true;
}
resolve(resolveData);
return;
}
// 默认按钮,确认为 resolve取消为 reject
if (dataset.type === 'confirm') {
resolve({
type: 'confirm'
});
} else {
reject({
type: 'cancel'
});
}
this.setData({
promiseFunc: {}
});
},
// 以下为处理微信按钮开放能力的逻辑
handleUserInfoResponse: function handleUserInfoResponse(_ref2) {
var detail = _ref2.detail;
this.__handleOpenDataResponse({
type: detail.errMsg === 'getUserInfo:ok' ? 'resolve' : 'reject',
data: detail
});
},
handlePhoneResponse: function handlePhoneResponse(_ref3) {
var detail = _ref3.detail;
this.__handleOpenDataResponse({
type: detail.errMsg === 'getPhoneNumber:ok' ? 'resolve' : 'reject',
data: detail
});
},
handleOpenSettingResponse: function handleOpenSettingResponse(_ref4) {
var detail = _ref4.detail;
this.__handleOpenDataResponse({
type: detail.errMsg === 'openSetting:ok' ? 'resolve' : 'reject',
data: detail
});
},
__handleOpenDataResponse: function __handleOpenDataResponse(_ref5) {
var _ref5$type = _ref5.type,
type = _ref5$type === undefined ? 'resolve' : _ref5$type,
_ref5$data = _ref5.data,
data = _ref5$data === undefined ? {} : _ref5$data;
var promiseFuncs = this.data.openTypePromiseFunc || {};
var responseFunc = promiseFuncs[type] || _f;
responseFunc(data);
this.setData({
openTypePromiseFunc: null
});
}
}
});

View File

@@ -0,0 +1,7 @@
{
"component": true,
"usingComponents": {
"pop-manager": "../pop-manager/index",
"zan-button": "../btn/index"
}
}

View File

@@ -0,0 +1,18 @@
<pop-manager show="{{ show }}" type="center">
<view class="zan-dialog--container">
<view wx:if="{{ title }}" class="zan-dialog__header">{{ title }}</view>
<view class="zan-dialog__content {{ title ? 'zan-dialog__content--title' : '' }}">
<scroll-view class="zan-dialog__content--scroll" scroll-y="{{ isScroll }}">
<text>{{ message }}</text>
</scroll-view>
</view>
<view class="zan-dialog__footer {{ buttonsShowVertical ? 'zan-dialog__footer--vertical' : 'zan-dialog__footer--horizon' }}">
<block wx:for="{{ buttons }}" wx:key="this">
<zan-button class="zan-dialog__button" custom-class="{{ index === 0 ? 'zan-dialog__button-inside--first' : 'zan-dialog__button-inside' }}" data-type="{{ item.type }}" data-open-type="{{ item.openType }}" open-type="{{ item.openType }}" bind:btnclick="handleButtonClick"
bind:getuserinfo="handleUserInfoResponse" bind:getphonenumber="handlePhoneResponse" bind:opensetting="handleOpenSettingResponse">
<view style="color: {{ item.color || '#333' }}">{{ item.text }}</view>
</zan-button>
</block>
</view>
</view>
</pop-manager>

View File

@@ -0,0 +1,79 @@
.zan-dialog--container {
width: 80vw;
font-size: 16px;
overflow: hidden;
border-radius: 4px;
background-color: #fff;
color: #333;
}
.zan-dialog__header {
padding: 15px 0 0;
text-align: center;
}
.zan-dialog__content {
position: relative;
padding: 15px 20px;
line-height: 1.5;
min-height: 40px;
}
.zan-dialog__content::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 200%;
height: 200%;
-webkit-transform: scale(0.5);
transform: scale(0.5);
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
pointer-events: none;
box-sizing: border-box;
border: 0 solid #e5e5e5;
border-bottom-width: 1px;
}
.zan-dialog__content--title {
color: #999;
font-size: 14px;
}
.zan-dialog__content--scroll {
max-height: 70vh;
}
.zan-dialog__footer {
overflow: hidden;
}
.zan-dialog__button {
-webkit-box-flex: 1;
flex: 1;
}
.zan-dialog__button-inside, .zan-dialog__button-inside--first {
margin-bottom: 0;
line-height: 50px;
height: 50px;
}
.zan-dialog__button-inside--first::after, .zan-dialog__button-inside::after {
border-width: 0;
border-radius: 0;
}
.zan-dialog__footer--horizon {
display: -webkit-box;
display: flex;
}
.zan-dialog__footer--horizon .zan-dialog__button-inside::after {
border-left-width: 1px;
}
.zan-dialog__footer--vertical .zan-dialog__button-inside::after {
border-top-width: 1px;
}

View File

@@ -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: 26rpx;
color: gray;
margin: 10rpx 0rpx 40rpx 0rpx;
}
.empty-icon .image {
width: 280rpx;
}
}
</style>

View File

@@ -0,0 +1,869 @@
<template>
<view class="goods-sku-popup popup" catchtouchmove="true" :class="(value && complete) ? 'show' : 'none'"
@touchmove.stop.prevent="moveHandle">
<!-- 页面内容开始 -->
<view class="mask" @click="close('mask')"></view>
<!-- 页面开始 -->
<view class="layer attr-content" :style="'border-radius: '+borderRadius+'rpx '+borderRadius+'rpx 0 0;'">
<view class="specification-wrapper">
<scroll-view class="specification-wrapper-content" scroll-y="true">
<view class="specification-header">
<view class="specification-left">
<image class="product-img" :src="selectShop.image ? selectShop.image : goodsInfo[goodsThumbName]"
mode="aspectFill"></image>
</view>
<view class="specification-right">
<view class="price-content" :style="'color: '+priceColor+' ;'">
<text class="sign">¥</text>
<text class="price">{{ (selectShop.price || defaultPrice) | priceFilter }}</text>
</view>
<view class="inventory">{{ stockText }}{{ selectShop[stockName] || defaultStock }}</view>
<view class="choose" v-show="goodsInfo[specListName] && goodsInfo[specListName][0].name !== defaultSingleSkuName">
<text v-if="!selectArr.every(val => val == '')">已选{{ selectArr.join(' ') }}</text>
</view>
</view>
</view>
<view class="specification-content">
<view v-show="goodsInfo[specListName][0].name !== defaultSingleSkuName" class="specification-item" v-for="(item, index1) in goodsInfo[specListName]" :key="index1">
<view class="item-title">{{ item.name }}</view>
<view class="item-wrapper">
<view class="item-content" @tap="skuClick(item_value, index1, $event, index2)" v-for="(item_value, index2) in item.list"
:key="index2" :class="[item_value.ishow ? '' : 'noactived', subIndex[index1] == index2 ? 'actived' : '']"
:style="[item_value.ishow ? '' : disableStyle,
item_value.ishow ? btnStyle :'',
subIndex[index1] == index2 ? activedStyle : ''
]">
{{ item_value.name }}
</view>
</view>
</view>
<view style="display: flex;">
<view style="flex: 1;">
<text style="font-size: 26rpx; color: #333; line-height: 50rpx;">数量</text>
</view>
<view style="flex: 4;text-align: right;">
<number-box :min="minBuyNum" :max="maxBuyNum" :step="stepBuyNum" v-model="selectNum"
:positive-integer="true">
</number-box>
</view>
</view>
</view>
</scroll-view>
<view class="close" @click="close('close')" v-if="showClose">
<image class="close-item" :src="closeImage"></image>
</view>
</view>
<view class="btn-option">
<view class="btn-wrapper" v-if="outFoStock || mode == 4">
<view class="sure" style="color:#ffffff;background-color:#cccccc">{{ noStockText }}</view>
</view>
<view class="btn-wrapper" v-else-if="mode == 1">
<view class="sure add-cart" style="border-radius:38rpx 0rpx 0rpx 38rpx;" @click="addCart" :style="'color:'+addCartColor+';background:'+addCartBackgroundColor">{{ addCartText }}</view>
<view class="sure" style="border-radius:0rpx 38rpx 38rpx 0rpx;" @click="buyNow" :style="'color:'+buyNowColor+';background-color:'+buyNowBackgroundColor">{{ buyNowText }}</view>
</view>
<view class="btn-wrapper" v-else-if="mode == 2">
<view class="sure add-cart" @click="addCart" :style="'color:'+addCartColor+';background:'+addCartBackgroundColor">{{ addCartText }}</view>
</view>
<view class="btn-wrapper" v-else-if="mode == 3">
<view class="sure" @click="buyNow" :style="'color:'+buyNowColor+';background:'+buyNowBackgroundColor">{{ buyNowText }}</view>
</view>
</view>
<!-- 页面结束 -->
</view>
<!-- 页面内容结束 -->
</view>
</template>
<script>
import NumberBox from './number-box'
var that; // 当前页面对象
var vk; // 自定义函数集
export default {
name: 'GoodsSkuPopup',
components: {
NumberBox
},
props: {
// true 组件显示 false 组件隐藏
value: {
Type: Boolean,
default: false
},
// vk云函数路由模式参数开始-----------------------------------------------------------
// 商品id
goodsId: {
Type: String,
default: ""
},
// vk路由模式框架下的云函数地址
action: {
Type: String,
default: ""
},
// vk云函数路由模式参数结束-----------------------------------------------------------
// 该商品已抢完时的按钮文字
noStockText: {
Type: String,
default: "该商品已抢完"
},
// 库存文字
stockText: {
Type: String,
default: "库存"
},
// 商品表id的字段名
goodsIdName: {
Type: String,
default: "_id"
},
// sku表id的字段名
skuIdName: {
Type: String,
default: "_id"
},
// sku_list的字段名
skuListName: {
Type: String,
default: "sku_list"
},
// spec_list的字段名
specListName: {
Type: String,
default: "spec_list"
},
// stock的字段名
stockName: {
Type: String,
default: "stock"
},
// sku_name的字段名
skuName: {
Type: String,
default: "sku_name"
},
// sku组合路径的字段名
skuArrName: {
Type: String,
default: "sku_name_arr"
},
// 默认单规格时的规格组名称
defaultSingleSkuName: {
Type: String,
default: "默认"
},
// 模式 1:都显示 2:只显示购物车 3:只显示立即购买 4:显示缺货按钮 默认 1
mode: {
Type: Number,
default: 1
},
// 点击遮罩是否关闭组件 true 关闭 false 不关闭 默认true
maskCloseAble: {
Type: Boolean,
default: true
},
// 顶部圆角值
borderRadius: {
Type: [String, Number],
default: 0
},
// 商品缩略图字段名(未选择sku时)
goodsThumbName: {
Type: [String],
default: "goods_thumb"
},
// 最小购买数量
minBuyNum: {
Type: Number,
default: 1
},
// 最大购买数量
maxBuyNum: {
Type: Number,
default: 100000
},
// 每次点击后的数量
stepBuyNum: {
Type: Number,
default: 1
},
// 自定义获取商品信息的函数
customAction: {
Type: [Function],
default: null
},
// 价格的字体颜色
priceColor: {
Type: String,
default: "#fe560a"
},
// 立即购买按钮的文字
buyNowText: {
Type: String,
default: "立即购买"
},
// 立即购买按钮的字体颜色
buyNowColor: {
Type: String,
default: "#ffffff"
},
// 立即购买按钮的背景颜色
buyNowBackgroundColor: {
Type: String,
default: "linear-gradient(to right, $fuint-theme, $fuint-theme)"
},
// 加入购物车按钮的文字
addCartText: {
Type: String,
default: "加入购物车"
},
// 加入购物车按钮的字体颜色
addCartColor: {
Type: String,
default: "#ffffff"
},
// 加入购物车按钮的背景颜色
addCartBackgroundColor: {
Type: String,
default: "linear-gradient(to right, $fuint-theme, $fuint-theme)"
},
// 不可点击时,按钮的样式
disableStyle: {
Type: Object,
default: null
},
// 按钮点击时的样式
activedStyle: {
Type: Object,
default: null
},
// 按钮常态的样式
btnStyle: {
Type: Object,
default: null
},
// 是否显示右上角关闭按钮
showClose: {
Type: Boolean,
default: true
},
// 关闭按钮的图片地址
closeImage: {
Type: String,
default: "https://img.alicdn.com/imgextra/i1/121022687/O1CN01ImN0O11VigqwzpLiK_!!121022687.png"
},
// 默认库存数量 (未选择sku时)
defaultStock: {
Type: Number,
default: 0
},
// 默认显示的价格 (未选择sku时)
defaultPrice: {
Type: Number,
default: 0
},
},
data() {
return {
complete: false, // 组件是否加载完成
goodsInfo: {}, // 商品信息
isShow: false, // true 显示 false 隐藏
initKey: true, // 是否已初始化
shopItemInfo: {}, // 存放要和选中的值进行匹配的数据
selectArr: [], // 存放被选中的值
subIndex: [], // 是否选中 因为不确定是多规格还是单规格,所以这里定义数组来判断
selectShop: {}, // 存放最后选中的商品
selectNum: this.minBuyNum, // 选中数量
outFoStock: false, // 是否全部sku都缺货
};
},
mounted() {
that = this;
vk = that.vk;
if (that.value) {
that.open();
}
},
methods: {
// 初始化
init() {
// 清空之前的数据
that.selectArr = [];
that.subIndex = [];
that.selectShop = {};
that.selectNum = that.minBuyNum;
that.outFoStock = false;
that.shopItemInfo = {};
let specListName = that.specListName;
that.goodsInfo[specListName].map(item => {
that.selectArr.push('');
that.subIndex.push(-1);
});
that.checkItem(); // 计算sku里面规格形成路径
that.checkInpath(-1); // 传-1是为了不跳过循环
that.autoClickSku(); // 自动选择sku策略
},
// 使用vk路由模式框架获取商品信息
findGoodsInfo() {
if (typeof vk == "undefined") {
that.toast("custom-action必须是function");
return false;
}
vk.callFunction({
url: that.action,
title: '请求中...',
data: {
goods_id: that.goodsId
},
success(data) {
that.updateGoodsInfo(data.goodsInfo);
}
});
},
// 更新商品信息(库存、名称、图片)
updateGoodsInfo(goodsInfo) {
let skuListName = that.skuListName;
if (JSON.stringify(that.goodsInfo) === "{}" || that.goodsInfo[that.goodsIdName] !== goodsInfo[that.goodsIdName]) {
that.goodsInfo = goodsInfo;
that.initKey = true;
} else {
that.goodsInfo[skuListName] = goodsInfo[skuListName];
}
if (that.initKey) {
that.initKey = false;
that.init();
}
// 更新选中sku的库存信息
let select_sku_info = that.getListItem(that.goodsInfo[skuListName], that.skuIdName, that.selectShop[that.skuIdName]);
Object.assign(that.selectShop, select_sku_info);
that.complete = true;
that.$emit("open", true);
that.$emit("input", true);
},
async open() {
let findGoodsInfoRun = true;
let skuListName = that.skuListName;
if (that.customAction && typeof(that.customAction) === 'function') {
let goodsInfo = await that.customAction();
if (goodsInfo && typeof goodsInfo == "object" && JSON.stringify(goodsInfo) != "{}") {
findGoodsInfoRun = false;
that.updateGoodsInfo(goodsInfo);
} else {
that.toast("无法获取到商品信息");
that.$emit("input", false);
return false;
}
} else {
if (findGoodsInfoRun) {
that.findGoodsInfo();
}
}
},
// 监听 - 弹出层收起
close(s) {
if (s == "close") {
that.$emit("input", false);
that.$emit("close", "close");
} else if (s == "mask") {
if (that.maskCloseAble) {
that.$emit("input", false);
that.$emit("close", "mask");
}
}
},
moveHandle() {
//禁止父元素滑动
},
// sku按钮的点击事件
skuClick(value, index1, event, index2) {
if (value.ishow) {
if (that.selectArr[index1] != value.name) {
that.$set(that.selectArr, index1, value.name);
that.$set(that.subIndex, index1, index2);
} else {
that.$set(that.selectArr, index1, '');
that.$set(that.subIndex, index1, -1);
}
that.checkInpath(index1);
// 如果全部选完
that.checkSelectShop();
}
},
// 检测是否已经选完sku
checkSelectShop() {
// 如果全部选完
if (that.selectArr.every(item => item != '')) {
that.selectShop = that.shopItemInfo[that.selectArr];
that.selectNum = that.minBuyNum;
} else {
that.selectShop = {};
}
},
// 检查路径
checkInpath(clickIndex) {
let specListName = that.specListName;
//循环所有属性判断哪些属性可选
//当前选中的兄弟节点和已选中属性不需要循环
let specList = that.goodsInfo[specListName];
for (let i = 0, len = specList.length; i < len; i++) {
if (i == clickIndex) {
continue;
}
let len2 = specList[i].list.length;
for (let j = 0; j < len2; j++) {
if (that.subIndex[i] != -1 && j == that.subIndex[i]) {
continue;
}
let choosed_copy = [...that.selectArr];
that.$set(choosed_copy, i, specList[i].list[j].name);
let choosed_copy2 = choosed_copy.filter(item => item !== '' && typeof item !== 'undefined');
if (that.shopItemInfo.hasOwnProperty(choosed_copy2)) {
specList[i].list[j].ishow = true;
} else {
specList[i].list[j].ishow = false;
}
}
}
that.$set(that.goodsInfo, specListName, specList);
},
// 计算sku里面规格形成路径
checkItem() {
let skuListName = that.skuListName;
// console.time('计算有多小种可选路径需要的时间是');
// 去除库存小于等于0的商品sku
let skuList = that.goodsInfo[skuListName];
let stockNum = 0;
for (let i = 0; i < skuList.length; i++) {
if (skuList[i][that.stockName] <= 0) {
skuList.splice(i, 1);
i--;
} else {
stockNum += skuList[i][that.stockName];
}
}
if (stockNum <= 0) {
that.outFoStock = true;
}
// 计算有多小种可选路径
let result = skuList.reduce(
(arrs, items) => {
return arrs.concat(
items[that.skuArrName].reduce(
(arr, item) => {
return arr.concat(
arr.map(item2 => {
// 利用对象属性的唯一性实现二维数组去重
if (!that.shopItemInfo.hasOwnProperty([...item2, item])) {
that.shopItemInfo[[...item2, item]] = items;
}
return [...item2, item];
})
);
},
[
[]
]
)
);
},
[
[]
]
);
},
// 检测sku选项是否已全部选完,且有库存
checkSelectComplete(obj = {}) {
let selectShop = that.selectShop;
if (selectShop && selectShop[that.skuIdName] !== undefined) {
// 判断库存
if (that.selectNum <= selectShop[that.stockName]) {
if (typeof obj.success == "function") obj.success(selectShop);
} else {
that.toast(that.stockText + "不足", "none")
}
} else {
that.toast("请先选择对应规格", "none");
}
},
// 加入购物车
addCart() {
that.checkSelectComplete({
success: function(selectShop) {
selectShop.buy_num = that.selectNum;
that.$emit("add-cart", selectShop);
}
});
},
// 立即购买
buyNow() {
that.checkSelectComplete({
success: function(selectShop) {
selectShop.buy_num = that.selectNum;
that.$emit("buy-now", selectShop);
}
});
},
// 弹窗
toast(title, icon) {
uni.showToast({
title: title,
icon: icon
});
},
// 获取对象数组中的某一个item,根据指定的键值
getListItem(list, key, value) {
let item;
for (let i in list) {
if (typeof value == "object") {
if (JSON.stringify(list[i][key]) === JSON.stringify(value)) {
item = list[i];
break;
}
} else {
if (list[i][key] === value) {
item = list[i];
break;
}
}
}
return item;
},
// 自动选择sku前提是只有一组sku,默认自动选择最前面的有库存的sku
autoClickSku() {
let skuList = that.goodsInfo[that.skuListName];
let specListArr = that.goodsInfo[that.specListName];
if (specListArr.length == 1) {
let specList = specListArr[0].list;
for (let i = 0; i < specList.length; i++) {
let sku = that.getListItem(skuList, that.skuArrName, [specList[i].name]);
if (sku) {
that.skuClick(specList[i], 0, {}, i);
break;
}
}
}
}
},
// 过滤器
filters: {
// 金额显示过滤器
priceFilter(n = 0) {
if (typeof n == "string") {
n = parseFloat(n)
}
return n ? n.toFixed(2) : n
}
},
// 计算属性
computed: {
},
watch: {
value: function(val) {
if (val) {
that.open();
}
},
}
};
</script>
<style lang="scss" scoped>
/* sku弹出层 */
.goods-sku-popup {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 9999999999;
overflow: hidden;
&.show {
display: block;
.mask {
animation: showPopup 0.2s linear both;
}
.layer {
animation: showLayer 0.2s linear both;
}
}
&.hide {
.mask {
animation: hidePopup 0.2s linear both;
}
.layer {
animation: hideLayer 0.2s linear both;
}
}
&.none {
display: none;
}
.mask {
position: fixed;
top: 0;
width: 100%;
height: 100%;
z-index: 1;
background-color: rgba(0, 0, 0, 0.65);
}
.layer {
display: flex;
width: 100%;
max-height: 1200rpx;
flex-direction: column;
position: fixed;
z-index: 999999;
bottom: 0;
border-radius: 10rpx 10rpx 0 0;
background-color: #ffffff;
margin-top: 10rpx;
overflow-y: scroll;
.btn-option {
padding: 1rpx;
display: block;
clear: both;
margin-bottom: 60rpx;
}
.specification-wrapper {
width: 100%;
margin-top: 20rpx;
padding: 30rpx 25rpx;
box-sizing: border-box;
.specification-wrapper-content {
width: 100%;
min-height: 300rpx;
&::-webkit-scrollbar {
/*隐藏滚轮*/
display: none;
}
.specification-header {
width: 100%;
display: flex;
flex-direction: row;
position: relative;
margin-bottom: 40rpx;
.specification-left {
width: 180rpx;
height: 180rpx;
flex: 0 0 180rpx;
.product-img {
width: 180rpx;
height: 180rpx;
background-color: #999999;
}
}
.specification-right {
flex: 1;
padding: 0 35rpx 10rpx 28rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: flex-end;
font-weight: 500;
.price-content {
color: #fe560a;
margin-bottom: 10rpx;
.sign {
font-size: 28rpx;
margin-right: 4rpx;
}
.price {
font-size: 44rpx;
}
}
.inventory {
font-size: 24rpx;
color: #525252;
margin-bottom: 14rpx;
}
.choose {
font-size: 24rpx;
color: #525252;
min-height: 32rpx;
}
}
}
.specification-content {
font-weight: 500;
.specification-item {
margin-bottom: 40rpx;
&:last-child {
margin-bottom: 0;
}
.item-title {
margin-bottom: 15rpx;
font-size: 32rpx;
font-weight: bold;
color: #000000;
}
.item-wrapper {
display: flex;
flex-direction: row;
flex-flow: wrap;
margin-bottom: -20rpx;
.item-content {
display: block;
padding: 10rpx 20rpx;
min-width: 110rpx;
text-align: center;
font-size: 24rpx;
border-radius: 30rpx;
background-color: #ffffff;
color: #333333;
margin-right: 20rpx;
margin-bottom: 20rpx;
border: 2rpx solid #cccccc;
box-sizing: border-box;
&.actived {
border-color: #fe560a;
color: #fe560a;
}
&.noactived {
// background-color: #e4e4e4;
// border-color: #e4e4e4;
// color: #9e9e9e;
// text-decoration: line-through;
color: #c8c9cc;
background: #f2f3f5;
border-color: #f2f3f5;
}
}
}
}
}
}
.close {
position: absolute;
top: 30rpx;
right: 25rpx;
width: 50rpx;
height: 50rpx;
text-align: center;
line-height: 50rpx;
.close-item {
width: 40rpx;
height: 40rpx;
}
}
}
.btn-wrapper {
display: flex;
width: 100%;
height: 120rpx;
flex: 0 0 120rpx;
align-items: center;
justify-content: space-between;
padding: 0 26rpx;
box-sizing: border-box;
.layer-btn {
width: 335rpx;
height: 80rpx;
border-radius: 40rpx;
color: #fff;
line-height: 80rpx;
text-align: center;
font-weight: 500;
font-size: 28rpx;
&.add-cart {
background: #ffbe46;
}
&.buy {
background: #fe560a;
}
}
.sure {
width: 698rpx;
height: 80rpx;
border-radius: 38rpx;
color: #fff;
line-height: 80rpx;
text-align: center;
font-weight: 500;
font-size: 28rpx;
background: #fe560a;
}
.sure.add-cart {
background: $fuint-theme;
}
}
}
@keyframes showPopup {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes hidePopup {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes showLayer {
0% {
transform: translateY(120%);
}
100% {
transform: translateY(0%);
}
}
@keyframes hideLayer {
0% {
transform: translateY(0);
}
100% {
transform: translateY(120%);
}
}
}
</style>

View File

@@ -0,0 +1,394 @@
<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",
props: {
// 预显示的数字
value: {
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
},
// 是否禁用加减操作
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为falsedisabledInput为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;
})
}
},
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');
}
},
data() {
return {
inputVal: 1, // 输入框中的值不能直接使用props中的value因为应该改变props的状态
timer: null, // 用作长按的定时器
changeFromInner: false, // 值发生变化,是来自内部还是外部
innerChangeTimer: null, // 内部定时器
};
},
created() {
this.inputVal = Number(this.value);
},
computed: {
getCursorSpacing() {
// 先将值转为px单位再转为数值
return Number(uni.upx2px(this.cursorSpacing));
}
},
methods: {
// 点击退格键
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 (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;
if (val > this.max) {
val = this.max;
} else if (val < this.min) {
val = this.min;
}
// 新增stepFirst开始
if(this.stepFirst > 0 && this.inputVal < this.stepFirst && this.inputVal>0){
val = this.stepFirst;
}
// 新增stepFirst结束
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(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>

View File

@@ -0,0 +1,524 @@
<template>
<view class="grade-popup popup" catchtouchmove="true" :class="(value && complete) ? 'show' : 'none'"
@touchmove.stop.prevent="moveHandle">
<view class="mask" @click="close('mask')"></view>
<!-- 要购买的等级信息确认 start -->
<view v-if="!isShowPay" class="layer attr-content" :style="'border-radius: 10rpx 10rpx 0 0;'">
<view class="specification-wrapper">
<scroll-view class="specification-wrapper-content" scroll-y="true">
<view class="specification-header">
<view class="specification-name"><text class="price" v-if="memberGrade.catchValue > 0">{{ memberGrade.catchValue }}</text>购买{{ memberGrade.name }}</view>
</view>
<view class="specification-content">
<view class="grade-item" v-if="memberGrade.discount > 0">
<view class="item-rule"><view class="title">买单折扣</view>{{ memberGrade.discount }}</view>
</view>
<view class="grade-item" v-if="memberGrade.speedPoint > 0">
<view class="item-rule"><view class="title">积分加速</view>{{ memberGrade.speedPoint }}</view>
</view>
<view class="grade-item">
<view class="item-rule">
<view class="title">有效期限</view>
<text v-if="memberGrade.validDay > 0">{{ memberGrade.validDay }}</text>
<text v-else>永久</text>
</view>
</view>
<view class="grade-description">
<view class="item-rule">
<view class="title">权益说明</view>
<view>{{ memberGrade.userPrivilege ? memberGrade.userPrivilege : '暂无'}}</view>
</view>
</view>
</view>
</scroll-view>
<view class="close" @click="close('close')" v-if="showClose">
<image class="close-item" :src="closeImage"></image>
</view>
</view>
<view class="btn-wrapper">
<view class="sure" @click="buyNow">立即购买</view>
</view>
<!-- 页面结束 -->
</view>
<!-- 要购买的等级信息确认 end -->
<!-- 支付信息确认 start -->
<view class="confirm" v-if="isShowPay">
<view class="layer attr-content" :style="'border-radius: 10rpx 10rpx 0 0;'">
<view class="specification-wrapper">
<scroll-view class="specification-wrapper-content" scroll-y="true">
<view class="specification-header">
<view class="specification-name">支付确认</view>
</view>
<view class="specification-content">
<view v-if="couponInfo && couponInfo.amount" class="pay-item">
<view class="item-point">
<view class="title" @click="doUseCoupon">
<text v-if="useCoupon" class="iconfont is-use icon-success"></text>
<text v-if="!useCoupon" class="iconfont icon-success"></text>
<text class="point-amount">使用卡券抵扣</text>
<text class="amount">{{ couponInfo.amount }}</text>
</view>
</view>
</view>
<view class="pay-item">
<view class="item-amount">
<view class="title">
实付金额<text class="amount">{{ ((parseFloat(memberGrade.catchValue) - parseFloat(useCouponInfo.amount)) >= 0 ? (parseFloat(memberGrade.catchValue) - parseFloat(useCouponInfo.amount)) : 0.0.toFixed(2)) }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="close" @click="close('close')" v-if="showClose">
<image class="close-item" :src="closeImage"></image>
</view>
</view>
<view class="btn-wrapper">
<view class="sure" @click="doBuy">确认支付</view>
</view>
<!-- 页面结束 -->
</view>
</view>
<!-- 支付信息确认 end -->
</view>
</template>
<script>
import * as SettlementApi from '@/api/settlement'
import PayTypeEnum from '@/common/enum/order/PayType'
import { wxPayment } from '@/utils/app'
var that; // 当前页面对象
var vk; // 自定义函数集
export default {
name: 'GradePopup',
props: {
// true 组件显示 false 组件隐藏
value: {
Type: Boolean,
default: false
},
// vk云函数路由模式参数开始-----------------------------------------------------------
// 等级信息
memberGrade: {
Type: Object,
default: {}
},
// vk云函数路由模式参数结束-----------------------------------------------------------
// 点击遮罩是否关闭组件 true 关闭 false 不关闭 默认true
maskCloseAble: {
Type: Boolean,
default: true
},
// 是否显示右上角关闭按钮
showClose: {
Type: Boolean,
default: true
},
// 关闭按钮的图片地址
closeImage: {
Type: String,
default: "https://img.alicdn.com/imgextra/i1/121022687/O1CN01ImN0O11VigqwzpLiK_!!121022687.png"
}
},
data() {
return {
complete: false, // 组件是否加载完成
isShowPay: false, // true 显示 false 隐藏
useCoupon: true, // 是否使用卡券
useCouponInfo: { amount: 0, id: '' }, // 使用的卡券
couponInfo: null // 可用卡券
};
},
mounted() {
that = this;
},
methods: {
async open() {
that.complete = true;
that.$emit("open", true);
that.$emit("input", true);
},
// 监听 - 弹出层收起
close(s) {
if (s == "close") {
that.$emit("input", false);
that.$emit("close", "close");
that.isShowPay = false;
that.useCoupon = true;
} else if (s == "mask") {
if (that.maskCloseAble) {
that.$emit("input", false);
that.$emit("close", "mask");
}
}
},
moveHandle() {
//禁止父元素滑动
},
// 立即购买
buyNow() {
this.prePay();
},
// 是否使用卡券
doUseCoupon() {
if (this.useCoupon) {
this.useCoupon = false;
this.useCouponInfo = { amount: 0, id: '' };
} else {
this.useCoupon = true;
this.useCouponInfo = this.couponInfo;
}
},
// 确认购买
doBuy() {
const app = this
let couponId = "";
if (app.useCoupon) {
couponId = app.couponInfo ? app.couponInfo.userCouponId : 0;
}
// 请求api
SettlementApi.submit(app.memberGrade.id, "", "member", "", "", 0, couponId, "", 0, 0, 0, "", "JSAPI")
.then(result => app.onSubmitCallback(result))
.catch(err => {
if (err.result) {
const errData = err.result.data
if (errData) {
return false
}
}
})
},
// 支付前查询
prePay() {
const app = this
// 请求api
SettlementApi.prePay({ type: 'memberGrade' })
.then(result => {
if (result.data) {
if (result.data.canUseCouponInfo) {
app.couponInfo = result.data.canUseCouponInfo;
if (app.useCoupon) {
app.useCouponInfo = app.couponInfo;
}
}
app.isShowPay = true;
}
})
.catch(err => {
if (err.result) {
const errData = err.result.data;
if (errData) {
return false;
}
}
})
},
// 订单提交成功后回调
onSubmitCallback(result) {
const app = this
// 微信支付
if (result.data.payType == PayTypeEnum.WECHAT.value) {
wxPayment(result.data.payment)
.then(() => {
uni.showModal({
title: '支付结果',
content: '支付成功',
showCancel: false,
success(o) {
if (o.confirm) {
app.$router.go(0);
app.$emit('onPaySuccess');
}
}
})
})
.catch(err => app.$error('支付失败'))
.finally(() => {
//empty
})
}
// 余额支付
if (result.data.payType == PayTypeEnum.BALANCE.value) {
app.$error('支付成功');
app.$emit('onPaySuccess');
}
},
// 弹窗
toast(title, icon) {
uni.showToast({
title: title,
icon: icon
});
}
},
watch: {
value: function(val) {
if (val) {
that.open();
}
},
}
};
</script>
<style lang="scss" scoped>
.grade-popup {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 21;
overflow: hidden;
&.show {
display: block;
.mask {
animation: showPopup 0.2s linear both;
}
.layer {
animation: showLayer 0.2s linear both;
}
}
&.hide {
.mask {
animation: hidePopup 0.2s linear both;
}
.layer {
animation: hideLayer 0.2s linear both;
}
}
&.none {
display: none;
}
.mask {
position: fixed;
top: 0;
width: 100%;
height: 100%;
z-index: 1;
background-color: rgba(0, 0, 0, 0.65);
}
.layer {
display: flex;
width: 100%;
flex-direction: column;
position: fixed;
z-index: 99;
bottom: 0;
border-radius: 10rpx 10rpx 0 0;
background-color: #fff;
.specification-wrapper {
width: 100%;
padding: 30rpx 25rpx 10rpx 25rpx;
box-sizing: border-box;
background: #ffffff;
.specification-wrapper-content {
width: 100%;
max-height: 1200rpx;
min-height: 300rpx;
&::-webkit-scrollbar {
/*隐藏滚轮*/
display: none;
}
.specification-header {
width: 100%;
display: flex;
flex-direction: row;
position: relative;
margin-bottom: 40rpx;
text-align: center;
.specification-name {
.price {
color: #f03c3c;
}
font-weight: bold;
width: 100%;
font-size: 30rpx;
padding: 10rpx;
}
}
.specification-content {
font-weight: 500;
font-size: 26rpx;
.grade-item {
.title {
font-weight: bold;
display: flex;
float: left;
}
display: flex;
height: 100rpx;
padding-top:30rpx;
cursor:pointer;
.item-rule {
padding: 20rpx;
border-bottom: solid 1px #cccccc;
width: 100%;
text-align: left;
}
}
.grade-description {
margin-top: 20rpx;
padding: 20rpx;
min-height: 100rpx;
.title {
font-weight: bold;
}
}
.pay-item {
padding: 30rpx;
font-size: 30rpx;
background: #fff;
border: 1rpx solid $fuint-theme;
border-radius: 8rpx;
color: #888;
margin-bottom: 12rpx;
text-align: center;
.amount {
color: #f03c3c;
font-weight: bold;
}
.iconfont {
margin-right: 10rpx;
}
.is-use {
color: $fuint-theme
}
.item-left_icon {
margin-right: 20rpx;
font-size: 48rpx;
&.wechat {
color: #00c800;
}
&.balance {
color: $fuint-theme;
}
}
}
}
}
.close {
position: absolute;
top: 30rpx;
right: 25rpx;
width: 50rpx;
height: 50rpx;
text-align: center;
line-height: 50rpx;
.close-item {
width: 40rpx;
height: 40rpx;
}
}
}
.btn-wrapper {
display: flex;
width: 100%;
height: 120rpx;
flex: 0 0 120rpx;
align-items: center;
justify-content: space-between;
padding: 0 26rpx;
box-sizing: border-box;
margin-bottom: 120rpx;
.layer-btn {
width: 335rpx;
height: 76rpx;
border-radius: 38rpx;
color: #fff;
line-height: 76rpx;
text-align: center;
font-weight: 500;
font-size: 28rpx;
&.add-cart {
background: #ffbe46;
}
&.buy {
background: #fe560a;
}
}
.sure {
width: 698rpx;
height: 80rpx;
border-radius: 40rpx;
color: #fff;
line-height: 80rpx;
text-align: center;
font-weight: 500;
font-size: 28rpx;
background:linear-gradient(to right, #f9211c, #ff6335)
}
.sure.add-cart {
background: $fuint-theme;
}
}
}
@keyframes showPopup {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes hidePopup {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes showLayer {
0% {
transform: translateY(120%);
}
100% {
transform: translateY(0%);
}
}
@keyframes hideLayer {
0% {
transform: translateY(0);
}
100% {
transform: translateY(120%);
}
}
}
</style>

View File

@@ -0,0 +1,645 @@
<template>
<view>
<slot v-if="!nodes.length" />
<!--#ifdef APP-PLUS-NVUE-->
<web-view id="_top" ref="web" :style="'margin-top:-2px;height:'+height+'px'" @onPostMessage="_message" />
<!--#endif-->
<!--#ifndef APP-PLUS-NVUE-->
<view id="_top" :style="showAm+(selectable?';user-select:text;-webkit-user-select:text':'')">
<!--#ifdef H5 || MP-360-->
<div :id="'rtf'+uid"></div>
<!--#endif-->
<!--#ifndef H5 || MP-360-->
<trees :nodes="nodes" :lazyLoad="lazyLoad" :loading="loadingImg" />
<!--#endif-->
</view>
<!--#endif-->
</view>
</template>
<script>
var search;
// #ifndef H5 || APP-PLUS-NVUE || MP-360
import trees from './libs/trees';
var cache = {},
// #ifdef MP-WEIXIN || MP-TOUTIAO
fs = uni.getFileSystemManager ? uni.getFileSystemManager() : null,
// #endif
Parser = require('./libs/MpHtmlParser.js');
var dom;
// 计算 cache 的 key
function hash(str) {
for (var i = str.length, val = 5381; i--;)
val += (val << 5) + str.charCodeAt(i);
return val;
}
// #endif
// #ifdef H5 || APP-PLUS-NVUE || MP-360
var {
windowWidth,
platform
} = uni.getSystemInfoSync(),
cfg = require('./libs/config.js');
// #endif
// #ifdef APP-PLUS-NVUE
var weexDom = weex.requireModule('dom');
// #endif
/**
* Parser 富文本组件
* @tutorial https://github.com/jin-yufeng/Parser
* @property {String} html 富文本数据
* @property {Boolean} autopause 是否在播放一个视频时自动暂停其他视频
* @property {Boolean} autoscroll 是否自动给所有表格添加一个滚动层
* @property {Boolean} autosetTitle 是否自动将 title 标签中的内容设置到页面标题
* @property {Number} compress 压缩等级
* @property {String} domain 图片、视频等链接的主域名
* @property {Boolean} lazyLoad 是否开启图片懒加载
* @property {String} loadingImg 图片加载完成前的占位图
* @property {Boolean} selectable 是否开启长按复制
* @property {Object} tagStyle 标签的默认样式
* @property {Boolean} showWithAnimation 是否使用渐显动画
* @property {Boolean} useAnchor 是否使用锚点
* @property {Boolean} useCache 是否缓存解析结果
* @event {Function} parse 解析完成事件
* @event {Function} load dom 加载完成事件
* @event {Function} ready 所有图片加载完毕事件
* @event {Function} error 错误事件
* @event {Function} imgtap 图片点击事件
* @event {Function} linkpress 链接点击事件
* @author JinYufeng
* @version 20201029
* @listens MIT
*/
export default {
name: 'parser',
data() {
return {
// #ifdef H5 || MP-360
uid: this._uid,
// #endif
// #ifdef APP-PLUS-NVUE
height: 1,
// #endif
// #ifndef APP-PLUS-NVUE
showAm: '',
// #endif
nodes: []
}
},
// #ifndef H5 || APP-PLUS-NVUE || MP-360
components: {
trees
},
// #endif
props: {
html: String,
autopause: {
type: Boolean,
default: true
},
autoscroll: Boolean,
autosetTitle: {
type: Boolean,
default: true
},
// #ifndef H5 || APP-PLUS-NVUE || MP-360
compress: Number,
loadingImg: String,
useCache: Boolean,
// #endif
domain: String,
lazyLoad: Boolean,
selectable: Boolean,
tagStyle: Object,
showWithAnimation: Boolean,
useAnchor: Boolean
},
watch: {
html(html) {
this.setContent(html);
}
},
created() {
// 图片数组
this.imgList = [];
this.imgList.each = function(f) {
for (var i = 0, len = this.length; i < len; i++)
this.setItem(i, f(this[i], i, this));
}
this.imgList.setItem = function(i, src) {
if (i == void 0 || !src) return;
// #ifndef MP-ALIPAY || APP-PLUS
// 去重
if (src.indexOf('http') == 0 && this.includes(src)) {
var newSrc = src.split('://')[0];
for (var j = newSrc.length, c; c = src[j]; j++) {
if (c == '/' && src[j - 1] != '/' && src[j + 1] != '/') break;
newSrc += Math.random() > 0.5 ? c.toUpperCase() : c;
}
newSrc += src.substr(j);
return this[i] = newSrc;
}
// #endif
this[i] = src;
// 暂存 data src
if (src.includes('data:image')) {
var filePath, info = src.match(/data:image\/(\S+?);(\S+?),(.+)/);
if (!info) return;
// #ifdef MP-WEIXIN || MP-TOUTIAO
filePath = `${wx.env.USER_DATA_PATH}/${Date.now()}.${info[1]}`;
fs && fs.writeFile({
filePath,
data: info[3],
encoding: info[2],
success: () => this[i] = filePath
})
// #endif
// #ifdef APP-PLUS
filePath = `_doc/parser_tmp/${Date.now()}.${info[1]}`;
var bitmap = new plus.nativeObj.Bitmap();
bitmap.loadBase64Data(src, () => {
bitmap.save(filePath, {}, () => {
bitmap.clear()
this[i] = filePath;
})
})
// #endif
}
}
},
mounted() {
// #ifdef H5 || MP-360
this.document = document.getElementById('rtf' + this._uid);
// #endif
// #ifndef H5 || APP-PLUS-NVUE || MP-360
if (dom) this.document = new dom(this);
// #endif
if (search) this.search = args => search(this, args);
// #ifdef APP-PLUS-NVUE
this.document = this.$refs.web;
setTimeout(() => {
// #endif
if (this.html) this.setContent(this.html);
// #ifdef APP-PLUS-NVUE
}, 30)
// #endif
},
beforeDestroy() {
// #ifdef H5 || MP-360
if (this._observer) this._observer.disconnect();
// #endif
this.imgList.each(src => {
// #ifdef APP-PLUS
if (src && src.includes('_doc')) {
plus.io.resolveLocalFileSystemURL(src, entry => {
entry.remove();
});
}
// #endif
// #ifdef MP-WEIXIN || MP-TOUTIAO
if (src && src.includes(uni.env.USER_DATA_PATH))
fs && fs.unlink({
filePath: src
})
// #endif
})
clearInterval(this._timer);
},
methods: {
// 设置富文本内容
setContent(html, append) {
// #ifdef APP-PLUS-NVUE
if (!html)
return this.height = 1;
if (append)
this.$refs.web.evalJs("var b=document.createElement('div');b.innerHTML='" + html.replace(/'/g, "\\'") +
"';document.getElementById('parser').appendChild(b)");
else {
html =
'<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"><style>html,body{width:100%;height:100%;overflow:hidden}body{margin:0}</style><base href="' +
this.domain + '"><div id="parser"' + (this.selectable ? '>' : ' style="user-select:none">') + this._handleHtml(html).replace(/\n/g, '\\n') +
'</div><script>"use strict";function e(e){if(window.__dcloud_weex_postMessage||window.__dcloud_weex_){var t={data:[e]};window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessage(t):window.__dcloud_weex_.postMessage(JSON.stringify(t))}}document.body.onclick=function(){e({action:"click"})},' +
(this.showWithAnimation ? 'document.body.style.animation="_show .5s",' : '') +
'setTimeout(function(){e({action:"load",text:document.body.innerText,height:document.getElementById("parser").scrollHeight})},50);\x3c/script>';
if (platform == 'android') html = html.replace(/%/g, '%25');
this.$refs.web.evalJs("document.write('" + html.replace(/'/g, "\\'") + "');document.close()");
}
this.$refs.web.evalJs(
'var t=document.getElementsByTagName("title");t.length&&e({action:"getTitle",title:t[0].innerText});for(var o,n=document.getElementsByTagName("style"),r=1;o=n[r++];)o.innerHTML=o.innerHTML.replace(/body/g,"#parser");for(var a,c=document.getElementsByTagName("img"),s=[],i=0==c.length,d=0,l=0,g=0;a=c[l];l++)parseInt(a.style.width||a.getAttribute("width"))>' +
windowWidth + '&&(a.style.height="auto"),a.onload=function(){++d==c.length&&(i=!0)},a.onerror=function(){++d==c.length&&(i=!0),' + (cfg.errorImg ? 'this.src="' + cfg.errorImg + '",' : '') +
'e({action:"error",source:"img",target:this})},a.hasAttribute("ignore")||"A"==a.parentElement.nodeName||(a.i=g++,s.push(a.getAttribute("original-src")||a.src||a.getAttribute("data-src")),a.onclick=function(t){t.stopPropagation(),e({action:"preview",img:{i:this.i,src:this.src}})});e({action:"getImgList",imgList:s});for(var u,m=document.getElementsByTagName("a"),f=0;u=m[f];f++)u.onclick=function(m){m.stopPropagation();var t,o=this.getAttribute("href");if("#"==o[0]){var n=document.getElementById(o.substr(1));n&&(t=n.offsetTop)}return e({action:"linkpress",href:o,offset:t}),!1};for(var h,y=document.getElementsByTagName("video"),v=0;h=y[v];v++)h.style.maxWidth="100%",h.onerror=function(){e({action:"error",source:"video",target:this})}' +
(this.autopause ? ',h.onplay=function(){for(var e,t=0;e=y[t];t++)e!=this&&e.pause()}' : '') +
';for(var _,p=document.getElementsByTagName("audio"),w=0;_=p[w];w++)_.onerror=function(){e({action:"error",source:"audio",target:this})};' +
(this.autoscroll ? 'for(var T,E=document.getElementsByTagName("table"),B=0;T=E[B];B++){var N=document.createElement("div");N.style.overflow="scroll",T.parentNode.replaceChild(N,T),N.appendChild(T)}' : '') +
'var x=document.getElementById("parser");clearInterval(window.timer),window.timer=setInterval(function(){i&&clearInterval(window.timer),e({action:"ready",ready:i,height:x.scrollHeight})},350)'
)
this.nodes = [1];
// #endif
// #ifdef H5 || MP-360
if (!html) {
if (this.rtf && !append) this.rtf.parentNode.removeChild(this.rtf);
return;
}
var div = document.createElement('div');
if (!append) {
if (this.rtf) this.rtf.parentNode.removeChild(this.rtf);
this.rtf = div;
} else {
if (!this.rtf) this.rtf = div;
else this.rtf.appendChild(div);
}
div.innerHTML = this._handleHtml(html, append);
for (var styles = this.rtf.getElementsByTagName('style'), i = 0, style; style = styles[i++];) {
style.innerHTML = style.innerHTML.replace(/body/g, '#rtf' + this._uid);
style.setAttribute('scoped', 'true');
}
// 懒加载
if (!this._observer && this.lazyLoad && IntersectionObserver) {
this._observer = new IntersectionObserver(changes => {
for (let item, i = 0; item = changes[i++];) {
if (item.isIntersecting) {
item.target.src = item.target.getAttribute('data-src');
item.target.removeAttribute('data-src');
this._observer.unobserve(item.target);
}
}
}, {
rootMargin: '500px 0px 500px 0px'
})
}
var _ts = this;
// 获取标题
var title = this.rtf.getElementsByTagName('title');
if (title.length && this.autosetTitle)
uni.setNavigationBarTitle({
title: title[0].innerText
})
// 填充 domain
var fill = target => {
var src = target.getAttribute('src');
if (this.domain && src) {
if (src[0] == '/') {
if (src[1] == '/')
target.src = (this.domain.includes('://') ? this.domain.split('://')[0] : '') + ':' + src;
else target.src = this.domain + src;
} else if (!src.includes('://') && src.indexOf('data:') != 0) target.src = this.domain + '/' + src;
}
}
// 图片处理
this.imgList.length = 0;
var imgs = this.rtf.getElementsByTagName('img');
for (let i = 0, j = 0, img; img = imgs[i]; i++) {
if (parseInt(img.style.width || img.getAttribute('width')) > windowWidth)
img.style.height = 'auto';
fill(img);
if (!img.hasAttribute('ignore') && img.parentElement.nodeName != 'A') {
img.i = j++;
_ts.imgList.push(img.getAttribute('original-src') || img.src || img.getAttribute('data-src'));
img.onclick = function(e) {
e.stopPropagation();
var preview = true;
this.ignore = () => preview = false;
_ts.$emit('imgtap', this);
if (preview) {
// uni.previewImage({
// current: this.i,
// urls: _ts.imgList
// });
}
}
}
img.onerror = function() {
if (cfg.errorImg)
_ts.imgList[this.i] = this.src = cfg.errorImg;
_ts.$emit('error', {
source: 'img',
target: this
});
}
if (_ts.lazyLoad && this._observer && img.src && img.i != 0) {
img.setAttribute('data-src', img.src);
img.removeAttribute('src');
this._observer.observe(img);
}
}
// 链接处理
var links = this.rtf.getElementsByTagName('a');
for (var link of links) {
link.onclick = function(e) {
e.stopPropagation();
var jump = true,
href = this.getAttribute('href');
_ts.$emit('linkpress', {
href,
ignore: () => jump = false
});
if (jump && href) {
if (href[0] == '#') {
if (_ts.useAnchor) {
_ts.navigateTo({
id: href.substr(1)
})
}
} else if (href.indexOf('http') == 0 || href.indexOf('//') == 0)
return true;
else
uni.navigateTo({
url: href
})
}
return false;
}
}
// 视频处理
var videos = this.rtf.getElementsByTagName('video');
_ts.videoContexts = videos;
for (let video, i = 0; video = videos[i++];) {
fill(video);
video.style.maxWidth = '100%';
video.onerror = function() {
_ts.$emit('error', {
source: 'video',
target: this
});
}
video.onplay = function() {
if (_ts.autopause)
for (let item, i = 0; item = _ts.videoContexts[i++];)
if (item != this) item.pause();
}
}
// 音频处理
var audios = this.rtf.getElementsByTagName('audio');
for (var audio of audios) {
fill(audio);
audio.onerror = function() {
_ts.$emit('error', {
source: 'audio',
target: this
});
}
}
// 表格处理
if (this.autoscroll) {
var tables = this.rtf.getElementsByTagName('table');
for (var table of tables) {
let div = document.createElement('div');
div.style.overflow = 'scroll';
table.parentNode.replaceChild(div, table);
div.appendChild(table);
}
}
if (!append) this.document.appendChild(this.rtf);
this.$nextTick(() => {
this.nodes = [1];
this.$emit('load');
});
setTimeout(() => this.showAm = '', 500);
// #endif
// #ifndef APP-PLUS-NVUE
// #ifndef H5 || MP-360
var nodes;
if (!html) return this.nodes = [];
var parser = new Parser(html, this);
// 缓存读取
if (this.useCache) {
var hashVal = hash(html);
if (cache[hashVal])
nodes = cache[hashVal];
else {
nodes = parser.parse();
cache[hashVal] = nodes;
}
} else nodes = parser.parse();
this.$emit('parse', nodes);
if (append) this.nodes = this.nodes.concat(nodes);
else this.nodes = nodes;
if (nodes.length && nodes.title && this.autosetTitle)
uni.setNavigationBarTitle({
title: nodes.title
})
if (this.imgList) this.imgList.length = 0;
this.videoContexts = [];
this.$nextTick(() => {
(function f(cs) {
for (var i = cs.length; i--;) {
if (cs[i].top) {
cs[i].controls = [];
cs[i].init();
f(cs[i].$children);
}
}
})(this.$children)
this.$emit('load');
})
// #endif
var height;
clearInterval(this._timer);
this._timer = setInterval(() => {
// #ifdef H5 || MP-360
this.rect = this.rtf.getBoundingClientRect();
// #endif
// #ifndef H5 || MP-360
uni.createSelectorQuery().in(this)
.select('#_top').boundingClientRect().exec(res => {
if (!res) return;
this.rect = res[0];
// #endif
if (this.rect.height == height) {
this.$emit('ready', this.rect)
clearInterval(this._timer);
}
height = this.rect.height;
// #ifndef H5 || MP-360
});
// #endif
}, 350);
if (this.showWithAnimation && !append) this.showAm = 'animation:_show .5s';
// #endif
},
// 获取文本内容
getText(ns = this.nodes) {
var txt = '';
// #ifdef APP-PLUS-NVUE
txt = this._text;
// #endif
// #ifdef H5 || MP-360
txt = this.rtf.innerText;
// #endif
// #ifndef H5 || APP-PLUS-NVUE || MP-360
for (var i = 0, n; n = ns[i++];) {
if (n.type == 'text') txt += n.text.replace(/&nbsp;/g, '\u00A0').replace(/&lt;/g, '<').replace(/&gt;/g, '>')
.replace(/&amp;/g, '&');
else if (n.type == 'br') txt += '\n';
else {
// 块级标签前后加换行
var block = n.name == 'p' || n.name == 'div' || n.name == 'tr' || n.name == 'li' || (n.name[0] == 'h' && n.name[1] >
'0' && n.name[1] < '7');
if (block && txt && txt[txt.length - 1] != '\n') txt += '\n';
if (n.children) txt += this.getText(n.children);
if (block && txt[txt.length - 1] != '\n') txt += '\n';
else if (n.name == 'td' || n.name == 'th') txt += '\t';
}
}
// #endif
return txt;
},
// 锚点跳转
in (obj) {
if (obj.page && obj.selector && obj.scrollTop) this._in = obj;
},
navigateTo(obj) {
if (!this.useAnchor) return obj.fail && obj.fail('Anchor is disabled');
// #ifdef APP-PLUS-NVUE
if (!obj.id)
weexDom.scrollToElement(this.$refs.web);
else
this.$refs.web.evalJs('var pos=document.getElementById("' + obj.id +
'");if(pos)post({action:"linkpress",href:"#",offset:pos.offsetTop+' + (obj.offset || 0) + '})');
obj.success && obj.success();
// #endif
// #ifndef APP-PLUS-NVUE
var d = ' ';
// #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
d = '>>>';
// #endif
var selector = uni.createSelectorQuery().in(this._in ? this._in.page : this).select((this._in ? this._in.selector :
'#_top') + (obj.id ? `${d}#${obj.id},${this._in?this._in.selector:'#_top'}${d}.${obj.id}` : '')).boundingClientRect();
if (this._in) selector.select(this._in.selector).scrollOffset().select(this._in.selector).boundingClientRect();
else selector.selectViewport().scrollOffset();
selector.exec(res => {
if (!res[0]) return obj.fail && obj.fail('Label not found')
var scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + (obj.offset || 0);
if (this._in) this._in.page[this._in.scrollTop] = scrollTop;
else uni.pageScrollTo({
scrollTop,
duration: 300
})
obj.success && obj.success();
})
// #endif
},
// 获取视频对象
getVideoContext(id) {
// #ifndef APP-PLUS-NVUE
if (!id) return this.videoContexts;
else
for (var i = this.videoContexts.length; i--;)
if (this.videoContexts[i].id == id) return this.videoContexts[i];
// #endif
},
// #ifdef H5 || APP-PLUS-NVUE || MP-360
_handleHtml(html, append) {
if (!append) {
// 处理 tag-style 和 userAgentStyles
var style = '<style>@keyframes _show{0%{opacity:0}100%{opacity:1}}img{max-width:100%;display:block}';
for (var item in cfg.userAgentStyles)
style += `${item}{${cfg.userAgentStyles[item]}}`;
for (item in this.tagStyle)
style += `${item}{${this.tagStyle[item]}}`;
style += '</style>';
html = style + html;
}
// 处理 rpx
if (html.includes('rpx'))
html = html.replace(/[0-9.]+\s*rpx/g, $ => (parseFloat($) * windowWidth / 750) + 'px');
return html;
},
// #endif
// #ifdef APP-PLUS-NVUE
_message(e) {
// 接收 web-view 消息
var d = e.detail.data[0];
switch (d.action) {
case 'load':
this.$emit('load');
this.height = d.height;
this._text = d.text;
break;
case 'getTitle':
if (this.autosetTitle)
uni.setNavigationBarTitle({
title: d.title
})
break;
case 'getImgList':
this.imgList.length = 0;
for (var i = d.imgList.length; i--;)
this.imgList.setItem(i, d.imgList[i]);
break;
case 'preview':
var preview = true;
d.img.ignore = () => preview = false;
this.$emit('imgtap', d.img);
if (preview)
// uni.previewImage({
// current: d.img.i,
// urls: this.imgList
// })
break;
case 'linkpress':
var jump = true,
href = d.href;
this.$emit('linkpress', {
href,
ignore: () => jump = false
})
if (jump && href) {
if (href[0] == '#') {
if (this.useAnchor)
weexDom.scrollToElement(this.$refs.web, {
offset: d.offset
})
} else if (href.includes('://'))
plus.runtime.openWeb(href);
else
uni.navigateTo({
url: href
})
}
break;
case 'error':
if (d.source == 'img' && cfg.errorImg)
this.imgList.setItem(d.target.i, cfg.errorImg);
this.$emit('error', {
source: d.source,
target: d.target
})
break;
case 'ready':
this.height = d.height;
if (d.ready) uni.createSelectorQuery().in(this).select('#_top').boundingClientRect().exec(res => {
this.rect = res[0];
this.$emit('ready', res[0]);
})
break;
case 'click':
this.$emit('click');
this.$emit('tap');
}
},
// #endif
}
}
</script>
<style>
@keyframes _show {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
/* #ifdef MP-WEIXIN */
:host {
display: block;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
/* #endif */
</style>

View File

@@ -0,0 +1,100 @@
const cfg = require('./config.js'),
isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
function CssHandler(tagStyle) {
var styles = Object.assign(Object.create(null), cfg.userAgentStyles);
for (var item in tagStyle)
styles[item] = (styles[item] ? styles[item] + ';' : '') + tagStyle[item];
this.styles = styles;
}
CssHandler.prototype.getStyle = function(data) {
this.styles = new parser(data, this.styles).parse();
}
CssHandler.prototype.match = function(name, attrs) {
var tmp, matched = (tmp = this.styles[name]) ? tmp + ';' : '';
if (attrs.class) {
var items = attrs.class.split(' ');
for (var i = 0, item; item = items[i]; i++)
if (tmp = this.styles['.' + item])
matched += tmp + ';';
}
if (tmp = this.styles['#' + attrs.id])
matched += tmp + ';';
return matched;
}
module.exports = CssHandler;
function parser(data, init) {
this.data = data;
this.floor = 0;
this.i = 0;
this.list = [];
this.res = init;
this.state = this.Space;
}
parser.prototype.parse = function() {
for (var c; c = this.data[this.i]; this.i++)
this.state(c);
return this.res;
}
parser.prototype.section = function() {
return this.data.substring(this.start, this.i);
}
// 状态机
parser.prototype.Space = function(c) {
if (c == '.' || c == '#' || isLetter(c)) {
this.start = this.i;
this.state = this.Name;
} else if (c == '/' && this.data[this.i + 1] == '*')
this.Comment();
else if (!cfg.blankChar[c] && c != ';')
this.state = this.Ignore;
}
parser.prototype.Comment = function() {
this.i = this.data.indexOf('*/', this.i) + 1;
if (!this.i) this.i = this.data.length;
this.state = this.Space;
}
parser.prototype.Ignore = function(c) {
if (c == '{') this.floor++;
else if (c == '}' && !--this.floor) {
this.list = [];
this.state = this.Space;
}
}
parser.prototype.Name = function(c) {
if (cfg.blankChar[c]) {
this.list.push(this.section());
this.state = this.NameSpace;
} else if (c == '{') {
this.list.push(this.section());
this.Content();
} else if (c == ',') {
this.list.push(this.section());
this.Comma();
} else if (!isLetter(c) && (c < '0' || c > '9') && c != '-' && c != '_')
this.state = this.Ignore;
}
parser.prototype.NameSpace = function(c) {
if (c == '{') this.Content();
else if (c == ',') this.Comma();
else if (!cfg.blankChar[c]) this.state = this.Ignore;
}
parser.prototype.Comma = function() {
while (cfg.blankChar[this.data[++this.i]]);
if (this.data[this.i] == '{') this.Content();
else {
this.start = this.i--;
this.state = this.Name;
}
}
parser.prototype.Content = function() {
this.start = ++this.i;
if ((this.i = this.data.indexOf('}', this.i)) == -1) this.i = this.data.length;
var content = this.section();
for (var i = 0, item; item = this.list[i++];)
if (this.res[item]) this.res[item] += ';' + content;
else this.res[item] = content;
this.list = [];
this.state = this.Space;
}

View File

@@ -0,0 +1,580 @@
/**
* html 解析器
* @tutorial https://github.com/jin-yufeng/Parser
* @version 20201029
* @author JinYufeng
* @listens MIT
*/
const cfg = require('./config.js'),
blankChar = cfg.blankChar,
CssHandler = require('./CssHandler.js'),
windowWidth = uni.getSystemInfoSync().windowWidth;
var emoji;
function MpHtmlParser(data, options = {}) {
this.attrs = {};
this.CssHandler = new CssHandler(options.tagStyle, windowWidth);
this.data = data;
this.domain = options.domain;
this.DOM = [];
this.i = this.start = this.audioNum = this.imgNum = this.videoNum = 0;
options.prot = (this.domain || '').includes('://') ? this.domain.split('://')[0] : 'http';
this.options = options;
this.state = this.Text;
this.STACK = [];
// 工具函数
this.bubble = () => {
for (var i = this.STACK.length, item; item = this.STACK[--i];) {
if (cfg.richOnlyTags[item.name]) return false;
item.c = 1;
}
return true;
}
this.decode = (val, amp) => {
var i = -1,
j, en;
while (1) {
if ((i = val.indexOf('&', i + 1)) == -1) break;
if ((j = val.indexOf(';', i + 2)) == -1) break;
if (val[i + 1] == '#') {
en = parseInt((val[i + 2] == 'x' ? '0' : '') + val.substring(i + 2, j));
if (!isNaN(en)) val = val.substr(0, i) + String.fromCharCode(en) + val.substr(j + 1);
} else {
en = val.substring(i + 1, j);
if (cfg.entities[en] || en == amp)
val = val.substr(0, i) + (cfg.entities[en] || '&') + val.substr(j + 1);
}
}
return val;
}
this.getUrl = url => {
if (url[0] == '/') {
if (url[1] == '/') url = this.options.prot + ':' + url;
else if (this.domain) url = this.domain + url;
} else if (this.domain && url.indexOf('data:') != 0 && !url.includes('://'))
url = this.domain + '/' + url;
return url;
}
this.isClose = () => this.data[this.i] == '>' || (this.data[this.i] == '/' && this.data[this.i + 1] == '>');
this.section = () => this.data.substring(this.start, this.i);
this.parent = () => this.STACK[this.STACK.length - 1];
this.siblings = () => this.STACK.length ? this.parent().children : this.DOM;
}
MpHtmlParser.prototype.parse = function() {
if (emoji) this.data = emoji.parseEmoji(this.data);
for (var c; c = this.data[this.i]; this.i++)
this.state(c);
if (this.state == this.Text) this.setText();
while (this.STACK.length) this.popNode(this.STACK.pop());
return this.DOM;
}
// 设置属性
MpHtmlParser.prototype.setAttr = function() {
var name = this.attrName.toLowerCase(),
val = this.attrVal;
if (cfg.boolAttrs[name]) this.attrs[name] = 'T';
else if (val) {
if (name == 'src' || (name == 'data-src' && !this.attrs.src)) this.attrs.src = this.getUrl(this.decode(val, 'amp'));
else if (name == 'href' || name == 'style') this.attrs[name] = this.decode(val, 'amp');
else if (name.substr(0, 5) != 'data-') this.attrs[name] = val;
}
this.attrVal = '';
while (blankChar[this.data[this.i]]) this.i++;
if (this.isClose()) this.setNode();
else {
this.start = this.i;
this.state = this.AttrName;
}
}
// 设置文本节点
MpHtmlParser.prototype.setText = function() {
var back, text = this.section();
if (!text) return;
text = (cfg.onText && cfg.onText(text, () => back = true)) || text;
if (back) {
this.data = this.data.substr(0, this.start) + text + this.data.substr(this.i);
let j = this.start + text.length;
for (this.i = this.start; this.i < j; this.i++) this.state(this.data[this.i]);
return;
}
if (!this.pre) {
// 合并空白符
var flag, tmp = [];
for (let i = text.length, c; c = text[--i];)
if (!blankChar[c]) {
tmp.unshift(c);
if (!flag) flag = 1;
} else {
if (tmp[0] != ' ') tmp.unshift(' ');
if (c == '\n' && flag == void 0) flag = 0;
}
if (flag == 0) return;
text = tmp.join('');
}
this.siblings().push({
type: 'text',
text: this.decode(text)
});
}
// 设置元素节点
MpHtmlParser.prototype.setNode = function() {
var node = {
name: this.tagName.toLowerCase(),
attrs: this.attrs
},
close = cfg.selfClosingTags[node.name];
if (this.options.nodes.length) node.type = 'node';
this.attrs = {};
if (!cfg.ignoreTags[node.name]) {
// 处理属性
var attrs = node.attrs,
style = this.CssHandler.match(node.name, attrs, node) + (attrs.style || ''),
styleObj = {};
if (attrs.id) {
if (this.options.compress & 1) attrs.id = void 0;
else if (this.options.useAnchor) this.bubble();
}
if ((this.options.compress & 2) && attrs.class) attrs.class = void 0;
switch (node.name) {
case 'a':
case 'ad': // #ifdef APP-PLUS
case 'iframe':
// #endif
this.bubble();
break;
case 'font':
if (attrs.color) {
styleObj['color'] = attrs.color;
attrs.color = void 0;
}
if (attrs.face) {
styleObj['font-family'] = attrs.face;
attrs.face = void 0;
}
if (attrs.size) {
var size = parseInt(attrs.size);
if (size < 1) size = 1;
else if (size > 7) size = 7;
var map = ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'];
styleObj['font-size'] = map[size - 1];
attrs.size = void 0;
}
break;
case 'embed':
// #ifndef APP-PLUS
var src = node.attrs.src || '',
type = node.attrs.type || '';
if (type.includes('video') || src.includes('.mp4') || src.includes('.3gp') || src.includes('.m3u8'))
node.name = 'video';
else if (type.includes('audio') || src.includes('.m4a') || src.includes('.wav') || src.includes('.mp3') || src.includes(
'.aac'))
node.name = 'audio';
else break;
if (node.attrs.autostart)
node.attrs.autoplay = 'T';
node.attrs.controls = 'T';
// #endif
// #ifdef APP-PLUS
this.bubble();
break;
// #endif
case 'video':
case 'audio':
if (!attrs.id) attrs.id = node.name + (++this[`${node.name}Num`]);
else this[`${node.name}Num`]++;
if (node.name == 'video') {
if (this.videoNum > 3)
node.lazyLoad = 1;
if (attrs.width) {
styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px');
attrs.width = void 0;
}
if (attrs.height) {
styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px');
attrs.height = void 0;
}
}
if (!attrs.controls && !attrs.autoplay) attrs.controls = 'T';
attrs.source = [];
if (attrs.src) {
attrs.source.push(attrs.src);
attrs.src = void 0;
}
this.bubble();
break;
case 'td':
case 'th':
if (attrs.colspan || attrs.rowspan)
for (var k = this.STACK.length, item; item = this.STACK[--k];)
if (item.name == 'table') {
item.flag = 1;
break;
}
}
if (attrs.align) {
if (node.name == 'table') {
if (attrs.align == 'center') styleObj['margin-inline-start'] = styleObj['margin-inline-end'] = 'auto';
else styleObj['float'] = attrs.align;
} else styleObj['text-align'] = attrs.align;
attrs.align = void 0;
}
// 压缩 style
var styles = style.split(';');
style = '';
for (var i = 0, len = styles.length; i < len; i++) {
var info = styles[i].split(':');
if (info.length < 2) continue;
let key = info[0].trim().toLowerCase(),
value = info.slice(1).join(':').trim();
if (value[0] == '-' || value.includes('safe'))
style += `;${key}:${value}`;
else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import'))
styleObj[key] = value;
}
if (node.name == 'img') {
if (attrs.src && !attrs.ignore) {
if (this.bubble())
attrs.i = (this.imgNum++).toString();
else attrs.ignore = 'T';
}
if (attrs.ignore) {
style += ';-webkit-touch-callout:none';
styleObj['max-width'] = '100%';
}
var width;
if (styleObj.width) width = styleObj.width;
else if (attrs.width) width = attrs.width.includes('%') ? attrs.width : parseFloat(attrs.width) + 'px';
if (width) {
styleObj.width = width;
attrs.width = '100%';
if (parseInt(width) > windowWidth) {
styleObj.height = '';
if (attrs.height) attrs.height = void 0;
}
}
if (styleObj.height) {
attrs.height = styleObj.height;
styleObj.height = '';
} else if (attrs.height && !attrs.height.includes('%'))
attrs.height = parseFloat(attrs.height) + 'px';
}
for (var key in styleObj) {
var value = styleObj[key];
if (!value) continue;
if (key.includes('flex') || key == 'order' || key == 'self-align') node.c = 1;
// 填充链接
if (value.includes('url')) {
var j = value.indexOf('(');
if (j++ != -1) {
while (value[j] == '"' || value[j] == "'" || blankChar[value[j]]) j++;
value = value.substr(0, j) + this.getUrl(value.substr(j));
}
}
// 转换 rpx
else if (value.includes('rpx'))
value = value.replace(/[0-9.]+\s*rpx/g, $ => parseFloat($) * windowWidth / 750 + 'px');
else if (key == 'white-space' && value.includes('pre') && !close)
this.pre = node.pre = true;
style += `;${key}:${value}`;
}
style = style.substr(1);
if (style) attrs.style = style;
if (!close) {
node.children = [];
if (node.name == 'pre' && cfg.highlight) {
this.remove(node);
this.pre = node.pre = true;
}
this.siblings().push(node);
this.STACK.push(node);
} else if (!cfg.filter || cfg.filter(node, this) != false)
this.siblings().push(node);
} else {
if (!close) this.remove(node);
else if (node.name == 'source') {
var parent = this.parent();
if (parent && (parent.name == 'video' || parent.name == 'audio') && node.attrs.src)
parent.attrs.source.push(node.attrs.src);
} else if (node.name == 'base' && !this.domain) this.domain = node.attrs.href;
}
if (this.data[this.i] == '/') this.i++;
this.start = this.i + 1;
this.state = this.Text;
}
// 移除标签
MpHtmlParser.prototype.remove = function(node) {
var name = node.name,
j = this.i;
// 处理 svg
var handleSvg = () => {
var src = this.data.substring(j, this.i + 1);
node.attrs.xmlns = 'http://www.w3.org/2000/svg';
for (var key in node.attrs) {
if (key == 'viewbox') src = ` viewBox="${node.attrs.viewbox}"` + src;
else if (key != 'style') src = ` ${key}="${node.attrs[key]}"` + src;
}
src = '<svg' + src;
var parent = this.parent();
if (node.attrs.width == '100%' && parent && (parent.attrs.style || '').includes('inline'))
parent.attrs.style = 'width:300px;max-width:100%;' + parent.attrs.style;
this.siblings().push({
name: 'img',
attrs: {
src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),
style: node.attrs.style,
ignore: 'T'
}
})
}
if (node.name == 'svg' && this.data[j] == '/') return handleSvg(this.i++);
while (1) {
if ((this.i = this.data.indexOf('</', this.i + 1)) == -1) {
if (name == 'pre' || name == 'svg') this.i = j;
else this.i = this.data.length;
return;
}
this.start = (this.i += 2);
while (!blankChar[this.data[this.i]] && !this.isClose()) this.i++;
if (this.section().toLowerCase() == name) {
// 代码块高亮
if (name == 'pre') {
this.data = this.data.substr(0, j + 1) + cfg.highlight(this.data.substring(j + 1, this.i - 5), node.attrs) + this.data
.substr(this.i - 5);
return this.i = j;
} else if (name == 'style')
this.CssHandler.getStyle(this.data.substring(j + 1, this.i - 7));
else if (name == 'title')
this.DOM.title = this.data.substring(j + 1, this.i - 7);
if ((this.i = this.data.indexOf('>', this.i)) == -1) this.i = this.data.length;
if (name == 'svg') handleSvg();
return;
}
}
}
// 节点出栈处理
MpHtmlParser.prototype.popNode = function(node) {
// 空白符处理
if (node.pre) {
node.pre = this.pre = void 0;
for (let i = this.STACK.length; i--;)
if (this.STACK[i].pre)
this.pre = true;
}
var siblings = this.siblings(),
len = siblings.length,
childs = node.children;
if (node.name == 'head' || (cfg.filter && cfg.filter(node, this) == false))
return siblings.pop();
var attrs = node.attrs;
// 替换一些标签名
if (cfg.blockTags[node.name]) node.name = 'div';
else if (!cfg.trustTags[node.name]) node.name = 'span';
// 处理列表
if (node.c && (node.name == 'ul' || node.name == 'ol')) {
if ((node.attrs.style || '').includes('list-style:none')) {
for (let i = 0, child; child = childs[i++];)
if (child.name == 'li')
child.name = 'div';
} else if (node.name == 'ul') {
var floor = 1;
for (let i = this.STACK.length; i--;)
if (this.STACK[i].name == 'ul') floor++;
if (floor != 1)
for (let i = childs.length; i--;)
childs[i].floor = floor;
} else {
for (let i = 0, num = 1, child; child = childs[i++];)
if (child.name == 'li') {
child.type = 'ol';
child.num = ((num, type) => {
if (type == 'a') return String.fromCharCode(97 + (num - 1) % 26);
if (type == 'A') return String.fromCharCode(65 + (num - 1) % 26);
if (type == 'i' || type == 'I') {
num = (num - 1) % 99 + 1;
var one = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'],
ten = ['X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC'],
res = (ten[Math.floor(num / 10) - 1] || '') + (one[num % 10 - 1] || '');
if (type == 'i') return res.toLowerCase();
return res;
}
return num;
})(num++, attrs.type) + '.';
}
}
}
// 处理表格
if (node.name == 'table') {
var padding = parseFloat(attrs.cellpadding),
spacing = parseFloat(attrs.cellspacing),
border = parseFloat(attrs.border);
if (node.c) {
if (isNaN(padding)) padding = 2;
if (isNaN(spacing)) spacing = 2;
}
if (border) attrs.style = `border:${border}px solid gray;${attrs.style || ''}`;
if (node.flag && node.c) {
// 有 colspan 或 rowspan 且含有链接的表格转为 grid 布局实现
attrs.style = `${attrs.style || ''};${spacing ? `;grid-gap:${spacing}px` : ';border-left:0;border-top:0'}`;
var row = 1,
col = 1,
colNum,
trs = [],
children = [],
map = {};
(function f(ns) {
for (var i = 0; i < ns.length; i++) {
if (ns[i].name == 'tr') trs.push(ns[i]);
else f(ns[i].children || []);
}
})(node.children)
for (let i = 0; i < trs.length; i++) {
for (let j = 0, td; td = trs[i].children[j]; j++) {
if (td.name == 'td' || td.name == 'th') {
while (map[row + '.' + col]) col++;
var cell = {
name: 'div',
c: 1,
attrs: {
style: (td.attrs.style || '') + (border ? `;border:${border}px solid gray` + (spacing ? '' :
';border-right:0;border-bottom:0') : '') + (padding ? `;padding:${padding}px` : '')
},
children: td.children
}
if (td.attrs.colspan) {
cell.attrs.style += ';grid-column-start:' + col + ';grid-column-end:' + (col + parseInt(td.attrs.colspan));
if (!td.attrs.rowspan) cell.attrs.style += ';grid-row-start:' + row + ';grid-row-end:' + (row + 1);
col += parseInt(td.attrs.colspan) - 1;
}
if (td.attrs.rowspan) {
cell.attrs.style += ';grid-row-start:' + row + ';grid-row-end:' + (row + parseInt(td.attrs.rowspan));
if (!td.attrs.colspan) cell.attrs.style += ';grid-column-start:' + col + ';grid-column-end:' + (col + 1);
for (var k = 1; k < td.attrs.rowspan; k++) map[(row + k) + '.' + col] = 1;
}
children.push(cell);
col++;
}
}
if (!colNum) {
colNum = col - 1;
attrs.style += `;grid-template-columns:repeat(${colNum},auto)`
}
col = 1;
row++;
}
node.children = children;
} else {
attrs.style = `border-spacing:${spacing}px;${attrs.style || ''}`;
if (border || padding)
(function f(ns) {
for (var i = 0, n; n = ns[i]; i++) {
if (n.name == 'th' || n.name == 'td') {
if (border) n.attrs.style = `border:${border}px solid gray;${n.attrs.style || ''}`;
if (padding) n.attrs.style = `padding:${padding}px;${n.attrs.style || ''}`;
} else f(n.children || []);
}
})(childs)
}
if (this.options.autoscroll) {
var table = Object.assign({}, node);
node.name = 'div';
node.attrs = {
style: 'overflow:scroll'
}
node.children = [table];
}
}
this.CssHandler.pop && this.CssHandler.pop(node);
// 自动压缩
if (node.name == 'div' && !Object.keys(attrs).length && childs.length == 1 && childs[0].name == 'div')
siblings[len - 1] = childs[0];
}
// 状态机
MpHtmlParser.prototype.Text = function(c) {
if (c == '<') {
var next = this.data[this.i + 1],
isLetter = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
if (isLetter(next)) {
this.setText();
this.start = this.i + 1;
this.state = this.TagName;
} else if (next == '/') {
this.setText();
if (isLetter(this.data[++this.i + 1])) {
this.start = this.i + 1;
this.state = this.EndTag;
} else this.Comment();
} else if (next == '!' || next == '?') {
this.setText();
this.Comment();
}
}
}
MpHtmlParser.prototype.Comment = function() {
var key;
if (this.data.substring(this.i + 2, this.i + 4) == '--') key = '-->';
else if (this.data.substring(this.i + 2, this.i + 9) == '[CDATA[') key = ']]>';
else key = '>';
if ((this.i = this.data.indexOf(key, this.i + 2)) == -1) this.i = this.data.length;
else this.i += key.length - 1;
this.start = this.i + 1;
this.state = this.Text;
}
MpHtmlParser.prototype.TagName = function(c) {
if (blankChar[c]) {
this.tagName = this.section();
while (blankChar[this.data[this.i]]) this.i++;
if (this.isClose()) this.setNode();
else {
this.start = this.i;
this.state = this.AttrName;
}
} else if (this.isClose()) {
this.tagName = this.section();
this.setNode();
}
}
MpHtmlParser.prototype.AttrName = function(c) {
if (c == '=' || blankChar[c] || this.isClose()) {
this.attrName = this.section();
if (blankChar[c])
while (blankChar[this.data[++this.i]]);
if (this.data[this.i] == '=') {
while (blankChar[this.data[++this.i]]);
this.start = this.i--;
this.state = this.AttrValue;
} else this.setAttr();
}
}
MpHtmlParser.prototype.AttrValue = function(c) {
if (c == '"' || c == "'") {
this.start++;
if ((this.i = this.data.indexOf(c, this.i + 1)) == -1) return this.i = this.data.length;
this.attrVal = this.section();
this.i++;
} else {
for (; !blankChar[this.data[this.i]] && !this.isClose(); this.i++);
this.attrVal = this.section();
}
this.setAttr();
}
MpHtmlParser.prototype.EndTag = function(c) {
if (blankChar[c] || c == '>' || c == '/') {
var name = this.section().toLowerCase();
for (var i = this.STACK.length; i--;)
if (this.STACK[i].name == name) break;
if (i != -1) {
var node;
while ((node = this.STACK.pop()).name != name) this.popNode(node);
this.popNode(node);
} else if (name == 'p' || name == 'br')
this.siblings().push({
name,
attrs: {}
});
this.i = this.data.indexOf('>', this.i);
this.start = this.i + 1;
if (this.i == -1) this.i = this.data.length;
else this.state = this.Text;
}
}
module.exports = MpHtmlParser;

View File

@@ -0,0 +1,80 @@
/* 配置文件 */
var cfg = {
// 出错占位图
errorImg: null,
// 过滤器函数
filter: null,
// 代码高亮函数
highlight: null,
// 文本处理函数
onText: null,
// 实体编码列表
entities: {
quot: '"',
apos: "'",
semi: ';',
nbsp: '\xA0',
ensp: '\u2002',
emsp: '\u2003',
ndash: '',
mdash: '—',
middot: '·',
lsquo: '',
rsquo: '',
ldquo: '“',
rdquo: '”',
bull: '•',
hellip: '…'
},
blankChar: makeMap(' ,\xA0,\t,\r,\n,\f'),
boolAttrs: makeMap('allowfullscreen,autoplay,autostart,controls,ignore,loop,muted'),
// 块级标签,将被转为 div
blockTags: makeMap('address,article,aside,body,caption,center,cite,footer,header,html,nav,pre,section'),
// 将被移除的标签
ignoreTags: makeMap('area,base,canvas,frame,iframe,input,link,map,meta,param,script,source,style,svg,textarea,title,track,wbr'),
// 只能被 rich-text 显示的标签
richOnlyTags: makeMap('a,colgroup,fieldset,legend'),
// 自闭合的标签
selfClosingTags: makeMap('area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr'),
// 信任的标签
trustTags: makeMap('a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video'),
// 默认的标签样式
userAgentStyles: {
address: 'font-style:italic',
big: 'display:inline;font-size:1.2em',
blockquote: 'background-color:#f6f6f6;border-left:3px solid #dbdbdb;color:#6c6c6c;padding:5px 0 5px 10px',
caption: 'display:table-caption;text-align:center',
center: 'text-align:center',
cite: 'font-style:italic',
dd: 'margin-left:40px',
mark: 'background-color:yellow',
pre: 'font-family:monospace;white-space:pre;overflow:scroll',
s: 'text-decoration:line-through',
small: 'display:inline;font-size:0.8em',
u: 'text-decoration:underline'
}
}
function makeMap(str) {
var map = Object.create(null),
list = str.split(',');
for (var i = list.length; i--;)
map[list[i]] = true;
return map;
}
// #ifdef MP-WEIXIN
if (wx.canIUse('editor')) {
cfg.blockTags.pre = void 0;
cfg.ignoreTags.rp = true;
Object.assign(cfg.richOnlyTags, makeMap('bdi,bdo,caption,rt,ruby'));
Object.assign(cfg.trustTags, makeMap('bdi,bdo,caption,pre,rt,ruby'));
}
// #endif
// #ifdef APP-PLUS
cfg.ignoreTags.iframe = void 0;
Object.assign(cfg.trustTags, makeMap('embed,iframe'));
// #endif
module.exports = cfg;

View File

@@ -0,0 +1,22 @@
var inline = {
abbr: 1,
b: 1,
big: 1,
code: 1,
del: 1,
em: 1,
i: 1,
ins: 1,
label: 1,
q: 1,
small: 1,
span: 1,
strong: 1,
sub: 1,
sup: 1
}
module.exports = {
use: function(item) {
return !item.c && !inline[item.name] && (item.attrs.style || '').indexOf('display:inline') == -1
}
}

View File

@@ -0,0 +1,506 @@
<template>
<view :class="'interlayer '+(c||'')" :style="s">
<block v-for="(n, i) in nodes" v-bind:key="i">
<!--图片-->
<view v-if="n.name=='img'" :class="'_img '+n.attrs.class" :style="n.attrs.style" :data-attrs="n.attrs" @tap.stop="imgtap">
<rich-text v-if="ctrl[i]!=0" :nodes="[{attrs:{src:loading&&(ctrl[i]||0)<2?loading:(lazyLoad&&!ctrl[i]?placeholder:(ctrl[i]==3?errorImg:n.attrs.src||'')),alt:n.attrs.alt||'',width:n.attrs.width||'',style:'-webkit-touch-callout:none;max-width:100%;display:block'+(n.attrs.height?';height:'+n.attrs.height:'')},name:'img'}]" />
<image class="_image" :src="lazyLoad&&!ctrl[i]?placeholder:n.attrs.src" :lazy-load="lazyLoad"
:show-menu-by-longpress="!n.attrs.ignore" :data-i="i" :data-index="n.attrs.i" data-source="img" @load="loadImg"
@error="error" />
</view>
<!--文本-->
<text v-else-if="n.type=='text'" decode>{{n.text}}</text>
<!--#ifndef MP-BAIDU-->
<text v-else-if="n.name=='br'">\n</text>
<!--#endif-->
<!--视频-->
<view v-else-if="((n.lazyLoad&&!n.attrs.autoplay)||(n.name=='video'&&!loadVideo))&&ctrl[i]==undefined" :id="n.attrs.id"
:class="'_video '+(n.attrs.class||'')" :style="n.attrs.style" :data-i="i" @tap.stop="_loadVideo" />
<video v-else-if="n.name=='video'" :id="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :autoplay="n.attrs.autoplay||ctrl[i]==0"
:controls="n.attrs.controls" :loop="n.attrs.loop" :muted="n.attrs.muted" :poster="n.attrs.poster" :src="n.attrs.source[ctrl[i]||0]"
:unit-id="n.attrs['unit-id']" :data-id="n.attrs.id" :data-i="i" data-source="video" @error="error" @play="play" />
<!--音频-->
<audio v-else-if="n.name=='audio'" :ref="n.attrs.id" :class="n.attrs.class" :style="n.attrs.style" :author="n.attrs.author"
:autoplay="n.attrs.autoplay" :controls="n.attrs.controls" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster"
:src="n.attrs.source[ctrl[i]||0]" :data-i="i" :data-id="n.attrs.id" data-source="audio" @error.native="error"
@play.native="play" />
<!--链接-->
<view v-else-if="n.name=='a'" :id="n.attrs.id" :class="'_a '+(n.attrs.class||'')" hover-class="_hover" :style="n.attrs.style"
:data-attrs="n.attrs" @tap.stop="linkpress">
<trees class="_span" c="_span" :nodes="n.children" />
</view>
<!--广告-->
<!--<ad v-else-if="n.name=='ad'" :class="n.attrs.class" :style="n.attrs.style" :unit-id="n.attrs['unit-id']" :appid="n.attrs.appid" :apid="n.attrs.apid" :type="n.attrs.type" :adpid="n.attrs.adpid" data-source="ad" @error="error" />-->
<!--列表-->
<view v-else-if="n.name=='li'" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:flex;flex-direction:row'">
<view v-if="n.type=='ol'" class="_ol-bef">{{n.num}}</view>
<view v-else class="_ul-bef">
<view v-if="n.floor%3==0" class="_ul-p1"></view>
<view v-else-if="n.floor%3==2" class="_ul-p2" />
<view v-else class="_ul-p1" style="border-radius:50%"></view>
</view>
<trees class="_li" c="_li" :nodes="n.children" :lazyLoad="lazyLoad" :loading="loading" />
</view>
<!--表格-->
<view v-else-if="n.name=='table'&&n.c&&n.flag" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:grid'">
<trees v-for="(cell,n) in n.children" v-bind:key="n" :class="cell.attrs.class" :c="cell.attrs.class" :style="cell.attrs.style"
:s="cell.attrs.style" :nodes="cell.children" />
</view>
<view v-else-if="n.name=='table'&&n.c" :id="n.attrs.id" :class="n.attrs.class" :style="(n.attrs.style||'')+';display:table'">
<view v-for="(tbody, o) in n.children" v-bind:key="o" :class="tbody.attrs.class" :style="(tbody.attrs.style||'')+(tbody.name[0]=='t'?';display:table-'+(tbody.name=='tr'?'row':'row-group'):'')">
<view v-for="(tr, p) in tbody.children" v-bind:key="p" :class="tr.attrs.class" :style="(tr.attrs.style||'')+(tr.name[0]=='t'?';display:table-'+(tr.name=='tr'?'row':'cell'):'')">
<trees v-if="tr.name=='td'" :nodes="tr.children" />
<trees v-else v-for="(td, q) in tr.children" v-bind:key="q" :class="td.attrs.class" :c="td.attrs.class" :style="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')"
:s="(td.attrs.style||'')+(td.name[0]=='t'?';display:table-'+(td.name=='tr'?'row':'cell'):'')" :nodes="td.children" />
</view>
</view>
</view>
<!--#ifdef APP-PLUS-->
<iframe v-else-if="n.name=='iframe'" :style="n.attrs.style" :allowfullscreen="n.attrs.allowfullscreen" :frameborder="n.attrs.frameborder"
:width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
<embed v-else-if="n.name=='embed'" :style="n.attrs.style" :width="n.attrs.width" :height="n.attrs.height" :src="n.attrs.src" />
<!--#endif-->
<!--富文本-->
<!--#ifdef MP-WEIXIN || MP-QQ || APP-PLUS-->
<rich-text v-else-if="handler.use(n)" :id="n.attrs.id" :class="'_p __'+n.name" :nodes="[n]" />
<!--#endif-->
<!--#ifndef MP-WEIXIN || MP-QQ || APP-PLUS-->
<rich-text v-else-if="!n.c" :id="n.attrs.id" :nodes="[n]" style="display:inline" />
<!--#endif-->
<trees v-else :class="(n.attrs.id||'')+' _'+n.name+' '+(n.attrs.class||'')" :c="(n.attrs.id||'')+' _'+n.name+' '+(n.attrs.class||'')"
:style="n.attrs.style" :s="n.attrs.style" :nodes="n.children" :lazyLoad="lazyLoad" :loading="loading" />
</block>
</view>
</template>
<script module="handler" lang="wxs" src="./handler.wxs"></script>
<script>
global.Parser = {};
import trees from './trees'
const errorImg = require('../libs/config.js').errorImg;
export default {
components: {
trees
},
name: 'trees',
data() {
return {
ctrl: [],
placeholder: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="300" height="225"/>',
errorImg,
loadVideo: typeof plus == 'undefined',
// #ifndef MP-ALIPAY
c: '',
s: ''
// #endif
}
},
props: {
nodes: Array,
lazyLoad: Boolean,
loading: String,
// #ifdef MP-ALIPAY
c: String,
s: String
// #endif
},
mounted() {
for (this.top = this.$parent; this.top.$options.name != 'parser'; this.top = this.top.$parent);
this.init();
},
// #ifdef APP-PLUS
beforeDestroy() {
this.observer && this.observer.disconnect();
},
// #endif
methods: {
init() {
for (var i = this.nodes.length, n; n = this.nodes[--i];) {
if (n.name == 'img') {
this.top.imgList.setItem(n.attrs.i, n.attrs['original-src'] || n.attrs.src);
// #ifdef APP-PLUS
if (this.lazyLoad && !this.observer) {
this.observer = uni.createIntersectionObserver(this).relativeToViewport({
top: 500,
bottom: 500
});
setTimeout(() => {
this.observer.observe('._img', res => {
if (res.intersectionRatio) {
for (var j = this.nodes.length; j--;)
if (this.nodes[j].name == 'img')
this.$set(this.ctrl, j, 1);
this.observer.disconnect();
}
})
}, 0)
}
// #endif
} else if (n.name == 'video' || n.name == 'audio') {
var ctx;
if (n.name == 'video') {
ctx = uni.createVideoContext(n.attrs.id
// #ifndef MP-BAIDU
, this
// #endif
);
} else if (this.$refs[n.attrs.id])
ctx = this.$refs[n.attrs.id][0];
if (ctx) {
ctx.id = n.attrs.id;
this.top.videoContexts.push(ctx);
}
}
}
// #ifdef APP-PLUS
// APP 上避免 video 错位需要延时渲染
setTimeout(() => {
this.loadVideo = true;
}, 1000)
// #endif
},
play(e) {
var contexts = this.top.videoContexts;
if (contexts.length > 1 && this.top.autopause)
for (var i = contexts.length; i--;)
if (contexts[i].id != e.currentTarget.dataset.id)
contexts[i].pause();
},
imgtap(e) {
var attrs = e.currentTarget.dataset.attrs;
if (!attrs.ignore) {
var preview = true,
data = {
id: e.target.id,
src: attrs.src,
ignore: () => preview = false
};
global.Parser.onImgtap && global.Parser.onImgtap(data);
this.top.$emit('imgtap', data);
if (preview) {
var urls = this.top.imgList,
current = urls[attrs.i] ? parseInt(attrs.i) : (urls = [attrs.src], 0);
uni.previewImage({
current,
urls
})
}
}
},
loadImg(e) {
var i = e.currentTarget.dataset.i;
if (this.lazyLoad && !this.ctrl[i]) {
// #ifdef QUICKAPP-WEBVIEW
this.$set(this.ctrl, i, 0);
this.$nextTick(function() {
// #endif
// #ifndef APP-PLUS
this.$set(this.ctrl, i, 1);
// #endif
// #ifdef QUICKAPP-WEBVIEW
})
// #endif
} else if (this.loading && this.ctrl[i] != 2) {
// #ifdef QUICKAPP-WEBVIEW
this.$set(this.ctrl, i, 0);
this.$nextTick(function() {
// #endif
this.$set(this.ctrl, i, 2);
// #ifdef QUICKAPP-WEBVIEW
})
// #endif
}
},
linkpress(e) {
var jump = true,
attrs = e.currentTarget.dataset.attrs;
attrs.ignore = () => jump = false;
global.Parser.onLinkpress && global.Parser.onLinkpress(attrs);
this.top.$emit('linkpress', attrs);
if (jump) {
// #ifdef MP
if (attrs['app-id']) {
return uni.navigateToMiniProgram({
appId: attrs['app-id'],
path: attrs.path
})
}
// #endif
if (attrs.href) {
if (attrs.href[0] == '#') {
if (this.top.useAnchor)
this.top.navigateTo({
id: attrs.href.substring(1)
})
} else if (attrs.href.indexOf('http') == 0 || attrs.href.indexOf('//') == 0) {
// #ifdef APP-PLUS
plus.runtime.openWeb(attrs.href);
// #endif
// #ifndef APP-PLUS
uni.setClipboardData({
data: attrs.href,
success: () =>
uni.showToast({
title: '链接已复制'
})
})
// #endif
} else
uni.navigateTo({
url: attrs.href,
fail() {
uni.switchTab({
url: attrs.href,
})
}
})
}
}
},
error(e) {
var target = e.currentTarget,
source = target.dataset.source,
i = target.dataset.i;
if (source == 'video' || source == 'audio') {
// 加载其他 source
var index = this.ctrl[i] ? this.ctrl[i].i + 1 : 1;
if (index < this.nodes[i].attrs.source.length)
this.$set(this.ctrl, i, index);
if (e.detail.__args__)
e.detail = e.detail.__args__[0];
} else if (errorImg && source == 'img') {
this.top.imgList.setItem(target.dataset.index, errorImg);
this.$set(this.ctrl, i, 3);
}
this.top && this.top.$emit('error', {
source,
target,
errMsg: e.detail.errMsg
});
},
_loadVideo(e) {
this.$set(this.ctrl, e.target.dataset.i, 0);
}
}
}
</script>
<style>
/* 在这里引入自定义样式 */
/* 链接和图片效果 */
._a {
display: inline;
padding: 1.5px 0 1.5px 0;
color: #366092;
word-break: break-all;
}
._hover {
text-decoration: underline;
opacity: 0.7;
}
._img {
/* display: inline-block; */
display: block;
max-width: 100%;
overflow: hidden;
}
/* #ifdef MP-WEIXIN */
:host {
display: inline;
}
/* #endif */
/* #ifndef MP-ALIPAY || APP-PLUS */
.interlayer {
display: inherit;
flex-direction: inherit;
flex-wrap: inherit;
align-content: inherit;
align-items: inherit;
justify-content: inherit;
width: 100%;
white-space: inherit;
}
/* #endif */
._b,
._strong {
font-weight: bold;
}
/* #ifndef MP-ALIPAY */
._blockquote,
._div,
._p,
._ol,
._ul,
._li {
display: block;
}
/* #endif */
._code {
font-family: monospace;
}
._del {
text-decoration: line-through;
}
._em,
._i {
font-style: italic;
}
._h1 {
font-size: 2em;
}
._h2 {
font-size: 1.5em;
}
._h3 {
font-size: 1.17em;
}
._h5 {
font-size: 0.83em;
}
._h6 {
font-size: 0.67em;
}
._h1,
._h2,
._h3,
._h4,
._h5,
._h6 {
display: block;
font-weight: bold;
}
._image {
display: block;
width: 100%;
height: 360px;
margin-top: -360px;
opacity: 0;
}
._ins {
text-decoration: underline;
}
._li {
flex: 1;
width: 0;
}
._ol-bef {
width: 36px;
margin-right: 5px;
text-align: right;
}
._ul-bef {
display: block;
margin: 0 12px 0 23px;
line-height: normal;
}
._ol-bef,
._ul-bef {
flex: none;
user-select: none;
}
._ul-p1 {
display: inline-block;
width: 0.3em;
height: 0.3em;
overflow: hidden;
line-height: 0.3em;
}
._ul-p2 {
display: inline-block;
width: 0.23em;
height: 0.23em;
border: 0.05em solid black;
border-radius: 50%;
}
._q::before {
content: '"';
}
._q::after {
content: '"';
}
._sub {
font-size: smaller;
vertical-align: sub;
}
._sup {
font-size: smaller;
vertical-align: super;
}
/* #ifdef MP-ALIPAY || APP-PLUS || QUICKAPP-WEBVIEW */
._abbr,
._b,
._code,
._del,
._em,
._i,
._ins,
._label,
._q,
._span,
._strong,
._sub,
._sup {
display: inline;
}
/* #endif */
/* #ifdef MP-WEIXIN || MP-QQ */
.__bdo,
.__bdi,
.__ruby,
.__rt {
display: inline-block;
}
/* #endif */
._video {
position: relative;
display: inline-block;
width: 300px;
height: 225px;
background-color: black;
}
._video::after {
position: absolute;
top: 50%;
left: 50%;
margin: -15px 0 0 -15px;
content: '';
border-color: transparent transparent transparent white;
border-style: solid;
border-width: 15px 0 15px 30px;
}
</style>

View File

@@ -0,0 +1,17 @@
import { VantComponent } from '../common/component';
VantComponent({
props: {
size: {
type: String,
value: '30px'
},
type: {
type: String,
value: 'circular'
},
color: {
type: String,
value: '#c9c9c9'
}
}
});

View File

@@ -0,0 +1,3 @@
{
"component": true
}

View File

@@ -0,0 +1,16 @@
<view
class="van-loading custom-class"
style="width: {{ size }}; height: {{ size }}"
>
<view
class="van-loading__spinner van-loading__spinner--{{ type }}"
style="color: {{ color }};"
>
<view
wx:if="{{ type === 'spinner' }}"
wx:for="item in 12"
wx:key="index"
class="van-loading__dot"
/>
</view>
</view>

Some files were not shown because too many files have changed in this diff Show More