This commit is contained in:
hengtou 2024-12-02 17:22:50 +08:00
commit a3d1d75a54
319 changed files with 103010 additions and 0 deletions

18
.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
.DS_Store
node_modules/
dist/
unpackage/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
tests/**/coverage/
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln

21
.hbuilderx/launch.json Normal file
View File

@ -0,0 +1,21 @@
{
"version" : "1.0",
"configurations" : [
{
"default" :
{
"launchtype" : "local"
},
"h5" :
{
"launchtype" : "local"
},
"mp-weixin" :
{
"launchtype" : "local"
},
"provider" : "aliyun",
"type" : "uniCloud"
}
]
}

711
API/index.js Normal file
View File

@ -0,0 +1,711 @@
import {
httpUrlFormat,
ajaxCarryAuthorization,ajaxSync
} from '@/utils/httpUtils.js'
import { ApiVersion,AppVersion } from '@/config/baseConfig.js'
const headerVersion = {
'API-Version': ApiVersion(),
'APP-Version':AppVersion()
}
const _flag = 'apiBaseUrl' // 请求的api空间
const apiUrl = {
'boxesPinnedApi': httpUrlFormat('/boxes/pinned', _flag),
'boxesTagsApi': httpUrlFormat('/boxes/tags', _flag),
'boxesApi': httpUrlFormat('/boxes', _flag),
'boxRecordApi': httpUrlFormat('/box/record', _flag),
'boxOrderApi': httpUrlFormat('/box/order', _flag),
'boxOrderReviewApi': httpUrlFormat('/box/order/review', _flag),
'boxOrderPaidApi': httpUrlFormat('/box/order/paid', _flag),
'orderPaidApi': httpUrlFormat('/order/paid', _flag),
'boxRecordResultApi': httpUrlFormat('/box/record/result', _flag),
'boxDeliveryPreviewApi': httpUrlFormat('/order/box/delivery/preview', _flag),
'boxDeliveryApi': httpUrlFormat('/order/box/delivery', _flag),
'boxActivityApi': httpUrlFormat('/boxes/activity', _flag),//活动盲盒
'boxActivityListApi': httpUrlFormat('/boxes/activity/list', _flag),//活动盲盒
'promptCountApi': httpUrlFormat('/user/prompt/count', _flag),//用户提示卡数量
'usePromptApi': httpUrlFormat('/box/order/use/prompt', _flag),//使用提示卡
'activityDetailApi': httpUrlFormat('/boxes/activity/detail', _flag),//
'activityDetailSourceApi': httpUrlFormat('/boxes/activity/detail/source', _flag),//
'activityBuyApi': httpUrlFormat('/coin/gear/activity/box', _flag),//活动盲盒购买
'activitySourceBuyApi': httpUrlFormat('/box/order/activity/source', _flag),//活动盲盒购买
'activityReviewApi': httpUrlFormat('/box/order/activity/review', _flag),//活动盲盒订单预览
'activityReviewSourceApi': httpUrlFormat('/box/order/activity/review/source', _flag),//活动盲盒订单预览-新
'activityOrderApi': httpUrlFormat('/box/order/activity', _flag),
'recoveryBatchApi': httpUrlFormat('/box/record/recovery/batch', _flag),
'deliveryReviewBatchApi': httpUrlFormat('/order/box/delivery/preview/batch', _flag),
'deliveryBatchApi': httpUrlFormat('/order/box/delivery/batch', _flag),
'promptExchangeBatchApi': httpUrlFormat('/user/prompt/exchange/batch', _flag),
'boxResultApi': httpUrlFormat('/misc/box/result', _flag),
'noOpenListApi': httpUrlFormat('/box/record/no/open/list', _flag),//未开奖记录
'openListApi': httpUrlFormat('/box/record/open', _flag),//开奖记录
'boxesChannelPinnedApi': httpUrlFormat('/boxes/channel/pinned', _flag),//首页盲盒-渠道
'orderReceive': httpUrlFormat('/order/receive', _flag),
'orderList': httpUrlFormat('/order/mall_order', _flag),
'boxOrder': httpUrlFormat('/order/box_order', _flag),
'boxRecord': httpUrlFormat('/order/box_record', _flag),
'boxOpen': httpUrlFormat('/order/box_open', _flag),
'boxPickup': httpUrlFormat('/order/box_pickup', _flag),
'boxRecycle': httpUrlFormat('/order/box_recycle', _flag),
'boxInfo': httpUrlFormat('/order/box_info', _flag),
'orderCancel': httpUrlFormat('/order/cancel', _flag),
'boxOpenByBoxId': httpUrlFormat('/order/box_open_by_box_id', _flag),
'popupAdPageApi': httpUrlFormat('/misc/popup/ad/page', _flag),
'popupAdPageChannelApi': httpUrlFormat('/misc/popup/ad/page/channel', _flag),
'activityIconApi': httpUrlFormat('/activity/icon', _flag),
'activityDetailNewApi': httpUrlFormat('/activity/detail', _flag),
'pointClearApi': httpUrlFormat('/user/point/clear', _flag),
'activityTaskAwardApi': httpUrlFormat('/activity/task/award', _flag),
'activityTaskCreateApi': httpUrlFormat('/box/order/activity/create', _flag),
'freeBoxApi': httpUrlFormat('/boxes/free/box', _flag),
'activityGuideApi': httpUrlFormat('/activity/guide', _flag),
'indexGuideApi': httpUrlFormat('/user/index/guide', _flag),
'activityTopLastApi': httpUrlFormat('/activity/top/last', _flag),
}
export function boxRecoveryApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: httpUrlFormat(`/box/record/recovery/${data.id}`, _flag),
method: 'PUT',
data
})
}
//分页盲盒
export function boxesApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxesApi,
method: 'GET',
data: data
})
}
//首页盲盒
export function boxesPinnedApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.boxesPinnedApi,
method: 'GET',
data: data
})
}
//盲盒标签
export function boxesTagsApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxesTagsApi,
method: 'GET',
data: data
})
}
//盲盒开奖记录
export function boxRecordApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxRecordApi,
method: 'GET',
data: data
})
}
// 盲盒详情
export function boxesDetail(data,header = {}) {
const headerObj = {...headerVersion,...header}
// header['API-Version'] = ApiVersion()
return ajaxCarryAuthorization({
header:headerObj,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: httpUrlFormat(`/boxes/${data.id}`, _flag),
method: 'GET',
data
})
}
// 创建盲盒订单
export function boxOrderApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxOrderApi,
method: 'POST',
data
})
}
export function boxOrderListApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxOrderApi,
method: 'GET',
data
})
}
// 活动盲盒
export function boxActivityApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxActivityApi,
method: 'GET',
data
})
}
// 活动盲盒
export function boxActivityListApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxActivityListApi,
method: 'GET',
data
})
}
// 创建盲盒订单
export function boxOrderReviewApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxOrderReviewApi,
method: 'GET',
data
})
}
// 创建盲盒订单
export function boxOrderPaidApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxOrderPaidApi,
method: 'GET',
data
})
}
export function orderPaidApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.orderPaidApi,
method: 'GET',
data
})
}
// 盲盒开奖结果
export function boxRecordResultApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxRecordResultApi,
method: 'GET',
data
})
}
export function boxDeliveryPreviewApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header:headerObj,
// header: {'Content-Type': 'application/x-www-form-urlencoded','API-Version': ApiVersion()},
// url: apiUrl.cart_item,
url: apiUrl.boxDeliveryPreviewApi,
// url: httpUrlFormat(`/order/box/delivery/preview?boxRecordId=${data.boxRecordId}&quantity=${data.quantity}`, _flag),
method: 'POST',
data: data
})
}
export function boxDeliveryApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// url: apiUrl.cart_item,
url: apiUrl.boxDeliveryApi,
method: 'POST',
data: data
})
}
export function boxesChannelPinnedApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxesChannelPinnedApi,
method: 'GET',
data
})
}
//商城订单列表
export function orderList(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.order,
method: 'GET',
data: data
})
}
// 删除订单
export function delOrder(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
// url: apiUrl.cart_item,
url: apiUrl.order,
method: 'DELETE',
data: data
})
}
// 取消订单
export function orderCancel(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
// url: apiUrl.cart_item,
url: apiUrl.orderCancel,
method: 'PUT',
data: data
})
}
// 确认收货
export function orderReceive(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
// url: apiUrl.cart_item,
url: apiUrl.orderReceive,
method: 'POST',
data: data
})
}
//创建商城订单
export function createOrder(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.orderList,
method: 'POST',
data: data
})
}
//订单预览
export function orderPreview(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.orderPreview,
method: 'POST',
data: data
})
}
//盲盒订单列表
export function boxOrderList(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxOrder,
method: 'GET',
data: data
})
}
//创建盲盒订单
export function boxCreateOrder(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxOrder,
method: 'POST',
data: data
})
}
//礼盒/物品列表
export function boxRecord(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxRecord,
method: 'GET',
data: data
})
}
//开盲盒
export function boxOpen(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
header: headerObj,
url: apiUrl.boxOpen,
method: 'POST',
data: data
})
}
//提取盲盒
export function boxPickup(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxPickup,
method: 'POST',
data: data
})
}
//回收盲盒
export function boxRecycle(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.boxRecycle,
method: 'POST',
data: data
})
}
// 订单详情
export function orderDetail(no,data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: httpUrlFormat(`/order/${no}`, _flag),
method: 'GET',
data: data
})
}
//盲盒详情
export function boxInfo(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.boxInfo,
method: 'GET',
data: data
})
}
// 根据盲盒id开盒
export function boxOpenByBoxId(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.boxOpenByBoxId,
method: 'POST',
data: data
})
}
// 用户提示卡数量
export function promptCountApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.promptCountApi,
method: 'GET',
data: data
})
}
// 使用提示卡
export function usePromptApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.usePromptApi,
method: 'POST',
data: data
})
}
export function activityDetailApi(data,header = {}) {
// header['API-Version'] = ApiVersion()
const headerObj = {...headerVersion,...header}
return ajaxCarryAuthorization({
header:headerObj,
url: apiUrl.activityDetailApi,
method: 'GET',
data: data
})
}
export function activityDetailSourceApi(data,header = {}) {
// header['API-Version'] = ApiVersion()
const headerObj = {...headerVersion,...header}
return ajaxCarryAuthorization({
header:headerObj,
url: apiUrl.activityDetailSourceApi,
method: 'GET',
data: data
})
}
export function activityBuyApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.activityBuyApi,
method: 'POST',
data: data
})
}
export function activitySourceBuyApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.activitySourceBuyApi,
method: 'POST',
data: data
})
}
export function activityReviewApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.activityReviewApi,
method: 'GET',
data: data
})
}
export function activityReviewSourceApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.activityReviewSourceApi,
method: 'GET',
data: data
})
}
export function activityOrderApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.activityOrderApi,
method: 'POST',
data: data
})
}
export function recoveryBatchApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.recoveryBatchApi,
method: 'PUT',
data: data
})
}
export function deliveryReviewBatchApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.deliveryReviewBatchApi,
method: 'POST',
data: data
})
}
export function deliveryBatchApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.deliveryBatchApi,
method: 'POST',
data: data
})
}
export function promptExchangeBatchApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.promptExchangeBatchApi,
method: 'POST',
data: data
})
}
export function boxResultApi(data,header = {}) {
const headerObj = {...headerVersion,...header}
return ajaxCarryAuthorization({
header:headerObj,
url: apiUrl.boxResultApi,
method: 'GET',
data: data
})
}
export function noOpenListApi(data,header = {}) {
const headerObj = {...headerVersion,...header}
// header['API-Version'] = ApiVersion()
return ajaxCarryAuthorization({
header:headerObj,
url: apiUrl.noOpenListApi,
method: 'GET',
data: data
})
}
// export function openListApi(data,header = {}) {
// // header['API-Version'] = ApiVersion()
// const headerObj = {...headerVersion,...header}
// return ajaxCarryAuthorization({
// header:headerObj,
// url: apiUrl.openListApi,
// method: 'GET',
// data: data
// })
// }
export function openListApi(data,header = {}) {
// header['API-Version'] = ApiVersion()
const headerObj = {...headerVersion,...header}
return ajaxCarryAuthorization({
header:headerObj,
url: httpUrlFormat(`/box/record/open?ids=${data}`, _flag),
method: 'GET',
data: data
})
}
export function popupAdPageApi(data) {
return ajaxCarryAuthorization({
header:headerVersion,
url: apiUrl.popupAdPageApi,
method: 'GET',
data: data
})
}
export function popupAdPageChannelApi(data) {
return ajaxCarryAuthorization({
header:headerVersion,
url: apiUrl.popupAdPageChannelApi,
method: 'GET',
data: data
})
}
export function activityIconApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.activityIconApi,
method: 'POST',
data: data
})
}
export function activityDetailNewApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.activityDetailNewApi,
method: 'POST',
data: data
})
}
export function pointClearApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.pointClearApi,
method: 'POST',
data: data
})
}
export function activityTaskAwardApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.activityTaskAwardApi,
method: 'POST',
data: data
})
}
export function activityTaskCreateApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.activityTaskCreateApi,
method: 'POST',
data: data
})
}
// 免费开盲盒
export function freeBoxApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.freeBoxApi,
method: 'GET',
data
})
}
export function activityGuideApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.activityGuideApi,
method: 'GET',
data: data
})
}
export function setActivityGuideApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.activityGuideApi,
method: 'POST',
data: data
})
}
export function indexGuideApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.indexGuideApi,
method: 'GET',
data: data
})
}
export function setIndexGuideApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.indexGuideApi,
method: 'POST',
data: data
})
}
export function activityTopLastApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.activityTopLastApi,
method: 'GET',
data: data
})
}

29
API/indexApi.js Normal file
View File

@ -0,0 +1,29 @@
import {
httpUrlFormat,
ajaxCarryAuthorization,ajaxSync
} from '@/utils/httpUtils.js'
import { ApiVersion,AppVersion } from '@/config/baseConfig.js'
const headerVersion = {
'API-Version': ApiVersion(),
'APP-Version':AppVersion()
}
const _flag = 'apiBaseUrl' // 请求的api空间
const apiUrl = {
'index': httpUrlFormat('/index', _flag),
}
export function index(data) {
return ajaxSync({
header: headerVersion,
url: apiUrl.index,
method: 'GET',
data: data
})
}

148
API/login.js Normal file
View File

@ -0,0 +1,148 @@
import {
httpUrlFormat,
ajaxCarryAuthorization,ajaxSync
} from '@/utils/httpUtils.js'
import { ApiVersion,AppVersion } from '@/config/baseConfig.js'
const headerVersion = {
'API-Version': ApiVersion(),
'APP-Version':AppVersion()
}
const _flag = 'apiBaseUrl' // 请求的api空间
const apiUrl = {
'generateCode': httpUrlFormat('/generate/code', _flag),//发送短信验证码
'sms_check': httpUrlFormat('/sms_check', _flag),//验证短信验证码
'register': httpUrlFormat('/register', _flag),//注册
'login': httpUrlFormat('/login', _flag),//登录
'loginCode': httpUrlFormat('/login/code', _flag),//验证码登录
'wechatAppid': httpUrlFormat('/wechat/appid', _flag),//微信获取appid
'loginWechat': httpUrlFormat('/login/wechat', _flag),//微信登录
'phoneWechat': httpUrlFormat('/wechat/user/phone', _flag),//微信登录
'reset_password': httpUrlFormat('/reset_password', _flag),//重置密码
'bindPhoneApi': httpUrlFormat('/user/bind/phone', _flag),//绑定手机号
'loginXc': httpUrlFormat('/login/xiaochuan', _flag),//用户登录第三方小川
'agreementDetailApi': httpUrlFormat('/misc/user/agreement/detail', _flag),//用户协议
}
export function loginXc(data) {
return ajaxSync({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.loginXc,
method: 'POST',
data: data
})
}
export function smsSend(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxSync({
header: headerObj,
url: apiUrl.generateCode,
method: 'POST',
data: data
})
}
export function smsCheck(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxSync({
header: headerObj,
url: apiUrl.sms_check,
method: 'POST',
data: data
})
}
export function register(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxSync({
header: headerObj,
url: apiUrl.register,
method: 'POST',
data: data
})
}
export function loginCode(data) {
return ajaxSync({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.loginCode,
method: 'POST',
data: data
})
}
export function loginWechat(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxSync({
header: headerObj,
url: apiUrl.loginWechat,
method: 'POST',
data: data
})
}
export function loginApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxSync({
header: headerObj,
url: apiUrl.login,
method: 'POST',
data: data
})
}
export function resetPassword(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxSync({
header: headerObj,
url: apiUrl.reset_password,
method: 'POST',
data: data
})
}
export function wechatAppid(data) {
return ajaxSync({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.wechatAppid,
method: 'GET',
data: data
})
}
export function phoneWechat(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxSync({
header: headerObj,
url: apiUrl.phoneWechat,
method: 'POST',
data: data
})
}
export function bindPhoneApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.bindPhoneApi,
method: 'POST',
data: data
})
}
export function agreementDetailApi(data) {
return ajaxSync({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.agreementDetailApi,
method: 'GET',
data: data
})
}

288
API/order.js Normal file
View File

@ -0,0 +1,288 @@
import {
httpUrlFormat,
ajaxCarryAuthorization,ajaxSync
} from '@/utils/httpUtils.js'
import { ApiVersion,AppVersion } from '@/config/baseConfig.js'
const headerVersion = {
'API-Version': ApiVersion(),
'APP-Version':AppVersion()
}
const _flag = 'apiBaseUrl' // 请求的api空间
const apiUrl = {
'order': httpUrlFormat('/order', _flag),
'orderPreview': httpUrlFormat('/order/preview', _flag),
'orderReceive': httpUrlFormat('/order/receive', _flag),
'orderList': httpUrlFormat('/order/mall_order', _flag),
'boxOrder': httpUrlFormat('/order/box_order', _flag),
'boxRecord': httpUrlFormat('/order/box_record', _flag),
'boxOpen': httpUrlFormat('/order/box_open', _flag),
'boxPickup': httpUrlFormat('/order/box_pickup', _flag),
'boxRecycle': httpUrlFormat('/order/box_recycle', _flag),
'boxInfo': httpUrlFormat('/order/box_info', _flag),
'orderCancel': httpUrlFormat('/order/cancel', _flag),
'boxOpenByBoxId': httpUrlFormat('/order/box_open_by_box_id', _flag),
'orderRefundApi': httpUrlFormat('/order/refund', _flag),
'channelGetApi': httpUrlFormat('/pay/channel/get', _flag),
'orderReceiptApi': httpUrlFormat('/order/receipt', _flag),
'refundTimeApi': httpUrlFormat('/misc/mall/refund/expire/time', _flag),//商品退款-过期时间
'boxRefundTimeApi': httpUrlFormat('/order/box/record/expire/time', _flag),//盲盒退款-过期时间
'boxRefundTimeExpiredApi': httpUrlFormat('/order/box/record/expired', _flag),//盲盒退款-过期时间
}
//订单预览
export function orderPreview(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
console.log(headerObj)
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.orderPreview,
method: 'POST',
data: data
})
}
export function channelGetApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.channelGetApi,
method: 'GET',
data: data
})
}
export function createShopOrder(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.order,
method: 'POST',
data: data
})
}
//商城订单列表
export function orderList(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.order,
method: 'GET',
data: data
})
}
// 删除订单
export function delOrder(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
// url: apiUrl.cart_item,
url: apiUrl.order,
method: 'DELETE',
data: data
})
}
// 取消订单
export function orderCancel(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
// url: apiUrl.cart_item,
url: apiUrl.orderCancel,
method: 'PUT',
data: data
})
}
// 确认收货
export function orderReceive(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
// url: apiUrl.cart_item,
url: apiUrl.orderReceive,
method: 'POST',
data: data
})
}
//创建商城订单
export function createOrder(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.orderList,
method: 'POST',
data: data
})
}
//盲盒订单列表
export function boxOrderList(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxOrder,
method: 'GET',
data: data
})
}
//创建盲盒订单
export function boxCreateOrder(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxOrder,
method: 'POST',
data: data
})
}
//礼盒/物品列表
export function boxRecord(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxRecord,
method: 'GET',
data: data
})
}
//开盲盒
export function boxOpen(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxOpen,
method: 'POST',
data: data
})
}
//提取盲盒
export function boxPickup(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxPickup,
method: 'POST',
data: data
})
}
//回收盲盒
export function boxRecycle(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxRecycle,
method: 'POST',
data: data
})
}
// 订单详情
export function orderDetail(no,data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: httpUrlFormat(`/order/no/${no}`, _flag),
method: 'GET',
data: data
})
}
export function orderIdDetail(id) {
return ajaxCarryAuthorization({
header: headerVersion,
url: httpUrlFormat(`/box/order/${id}`, _flag),
method: 'GET',
})
}
export function orderDetailId(id) {
return ajaxCarryAuthorization({
header: headerVersion,
url: httpUrlFormat(`/order/${id}`, _flag),
method: 'GET',
})
}
//盲盒详情
export function boxInfo(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.boxInfo,
method: 'GET',
data: data
})
}
// 根据盲盒id开盒
export function boxOpenByBoxId(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxOpenByBoxId,
method: 'POST',
data: data
})
}
export function orderReceiptApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
// url: apiUrl.cart_item,
url: apiUrl.orderReceiptApi,
method: 'PUT',
data: data
})
}
export function orderRefundApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
// url: apiUrl.cart_item,
url: apiUrl.orderRefundApi,
method: 'PUT',
data: data
})
}
// 商品退款过期时间
export function refundTimeApi(data) {
return ajaxCarryAuthorization({
url: apiUrl.refundTimeApi,
method: 'GET',
data: data
})
}
// 盲盒退款过期时间
export function boxRefundTimeApi(data) {
return ajaxCarryAuthorization({
url: apiUrl.boxRefundTimeApi,
method: 'GET',
data: data
})
}
export function boxRefundTimeExpiredApi(data) {
return ajaxCarryAuthorization({
url: apiUrl.boxRefundTimeExpiredApi,
method: 'GET',
data: data
})
}

65
API/pay.js Normal file
View File

@ -0,0 +1,65 @@
import {
httpUrlFormat,
ajaxCarryAuthorization,ajaxSync
} from '@/utils/httpUtils.js'
import { ApiVersion,AppVersion } from '@/config/baseConfig.js'
const headerVersion = {
'API-Version': ApiVersion(),
'APP-Version':AppVersion()
}
const _flag = 'apiBaseUrl' // 请求的api空间
const apiUrl = {
'createOrderPay': httpUrlFormat('/pay/create_order', _flag),
'orderPaid': httpUrlFormat('/order/box_order_paid', _flag),
'orderShopPaid': httpUrlFormat('/order/order_paid', _flag),
'mpScheme': httpUrlFormat('/other/wechat/mp/scheme', _flag),
}
export function createOrderPay(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.createOrderPay,
method: 'POST',
data: data
})
}
// 查询盲盒订单是否成功
export function orderPaid(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.orderPaid,
method: 'GET',
data: data
})
}
// 查询商品订单是否成功
export function orderShopPaid(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.orderShopPaid,
method: 'GET',
data: data
})
}
// https://test.06zk.com/test/pay/create_order
export function mpScheme(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.mpScheme,
method: 'GET',
data
})
}

261
API/product.js Normal file
View File

@ -0,0 +1,261 @@
import {
httpUrlFormat,
ajaxCarryAuthorization,ajaxSync
} from '@/utils/httpUtils.js'
import { ApiVersion,AppVersion } from '@/config/baseConfig.js'
const headerVersion = {
'API-Version': ApiVersion(),
'APP-Version':AppVersion()
}
const _flag = 'apiBaseUrl' // 请求的api空间
const apiUrl = {
'productApi': httpUrlFormat('/product', _flag),
'productTagsApi': httpUrlFormat('/product/tags', _flag),
'boxesTagsApi': httpUrlFormat('/boxes/tags', _flag),
'boxesApi': httpUrlFormat('/boxes', _flag),
'favorItemApi': httpUrlFormat('/user/favor/item', _flag),
'favorItemProductApi': httpUrlFormat('/user/favor/item/product', _flag),
'orderPreview': httpUrlFormat('/order/preview', _flag),
'orderReceive': httpUrlFormat('/order/receive', _flag),
'orderList': httpUrlFormat('/order/mall_order', _flag),
'boxOrder': httpUrlFormat('/order/box_order', _flag),
'boxRecord': httpUrlFormat('/order/box_record', _flag),
'boxOpen': httpUrlFormat('/order/box_open', _flag),
'boxPickup': httpUrlFormat('/order/box_pickup', _flag),
'boxRecycle': httpUrlFormat('/order/box_recycle', _flag),
'boxInfo': httpUrlFormat('/order/box_info', _flag),
'orderCancel': httpUrlFormat('/order/cancel', _flag),
'boxOpenByBoxId': httpUrlFormat('/order/box_open_by_box_id', _flag),
}
//商品标签
export function productTagsApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.productTagsApi,
method: 'GET',
data: data
})
}
//商品分页
export function productApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.productApi,
method: 'GET',
data: data
})
}
//盲盒标签
export function boxesTagsApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxesTagsApi,
method: 'GET',
data: data
})
}
export function productDetailApi(data,header = {}) {
const headerObj = {...header,...headerVersion}
return ajaxCarryAuthorization({
header:headerObj,
url: httpUrlFormat(`/product/${data.id}`, _flag),
method: 'GET',
data: data
})
}
// 收藏
export function favorItemApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.favorItemApi,
method: 'POST',
data: data
})
}
// 删除收藏
export function delFavorItemApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.favorItemProductApi,
method: 'DELETE',
data: data
})
}
//商城订单列表
export function orderList(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.order,
method: 'GET',
data: data
})
}
// 删除订单
export function delOrder(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
// url: apiUrl.cart_item,
url: apiUrl.order,
method: 'DELETE',
data: data
})
}
// 取消订单
export function orderCancel(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
// url: apiUrl.cart_item,
url: apiUrl.orderCancel,
method: 'PUT',
data: data
})
}
// 确认收货
export function orderReceive(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
// url: apiUrl.cart_item,
url: apiUrl.orderReceive,
method: 'POST',
data: data
})
}
//创建商城订单
export function createOrder(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.orderList,
method: 'POST',
data: data
})
}
//订单预览
export function orderPreview(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.orderPreview,
method: 'POST',
data: data
})
}
//盲盒订单列表
export function boxOrderList(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxOrder,
method: 'GET',
data: data
})
}
//创建盲盒订单
export function boxCreateOrder(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxOrder,
method: 'POST',
data: data
})
}
//礼盒/物品列表
export function boxRecord(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxRecord,
method: 'GET',
data: data
})
}
//开盲盒
export function boxOpen(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
header: headerObj,
url: apiUrl.boxOpen,
method: 'POST',
data: data
})
}
//提取盲盒
export function boxPickup(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxPickup,
method: 'POST',
data: data
})
}
//回收盲盒
export function boxRecycle(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.boxRecycle,
method: 'POST',
data: data
})
}
// 订单详情
export function orderDetail(no,data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: httpUrlFormat(`/order/${no}`, _flag),
method: 'GET',
data: data
})
}
//盲盒详情
export function boxInfo(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.boxInfo,
method: 'GET',
data: data
})
}
// 根据盲盒id开盒
export function boxOpenByBoxId(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.boxOpenByBoxId,
method: 'POST',
data: data
})
}

89
API/ranking.js Normal file
View File

@ -0,0 +1,89 @@
import {
httpUrlFormat,
ajaxCarryAuthorization,ajaxSync
} from '@/utils/httpUtils.js'
import { ApiVersion,AppVersion } from '@/config/baseConfig.js'
const headerVersion = {
'API-Version': ApiVersion(),
'APP-Version':AppVersion()
}
const _flag = 'apiBaseUrl' // 请求的api空间
const apiUrl = {
'topStatusApi': httpUrlFormat('/top/status', _flag),
}
export function topUserListApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: httpUrlFormat(`/top/user/list/${data.type}`, _flag),
method: 'GET',
data
})
}
export function topUserSelfApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: httpUrlFormat(`/top/user/self/${data.type}`, _flag),
method: 'GET',
data
})
}
export function topStatusApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.topStatusApi,
method: 'GET',
data
})
}
export function topUserListLastApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: httpUrlFormat(`/top/user/list/last/${data.type}`, _flag),
method: 'GET',
data
})
}
export function topUserSelfLastApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: httpUrlFormat(`/top/user/self/last/${data.type}`, _flag),
method: 'GET',
data
})
}
export function topRuleApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: httpUrlFormat(`/top/rule/${data.type}`, _flag),
method: 'GET',
data
})
}
export function topExpiredTimeApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: httpUrlFormat(`/top/expired/time/${data.type}`, _flag),
method: 'GET',
data
})
}
export function topDetailApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: httpUrlFormat(`/top/${data.type}`, _flag),
method: 'GET',
data
})
}

44
API/report.js Normal file
View File

@ -0,0 +1,44 @@
import {
httpUrlFormat,
ajaxCarryAuthorization,ajaxSync
} from '@/utils/httpUtils.js'
import { ApiVersion,AppVersion } from '@/config/baseConfig.js'
const headerVersion = {
'API-Version': ApiVersion(),
'APP-Version':AppVersion()
}
const _flag = 'apiBaseUrl' // 请求的api空间
const apiUrl = {
'reportHeartbeat': httpUrlFormat('/report/heartbeat', _flag),
'reportClick': httpUrlFormat('/report/click', _flag),
}
//心跳埋点
export function reportHeartbeatApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.reportHeartbeat,
method: 'POST',
data: data
})
}
//点击埋点
export function reportClickApi(data) {
// const headerObj = {'Content-Type': 'application/x-www-form-urlencoded',...headerVersion}
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.reportClick,
method: 'POST',
data: data
})
}

232
API/shop.js Normal file
View File

@ -0,0 +1,232 @@
import {
httpUrlFormat,
ajaxCarryAuthorization,ajaxSync
} from '@/utils/httpUtils.js'
import { ApiVersion,AppVersion } from '@/config/baseConfig.js'
const headerVersion = {
'API-Version': ApiVersion(),
'APP-Version':AppVersion()
}
const _flag = 'apiBaseUrl' // 请求的api空间
const apiUrl = {
'product': httpUrlFormat('/product', _flag),//商品分页
'categories': httpUrlFormat('/product/categories', _flag),//商品分类
'rollingImgs': httpUrlFormat('/rolling_imgs', _flag),//商品分类
'favorItem': httpUrlFormat('/user/favor_item', _flag),//收藏
'deliveryFee': httpUrlFormat('/product/delivery_fee', _flag),//运费
'favorItemProduct': httpUrlFormat('/user/favor_item_product', _flag),//运费
'searchHistoryRecommend': httpUrlFormat('/search_history/recommend', _flag),//搜索推荐
'searchHistoryPersonal': httpUrlFormat('/search/history/personal', _flag),//搜索推荐
'orderBoxRecordCount': httpUrlFormat('/order/box_record_count', _flag),//盲盒统计
'carriageRule': httpUrlFormat('/misc/carriage/rule', _flag),
'carriageGetRule': httpUrlFormat('/misc/carriage/get', _flag),
'tagsChannelApi': httpUrlFormat('/product/tags/channel', _flag),
'productChannelApi': httpUrlFormat('/product/channel', _flag),
'productRecommendChannel': httpUrlFormat('/product/product/recommend/channel', _flag),
'productChannelPointApi': httpUrlFormat('/product/channel/points', _flag),
'tagsPointApi': httpUrlFormat('/misc/product/points/tag', _flag),
}
// 渠道商品tag
export function tagsChannelApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.tagsChannelApi,
method: 'GET',
data: data
})
}
// 渠道商品
export function productChannelApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.productChannelApi,
method: 'GET',
data: data
})
}
export function productRecommendChannel(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.productRecommendChannel,
method: 'GET',
data: data
})
}
// 渠道商品积分筛选
export function productChannelPointApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.productChannelPointApi,
method: 'GET',
data: data
})
}
// 运费
export function deliveryFee() {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.deliveryFee,
method: 'GET',
})
}
export function product(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.product,
method: 'GET',
data: data
})
}
// 商品分类
export function categories(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.categories,
method: 'GET',
data: data
})
}
// 商品详情
export function productDetail(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: httpUrlFormat(`/product/${data}`, _flag),
method: 'GET',
})
}
// 商城轮播
export function rollingImgs(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.rollingImgs,
method: 'GET',
})
}
// 收藏列表
export function favorItemList(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.favorItem,
method: 'GET',
data
})
}
// 收藏
export function favorItem(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.favorItem,
method: 'POST',
data
})
}
// 删除收藏
export function delFavorItem(ids) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
// url: apiUrl.cart_item,
url: httpUrlFormat(`/user/favor_item${ids}`, _flag),
method: 'DELETE'
})
}
// 删除商品详情收藏
export function favorItemProduct(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
// url: apiUrl.cart_item,
url: httpUrlFormat(`/user/favor_item_product${data}`, _flag),
method: 'DELETE',
// data: data
})
}
// 搜索推荐
export function searchHistoryRecommend(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.searchHistoryRecommend,
method: 'GET',
data
})
}
// 搜索历史
export function searchHistoryPersonal(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.searchHistoryPersonal,
method: 'GET',
data
})
}
// 删除搜索历史
export function delSearchHistoryPersonal() {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.searchHistoryPersonal,
method: 'DELETE',
})
}
// 盲盒统计
export function orderBoxRecordCount(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.orderBoxRecordCount,
method: 'GET',
data
})
}
export function carriageRule(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.carriageRule,
method: 'GET',
data
})
}
export function carriageGetRule(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.carriageGetRule,
method: 'GET',
data
})
}
export function tagsPointApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.tagsPointApi,
method: 'GET',
data
})
}

57
API/sign.js Normal file
View File

@ -0,0 +1,57 @@
import {
httpUrlFormat,
ajaxCarryAuthorization,ajaxSync
} from '@/utils/httpUtils.js'
import { ApiVersion,AppVersion } from '@/config/baseConfig.js'
const headerVersion = {
'API-Version': ApiVersion(),
'APP-Version':AppVersion()
}
const _flag = 'apiBaseUrl' // 请求的api空间
const apiUrl = {
'signInInfo': httpUrlFormat('/user/sign/in/info', _flag),//用户签到信息
'signIn': httpUrlFormat('/user/sign/in', _flag),//用户签到
'taskList': httpUrlFormat('/task/list', _flag),//任务列表
}
export function signInInfo(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.signInInfo,
method: 'GET',
data: data
})
}
export function signIn(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.signIn,
method: 'POST',
data
})
}
export function taskListApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: apiUrl.taskList,
method: 'GET',
data: data
})
}
export function taskReceive(taskId) {
return ajaxCarryAuthorization({
header: headerVersion,
url: httpUrlFormat(`/task/receive/${taskId}`, _flag),
method: 'POST',
})
}

393
API/user.js Normal file
View File

@ -0,0 +1,393 @@
import {
httpUrlFormat,
ajaxCarryAuthorization,ajaxSync
} from '@/utils/httpUtils.js'
import { ApiVersion,AppVersion } from '@/config/baseConfig.js'
const headerVersion = {
'API-Version': ApiVersion(),
'APP-Version':AppVersion()
}
const _flag = 'apiBaseUrl' // 请求的api空间
const apiUrl = {
'userInfo': httpUrlFormat('/user/info', _flag),//用户信息
'userPoint': httpUrlFormat('/user/point', _flag),//
'favorItemApi': httpUrlFormat('/user/favor/item', _flag),//
'userAddress': httpUrlFormat('/user/address', _flag),//收获地址
'boxCount': httpUrlFormat('/user/box_count', _flag),//
'notice': httpUrlFormat('/misc/notice', _flag),//公告
'noticeTopApi': httpUrlFormat('/misc/notice/top', _flag),//置顶公告
'uploadToken': httpUrlFormat('/user/upload_token', _flag),//上传oss凭证
'feedback': httpUrlFormat('/user/feedback', _flag),//意见反馈
'customer': httpUrlFormat('/misc/customer', _flag),//
'popupAdApi': httpUrlFormat('/misc/popup/ad', _flag),//
'couponApi': httpUrlFormat('/coupon', _flag),//优惠券
'userCouponApi': httpUrlFormat('/user/conpon', _flag),//用户优惠券
'userCouponBatchApi': httpUrlFormat('/user/conpon/reward/batch', _flag),//用户优惠券
'userCouponRewardApi': httpUrlFormat('/user/conpon/reward', _flag),//兑换优惠券
'coinGearApi': httpUrlFormat('/coin/gear', _flag),//代币充值
'coinGearRechargeApi': httpUrlFormat('/coin/gear/recharge', _flag),//代币充值订单
'payMethodApi': httpUrlFormat('/misc/pay/method', _flag),//获取支付方式
'coinPaidApi': httpUrlFormat('/coin/gear/recharge/paid', _flag),//
'couponCountApi': httpUrlFormat('/user/coupon/count', _flag),//
'miscPromptApi': httpUrlFormat('/misc/prompt', _flag),//
'shareApi': httpUrlFormat('/misc/build/share', _flag),//
'updateNickNameApi': httpUrlFormat('/user/update/nickname', _flag),//更新用户昵称
'recordCountApi': httpUrlFormat('/user/box/record/count', _flag),//用户盲盒开奖结果数量
'aliyunUploadTokenApi': httpUrlFormat('/misc/aliyun_upload_token', _flag),//获取oss上传凭证
'updateAvatarApi': httpUrlFormat('/user/update/avatar', _flag),//更新头像
'versionControlApi': httpUrlFormat('/misc/version/control', _flag),//版本控制
'userPromptListApi': httpUrlFormat('/user/prompt', _flag),//版本控制
}
// 更新用户昵称
export function updateNickNameApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.updateNickNameApi,
method: 'POST',
data: data
})
}
export function favorItemDelApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.favorItemApi,
method: 'DELETE',
data: data
})
}
export function favorItemApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.favorItemApi,
method: 'GET',
data: data
})
}
export function recordCountApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.recordCountApi,
method: 'GET',
data: data
})
}
export function userPoint(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.userPoint,
method: 'GET',
data: data
})
}
export function userInfo(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.userInfo,
method: 'GET',
data: data
})
}
// 收货地址列表
export function userAddress(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.userAddress,
method: 'GET',
data: data
})
}
// 新增收货地址
export function addAddress(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.userAddress,
method: 'POST',
data: data
})
}
// 收货地址详情
export function addressDetail(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: httpUrlFormat(`/user/address/${data}`, _flag),
method: 'GET',
})
}
// 编辑收货地址
export function editAddress(id,data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: httpUrlFormat(`/user/address/${id}`, _flag),
method: 'PUT',
data: data
})
}
// 删除地址
export function delAddress(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: httpUrlFormat(`/user/address?ids=${data.id}`, _flag),
method: 'DELETE',
data,data
})
}
// 礼盒物品数量
export function boxCount(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.boxCount,
method: 'GET',
})
}
// 系统公告
export function notice(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.notice,
method: 'GET',
data,data
})
}
// 系统公告详情
export function noticeDetail(id,data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: httpUrlFormat(`/misc/notice/${id}`, _flag),
method: 'GET',
data,data
})
}
// 系统公告详情
export function uploadToken(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.uploadToken,
method: 'GET',
data,data
})
}
// 意见反馈
export function feedback(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.feedback,
method: 'POST',
data,data
})
}
export function customerApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.customer,
method: 'GET',
data,data
})
}
export function noticeTopApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.noticeTopApi,
method: 'GET',
data
})
}
export function popupAdApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.popupAdApi,
method: 'GET',
data
})
}
export function couponApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.couponApi,
method: 'GET',
data
})
}
export function userCouponApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.userCouponApi,
method: 'GET',
data
})
}
export function userCouponRewardApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.userCouponRewardApi,
method: 'POST',
data: data
})
}
export function coinGearApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.coinGearApi,
method: 'GET',
data
})
}
export function payMethodApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.payMethodApi,
method: 'GET',
data
})
}
export function coinGearRechargeApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.coinGearRechargeApi,
method: 'POST',
data: data
})
}
export function coinPaidApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.coinPaidApi,
method: 'GET',
data
})
}
export function userCouponBatchApi(userId,data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: httpUrlFormat(`/user/conpon/reward/batch?userId=${userId}`, _flag),
method: 'POST',
data: data
})
}
export function couponCountApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.couponCountApi,
method: 'GET',
data: data
})
}
export function miscPromptApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.miscPromptApi,
method: 'GET',
data: data
})
}
export function shareApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.shareApi,
method: 'GET',
data: data
})
}
export function aliyunUploadTokenApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.aliyunUploadTokenApi,
method: 'GET',
data
})
}
export function updateAvatarApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.updateAvatarApi,
method: 'POST',
data: data
})
}
export function versionControlApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.versionControlApi,
method: 'POST',
data
})
}
export function userPromptListApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.userPromptListApi,
method: 'GET',
data
})
}

95
API/virtual.js Normal file
View File

@ -0,0 +1,95 @@
import {
httpUrlFormat,
ajaxCarryAuthorization,ajaxSync
} from '@/utils/httpUtils.js'
import { ApiVersion,AppVersion } from '@/config/baseConfig.js'
const headerVersion = {
'API-Version': ApiVersion(),
'APP-Version':AppVersion()
}
const _flag = 'apiBaseUrl' // 请求的api空间
const apiUrl = {
'virtualOrderPreviewApi': httpUrlFormat('/virtual/order/preview', _flag),//商城订单预览
'virtualOrderBoxPreviewApi': httpUrlFormat('/virtual/order/box/delivery/preview', _flag),//盲盒订单预览
'virtualOrderApi': httpUrlFormat('/virtual/order/box/delivery', _flag),
'virtualOrderPaidApi': httpUrlFormat('/virtual/order/paid', _flag),
'virtualOrderListApi': httpUrlFormat('/virtual/order', _flag),
}
export function virtualShopOrderApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.virtualOrderListApi,
method: 'POST',
data
})
}
export function virtualOrderPreviewApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.virtualOrderPreviewApi,
method: 'POST',
data: data
})
}
export function virtualOrderBoxPreviewApi(data) {
const headerObj = {...headerVersion,'Content-Type': 'application/x-www-form-urlencoded'}
return ajaxCarryAuthorization({
header: headerObj,
url: apiUrl.virtualOrderBoxPreviewApi,
method: 'POST',
data: data
})
}
// 创建订单
export function virtualOrderApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.virtualOrderApi,
method: 'POST',
data
})
}
export function virtualOrderPaidApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.virtualOrderPaidApi,
method: 'GET',
data
})
}
export function virtualOrderListApi(data) {
return ajaxCarryAuthorization({
header: headerVersion,
// header: {'Content-Type': 'application/x-www-form-urlencoded'},
url: apiUrl.virtualOrderListApi,
method: 'GET',
data: data
})
}
export function virtualOrderDetailId(id) {
return ajaxCarryAuthorization({
header: headerVersion,
url: httpUrlFormat(`/virtual/order/${id}`, _flag),
method: 'GET',
})
}
export function virtualOrderDetail(no,data) {
return ajaxCarryAuthorization({
header: headerVersion,
url: httpUrlFormat(`/virtual/order/no/${no}`, _flag),
method: 'GET',
data: data
})
}

84
App.vue Normal file
View File

@ -0,0 +1,84 @@
<script>
import {uuid} from '@/utils/jsUtils.js'
import { reportHeartbeatApi,reportClickApi } from '@/API/report.js'
export default {
globalData: {
},
onLaunch: function() {
// console.log("")
// // app
// const systemInfo = uni.getSystemInfoSync();
// //
// // H5
// // #ifdef H5
// console.log(systemInfo.appVersion,'');
// // #endif
// console.log(systemInfo.appVersion,'');
// let version = systemInfo.appVersion ? systemInfo.appVersion : '0.0.1'
uni.setStorageSync('activityShow', false)
uni.setStorageSync('firstTime', false),//
reportHeartbeatApi().then(res => {})
setInterval(() => {
reportHeartbeatApi().then(res => {
console.log("我是三十秒的定时器被调用了:",res)
})
},30000)
},
onShow: function() {
// console.log('App Show')
uni.setStorageSync('ad','frist')
},
onHide: function() {
// console.log('App Hide')
// uni.removeStorageSync('ad')
uni.setStorageSync('ad','frist')
uni.removeStorageSync('activityShow')
},
onUnload:function() {
uni.setStorageSync('ad','frist')
}
}
</script>
<style>
/*每个页面公共css */
/* #ifndef APP-NVUE */
@import url("static/app.css");
/* #endif */
@font-face {
font-family: YouSheBiaoTiHei;
src: url('https://box-mall.oss-cn-hangzhou.aliyuncs.com/static/font/YouSheBiaoTiHei.ttf');
}
@font-face {
font-family: Zhxs;
src: url('./static/zhxs.ttf');
}
@font-face {
font-family: Impacts;
src: url('https://box-mall.oss-cn-hangzhou.aliyuncs.com/static/font/impact-2.ttf');
}
@font-face {
font-family: ZhanKuQingKeHuangYouTi;
src: url('https://box-mall.oss-cn-hangzhou.aliyuncs.com/static/font/ZhanKuQingKeHuangYouTi-2.ttf');
}
.no-data{
font-size: 24rpx;
text-align: center;
padding: 10rpx 0 30rpx 0;
}
uni-swiper .uni-swiper-dot{
width: 5rpx;
height: 5rpx;
background: rgba(16,16,16,0.2);
border-radius: 50%;
}
uni-swiper .uni-swiper-dot-active{
width: 16rpx;
height: 5rpx;
background: #FFFFFF;
border-radius: 2rpx;
}
</style>

3
Dockerfile Executable file
View File

@ -0,0 +1,3 @@
FROM nginx
EXPOSE 80
COPY /unpackage/dist/build/h5 /usr/share/nginx/html

48
README.md Normal file
View File

@ -0,0 +1,48 @@
# 盲盒H5基础版
## 登录
```bash
1.登录页面('账号密码登录、验证码登录')
```
## 首页
```bash
1.对接首页盲盒
2.盲盒标签以及标签下的盲盒
3.盲盒搜索
4.对接置顶公告
5.广告弹窗、轮播
7.对接盲盒详情
8.提示卡('页面完成没有对接功能')
9.购买盲盒订单预览页面
10.支付('暂未对接支付通道')、购买成功页面
11.开奖页面('暂无动画')
```
## 商城
```bash
1.商场首页('轮播未完成')
2.商场搜索,搜索历史
3.商品详情
4.商品收藏/取消收藏
5.购买提交订单
6.购买成功页
```
## 签到
```bash
1.页面完成 暂未对接
```
## 个人中心
```bash
1.个人中心已完成
2.设置页面('更换头像、修改昵称、关于我们暂未对接')
3.隐私政策、用户协议页面完成 暂未更新内容
4.优惠券、更多优惠券页面完成 暂未对接
5.积分使用记录已完成
6.物品页面完成 发货 回收接口已完成('发货回收接口需改成批量 页面已完成暂未对接')
7.订单详情已完成(订单详情、确认收货、查看物流)
8.盲盒订单已完成
9.地址管理已完成(新增地址、设置默认地址)
10.我的收藏已完成(取消收藏)
```

View File

@ -0,0 +1,176 @@
<template>
<view>
<view class="flex a-center content" v-if="lineData">
<view><slot name="content"></slot></view>
</view>
<view class="flex a-center" style="padding-right: 0;">
<view class="progress-container" id="container" ref="progressContainer" :style="{ background: inBgColor }">
<view
class="progress-content flex j-end"
id="content"
ref="progressContent"
:style="{ height: strokeWidth + 'px', background: bgColor, width: contentWidth, transition: `width ${duration / 1000}s ease` }"
v-if="isAnimate"
>
<view class="textInside flex a-center j-center" v-if="textInside && !noData">
<view>{{ percentage }}%</view>
</view>
</view>
<view v-if="!isAnimate" class="progress-content flex j-end" :style="{ width: percentage + '%', height: strokeWidth + 'px', background: bgColor }">
<view class="textInside flex a-center j-center" v-if="textInside && !noData">
<view class="text">{{ percentage }}%</view>
</view>
</view>
</view>
<view>
<!-- <view class="percentage" v-if="!textInside && !lineData && !noData && !isAnimate">{{ percentage }}%</view> -->
</view>
</view>
</view>
</template>
<script>
export default {
name: 'AiProgress',
components: {},
props: {
//
percentage: {
type: [Number, String],
required: true
},
//
textInside: {
type: Boolean,
default: false
},
//
strokeWidth: {
type: [Number, String],
default: 6
},
//
duration: {
type: [Number, String],
default: 2000
},
//
isAnimate: {
type: Boolean,
default: false
},
//
bgColor: {
type: String,
default: '#15B9D0'
},
//
noData: {
type: Boolean,
default: false
},
//
lineData: {
type: Boolean,
default: false
},
//
inBgColor: {
type: String,
default: '#DCDADA'
}
},
data() {
return {
width: 0,
timer: null,
containerWidth: 0,
contentWidth: 0
};
},
methods: {
start() {
if (this.isAnimate) {
// #ifdef H5
this.$nextTick(() => {
let progressContainer = this.$refs.progressContainer.$el;
let progressContent = this.$refs.progressContent.$el;
let style = window.getComputedStyle(progressContainer, null);
let width = style.width.replace('px', '') * ((this.percentage * 1) / 100);
progressContent.style.width = width.toFixed(2) + 'px';
progressContent.style.transition = `width ${this.duration / 1000}s ease`;
});
// #endif
const container = uni
.createSelectorQuery()
.in(this)
.selectAll('#container');
const content = uni
.createSelectorQuery()
.in(this)
.selectAll('#content');
container.boundingClientRect().exec(res1 => {
this.contentWidth = res1[0][0].width * 1 * ((this.percentage * 1) / 100).toFixed(2) + 'px';
});
}
}
},
mounted() {
this.$nextTick(() => {
this.start();
});
},
created() {},
filters: {},
computed: {},
watch: {},
directives: {}
};
</script>
<style scoped lang="scss">
.content {
margin-bottom: 10rpx;
.c-per {
font-size: 26rpx;
}
}
.progress-container {
width: 100%;
border-radius: 100rpx;
.progress-content {
border-radius: 100rpx;
width: 0;
}
.textInside {
color: #fff;
margin-right: 10rpx;
position: relative;
}
}
.text {
margin-left: 10rpx;
}
.percentage {
margin-left: 6rpx;
font-size: 12rpx;
width: 30rpx;
}
.flex {
display: flex;
}
.a-center {
align-items: center;
}
.j-center {
justify-content: center;
}
.j-between {
justify-content: space-between;
}
.content {
margin-bottom: 10rpx;
color: #666;
font-size: 32rpx;
}
</style>

View File

@ -0,0 +1,69 @@
<!-- 底部固定布局组件 -->
<template>
<view class="BottomFixed" :style="[styleCom]">
<view class="BottomNavBarFixed" :style="{height: barHeightCom, bottom: fixedBottomCom, 'background-color': bgColor}">
<slot></slot>
</view>
</view>
</template>
<script>
export default {
name: 'BottomFixed',
props: {
//
barHeight: {
type: [String, Number],
default: 60
},
//
unit: {
type: String,
default: 'px'
},
//
bgColor: {
type: String,
default: ''
},
//
pHasH: {
type: Boolean,
default: true
},
//
fixedBottom: {
type: [String, Number],
default: 0
}
},
computed: {
styleCom () {
if (this.pHasH) {
return {
height: `${this.barHeight}${this.unit}`
}
}
return {}
},
barHeightCom () {
return `${this.barHeight}${this.unit}`
},
fixedBottomCom () {
return `${this.fixedBottom}${this.unit}`
}
}
}
</script>
<style lang="scss" scoped>
.BottomFixed {
position: relative;
.BottomNavBarFixed {
position: fixed;
bottom: 0;
width: 100%;
}
}
</style>

View File

@ -0,0 +1,120 @@
<!-- 引导页组件 -->
<template>
<view class="codeElfGuide">
<swiper
class="swiperView"
indicator-color="rgba(255, 255, 255, .5)"
indicator-active-color="#fff"
:indicator-dots="dotsShow"
:autoplay="autoplay"
:duration="duration"
@change="swiperChange">
<template v-for="item in guideList">
<swiper-item :key="item.id">
<view class="swiperItemView" :style="{ 'background-color': item.bg }">
<image class="imgView" :src="item.img" mode="aspectFit"></image>
<view class="titleView">
<view class="title">{{ item.title }}</view>
<view class="dec">{{ item.dec }}</view>
</view>
<view v-if="(item.id + 1) === guideList.length" class="actBtnView">
<virtual-button style="width: 300rpx;" btnHeight="80" type="376AF5" shape="round" @click="launchFlag">
<view style="font-size: 28rpx;">{{ experience }}</view>
</virtual-button>
</view>
</view>
</swiper-item>
</template>
</swiper>
</view>
</template>
<script>
export default {
name: 'CodeElfGuide',
data() {
return {
autoplay: false,
duration: 500,
dotsShow: true, //
experience: '立即体验',
guideList: [
{ id: 0, img: '/static/components/guidePages/2.png', bg: '#00022D', title: '专业行情机制', dec: '实时更新行业动态数据 及时把握行情趋势' },
{ id: 1, img: '/static/components/guidePages/3.1.png', bg: '#171C30', title: '账户安全模式', dec: '实时更新行业动态数据 及时把握行情趋势' },
{ id: 2, img: '/static/components/guidePages/1.png', bg: '#000000', title: '去中心化交易', dec: '实时更新行业动态数据 及时把握行情趋势' }
]
}
},
methods: {
launchFlag: function(){
/**
* 向本地存储中设置launchFlag的值即启动标识
*/
uni.setStorage({
key: 'launchFlag',
data: true,
});
uni.switchTab({
url: '/pages/index/index'
});
},
swiperChange (e) {
const _current = e.detail.current
if (_current === 2) {
this.dotsShow = false
} else {
this.dotsShow = true
}
}
}
}
</script>
<style lang="scss" scoped>
.codeElfGuide,
.swiperView {
width: 100%;
height: 100%;
}
.swiperView {
//
/deep/ .uni-swiper-dots.uni-swiper-dots-horizontal {
bottom: 90rpx;
}
.swiperItemView {
position: relative;
width: 100%;
height: 100%;
.imgView {
width: 100%;
height: 100%;
}
.titleView {
width: 100%;
position: absolute;
bottom: 360rpx;
left: 0;
text-align: center;
.title {
font-size: 48rpx;
color: #FFFFFF;
}
.dec {
margin-top: 30rpx;
font-size: 28rpx;
color: #CECECE;
}
}
.actBtnView {
width: 100%;
position: absolute;
bottom: 90rpx;
left: 0;
text-align: center;
}
}
}
</style>

View File

@ -0,0 +1,68 @@
<!-- 自定义radio组件 -->
<template>
<view class="CustomRadioItem" @click="changeCheckHandle">
<image v-if="isAnonymous" src="@/static/components/customRadioItem/goods_checkbox_pressed.png" mode="" class="anonymity-img"></image>
<image v-if="!isAnonymous" src="@/static/components/customRadioItem/goods_checkbox_normal.png" mode="" class="anonymity-img"></image>
<slot>
<text>{{ radioTitle }}</text>
</slot>
</view>
</template>
<script>
export default {
name: 'CustomRadioItem',
props: {
radioTitle: {
type: String,
default: ''
},
//
checked: {
type: Boolean,
default: false
}
},
model: {
prop: 'checked',
event: 'change'
},
data() {
return {
isAnonymous: false
};
},
watch: {
checked: function (nVal) {
this.isAnonymous = nVal
}
},
mounted () {
this.isAnonymous = this.checked
},
updated() {
this.isAnonymous = this.checked
},
methods: {
changeCheckHandle () {
this.isAnonymous = !this.isAnonymous
this.$emit('change', this.isAnonymous);
}
}
}
</script>
<style lang="scss" scoped>
.CustomRadioItem {
display: flex;
align-items: center;
font-size: 28rpx;
color: #564F5F;
.anonymity-img{
width: 36rpx;
height: 36rpx;
margin-right: 20rpx;
}
}
</style>

View File

@ -0,0 +1,58 @@
<!-- empty空数据提示组件 -->
<template>
<view class="empty" :style="styleObj">
<view v-if="emptyImg.length > 0" class="emptyImgView">
<image class="emptyImg" :src="emptyImg" mode=""></image>
</view>
<view class="emptyTitle">
{{ title }}
</view>
</view>
</template>
<script>
export default {
name: 'Empty',
props: {
//
title: {
type: String,
default: '暂无数据'
},
//
emptyImg: {
type: String,
default: '/static/new/mine/empty.png'
},
//
styleObj: {
type: Object,
default: function () {
return {}
}
}
}
}
</script>
<style lang="scss" scoped>
.empty {
.emptyImgView {
display: flex;
align-items: center;
justify-content: center;
.emptyImg {
width: 456rpx;
height: 262rpx;
}
}
.emptyTitle {
padding-top: 41rpx;
text-align: center;
font-weight: 400;
font-size: 20rpx;
color: #888888;
}
}
</style>

View File

@ -0,0 +1,269 @@
<template>
<view :style="[customStyle]" class="u-icon" @tap="click" :class="['u-icon--' + labelPos]">
<image class="u-icon__img" v-if="isImg" :src="name" :mode="imgMode" :style="[imgStyle]"></image>
<text v-else class="u-icon__icon" :class="customClass" :style="[iconStyle]" :hover-class="hoverClass" @touchstart="touchstart"></text>
<text v-if="label" class="u-icon__label" :style="{
color: labelColor,
fontSize: addUnit(labelSize),
marginLeft: labelPos == 'right' ? addUnit(marginLeft) : 0,
marginTop: labelPos == 'bottom' ? addUnit(marginTop) : 0,
marginRight: labelPos == 'left' ? addUnit(marginRight) : 0,
marginBottom: labelPos == 'top' ? addUnit(marginBottom) : 0,
}">{{label}}</text>
</view>
</template>
<script>
/**
* icon 图标
* @description 基于字体的图标集包含了大多数常见场景的图标
* @tutorial https://www.uviewui.com/components/icon.html
* @property {String} name 图标名称见示例图标集
* @property {String} color 图标颜色默认inherit
* @property {String | Number} size 图标字体大小单位rpx默认32
* @property {String | Number} label-size label字体大小单位rpx默认28
* @property {String} label 图标右侧的label文字默认28
* @property {String} label-pos label文字相对于图标的位置只能right或bottom默认right
* @property {String} label-color label字体颜色默认#606266
* @property {Object} custom-style icon的样式对象形式
* @property {String} custom-prefix 自定义字体图标库时需要写上此值
* @property {String | Number} margin-left label在右侧时与图标的距离单位rpx默认6
* @property {String | Number} margin-top label在下方时与图标的距离单位rpx默认6
* @property {String | Number} margin-bottom label在上方时与图标的距离单位rpx默认6
* @property {String | Number} margin-right label在左侧时与图标的距离单位rpx默认6
* @property {String} label-pos label相对于图标的位置只能right或bottom默认right
* @property {String} index 一个用于区分多个图标的值点击图标时通过click事件传出
* @property {String} hover-class 图标按下去的样式类用法同uni的view组件的hover-class参数详情见官网
* @property {String} width 显示图片小图标时的宽度
* @property {String} height 显示图片小图标时的高度
* @property {String} top 图标在垂直方向上的定位
* @event {Function} click 点击图标时触发
* @example <icon-view name="photo" color="#2979ff" size="28"></icon-view>
*/
export default {
name: 'iconView',
props: {
//
name: {
type: String,
default: ''
},
//
color: {
type: String,
default: ''
},
// rpx
size: {
type: [Number, String],
default: 'inherit'
},
//
bold: {
type: Boolean,
default: false
},
// index
index: {
type: [Number, String],
default: ''
},
//
hoverClass: {
type: String,
default: ''
},
// 便
customPrefix: {
type: String,
default: 'uicon'
},
//
label: {
type: String,
default: ''
},
// label
labelPos: {
type: String,
default: 'right'
},
// label
labelSize: {
type: [String, Number],
default: '28'
},
// label
labelColor: {
type: String,
default: '#606266'
},
// label()
marginLeft: {
type: [String, Number],
default: '6'
},
// label()
marginTop: {
type: [String, Number],
default: '6'
},
// label()
marginRight: {
type: [String, Number],
default: '6'
},
// label()
marginBottom: {
type: [String, Number],
default: '6'
},
// mode
imgMode: {
type: String,
default: 'widthFix'
},
//
customStyle: {
type: Object,
default() {
return {}
}
},
//
width: {
type: [String, Number],
default: ''
},
//
height: {
type: [String, Number],
default: ''
},
//
top: {
type: [String, Number],
default: 0
}
},
data () {
return {
//
type: [
'primary',
'success',
'info',
'error',
'warning'
]
}
},
computed: {
customClass() {
let classes = [];
classes.push(this.customPrefix + '-' + this.name);
// uViewu-iconfont
if (this.customPrefix == 'uicon') classes.push('u-iconfont');
else classes.push(this.customPrefix);
//
if (this.color && this.type.includes(this.color)) classes.push('u-icon__icon--' + this.color);
// 使[a, b, c]
//
//#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
classes = classes.join(' ');
//#endif
return classes;
},
iconStyle() {
let style = {};
style = {
fontSize: this.size == 'inherit' ? 'inherit' : this.addUnit(this.size),
fontWeight: this.bold ? 'bold' : 'normal',
//
top: this.addUnit(this.top)
};
//
if (this.color && !this.type.includes(this.color)) style.color = this.color;
return style;
},
// name"/"
isImg() {
return this.name.indexOf('/') !== -1;
},
imgStyle() {
let style = {};
// widthheight使使size
style.width = this.width ? this.addUnit(this.width) : this.addUnit(this.size);
style.height = this.height ? this.addUnit(this.height) : this.addUnit(this.size);
return style;
}
},
methods: {
click() {
this.$emit('click', this.index);
},
touchstart() {
this.$emit('touchstart', this.index);
},
//
addUnit (value = 'auto', unit = 'rpx') {
value = String(value);
// uViewnumber
return this.testNumber(value) ? `${value}${unit}` : value;
},
testNumber (value) {
return /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value)
}
}
};
</script>
<style scoped lang="scss">
@import './iconfont.css';
.u-icon {
display: inline-flex;
align-items: center;
&--left {
flex-direction: row-reverse;
align-items: center;
}
&--right {
flex-direction: row;
align-items: center;
}
&--top {
flex-direction: column-reverse;
justify-content: center;
}
&--bottom {
flex-direction: column;
justify-content: center;
}
&__icon {
position: relative;
&--primary {
color: #2979ff;
}
&--success {
color: #19be6b;
}
&--error {
color: #fa3534;
}
&--warning {
color: #ff9900;
}
&--info {
color: #909399;
}
}
&__img {
height: auto;
will-change: transform;
}
&__label {
line-height: 1;
}
}
</style>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,107 @@
<!-- entranceList组件通用组件 -->
<template>
<uni-grid class="entrance" :column="column" :showBorder="false" :highlight="false">
<uni-grid-item class="entrance-grid-item" v-for="(v, i) in list" :key="i" @click="itemClickHandle(v)">
<view class="entrance-item">
<!-- <image class="image" :style="[imgStyle]" :src="v[imageKey]" mode=""></image>
<text class="title" :style="[titleStyle]">{{ v[titleKey] }}</text> -->
<image class="image" :style="{width:v.width,height:v.height}" :src="v[imageKey]" mode=""></image>
<text class="title" :style="[titleStyle]">{{ v[titleKey] }}</text>
</view>
</uni-grid-item>
</uni-grid>
</template>
<script>
export default {
name: 'IndexEntrance',
props: {
column: {
type: Number,
default: 4
},
list: {
type: Array,
default: function () {
return []
}
},
//
imageH: {
type: [String, Number],
default: 60
},
imageW: {
type: [String, Number],
default: 60
},
//
unit: {
type: String,
default: 'rpx'
},
imageKey: {
type: String,
default: 'image'
},
titleKey: {
type: String,
default: 'title'
},
// title
titleStyle: {
type: Object,
default () {
return {}
}
}
},
data () {
return {
}
},
computed: {
imgStyle () {
return {
width: `${this.imageW}${this.unit}`,
height: `${this.imageH}${this.unit}`
}
}
},
methods: {
itemClickHandle (item) {
this.$emit('click', item)
}
}
}
</script>
<style lang="scss" scoped>
.entrance {
%flexCenter {
display: flex;
align-items: center;
justify-content: center;
}
.entrance-grid-item {
@extend %flexCenter;
}
.entrance-item {
height: 100%;
width: 100%;
@extend %flexCenter;
flex-direction: column;
}
.image {
width: 80rpx;
height: 80rpx;
}
.title {
margin-top: 15rpx;
font-size: 24rpx;
text-align: center;
}
}
</style>

View File

@ -0,0 +1,434 @@
<p align="center">
<a href="https://github.com/liub1934/uni-lb-picker">
<img src="https://img.shields.io/github/stars/liub1934/uni-lb-picker">
</a>
<a href="https://github.com/liub1934/uni-lb-picker/fork">
<img src="https://img.shields.io/github/forks/liub1934/uni-lb-picker">
</a>
<a href="https://github.com/liub1934/uni-lb-picker/issues">
<img src="https://img.shields.io/github/issues/liub1934/uni-lb-picker">
</a>
<a href="https://www.npmjs.com/package/uni-lb-picker">
<img src="https://img.shields.io/npm/v/uni-lb-picker">
</a>
<a href="https://npmcharts.com/compare/uni-lb-picker?minimal=true">
<img src="https://img.shields.io/npm/dm/uni-lb-picker">
</a>
<a href="https://github.com/liub1934/uni-lb-picker/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/liub1934/uni-lb-picker">
</a>
</p>
插件市场里面的 picker 选择器不满足自己的需求,所以自己写了一个简单的 picker 选择器,可扩展、可自定义,一般满足日常需要。
Github[uni-lb-picker](https://github.com/liub1934/uni-lb-picker)
插件市场:[uni-lb-picker](https://ext.dcloud.net.cn/plugin?id=1111)
> 如果问题最好去 github 反馈,插件市场评论区留下五星好评即可,[点我去反馈](https://github.com/liub1934/uni-lb-picker/issues/new)
> **由于之前`cancel`拼写失误,写成了`cancle``v1.08`现已修正,如果之前版本有使用`cancel`事件的,更新后请及时修正。**
## 兼容性
App + H5 + 各平台小程序(快应用及 360 未测试nvue 待支持)
## 功能
1、单选
2、多级联动非多级联动理论支持任意级数
3、省市区选择基于多级联动
4、自定义选择器头部确定取消按钮颜色及插槽支持
5、选择器可视区自定义滚动个数
6、自定义数据字段满足不同人的需求
7、自定义选择器样式
8、单选及非联动选择支持扁平化的简单数据如下形式
```javascript
// 单选列表
list1: ['选项1', '选项2', '选项2'],
// 非联动选择列表
list2: [
['选项1', '选项2', '选项3'],
['选项11', '选项22', '选项33'],
['选项111', '选项222', '选项333']
]
```
## 引入插件
单独引入,在需要使用的页面上 import 引入即可
```html
<template>
<view>
<lb-picker></lb-picker>
</view>
</template>
<script>
import LbPicker from '@/components/lb-picker'
export default {
components: {
LbPicker
}
}
</script>
```
全局引入,`main.js`中 import 引入并注册即可全局使用
```jsvascript
import LbPicker from '@/components/lb-picker'
Vue.component("lb-picker", LbPicker)
```
easycom 引入
`pages.json`加上如下配置:
```json
"easycom": {
"autoscan": true,
"custom": {
"lb-picker": "@/components/lb-picker/index.vue"
}
}
```
npm 安装引入:
```shell
npm install uni-lb-picker
```
```jsvascript
import LbPicker from 'uni-lb-picker'
```
## 选择器数据格式
### 单选
常规数据
```javascript
list: [
{
label: '选项1',
value: '1'
},
{
label: '选项2',
value: '2'
}
]
```
扁平化简单数据
```javascript
list: ['选项1', '选项2']
```
### 多级联动
```javascript
list: [
{
label: '选项1',
value: '1',
children: [
{
label: '选项1-1',
value: '1-1',
children: [
{
label: '选项1-1-1',
value: '1-1-1'
}
]
}
]
}
]
```
### 非联动选择
常规数据
```javascript
list: [
[
{ label: '选项1', value: '1' },
{ label: '选项2', value: '2' },
{ label: '选项3', value: '3' }
],
[
{ label: '选项11', value: '11' },
{ label: '选项22', value: '22' },
{ label: '选项33', value: '33' }
],
[
{ label: '选项111', value: '111' },
{ label: '选项222', value: '222' },
{ label: '选项333', value: '333' }
]
]
```
扁平化简单数据
```javascript
list: [
['选项1', '选项2', '选项3'],
['选项11', '选项22', '选项33'],
['选项111', '选项222', '选项333']
]
```
## 调用显示选择器
通过`ref`形式手动调用`show`方法显示,隐藏同理调用`hide`
```text
<lb-picker ref="picker"></lb-picker>
this.$refs.picker.show() // 显示
this.$refs.picker.hide() // 隐藏
```
## 绑定值及设置默认值
支持 vue 中`v-model`写法绑定值,无需自己维护选中值的索引。
```javascript
<lb-picker v-model="value1"></lb-picker>
<lb-picker v-model="value2"></lb-picker>
data () {
return {
value1: '' // 单选
value2: [] // 多列联动选择
}
}
```
## 多个选择器
通过设置不同的`ref`,然后调用即可
```javascript
<lb-picker ref="picker1"></lb-picker>
<lb-picker ref="picker2"></lb-picker>
this.$refs.picker1.show() // picker1显示
this.$refs.picker2.show() // picker2显示
```
## 省市区选择
省市区选择是基于多列联动选择,数据来源:[https://github.com/modood/Administrative-divisions-of-China](https://github.com/modood/Administrative-divisions-of-China)
省市区文件位于`/pages/demos/area-data-min.js`,自行引入即可,可参考`demo3省市区选择`
也可使用自己已有的省市区数据,如果数据字段不一样,也可以自定义,参考下方自定义数据字段。
## 自定义数据字段
为了满足不同人的需求,插件支持自定义数据字段名称, 插件默认的数据字段如下形式:
```javascript
list: [
{
label: '选择1',
value: 1,
children: []
},
{
label: '选择1',
value: 1,
children: []
}
]
```
如果你的数据字段和上面不一样,如下形式:
```javascript
list: [
{
text: '选择1',
id: 1,
child: []
},
{
text: '选择1',
id: 1,
child: []
}
]
```
通过设置参数中的`props`即可,如下所示:
```javascript
<lb-picker :props="myProps"></lb-picker>
data () {
return {
myProps: {
label: 'text',
value: 'id',
children: 'child'
}
}
}
```
## 插槽使用
选择器支持一些可自定义化的插槽,如选择器的取消和确定文字按钮,如果需要对其自定义处理的话,比如加个 icon 图标之类的,可使用插槽,使用方法如下:
```html
<lb-picker>
<view slot="cancel-text">我是自定义取消</view>
<view slot="confirm-text">我是自定义确定</view>
</lb-picker>
```
其他插槽见下。
## 参数及事件
### Props
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
| :---------------------- | :--------------------------------------------------------------------------------------------------------------------------------- | :------------------ | :--------------------------------------------------------------- | :------------------------------------------------ |
| value/v-model | 绑定值,联动选择为 Array 类型 | String/Number/Array | - | - |
| mode | 选择器类型,支持单列,多列联动 | String | selector 单选/multiSelector 多级联动/unlinkedSelector 多级非联动 | selector |
| list | 选择器数据(v1.0.7 单选及非联动多选支持扁平数据:['选项 1', '选项 2']) | Array | - | - |
| level | 多列联动层级,仅 mode 为 multiSelector 有效 | Number | - | 2 |
| props | 自定义数据字段 | Object | - | {label:'label',value:'value',children:'children'} |
| cancel-text | 取消文字 | String | - | 取消 |
| cancel-color | 取消文字颜色 | String | - | #999 |
| confirm-text | 确定文字 | String | - | 确定 |
| confirm-color | 确定文字颜色 | String | - | #007aff |
| empty-text | (v1.0.7 新增)选择器列表为空的时候显示的文字 | String | - | 暂无数据 |
| empty-color | (v1.0.7 新增)暂无数据文字颜色 | String | - | #999 |
| column-num | 可视滚动区域内滚动个数,最好设置奇数值 | Number | - | 5 |
| radius | 选择器顶部圆角,支持 rpx如 radius="10rpx" | String | - | - |
| ~~column-style~~ | ~~选择器默认样式(已弃用,见下方自定义样式说明)~~ | Object | - | - |
| ~~active-column-style~~ | ~~选择器选中样式(已弃用,见下方自定义样式说明)~~ | Object | - | - |
| loading | 选择器是否显示加载中,可使用 loading 插槽自定义加载效果 | Boolean | - | - |
| mask-color | 遮罩层颜色 | String | - | rgba(0, 0, 0, 0.4) |
| close-on-click-mask | 点击遮罩层是否关闭选择器 | Boolean | true/false | true |
| ~~change-on-init~~ | ~~(v1.0.7 已弃用)初始化时是否触发 change 事件~~ | Boolean | true/false | - |
| dataset | (v1.0.7 新增)可以向组件中传递任意的自定义的数据(对象形式数据),如`:dataset="{name:'test'}"`,在`confirm`或`change`事件中可以取到 | Object | - | - |
| show-header | (v1.0.8 新增)是否显示选择器头部 | Boolean | - | true |
| inline | (v1.0.8 新增)inline 模式,开启后默认显示选择器,无需点击弹出,可以配合`show-header`一起使用 | Boolean | - | - |
| z-index | (v1.0.9 新增)选择器层级,遮罩层默认-1 | Number | - | 999 |
### 方法
| 方法名 | 说明 | 参数 | 返回值 |
| :------------- | :------------------------------------- | :-------------- | :----------------------------------------------------------------------------------------------------------- |
| show | 打开选择器 | - | |
| hide | 关闭选择器 | - | |
| getColumnsInfo | (v1.1.0 新增)根据 value 获取选择器信息 | 绑定值的`value` | 同`change` `confirm`回调参数,如果传入的`value`获取不到信息则只返回一个含有`dataset`的对象,具体自行打印查看 |
### Events
| 事件名称 | 说明 | 回调参数 |
| :------- | :--------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| show | 选择器打开时触发 | - |
| hide | 选择器隐藏时触发 | - |
| change | 选择器滚动时触发,此时不会改变绑定的值 | `{ index, item, value, change }` `index`触发滚动后新的索引,单选时是具体的索引值,多列联动选择时为数组。`item`触发滚动后新的的完整内容,包括`label`、`value`等,单选时为对象,多列选择时为数组对象。`value`触发滚动后新的 value 值,单列选择时为具体值,多列联动选择时为数组。`change`触发事件的类型,详情参考下面的 change 事件备注 |
| confirm | 点击选择器确定时触发,此时会改变绑定的值 | 同上`change`事件说明 |
| cancel | 点击选择器取消时触发 | 同上`change`事件说明 |
### `change` 事件备注
如果绑定的值是空的,`change`触发后里面的内容都是列表的第一项。
`change`事件会在以下情况触发:
- 初始化
- 绑定值 value 变化
- 选择器 list 列表变化
- 滚动选择器
以上情况会在回调函数中都可以取到`change`变化的类型,对应上面的情况包括以下:
- `init`
- `value`
- `list`
- `scroll`
根据这些类型大家可以在`change`的时候按需处理自己的业务逻辑,`init`现在指挥在调用选择器弹出的时候触发。
下面的说明情况已失效,如需要在页面显示的时候根据`value`的值显示相应的中文,调用`v1.10`新增的方法`getColumnsInfo`,传入绑定的值即可获取到你想要的所有信息。
~~比如一种常见的情况,有默认值的时候需要显示默认值的文字,此时可以`change`事件中判断`change`的类型是否是`init`,如果是的话可以取事件回调中的`item`进行显示绑定值对应的文字信息。~~
```javascript
handleChange (e) {
if (e.change === 'init') {
console.log(e.item.label) // 单选 选项1
console.log(e.item.map(item => item.label).join('-')) // 多选 选项1-选项11
}
}
```
### 插槽
| 插槽名 | 说明 |
| :------------ | :------------------ |
| cancel-text | 选择器取消文字插槽 |
| action-center | 选择器顶部中间插槽 |
| confirm-text | 选择器确定文字插槽 |
| loading | 选择器 loading 插槽 |
| empty | 选择器 空数据 插槽 |
### 选择器自定义样式
原先的`column-style`和`active-column-style`已弃用,如需修改默认样式及选中样式参考`demo9`
```css
<style lang="scss" scoped>
/deep/ .lb-picker {
.lb-picker-column-label {
color: #f0ad4e;
}
.lb-picker-column-active {
.lb-picker-column-label {
color: #007aff;
font-weight: 700;
}
}
}
</style>
```
### 获取选中值的文字
`@confirm`事件中可以拿到:
单选:
```javascript
handleConfirm (e) {
console.log(e.item.label) // 选项1
}
```
联动选择:
```javascript
handleConfirm (e) {
console.log(e.item.map(item => item.label).join('-')) // 选项1-选项11
}
```
## Tips
微信小程序端,滚动时在 iOS 自带振动反馈,可在系统设置 -> 声音与触感 -> 系统触感反馈中关闭
## 其他
其他功能参考示例 Demo 代码。

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,45 @@
import { getColumns } from '../utils'
export const commonMixin = {
data () {
return {
indicatorStyle: `height: 34px`
}
},
created () {
this.init('init')
},
methods: {
init (changeType) {
if (this.list && this.list.length) {
const column = getColumns({
value: this.value,
list: this.list,
mode: this.mode,
props: this.props,
level: this.level
})
const { columns, value, item, index } = column
this.selectValue = value
this.selectItem = item
this.pickerColumns = columns
this.pickerValue = index
this.$emit('change', {
value: this.selectValue,
item: this.selectItem,
index: this.pickerValue,
change: changeType
})
}
}
},
watch: {
value () {
if (!this.isConfirmChange) {
this.init('value')
}
},
list () {
this.init('list')
}
}
}

View File

@ -0,0 +1,93 @@
<template>
<view class="lb-multi-selector lb-picker-item"
:style="{ height: height }">
<picker-view :value="pickerValue"
:indicator-style="indicatorStyle"
:style="{ height: height }"
@change="handleChange">
<picker-view-column v-for="(column, index) in pickerColumns"
:key="index">
<view v-for="(item, i) in column || []"
:class="[
'lb-picker-column',
item[props.value] === selectValue[index]
? 'lb-picker-column-active'
: ''
]"
:key="i">
<text class="lb-picker-column-label">
{{ item[props.label] || item }}
</text>
</view>
</picker-view-column>
</picker-view>
</view>
</template>
<script>
import { commonMixin } from '../mixins'
export default {
props: {
value: Array,
list: Array,
mode: String,
props: Object,
level: Number,
visible: Boolean,
height: String,
isConfirmChange: Boolean
},
mixins: [commonMixin],
data () {
return {
pickerValue: [],
pickerColumns: [],
selectValue: [],
selectItem: []
}
},
methods: {
handleChange (item) {
const pickerValue = item.detail.value
const columnIndex = pickerValue.findIndex((item, i) => item !== this.pickerValue[i])
const valueIndex = pickerValue[columnIndex]
this.setPickerChange(pickerValue, valueIndex, columnIndex)
},
setPickerChange (pickerValue, valueIndex, columnIndex) {
for (let i = 0; i < this.level; i++) {
if (i > columnIndex) {
pickerValue[i] = 0
const column =
this.pickerColumns[i - 1][valueIndex] ||
this.pickerColumns[i - 1][0]
this.$set(this.pickerColumns, i, column[this.props.children] || [])
valueIndex = 0
}
this.$set(this.pickerValue, i, pickerValue[i])
const selectItem = this.pickerColumns[i][pickerValue[i]]
if (selectItem) {
this.selectItem[i] = selectItem
this.selectValue[i] = selectItem[this.props.value]
} else {
const spliceNum = this.level - i
this.pickerValue.splice(i, spliceNum)
this.selectValue.splice(i, spliceNum)
this.selectItem.splice(i, spliceNum)
this.pickerColumns.splice(i, spliceNum)
break
}
}
this.$emit('change', {
value: this.selectValue,
item: this.selectItem,
index: this.pickerValue,
change: 'scroll'
})
}
}
}
</script>
<style lang="scss" scoped>
@import "../style/picker-item.scss";
</style>

View File

@ -0,0 +1,68 @@
<template>
<view class="lb-selector-picker lb-picker-item"
:style="{ height: height }">
<picker-view :value="pickerValue"
:style="{ height: height }"
:indicator-style="indicatorStyle"
@change="handleChange">
<picker-view-column>
<view v-for="(item, i) in list"
:class="[
'lb-picker-column',
(item[props.value] || item) === selectValue
? 'lb-picker-column-active'
: ''
]"
:key="i">
<text class="lb-picker-column-label">
{{ item[props.label] || item }}
</text>
</view>
</picker-view-column>
</picker-view>
</view>
</template>
<script>
import { isObject } from '../utils'
import { commonMixin } from '../mixins'
export default {
props: {
value: [String, Number],
list: Array,
mode: String,
props: Object,
visible: Boolean,
height: String,
isConfirmChange: Boolean
},
mixins: [commonMixin],
data () {
return {
pickerValue: [],
selectValue: '',
selectItem: null
}
},
methods: {
handleChange (item) {
const index = item.detail.value[0] || 0
this.selectItem = this.list[index]
this.selectValue = isObject(this.selectItem)
? this.selectItem[this.props.value]
: this.selectItem
this.pickerValue = item.detail.value
this.$emit('change', {
value: this.selectValue,
item: this.selectItem,
index: index,
change: 'scroll'
})
}
}
}
</script>
<style lang="scss" scoped>
@import "../style/picker-item.scss";
</style>

View File

@ -0,0 +1,76 @@
<template>
<view class="lb-selector-picker lb-picker-item"
:style="{ height: height }">
<picker-view :value="pickerValue"
:indicator-style="indicatorStyle"
:style="{ height: height }"
@change="handleChange">
<picker-view-column v-for="(column, index) in pickerColumns"
:key="index">
<view v-for="(item, i) in column || []"
:class="[
'lb-picker-column',
(item[props.value] || item) === selectValue[index]
? 'lb-picker-column-active'
: ''
]"
:key="i">
<text class="lb-picker-column-label">
{{ item[props.label] || item }}
</text>
</view>
</picker-view-column>
</picker-view>
</view>
</template>
<script>
import { isObject } from '../utils'
import { commonMixin } from '../mixins'
export default {
props: {
value: Array,
list: Array,
mode: String,
props: Object,
visible: Boolean,
height: String,
isConfirmChange: Boolean
},
mixins: [commonMixin],
data () {
return {
pickerValue: [],
pickerColumns: [],
selectValue: [],
selectItem: []
}
},
methods: {
handleChange (item) {
const pickerValue = item.detail.value
const columnIndex = pickerValue.findIndex((item, i) => item !== this.pickerValue[i])
if (columnIndex > -1) {
const valueIndex = pickerValue[columnIndex]
const columnItem = this.list[columnIndex][valueIndex]
const valueItem = isObject(columnItem)
? columnItem[this.props.value]
: columnItem
this.pickerValue = pickerValue
this.$set(this.selectValue, columnIndex, valueItem)
this.$set(this.selectItem, columnIndex, columnItem)
this.$emit('change', {
value: this.selectValue,
item: this.selectItem,
index: this.pickerValue,
change: 'scroll'
})
}
}
}
}
</script>
<style lang="scss" scoped>
@import "../style/picker-item.scss";
</style>

View File

@ -0,0 +1,23 @@
.lb-picker-column {
height: 34px;
/* #ifndef APP-NVUE */
display: flex;
box-sizing: border-box;
white-space: nowrap;
overflow: hidden;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: center;
}
.lb-picker-column-label {
font-size: 16px;
text-align: center;
text-overflow: ellipsis;
transition-property: color;
transition-duration: 0.3s;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@ -0,0 +1,151 @@
.lb-picker {
position: relative;
}
.lb-picker-mask {
background-color: rgba(0, 0, 0, 0.0);
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
}
.lb-picker-mask-animation {
transition-property: background-color;
transition-duration: 0.3s;
}
.lb-picker-container {
position: relative;
}
.lb-picker-container-fixed {
position: fixed;
left: 0;
right: 0;
bottom: 0;
transform: translateY(100%);
/* #ifndef APP-PLUS */
overflow: hidden;
/* #endif */
}
.lb-picker-container-animation {
transition-property: transform;
transition-duration: 0.3s;
}
.lb-picker-container-show {
transform: translateY(0);
}
.lb-picker-header {
position: relative;
height: 45px;
background-color: #fff;
/* #ifdef APP-NVUE */
border-width: 1px;
border-style: solid;
border-color: #e5e5e5;
/* #endif */
/* #ifndef APP-NVUE */
box-sizing: border-box;
display: flex;
/* #endif */
flex-direction: row;
justify-content: space-between;
flex-wrap: nowrap;
}
/* #ifndef APP-PLUS */
.lb-picker-header::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
right: 0;
height: 1px;
clear: both;
border-bottom: 1px solid #e5e5e5;
color: #e5e5e5;
transform-origin: 0 100%;
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
}
/* #endif */
.lb-picker-action {
padding-left: 14px;
padding-right: 14px;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: center;
}
.lb-picker-action-cancel {
text-align: center;
}
.lb-picker-action-cancel-text {
font-size: 16px;
color: #999;
}
.lb-picker-action-confirm {
text-align: center;
}
.lb-picker-action-confirm-text {
font-size: 16px;
color: #007aff;
}
.lb-picker-content {
position: relative;
background-color: #fff;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
flex-direction: column;
}
.lb-picker-loading,
.lb-picker-empty {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center;
}
.lb-picker-empty-text {
color: #999;
font-size: 16px;
}
.lb-picker-loading-img {
width: 25px;
height: 25px;
/* #ifndef APP-NVUE */
animation: rotating 2s linear infinite;
/* #endif */
}
/* #ifndef APP-NVUE */
@keyframes rotating {
0% {
transform: rotate(0deg)
}
to {
transform: rotate(1turn)
}
}
/* #endif */

View File

@ -0,0 +1,110 @@
/**
* 判断是否是对象
*
* @export
* @param {*} val
* @returns true/false
*/
export function isObject (val) {
return Object.prototype.toString.call(val) === '[object Object]'
}
/**
* 根据value获取columns信息
*
* @export
* @param {*} { value, list, mode, props, level }
* @param {number} [type=2] 查询不到value数据返回数据类型 1空值null 2默认第一个选项
* @returns
*/
export function getColumns ({ value, list, mode, props, level }, type = 2) {
let pickerValue = []
let pickerColumns = []
let selectValue = []
let selectItem = []
let columnsInfo = null
switch (mode) {
case 'selector':
let index = list.findIndex(item => {
return isObject(item) ? item[props.value] === value : item === value
})
if (index === -1 && type === 1) {
columnsInfo = null
} else {
index = index > -1 ? index : 0
selectItem = list[index]
selectValue = isObject(selectItem)
? selectItem[props.value]
: selectItem
pickerColumns = list
pickerValue = [index]
columnsInfo = {
index: pickerValue,
value: selectValue,
item: selectItem,
columns: pickerColumns
}
}
break
case 'multiSelector':
const setPickerItems = (data = [], index = 0) => {
if (!data.length) return
const defaultValue = value || []
if (index < level) {
const value = defaultValue[index] || ''
let i = data.findIndex(item => item[props.value] === value)
if (i === -1 && type === 1) return
i = i > -1 ? i : 0
pickerValue[index] = i
pickerColumns[index] = data
if (data[i]) {
selectValue[index] = data[i][props.value]
selectItem[index] = data[i]
setPickerItems(data[i][props.children] || [], index + 1)
}
}
}
setPickerItems(list)
if (!selectValue.length && type === 1) {
columnsInfo = null
} else {
columnsInfo = {
index: pickerValue,
value: selectValue,
item: selectItem,
columns: pickerColumns
}
}
break
case 'unlinkedSelector':
list.forEach((item, i) => {
let index = item.findIndex(item => {
return isObject(item)
? item[props.value] === value[i]
: item === value[i]
})
if (index === -1 && type === 1) return
index = index > -1 ? index : 0
const columnItem = list[i][index]
const valueItem = isObject(columnItem)
? columnItem[props.value]
: columnItem
pickerValue[i] = index
selectValue[i] = valueItem
selectItem[i] = columnItem
})
pickerColumns = list
if (!selectValue.length && type === 1) {
columnsInfo = null
} else {
columnsInfo = {
index: pickerValue,
value: selectValue,
item: selectItem,
columns: pickerColumns
}
}
break
}
return columnsInfo
}

View File

@ -0,0 +1,120 @@
<!-- 遮罩 -->
<template>
<view class="u-mask" hover-stop-propagation :style="[maskStyle, zoomStyle]" @tap="click" @touchmove.stop.prevent :class="{
'u-mask-zoom': zoom,
'u-mask-show': show
}">
<slot />
</view>
</template>
<script>
/**
* mask 遮罩
* @description 创建一个遮罩层用于强调特定的页面元素并阻止用户对遮罩下层的内容进行操作一般用于弹窗场景
* @tutorial https://www.uviewui.com/components/mask.html
* @property {Boolean} show 是否显示遮罩默认false
* @property {String Number} z-index z-index 层级默认1070
* @property {Object} custom-style 自定义样式对象见上方说明
* @property {String Number} duration 动画时长单位毫秒默认300
* @property {Boolean} zoom 是否使用scale对这招进行缩放默认true
* @property {Boolean} mask-click-able 遮罩是否可点击为false时点击不会发送click事件默认true
* @event {Function} click mask-click-able为true时点击遮罩发送此事件
* @example <mask-view :show="show" @click="show = false"></mask-view>
*/
export default {
name: "maskView",
props: {
//
show: {
type: Boolean,
default: false
},
// z-index
zIndex: {
type: [Number, String],
default: ''
},
//
customStyle: {
type: Object,
default () {
return {}
}
},
// 使使zoomscale
zoom: {
type: Boolean,
default: true
},
// ms
duration: {
type: [Number, String],
default: 300
},
//
maskClickAble: {
type: Boolean,
default: true
}
},
data() {
return {
zoomStyle: {
transform: ''
},
scale: 'scale(1.2, 1.2)'
}
},
watch: {
show(n) {
if(n && this.zoom) {
// scale1(1.2)
this.zoomStyle.transform = 'scale(1, 1)';
} else if(!n && this.zoom) {
// scale1.2(1)
this.zoomStyle.transform = this.scale;
}
}
},
computed: {
maskStyle() {
let style = {};
style.backgroundColor = "rgba(0, 0, 0, 0.6)";
if(this.show) style.zIndex = this.zIndex ? this.zIndex : 10070;
else style.zIndex = -1;
style.transition = `all ${this.duration / 1000}s ease-in-out`;
//
if (Object.keys(this.customStyle).length) style = { ...style,
...this.customStyle
};
return style;
}
},
methods: {
click() {
if (!this.maskClickAble) return;
this.$emit('click');
}
}
}
</script>
<style lang="scss" scoped>
.u-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
transition: transform 0.3s;
}
.u-mask-show {
opacity: 1;
}
.u-mask-zoom {
transform: scale(1.2, 1.2);
}
</style>

View File

@ -0,0 +1,55 @@
/* 下拉刷新区域 */
.mescroll-downwarp {
position: absolute;
top: -100%;
left: 0;
width: 100%;
height: 100%;
text-align: center;
}
/* 下拉刷新--内容区,定位于区域底部 */
.mescroll-downwarp .downwarp-content {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
min-height: 60rpx;
padding: 20rpx 0;
text-align: center;
}
/* 下拉刷新--提示文本 */
.mescroll-downwarp .downwarp-tip {
display: inline-block;
font-size: 28rpx;
vertical-align: middle;
margin-left: 16rpx;
/* color: gray; 已在style设置color,此处删去*/
}
/* 下拉刷新--旋转进度条 */
.mescroll-downwarp .downwarp-progress {
display: inline-block;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid gray;
border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
vertical-align: middle;
}
/* 旋转动画 */
.mescroll-downwarp .mescroll-rotate {
animation: mescrollDownRotate 0.6s linear infinite;
}
@keyframes mescrollDownRotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,47 @@
<!-- 下拉刷新区域 -->
<template>
<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
<view class="downwarp-content">
<view class="downwarp-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mOption.textColor, 'transform':downRotate}"></view>
<view class="downwarp-tip">{{downText}}</view>
</view>
</view>
</template>
<script>
export default {
props: {
option: Object , // down
type: Number, // inOffset1 outOffset2 showLoading3 endDownScroll4
rate: Number // (inOffset: rate<1; outOffset: rate>=1)
},
computed: {
// ,propdefault
mOption(){
return this.option || {}
},
//
isDownLoading(){
return this.type === 3
},
//
downRotate(){
return 'rotate(' + 360 * this.rate + 'deg)'
},
//
downText(){
switch (this.type){
case 1: return this.mOption.textInOffset;
case 2: return this.mOption.textOutOffset;
case 3: return this.mOption.textLoading;
case 4: return this.mOption.textLoading;
default: return this.mOption.textInOffset;
}
}
}
};
</script>
<style>
@import "./mescroll-down.css";
</style>

View File

@ -0,0 +1,90 @@
<!--空布局
可作为独立的组件, 不使用mescroll的页面也能单独引入, 以便APP全局统一管理:
import MescrollEmpty from '@/components/mescroll-uni/components/mescroll-empty.vue';
<mescroll-empty v-if="isShowEmpty" :option="optEmpty" @emptyclick="emptyClick"></mescroll-empty>
-->
<template>
<view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }" :style="{ 'z-index': option.zIndex, top: option.top }">
<view> <image v-if="icon" class="empty-icon" :src="icon" mode="widthFix" /> </view>
<view v-if="tip" class="empty-tip">{{ tip }}</view>
<view v-if="option.btnText" class="empty-btn" @click="emptyClick">{{ option.btnText }}</view>
</view>
</template>
<script>
//
import GlobalOption from './../mescroll-uni-option.js';
export default {
props: {
// empty: GlobalOption.up.empty
option: {
type: Object,
default() {
return {};
}
}
},
// 使computed,option
computed: {
//
icon() {
return this.option.icon == null ? GlobalOption.up.empty.icon : this.option.icon; // 使,
},
//
tip() {
return this.option.tip == null ? GlobalOption.up.empty.tip : this.option.tip; // 使,
}
},
methods: {
//
emptyClick() {
this.$emit('emptyclick');
}
}
};
</script>
<style>
/* 无任何数据的空布局 */
.mescroll-empty {
box-sizing: border-box;
width: 100%;
padding: 100rpx 50rpx;
text-align: center;
}
.mescroll-empty.empty-fixed {
z-index: 99;
position: absolute; /*transform会使fixed失效,最终会降级为absolute */
top: 100rpx;
left: 0;
}
.mescroll-empty .empty-icon {
width: 280rpx;
height: 280rpx;
}
.mescroll-empty .empty-tip {
margin-top: 20rpx;
font-size: 24rpx;
color: gray;
}
.mescroll-empty .empty-btn {
display: inline-block;
margin-top: 40rpx;
min-width: 200rpx;
padding: 18rpx;
font-size: 28rpx;
border: 2rpx solid #e04b28;
border-radius: 60rpx;
color: #e04b28;
}
.mescroll-empty .empty-btn:active {
opacity: 0.75;
}
</style>

View File

@ -0,0 +1,83 @@
<!-- 回到顶部的按钮 -->
<template>
<image
v-if="mOption.src"
class="mescroll-totop"
:class="[value ? 'mescroll-totop-in' : 'mescroll-totop-out', {'mescroll-totop-safearea': mOption.safearea}]"
:style="{'z-index':mOption.zIndex, 'left': left, 'right': right, 'bottom':addUnit(mOption.bottom), 'width':addUnit(mOption.width), 'border-radius':addUnit(mOption.radius)}"
:src="mOption.src"
mode="widthFix"
@click="toTopClick"
/>
</template>
<script>
export default {
props: {
// up.toTop
option: Object,
//
value: false
},
computed: {
// ,propdefault
mOption(){
return this.option || {}
},
//
left(){
return this.mOption.left ? this.addUnit(this.mOption.left) : 'auto';
},
// ()
right() {
return this.mOption.left ? 'auto' : this.addUnit(this.mOption.right);
}
},
methods: {
addUnit(num){
if(!num) return 0;
if(typeof num === 'number') return num + 'rpx';
return num
},
toTopClick() {
this.$emit('input', false); // 使v-model
this.$emit('click'); //
}
}
};
</script>
<style>
/* 回到顶部的按钮 */
.mescroll-totop {
z-index: 9990;
position: fixed !important; /* 加上important避免编译到H5,在多mescroll中定位失效 */
right: 20rpx;
bottom: 120rpx;
width: 72rpx;
height: auto;
border-radius: 50%;
opacity: 0;
transition: opacity 0.5s; /* 过渡 */
margin-bottom: var(--window-bottom); /* css变量 */
}
/* 适配 iPhoneX */
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
.mescroll-totop-safearea {
margin-bottom: calc(var(--window-bottom) + constant(safe-area-inset-bottom)); /* window-bottom + 适配 iPhoneX */
margin-bottom: calc(var(--window-bottom) + env(safe-area-inset-bottom));
}
}
/* 显示 -- 淡入 */
.mescroll-totop-in {
opacity: 1;
}
/* 隐藏 -- 淡出且不接收事件*/
.mescroll-totop-out {
opacity: 0;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,47 @@
/* 上拉加载区域 */
.mescroll-upwarp {
box-sizing: border-box;
min-height: 110rpx;
padding: 30rpx 0;
text-align: center;
clear: both;
}
/*提示文本 */
.mescroll-upwarp .upwarp-tip,
.mescroll-upwarp .upwarp-nodata {
display: inline-block;
font-size: 28rpx;
vertical-align: middle;
/* color: gray; 已在style设置color,此处删去*/
}
.mescroll-upwarp .upwarp-tip {
margin-left: 16rpx;
}
/*旋转进度条 */
.mescroll-upwarp .upwarp-progress {
display: inline-block;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid gray;
border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
vertical-align: middle;
}
/* 旋转动画 */
.mescroll-upwarp .mescroll-rotate {
animation: mescrollUpRotate 0.6s linear infinite;
}
@keyframes mescrollUpRotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,39 @@
<!-- 上拉加载区域 -->
<template>
<view class="mescroll-upwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-show="isUpLoading">
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mOption.textColor}"></view>
<view class="upwarp-tip">{{ mOption.textLoading }}</view>
</view>
<!-- 无数据 -->
<view v-if="isUpNoMore" class="upwarp-nodata">{{ mOption.textNoMore }}</view>
</view>
</template>
<script>
export default {
props: {
option: Object, // up
type: Number // 0loading1loading2
},
computed: {
// ,propdefault
mOption() {
return this.option || {};
},
//
isUpLoading() {
return this.type === 1;
},
//
isUpNoMore() {
return this.type === 2;
}
}
};
</script>
<style>
@import './mescroll-up.css';
</style>

View File

@ -0,0 +1,14 @@
.mescroll-body {
position: relative; /* 下拉刷新区域相对自身定位 */
height: auto; /* 不可固定高度,否则overflow:hidden导致无法滑动; 同时使设置的最小高生效,实现列表不满屏仍可下拉*/
overflow: hidden; /* 当有元素写在mescroll-body标签前面时,可遮住下拉刷新区域 */
box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
}
/* 适配 iPhoneX */
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
.mescroll-safearea {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}

View File

@ -0,0 +1,327 @@
<template>
<view
class="mescroll-body mescroll-render-touch"
:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}"
@touchstart="wxsBiz.touchstartEvent"
@touchmove="wxsBiz.touchmoveEvent"
@touchend="wxsBiz.touchendEvent"
@touchcancel="wxsBiz.touchendEvent"
:change:prop="wxsBiz.propObserver"
:prop="wxsProp"
>
<!-- 状态栏 -->
<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
<view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp">
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
<view class="downwarp-content">
<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
<view class="downwarp-tip">{{downText}}</view>
</view>
</view>
<!-- 列表内容 -->
<slot></slot>
<!-- 空布局 -->
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-show="upLoadType===1">
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
</view>
<!-- 无数据 -->
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
</view>
</view>
<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
<!-- #ifdef H5 -->
<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
<!-- #endif -->
<!-- 适配iPhoneX -->
<view v-if="safearea" class="mescroll-safearea"></view>
<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
<!-- #ifdef MP-WEIXIN || APP-PLUS || H5 -->
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
<!-- #endif -->
</view>
</template>
<!-- 微信小程序, app, h5使用wxs -->
<!-- #ifdef MP-WEIXIN || APP-PLUS || H5-->
<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
<!-- #endif -->
<!-- app, h5使用renderjs -->
<!-- #ifdef APP-PLUS || H5 -->
<script module="renderBiz" lang="renderjs">
import renderBiz from './wxs/renderjs.js';
export default {
mixins: [renderBiz]
}
</script>
<!-- #endif -->
<script>
// mescroll-uni.js,
import MeScroll from './mescroll-uni.js';
//
import GlobalOption from './mescroll-uni-option.js';
//
import MescrollEmpty from './components/mescroll-empty.vue';
//
import MescrollTop from './components/mescroll-top.vue';
// wxs(renderjs)mixins
import WxsMixin from './wxs/mixins.js';
export default {
mixins: [WxsMixin],
components: {
MescrollEmpty,
MescrollTop
},
data() {
return {
mescroll: {optDown:{},optUp:{}}, // mescroll
downHight: 0, //:
downRate: 0, // (inOffset: rate<1; outOffset: rate>=1)
downLoadType: 0, // : 0(loading), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
upLoadType: 0, // 0loading1loading2,END3,END
isShowEmpty: false, //
isShowToTop: false, //
windowHeight: 0, // 使
windowBottom: 0, // 使
statusBarHeight: 0 //
};
},
props: {
down: Object, //
up: Object, //
top: [String, Number], // (20, "20rpx", "20px", "20%", rpx, windowHeight)
topbar: [Boolean, String], // top, false (使:,, ,,,)
bottom: [String, Number], // (20, "20rpx", "20px", "20%", rpx, windowHeight)
safearea: Boolean, // bottom, false (iPhoneX使)
height: [String, Number], // mescroll,windowHeight,使
bottombar:{ // TabBar(H5tab)
type: Boolean,
default: true
}
},
computed: {
// mescroll,windowHeight,使
minHeight(){
return this.toPx(this.height || '100%') + 'px'
},
// (px)
numTop() {
return this.toPx(this.top)
},
padTop() {
return this.numTop + 'px';
},
// (px)
numBottom() {
return this.toPx(this.bottom);
},
padBottom() {
return this.numBottom + 'px';
},
//
isDownReset() {
return this.downLoadType === 3 || this.downLoadType === 4;
},
//
transition() {
return this.isDownReset ? 'transform 300ms' : '';
},
translateY() {
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform使fixed,fixedmescroll
},
//
isDownLoading(){
return this.downLoadType === 3
},
//
downRotate(){
return 'rotate(' + 360 * this.downRate + 'deg)'
},
//
downText(){
if(!this.mescroll) return ""; //
switch (this.downLoadType){
case 1: return this.mescroll.optDown.textInOffset;
case 2: return this.mescroll.optDown.textOutOffset;
case 3: return this.mescroll.optDown.textLoading;
case 4: return this.mescroll.optDown.textLoading;
default: return this.mescroll.optDown.textInOffset;
}
}
},
methods: {
//number,rpx,upx,px,% --> px
toPx(num) {
if (typeof num === 'string') {
if (num.indexOf('px') !== -1) {
if (num.indexOf('rpx') !== -1) {
// "10rpx"
num = num.replace('rpx', '');
} else if (num.indexOf('upx') !== -1) {
// "10upx"
num = num.replace('upx', '');
} else {
// "10px"
return Number(num.replace('px', ''));
}
} else if (num.indexOf('%') !== -1) {
// ,windowHeight,"10%"windowHeight10%
let rate = Number(num.replace('%', '')) / 100;
return this.windowHeight * rate;
}
}
return num ? uni.upx2px(Number(num)) : 0;
},
//
emptyClick() {
this.$emit('emptyclick', this.mescroll);
},
//
toTopClick() {
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); //
this.$emit('topclick', this.mescroll); //
}
},
// 使createdmescroll; mountedcssH5
created() {
let vm = this;
let diyOption = {
//
down: {
inOffset() {
vm.downLoadType = 1; // offset (mescroll,)
},
outOffset() {
vm.downLoadType = 2; // offset (mescroll,)
},
onMoving(mescroll, rate, downHight) {
// ,;
vm.downHight = downHight; // (mescroll,)
vm.downRate = rate; // (inOffset: rate<1; outOffset: rate>=1)
},
showLoading(mescroll, downHight) {
vm.downLoadType = 3; // (mescroll,)
vm.downHight = downHight; // (mescroll,)
},
endDownScroll() {
vm.downLoadType = 4; // (mescroll,)
vm.downHight = 0; // (mescroll,)
if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} //
vm.downResetTimer = setTimeout(()=>{ // ,0,inOffsettextInOffset
if(vm.downLoadType === 4) vm.downLoadType = 0
},300)
},
//
callback: function(mescroll) {
vm.$emit('down', mescroll);
}
},
//
up: {
//
showLoading() {
vm.upLoadType = 1;
},
//
showNoMore() {
vm.upLoadType = 2;
},
//
hideUpScroll(mescroll) {
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
},
//
empty: {
onShow(isShow) {
//
vm.isShowEmpty = isShow;
}
},
//
toTop: {
onShow(isShow) {
//
vm.isShowToTop = isShow;
}
},
//
callback: function(mescroll) {
vm.$emit('up', mescroll);
}
}
};
MeScroll.extend(diyOption, GlobalOption); //
let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // ,props
MeScroll.extend(myOption, diyOption); //
// MeScroll
vm.mescroll = new MeScroll(myOption, true); // true,body
// initmescroll
vm.$emit('init', vm.mescroll);
//
const sys = uni.getSystemInfoSync();
if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
// 使downbottomOffset
vm.mescroll.setBodyHeight(sys.windowHeight);
// 使pagescroll,scrollTo
vm.mescroll.resetScrollTo((y, t) => {
if(typeof y === 'string'){
// view (yid,#)
setTimeout(()=>{ // view; 使$nextTick
uni.createSelectorQuery().select('#'+y).boundingClientRect(function(rect){
let top = rect.top
top += vm.mescroll.getScrollTop()
uni.pageScrollTo({
scrollTop: top,
duration: t
})
}).exec()
},30)
} else{
// (y)
uni.pageScrollTo({
scrollTop: y,
duration: t
})
}
});
// up.toTop.safearea,vuesafearea
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
vm.mescroll.optUp.toTop.safearea = vm.safearea;
}
}
};
</script>
<style>
@import "./mescroll-body.css";
@import "./components/mescroll-down.css";
@import './components/mescroll-up.css';
</style>

View File

@ -0,0 +1,65 @@
// mescroll-body 和 mescroll-uni 通用
// import MescrollUni from "./mescroll-uni.vue";
// import MescrollBody from "./mescroll-body.vue";
const MescrollMixin = {
// components: { // 非H5端无法通过mixin注册组件, 只能在main.js中注册全局组件或具体界面中注册
// MescrollUni,
// MescrollBody
// },
data() {
return {
mescroll: null //mescroll实例对象
}
},
// 注册系统自带的下拉刷新 (配置down.native为true时生效, 还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
onPullDownRefresh(){
this.mescroll && this.mescroll.onPullDownRefresh();
},
// 注册列表滚动事件,用于判定在顶部可下拉刷新,在指定位置可显示隐藏回到顶部按钮 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
onPageScroll(e) {
this.mescroll && this.mescroll.onPageScroll(e);
},
// 注册滚动到底部的事件,用于上拉加载 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
onReachBottom() {
this.mescroll && this.mescroll.onReachBottom();
},
methods: {
// mescroll组件初始化的回调,可获取到mescroll对象
mescrollInit(mescroll) {
this.mescroll = mescroll;
this.mescrollInitByRef(); // 兼容字节跳动小程序
},
// 以ref的方式初始化mescroll对象 (兼容字节跳动小程序: http://www.mescroll.com/qa.html?v=20200107#q26)
mescrollInitByRef() {
if(!this.mescroll || !this.mescroll.resetUpScroll){
let mescrollRef = this.$refs.mescrollRef;
if(mescrollRef) this.mescroll = mescrollRef.mescroll
}
},
// 下拉刷新的回调 (mixin默认resetUpScroll)
downCallback() {
if(this.mescroll.optUp.use){
this.mescroll.resetUpScroll()
}else{
setTimeout(()=>{
this.mescroll.endSuccess();
}, 500)
}
},
// 上拉加载的回调
upCallback() {
// mixin默认延时500自动结束加载
setTimeout(()=>{
this.mescroll.endErr();
}, 500)
}
},
mounted() {
this.mescrollInitByRef(); // 兼容字节跳动小程序, 避免未设置@init或@init此时未能取到ref的情况
}
}
export default MescrollMixin;

View File

@ -0,0 +1,33 @@
// 全局配置
// mescroll-body 和 mescroll-uni 通用
const GlobalOption = {
down: {
// 其他down的配置参数也可以写,这里只展示了常用的配置:
textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
textLoading: '加载中 ...', // 加载中的提示文本
offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
},
up: {
// 其他up的配置参数也可以写,这里只展示了常用的配置:
textLoading: '加载中 ...', // 加载中的提示文本
textNoMore: '-- END --', // 没有更多数据的提示文本
offset: 80, // 距底部多远时,触发upCallback
toTop: {
// 回到顶部按钮,需配置src才显示
src: "http://www.mescroll.com/img/mescroll-totop.png?v=1", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
},
empty: {
use: true, // 是否显示空布局
icon: "http://www.mescroll.com/img/mescroll-empty.png?v=1", // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
tip: '~ 空空如也 ~' // 提示
}
}
}
export default GlobalOption

View File

@ -0,0 +1,36 @@
.mescroll-uni-warp{
height: 100%;
}
.mescroll-uni-content{
height: 100%;
}
.mescroll-uni {
position: relative;
width: 100%;
height: 100%;
min-height: 200rpx;
overflow-y: auto;
box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
}
/* 定位的方式固定高度 */
.mescroll-uni-fixed{
z-index: 1;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: auto; /* 使right生效 */
height: auto; /* 使bottom生效 */
}
/* 适配 iPhoneX */
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
.mescroll-safearea {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}

View File

@ -0,0 +1,788 @@
/* mescroll
* version 1.3.1
* 2020-07-27 wenju
* http://www.mescroll.com
*/
export default function MeScroll(options, isScrollBody) {
let me = this;
me.version = '1.3.1'; // mescroll版本号
me.options = options || {}; // 配置
me.isScrollBody = isScrollBody || false; // 滚动区域是否为原生页面滚动; 默认为scroll-view
me.isDownScrolling = false; // 是否在执行下拉刷新的回调
me.isUpScrolling = false; // 是否在执行上拉加载的回调
let hasDownCallback = me.options.down && me.options.down.callback; // 是否配置了down的callback
// 初始化下拉刷新
me.initDownScroll();
// 初始化上拉加载,则初始化
me.initUpScroll();
// 自动加载
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
// 自动触发下拉刷新 (只有配置了down的callback才自动触发下拉刷新)
if ((me.optDown.use || me.optDown.native) && me.optDown.auto && hasDownCallback) {
if (me.optDown.autoShowLoading) {
me.triggerDownScroll(); // 显示下拉进度,执行下拉回调
} else {
me.optDown.callback && me.optDown.callback(me); // 不显示下拉进度,直接执行下拉回调
}
}
// 自动触发上拉加载
if(!me.isUpAutoLoad){ // 部分小程序(头条小程序)emit是异步, 会导致isUpAutoLoad判断有误, 先延时确保先执行down的callback,再执行up的callback
setTimeout(function(){
me.optUp.use && me.optUp.auto && !me.isUpAutoLoad && me.triggerUpScroll();
},100)
}
}, 30); // 需让me.optDown.inited和me.optUp.inited先执行
}
/* 配置参数:下拉刷新 */
MeScroll.prototype.extendDownScroll = function(optDown) {
// 下拉刷新的配置
MeScroll.extend(optDown, {
use: true, // 是否启用下拉刷新; 默认true
auto: true, // 是否在初始化完毕之后自动执行下拉刷新的回调; 默认true
native: false, // 是否使用系统自带的下拉刷新; 默认false; 仅mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
autoShowLoading: false, // 如果设置auto=true(在初始化完毕之后自动执行下拉刷新的回调),那么是否显示下拉刷新的进度; 默认false
isLock: false, // 是否锁定下拉刷新,默认false;
offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
startTop: 100, // scroll-view快速滚动到顶部时,此时的scroll-top可能大于0, 此值用于控制最大的误差
inOffsetRate: 1, // 在列表顶部,下拉的距离小于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
outOffsetRate: 0.2, // 在列表顶部,下拉的距离大于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
bottomOffset: 20, // 当手指touchmove位置在距离body底部20px范围内的时候结束上拉刷新,避免Webview嵌套导致touchend事件不执行
minAngle: 45, // 向下滑动最少偏移的角度,取值区间 [0,90];默认45度,即向下滑动的角度大于45度则触发下拉;而小于45度,将不触发下拉,避免与左右滑动的轮播等组件冲突;
textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
textLoading: '加载中 ...', // 加载中的提示文本
bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorTop)
textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
inited: null, // 下拉刷新初始化完毕的回调
inOffset: null, // 下拉的距离进入offset范围内那一刻的回调
outOffset: null, // 下拉的距离大于offset那一刻的回调
onMoving: null, // 下拉过程中的回调,滑动过程一直在执行; rate下拉区域当前高度与指定距离的比值(inOffset: rate<1; outOffset: rate>=1); downHight当前下拉区域的高度
beforeLoading: null, // 准备触发下拉刷新的回调: 如果return true,将不触发showLoading和callback回调; 常用来完全自定义下拉刷新, 参考案例【淘宝 v6.8.0】
showLoading: null, // 显示下拉刷新进度的回调
afterLoading: null, // 显示下拉刷新进度的回调之后,马上要执行的代码 (如: 在wxs中使用)
beforeEndDownScroll: null, // 准备结束下拉的回调. 返回结束下拉的延时执行时间,默认0ms; 常用于结束下拉之前再显示另外一小段动画,才去隐藏下拉刷新的场景, 参考案例【dotJump】
endDownScroll: null, // 结束下拉刷新的回调
afterEndDownScroll: null, // 结束下拉刷新的回调,马上要执行的代码 (如: 在wxs中使用)
callback: function(mescroll) {
// 下拉刷新的回调;默认重置上拉加载列表为第一页
mescroll.resetUpScroll();
}
})
}
/* 配置参数:上拉加载 */
MeScroll.prototype.extendUpScroll = function(optUp) {
// 上拉加载的配置
MeScroll.extend(optUp, {
use: true, // 是否启用上拉加载; 默认true
auto: true, // 是否在初始化完毕之后自动执行上拉加载的回调; 默认true
isLock: false, // 是否锁定上拉加载,默认false;
isBoth: true, // 上拉加载时,如果滑动到列表顶部是否可以同时触发下拉刷新;默认true,两者可同时触发;
callback: null, // 上拉加载的回调;function(page,mescroll){ }
page: {
num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
size: 10, // 每页数据的数量
time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
},
noMoreSize: 5, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
offset: 80, // 距底部多远时,触发upCallback
textLoading: '加载中 ...', // 加载中的提示文本
textNoMore: '-- END --', // 没有更多数据的提示文本
bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorBottom)
textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
inited: null, // 初始化完毕的回调
showLoading: null, // 显示加载中的回调
showNoMore: null, // 显示无更多数据的回调
hideUpScroll: null, // 隐藏上拉加载的回调
errDistance: 60, // endErr的时候需往上滑动一段距离,使其往下滑动时再次触发onReachBottom,仅mescroll-body生效
toTop: {
// 回到顶部按钮,需配置src才显示
src: null, // 图片路径,默认null (绝对路径或网络图)
offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000
duration: 300, // 回到顶部的动画时长,默认300ms (当值为0或300则使用系统自带回到顶部,更流畅; 其他值则通过step模拟,部分机型可能不够流畅,所以非特殊情况不建议修改此项)
btnClick: null, // 点击按钮的回调
onShow: null, // 是否显示的回调
zIndex: 9990, // fixed定位z-index值
left: null, // 到左边的距离, 默认null. 此项有值时,right不生效. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
right: 20, // 到右边的距离, 默认20 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
bottom: 120, // 到底部的距离, 默认120 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
safearea: false, // bottom的偏移量是否加上底部安全区的距离, 默认false, 需要适配iPhoneX时使用 (具体的界面如果不配置此项,则取本vue的safearea值)
width: 72, // 回到顶部图标的宽度, 默认72 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
radius: "50%" // 圆角, 默认"50%" (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
},
empty: {
use: true, // 是否显示空布局
icon: null, // 图标路径
tip: '~ 暂无相关数据 ~', // 提示
btnText: '', // 按钮
btnClick: null, // 点击按钮的回调
onShow: null, // 是否显示的回调
fixed: false, // 是否使用fixed定位,默认false; 配置fixed为true,以下的top和zIndex才生效 (transform会使fixed失效,最终会降级为absolute)
top: "100rpx", // fixed定位的top值 (完整的单位值,如 "10%"; "100rpx")
zIndex: 99 // fixed定位z-index值
},
onScroll: false // 是否监听滚动事件
})
}
/* 配置参数 */
MeScroll.extend = function(userOption, defaultOption) {
if (!userOption) return defaultOption;
for (let key in defaultOption) {
if (userOption[key] == null) {
let def = defaultOption[key];
if (def != null && typeof def === 'object') {
userOption[key] = MeScroll.extend({}, def); // 深度匹配
} else {
userOption[key] = def;
}
} else if (typeof userOption[key] === 'object') {
MeScroll.extend(userOption[key], defaultOption[key]); // 深度匹配
}
}
return userOption;
}
/* 简单判断是否配置了颜色 (非透明,非白色) */
MeScroll.prototype.hasColor = function(color) {
if(!color) return false;
let c = color.toLowerCase();
return c != "#fff" && c != "#ffffff" && c != "transparent" && c != "white"
}
/* -------初始化下拉刷新------- */
MeScroll.prototype.initDownScroll = function() {
let me = this;
// 配置参数
me.optDown = me.options.down || {};
if(!me.optDown.textColor && me.hasColor(me.optDown.bgColor)) me.optDown.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
me.extendDownScroll(me.optDown);
// 如果是mescroll-body且配置了native,则禁止自定义的下拉刷新
if(me.isScrollBody && me.optDown.native){
me.optDown.use = false
}else{
me.optDown.native = false // 仅mescroll-body支持,mescroll-uni不支持
}
me.downHight = 0; // 下拉区域的高度
// 在页面中加入下拉布局
if (me.optDown.use && me.optDown.inited) {
// 初始化完毕的回调
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
me.optDown.inited(me);
}, 0)
}
}
/* 列表touchstart事件 */
MeScroll.prototype.touchstartEvent = function(e) {
if (!this.optDown.use) return;
this.startPoint = this.getPoint(e); // 记录起点
this.startTop = this.getScrollTop(); // 记录此时的滚动条位置
this.startAngle = 0; // 初始角度
this.lastPoint = this.startPoint; // 重置上次move的点
this.maxTouchmoveY = this.getBodyHeight() - this.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
this.inTouchend = false; // 标记不是touchend
}
/* 列表touchmove事件 */
MeScroll.prototype.touchmoveEvent = function(e) {
if (!this.optDown.use) return;
let me = this;
let scrollTop = me.getScrollTop(); // 当前滚动条的距离
let curPoint = me.getPoint(e); // 当前点
let moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
// 向下拉 && 在顶部
// mescroll-body,直接判定在顶部即可
// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
if (moveY > 0 && (
(me.isScrollBody && scrollTop <= 0)
||
(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
)) {
// 可下拉的条件
if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
me.optUp.isBoth))) {
// 下拉的初始角度是否在配置的范围内
if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
if (me.startAngle < me.optDown.minAngle) return; // 如果小于配置的角度,则不往下执行下拉刷新
// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
me.inTouchend = true; // 标记执行touchend
me.touchendEvent(); // 提前触发touchend
return;
}
me.preventDefault(e); // 阻止默认事件
let diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
// 下拉距离 < 指定距离
if (me.downHight < me.optDown.offset) {
if (me.movetype !== 1) {
me.movetype = 1; // 加入标记,保证只执行一次
me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
}
me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
// 指定距离 <= 下拉距离
} else {
if (me.movetype !== 2) {
me.movetype = 2; // 加入标记,保证只执行一次
me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
}
if (diff > 0) { // 向下拉
me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
} else { // 向上收
me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
}
}
me.downHight = Math.round(me.downHight) // 取整
let rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
}
}
me.lastPoint = curPoint; // 记录本次移动的点
}
/* 列表touchend事件 */
MeScroll.prototype.touchendEvent = function(e) {
if (!this.optDown.use) return;
// 如果下拉区域高度已改变,则需重置回来
if (this.isMoveDown) {
if (this.downHight >= this.optDown.offset) {
// 符合触发刷新的条件
this.triggerDownScroll();
} else {
// 不符合的话 则重置
this.downHight = 0;
this.endDownScrollCall(this);
}
this.movetype = 0;
this.isMoveDown = false;
} else if (!this.isScrollBody && this.getScrollTop() === this.startTop) { // scroll-view到顶/左/右/底的滑动事件
let isScrollUp = this.getPoint(e).y - this.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
// 上滑
if (isScrollUp) {
// 需检查滑动的角度
let angle = this.getAngle(this.getPoint(e), this.startPoint); // 两点之间的角度,区间 [0,90]
if (angle > 80) {
// 检查并触发上拉
this.triggerUpScroll(true);
}
}
}
}
/* 根据点击滑动事件获取第一个手指的坐标 */
MeScroll.prototype.getPoint = function(e) {
if (!e) {
return {
x: 0,
y: 0
}
}
if (e.touches && e.touches[0]) {
return {
x: e.touches[0].pageX,
y: e.touches[0].pageY
}
} else if (e.changedTouches && e.changedTouches[0]) {
return {
x: e.changedTouches[0].pageX,
y: e.changedTouches[0].pageY
}
} else {
return {
x: e.clientX,
y: e.clientY
}
}
}
/* 计算两点之间的角度: 区间 [0,90]*/
MeScroll.prototype.getAngle = function(p1, p2) {
let x = Math.abs(p1.x - p2.x);
let y = Math.abs(p1.y - p2.y);
let z = Math.sqrt(x * x + y * y);
let angle = 0;
if (z !== 0) {
angle = Math.asin(y / z) / Math.PI * 180;
}
return angle
}
/* 触发下拉刷新 */
MeScroll.prototype.triggerDownScroll = function() {
if (this.optDown.beforeLoading && this.optDown.beforeLoading(this)) {
//return true则处于完全自定义状态
} else {
this.showDownScroll(); // 下拉刷新中...
!this.optDown.native && this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
}
}
/* 显示下拉进度布局 */
MeScroll.prototype.showDownScroll = function() {
this.isDownScrolling = true; // 标记下拉中
if (this.optDown.native) {
uni.startPullDownRefresh(); // 系统自带的下拉刷新
this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
} else{
this.downHight = this.optDown.offset; // 更新下拉区域高度
this.showDownLoadingCall(this.downHight); // 下拉刷新中...
}
}
MeScroll.prototype.showDownLoadingCall = function(downHight) {
this.optDown.showLoading && this.optDown.showLoading(this, downHight); // 下拉刷新中...
this.optDown.afterLoading && this.optDown.afterLoading(this, downHight); // 下拉刷新中...触发之后马上要执行的代码
}
/* 显示系统自带的下拉刷新时需要处理的业务 */
MeScroll.prototype.onPullDownRefresh = function() {
this.isDownScrolling = true; // 标记下拉中
this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
}
/* 结束下拉刷新 */
MeScroll.prototype.endDownScroll = function() {
if (this.optDown.native) { // 结束原生下拉刷新
this.isDownScrolling = false;
this.endDownScrollCall(this);
uni.stopPullDownRefresh();
return
}
let me = this;
// 结束下拉刷新的方法
let endScroll = function() {
me.downHight = 0;
me.isDownScrolling = false;
me.endDownScrollCall(me);
if(!me.isScrollBody){
me.setScrollHeight(0) // scroll-view重置滚动区域,使数据不满屏时仍可检查触发翻页
me.scrollTo(0,0) // scroll-view需重置滚动条到顶部,避免startTop大于0时,对下拉刷新的影响
}
}
// 结束下拉刷新时的回调
let delay = 0;
if (me.optDown.beforeEndDownScroll) delay = me.optDown.beforeEndDownScroll(me); // 结束下拉刷新的延时,单位ms
if (typeof delay === 'number' && delay > 0) {
setTimeout(endScroll, delay);
} else {
endScroll();
}
}
MeScroll.prototype.endDownScrollCall = function() {
this.optDown.endDownScroll && this.optDown.endDownScroll(this);
this.optDown.afterEndDownScroll && this.optDown.afterEndDownScroll(this);
}
/* 锁定下拉刷新:isLock=ture,null锁定;isLock=false解锁 */
MeScroll.prototype.lockDownScroll = function(isLock) {
if (isLock == null) isLock = true;
this.optDown.isLock = isLock;
}
/* 锁定上拉加载:isLock=ture,null锁定;isLock=false解锁 */
MeScroll.prototype.lockUpScroll = function(isLock) {
if (isLock == null) isLock = true;
this.optUp.isLock = isLock;
}
/* -------初始化上拉加载------- */
MeScroll.prototype.initUpScroll = function() {
let me = this;
// 配置参数
me.optUp = me.options.up || {use: false}
if(!me.optUp.textColor && me.hasColor(me.optUp.bgColor)) me.optUp.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
me.extendUpScroll(me.optUp);
if (me.optUp.use === false) return; // 配置不使用上拉加载时,则不初始化上拉布局
me.optUp.hasNext = true; // 如果使用上拉,则默认有下一页
me.startNum = me.optUp.page.num + 1; // 记录page开始的页码
// 初始化完毕的回调
if (me.optUp.inited) {
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
me.optUp.inited(me);
}, 0)
}
}
/*滚动到底部的事件 (仅mescroll-body生效)*/
MeScroll.prototype.onReachBottom = function() {
if (this.isScrollBody && !this.isUpScrolling) { // 只能支持下拉刷新的时候同时可以触发上拉加载,否则滚动到底部就需要上滑一点才能触发onReachBottom
if (!this.optUp.isLock && this.optUp.hasNext) {
this.triggerUpScroll();
}
}
}
/*列表滚动事件 (仅mescroll-body生效)*/
MeScroll.prototype.onPageScroll = function(e) {
if (!this.isScrollBody) return;
// 更新滚动条的位置 (主要用于判断下拉刷新时,滚动条是否在顶部)
this.setScrollTop(e.scrollTop);
// 顶部按钮的显示隐藏
if (e.scrollTop >= this.optUp.toTop.offset) {
this.showTopBtn();
} else {
this.hideTopBtn();
}
}
/*列表滚动事件*/
MeScroll.prototype.scroll = function(e, onScroll) {
// 更新滚动条的位置
this.setScrollTop(e.scrollTop);
// 更新滚动内容高度
this.setScrollHeight(e.scrollHeight);
// 向上滑还是向下滑动
if (this.preScrollY == null) this.preScrollY = 0;
this.isScrollUp = e.scrollTop - this.preScrollY > 0;
this.preScrollY = e.scrollTop;
// 上滑 && 检查并触发上拉
this.isScrollUp && this.triggerUpScroll(true);
// 顶部按钮的显示隐藏
if (e.scrollTop >= this.optUp.toTop.offset) {
this.showTopBtn();
} else {
this.hideTopBtn();
}
// 滑动监听
this.optUp.onScroll && onScroll && onScroll()
}
/* 触发上拉加载 */
MeScroll.prototype.triggerUpScroll = function(isCheck) {
if (!this.isUpScrolling && this.optUp.use && this.optUp.callback) {
// 是否校验在底部; 默认不校验
if (isCheck === true) {
let canUp = false;
// 还有下一页 && 没有锁定 && 不在下拉中
if (this.optUp.hasNext && !this.optUp.isLock && !this.isDownScrolling) {
if (this.getScrollBottom() <= this.optUp.offset) { // 到底部
canUp = true; // 标记可上拉
}
}
if (canUp === false) return;
}
this.showUpScroll(); // 上拉加载中...
this.optUp.page.num++; // 预先加一页,如果失败则减回
this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
this.num = this.optUp.page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
this.size = this.optUp.page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
this.time = this.optUp.page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
this.optUp.callback(this); // 执行回调,联网加载数据
}
}
/* 显示上拉加载中 */
MeScroll.prototype.showUpScroll = function() {
this.isUpScrolling = true; // 标记上拉加载中
this.optUp.showLoading && this.optUp.showLoading(this); // 回调
}
/* 显示上拉无更多数据 */
MeScroll.prototype.showNoMore = function() {
this.optUp.hasNext = false; // 标记无更多数据
this.optUp.showNoMore && this.optUp.showNoMore(this); // 回调
}
/* 隐藏上拉区域**/
MeScroll.prototype.hideUpScroll = function() {
this.optUp.hideUpScroll && this.optUp.hideUpScroll(this); // 回调
}
/* 结束上拉加载 */
MeScroll.prototype.endUpScroll = function(isShowNoMore) {
if (isShowNoMore != null) { // isShowNoMore=null,不处理下拉状态,下拉刷新的时候调用
if (isShowNoMore) {
this.showNoMore(); // isShowNoMore=true,显示无更多数据
} else {
this.hideUpScroll(); // isShowNoMore=false,隐藏上拉加载
}
}
this.isUpScrolling = false; // 标记结束上拉加载
}
/*
*isShowLoading 是否显示进度布局;
* 1.默认null,不传参,则显示上拉加载的进度布局
* 2.传参true, 则显示下拉刷新的进度布局
* 3.传参false,则不显示上拉和下拉的进度 (常用于静默更新列表数据)
*/
MeScroll.prototype.resetUpScroll = function(isShowLoading) {
if (this.optUp && this.optUp.use) {
let page = this.optUp.page;
this.prePageNum = page.num; // 缓存重置前的页码,加载失败可退回
this.prePageTime = page.time; // 缓存重置前的时间,加载失败可退回
page.num = this.startNum; // 重置为第一页
page.time = null; // 重置时间为空
if (!this.isDownScrolling && isShowLoading !== false) { // 如果不是下拉刷新触发的resetUpScroll并且不配置列表静默更新,则显示进度;
if (isShowLoading == null) {
this.removeEmpty(); // 移除空布局
this.showUpScroll(); // 不传参,默认显示上拉加载的进度布局
} else {
this.showDownScroll(); // 传true,显示下拉刷新的进度布局,不清空列表
}
}
this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
this.num = page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
this.size = page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
this.time = page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
this.optUp.callback && this.optUp.callback(this); // 执行上拉回调
}
}
/* 设置page.num的值 */
MeScroll.prototype.setPageNum = function(num) {
this.optUp.page.num = num - 1;
}
/* 设置page.size的值 */
MeScroll.prototype.setPageSize = function(size) {
this.optUp.page.size = size;
}
/* ,
* dataSize: 当前页的数据量(必传)
* totalPage: 总页数(必传)
* systime: 服务器时间 (可空)
*/
MeScroll.prototype.endByPage = function(dataSize, totalPage, systime) {
let hasNext;
if (this.optUp.use && totalPage != null) hasNext = this.optUp.page.num < totalPage; // 是否还有下一页
this.endSuccess(dataSize, hasNext, systime);
}
/* ,
* dataSize: 当前页的数据量(必传)
* totalSize: 列表所有数据总数量(必传)
* systime: 服务器时间 (可空)
*/
MeScroll.prototype.endBySize = function(dataSize, totalSize, systime) {
let hasNext;
if (this.optUp.use && totalSize != null) {
let loadSize = (this.optUp.page.num - 1) * this.optUp.page.size + dataSize; // 已加载的数据总数
hasNext = loadSize < totalSize; // 是否还有下一页
}
this.endSuccess(dataSize, hasNext, systime);
}
/* ,
* dataSize: 当前页的数据个数(不是所有页的数据总和),用于上拉加载判断是否还有下一页.如果不传,则会判断还有下一页
* hasNext: 是否还有下一页,布尔类型;用来解决这个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据dataSize判断,则需翻到第三页才会知道无更多数据,如果传了hasNext,则翻到第二页即可显示无更多数据.
* systime: 服务器时间(可空);用来解决这个小问题:当准备翻下一页时,数据库新增了几条记录,此时翻下一页,前面的几条数据会和上一页的重复;这里传入了systime,那么upCallback的page.time就会有值,把page.time传给服务器,让后台过滤新加入的那几条记录
*/
MeScroll.prototype.endSuccess = function(dataSize, hasNext, systime) {
let me = this;
// 结束下拉刷新
if (me.isDownScrolling) me.endDownScroll();
// 结束上拉加载
if (me.optUp.use) {
let isShowNoMore; // 是否已无更多数据
if (dataSize != null) {
let pageNum = me.optUp.page.num; // 当前页码
let pageSize = me.optUp.page.size; // 每页长度
// 如果是第一页
if (pageNum === 1) {
if (systime) me.optUp.page.time = systime; // 设置加载列表数据第一页的时间
}
if (dataSize < pageSize || hasNext === false) {
// 返回的数据不满一页时,则说明已无更多数据
me.optUp.hasNext = false;
if (dataSize === 0 && pageNum === 1) {
// 如果第一页无任何数据且配置了空布局
isShowNoMore = false;
me.showEmpty();
} else {
// 总列表数少于配置的数量,则不显示无更多数据
let allDataSize = (pageNum - 1) * pageSize + dataSize;
if (allDataSize < me.optUp.noMoreSize) {
isShowNoMore = false;
} else {
isShowNoMore = true;
}
me.removeEmpty(); // 移除空布局
}
} else {
// 还有下一页
isShowNoMore = false;
me.optUp.hasNext = true;
me.removeEmpty(); // 移除空布局
}
}
// 隐藏上拉
me.endUpScroll(isShowNoMore);
}
}
/* 回调失败,结束下拉刷新和上拉加载 */
MeScroll.prototype.endErr = function(errDistance) {
// 结束下拉,回调失败重置回原来的页码和时间
if (this.isDownScrolling) {
let page = this.optUp.page;
if (page && this.prePageNum) {
page.num = this.prePageNum;
page.time = this.prePageTime;
}
this.endDownScroll();
}
// 结束上拉,回调失败重置回原来的页码
if (this.isUpScrolling) {
this.optUp.page.num--;
this.endUpScroll(false);
// 如果是mescroll-body,则需往回滚一定距离
if(this.isScrollBody && errDistance !== 0){ // 不处理0
if(!errDistance) errDistance = this.optUp.errDistance; // 不传,则取默认
this.scrollTo(this.getScrollTop() - errDistance, 0) // 往上回滚的距离
}
}
}
/* 显示空布局 */
MeScroll.prototype.showEmpty = function() {
this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(true)
}
/* 移除空布局 */
MeScroll.prototype.removeEmpty = function() {
this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(false)
}
/* 显示回到顶部的按钮 */
MeScroll.prototype.showTopBtn = function() {
if (!this.topBtnShow) {
this.topBtnShow = true;
this.optUp.toTop.onShow && this.optUp.toTop.onShow(true);
}
}
/* 隐藏回到顶部的按钮 */
MeScroll.prototype.hideTopBtn = function() {
if (this.topBtnShow) {
this.topBtnShow = false;
this.optUp.toTop.onShow && this.optUp.toTop.onShow(false);
}
}
/* 获取滚动条的位置 */
MeScroll.prototype.getScrollTop = function() {
return this.scrollTop || 0
}
/* 记录滚动条的位置 */
MeScroll.prototype.setScrollTop = function(y) {
this.scrollTop = y;
}
/* 滚动到指定位置 */
MeScroll.prototype.scrollTo = function(y, t) {
this.myScrollTo && this.myScrollTo(y, t) // scrollview需自定义回到顶部方法
}
/* 自定义scrollTo */
MeScroll.prototype.resetScrollTo = function(myScrollTo) {
this.myScrollTo = myScrollTo
}
/* 滚动条到底部的距离 */
MeScroll.prototype.getScrollBottom = function() {
return this.getScrollHeight() - this.getClientHeight() - this.getScrollTop()
}
/*
star: 开始值
end: 结束值
callback(step,timer): 回调step值,计步器timer,可自行通过window.clearInterval(timer)结束计步器;
t: 计步时长,传0则直接回调end值;不传则默认300ms
rate: 周期;不传则默认30ms计步一次
* */
MeScroll.prototype.getStep = function(star, end, callback, t, rate) {
let diff = end - star; // 差值
if (t === 0 || diff === 0) {
callback && callback(end);
return;
}
t = t || 300; // 时长 300ms
rate = rate || 30; // 周期 30ms
let count = t / rate; // 次数
let step = diff / count; // 步长
let i = 0; // 计数
let timer = setInterval(function() {
if (i < count - 1) {
star += step;
callback && callback(star, timer);
i++;
} else {
callback && callback(end, timer); // 最后一次直接设置end,避免计算误差
clearInterval(timer);
}
}, rate);
}
/* 滚动容器的高度 */
MeScroll.prototype.getClientHeight = function(isReal) {
let h = this.clientHeight || 0
if (h === 0 && isReal !== true) { // 未获取到容器的高度,可临时取body的高度 (可能会有误差)
h = this.getBodyHeight()
}
return h
}
MeScroll.prototype.setClientHeight = function(h) {
this.clientHeight = h;
}
/* 滚动内容的高度 */
MeScroll.prototype.getScrollHeight = function() {
return this.scrollHeight || 0;
}
MeScroll.prototype.setScrollHeight = function(h) {
this.scrollHeight = h;
}
/* body的高度 */
MeScroll.prototype.getBodyHeight = function() {
return this.bodyHeight || 0;
}
MeScroll.prototype.setBodyHeight = function(h) {
this.bodyHeight = h;
}
/* 阻止浏览器默认滚动事件 */
MeScroll.prototype.preventDefault = function(e) {
// 小程序不支持e.preventDefault, 已在wxs中禁止
// app的bounce只能通过配置pages.json的style.app-plus.bounce为"none"来禁止, 或使用renderjs禁止
// cancelable:是否可以被禁用; defaultPrevented:是否已经被禁用
if (e && e.cancelable && !e.defaultPrevented) e.preventDefault()
}

View File

@ -0,0 +1,413 @@
<template>
<view class="mescroll-uni-warp">
<scroll-view :id="viewId" class="mescroll-uni" :class="{'mescroll-uni-fixed':isFixed}" :style="{'height':scrollHeight,'padding-top':padTop,'padding-bottom':padBottom,'top':fixedTop,'bottom':fixedBottom}" :scroll-top="scrollTop" :scroll-into-view="scrollToViewId" :scroll-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true">
<view class="mescroll-uni-content mescroll-render-touch"
@touchstart="wxsBiz.touchstartEvent"
@touchmove="wxsBiz.touchmoveEvent"
@touchend="wxsBiz.touchendEvent"
@touchcancel="wxsBiz.touchendEvent"
:change:prop="wxsBiz.propObserver"
:prop="wxsProp">
<!-- 状态栏 -->
<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
<view class="mescroll-wxs-content" :style="{'transform': translateY, 'transition': transition}" :change:prop="wxsBiz.callObserver" :prop="callProp">
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
<view class="downwarp-content">
<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view>
<view class="downwarp-tip">{{downText}}</view>
</view>
</view>
<!-- 列表内容 -->
<slot></slot>
<!-- 空布局 -->
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}">
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
<view v-show="upLoadType===1">
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view>
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view>
</view>
<!-- 无数据 -->
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view>
</view>
</view>
<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
<!-- #ifdef H5 -->
<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
<!-- #endif -->
<!-- 适配iPhoneX -->
<view v-if="safearea" class="mescroll-safearea"></view>
</view>
</scroll-view>
<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
<!-- #ifdef MP-WEIXIN || APP-PLUS || H5 -->
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
<!-- #endif -->
</view>
</template>
<!-- 微信小程序, app, h5使用wxs -->
<!-- #ifdef MP-WEIXIN || APP-PLUS || H5-->
<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
<!-- #endif -->
<!-- app, h5使用renderjs -->
<!-- #ifdef APP-PLUS || H5 -->
<script module="renderBiz" lang="renderjs">
import renderBiz from './wxs/renderjs.js';
export default {
mixins:[renderBiz]
}
</script>
<!-- #endif -->
<script>
// mescroll-uni.js,
import MeScroll from './mescroll-uni.js';
//
import GlobalOption from './mescroll-uni-option.js';
//
import MescrollEmpty from './components/mescroll-empty.vue';
//
import MescrollTop from './components/mescroll-top.vue';
// wxs(renderjs)mixins
import WxsMixin from './wxs/mixins.js';
export default {
mixins: [WxsMixin],
components: {
MescrollEmpty,
MescrollTop
},
data() {
return {
mescroll: {optDown:{},optUp:{}}, // mescroll
viewId: 'id_' + Math.random().toString(36).substr(2,16), // mescrollid(,)
downHight: 0, //:
downRate: 0, // (inOffset: rate<1; outOffset: rate>=1)
downLoadType: 0, // : 0(loading), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
upLoadType: 0, // : 0(loading), 1loading, 2,END, 3(,END)
isShowEmpty: false, //
isShowToTop: false, //
scrollTop: 0, //
scrollAnim: false, //
windowTop: 0, // 使
windowBottom: 0, // 使
windowHeight: 0, // 使
statusBarHeight: 0, //
scrollToViewId: '' // viewid
}
},
props: {
down: Object, //
up: Object, //
top: [String, Number], // (20, "20rpx", "20px", "20%", rpx, windowHeight)
topbar: [Boolean, String], // top, false (使:,, ,,,)
bottom: [String, Number], // (20, "20rpx", "20px", "20%", rpx, windowHeight)
safearea: Boolean, // bottom, false (iPhoneX使)
fixed: { // fixedmescroll, true
type: Boolean,
default: true
},
height: [String, Number], // mescroll, ,使fixed. (20, "20rpx", "20px", "20%", rpx, windowHeight)
bottombar:{ // TabBar(H5tab)
type: Boolean,
default: true
}
},
computed: {
// 使fixed (height,使)
isFixed(){
return !this.height && this.fixed
},
// mescroll
scrollHeight(){
if (this.isFixed) {
return "auto"
} else if(this.height){
return this.toPx(this.height) + 'px'
}else{
return "100%"
}
},
// (px)
numTop() {
return this.toPx(this.top)
},
fixedTop() {
return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0
},
padTop() {
return !this.isFixed ? this.numTop + 'px' : 0
},
// (px)
numBottom() {
return this.toPx(this.bottom)
},
fixedBottom() {
return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0
},
padBottom() {
return !this.isFixed ? this.numBottom + 'px' : 0
},
//
isDownReset(){
return this.downLoadType===3 || this.downLoadType===4
},
//
transition() {
return this.isDownReset ? 'transform 300ms' : '';
},
translateY() {
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform使fixed,fixedmescroll
},
//
scrollable(){
return this.downLoadType===0 || this.isDownReset
},
//
isDownLoading(){
return this.downLoadType === 3
},
//
downRotate(){
return 'rotate(' + 360 * this.downRate + 'deg)'
},
//
downText(){
if(!this.mescroll) return ""; //
switch (this.downLoadType){
case 1: return this.mescroll.optDown.textInOffset;
case 2: return this.mescroll.optDown.textOutOffset;
case 3: return this.mescroll.optDown.textLoading;
case 4: return this.mescroll.optDown.textLoading;
default: return this.mescroll.optDown.textInOffset;
}
}
},
methods: {
//number,rpx,upx,px,% --> px
toPx(num){
if(typeof num === "string"){
if (num.indexOf('px') !== -1) {
if(num.indexOf('rpx') !== -1) { // "10rpx"
num = num.replace('rpx', '');
} else if(num.indexOf('upx') !== -1) { // "10upx"
num = num.replace('upx', '');
} else { // "10px"
return Number(num.replace('px', ''))
}
}else if (num.indexOf('%') !== -1){
// ,windowHeight,"10%"windowHeight10%
let rate = Number(num.replace("%","")) / 100
return this.windowHeight * rate
}
}
return num ? uni.upx2px(Number(num)) : 0
},
//,
scroll(e) {
this.mescroll.scroll(e.detail, () => {
this.$emit('scroll', this.mescroll) // this.mescroll.scrollTop; this.mescroll.isScrollUp
})
},
//
emptyClick() {
this.$emit('emptyclick', this.mescroll)
},
//
toTopClick() {
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); //
this.$emit('topclick', this.mescroll); //
},
// (使,)
setClientHeight() {
if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) {
this.isExec = true; //
this.$nextTick(() => { // dom
let query = uni.createSelectorQuery();
// #ifndef MP-ALIPAY
query = query.in(this) // in(this),in(this),
// #endif
let view = query.select('#' + this.viewId);
view.boundingClientRect(data => {
this.isExec = false;
if (data) {
this.mescroll.setClientHeight(data.height);
} else if (this.clientNum != 3) { // ,dom,,3
this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1;
setTimeout(() => {
this.setClientHeight()
}, this.clientNum * 100)
}
}).exec();
})
}
}
},
// 使createdmescroll; mountedcssH5
created() {
let vm = this;
let diyOption = {
//
down: {
inOffset() {
vm.downLoadType = 1; // offset (mescroll,)
},
outOffset() {
vm.downLoadType = 2; // offset (mescroll,)
},
onMoving(mescroll, rate, downHight) {
// ,;
vm.downHight = downHight; // (mescroll,)
vm.downRate = rate; // (inOffset: rate<1; outOffset: rate>=1)
},
showLoading(mescroll, downHight) {
vm.downLoadType = 3; // (mescroll,)
vm.downHight = downHight; // (mescroll,)
},
endDownScroll() {
vm.downLoadType = 4; // (mescroll,)
vm.downHight = 0; // (mescroll,)
vm.downResetTimer && clearTimeout(vm.downResetTimer)
vm.downResetTimer = setTimeout(()=>{ // ,0,便this.transition,iOS
if(vm.downLoadType===4) vm.downLoadType = 0
},300)
},
//
callback: function(mescroll) {
vm.$emit('down', mescroll)
}
},
//
up: {
//
showLoading() {
vm.upLoadType = 1;
},
//
showNoMore() {
vm.upLoadType = 2;
},
//
hideUpScroll(mescroll) {
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
},
//
empty: {
onShow(isShow) { //
vm.isShowEmpty = isShow;
}
},
//
toTop: {
onShow(isShow) { //
vm.isShowToTop = isShow;
}
},
//
callback: function(mescroll) {
vm.$emit('up', mescroll);
// (mescroll)
vm.setClientHeight()
}
}
}
MeScroll.extend(diyOption, GlobalOption); //
let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // ,props
MeScroll.extend(myOption, diyOption); //
// MeScroll
vm.mescroll = new MeScroll(myOption);
vm.mescroll.viewId = vm.viewId; // id
// initmescroll
vm.$emit('init', vm.mescroll);
//
const sys = uni.getSystemInfoSync();
if(sys.windowTop) vm.windowTop = sys.windowTop;
if(sys.windowBottom) vm.windowBottom = sys.windowBottom;
if(sys.windowHeight) vm.windowHeight = sys.windowHeight;
if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
// 使downbottomOffset
vm.mescroll.setBodyHeight(sys.windowHeight);
// 使scrollview,scrollTo
vm.mescroll.resetScrollTo((y, t) => {
vm.scrollAnim = (t !== 0); // t0,使
if(typeof y === 'string'){ // ,使scroll-into-view
// #ifdef MP-WEIXIN
// slotscroll-into-view,
uni.createSelectorQuery().select('#'+vm.viewId).boundingClientRect(function(rect){
let mescrollTop = rect.top // mescroll
uni.createSelectorQuery().select('#'+y).boundingClientRect(function(rect){
let curY = vm.mescroll.getScrollTop()
let top = rect.top - mescrollTop
top += curY
if(!vm.isFixed) top -= vm.numTop
vm.scrollTop = curY;
vm.$nextTick(function() {
vm.scrollTop = top
})
}).exec()
}).exec()
// #endif
// #ifndef MP-WEIXIN
if (vm.scrollToViewId != y) {
vm.scrollToViewId = y;
} else{
vm.scrollToViewId = ''; // scrollToViewId,
vm.$nextTick(function(){
vm.scrollToViewId = y;
})
}
// #endif
return;
}
let curY = vm.mescroll.getScrollTop()
if (t === 0 || t === 300) { // t使300,使
vm.scrollTop = curY;
vm.$nextTick(function() {
vm.scrollTop = y
})
} else {
vm.mescroll.getStep(curY, y, step => { // t
vm.scrollTop = step
}, t)
}
})
// up.toTop.safearea,vuesafearea
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
vm.mescroll.optUp.toTop.safearea = vm.safearea;
}
},
mounted() {
//
this.setClientHeight()
}
}
</script>
<style>
@import "./mescroll-uni.css";
@import "./components/mescroll-down.css";
@import './components/mescroll-up.css';
</style>

View File

@ -0,0 +1,50 @@
/**
* mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期:
* 当一个页面只有一个mescroll-body写在子组件时, 则使用mescroll-comp.js (参考 mescroll-comp 案例)
* 当一个页面有多个mescroll-body写在子组件时, 则使用mescroll-more.js (参考 mescroll-more 案例)
*/
const MescrollCompMixin = {
// 因为子组件无onPageScroll和onReachBottom的页面生命周期需在页面传递进到子组件 (一级)
onPageScroll(e) {
this.handlePageScroll(e)
},
onReachBottom() {
this.handleReachBottom()
},
// 当down的native: true时, 还需传递此方法进到子组件
onPullDownRefresh(){
this.handlePullDownRefresh()
},
// mescroll-body写在子子子...组件的情况 (多级)
data() {
return {
mescroll: {
onPageScroll: e=>{
this.handlePageScroll(e)
},
onReachBottom: ()=>{
this.handleReachBottom()
},
onPullDownRefresh: ()=>{
this.handlePullDownRefresh()
}
}
}
},
methods:{
handlePageScroll(e){
let item = this.$refs["mescrollItem"];
if(item && item.mescroll) item.mescroll.onPageScroll(e);
},
handleReachBottom(){
let item = this.$refs["mescrollItem"];
if(item && item.mescroll) item.mescroll.onReachBottom();
},
handlePullDownRefresh(){
let item = this.$refs["mescrollItem"];
if(item && item.mescroll) item.mescroll.onPullDownRefresh();
}
}
}
export default MescrollCompMixin;

View File

@ -0,0 +1,51 @@
/**
* mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例)
*/
const MescrollMoreItemMixin = {
// 支付宝小程序不支持props的mixin,需写在具体的页面中
// #ifndef MP-ALIPAY
props:{
i: Number, // 每个tab页的专属下标
index: { // 当前tab的下标
type: Number,
default(){
return 0
}
}
},
// #endif
data() {
return {
downOption:{
auto:false // 不自动加载
},
upOption:{
auto:false // 不自动加载
},
isInit: false // 当前tab是否已初始化
}
},
watch:{
// 监听下标的变化
index(val){
if (this.i === val && !this.isInit) {
this.isInit = true; // 标记为true
this.mescroll && this.mescroll.triggerDownScroll();
}
}
},
methods: {
// mescroll组件初始化的回调,可获取到mescroll对象 (覆盖mescroll-mixins.js的mescrollInit, 为了标记isInit)
mescrollInit(mescroll) {
this.mescroll = mescroll;
this.mescrollInitByRef && this.mescrollInitByRef(); // 兼容字节跳动小程序
// 自动加载当前tab的数据
if(this.i === this.index){
this.isInit = true; // 标记为true
this.mescroll.triggerDownScroll();
}
},
}
}
export default MescrollMoreItemMixin;

View File

@ -0,0 +1,56 @@
/**
* mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期:
* 当一个页面只有一个mescroll-body写在子组件时, 则使用mescroll-comp.js (参考 mescroll-comp 案例)
* 当一个页面有多个mescroll-body写在子组件时, 则使用mescroll-more.js (参考 mescroll-more 案例)
*/
const MescrollMoreMixin = {
data() {
return {
tabIndex: 0 // 当前tab下标
}
},
// 因为子组件无onPageScroll和onReachBottom的页面生命周期需在页面传递进到子组件
onPageScroll(e) {
let mescroll = this.getMescroll(this.tabIndex);
mescroll && mescroll.onPageScroll(e);
},
onReachBottom() {
let mescroll = this.getMescroll(this.tabIndex);
mescroll && mescroll.onReachBottom();
},
// 当down的native: true时, 还需传递此方法进到子组件
onPullDownRefresh(){
let mescroll = this.getMescroll(this.tabIndex);
mescroll && mescroll.onPullDownRefresh();
},
methods:{
// 根据下标获取对应子组件的mescroll
getMescroll(i){
if(!this.mescrollItems) this.mescrollItems = [];
if(!this.mescrollItems[i]) {
// v-for中的refs
let vForItem = this.$refs["mescrollItem"];
if(vForItem){
this.mescrollItems[i] = vForItem[i]
}else{
// 普通的refs,不可重复
this.mescrollItems[i] = this.$refs["mescrollItem"+i];
}
}
let item = this.mescrollItems[i]
return item ? item.mescroll : null
},
// 切换tab,恢复滚动条位置
tabChange(i){
let mescroll = this.getMescroll(i);
if(mescroll){
// 延时(比$nextTick靠谱一些),确保元素已渲染
setTimeout(()=>{
mescroll.scrollTo(mescroll.getScrollTop(),0)
},30)
}
}
}
}
export default MescrollMoreMixin;

View File

@ -0,0 +1,102 @@
// 定义在wxs (含renderjs) 逻辑层的数据和方法, 与视图层相互通信
const WxsMixin = {
data() {
return {
// 传入wxs视图层的数据 (响应式)
wxsProp: {
optDown:{}, // 下拉刷新的配置
scrollTop:0, // 滚动条的距离
bodyHeight:0, // body的高度
isDownScrolling:false, // 是否正在下拉刷新中
isUpScrolling:false, // 是否正在上拉加载中
isScrollBody:true, // 是否为mescroll-body滚动
isUpBoth:true, // 上拉加载时,是否同时可以下拉刷新
t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
},
// 标记调用wxs视图层的方法
callProp: {
callType: '', // 方法名
t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
},
// 不用wxs的平台使用此处的wxsBiz对象,抹平wxs的写法 (微信小程序和APP使用的wxsBiz对象是./wxs/wxs.wxs)
// #ifndef MP-WEIXIN || APP-PLUS || H5
wxsBiz: {
//注册列表touchstart事件,用于下拉刷新
touchstartEvent: e=> {
this.mescroll.touchstartEvent(e);
},
//注册列表touchmove事件,用于下拉刷新
touchmoveEvent: e=> {
this.mescroll.touchmoveEvent(e);
},
//注册列表touchend事件,用于下拉刷新
touchendEvent: e=> {
this.mescroll.touchendEvent(e);
},
propObserver(){}, // 抹平wxs的写法
callObserver(){} // 抹平wxs的写法
},
// #endif
// 不用renderjs的平台使用此处的renderBiz对象,抹平renderjs的写法 (app 和 h5 使用的renderBiz对象是./wxs/renderjs.js)
// #ifndef APP-PLUS || H5
renderBiz: {
propObserver(){} // 抹平renderjs的写法
}
// #endif
}
},
methods: {
// wxs视图层调用逻辑层的回调
wxsCall(msg){
if(msg.type === 'setWxsProp'){
// 更新wxsProp数据 (值改变才触发更新)
this.wxsProp = {
optDown: this.mescroll.optDown,
scrollTop: this.mescroll.getScrollTop(),
bodyHeight: this.mescroll.getBodyHeight(),
isDownScrolling: this.mescroll.isDownScrolling,
isUpScrolling: this.mescroll.isUpScrolling,
isUpBoth: this.mescroll.optUp.isBoth,
isScrollBody:this.mescroll.isScrollBody,
t: Date.now()
}
}else if(msg.type === 'setLoadType'){
// 设置inOffset,outOffset的状态
this.downLoadType = msg.downLoadType
}else if(msg.type === 'triggerDownScroll'){
// 主动触发下拉刷新
this.mescroll.triggerDownScroll();
}else if(msg.type === 'endDownScroll'){
// 结束下拉刷新
this.mescroll.endDownScroll();
}else if(msg.type === 'triggerUpScroll'){
// 主动触发上拉加载
this.mescroll.triggerUpScroll(true);
}
}
},
mounted() {
// #ifdef MP-WEIXIN || APP-PLUS || H5
// 配置主动触发wxs显示加载进度的回调
this.mescroll.optDown.afterLoading = ()=>{
this.callProp = {callType: "showLoading", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
}
// 配置主动触发wxs隐藏加载进度的回调
this.mescroll.optDown.afterEndDownScroll = ()=>{
this.callProp = {callType: "endDownScroll", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
setTimeout(()=>{
if(this.downLoadType === 4 || this.downLoadType === 0){
this.callProp = {callType: "clearTransform", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
}
},320)
}
// 初始化wxs的数据
this.wxsCall({type: 'setWxsProp'})
// #endif
}
}
export default WxsMixin;

View File

@ -0,0 +1,92 @@
// 使用renderjs直接操作window对象,实现动态控制app和h5的bounce
// bounce: iOS橡皮筋,Android半月弧,h5浏览器下拉背景等效果 (下拉刷新时禁止)
// https://uniapp.dcloud.io/frame?id=renderjs
// 与wxs的me实例一致
var me = {}
// 初始化window对象的touch事件 (仅初始化一次)
if(window && !window.$mescrollRenderInit){
window.$mescrollRenderInit = true
window.addEventListener('touchstart', function(e){
if (me.disabled()) return;
me.startPoint = me.getPoint(e); // 记录起点
}, {passive: true})
window.addEventListener('touchmove', function(e){
if (me.disabled()) return;
if (me.getScrollTop() > 0) return; // 需在顶部下拉,才禁止bounce
var curPoint = me.getPoint(e); // 当前点
var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
// 向下拉
if (moveY > 0) {
// 可下拉的条件
if (!me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling && me.isUpBoth))) {
// 只有touch在mescroll的view上面,才禁止bounce
var el = e.target;
var isMescrollTouch = false;
while (el && el.tagName && el.tagName !== 'UNI-PAGE-BODY' && el.tagName != "BODY") {
var cls = el.classList;
if (cls && cls.contains('mescroll-render-touch')) {
isMescrollTouch = true
break;
}
el = el.parentNode; // 继续检查其父元素
}
// 禁止bounce (不会对swiper和iOS侧滑返回造成影响)
if (isMescrollTouch && e.cancelable && !e.defaultPrevented) e.preventDefault();
}
}
}, {passive: false})
}
/* 获取滚动条的位置 */
me.getScrollTop = function() {
return me.scrollTop || 0
}
/* 是否禁用下拉刷新 */
me.disabled = function(){
return !me.optDown || !me.optDown.use || me.optDown.native
}
/* 根据点击滑动事件获取第一个手指的坐标 */
me.getPoint = function(e) {
if (!e) {
return {x: 0,y: 0}
}
if (e.touches && e.touches[0]) {
return {x: e.touches[0].pageX,y: e.touches[0].pageY}
} else if (e.changedTouches && e.changedTouches[0]) {
return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
} else {
return {x: e.clientX,y: e.clientY}
}
}
/**
* 监听逻辑层数据的变化 (实时更新数据)
*/
function propObserver(wxsProp) {
me.optDown = wxsProp.optDown
me.scrollTop = wxsProp.scrollTop
me.isDownScrolling = wxsProp.isDownScrolling
me.isUpScrolling = wxsProp.isUpScrolling
me.isUpBoth = wxsProp.isUpBoth
}
/* 导出模块 */
const renderBiz = {
data() {
return {
propObserver: propObserver,
}
}
}
export default renderBiz;

View File

@ -0,0 +1,268 @@
// 使用wxs处理交互动画, 提高性能, 同时避免小程序bounce对下拉刷新的影响
// https://uniapp.dcloud.io/frame?id=wxs
// https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html
// 模拟mescroll实例, 与mescroll.js的写法尽量保持一致
var me = {}
// ------ 自定义下拉刷新动画 start ------
/* 下拉过程中的回调,滑动过程一直在执行 (rate<1为inOffset; rate>1为outOffset) */
me.onMoving = function (ins, rate, downHight){
ins.requestAnimationFrame(function () {
ins.selectComponent('.mescroll-wxs-content').setStyle({
'will-change': 'transform', // 可解决下拉过程中, image和swiper脱离文档流的问题
'transform': 'translateY(' + downHight + 'px)',
'transition': ''
})
// 环形进度条
var progress = ins.selectComponent('.mescroll-wxs-progress')
progress && progress.setStyle({transform: 'rotate(' + 360 * rate + 'deg)'})
})
}
/* 显示下拉刷新进度 */
me.showLoading = function (ins){
me.downHight = me.optDown.offset
ins.requestAnimationFrame(function () {
ins.selectComponent('.mescroll-wxs-content').setStyle({
'will-change': 'auto',
'transform': 'translateY(' + me.downHight + 'px)',
'transition': 'transform 300ms'
})
})
}
/* 结束下拉 */
me.endDownScroll = function (ins){
me.downHight = 0;
me.isDownScrolling = false;
ins.requestAnimationFrame(function () {
ins.selectComponent('.mescroll-wxs-content').setStyle({
'will-change': 'auto',
'transform': 'translateY(0)', // 不可以写空串,否则scroll-view渲染不完整 (延时350ms会调clearTransform置空)
'transition': 'transform 300ms'
})
})
}
/* 结束下拉动画执行完毕后, 清除transform和transition, 避免对列表内容样式造成影响, 如: h5的list-msg示例下拉进度条漏出来等 */
me.clearTransform = function (ins){
ins.requestAnimationFrame(function () {
ins.selectComponent('.mescroll-wxs-content').setStyle({
'will-change': '',
'transform': '',
'transition': ''
})
})
}
// ------ 自定义下拉刷新动画 end ------
/**
* 监听逻辑层数据的变化 (实时更新数据)
*/
function propObserver(wxsProp) {
me.optDown = wxsProp.optDown
me.scrollTop = wxsProp.scrollTop
me.bodyHeight = wxsProp.bodyHeight
me.isDownScrolling = wxsProp.isDownScrolling
me.isUpScrolling = wxsProp.isUpScrolling
me.isUpBoth = wxsProp.isUpBoth
me.isScrollBody = wxsProp.isScrollBody
me.startTop = wxsProp.scrollTop // 及时更新touchstart触发的startTop, 避免scroll-view快速惯性滚动到顶部取值不准确
}
/**
* 监听逻辑层数据的变化 (调用wxs的方法)
*/
function callObserver(callProp, oldValue, ins) {
if (me.disabled()) return;
if(callProp.callType){
// 逻辑层App Service的style已失效,需在视图层Webview设置style
if(callProp.callType === 'showLoading'){
me.showLoading(ins)
}else if(callProp.callType === 'endDownScroll'){
me.endDownScroll(ins)
}else if(callProp.callType === 'clearTransform'){
me.clearTransform(ins)
}
}
}
/**
* touch事件
*/
function touchstartEvent(e, ins) {
me.downHight = 0; // 下拉的距离
me.startPoint = me.getPoint(e); // 记录起点
me.startTop = me.getScrollTop(); // 记录此时的滚动条位置
me.startAngle = 0; // 初始角度
me.lastPoint = me.startPoint; // 重置上次move的点
me.maxTouchmoveY = me.getBodyHeight() - me.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
me.inTouchend = false; // 标记不是touchend
me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
}
function touchmoveEvent(e, ins) {
var isPrevent = true // false表示不往上冒泡相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
if (me.disabled()) return isPrevent;
var scrollTop = me.getScrollTop(); // 当前滚动条的距离
var curPoint = me.getPoint(e); // 当前点
var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
// 向下拉 && 在顶部
// mescroll-body,直接判定在顶部即可
// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
if (moveY > 0 && (
(me.isScrollBody && scrollTop <= 0)
||
(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
)) {
// 可下拉的条件
if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
me.isUpBoth))) {
// 下拉的角度是否在配置的范围内
if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
if (me.startAngle < me.optDown.minAngle) return isPrevent; // 如果小于配置的角度,则不往下执行下拉刷新
// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
me.inTouchend = true; // 标记执行touchend
touchendEvent(e, ins); // 提前触发touchend
return isPrevent;
}
isPrevent = false // 小程序是return false
var diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
// 下拉距离 < 指定距离
if (me.downHight < me.optDown.offset) {
if (me.movetype !== 1) {
me.movetype = 1; // 加入标记,保证只执行一次
// me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
me.callMethod(ins, {type: 'setLoadType', downLoadType: 1})
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
}
me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
// 指定距离 <= 下拉距离
} else {
if (me.movetype !== 2) {
me.movetype = 2; // 加入标记,保证只执行一次
// me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
me.callMethod(ins, {type: 'setLoadType', downLoadType: 2})
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
}
if (diff > 0) { // 向下拉
me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
} else { // 向上收
me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
}
}
me.downHight = Math.round(me.downHight) // 取整
var rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
// me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
me.onMoving(ins, rate, me.downHight)
}
}
me.lastPoint = curPoint; // 记录本次移动的点
return isPrevent // false表示不往上冒泡相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
}
function touchendEvent(e, ins) {
// 如果下拉区域高度已改变,则需重置回来
if (me.isMoveDown) {
if (me.downHight >= me.optDown.offset) {
// 符合触发刷新的条件
me.downHight = me.optDown.offset; // 更新下拉区域高度
// me.triggerDownScroll();
me.callMethod(ins, {type: 'triggerDownScroll'})
} else {
// 不符合的话 则重置
me.downHight = 0;
// me.optDown.endDownScroll && me.optDown.endDownScroll(me);
me.callMethod(ins, {type: 'endDownScroll'})
}
me.movetype = 0;
me.isMoveDown = false;
} else if (!me.isScrollBody && me.getScrollTop() === me.startTop) { // scroll-view到顶/左/右/底的滑动事件
var isScrollUp = me.getPoint(e).y - me.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
// 上滑
if (isScrollUp) {
// 需检查滑动的角度
var angle = me.getAngle(me.getPoint(e), me.startPoint); // 两点之间的角度,区间 [0,90]
if (angle > 80) {
// 检查并触发上拉
// me.triggerUpScroll(true);
me.callMethod(ins, {type: 'triggerUpScroll'})
}
}
}
me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
}
/* 是否禁用下拉刷新 */
me.disabled = function(){
return !me.optDown || !me.optDown.use || me.optDown.native
}
/* 根据点击滑动事件获取第一个手指的坐标 */
me.getPoint = function(e) {
if (!e) {
return {x: 0,y: 0}
}
if (e.touches && e.touches[0]) {
return {x: e.touches[0].pageX,y: e.touches[0].pageY}
} else if (e.changedTouches && e.changedTouches[0]) {
return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
} else {
return {x: e.clientX,y: e.clientY}
}
}
/* 计算两点之间的角度: 区间 [0,90]*/
me.getAngle = function (p1, p2) {
var x = Math.abs(p1.x - p2.x);
var y = Math.abs(p1.y - p2.y);
var z = Math.sqrt(x * x + y * y);
var angle = 0;
if (z !== 0) {
angle = Math.asin(y / z) / Math.PI * 180;
}
return angle
}
/* 获取滚动条的位置 */
me.getScrollTop = function() {
return me.scrollTop || 0
}
/* 获取body的高度 */
me.getBodyHeight = function() {
return me.bodyHeight || 0;
}
/* 调用逻辑层的方法 */
me.callMethod = function(ins, param) {
if(ins) ins.callMethod('wxsCall', param)
}
/* 导出模块 */
module.exports = {
propObserver: propObserver,
callObserver: callObserver,
touchstartEvent: touchstartEvent,
touchmoveEvent: touchmoveEvent,
touchendEvent: touchendEvent
}

View File

@ -0,0 +1,118 @@
<!-- 公告轮播 -->
<template>
<view class="NoticeList" :style="'background-color: '+bgColor+';'">
<view class="notice-image">
<image :src="imageUrl" mode="aspectFit" />
</view>
<view class="notice-content">
<view v-if="list.length < 1">
<text class="content-item-text" :style="'color: '+color+';'">暂无消息</text>
</view>
<swiper v-else class="content-item" :vertical="vertical" :autoplay="true" :interval="interval" :circular="circular">
<swiper-item v-for="(item, index) in list" :key="index" @click="noticeItemClickHandle(item)">
<text class="content-item-text" :style="'color: '+color+';'">{{ item[itemKey] }}</text>
</swiper-item>
</swiper>
</view>
</view>
</template>
<script>
export default {
name: 'NoticeList',
props:{
list: {
type: Array,
default: function () {
return []
}
},
//
imageUrl: {
type: String,
default: ''
},
//
color: {
type: String,
default: '#564F5F'
},
//
bgColor: {
type: String,
default: '#F5F5F5'
},
//
vertical: {
type: Boolean,
default: true
},
//
interval: {
type: Number,
default: 5 * 1000
},
//
circular: {
type: Boolean,
default: true
},
//
itemKey: {
type: String,
default: 'title'
}
},
data() {
return {};
},
methods: {
noticeItemClickHandle (item) {
this.$emit('click', item)
}
}
}
</script>
<style lang="scss" scoped>
.NoticeList{
width: 100%;
height: 38rpx;
overflow: hidden;
display: flex;
justify-content: flex-start;
.notice-image {
line-height: 38rpx;
padding-right: 20rpx;
// vertical-align: middle;
// text-align: center;
display: flex;
align-items: center;
image {
width: 28rpx;
height: 28rpx;
}
}
.notice-content {
flex: 1;
position: relative;
font-size: 24rpx;
.content-item {
width: 100%;
height: 38rpx;
text-align: left;
line-height: 38rpx;
}
.content-item-text{
display: inline-block;
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
line-height: 40rpx;
overflow: hidden;
}
}
}
</style>

View File

@ -0,0 +1,138 @@
<template>
<picker mode="multiSelector"
:value="multiIndex"
:range="multiArray"
@change="handleValueChange"
@columnchange="handleColumnChange">
<slot></slot>
</picker>
</template>
<script>
const CHINA_REGIONS = require('./regions.json')
export default {
props:{
defaultRegions:{
type:Array,
default(){
return []
}
},
defaultRegionCode:{
type:String
},
defaultRegion:[String,Array]
},
data() {
return {
cityArr:CHINA_REGIONS[0].childs,
districtArr:CHINA_REGIONS[0].childs[0].childs,
multiIndex: [0, 0, 0],
isInitMultiArray:true,
}
},
watch:{
defaultRegion:{
handler(region,oldRegion){
if(Array.isArray(region)){
//
oldRegion = oldRegion || []
if(region.join('')!==oldRegion.join('')){
this.handleDefaultRegion(region)
}
}else if(region&&region.length == 6){
this.handleDefaultRegion(region)
}else{
console.warn('defaultRegion非有效格式')
}
},
immediate:true,
}
},
computed:{
multiArray(){
console.log(this.pickedArr)
return this.pickedArr.map(arr=>arr.map(item=>item.name))
},
pickedArr(){
//
if(this.isInitMultiArray){
return [
CHINA_REGIONS,
CHINA_REGIONS[0].childs,
CHINA_REGIONS[0].childs[0].childs
]
}
return [CHINA_REGIONS,this.cityArr,this.districtArr];
}
},
methods: {
handleColumnChange(e){
// console.log(e);
this.isInitMultiArray = false;
const that = this;
let col = e.detail.column;
let row = e.detail.value;
that.multiIndex[col] = row;
try{
switch(col){
case 0:
if(CHINA_REGIONS[that.multiIndex[0]].childs.length==0){
that.cityArr = that.districtArr = [CHINA_REGIONS[that.multiIndex[0]]]
break;
}
that.cityArr = CHINA_REGIONS[that.multiIndex[0]].childs
that.districtArr = CHINA_REGIONS[that.multiIndex[0]].childs[that.multiIndex[1]].childs
break;
case 1:
that.districtArr = CHINA_REGIONS[that.multiIndex[0]].childs[that.multiIndex[1]].childs
break;
case 2:
break;
}
}catch(e){
// console.log(e);
that.districtArr = CHINA_REGIONS[that.multiIndex[0]].childs[0].childs
}
},
handleValueChange(e){
//
let [index0,index1,index2] = e.detail.value;
let [arr0,arr1,arr2] = this.pickedArr;
let address = [arr0[index0],arr1[index1],arr2[index2]];
// console.log(address);
this.$emit('getRegion',address)
},
handleDefaultRegion(region){
const isCode = !Array.isArray(region)
this.isInitMultiArray = false;
let children = CHINA_REGIONS
for(let i=0;i<3;i++){
for(let j=0;j<children.length;j++){
let condition = isCode?children[j].code==region.slice(0,(i+1)*2):children[j].name.includes(region[i]);
if(condition){
//
// console.log(i,j,children.length-1);
children = children[j].childs;
if(i==0){
this.cityArr = children
}else if(i==1){
this.districtArr = children
}
this.$set(this.multiIndex,i,j)
// console.log(this.multiIndex);
break;
}else{
//
// console.log(i,j,children.length-1);
if(i==0 && j==(children.length-1)){
this.isInitMultiArray = true;
}
}
}
}
}
},
}
</script>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,474 @@
<template>
<view v-if="visibleSync" :style="[customStyle, {
zIndex: uZindex - 1
}]" :class="{ 'u-drawer-visible': showDrawer }" class="u-drawer" hover-stop-propagation>
<mask-view :custom-style="maskCustomStyle" :maskClickAble="maskCloseAble" :z-index="uZindex - 2" :show="showDrawer && mask" @click="maskClick"></mask-view>
<view
class="u-drawer-content"
@tap="modeCenterClose(mode)"
:class="[
safeAreaInsetBottom ? 'safe-area-inset-bottom' : '',
'u-drawer-' + mode,
showDrawer ? 'u-drawer-content-visible' : '',
zoom && mode == 'center' ? 'u-animation-zoom' : ''
]"
@touchmove.stop.prevent
@tap.stop.prevent
:style="[style]"
>
<view class="u-mode-center-box" @tap.stop.prevent @touchmove.stop.prevent v-if="mode == 'center'" :style="[centerStyle]">
<icon-view
@click="close"
v-if="closeable"
class="u-close"
:class="['u-close--' + closeIconPos]"
:name="closeIcon"
:color="closeIconColor"
:size="closeIconSize"
></icon-view>
<scroll-view class="u-drawer__scroll-view" scroll-y="true" v-if="!isView">
<slot />
</scroll-view>
<view class="u-drawer__scroll-view" scroll-y="true" v-if="isView">
<slot />
</view>
</view>
<scroll-view class="u-drawer__scroll-view" scroll-y="true" v-else>
<slot />
</scroll-view>
<view @tap="close" class="u-close" :class="['u-close--' + closeIconPos]">
<icon-view
v-if="mode != 'center' && closeable"
:name="closeIcon"
:color="closeIconColor"
:size="closeIconSize"
></icon-view>
</view>
</view>
</view>
</template>
<script>
/**
* popup 弹窗
* @description 弹出层容器用于展示弹窗信息提示等内容支持上右和中部弹出组件只提供容器内部内容由用户自定义
* @tutorial https://www.uviewui.com/components/popup.html
* @property {String} mode 弹出方向默认left
* @property {Boolean} mask 是否显示遮罩默认true
* @property {Stringr | Number} length mode=left | 见官网说明默认auto
* @property {Boolean} zoom 是否开启缩放动画只在mode为center时有效默认true
* @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配默认false
* @property {Boolean} mask-close-able 点击遮罩是否可以关闭弹出层默认true
* @property {Object} custom-style 用户自定义样式
* @property {Stringr | Number} negative-top 中部弹出时往上偏移的值
* @property {Numberr | String} border-radius 弹窗圆角值默认0
* @property {Numberr | String} z-index 弹出内容的z-index值默认1075
* @property {Boolean} closeable 是否显示关闭图标默认false
* @property {String} close-icon 关闭图标的名称只能uView的内置图标
* @property {String} close-icon-pos 自定义关闭图标位置默认top-right
* @property {String} close-icon-color 关闭图标的颜色默认#909399
* @property {Number | String} close-icon-size 关闭图标的大小单位rpx默认30
* @property Boolean} isView 是否可以超出内容区
* @event {Function} open 弹出层打开
* @event {Function} close 弹出层收起
* @example <popup-view v-model="show"><view>出淤泥而不染濯清涟而不妖</view></popup-view>
*/
export default {
name: 'popupView',
props: {
/**
* 显示状态
*/
show: {
type: Boolean,
default: false
},
/**
* 弹出方向left|right|top|bottom|center
*/
mode: {
type: String,
default: 'left'
},
/**
* 是否显示遮罩
*/
mask: {
type: Boolean,
default: true
},
// (mode=left|right)(mode=top|bottom)rpx"auto"
// "50%"
length: {
type: [Number, String],
default: 'auto'
},
// mode=center
zoom: {
type: Boolean,
default: true
},
// iPhoneX
safeAreaInsetBottom: {
type: Boolean,
default: false
},
//
maskCloseAble: {
type: Boolean,
default: true
},
//
isView: {
type: Boolean,
default: false
},
//
customStyle: {
type: Object,
default() {
return {};
}
},
value: {
type: Boolean,
default: false
},
// 使Pickerkeyboard
// v-modelprops
popup: {
type: Boolean,
default: true
},
// rpx
borderRadius: {
type: [Number, String],
default: 0
},
zIndex: {
type: [Number, String],
default: ''
},
//
closeable: {
type: Boolean,
default: false
},
// uView
closeIcon: {
type: String,
default: 'close'
},
// top-lefttop-rightbottom-leftbottom-right
closeIconPos: {
type: String,
default: 'top-right'
},
//
closeIconColor: {
type: String,
default: '#909399'
},
// rpx
closeIconSize: {
type: [String, Number],
default: '30'
},
// rpx"auto"
// "50%"length
width: {
type: String,
default: ''
},
// rpx"auto"
// "50%"length
height: {
type: String,
default: ''
},
// margin-topmode=center
negativeTop: {
type: [String, Number],
default: 0
},
//
maskCustomStyle: {
type: Object,
default() {
return {}
}
},
//
centerBoxBgColor: {
type: [String, Boolean],
default: '#ffffff'
},
backgroundColor: {
type: [String, Boolean],
default: '#ffffff'
},
},
data() {
return {
visibleSync: false,
showDrawer: false,
timer: null,
closeFromInner: false, // value
};
},
computed: {
// mode(mode = left|right)(mode = top|bottom)
style() {
let style = {};
// translate
if (this.mode == 'left' || this.mode == 'right') {
style = {
width: this.width ? this.getUnitValue(this.width) : this.getUnitValue(this.length),
height: '100%',
transform: `translate3D(${this.mode == 'left' ? '-100%' : '100%'},0px,0px)`
};
} else if (this.mode == 'top' || this.mode == 'bottom') {
style = {
width: '100%',
height: this.height ? this.getUnitValue(this.height) : this.getUnitValue(this.length),
transform: `translate3D(0px,${this.mode == 'top' ? '-100%' : '100%'},0px)`
};
}
style.zIndex = this.uZindex;
// borderRadius
if (this.borderRadius) {
switch (this.mode) {
case 'left':
style.borderRadius = `0 ${this.borderRadius}rpx ${this.borderRadius}rpx 0`;
break;
case 'top':
style.borderRadius = `0 0 ${this.borderRadius}rpx ${this.borderRadius}rpx`;
break;
case 'right':
style.borderRadius = `${this.borderRadius}rpx 0 0 ${this.borderRadius}rpx`;
break;
case 'bottom':
style.borderRadius = `${this.borderRadius}rpx ${this.borderRadius}rpx 0 0`;
break;
default:
}
//
style.overflow = 'hidden';
}
// style['background-color'] = this.backgroundColor
return style;
},
//
centerStyle() {
let style = {};
style.width = this.width ? this.getUnitValue(this.width) : this.getUnitValue(this.length);
// auto
style.height = this.height ? this.getUnitValue(this.height) : 'auto';
style.zIndex = this.uZindex;
style.marginTop = `-${this.addUnit(this.negativeTop)}`;
if (this.borderRadius) {
style.borderRadius = `${this.borderRadius}rpx`;
//
style.overflow = 'hidden';
}else{
style.borderRadius = `20rpx`;
// style.overflow = 'unset !important';
console.log("啊啊啊啊")
}
//
if (this.centerBoxBgColor) {
style['background-color'] = this.centerBoxBgColor;
} else {
//
style['background-color'] = 'transparent';
}
return style;
},
// z-index
uZindex() {
return this.zIndex ? this.zIndex : 10075;
}
},
watch: {
value(val) {
if (val) {
this.open();
} else if(!this.closeFromInner) {
this.close();
}
this.closeFromInner = false;
}
},
mounted() {
// valuetruepopup
this.value && this.open();
},
methods: {
// rpx
getUnitValue(val) {
if(/(%|px|rpx|auto)$/.test(val)) return val;
else return val + 'rpx'
},
//
maskClick() {
this.close();
},
close() {
// valuewatchvalueclose
// @close
this.closeFromInner = true;
this.change('showDrawer', 'visibleSync', false);
},
// .u-drawer-content
// mode=center
modeCenterClose(mode) {
if (mode != 'center' || !this.maskCloseAble) return;
this.close();
},
open() {
this.change('visibleSync', 'showDrawer', true);
},
//
//
change(param1, param2, status) {
// this.popupfalsepickeractionsheetpopup
if (this.popup == true) {
this.$emit('input', status);
}
this[param1] = status;
if(status) {
// #ifdef H5 || MP
this.timer = setTimeout(() => {
this[param2] = status;
this.$emit(status ? 'open' : 'close');
}, 50);
// #endif
// #ifndef H5 || MP
this.$nextTick(() => {
this[param2] = status;
this.$emit(status ? 'open' : 'close');
})
// #endif
} else {
this.timer = setTimeout(() => {
this[param2] = status;
this.$emit(status ? 'open' : 'close');
}, 300);
}
},
//
addUnit (value = 'auto', unit = 'rpx') {
value = String(value);
// uViewnumber
return this.testNumber(value) ? `${value}${unit}` : value;
},
testNumber (value) {
return /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value)
}
}
};
</script>
<style scoped lang="scss">
.u-drawer {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
.u-drawer-content {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: absolute;
z-index: 1003;
transition: all 0.3s linear;
}
.u-drawer__scroll-view {
width: 100%;
height: 100%;
}
.u-drawer-left {
top: 0;
bottom: 0;
left: 0;
background-color: #ffffff;
}
.u-drawer-right {
right: 0;
top: 0;
bottom: 0;
background-color: #ffffff;
}
.u-drawer-top {
top: 0;
left: 0;
right: 0;
background-color: #ffffff;
}
.u-drawer-bottom {
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
}
.u-drawer-center {
/* #ifndef APP-NVUE */
display: flex;
flex-direction: column;
/* #endif */
bottom: 0;
left: 0;
right: 0;
top: 0;
justify-content: center;
align-items: center;
opacity: 0;
z-index: 99999;
}
.u-mode-center-box {
min-width: 100rpx;
min-height: 100rpx;
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: relative;
background-color: #ffffff;
}
.u-drawer-content-visible.u-drawer-center {
transform: scale(1);
opacity: 1;
}
.u-animation-zoom {
transform: scale(1.15);
}
.u-drawer-content-visible {
transform: translate3D(0px, 0px, 0px) !important;
}
.u-close {
position: absolute;
z-index: 3;
}
.u-close--top-left {
top: 30rpx;
left: 30rpx;
}
.u-close--top-right {
top: 30rpx;
right: 30rpx;
}
.u-close--bottom-left {
bottom: 30rpx;
left: 30rpx;
}
.u-close--bottom-right {
right: 30rpx;
bottom: 30rpx;
}
</style>

View File

@ -0,0 +1,322 @@
<!-- tabbar组件 -->
<template>
<view class="TabControl" :style="[tabControlStyle]">
<scroll-view scroll-x="true" :style="'background-color:'+bgc+';top:'+top+'px;'" :class="fixed?'fxied':''" :scroll-left='scrollLeft' scroll-with-animation id="tabcard">
<image src="@/static/signUp/ico_return@3x.png" class="goBackImg" mode="" @click="goBack" v-if="goBackImg"></image>
<view class="tabList" :style="[tabListStyle]">
<view
:class="'tabItem'+(currentIndex==index?' thisOpenSelect':'')"
:style="[tabItemStyle]"
v-for="(item,index) in values"
:id="'item'+index"
:key='index'
@click="_onClick(item, index)">
<view class="tabItemName">
<text :style="(currentIndex==index?'font-size:'+activeSize+'rpx;color:'+activeColor:'font-size:'+itemSize+'rpx')">{{ item.name }}</text>
<!-- 排序箭头显示 -->
<view
v-show='item.sort && item.sortType !== 1'
class="arrow"
:style="'transform: rotate(' + (([4, 2].indexOf(item.sortType) !== -1) ? '180' : '0') + 'deg);'" />
</view>
<!-- 激活下划线如果需要显示则sort不要设置 -->
<view v-if='activeLineShow' class="activeLine" :style="{'background-color': activeLineColor}"></view>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
name:'TabControl',
props:{
//
goBackImg: {
type: Boolean,
default: false
},
//
current: {
type: Number,
default: 0
},
// tab
values: {
type: Array,
default () {
return []
}
},
//
bgc:{
type: String,
default: '#fff'
},
//
fixed: {
type: Boolean,
default: false
},
// tabBar
scrollFlag: {
type:Boolean,
default:false
},
// 线
lineWidth: {
type: Number,
default: 48
},
// tab
itemSize: {
type: Number,
default: 28
},
// tab
activeSize: {
type: Number,
default: 28
},
// tab
activeColor: {
type: String,
default: '#FE4066'
},
// top
top: {
type: Number,
default: 0
},
// tab
isEqually: {
type: Boolean,
default: false
},
// tab
tabListHeight: {
type: [Number, String],
default: 40
},
// 线
activeLineShow: {
type: Boolean,
default: true
},
//线
activeLineColor: {
type: String,
default: '#FE4066'
},
tabWrapNeedHeight: {
type: Boolean,
default: true
}
},
data() {
return {
currentIndex: 0,
windowWidth:0, //
leftList:[], //
widthList:[], //
scrollLeft:0, //
newScroll:0, //()
wornScroll:0, //()
};
},
watch: {
current(val) {
if (val !== this.currentIndex) {
this.currentIndex = val
if(this.scrollFlag){
this.tabListScroll(val)
}
}
}
},
computed: {
tabItemStyle: function () {
const _style = {}
if (this.isEqually) {
_style['width'] = this.windowWidth/this.values.length + 'px'
_style['margin-right'] = 0
}
if (!this.activeLineShow) {
_style['height'] = '100%'
}
return _style
},
tabListStyle: function () {
const _style = {
height: this.tabListHeight + 'px'
}
if (this.isEqually) {
_style['display'] = 'flex'
_style['justify-content'] = 'space-between'
_style['padding-left'] = 0
}
return _style
},
tabControlStyle: function () {
const _style = {}
if (this.tabWrapNeedHeight) {
_style['height'] = this.tabListHeight + 'px'
}
return _style
}
},
created() {
this.currentIndex = this.current
if(this.scrollFlag){
setTimeout(()=>{
this.tabListScroll(this.current)
},300)
}
},
mounted(){
setTimeout(()=>{
uni.createSelectorQuery().in(this).select("#tabcard").boundingClientRect((res)=>{
this.$emit('getTabCardHeight', {height:res.height})
}).exec()
uni.getSystemInfo({
success: (res)=> {
this.windowWidth = res.windowWidth;
// console.log(this.windowWidth);
this.values.forEach((i,v)=>{
let info = uni.createSelectorQuery().in(this);
info.select("#item"+v).boundingClientRect((res)=>{
//
// if(v==0){
// this.startLenght = res.left
// }
this.widthList.push(res.width)
this.leftList.push(res.left)
}).exec()
})
// console.log(this.leftList)
// console.log(this.widthList)
}
});
})
},
methods: {
goBack(){
uni.navigateBack({
})
},
_onClick(item, index) {
this.$emit('clickItem', { currentIndex: index, ...item })
if (this.currentIndex !== index) {
this.currentIndex = index
//
if(this.scrollFlag){
this.tabListScroll(index)
}
}
},
//
tabListScroll(index){
let scoll = 0;
this.wornScroll = index;
// this.wornScroll-this.newScroll>0
if(this.wornScroll-this.newScroll>0){
for(let i = 0;i<this.leftList.length;i++){
if(i>1&&i==this.currentIndex){
scoll = this.leftList[i-1]
}
}
// console.log('',scoll)
}else{
if(index>1){
for(let i = 0;i<this.leftList.length;i++){
if(i<index-1){
scoll = this.leftList[i]
}
}
}else{
scoll = 0
}
// console.log('')
}
this.newScroll = this.wornScroll;
this.scrollLeft = scoll;
}
}
}
</script>
<style lang="scss" scoped>
.goBackImg{
width: 18upx;
height: 32upx;
position: absolute;
left: 30upx;
top: 30upx;
z-index: 999;
}
.TabControl {
.fxied {
position: fixed;
z-index: 2;
}
.tabList {
height: 80rpx;
padding-left: 24rpx;
white-space: nowrap;
text-align: center;
.tabItem {
position: relative;
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-right: 60rpx;
.tabItemName {
display: flex;
align-items: center;
justify-content: center;
text {
line-height: 44rpx;
color: #564F5F;
transition: all 0.3s ease 0s;
}
.arrow {
width: 10rpx;
height: 6rpx;
margin-left: 6rpx;
background: url('@/static/components/triangular_arrow.png');
background-size: 100% 100%;
// transform: rotate(180deg);
}
}
.activeLine {
width: 48rpx;
height: 8rpx;
border-radius: 4rpx;
// background-color: #FE4066;
margin: 8rpx auto 0;
opacity: 0;
transition: all 0.5s ease 0s;
}
}
.tabItem:first-child {
// margin-left: 22rpx;
}
.tabItem:last-child {
margin-right: 24rpx;
}
.thisOpenSelect {
text {
color: #333;
font-weight: 600;
}
.activeLine {
opacity: 1;
}
}
}
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<!--增加audio标签支持-->
<audio
:id="node.attr.id"
:class="node.classStr"
:style="node.styleStr"
:src="node.attr.src"
:loop="node.attr.loop"
:poster="node.attr.poster"
:name="node.attr.name"
:author="node.attr.author"
controls></audio>
</template>
<script>
export default {
name: 'wxParseAudio',
props: {
node: {
type: Object,
default() {
return {};
},
},
},
};
</script>

View File

@ -0,0 +1,86 @@
<template>
<image
:mode="node.attr.mode"
:lazy-load="node.attr.lazyLoad"
:class="node.classStr"
:style="newStyleStr || node.styleStr"
:data-src="node.attr.src"
:src="node.attr.src"
@tap="wxParseImgTap"
@load="wxParseImgLoad"
/>
</template>
<script>
export default {
name: 'wxParseImg',
data() {
return {
newStyleStr: '',
preview: true,
};
},
props: {
node: {
type: Object,
default() {
return {};
},
},
},
methods: {
wxParseImgTap(e) {
if (!this.preview) return;
const { src } = e.currentTarget.dataset;
if (!src) return;
let parent = this.$parent;
while(!parent.preview || typeof parent.preview !== 'function') {// TODO
parent = parent.$parent;
}
parent.preview(src, e);
},
//
wxParseImgLoad(e) {
const { src } = e.currentTarget.dataset;
if (!src) return;
const { width, height } = e.mp.detail;
const recal = this.wxAutoImageCal(width, height);
const { imageheight, imageWidth } = recal;
const { padding, mode } = this.node.attr;
const { styleStr } = this.node;
const imageHeightStyle = mode === 'widthFix' ? '' : `height: ${imageheight}px;`;
this.newStyleStr = `${styleStr}; ${imageHeightStyle}; width: ${imageWidth}px; padding: 0 ${+padding}px;`;
},
//
wxAutoImageCal(originalWidth, originalHeight) {
//
const { padding } = this.node.attr;
const windowWidth = this.node.$screen.width - (2 * padding);
const results = {};
if (originalWidth < 60 || originalHeight < 60) {
const { src } = this.node.attr;
let parent = this.$parent;
while(!parent.preview || typeof parent.preview !== 'function') {
parent = parent.$parent;
}
parent.removeImageUrl(src);
this.preview = false;
}
//
if (originalWidth > windowWidth) {
// widthwidth
results.imageWidth = windowWidth;
results.imageheight = windowWidth * (originalHeight / originalWidth);
} else {
//
results.imageWidth = originalWidth;
results.imageheight = originalHeight;
}
return results;
},
},
};
</script>

View File

@ -0,0 +1,107 @@
<template>
<view>
<!--判断是否是标签节点-->
<block v-if="node.node == 'element'">
<block v-if="node.tag == 'button'">
<button type="default" size="mini">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</button>
</block>
<!--li类型-->
<block v-else-if="node.tag == 'li'">
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--video类型-->
<block v-else-if="node.tag == 'video'">
<wx-parse-video :node="node" />
</block>
<!--audio类型-->
<block v-else-if="node.tag == 'audio'">
<wx-parse-audio :node="node" />
</block>
<!--img类型-->
<block v-else-if="node.tag == 'img'">
<wx-parse-img :node="node" />
</block>
<!--a类型-->
<block v-else-if="node.tag == 'a'">
<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--table类型-->
<block v-else-if="node.tag == 'table'">
<view :class="node.classStr" class="table" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--br类型-->
<block v-else-if="node.tag == 'br'">
<text>\n</text>
</block>
<!--其他标签-->
<block v-else>
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
</block>
<!--判断是否是文本节点-->
<block v-else-if="node.node == 'text'">{{node.text}}</block>
</view>
</template>
<script>
import wxParseTemplate from './wxParseTemplate1';
import wxParseImg from './wxParseImg';
import wxParseVideo from './wxParseVideo';
import wxParseAudio from './wxParseAudio';
export default {
name: 'wxParseTemplate0',
props: {
node: {},
},
components: {
wxParseTemplate,
wxParseImg,
wxParseVideo,
wxParseAudio,
},
methods: {
wxParseATap(e) {
const {
href
} = e.currentTarget.dataset;// TODO currentTargetdataset
if (!href) return;
let parent = this.$parent;
while(!parent.preview || typeof parent.preview !== 'function') {// TODO
parent = parent.$parent;
}
parent.navigate(href, e);
},
},
};
</script>

View File

@ -0,0 +1,99 @@
<template>
<view :class="(node.tag == 'li' ? node.classStr : (node.node==='text'?'text':''))">
<!--判断是否是标签节点-->
<block v-if="node.node == 'element'">
<block v-if="node.tag == 'button'">
<button type="default" size="mini">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</button>
</block>
<!--li类型-->
<block v-else-if="node.tag == 'li'">
<!-- <view :class="node.classStr" :style="node.styleStr"> -->
<view :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--video类型-->
<block v-else-if="node.tag == 'video'">
<wx-parse-video :node="node" />
</block>
<!--audio类型-->
<block v-else-if="node.tag == 'audio'">
<wx-parse-audio :node="node" />
</block>
<!--img类型-->
<block v-else-if="node.tag == 'img'">
<wx-parse-img :node="node" />
</block>
<!--a类型-->
<block v-else-if="node.tag == 'a'">
<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--br类型-->
<block v-else-if="node.tag == 'br'">
<text>\n</text>
</block>
<!--其他标签-->
<block v-else>
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
</block>
<!--判断是否是文本节点-->
<block v-else-if="node.node == 'text'">{{node.text}}</block>
</view>
</template>
<script>
import wxParseTemplate from './wxParseTemplate2';
import wxParseImg from './wxParseImg';
import wxParseVideo from './wxParseVideo';
import wxParseAudio from './wxParseAudio';
export default {
name: 'wxParseTemplate1',
props: {
node: {},
},
components: {
wxParseTemplate,
wxParseImg,
wxParseVideo,
wxParseAudio,
},
methods: {
wxParseATap(e) {
const {
href
} = e.currentTarget.dataset;
if (!href) return;
let parent = this.$parent;
while(!parent.preview || typeof parent.preview !== 'function') {
parent = parent.$parent;
}
parent.navigate(href, e);
},
},
};
</script>

View File

@ -0,0 +1,97 @@
<template>
<view>
<!--判断是否是标签节点-->
<block v-if="node.node == 'element'">
<block v-if="node.tag == 'button'">
<button type="default" size="mini">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</button>
</block>
<!--li类型-->
<block v-else-if="node.tag == 'li'">
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--video类型-->
<block v-else-if="node.tag == 'video'">
<wx-parse-video :node="node" />
</block>
<!--audio类型-->
<block v-else-if="node.tag == 'audio'">
<wx-parse-audio :node="node" />
</block>
<!--img类型-->
<block v-else-if="node.tag == 'img'">
<wx-parse-img :node="node" />
</block>
<!--a类型-->
<block v-else-if="node.tag == 'a'">
<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--br类型-->
<block v-else-if="node.tag == 'br'">
<text>\n</text>
</block>
<!--其他标签-->
<block v-else>
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
</block>
<!--判断是否是文本节点-->
<block v-else-if="node.node == 'text'">{{node.text}}</block>
</view>
</template>
<script>
import wxParseTemplate from './wxParseTemplate11';
import wxParseImg from './wxParseImg';
import wxParseVideo from './wxParseVideo';
import wxParseAudio from './wxParseAudio';
export default {
name: 'wxParseTemplate10',
props: {
node: {},
},
components: {
wxParseTemplate,
wxParseImg,
wxParseVideo,
wxParseAudio,
},
methods: {
wxParseATap(e) {
const {
href
} = e.currentTarget.dataset;
if (!href) return;
let parent = this.$parent;
while(!parent.preview || typeof parent.preview !== 'function') {
parent = parent.$parent;
}
parent.navigate(href, e);
},
},
};
</script>

View File

@ -0,0 +1,87 @@
<template>
<view>
<!--判断是否是标签节点-->
<block v-if="node.node == 'element'">
<!--button类型-->
<block v-if="node.tag == 'button'">
<button type="default" size="mini">
</button>
</block>
<!--li类型-->
<block v-else-if="node.tag == 'li'">
<view :class="node.classStr" :style="node.styleStr">
{{node.text}}
</view>
</block>
<!--video类型-->
<block v-else-if="node.tag == 'video'">
<wx-parse-video :node="node" />
</block>
<!--audio类型-->
<block v-else-if="node.tag == 'audio'">
<wx-parse-audio :node="node" />
</block>
<!--img类型-->
<block v-else-if="node.tag == 'img'">
<wx-parse-img :node="node" />
</block>
<!--a类型-->
<block v-else-if="node.tag == 'a'">
<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
{{node.text}}
</view>
</block>
<!--br类型-->
<block v-else-if="node.tag == 'br'">
<text>\n</text>
</block>
<!--其他标签-->
<block v-else>
<view :class="node.classStr" :style="node.styleStr">
{{node.text}}
</view>
</block>
</block>
<!--判断是否是文本节点-->
<block v-else-if="node.node == 'text'">{{node.text}}</block>
</view>
</template>
<script>
import wxParseImg from './wxParseImg';
import wxParseVideo from './wxParseVideo';
import wxParseAudio from './wxParseAudio';
export default {
name: 'wxParseTemplate11',
props: {
node: {},
},
components: {
wxParseImg,
wxParseVideo,
wxParseAudio,
},
methods: {
wxParseATap(e) {
const {
href
} = e.currentTarget.dataset;
if (!href) return;
let parent = this.$parent;
while(!parent.preview || typeof parent.preview !== 'function') {
parent = parent.$parent;
}
parent.navigate(href, e);
},
},
};
</script>

View File

@ -0,0 +1,98 @@
<template>
<view>
<!--判断是否是标签节点-->
<block v-if="node.node == 'element'">
<block v-if="node.tag == 'button'">
<button type="default" size="mini">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</button>
</block>
<!--li类型-->
<block v-else-if="node.tag == 'li'">
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--video类型-->
<block v-else-if="node.tag == 'video'">
<wx-parse-video :node="node" />
</block>
<!--audio类型-->
<block v-else-if="node.tag == 'audio'">
<wx-parse-audio :node="node" />
</block>
<!--img类型-->
<block v-else-if="node.tag == 'img'">
<wx-parse-img :node="node" />
</block>
<!--a类型-->
<block v-else-if="node.tag == 'a'">
<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--br类型-->
<block v-else-if="node.tag == 'br'">
<text>\n</text>
</block>
<!--其他标签-->
<block v-else>
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
</block>
<!--判断是否是文本节点-->
<block v-else-if="node.node == 'text'">{{node.text}}</block>
</view>
</template>
<script>
import wxParseTemplate from './wxParseTemplate3';
import wxParseImg from './wxParseImg';
import wxParseVideo from './wxParseVideo';
import wxParseAudio from './wxParseAudio';
export default {
name: 'wxParseTemplate2',
props: {
node: {},
},
components: {
wxParseTemplate,
wxParseImg,
wxParseVideo,
wxParseAudio,
},
methods: {
wxParseATap(e) {
const {
href
} = e.currentTarget.dataset;
if (!href) return;
let parent = this.$parent;
while(!parent.preview || typeof parent.preview !== 'function') {
parent = parent.$parent;
}
parent.navigate(href, e);
},
},
};
</script>

View File

@ -0,0 +1,98 @@
<template>
<view>
<!--判断是否是标签节点-->
<block v-if="node.node == 'element'">
<block v-if="node.tag == 'button'">
<button type="default" size="mini">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</button>
</block>
<!--li类型-->
<block v-else-if="node.tag == 'li'">
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--video类型-->
<block v-else-if="node.tag == 'video'">
<wx-parse-video :node="node" />
</block>
<!--audio类型-->
<block v-else-if="node.tag == 'audio'">
<wx-parse-audio :node="node" />
</block>
<!--img类型-->
<block v-else-if="node.tag == 'img'">
<wx-parse-img :node="node" />
</block>
<!--a类型-->
<block v-else-if="node.tag == 'a'">
<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--br类型-->
<block v-else-if="node.tag == 'br'">
<text>\n</text>
</block>
<!--其他标签-->
<block v-else>
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
</block>
<!--判断是否是文本节点-->
<block v-else-if="node.node == 'text'">{{node.text}}</block>
</view>
</template>
<script>
import wxParseTemplate from './wxParseTemplate4';
import wxParseImg from './wxParseImg';
import wxParseVideo from './wxParseVideo';
import wxParseAudio from './wxParseAudio';
export default {
name: 'wxParseTemplate3',
props: {
node: {},
},
components: {
wxParseTemplate,
wxParseImg,
wxParseVideo,
wxParseAudio,
},
methods: {
wxParseATap(e) {
const {
href
} = e.currentTarget.dataset;
if (!href) return;
let parent = this.$parent;
while(!parent.preview || typeof parent.preview !== 'function') {
parent = parent.$parent;
}
parent.navigate(href, e);
},
},
};
</script>

View File

@ -0,0 +1,98 @@
<template>
<view>
<!--判断是否是标签节点-->
<block v-if="node.node == 'element'">
<block v-if="node.tag == 'button'">
<button type="default" size="mini">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</button>
</block>
<!--li类型-->
<block v-else-if="node.tag == 'li'">
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--video类型-->
<block v-else-if="node.tag == 'video'">
<wx-parse-video :node="node" />
</block>
<!--audio类型-->
<block v-else-if="node.tag == 'audio'">
<wx-parse-audio :node="node" />
</block>
<!--img类型-->
<block v-else-if="node.tag == 'img'">
<wx-parse-img :node="node" />
</block>
<!--a类型-->
<block v-else-if="node.tag == 'a'">
<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--br类型-->
<block v-else-if="node.tag == 'br'">
<text>\n</text>
</block>
<!--其他标签-->
<block v-else>
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
</block>
<!--判断是否是文本节点-->
<block v-else-if="node.node == 'text'">{{node.text}}</block>
</view>
</template>
<script>
import wxParseTemplate from './wxParseTemplate5';
import wxParseImg from './wxParseImg';
import wxParseVideo from './wxParseVideo';
import wxParseAudio from './wxParseAudio';
export default {
name: 'wxParseTemplate4',
props: {
node: {},
},
components: {
wxParseTemplate,
wxParseImg,
wxParseVideo,
wxParseAudio,
},
methods: {
wxParseATap(e) {
const {
href
} = e.currentTarget.dataset;
if (!href) return;
let parent = this.$parent;
while(!parent.preview || typeof parent.preview !== 'function') {
parent = parent.$parent;
}
parent.navigate(href, e);
},
},
};
</script>

View File

@ -0,0 +1,98 @@
<template>
<view>
<!--判断是否是标签节点-->
<block v-if="node.node == 'element'">
<block v-if="node.tag == 'button'">
<button type="default" size="mini">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</button>
</block>
<!--li类型-->
<block v-else-if="node.tag == 'li'">
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--video类型-->
<block v-else-if="node.tag == 'video'">
<wx-parse-video :node="node" />
</block>
<!--audio类型-->
<block v-else-if="node.tag == 'audio'">
<wx-parse-audio :node="node" />
</block>
<!--img类型-->
<block v-else-if="node.tag == 'img'">
<wx-parse-img :node="node" />
</block>
<!--a类型-->
<block v-else-if="node.tag == 'a'">
<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--br类型-->
<block v-else-if="node.tag == 'br'">
<text>\n</text>
</block>
<!--其他标签-->
<block v-else>
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
</block>
<!--判断是否是文本节点-->
<block v-else-if="node.node == 'text'">{{node.text}}</block>
</view>
</template>
<script>
import wxParseTemplate from './wxParseTemplate6';
import wxParseImg from './wxParseImg';
import wxParseVideo from './wxParseVideo';
import wxParseAudio from './wxParseAudio';
export default {
name: 'wxParseTemplate5',
props: {
node: {},
},
components: {
wxParseTemplate,
wxParseImg,
wxParseVideo,
wxParseAudio,
},
methods: {
wxParseATap(e) {
const {
href
} = e.currentTarget.dataset;
if (!href) return;
let parent = this.$parent;
while(!parent.preview || typeof parent.preview !== 'function') {
parent = parent.$parent;
}
parent.navigate(href, e);
},
},
};
</script>

View File

@ -0,0 +1,98 @@
<template>
<view>
<!--判断是否是标签节点-->
<block v-if="node.node == 'element'">
<block v-if="node.tag == 'button'">
<button type="default" size="mini">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</button>
</block>
<!--li类型-->
<block v-else-if="node.tag == 'li'">
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--video类型-->
<block v-else-if="node.tag == 'video'">
<wx-parse-video :node="node" />
</block>
<!--audio类型-->
<block v-else-if="node.tag == 'audio'">
<wx-parse-audio :node="node" />
</block>
<!--img类型-->
<block v-else-if="node.tag == 'img'">
<wx-parse-img :node="node" />
</block>
<!--a类型-->
<block v-else-if="node.tag == 'a'">
<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--br类型-->
<block v-else-if="node.tag == 'br'">
<text>\n</text>
</block>
<!--其他标签-->
<block v-else>
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
</block>
<!--判断是否是文本节点-->
<block v-else-if="node.node == 'text'">{{node.text}}</block>
</view>
</template>
<script>
import wxParseTemplate from './wxParseTemplate7';
import wxParseImg from './wxParseImg';
import wxParseVideo from './wxParseVideo';
import wxParseAudio from './wxParseAudio';
export default {
name: 'wxParseTemplate6',
props: {
node: {},
},
components: {
wxParseTemplate,
wxParseImg,
wxParseVideo,
wxParseAudio,
},
methods: {
wxParseATap(e) {
const {
href
} = e.currentTarget.dataset;
if (!href) return;
let parent = this.$parent;
while(!parent.preview || typeof parent.preview !== 'function') {
parent = parent.$parent;
}
parent.navigate(href, e);
},
},
};
</script>

View File

@ -0,0 +1,98 @@
<template>
<view>
<!--判断是否是标签节点-->
<block v-if="node.node == 'element'">
<block v-if="node.tag == 'button'">
<button type="default" size="mini">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</button>
</block>
<!--li类型-->
<block v-else-if="node.tag == 'li'">
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--video类型-->
<block v-else-if="node.tag == 'video'">
<wx-parse-video :node="node" />
</block>
<!--audio类型-->
<block v-else-if="node.tag == 'audio'">
<wx-parse-audio :node="node" />
</block>
<!--img类型-->
<block v-else-if="node.tag == 'img'">
<wx-parse-img :node="node" />
</block>
<!--a类型-->
<block v-else-if="node.tag == 'a'">
<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--br类型-->
<block v-else-if="node.tag == 'br'">
<text>\n</text>
</block>
<!--其他标签-->
<block v-else>
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
</block>
<!--判断是否是文本节点-->
<block v-else-if="node.node == 'text'">{{node.text}}</block>
</view>
</template>
<script>
import wxParseTemplate from './wxParseTemplate8';
import wxParseImg from './wxParseImg';
import wxParseVideo from './wxParseVideo';
import wxParseAudio from './wxParseAudio';
export default {
name: 'wxParseTemplate7',
props: {
node: {},
},
components: {
wxParseTemplate,
wxParseImg,
wxParseVideo,
wxParseAudio,
},
methods: {
wxParseATap(e) {
const {
href
} = e.currentTarget.dataset;
if (!href) return;
let parent = this.$parent;
while(!parent.preview || typeof parent.preview !== 'function') {
parent = parent.$parent;
}
parent.navigate(href, e);
},
},
};
</script>

View File

@ -0,0 +1,98 @@
<template>
<view>
<!--判断是否是标签节点-->
<block v-if="node.node == 'element'">
<block v-if="node.tag == 'button'">
<button type="default" size="mini">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</button>
</block>
<!--li类型-->
<block v-else-if="node.tag == 'li'">
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--video类型-->
<block v-else-if="node.tag == 'video'">
<wx-parse-video :node="node" />
</block>
<!--audio类型-->
<block v-else-if="node.tag == 'audio'">
<wx-parse-audio :node="node" />
</block>
<!--img类型-->
<block v-else-if="node.tag == 'img'">
<wx-parse-img :node="node" />
</block>
<!--a类型-->
<block v-else-if="node.tag == 'a'">
<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--br类型-->
<block v-else-if="node.tag == 'br'">
<text>\n</text>
</block>
<!--其他标签-->
<block v-else>
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
</block>
<!--判断是否是文本节点-->
<block v-else-if="node.node == 'text'">{{node.text}}</block>
</view>
</template>
<script>
import wxParseTemplate from './wxParseTemplate9';
import wxParseImg from './wxParseImg';
import wxParseVideo from './wxParseVideo';
import wxParseAudio from './wxParseAudio';
export default {
name: 'wxParseTemplate8',
props: {
node: {},
},
components: {
wxParseTemplate,
wxParseImg,
wxParseVideo,
wxParseAudio,
},
methods: {
wxParseATap(e) {
const {
href
} = e.currentTarget.dataset;
if (!href) return;
let parent = this.$parent;
while(!parent.preview || typeof parent.preview !== 'function') {
parent = parent.$parent;
}
parent.navigate(href, e);
},
},
};
</script>

View File

@ -0,0 +1,98 @@
<template>
<view>
<!--判断是否是标签节点-->
<block v-if="node.node == 'element'">
<block v-if="node.tag == 'button'">
<button type="default" size="mini">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</button>
</block>
<!--li类型-->
<block v-else-if="node.tag == 'li'">
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--video类型-->
<block v-else-if="node.tag == 'video'">
<wx-parse-video :node="node" />
</block>
<!--audio类型-->
<block v-else-if="node.tag == 'audio'">
<wx-parse-audio :node="node" />
</block>
<!--img类型-->
<block v-else-if="node.tag == 'img'">
<wx-parse-img :node="node" />
</block>
<!--a类型-->
<block v-else-if="node.tag == 'a'">
<view @click="wxParseATap" :class="node.classStr" :data-href="node.attr.href" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
<!--br类型-->
<block v-else-if="node.tag == 'br'">
<text>\n</text>
</block>
<!--其他标签-->
<block v-else>
<view :class="node.classStr" :style="node.styleStr">
<block v-for="(node, index) of node.nodes" :key="index">
<wx-parse-template :node="node" />
</block>
</view>
</block>
</block>
<!--判断是否是文本节点-->
<block v-else-if="node.node == 'text'">{{node.text}}</block>
</view>
</template>
<script>
import wxParseTemplate from './wxParseTemplate10';
import wxParseImg from './wxParseImg';
import wxParseVideo from './wxParseVideo';
import wxParseAudio from './wxParseAudio';
export default {
name: 'wxParseTemplate9',
props: {
node: {},
},
components: {
wxParseTemplate,
wxParseImg,
wxParseVideo,
wxParseAudio,
},
methods: {
wxParseATap(e) {
const {
href
} = e.currentTarget.dataset;
if (!href) return;
let parent = this.$parent;
while(!parent.preview || typeof parent.preview !== 'function') {
parent = parent.$parent;
}
parent.navigate(href, e);
},
},
};
</script>

View File

@ -0,0 +1,15 @@
<template>
<!--增加video标签支持并循环添加-->
<view :class="node.classStr" :style="node.styleStr">
<video :class="node.classStr" class="video-video" :src="node.attr.src"></video>
</view>
</template>
<script>
export default {
name: 'wxParseVideo',
props: {
node: {},
},
};
</script>

View File

@ -0,0 +1,261 @@
/**
* html2Json 改造来自: https://github.com/Jxck/html2json
*
*
* author: Di (微信小程序开发工程师)
* organization: WeAppDev(微信小程序开发论坛)(http://weappdev.com)
* 垂直微信小程序开发交流社区
*
* github地址: https://github.com/icindy/wxParse
*
* for: 微信小程序富文本解析
* detail : http://weappdev.com/t/wxparse-alpha0-1-html-markdown/184
*/
import wxDiscode from './wxDiscode';
import HTMLParser from './htmlparser';
function makeMap(str) {
const obj = {};
const items = str.split(',');
for (let i = 0; i < items.length; i += 1) obj[items[i]] = true;
return obj;
}
// Block Elements - HTML 5
const block = makeMap('br,code,address,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video');
// Inline Elements - HTML 5
const inline = makeMap('a,abbr,acronym,applet,b,basefont,bdo,big,button,cite,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var');
// Elements that you can, intentionally, leave open
// (and which close themselves)
const closeSelf = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr');
function removeDOCTYPE(html) {
const isDocument = /<body.*>([^]*)<\/body>/.test(html);
return isDocument ? RegExp.$1 : html;
}
function trimHtml(html) {
return html
.replace(/<!--.*?-->/gi, '')
.replace(/\/\*.*?\*\//gi, '')
.replace(/[ ]+</gi, '<')
.replace(/<script[^]*<\/script>/gi, '')
.replace(/<style[^]*<\/style>/gi, '');
}
function getScreenInfo() {
const screen = {};
wx.getSystemInfo({
success: (res) => {
screen.width = res.windowWidth;
screen.height = res.windowHeight;
},
});
return screen;
}
function html2json(html, customHandler, imageProp, host) {
// 处理字符串
html = removeDOCTYPE(html);
html = trimHtml(html);
html = wxDiscode.strDiscode(html);
// 生成node节点
const bufArray = [];
const results = {
nodes: [],
imageUrls: [],
};
const screen = getScreenInfo();
function Node(tag) {
this.node = 'element';
this.tag = tag;
this.$screen = screen;
}
HTMLParser(html, {
start(tag, attrs, unary) {
// node for this element
const node = new Node(tag);
if (bufArray.length !== 0) {
const parent = bufArray[0];
if (parent.nodes === undefined) {
parent.nodes = [];
}
}
if (block[tag]) {
node.tagType = 'block';
} else if (inline[tag]) {
node.tagType = 'inline';
} else if (closeSelf[tag]) {
node.tagType = 'closeSelf';
}
node.attr = attrs.reduce((pre, attr) => {
const { name } = attr;
let { value } = attr;
if (name === 'class') {
node.classStr = value;
}
// has multi attibutes
// make it array of attribute
if (name === 'style') {
node.styleStr = value;
}
if (value.match(/ /)) {
value = value.split(' ');
}
// if attr already exists
// merge it
if (pre[name]) {
if (Array.isArray(pre[name])) {
// already array, push to last
pre[name].push(value);
} else {
// single value, make it array
pre[name] = [pre[name], value];
}
} else {
// not exist, put it
pre[name] = value;
}
return pre;
}, {});
// 优化样式相关属性
if (node.classStr) {
node.classStr += ` ${node.tag}`;
} else {
node.classStr = node.tag;
}
if (node.tagType === 'inline') {
node.classStr += ' inline';
}
// 对img添加额外数据
if (node.tag === 'img') {
let imgUrl = node.attr.src;
imgUrl = wxDiscode.urlToHttpUrl(imgUrl, imageProp.domain);
Object.assign(node.attr, imageProp, {
src: imgUrl || '',
});
if (imgUrl) {
results.imageUrls.push(imgUrl);
}
}
// 处理a标签属性
if (node.tag === 'a') {
node.attr.href = node.attr.href || '';
}
// 处理font标签样式属性
if (node.tag === 'font') {
const fontSize = [
'x-small',
'small',
'medium',
'large',
'x-large',
'xx-large',
'-webkit-xxx-large',
];
const styleAttrs = {
color: 'color',
face: 'font-family',
size: 'font-size',
};
if (!node.styleStr) node.styleStr = '';
Object.keys(styleAttrs).forEach((key) => {
if (node.attr[key]) {
const value = key === 'size' ? fontSize[node.attr[key] - 1] : node.attr[key];
node.styleStr += `${styleAttrs[key]}: ${value};`;
}
});
}
// 临时记录source资源
if (node.tag === 'source') {
results.source = node.attr.src;
}
if (customHandler.start) {
customHandler.start(node, results);
}
if (unary) {
// if this tag doesn't have end tag
// like <img src="hoge.png"/>
// add to parents
const parent = bufArray[0] || results;
if (parent.nodes === undefined) {
parent.nodes = [];
}
parent.nodes.push(node);
} else {
bufArray.unshift(node);
}
},
end(tag) {
// merge into parent tag
const node = bufArray.shift();
if (node.tag !== tag) {
console.error('invalid state: mismatch end tag');
}
// 当有缓存source资源时于于video补上src资源
if (node.tag === 'video' && results.source) {
node.attr.src = results.source;
delete results.source;
}
if (customHandler.end) {
customHandler.end(node, results);
}
if (bufArray.length === 0) {
results.nodes.push(node);
} else {
const parent = bufArray[0];
if (!parent.nodes) {
parent.nodes = [];
}
parent.nodes.push(node);
}
},
chars(text) {
if (!text.trim()) return;
const node = {
node: 'text',
text,
};
if (customHandler.chars) {
customHandler.chars(node, results);
}
if (bufArray.length === 0) {
results.nodes.push(node);
} else {
const parent = bufArray[0];
if (parent.nodes === undefined) {
parent.nodes = [];
}
parent.nodes.push(node);
}
},
});
return results;
}
export default html2json;

View File

@ -0,0 +1,156 @@
/**
*
* htmlParser改造自: https://github.com/blowsie/Pure-JavaScript-HTML5-Parser
*
* author: Di (微信小程序开发工程师)
* organization: WeAppDev(微信小程序开发论坛)(http://weappdev.com)
* 垂直微信小程序开发交流社区
*
* github地址: https://github.com/icindy/wxParse
*
* for: 微信小程序富文本解析
* detail : http://weappdev.com/t/wxparse-alpha0-1-html-markdown/184
*/
// Regular Expressions for parsing tags and attributes
const startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z0-9_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/;
const endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/;
const attr = /([a-zA-Z0-9_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g;
function makeMap(str) {
const obj = {};
const items = str.split(',');
for (let i = 0; i < items.length; i += 1) obj[items[i]] = true;
return obj;
}
// Empty Elements - HTML 5
const empty = makeMap('area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr');
// Block Elements - HTML 5
const block = makeMap('address,code,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video');
// Inline Elements - HTML 5
const inline = makeMap('a,abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var');
// Elements that you can, intentionally, leave open
// (and which close themselves)
const closeSelf = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr');
// Attributes that have their values filled in disabled="disabled"
const fillAttrs = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected');
function HTMLParser(html, handler) {
let index;
let chars;
let match;
let last = html;
const stack = [];
stack.last = () => stack[stack.length - 1];
function parseEndTag(tag, tagName) {
// If no tag name is provided, clean shop
let pos;
if (!tagName) {
pos = 0;
} else {
// Find the closest opened tag of the same type
tagName = tagName.toLowerCase();
for (pos = stack.length - 1; pos >= 0; pos -= 1) {
if (stack[pos] === tagName) break;
}
}
if (pos >= 0) {
// Close all the open elements, up the stack
for (let i = stack.length - 1; i >= pos; i -= 1) {
if (handler.end) handler.end(stack[i]);
}
// Remove the open elements from the stack
stack.length = pos;
}
}
function parseStartTag(tag, tagName, rest, unary) {
tagName = tagName.toLowerCase();
if (block[tagName]) {
while (stack.last() && inline[stack.last()]) {
parseEndTag('', stack.last());
}
}
if (closeSelf[tagName] && stack.last() === tagName) {
parseEndTag('', tagName);
}
unary = empty[tagName] || !!unary;
if (!unary) stack.push(tagName);
if (handler.start) {
const attrs = [];
rest.replace(attr, function genAttr(matches, name) {
const value = arguments[2] || arguments[3] || arguments[4] || (fillAttrs[name] ? name : '');
attrs.push({
name,
value,
escaped: value.replace(/(^|[^\\])"/g, '$1\\"'), // "
});
});
if (handler.start) {
handler.start(tagName, attrs, unary);
}
}
}
while (html) {
chars = true;
if (html.indexOf('</') === 0) {
match = html.match(endTag);
if (match) {
html = html.substring(match[0].length);
match[0].replace(endTag, parseEndTag);
chars = false;
}
// start tag
} else if (html.indexOf('<') === 0) {
match = html.match(startTag);
if (match) {
html = html.substring(match[0].length);
match[0].replace(startTag, parseStartTag);
chars = false;
}
}
if (chars) {
index = html.indexOf('<');
let text = '';
while (index === 0) {
text += '<';
html = html.substring(1);
index = html.indexOf('<');
}
text += index < 0 ? html : html.substring(0, index);
html = index < 0 ? '' : html.substring(index);
if (handler.chars) handler.chars(text);
}
if (html === last) throw new Error(`Parse Error: ${html}`);
last = html;
}
// Clean up any remaining tags
parseEndTag();
}
export default HTMLParser;

View File

@ -0,0 +1,195 @@
// HTML 支持的数学符号
function strNumDiscode(str) {
str = str.replace(/&forall;/g, '∀');
str = str.replace(/&part;/g, '∂');
str = str.replace(/&exist;/g, '∃');
str = str.replace(/&empty;/g, '∅');
str = str.replace(/&nabla;/g, '∇');
str = str.replace(/&isin;/g, '∈');
str = str.replace(/&notin;/g, '∉');
str = str.replace(/&ni;/g, '∋');
str = str.replace(/&prod;/g, '∏');
str = str.replace(/&sum;/g, '∑');
str = str.replace(/&minus;/g, '');
str = str.replace(/&lowast;/g, '');
str = str.replace(/&radic;/g, '√');
str = str.replace(/&prop;/g, '∝');
str = str.replace(/&infin;/g, '∞');
str = str.replace(/&ang;/g, '∠');
str = str.replace(/&and;/g, '∧');
str = str.replace(/&or;/g, '');
str = str.replace(/&cap;/g, '∩');
str = str.replace(/&cup;/g, '');
str = str.replace(/&int;/g, '∫');
str = str.replace(/&there4;/g, '∴');
str = str.replace(/&sim;/g, '');
str = str.replace(/&cong;/g, '≅');
str = str.replace(/&asymp;/g, '≈');
str = str.replace(/&ne;/g, '≠');
str = str.replace(/&le;/g, '≤');
str = str.replace(/&ge;/g, '≥');
str = str.replace(/&sub;/g, '⊂');
str = str.replace(/&sup;/g, '⊃');
str = str.replace(/&nsub;/g, '⊄');
str = str.replace(/&sube;/g, '⊆');
str = str.replace(/&supe;/g, '⊇');
str = str.replace(/&oplus;/g, '⊕');
str = str.replace(/&otimes;/g, '⊗');
str = str.replace(/&perp;/g, '⊥');
str = str.replace(/&sdot;/g, '⋅');
return str;
}
// HTML 支持的希腊字母
function strGreeceDiscode(str) {
str = str.replace(/&Alpha;/g, 'Α');
str = str.replace(/&Beta;/g, 'Β');
str = str.replace(/&Gamma;/g, 'Γ');
str = str.replace(/&Delta;/g, 'Δ');
str = str.replace(/&Epsilon;/g, 'Ε');
str = str.replace(/&Zeta;/g, 'Ζ');
str = str.replace(/&Eta;/g, 'Η');
str = str.replace(/&Theta;/g, 'Θ');
str = str.replace(/&Iota;/g, 'Ι');
str = str.replace(/&Kappa;/g, 'Κ');
str = str.replace(/&Lambda;/g, 'Λ');
str = str.replace(/&Mu;/g, 'Μ');
str = str.replace(/&Nu;/g, 'Ν');
str = str.replace(/&Xi;/g, 'Ν');
str = str.replace(/&Omicron;/g, 'Ο');
str = str.replace(/&Pi;/g, 'Π');
str = str.replace(/&Rho;/g, 'Ρ');
str = str.replace(/&Sigma;/g, 'Σ');
str = str.replace(/&Tau;/g, 'Τ');
str = str.replace(/&Upsilon;/g, 'Υ');
str = str.replace(/&Phi;/g, 'Φ');
str = str.replace(/&Chi;/g, 'Χ');
str = str.replace(/&Psi;/g, 'Ψ');
str = str.replace(/&Omega;/g, 'Ω');
str = str.replace(/&alpha;/g, 'α');
str = str.replace(/&beta;/g, 'β');
str = str.replace(/&gamma;/g, 'γ');
str = str.replace(/&delta;/g, 'δ');
str = str.replace(/&epsilon;/g, 'ε');
str = str.replace(/&zeta;/g, 'ζ');
str = str.replace(/&eta;/g, 'η');
str = str.replace(/&theta;/g, 'θ');
str = str.replace(/&iota;/g, 'ι');
str = str.replace(/&kappa;/g, 'κ');
str = str.replace(/&lambda;/g, 'λ');
str = str.replace(/&mu;/g, 'μ');
str = str.replace(/&nu;/g, 'ν');
str = str.replace(/&xi;/g, 'ξ');
str = str.replace(/&omicron;/g, 'ο');
str = str.replace(/&pi;/g, 'π');
str = str.replace(/&rho;/g, 'ρ');
str = str.replace(/&sigmaf;/g, 'ς');
str = str.replace(/&sigma;/g, 'σ');
str = str.replace(/&tau;/g, 'τ');
str = str.replace(/&upsilon;/g, 'υ');
str = str.replace(/&phi;/g, 'φ');
str = str.replace(/&chi;/g, 'χ');
str = str.replace(/&psi;/g, 'ψ');
str = str.replace(/&omega;/g, 'ω');
str = str.replace(/&thetasym;/g, 'ϑ');
str = str.replace(/&upsih;/g, 'ϒ');
str = str.replace(/&piv;/g, 'ϖ');
str = str.replace(/&middot;/g, '·');
return str;
}
function strcharacterDiscode(str) {
// 加入常用解析
str = str.replace(/&nbsp;/g, ' ');
str = str.replace(/&ensp;/g, '');
str = str.replace(/&emsp;/g, ' ');
str = str.replace(/&quot;/g, "'");
str = str.replace(/&amp;/g, '&');
str = str.replace(/&lt;/g, '<');
str = str.replace(/&gt;/g, '>');
str = str.replace(/&#8226;/g, '•');
return str;
}
// HTML 支持的其他实体
function strOtherDiscode(str) {
str = str.replace(/&OElig;/g, 'Œ');
str = str.replace(/&oelig;/g, 'œ');
str = str.replace(/&Scaron;/g, 'Š');
str = str.replace(/&scaron;/g, 'š');
str = str.replace(/&Yuml;/g, 'Ÿ');
str = str.replace(/&fnof;/g, 'ƒ');
str = str.replace(/&circ;/g, 'ˆ');
str = str.replace(/&tilde;/g, '˜');
str = str.replace(/&ensp;/g, '');
str = str.replace(/&emsp;/g, '');
str = str.replace(/&thinsp;/g, '');
str = str.replace(/&zwnj;/g, '');
str = str.replace(/&zwj;/g, '');
str = str.replace(/&lrm;/g, '');
str = str.replace(/&rlm;/g, '');
str = str.replace(/&ndash;/g, '');
str = str.replace(/&mdash;/g, '—');
str = str.replace(/&lsquo;/g, '');
str = str.replace(/&rsquo;/g, '');
str = str.replace(/&sbquo;/g, '');
str = str.replace(/&ldquo;/g, '“');
str = str.replace(/&rdquo;/g, '”');
str = str.replace(/&bdquo;/g, '„');
str = str.replace(/&dagger;/g, '†');
str = str.replace(/&Dagger;/g, '‡');
str = str.replace(/&bull;/g, '•');
str = str.replace(/&hellip;/g, '…');
str = str.replace(/&permil;/g, '‰');
str = str.replace(/&prime;/g, '');
str = str.replace(/&Prime;/g, '″');
str = str.replace(/&lsaquo;/g, '');
str = str.replace(/&rsaquo;/g, '');
str = str.replace(/&oline;/g, '‾');
str = str.replace(/&euro;/g, '€');
str = str.replace(/&trade;/g, '™');
str = str.replace(/&larr;/g, '←');
str = str.replace(/&uarr;/g, '↑');
str = str.replace(/&rarr;/g, '→');
str = str.replace(/&darr;/g, '↓');
str = str.replace(/&harr;/g, '↔');
str = str.replace(/&crarr;/g, '↵');
str = str.replace(/&lceil;/g, '⌈');
str = str.replace(/&rceil;/g, '⌉');
str = str.replace(/&lfloor;/g, '⌊');
str = str.replace(/&rfloor;/g, '⌋');
str = str.replace(/&loz;/g, '◊');
str = str.replace(/&spades;/g, '♠');
str = str.replace(/&clubs;/g, '♣');
str = str.replace(/&hearts;/g, '♥');
str = str.replace(/&diams;/g, '♦');
str = str.replace(/&#39;/g, "'");
return str;
}
function strDiscode(str) {
str = strNumDiscode(str);
str = strGreeceDiscode(str);
str = strcharacterDiscode(str);
str = strOtherDiscode(str);
return str;
}
function urlToHttpUrl(url, domain) {
if (/^\/\//.test(url)) {
return `https:${url}`;
} else if (/^\//.test(url)) {
return `https://${domain}${url}`;
}
return url;
}
export default {
strDiscode,
urlToHttpUrl,
};

View File

@ -0,0 +1,102 @@
## uParse 适用于 uni-app/mpvue 的富文本解析组件
> 支持 Html、Markdown 解析Fork自: [mpvue-wxParse](https://github.com/F-loat/mpvue-wxParse)
## 属性
| 名称 | 类型 | 默认值 | 描述 |
| -----------------|--------------- | ------------- | ---------------- |
| loading | Boolean | false | 数据加载状态 |
| className | String | — | 自定义 class 名称 |
| content | String | — | 渲染内容 |
| noData | String | 数据不能为空 | 空数据时的渲染展示 |
| startHandler | Function | 见源码 | 自定义 parser 函数 |
| endHandler | Function | null | 自定义 parser 函数 |
| charsHandler | Function | null | 自定义 parser 函数 |
| imageProp | Object | 见下文 | 图片相关参数 |
### 自定义 parser 函数具体介绍
* 传入的参数为当前节点 `node` 对象及解析结果 `results` 对象,例如 `startHandler(node, results)`
* 无需返回值,通过对传入的参数直接操作来完成需要的改动
* 自定义函数会在原解析函数处理之后执行
### imageProp 对象具体属性
| 名称 | 类型 | 默认值 | 描述 |
| -----------------|--------------- | ------------- | ------------------ |
| mode | String | 'aspectFit' | 图片裁剪、缩放的模式 |
| padding | Number | 0 | 图片内边距 |
| lazyLoad | Boolean | false | 图片懒加载 |
| domain | String | '' | 图片服务域名 |
## 事件
| 名称 | 参数 | 描述 |
| -----------------|----------------- | ---------------- |
| preview | 图片地址,原始事件 | 预览图片时触发 |
| navigate | 链接地址,原始事件 | 点击链接时触发 |
## 基本使用方法
``` vue
<template>
<div>
<u-parse :content="article" @preview="preview" @navigate="navigate" />
</div>
</template>
<script>
import uParse from '@/components/u-parse/u-parse.vue'
export default {
components: {
uParse
},
data () {
return {
article: '<div>我是HTML代码</div>'
}
},
methods: {
preview(src, e) {
// do something
},
navigate(href, e) {
// do something
}
}
}
</script>
<style>
@import url("@/components/u-parse/u-parse.css");
</style>
```
## 渲染 Markdown
> 先将 markdown 转换为 html 即可
```
npm install marked
```
``` js
import marked from 'marked'
import uParse from '@/components/u-parse/u-parse.vue'
export default {
components: {
uParse
},
data () {
return {
article: marked(`#hello, markdown!`)
}
}
}
```

View File

@ -0,0 +1,232 @@
/**
* author: Di (微信小程序开发工程师)
* organization: WeAppDev(微信小程序开发论坛)(http://weappdev.com)
* 垂直微信小程序开发交流社区
*
* github地址: https://github.com/icindy/wxParse
*
* for: 微信小程序富文本解析
* detail : http://weappdev.com/t/wxparse-alpha0-1-html-markdown/184
*/
.wxParse {
width: 100%;
font-family: Helvetica, sans-serif;
font-size: 30upx;
color: #666;
line-height: 1.8;
}
.wxParse view {
word-break: hyphenate;
}
.wxParse .inline {
display: inline;
margin: 0;
padding: 0;
}
.wxParse .div {
margin: 0;
padding: 0;
}
.wxParse .h1 .text {
font-size: 2em;
margin: 0.67em 0;
}
.wxParse .h2 .text {
font-size: 1.5em;
margin: 0.83em 0;
}
.wxParse .h3 .text {
font-size: 1.17em;
margin: 1em 0;
}
.wxParse .h4 .text {
margin: 1.33em 0;
}
.wxParse .h5 .text {
font-size: 0.83em;
margin: 1.67em 0;
}
.wxParse .h6 .text {
font-size: 0.67em;
margin: 2.33em 0;
}
.wxParse .h1 .text,
.wxParse .h2 .text,
.wxParse .h3 .text,
.wxParse .h4 .text,
.wxParse .h5 .text,
.wxParse .h6 .text,
.wxParse .b,
.wxParse .strong {
font-weight: bolder;
}
.wxParse .p {
margin: 1em 0;
}
.wxParse .i,
.wxParse .cite,
.wxParse .em,
.wxParse .var,
.wxParse .address {
font-style: italic;
}
.wxParse .pre,
.wxParse .tt,
.wxParse .code,
.wxParse .kbd,
.wxParse .samp {
font-family: monospace;
}
.wxParse .pre {
overflow: auto;
background: #f5f5f5;
/* padding: 16upx; */
white-space: pre;
margin: 1em 0upx;
}
.wxParse .code {
display: inline;
background: #f5f5f5;
}
.wxParse .big {
font-size: 1.17em;
}
.wxParse .small,
.wxParse .sub,
.wxParse .sup {
font-size: 0.83em;
}
.wxParse .sub {
vertical-align: sub;
}
.wxParse .sup {
vertical-align: super;
}
.wxParse .s,
.wxParse .strike,
.wxParse .del {
text-decoration: line-through;
}
.wxParse .strong,
.wxParse .s {
display: inline;
}
.wxParse .a {
color: deepskyblue;
}
.wxParse .video {
text-align: center;
margin: 22upx 0;
}
.wxParse .video-video {
width: 100%;
}
.wxParse .img {
display: inline-block;
width: 0;
height: 0;
max-width: 100%;
overflow: hidden;
}
.wxParse .blockquote {
margin: 10upx 0;
padding: 22upx 0 22upx 22upx;
font-family: Courier, Calibri, "宋体";
background: #f5f5f5;
border-left: 6upx solid #dbdbdb;
}
.wxParse .blockquote .p {
margin: 0;
}
.wxParse .ul, .wxParse .ol {
display: block;
margin: 1em 0;
padding-left: 33upx;
}
.wxParse .ol {
list-style-type: disc;
}
.wxParse .ol {
list-style-type: decimal;
}
.wxParse .ol>weixin-parse-template,.wxParse .ul>weixin-parse-template {
display: list-item;
align-items: baseline;
text-align: match-parent;
}
.wxParse .ol>.li,.wxParse .ul>.li {
display: list-item;
align-items: baseline;
text-align: match-parent;
}
.wxParse .ul .ul, .wxParse .ol .ul {
list-style-type: circle;
}
.wxParse .ol .ol .ul, .wxParse .ol .ul .ul, .wxParse .ul .ol .ul, .wxParse .ul .ul .ul {
list-style-type: square;
}
.wxParse .u {
text-decoration: underline;
}
.wxParse .hide {
display: none;
}
.wxParse .del {
display: inline;
}
.wxParse .figure {
overflow: hidden;
}
.wxParse .table {
width: 100%;
}
.wxParse .thead, .wxParse .tfoot, .wxParse .tr {
display: flex;
flex-direction: row;
}
.wxParse .tr {
width:100%;
display: flex;
border-right: 2upx solid #e0e0e0;
border-bottom: 2upx solid #e0e0e0;
}
.wxParse .th,
.wxParse .td {
display: flex;
width: 1276upx;
overflow: auto;
flex: 1;
padding: 11upx;
border-left: 2upx solid #e0e0e0;
}
.wxParse .td:last {
border-top: 2upx solid #e0e0e0;
}
.wxParse .th {
background: #f0f0f0;
border-top: 2upx solid #e0e0e0;
}

View File

@ -0,0 +1,118 @@
<!--**
* forked fromhttps://github.com/F-loat/mpvue-wxParse
*
* github地址: https://github.com/dcloudio/uParse
*
* for: uni-app框架下 富文本解析
*/-->
<template>
<!--基础元素-->
<div class="wxParse" :class="className" v-if="!loading">
<block v-for="(node,index) of nodes" :key="index">
<wxParseTemplate :node="node" />
</block>
</div>
</template>
<script>
import HtmlToJson from './libs/html2json';
import wxParseTemplate from './components/wxParseTemplate0';
export default {
name: 'wxParse',
props: {
loading: {
type: Boolean,
default: false,
},
className: {
type: String,
default: '',
},
content: {
type: String,
default: '',
},
noData: {
type: String,
default: '<div style="color: red;">数据不能为空</div>',
},
startHandler: {
type: Function,
default() {
return (node) => {
node.attr.class = null;
node.attr.style = null;
};
},
},
endHandler: {
type: Function,
default: null,
},
charsHandler: {
type: Function,
default: null,
},
imageProp: {
type: Object,
default() {
return {
mode: 'aspectFit',
padding: 0,
lazyLoad: false,
domain: '',
};
},
},
},
components: {
wxParseTemplate,
},
data() {
return {
imageUrls: [],
};
},
computed: {
nodes() {
const {
content,
noData,
imageProp,
startHandler,
endHandler,
charsHandler,
} = this;
const parseData = content || noData;
const customHandler = {
start: startHandler,
end: endHandler,
chars: charsHandler,
};
const results = HtmlToJson(parseData, customHandler, imageProp, this);
this.imageUrls = results.imageUrls;
console.log(results)
return results.nodes;
},
},
methods: {
navigate(href, $event) {
this.$emit('navigate', href, $event);
},
preview(src, $event) {
if (!this.imageUrls.length) return;
wx.previewImage({
current: src,
urls: this.imageUrls,
});
this.$emit('preview', src, $event);
},
removeImageUrl(src) {
const { imageUrls } = this;
imageUrls.splice(imageUrls.indexOf(src), 1);
},
},
};
</script>

View File

@ -0,0 +1,55 @@
<!-- 正在开发中提示组件 -->
<template>
<view class="underDevelopment" :class="{ fullScreen: fullScreen }">
<image class="tipsImg" :src="tipsImg" mode=""></image>
<text class="tipsTitle">{{ tipsTitle }}</text>
</view>
</template>
<script>
export default {
name: 'UnderDevelopment',
props: {
tipsImg: {
type: String,
default: '/static/components/pic_comingsoon@2x.png'
},
tipsTitle: {
type: String,
default: '正在研发中,敬请期待~'
},
//
fullScreen: {
type: Boolean,
default: true
}
}
}
</script>
<style lang="scss" scoped>
.underDevelopment {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
&.fullScreen {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.tipsImg {
}
.tipsTitle {
margin-top: 40rpx;
font-size: 40rpx;
color: #404C69;
font-weight: bold;
}
}
</style>

View File

@ -0,0 +1,109 @@
<template>
<view v-if="width" :style="'width:'+width+';'+(square?'height:'+width:'')" class="uni-grid-item">
<view :class="{ 'uni-grid-item--border': showBorder, 'uni-grid-item--border-top': showBorder && index < column, 'uni-highlight': highlight }"
:style="{ 'border-right-color': borderColor ,'border-bottom-color': borderColor ,'border-top-color': borderColor }"
class="uni-grid-item__box" @click="_onClick">
<slot />
</view>
</view>
</template>
<script>
export default {
name: 'UniGridItem',
inject: ['grid'],
props: {
index:{
type: Number,
default: 0
}
},
data() {
return {
column: 0,
showBorder: true,
square: true,
highlight: true,
left: 0,
top: 0,
openNum: 2,
width: 0,
borderColor: '#e5e5e5'
}
},
created() {
this.column = this.grid.column
this.showBorder = this.grid.showBorder
this.square = this.grid.square
this.highlight = this.grid.highlight
this.top = this.hor === 0 ? this.grid.hor : this.hor
this.left = this.ver === 0 ? this.grid.ver : this.ver
this.borderColor = this.grid.borderColor
this.grid.children.push(this)
// this.grid.init()
this.width = this.grid.width
},
beforeDestroy() {
this.grid.children.forEach((item, index) => {
if (item === this) {
this.grid.children.splice(index, 1)
}
})
},
methods: {
_onClick() {
this.grid.change({
detail: {
index: this.index
}
})
this.$emit('click')
}
}
}
</script>
<style lang="scss" scoped>
.uni-grid-item {
/* #ifndef APP-NVUE */
height: 100%;
display: flex;
/* #endif */
}
.uni-grid-item__box {
/* #ifndef APP-NVUE */
display: flex;
width: 100%;
/* #endif */
position: relative;
flex: 1;
flex-direction: column;
// justify-content: center;
// align-items: center;
}
.uni-grid-item--border {
position: relative;
border-bottom-color: $uni-border-color;
border-bottom-style: solid;
border-bottom-width: 1px;
border-right-color: $uni-border-color;
border-right-style: solid;
border-right-width: 1px;
}
.uni-grid-item--border-top {
border-top-color: $uni-border-color;
border-top-style: solid;
border-top-width: 1px;
/* #ifndef APP-NVUE */
height: 100%;
box-sizing: border-box;
/* #endif */
}
.uni-highlight:active {
background-color: $uni-bg-color-hover;
}
</style>

View File

@ -0,0 +1,120 @@
<template>
<view class="uni-grid-wrap">
<view :id="elId" ref="uni-grid" class="uni-grid" :class="{ 'uni-grid--border': showBorder }" :style="{ 'border-left-style':'solid','border-left-color':borderColor, 'border-left-width':showBorder?'1px':0 }">
<slot />
</view>
</view>
</template>
<script>
// #ifdef APP-NVUE
const dom = uni.requireNativePlugin('dom');
// #endif
export default {
name: 'UniGrid',
props: {
//
column: {
type: Number,
default: 3
},
//
showBorder: {
type: Boolean,
default: true
},
//
borderColor: {
type: String,
default: '#e5e5e5'
},
// , true
square: {
type: Boolean,
default: true
},
highlight: {
type: Boolean,
default: true
}
},
provide() {
return {
grid: this
}
},
data() {
const elId = `Uni_${Math.ceil(Math.random() * 10e5).toString(36)}`
return {
elId,
width: 0
}
},
created() {
this.children = []
},
mounted() {
this.init()
},
methods: {
init() {
setTimeout(() => {
this._getSize((width) => {
this.children.forEach((item, index) => {
item.width = width
})
})
}, 50)
},
change(e) {
this.$emit('change', e)
},
_getSize(fn) {
// #ifndef APP-NVUE
uni.createSelectorQuery()
.in(this)
.select(`#${this.elId}`)
.boundingClientRect()
.exec(ret => {
this.width = parseInt((ret[0].width-1) / this.column) + 'px'
fn(this.width)
})
// #endif
// #ifdef APP-NVUE
dom.getComponentRect(this.$refs['uni-grid'], (ret) => {
this.width = parseInt((ret.size.width-1) / this.column) + 'px'
fn(this.width)
})
// #endif
}
}
}
</script>
<style lang="scss" scoped>
.uni-grid-wrap {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex: 1;
flex-direction: column;
/* #ifdef H5 */
width: 100%;
/* #endif */
}
.uni-grid {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
// flex: 1;
flex-direction: row;
flex-wrap: wrap;
}
.uni-grid--border {
border-left-color: $uni-border-color;
border-left-style: solid;
border-left-width: 1px;
}
</style>

View File

@ -0,0 +1,96 @@
export default {
'contact': '\ue100',
'person': '\ue101',
'personadd': '\ue102',
'contact-filled': '\ue130',
'person-filled': '\ue131',
'personadd-filled': '\ue132',
'phone': '\ue200',
'email': '\ue201',
'chatbubble': '\ue202',
'chatboxes': '\ue203',
'phone-filled': '\ue230',
'email-filled': '\ue231',
'chatbubble-filled': '\ue232',
'chatboxes-filled': '\ue233',
'weibo': '\ue260',
'weixin': '\ue261',
'pengyouquan': '\ue262',
'chat': '\ue263',
'qq': '\ue264',
'videocam': '\ue300',
'camera': '\ue301',
'mic': '\ue302',
'location': '\ue303',
'mic-filled': '\ue332',
'speech': '\ue332',
'location-filled': '\ue333',
'micoff': '\ue360',
'image': '\ue363',
'map': '\ue364',
'compose': '\ue400',
'trash': '\ue401',
'upload': '\ue402',
'download': '\ue403',
'close': '\ue404',
'redo': '\ue405',
'undo': '\ue406',
'refresh': '\ue407',
'star': '\ue408',
'plus': '\ue409',
'minus': '\ue410',
'circle': '\ue411',
'checkbox': '\ue411',
'close-filled': '\ue434',
'clear': '\ue434',
'refresh-filled': '\ue437',
'star-filled': '\ue438',
'plus-filled': '\ue439',
'minus-filled': '\ue440',
'circle-filled': '\ue441',
'checkbox-filled': '\ue442',
'closeempty': '\ue460',
'refreshempty': '\ue461',
'reload': '\ue462',
'starhalf': '\ue463',
'spinner': '\ue464',
'spinner-cycle': '\ue465',
'search': '\ue466',
'plusempty': '\ue468',
'forward': '\ue470',
'back': '\ue471',
'left-nav': '\ue471',
'checkmarkempty': '\ue472',
'home': '\ue500',
'navigate': '\ue501',
'gear': '\ue502',
'paperplane': '\ue503',
'info': '\ue504',
'help': '\ue505',
'locked': '\ue506',
'more': '\ue507',
'flag': '\ue508',
'home-filled': '\ue530',
'gear-filled': '\ue532',
'info-filled': '\ue534',
'help-filled': '\ue535',
'more-filled': '\ue537',
'settings': '\ue560',
'list': '\ue562',
'bars': '\ue563',
'loop': '\ue565',
'paperclip': '\ue567',
'eye': '\ue568',
'arrowup': '\ue580',
'arrowdown': '\ue581',
'arrowleft': '\ue582',
'arrowright': '\ue583',
'arrowthinup': '\ue584',
'arrowthindown': '\ue585',
'arrowthinleft': '\ue586',
'arrowthinright': '\ue587',
'pulldown': '\ue588',
'closefill': '\ue589',
'sound': '\ue590',
'scan': '\ue612'
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,237 @@
<template>
<view class="uni-navbar">
<view :class="{ 'uni-navbar--fixed': fixed, 'uni-navbar--shadow': shadow, 'uni-navbar--border': border }" :style="{ 'background-color': backgroundColor }"
class="uni-navbar__content">
<uni-status-bar v-if="statusBar" />
<view :style="{ color: color,backgroundColor: backgroundColor }" class="uni-navbar__header uni-navbar__content_view">
<!-- <slot name="leftCustom"> -->
<view @tap="onClickLeft" class="uni-navbar__header-btns uni-navbar__header-btns-left uni-navbar__content_view">
<view class="uni-navbar__content_view" v-if="leftIcon.length">
<uni-icons :color="color" :type="leftIcon" size="24" />
</view>
<view :class="{ 'uni-navbar-btn-icon-left': !leftIcon.length }" class="uni-navbar-btn-text uni-navbar__content_view"
v-if="leftText.length">
<text :style="{ color: color, fontSize: '14px' }">{{ leftText }}</text>
</view>
<slot name="left" />
</view>
<!-- </slot> -->
<view class="uni-navbar__header-container uni-navbar__content_view">
<view class="uni-navbar__header-container-inner uni-navbar__content_view" v-if="title.length">
<text class="uni-nav-bar-text" :style="{color: color }">{{ title }}</text>
</view>
<!-- 标题插槽 -->
<slot />
</view>
<view v-if="rightShow" :class="title.length ? 'uni-navbar__header-btns-right' : ''" @tap="onClickRight" class="uni-navbar__header-btns uni-navbar__content_view">
<view class="uni-navbar__content_view" v-if="rightIcon.length">
<uni-icons :color="color" :type="rightIcon" size="24" />
</view>
<!-- 优先显示图标 -->
<view class="uni-navbar-btn-text uni-navbar__content_view" v-if="rightText.length && !rightIcon.length">
<text class="uni-nav-bar-right-text" :style="{color: rightTextColor}">{{ rightText }}</text>
</view>
<slot name="right" />
</view>
</view>
</view>
<view class="uni-navbar__placeholder" v-if="fixed">
<uni-status-bar v-if="statusBar" />
<view class="uni-navbar__placeholder-view" />
</view>
</view>
</template>
<script>
import uniStatusBar from "../uni-status-bar/uni-status-bar.vue";
import uniIcons from "../uni-icons/uni-icons.vue";
export default {
name: "UniNavBar",
components: {
uniStatusBar,
uniIcons
},
props: {
title: {
type: String,
default: ""
},
leftText: {
type: String,
default: ""
},
rightText: {
type: String,
default: ""
},
leftIcon: {
type: String,
default: ""
},
rightIcon: {
type: String,
default: ""
},
fixed: {
type: [Boolean, String],
default: false
},
color: {
type: String,
default: "#000000"
},
rightTextColor: {
type: String,
default: "#000000"
},
backgroundColor: {
type: String,
default: "#FFFFFF"
},
statusBar: {
type: [Boolean, String],
default: false
},
shadow: {
type: [String, Boolean],
default: false
},
border: {
type: [String, Boolean],
default: false
},
//
rightShow: {
type: [String, Boolean],
default: true
}
},
mounted() {
if(uni.report && this.title !== '') {
uni.report('title', this.title)
}
},
methods: {
onClickLeft() {
this.$emit("clickLeft");
},
onClickRight() {
this.$emit("clickRight");
}
}
};
</script>
<style lang="scss" scoped>
$nav-height: 50px;
.uni-nav-bar-text {
/* #ifdef APP-PLUS */
font-size: 34rpx;
/* #endif */
/* #ifndef APP-PLUS */
font-size: $uni-font-size-lg;
/* #endif */
font-weight: bold;
}
.uni-nav-bar-right-text {
font-size: $uni-font-size-base;
}
.uni-navbar {
width: 750rpx;
}
.uni-navbar__content {
position: relative;
width: 750rpx;
background-color: $uni-bg-color;
overflow: hidden;
}
.uni-navbar__content_view {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
flex-direction: row;
// background-color: #FFFFFF;
}
.uni-navbar__header {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
width: 750rpx;
height: $nav-height;
line-height: $nav-height;
font-size: 16px;
// background-color: #ffffff;
}
.uni-navbar__header-btns {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-wrap: nowrap;
width: 120rpx;
padding: 0 6px;
justify-content: center;
align-items: center;
}
.uni-navbar__header-btns-left {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
width: 150rpx;
justify-content: flex-start;
}
.uni-navbar__header-btns-right {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
width: 160rpx;
padding-right: 30rpx;
justify-content: flex-end;
}
.uni-navbar__header-container {
flex: 1;
}
.uni-navbar__header-container-inner {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex: 1;
align-items: center;
justify-content: center;
font-size: $uni-font-size-base;
}
.uni-navbar__placeholder-view {
height: $nav-height;
}
.uni-navbar--fixed {
position: fixed;
z-index: 998;
}
.uni-navbar--shadow {
/* #ifndef APP-NVUE */
box-shadow: 0 1px 6px #ccc;
/* #endif */
}
.uni-navbar--border {
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: $uni-border-color;
}
</style>

View File

@ -0,0 +1,25 @@
<template>
<view :style="{ height: statusBarHeight }" class="uni-status-bar">
<slot />
</view>
</template>
<script>
var statusBarHeight = uni.getSystemInfoSync().statusBarHeight + 'px'
export default {
name: 'UniStatusBar',
data() {
return {
statusBarHeight: statusBarHeight
}
}
}
</script>
<style lang="scss" scoped>
.uni-status-bar {
width: 750rpx;
height: 20px;
// height: var(--status-bar-height);
}
</style>

View File

@ -0,0 +1,157 @@
<!-- 虚拟按钮可设置圆角背景边框等 -->
<template>
<view class="virtualButton" :style="[btnStyle]" :class="{ 'blockBtn': block }" @click.stop="clickHandle">
<slot>
<text>虚拟按钮</text>
</slot>
</view>
</template>
<script>
export default {
name: 'VirtualButton',
props: {
//
btnHeight: {
type: [Number, String],
default: 64
},
// [ 'default', 'primary''dange' ]
type: {
type: String,
default: 'default'
},
//
border: {
type: Boolean,
default: false
},
// [ 'solid', 'dashed' ]
borderType: {
type: String,
default: 'solid'
},
//
borderColor: {
type: String,
default: '#564F5F'
},
//
block: {
type: Boolean,
default: false
},
// [ 'circle', 'round' ]
shape: {
type: String,
default: ''
},
//
ghost: {
type: Boolean,
default: false
}
},
data() {
return {
//
btnTypeSet: [ 'default', 'primary', 'dange', 'grey', 'D7E1FD', '376AF5', 'F19149', '61596C', 'F7CE68', 'FFCF00' ],
//
btnShapeSet: [ 'circle', 'round' ],
//
bgColor: {
'default': '#fff',
'primary': '#5FC48D',
'dange': '#FE4066',
'grey': '#999999',
'D7E1FD': '#D7E1FD',
'376AF5': '#376AF5',
'F19149': '#F19149',
'61596C': '#61596C',
'F7CE68': '#F7CE68',
'FFCF00': '#FFCF00',
},
//
fontColor: {
'default': '#999999',
'primary': '#FFFFFF',
'dange': '#FFFFFF',
'grey': '#FFFFFF',
'D7E1FD': '#FFFFFF',
'376AF5': '#FFFFFF',
'F19149': '#FFFFFF',
'61596C': '#FFFFFF',
'F7CE68': '#FFFFFF',
'FFCF00': '#FFFFFF',
}
};
},
computed: {
//
btnStyle () {
let _style = {
"height": this.btnHeight + 'rpx',
"line-height": this.btnHeight + 'rpx'
}
// /
if (this.btnTypeSet.indexOf(this.type) !== -1) {
if (this.ghost) {
_style['background-color'] = ''
_style['color'] = this.bgColor[this.type]
} else {
_style['background-color'] = this.bgColor[this.type]
_style['color'] = this.fontColor[this.type]
}
}
//
if (this.border) {
_style['border-width'] = '2rpx'
_style['border-style'] = this.borderType
if (this.ghost) {
_style['border-color'] = this.bgColor[this.type]
} else {
_style['border-color'] = this.borderColor
}
}
//
if (this.btnShapeSet.indexOf(this.shape) !== -1) {
if (this.shape === 'circle' && !this.block) {
_style['min-width'] = this.btnHeight + 'rpx'
} else {
_style['padding'] = '0 30rpx'
}
_style['border-radius'] = this.btnHeight + 'rpx'
} else {
_style['padding'] = '0 30rpx'
}
return _style
}
},
methods: {
clickHandle () {
this.$emit('click')
}
}
}
</script>
<style lang="scss" scoped>
.virtualButton {
display: inline-block;
font-size: 26rpx;
color: #999999;
text-align: center;
&.blockBtn {
display: flex;
align-items: center;
justify-content: center;
}
text {
display: inline-block;
height: inherit;
line-height: inherit;
}
}
</style>

View File

@ -0,0 +1,306 @@
<template>
<view class="page">
<view class="express">
<view class="shop">
<image :src="img" mode="aspectFit" class="express-img"></image>
</view>
<view class="express-detail">
<view class="status">{{wlInfo.logisticsStatus == 1 ? '已签收' : '配送中'}}</view>
<view class="name">{{name}}</view>
<view class="no">{{wlInfo.brand}} <text>{{wlInfo.no}}</text></view>
</view>
</view>
<!-- 收货地址 -->
<view class="content bgf">
<view>
<view class="flex list">
<!-- <view class="time"></view> -->
<view class="info flex1">
<view class="title address">[收货地址]{{address}}</view>
</view>
</view>
<!-- :class="{one: index+1 == wlInfo.item.length && wlInfo.delivery_status == 1}" -->
<view class="flex list" v-for="(item, index) in wlInfo.items" :key="index" :class="{one: index == 0}">
<!-- <view class="time">
<view class="day">{{item.time.split(' ')[0]}}</view>
<view>{{item.time.split(' ')[1]}}</view>
</view> -->
<view class="info flex1">
<view class="title" :class="{active:index == 0 && logisticsStatus == 1}">{{index == 0 && logisticsStatus == 1 ? '已签收' : '配送中'}}</view>
<view class="time-new">{{item.time.split(' ')[0]}} {{item.time.split(' ')[1]}}</view>
<view class="text">{{item.context}}</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
wlInfo: {
type: Object,
default: () => ({})
},
img:{
type:String,
default:''
},
name:{
type:String,
default:''
},
address:{
type:String,
default:''
},
logisticsStatus:{
type:Number,
default:0
}
},
data() {
return {}
},
mounted() {
console.log(this.wlInfo)
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.page {
max-height: 100vh;
/* #ifdef H5 */
max-height: calc(100vh - 44px);
/* #endif */
font-size: 24rpx;
background-color: #fff;
padding: 0 32rpx;
// overflow-y: auto;
}
/*flex 转换成flex容器*/
.flex {
display: flex;
flex-direction: row;
}
/*flex1 自动填充*/
.flex1 {
flex: 1;
}
/*ali-c 竖直居中*/
.ali-c {
display: flex;
flex-direction: row;
align-items: center;
}
.bgf {
background-color: #fff;
}
.express{
display: flex;
align-items: center;
padding-bottom: 32rpx;
border-bottom: 1px solid #F2F2F2;
.shop{
width: 160rpx;
height: 160rpx;
border-radius: 0 24rpx 24rpx 0;
background-image: url('@/static/new/shop-bg.png');
background-size: contain;
display: flex;
align-items: center;
justify-content: center;
.express-img{
width: 142rpx;
height: 132rpx;
}
}
.express-detail{
margin-left: 24rpx;
.status{
font-weight: bold;
font-size: 32rpx;
color: #000000;
}
.name{
width: 500rpx;
font-weight: 500;
font-size: 24rpx;
color: #999999;
margin-top: 24rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.no{
width: 500rpx;
margin-top: 8rpx;
font-weight: 500;
font-size: 24rpx;
color: #999999;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
//
.content {
// margin: 20rpx;
padding: 56rpx 0rpx 56rpx 0rpx;
border-radius: 20rpx;
.list {
&:first-child {
.info::before {
bottom: -20rpx;
margin-top: 40rpx;
border-left: 1px dashed #e5e5e5;
}
.title {
margin-bottom: 56rpx;
&::before {
width: 46rpx;
height: 46rpx;
left: -23rpx;
display:flex;
align-items: center;
justify-content: center;
background-image: url('@/static/new/mine/sh.png');
background-size: 46rpx 46rpx;
}
}
}
&:last-child {
.info::before {
height: 32rpx;
}
}
&.one {
.info::before {
margin-top: 20rpx;
}
.title {
color: #1a1a1a;
&::before {
width: 46rpx;
height: 46rpx;
left: -23rpx;
background: url('@/static/new/mine/yun.png') no-repeat center;
background-size: 46rpx 46rpx;
}
}
.active{
color: #1a1a1a;
&::before {
width: 46rpx;
height: 46rpx;
left: -23rpx;
background: url('@/static/new/mine/pai.png') no-repeat center;
background-size: 46rpx 46rpx;
}
}
.text {
color: #333333;
}
.time {
font-size: 16rpx;
color: #888;
.day {
font-size: 22rpx;
}
}
}
}
.time {
width: 100rpx;
padding-right: 30rpx;
text-align: right;
font-size: 16rpx;
color: #888888;
.day {
margin-bottom: 4rpx;
font-size: 22rpx;
color: #333333;
}
}
.info {
position: relative;
padding-top: 12rpx;
color: #999;
&::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
z-index: 1;
width: 0;
border-left: 1px solid #e5e5e5;
}
.title {
position: relative;
margin-bottom: 10rpx;
padding-left: 32rpx;
font-size: 28rpx;
&::before {
content: "";
position: absolute;
left: -3px;
top: 0;
bottom: 0;
z-index: 1;
width: 7px;
height: 7px;
margin: auto 0;
border-radius: 50%;
background-color: #cecece;
}
&.address {
font-size: 24rpx;
color: #333;
}
}
.text {
padding: 0 0 44rpx 32rpx;
}
}
.time-new{
padding-left: 30rpx;
padding-bottom: 10rpx;
}
}
</style>

63
config/baseConfig.js Normal file
View File

@ -0,0 +1,63 @@
// 环境判断:开发、生产
const envType = process.env.NODE_ENV === 'development' ? 'develop' : 'produce';
// console.log(envType);
// 获取当前app的版本
const systemInfo = uni.getSystemInfoSync();
// 应用程序版本号
// 条件编译只在H5渲染
const version_number = systemInfo.appVersion ? systemInfo.appVersion : '0.0.1';
console.log(version_number,'版本号');
const apiUrl = {
develop: {
// apiBaseUrl:'http://192.168.2.17:8080', // cky
// apiBaseUrl:'http://192.168.2.130:8905', // 130
// apiBaseUrl: 'http://192.168.2.51:9321', // 正式
// apiBaseUrl:'http://192.168.2.130:8905', // 130
// apiBaseUrl: 'http://192.168.2.51:9321', // 正式
// apiBaseUrl: 'http://192.168.2.130:8905',
apiBaseUrl: 'https://api.app.blindbox.06zk.com', // 正式
// apiBaseUrl:'https://test.blindbox.06zk.com/api', // 101
// apiBaseUrl:'http://192.168.2.71:8080', // 101
wsUrl: 'ws://xxx', // 正式WebSocket连接地址
weChatCode: 'https://auth.wechat.06zk.com',
apiVersion:'0.0.1',
appVersion:version_number
},
produce: {
// apiBaseUrl:'http://192.168.2.130:8905', // 130
// apiBaseUrl: 'https://api.app.blindbox.06zk.com', // 正式
// apiBaseUrl:'https://test.blindbox.06zk.com/api', // 101
// apiBaseUrl: 'http://192.168.2.51:9321', // 正式
apiBaseUrl: 'https://api.app.blindbox.06zk.com', // 正式
// apiBaseUrl:'https://test.blindbox.06zk.com/api', // 101
// apiBaseUrl: 'http://192.168.2.51:9321', // 正式
wsUrl: 'ws://xxxx', // 正式WebSocket连接地址
weChatCode: 'https://auth.wechat.06zk.com',
apiVersion:'0.0.1',
appVersion:version_number
// weChatCode: 'https://auth.wechat.miyamama.cn' //正式
}
};
const ApiVersion = function() {
return apiUrl[envType]['apiVersion']
};
const AppVersion = function() {
return apiUrl[envType]['appVersion']
};
const baseUrl = function(nameSpace) {
return apiUrl[envType][nameSpace]
};
// 其他服务地址配置
const otherServerUrl = {
// gameUrl: 'http://web.wgmwdt.com/games'
}
export {
baseUrl,
otherServerUrl,
ApiVersion,
AppVersion
};

95
get-weixin-code.html Executable file
View File

@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>微信登录</title>
</head>
<body>
<script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/vConsole/3.9.0/vconsole.min.js"></script>
<script>
var GWC = {
version: '1.2.0',
urlParams: {},
appendParams: function (url, params) {
if (params) {
var baseWithSearch = url.split('#')[0];
var hash = url.split('#')[1];
for (var key in params) {
var attrValue = params[key];
if (attrValue !== undefined) {
var newParam = key + "=" + attrValue;
if (baseWithSearch.indexOf('?') > 0) {
var oldParamReg = new RegExp('^' + key + '=[-%.!~*\'\(\)\\w]*', 'g');
if (oldParamReg.test(baseWithSearch)) {
baseWithSearch = baseWithSearch.replace(oldParamReg, newParam);
} else {
baseWithSearch += "&" + newParam;
}
} else {
baseWithSearch += "?" + newParam;
}
}
}
if (hash) {
url = baseWithSearch + '#' + hash;
} else {
url = baseWithSearch;
}
}
return url;
},
getUrlParams: function () {
var pairs = location.search.substring(1).split('&');
for (var i = 0; i < pairs.length; i++) {
var pos = pairs[i].indexOf('=');
if (pos === -1) {
continue;
}
GWC.urlParams[pairs[i].substring(0, pos)] = decodeURIComponent(pairs[i].substring(pos + 1));
}
},
doRedirect: function () {
var code = GWC.urlParams['code'];
var redirect_uri = GWC.urlParams['redirect_uri'];
// var appId = 'wx47db6cca5930ce74';
var appId = GWC.urlParams['appid'];
var scope = GWC.urlParams['scope'] || 'snsapi_base';
var state = GWC.urlParams['state'];
var isMp = GWC.urlParams['isMp']; //isMp为true时使用开放平台作授权登录false为网页扫码登录
var baseUrl;
var redirectUri;
console.log('appid',appId)
if (!code) {
baseUrl = "https://open.weixin.qq.com/connect/oauth2/authorize#wechat_redirect";
if (scope == 'snsapi_login' && !isMp) {
baseUrl = "https://open.weixin.qq.com/connect/qrconnect";
}
//第一步没有拿到code跳转至微信授权页面获取code
redirectUri = GWC.appendParams(baseUrl, {
'appid': appId,
'redirect_uri': encodeURIComponent(location.href),
'response_type': 'code',
'scope': scope,
'state': encodeURIComponent(state),
});
} else {
//第二步从微信授权页面跳转回来已经获取到了code再次跳转到实际所需页面
redirectUri = GWC.appendParams(GWC.urlParams['redirect_uri'], {
'code': code,
'state': encodeURIComponent(state)
});
}
location.href = redirectUri;
}
};
GWC.getUrlParams();
GWC.doRedirect();
</script>
</body>
</html>

View File

@ -0,0 +1,19 @@
{
"id": "nl-precisionNum",
"name": "防止加减乘除精度丢失",
"version": "1.0.2",
"description": "运算时防止数据精度丢失",
"keywords": [
"numAdd",
"numSub",
"numMulti",
"numDiv"
],
"dcloudext": {
"category": [
"JS SDK",
"通用 SDK"
]
},
"displayName": "防止加减乘除精度丢失"
}

View File

@ -0,0 +1,104 @@
/*
* 判断obj是否为一个整数 整数取整后还是等于自己利用这个特性来判断是否是整数
*/
function isInteger(obj) {
// 或者使用 Number.isInteger()
return Math.floor(obj) === obj
}
/*
* 将一个浮点数转成整数返回整数和倍数 3.14 >> 314倍数是 100
* @param floatNum {number} 小数
* @return {object}
* {times:100, num: 314}
*/
function toInteger(floatNum) {
// 初始化数字与精度 times精度倍数 num转化后的整数
var ret = {
times: 1,
num: 0
}
var isNegative = floatNum < 0 //是否是小数
if (isInteger(floatNum)) { // 是否是整数
ret.num = floatNum
return ret //是整数直接返回
}
var strfi = floatNum + '' // 转换为字符串
var dotPos = strfi.indexOf('.')
var len = strfi.substr(dotPos + 1).length // 拿到小数点之后的位数
var times = Math.pow(10, len) // 精度倍数
/* 0.5?
前面讲过乘法也会出现精度问题
假设传入0.16344556此时倍数为100000000
Math.abs(0.16344556) * 100000000=0.16344556*10000000=1634455.5999999999
少了0.0000000001
加上0.5 0.16344556*10000000+0.5=1634456.0999999999 parseInt之后乘法的精度问题得以矫正
*/
var intNum = parseInt(Math.abs(floatNum) * times + 0.5, 10)
ret.times = times
if (isNegative) {
intNum = -intNum
}
ret.num = intNum
return ret
}
/*
* 核心方法实现加减乘除运算确保不丢失精度
* 思路把小数放大为整数进行算术运算再缩小为小数
* @param a {number} 运算数1
* @param b {number} 运算数2
*/
function operation(a, b, op) {
var o1 = toInteger(a)
var o2 = toInteger(b)
var n1 = o1.num // 3.25+3.153
var n2 = o2.num
var t1 = o1.times
var t2 = o2.times
var max = t1 > t2 ? t1 : t2
var result = null
switch (op) {
// 加减需要根据倍数关系来处理
case 'add':
if (t1 === t2) { // 两个小数倍数相同
result = n1 + n2
} else if (t1 > t2) {
// o1 小数位 大于 o2
result = n1 + n2 * (t1 / t2)
} else { // o1小数位小于 o2
result = n1 * (t2 / t1) + n2
}
return result / max
case 'subtract':
if (t1 === t2) {
result = n1 - n2
} else if (t1 > t2) {
result = n1 - n2 * (t1 / t2)
} else {
result = n1 * (t2 / t1) - n2
}
return result / max
case 'multiply':
// 325*3153/(100*1000) 扩大100倍 ==>缩小100倍
result = (n1 * n2) / (t1 * t2)
return result
case 'divide':
// (325/3153)*(1000/100) 缩小100倍 ==>扩大100倍
result = (n1 / n2) * (t2 / t1)
return result
}
}
// 加减乘除的四个接口
export function numAdd(a, b) {
return operation(a, b, 'add')
}
export function numSub(a, b) {
return operation(a, b, 'subtract')
}
export function numMulti(a, b) {
return operation(a, b, 'multiply')
}
export function numDiv(a, b) {
return operation(a*100, b*100, 'divide')
}

53
login.html Executable file
View File

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Cache-Control" content="no-cache" />
<meta http-equiv="Cache-Control" content="no-cache" />
<META HTTP-EQUIV="pragma" CONTENT="no-cache">
<META HTTP-EQUIV="Cache-Control" CONTENT="no-store, must-revalidate">
<META HTTP-EQUIV="expires" CONTENT="Wed, 26 Feb 1997 08:21:57 GMT">
<META HTTP-EQUIV="expires" CONTENT="0">
</head>
<body>
<script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/vConsole/3.9.0/vconsole.min.js"></script>
<script>
// init vConsole
// var vConsole = new VConsole();
// console.log('Hello VConsole');
</script>
<script type="text/javascript">
function getUrlParam(name) {
return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.href) || [,
""
])[1].replace(/\+/g, '%20')) || null
}
window.onload = function() {
var code = getUrlParam('code');
// debugger;
console.log("-------code:",code)
location.href = `./index.html#/pages/login/login?code=${code}`;
// if (!code) { // 没有登入之前
// var redirect_uri = encodeURI(window.location.origin + window.location.pathname);
// var data = {
// appid: 'wx47db6cca5930ce74',
// redirect_uri: redirect_uri,
// response_type: 'code',
// scope: 'snsapi_userinfo',
// state: ''
// };
// location.href =
// `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${data.appid}&redirect_uri=${data.redirect_uri}&response_type=${data.response_type}&scope=${data.scope}&state=${data.state}#wechat_redirect`;
// } else { // 登入后
// localStorage.setItem('code', code);
// setTimeout(() => {
// location.href = './index.html#/pages/login/login';
// }, 500);
// }
};
</script>
</body>
</html>

38
main.js Normal file
View File

@ -0,0 +1,38 @@
import Vue from 'vue'
import App from './App'
import msgUtils from './utils/msgUtils.js'
import path from './utils/skip'
import '@/utils/filter/filterUtils.js'
// 注册全局组件
import MescrollBody from "@/components/mescroll-uni/mescroll-body.vue"
import MescrollUni from "@/components/mescroll-uni/mescroll-uni.vue"
Vue.component('mescroll-body', MescrollBody)
Vue.component('mescroll-uni', MescrollUni)
// #ifdef H5
// 提交前需要注释 本地调试使用
// const vconsole = require('vconsole')
// Vue.prototype.$vconsole = new vconsole() // 使用vconsole
// #endif
import CustomNavbarPage from '@/pages/components/customNavbarPage.vue'
import CropStatusbarHeight from '@/pages/components/crop-statusbar-height/crop-statusbar-height.vue'
Vue.component('custom-navbar-page', CustomNavbarPage)
Vue.component('crop-statusbar-height', CropStatusbarHeight)
Vue.config.productionTip = false
Vue.prototype.$api = msgUtils
Vue.prototype.$path = path
App.mpType = 'app'
const app = new Vue({
...App,
path
})
app.$mount()

3
make-dev.sh Normal file
View File

@ -0,0 +1,3 @@
docker build -t registry.cn-shanghai.aliyuncs.com/box-mall/box-mall-app-web:2.0 --platform=linux/amd64 .
docker push registry.cn-shanghai.aliyuncs.com/box-mall/box-mall-app-web:2.0

3
make-test.sh Normal file
View File

@ -0,0 +1,3 @@
docker build -t registry.cn-shanghai.aliyuncs.com/box-mall/box-mall-app-web:1.0 --platform=linux/amd64 .
docker push registry.cn-shanghai.aliyuncs.com/box-mall/box-mall-app-web:1.0

3
make.sh Normal file
View File

@ -0,0 +1,3 @@
docker build -t registry.cn-shanghai.aliyuncs.com/miyamama_box/box-mall-app-web:1.0 --platform=linux/amd64 .
docker push registry.cn-shanghai.aliyuncs.com/miyamama_box/box-mall-app-web:1.0

181
manifest.json Normal file
View File

@ -0,0 +1,181 @@
{
"name" : "妙趣盲盒",
"appid" : "__UNI__8AC2CA7",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : 100,
"transformPx" : false,
/* 5+App */
"app-plus" : {
"safearea" : {
"bottom" : {
"offset" : "auto"
}
},
"usingComponents" : true,
"nvueCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : false,
"waiting" : false,
"autoclose" : true,
"delay" : 0
},
/* */
"modules" : {
"Maps" : {},
"OAuth" : {},
"Payment" : {},
"Share" : {}
},
/* */
"distribute" : {
/* android */
"android" : {
"permissions" : [
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
],
"autoSdkPermissions" : false
},
/* ios */
"ios" : {},
/* SDK */
"sdkConfigs" : {
"ad" : {},
"maps" : {
"amap" : {
"appkey_ios" : "40a745f83e5c11c12aa6c7dea464dbb9",
"appkey_android" : "40a745f83e5c11c12aa6c7dea464dbb9"
}
},
"oauth" : {
"weixin" : {
"appid" : "wx836b447885f16f22",
"UniversalLinks" : ""
}
},
"payment" : {
"weixin" : {
"__platform__" : [ "ios", "android" ],
"appid" : "wx836b447885f16f22",
"UniversalLinks" : ""
},
"alipay" : {
"__platform__" : [ "ios", "android" ]
}
},
"share" : {
"weixin" : {
"appid" : "wx836b447885f16f22",
"UniversalLinks" : ""
}
}
},
"splashscreen" : {
"androidStyle" : "common",
"android" : {
"hdpi" : "static/common/splash.9.png",
"xhdpi" : "static/common/splash.9.png",
"xxhdpi" : "static/common/splash.9.png"
}
},
"icons" : {
"android" : {
"hdpi" : "unpackage/res/icons/72x72.png",
"xhdpi" : "unpackage/res/icons/96x96.png",
"xxhdpi" : "unpackage/res/icons/144x144.png",
"xxxhdpi" : "unpackage/res/icons/192x192.png"
},
"ios" : {
"appstore" : "unpackage/res/icons/1024x1024.png",
"ipad" : {
"app" : "unpackage/res/icons/76x76.png",
"app@2x" : "unpackage/res/icons/152x152.png",
"notification" : "unpackage/res/icons/20x20.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"proapp@2x" : "unpackage/res/icons/167x167.png",
"settings" : "unpackage/res/icons/29x29.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"spotlight" : "unpackage/res/icons/40x40.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png"
},
"iphone" : {
"app@2x" : "unpackage/res/icons/120x120.png",
"app@3x" : "unpackage/res/icons/180x180.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"notification@3x" : "unpackage/res/icons/60x60.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"settings@3x" : "unpackage/res/icons/87x87.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png",
"spotlight@3x" : "unpackage/res/icons/120x120.png"
}
}
}
},
"uniStatistics" : {
"enable" : false
}
},
/* */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "wx836b447885f16f22",
"setting" : {
"urlCheck" : false,
"es6" : true
},
"usingComponents" : true,
"permission" : {
"scope.userLocation" : {
"desc" : "获取与门店距离"
}
},
"requiredPrivateInfos" : [ "getLocation" ]
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"h5" : {
"router" : {
"mode" : "hash",
"base" : "./"
}
},
"uniCloud" : {
"provider" : "aliyun",
"spaceId" : "mp-ee8153d2-d55b-4e47-82d3-64303d9910ea",
"clientSecret" : "c8SiFkxUtsnS/CBjDJBgbg=="
}
}

76
mixins/mixins.js Executable file
View File

@ -0,0 +1,76 @@
export const myMixins = {
components: {},
data() {
return {
mixinsTimer:null,
mixinsTime:0,
btnTimer:null
};
},
onShow(){
// pages/index/index //首页
// pages/index/detail //盲盒详情
// pages/index/card //使用提示卡
// pages/index/confirmOrder //盲盒确认订单
// pages/index/confirmOrder //盲盒支付成功页面
// pages/index/prize //开奖页面
// pages/product/product //商品列表
// pages/search/index//商品搜索
// pages/search/search-list//商品搜索列表
// pages/product/detail //商品详情页
// pages/order/confirm //商品确认订单
// pages/shop/paySuccess //商品支付成功页面
// pages/mine/sign//签到
// pages/mine/index //个人中心
// pages/mine/setting //设置
// pages/mine/editName //修改昵称
// pages/agreement/index //用户协议
// pages/agreement/privacy //隐私政策
// pages/mine/about //关于我们
// pages/mine/point //积分使用记录
// pages/mine/coupon //优惠券
// pages/mine/moreCoupon //更多优惠券
// pages/mine/boxRecrd //物品
// pages/mine/boxConfirm//物品发货提交订单
// pages/order/index//我的订单
// pages/mine/orderStatusDetail//订单详情
// pages/mine/boxOrder//盲盒订单
// pages/mine/address//地址管理
// pages/mine/addAddress //编辑新增地址
// pages/mine/collect//收藏列表
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const url = currentPage.route; // 当前页面路径
const options = currentPage.options; // 当前页面参数
console.log(url);
this.mixinsTimer = setInterval(() => {
this.mixinsTime++
},1000)
},
onHide(){
clearInterval(this.mixinsTimer)
console.log(this.mixinsTime)
this.mixinsTimer = null
this.mixinsTime = 0
console.log('页面隐藏,即将返回上一页,');
this.btnTimer = Math.floor(Math.random() * (1 - 100000 + 1)) + 100000
clearInterval(this.btnTimer)
},
onUnload(){
console.log('页面卸载,即将返回上一页');
},
mounted() {
},
methods: {
},
};

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