Commit 6a8bdd26 by Jason Zhou

less to wxss

parent f9bc81d9
Page({
onLoad() {
wx.switchTab({
url: '/pages/blog/postIndex/postIndex',
})
},
})
<template name="img-content">
<view class="comment-img" style="border:1px solid #f5f5f5;" bindtap="previewCommentImage"
data-event-src="{{pictures}}" data-event-image="{{item.pictures}}">
<image style="width: 180rpx; height:180rpx;" mode="aspectFill" src="{{pictures}}"></image>
</view>
</template>
<template name="comment-list">
<view class="comment-list">
<view class="logo-icon">
<image src="{{item.avatarPhoto[0]}}" mode="aspectFill"></image>
</view>
<view class="comment-text">
<view class="t-header">
<text class="name">{{item.nickname}}</text>
<text class="time">{{item.createdAt}}</text>
</view>
<view class="t-body">
<view class="seller-payment">
<block wx:if="{{item.content.length > 82}}">
<block wx:if="{{item.isExtend}}">
{{item.shortContent}}
<text bindtap="onIsExtend" data-event-id="{{item.id}}">更多</text>
</block>
<block wx:else>
{{item.content}}
<text bindtap="onIsExtend" data-event-id="{{item.id}}">收起</text>
</block>
</block>
<block wx:else>
{{item.content}}
</block>
</view>
<view class="comment-img-box">
<view class="comment-picture">
<view wx:for="{{item.lessCommentImg || []}}" wx:key="{{item}}" wx:for-item="pictures">
<template is="img-content" data="{{pictures, item}}"/>
</view>
<view wx:if="{{item && item.pictures.length > 3}}" class="more-image"
data-event-src="{{item.moreCommentImg[0]}}" data-event-image="{{item.pictures}}"
bindtap="previewCommentImage">
<image style="width: 180rpx; height: 180rpx;" mode="aspectFill"
src="{{item.moreCommentImg}}"></image>
<view style="width: 45rpx; height: 33rpx;" class="overlay">
{{item.pictures.length || 0}}
</view>
</view>
<view wx:if="{{item && item.pictures.length === 2 }}">
<view class="store-img">
<image style="width: 180rpx; height: 180rpx;"></image>
</view>
</view>
</view>
<view class="replay-panel" wx:if="{{item.replyStatus === 'visible'}}">
<view class="store-boss">
<text>作者回复:</text>
<block wx:if="{{item.reply && item.reply.length > 82}}">
<block wx:if="{{item.isReplyExtend}}">
{{item.shortReply}}
<text bindtap="onIsReplyExtend" data-event-id="{{item.id}}">更多</text>
</block>
<block wx:else>
{{item.reply}}
<text bindtap="onIsReplyExtend" data-event-id="{{item.id}}">收起</text>
</block>
</block>
<block wx:else>
{{item.reply}}
</block>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
.comment-list {
margin-bottom: 25px;
display: flex;
}
.comment-list .logo-icon {
display: flex;
align-items: flex-start;
justify-content: flex-start;
margin-right: 20rpx;
flex: 0 0 auto;
}
.comment-list .logo-icon image {
width: 90rpx;
height: 90rpx;
border-radius: 50%;
}
.comment-list .comment-text .t-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 3px;
width: 560rpx;
}
.comment-list .comment-text .t-header .name {
font-size: 15px;
color: #292929;
font-weight: 500;
}
.comment-list .comment-text .t-header .time {
font-size: 12px;
color: #b1b1b1;
}
.comment-list .comment-text .t-body .replay-panel {
border-top: 1px solid #f3f3f3;
margin-top: 3px;
padding-top: 6px;
}
.comment-list .comment-text .t-body .replay-panel .store-boss {
font-size: 15px !important;
line-height: 1.93;
color: #757575 !important;
}
.comment-list .comment-text .t-body .replay-panel .store-boss text {
font-size: 15px !important;
color: #6D8697 !important;
}
.comment-list .comment-text .t-body .seller-payment {
font-size: 15px;
line-height: 1.79;
color: #757575;
display: block;
}
.comment-list .comment-text .t-body .seller-payment text {
font-size: 15px;
color: #6D8697;
}
.comment-list .comment-text .t-body text {
font-size: 13px;
color: #a9aeb2;
display: inline-flex;
justify-content: flex-end;
}
\ No newline at end of file
import { connect } from 'root/wmp-redux'
import compose from 'ramda/src/compose'
import urls from 'root/constants/blog/urls'
import {
fetchComments,
setComments,
onExtend,
onReplyExtend,
clearComments,
} from 'root/actions/blog/comment'
import { fetchPosts, likePost } from 'root/actions/blog/post'
import { fetchPostDetail, clearPostDetail } from 'root/actions/blog/postDetail'
import { setError } from 'root/actions/common/error'
import { fetchPostSharing } from 'root/actions/blog/sharing'
import { setGlobalData } from 'root/actions/common/global'
import Feature from 'root/constants/common/feature'
import iconPaths from 'root/constants/blog/iconPaths'
import { getComments } from 'root/selectors/blog/comment'
import { getPostDetail } from 'root/selectors/blog/postDetail'
import { getUserInfo, getAttr } from 'root/selectors/common/global'
import { getError } from 'root/selectors/common/error'
import { trackUI } from 'wechat_common/tracker/index.bs'
import { getTeamMemberId } from 'root/selectors/presentation/teamMemberSelector'
import wrappedWXTool, {
bindGetUserInfoHandler,
} from 'wechat_common/utils/wrappedWXTool'
import shareView from 'root/templates/shareView/shareView'
const {
iconWechat,
iconBookmark,
iconBookmarked,
iconLock,
iconComment,
iconLike,
imgAvatar,
iconShare,
iconShareMessage,
iconShareImage,
homeBtn,
iconMomentLike,
iconMomentLikeGrey,
iconMomentLikeTotal,
} = iconPaths
const page = {
data: {
currentPost: {},
iconWechat,
iconBookmark,
iconBookmarked,
iconLock,
iconComment,
iconLike,
imgAvatar,
submitting: false,
errorMessage: '',
iconShare,
iconShareMessage,
iconShareImage,
homeBtn,
iconMomentLike,
iconMomentLikeGrey,
iconMomentLikeTotal,
shareLoading: false,
cachedSections: null,
},
onLoad(options) {
const { postId, scene } = options
const postIdFromScene = decodeURIComponent(scene)
wx.getExtConfig({
success: res => {
this.fetchPosts(res.extConfig.attr.siteId)
},
})
this.setData({ postId: postId || postIdFromScene })
this.fetchPostDetail(postId || postIdFromScene)
this.fetchComments(postId || postIdFromScene)
},
onUnload() {
this.clearPostDetail()
this.clearComments()
},
onHide() {
this.closeShareView()
},
afterStateChange() {
/**
* bug fix for RDT-777
*
* root cause: wx.previewImage will trigger onHide and onShow,
* which leads rerender due to our injected newOnShow method
*
* solution: Use a varible cachedSections to cache sections, and use the cached
* value until page is destroyed.
*/
const { sections, cachedSections } = this.data
if (!cachedSections && sections) {
this.setData({
cachedSections: sections,
})
}
},
bindShare() {
wx.showShareMenu({
withShareTicket: true,
})
},
bindMark() {
this.setData({
'currentPost.marked': true,
})
wx.showToast({
title: '已收藏',
icon: 'success',
})
},
bindUnmark() {
this.setData({
'currentPost.marked': false,
})
wx.showToast({
title: '已取消收藏',
icon: 'success',
})
},
bindTextAreaChange(e) {
this.setData({
currentComment: e.detail.value,
})
},
bindGetUserInfoHandler,
onClickSubmit(e) {
const { currentComment } = this.data
if (!currentComment || !currentComment.trim()) {
this.setError('评论内容不能为空')
} else if (wx.isWept) {
this.setError('预览模式下暂不支持,请手机预览')
} else {
this.createComment(e.detail.formId)
}
},
createComment(formId) {
const { currentPost, currentComment, nickName, avatarUrl } = this.data
if (!nickName && !avatarUrl) {
return
}
const that = this
this.setData({
submitting: true,
})
const sendData = {
content: currentComment,
nickname: nickName,
wechat_photo: [avatarUrl],
settings: {
form_id: formId,
},
}
wrappedWXTool.login({
success: loginRes => {
sendData.code = loginRes.code
that.setComments(
currentPost.id,
sendData,
() => {
that.setData({
submitting: false,
currentComment: '',
})
trackUI(
'commentBlogPost',
currentPost.id,
JSON.stringify({
team_member_id: this.data.teamMemberId || -1,
key: `commentBlogPost${currentPost.id}`,
}),
)
wx.showToast({
title: '评论成功,审核通过后显示在留言列表',
icon: 'none',
duration: 2000,
})
},
() => {
that.setData({ submitting: false })
},
)
},
fail() {
wx.showModal({
content: '请求失败,请重试',
})
that.setData({
submitting: false,
})
},
})
},
showImage(e) {
wx.previewImage({
urls: [e.target.dataset.imageUrl],
})
},
loadMoreComment() {
const { paginationComments, postIdString } = this.data
if (paginationComments.nextPage) {
this.fetchComments(postIdString, paginationComments.nextPage)
}
},
onIsExtend(e) {
const currentId = e.currentTarget.dataset.eventId
this.onExtend(currentId)
},
onIsReplyExtend(e) {
const currentId = e.currentTarget.dataset.eventId
this.onReplyExtend(currentId)
},
gotoHome() {
wx.switchTab({
url: urls.PAGES.POST_INDEX,
})
this.setGlobalData({
isFromSharedProductCard: false,
isFromSharedProductQRCode: false,
})
},
onShareAppMessage() {
const { currentPost, headerTitleString } = this.data
return {
title: headerTitleString,
path: `/pages/blog/postDetail/postDetail?postId=${currentPost.id}`,
}
},
sharePicture() {
const { siteId, currentPost: { id: postId } } = this.data
if (wx.isWept) {
this.setError('预览模式下暂不支持,请手机预览')
} else {
this.setData({
shareLoading: true,
})
this.fetchPostSharing(siteId, {
scene: postId.toString(),
page: 'pages/blog/postDetail/postDetail',
}).then(res => {
trackUI(
'shareImageBlogPost',
postId,
JSON.stringify({
team_member_id: this.data.teamMemberId || -1,
key: `shareImageBlogPost${postId}`,
}),
)
this.setData({
shareLoading: false,
})
if (res.success) {
wx.navigateTo({
url: `${urls.PAGES.POST_SHARE}?postId=${postId}`,
})
} else {
wx.showModal({
title: '获取分享信息错误',
content: '请稍后重试',
showCancel: false,
success: res => {
if (res.confirm) {
this.closeShareView()
}
},
})
}
})
}
},
handleLike(e) {
const { currentPost, isLikingPost } = this.data
const { isLiked } = e.currentTarget.dataset
const cb = userInfo => {
const { nickName, avatarUrl } = userInfo
if (!isLikingPost) {
this.likePost(!isLiked, currentPost.id, nickName, avatarUrl)
if (!isLiked) {
trackUI(
'likeBlogPost',
currentPost.id,
JSON.stringify({
team_member_id: this.data.teamMemberId || -1,
key: `likeBlogPost${currentPost.id}`,
}),
)
}
}
}
bindGetUserInfoHandler(e, cb)
},
}
function mapStateToProps(state) {
const {
list: comments,
isFetching: isFetchingComments,
pagination: paginationComments,
totalCount: totalCountComments,
} = getComments(state)
const {
currentPost,
isLikingPost,
sections,
headerTitle,
headerTitleString,
headerSubTitle,
publishedAt,
textColor,
backgroundUrl,
} = getPostDetail(state)
const { nickName, avatarUrl } = getUserInfo(state)
const { canUseSearch, canUseCollection, canUseShare } = Feature
const { errorMessage } = getError(state)
const { siteId } = getAttr(state)
const enableComments = state.getIn([
'blog',
'setting',
'settings',
'enableComments',
])
const isFromSharedProductCard = state.getIn([
'globalData',
'isFromSharedProductCard',
])
const isFromSharedProductQRCode = state.getIn([
'globalData',
'isFromSharedProductQRCode',
])
const teamMemberId = getTeamMemberId(state)
return {
comments,
isFetchingComments,
totalCountComments,
paginationComments,
isLikingPost,
teamMemberId,
currentPost,
enableComments,
sections,
headerTitle,
headerTitleString,
headerSubTitle,
backgroundUrl,
publishedAt,
textColor,
nickName,
avatarUrl,
canUseSearch,
canUseCollection,
canUseShare,
errorMessage,
siteId,
isFromSharedProductCard,
isFromSharedProductQRCode,
}
}
function mapDispatchToProps(dispatch) {
return {
fetchPosts: siteId => dispatch(fetchPosts(urls.PAGES.POST_INDEX, siteId)),
fetchPostDetail: postId => dispatch(fetchPostDetail(postId)),
clearPostDetail: () => dispatch(clearPostDetail()),
clearComments: () => dispatch(clearComments()),
onExtend: currentId => dispatch(onExtend(currentId)),
onReplyExtend: currentId => dispatch(onReplyExtend(currentId)),
fetchComments: (postId, pageNum) =>
dispatch(fetchComments(postId, pageNum)),
setComments: (postId, setData, successCb, failCb) =>
dispatch(setComments(postId, setData, successCb, failCb)),
setError: errorMessage => dispatch(setError(errorMessage)),
fetchPostSharing: (siteId, data) =>
dispatch(fetchPostSharing(siteId, data)),
setGlobalData: data => dispatch(setGlobalData(data)),
likePost: (status, postId, nickName, avatarUrl) =>
dispatch(likePost(status, postId, nickName, avatarUrl)),
}
}
const enhance = compose(shareView, connect(mapStateToProps, mapDispatchToProps))
Page(enhance(page))
{
"navigationBarTitleText": ""
}
\ No newline at end of file
<import src="root/utils/wxParse/wxParse.wxml" />
<import src="./postItem/postItem.wxml" />
<import src="./commentList/commentList.wxml" />
<import src="root/templates/messageModal/messageModal.wxml" />
<import src="root/templates/shareView/shareView.wxml" />
<import src="root/templates/loaderPage/loaderPage.wxml" />
<view wx:if="{{errorMessage}}">
<template is="message-modal" data="{{message: errorMessage}}" />
</view>
<!-- 使用loading 占位 -->
<block wx:if="{{!errorMessage && !currentPost.id}}">
<template is='loader-page' />
</block>
<block class="post-detail" wx:if="{{currentPost}}">
<image wx:if="{{isFromSharedProductCard || isFromSharedProductQRCode}}" src="{{homeBtn}}" class="home-btn" bindtap="gotoHome" />
<view class="{{'post-header flex-col' + (backgroundUrl ? '' : ' no-background')}}" style="background-image: url('{{backgroundUrl}}');">
<view class="flex-col normal-flex post-header-wrapper bg-{{textColor}}">
<view class="post-title post-item-wrapper">
<view class="title">
<template wx:if="{{headerTitle.nodes}}" is="wxParse" data="{{...headerTitle}}" />
</view>
<view class="subTitle">
<template wx:if="{{headerSubTitle.nodes}}" is="wxParse" data="{{...headerSubTitle}}" />
</view>
<view class="time">{{ publishedAt }}</view>
</view>
</view>
<view wx:if="{{allTagsList[0]}}" class="tags">{{allTagsList[0]}}</view>
</view>
<view class="post-body">
<!-- bug fix for RDT-777, prevent rerender using a cached value. For details see postDetail.js -->
<block wx:for="{{cachedSections}}" wx:for-item="item" wx:key="item.id">
<template wx:if="{{item.component.type == 'Blog.Quote' || item.component.type == 'Quote'}}" is="post-item-quote" data="{{...item.component.value}}" />
<template wx:if="{{item.component.type == 'Blog.Text' || item.component.type == 'RichText'}}" is="post-item-text" data="{{...item.component.value}}" />
<template wx:if="{{item.component.type == 'Blog.Title' || item.component.type == 'Title'}}" is="post-item-title" data="{{...item.component.value}}" />
<template wx:if="{{item.component.type == 'Blog.Image' || item.component.type == 'Image'}}" is="post-item-image" data="{{...item.component}}" />
<template wx:if="{{item.component.type == 'Blog.Video' || item.component.type == 'Video'}}" is="post-item-video" data="{{...item.component}}" />
<template wx:if="{{item.component.type == 'Blog.Separator' || item.component.type == 'Separator'}}" is="post-item-separator" data="{{...item.component}}" />
</block>
<view class="share-container">
<view class="share-btn" bindtap="openShareView">
<image src="{{iconShare}}" />
<text>分享文章</text>
</view>
<button wx:if="{{currentPost.isLiked}}" class="share-btn" data-is-liked="{{currentPost.isLiked}}" open-type="getUserInfo" bindgetuserinfo="handleLike" style="margin-left: 25rpx; background-color: #708694;">
<image src="{{iconMomentLikeTotal}}" />
<text style="color: white">已赞</text>
</button>
<button wx:else class="share-btn" data-is-liked="{{currentPost.isLiked}}" open-type="getUserInfo" bindgetuserinfo="handleLike" style="margin-left: 25rpx;">
<image src="{{iconMomentLike}}" />
<text>赞一下</text>
</button>
</view>
</view>
<view class="post-footer">
<block wx:if="{{canUserShare && canUseCollection}}">
<view class="flex-row footer-buttons">
<button wx:if="{{!marked}}" class="bookmark-button" bindtap="bindMark">
<image src="{{iconBookmark}}" style="width: 20px; height: 20px;" />
添加至收藏
</button>
<button wx:if="{{marked}}" class="bookmark-button marked" bindtap="bindUnmark">
<image src="{{iconBookmarked}}" style="width: 20px; height: 20px;" />
已添加收藏
</button>
<button open-type="share" class="share-button" bindtap="bindShare">
<image src="{{iconWechat}}" style="width: 20px; height: 20px;" />
分享至微信
</button>
</view>
</block>
<view class="blog-comment">
<view class="text-with-icon">
<image src="{{iconMomentLikeGrey}}" style="width: 40rpx; height: 40rpx; margin-right: 8rpx" />
<text>点赞 ({{currentPost.likes && currentPost.likes.length ? currentPost.likes.length : 0}})</text>
</view>
<block wx:if="{{enableComments}}">
<view class="text-with-icon">
<image src="{{iconComment}}" style="width: 32rpx; height: 32rpx; margin-right: 16rpx" />
<text>评论 ({{comments.length}})</text>
</view>
<form bindsubmit="onClickSubmit" report-submit="true">
<textarea wx:if="{{!showShareVariation}}" placeholder="写点什么吧…" value="{{currentComment}}" bindinput="bindTextAreaChange" class="textarea" />
<input wx:else type="text" placeholder="写点什么吧…" class="input" placeholder-style="position: absolute; top: -90rpx; left: 22rpx; background: transparent" />
<view class="flex-row submit-buttons">
<view class="text-with-icon">
<image src="{{iconLock}}" style="width: 16px; height: 16px;" />
<text class="light-text" style="font-size:14px;">评论需审核后即可显示。</text>
</view>
<button formType="submit" class="submit-button" loading="{{submitting}}" open-type="getUserInfo" bindgetuserinfo="bindGetUserInfoHandler">
提交
</button>
</view>
</form>
</block>
</view>
<block wx:if="{{enableComments}}">
<view class="comment">
<view class="c-header"></view>
<view class="c-body" wx:if="{{comments && comments.length > 0}}">
<block wx:for="{{comments}}" wx:key="item" wx:for-item="item">
<template is="comment-list" data="{{item}}" />
</block>
</view>
<block wx:if="{{isFetchingComments}}">
<view style="text-align: center; height: 20px; margin: 10px 0 50px; position: relative;">
<view class="loader" />
</view>
</block>
<block wx:else>
<view wx:if="{{comments && comments.length === 0}}" class="empty-comment-wrapper" mode="aspectFill">
<!-- <image style="width: 100vw; height: 220px;" src="/assets/blog/comment-empty-bg.png"></image> -->
<view class="add-comment-enter">
<text>暂无评论</text>
</view>
</view>
</block>
</view>
<view style="text-align: center; height: 20px; margin: 10px 0 50px; position: relative;">
<view wx:if="{{comments.length > 0 && paginationMeta.nextPage}}" class="loader" />
<text wx:if="{{comments.length > 6 && !isFetchingComments && paginationMeta.nextPage === null}}" style="line-height: 20px; font-size: 12px; color: #999; padding-bottom: 20px;">
- 无更多评论 -
</text>
</view>
</block>
</view>
</block>
<template is="share-view" data="{{shareLoading, sharePicture, shareAnimation, showShareVariation, closeShareView, openShareView}}" />
<view class="flex-row flex-col center-align-item" style="height: 100vh;" wx:if="{{!currentPost}}">
<view class="loader" />
</view>
@import "/styles/main.wxss";
@import "/utils/wxParse/wxParse.wxss";
@import '/pages/blog/postDetail/commentList/commentList.wxss';
@import '/pages/blog/postDetail/postItem/postItem.wxss';
@import "/templates/messageModal/messageModal.wxss";
@import "/templates/shareView/shareView.wxss";
.wx-view {
letter-spacing: 0.10rpx;
font-size: inherit;
color: inherit;
}
.wxParse-li-text {
overflow: visible;
}
.wxParse-li-circle {
position: relative;
top: -4rpx;
margin-right: 20rpx;
}
.home-btn {
position: fixed;
top: 30rpx;
left: 30rpx;
width: 80rpx;
height: 80rpx;
z-index: 100;
}
.post-detail {
background: #fff;
min-height: 100vh;
}
.post-header {
background-size: cover;
background-position: center;
min-height: 40vh;
}
.post-header .post-header-wrapper {
justify-content: flex-end;
}
.post-header .bg-overlay {
background: rgba(0, 0, 0, 0.4);
}
.post-header .bg-light .post-title .title {
color: #fff;
}
.post-header .bg-light .post-title .title .wxParse-p {
color: #ffffff;
}
.post-header .bg-light .post-title .subTitle {
color: #fff;
}
.post-header .bg-light .post-title .time {
color: #fff;
}
.post-header .bg-dark {
text-shadow: 0 2rpx 24rpx rgba(255, 255, 255, 0.5);
}
.post-header .bg-dark .post-title .title {
color: #161616;
}
.post-header .bg-dark .post-title .title .wxParse-p {
color: #ffffff;
}
.post-header .bg-dark .post-title .subTitle {
color: #161616;
}
.post-header .bg-dark .post-title .time {
color: #161616;
}
.post-header .tags {
z-index: 100;
color: #fff;
font-size: 24rpx;
position: absolute;
top: 40rpx;
left: 40rpx;
line-height: 46rpx;
padding: 0 16rpx;
border-radius: 4rpx;
text-align: center;
background-color: rgba(0, 0, 0, 0.4);
}
.post-header.no-background {
min-height: 20vh;
}
.post-header.no-background .bg-overlay {
background: none;
}
.post-header.no-background .post-title view {
color: #000;
}
.post-header.no-background .post-title .time {
color: #999;
}
.post-title {
font-size: 42rpx;
padding-bottom: 40rpx;
}
.post-title .title {
font-size: 42rpx;
color: #fff;
}
.post-title .subTitle {
font-size: 32rpx;
color: #fff;
margin-top: 10rpx;
}
.post-title .time {
color: #ccc;
font-size: 24rpx;
margin-top: 10rpx;
}
.post-body {
font-size: 32rpx;
line-height: 1.75;
padding-top: 50rpx;
}
.post-item-title {
font-size: 38rpx;
}
.post-footer {
background: #f7f7f7;
}
.post-footer image {
vertical-align: middle;
}
.post-footer .text-with-icon {
margin-bottom: 32rpx;
}
.post-footer .text-with-icon text,
.post-footer .text-with-icon image {
vertical-align: middle;
}
.post-footer .text-with-icon text {
margin-right: 8rpx;
}
.post-footer .footer-buttons,
.post-footer .submit-buttons {
justify-content: space-between;
}
.post-footer .footer-buttons {
background: #fff;
padding: 30rpx 40rpx 60rpx;
}
.post-footer .bookmark-button,
.post-footer .share-button {
display: inline-block;
padding: 0 54rpx;
margin: 0;
border-radius: 200rpx;
flex: 0 0 auto;
}
.post-footer .bookmark-button image,
.post-footer .share-button image {
width: 40rpx;
height: 40rpx;
}
.post-footer .bookmark-button {
background: #fff;
border: 2rpx solid #52616a;
color: #52616a;
}
.post-footer .bookmark-button.marked {
background: #52616a;
color: #fff;
}
.post-footer .share-button {
background: #fff;
color: #3aae3a;
border: 2rpx solid #3aae3a;
}
.post-footer .submit-button {
display: inline-block;
padding: 0 40rpx;
margin: 0;
background: #40404c;
color: #fff;
border-color: #40404c;
}
.post-footer .blog-comment {
padding: 40rpx;
}
.post-footer .blog-comment .textarea {
font-size: 32rpx;
width: 100%;
padding: 30rpx;
background: #fff;
box-sizing: border-box;
box-shadow: inset 0 0 8rpx 0 rgba(0, 0, 0, 0.2);
border-radius: 8rpx;
margin: 30rpx 0;
}
.post-footer .blog-comment .input {
height: 259rpx;
font-size: 32rpx;
padding: 8rpx;
background: #fff;
box-shadow: inset 0 0 8rpx 0 rgba(0, 0, 0, 0.2);
border-radius: 8rpx;
margin: 30rpx 0;
}
.post-footer .comments-list {
margin-top: 60rpx;
}
.post-footer .comments-list .comment-title {
padding: 20rpx 0;
}
.post-footer .comments-list .comment-title .avatar-image {
margin-right: 20rpx;
border-radius: 50rpx;
}
.post-footer .comments-list .comment-title .comment-name {
vertical-align: middle;
display: inline-block;
}
.post-footer .comments-list .comment-text {
line-height: 1.93;
}
.share-container {
display: flex;
justify-content: center;
margin-bottom: 60rpx;
}
.share-container .share-btn {
display: flex;
justify-content: center;
align-items: center;
border-radius: 100rpx;
border: solid 2rpx #708694;
width: 320rpx;
height: 100rpx;
margin-right: 0;
margin-left: 0;
}
.share-container .share-btn image {
width: 40rpx;
height: 40rpx;
}
.share-container .share-btn text {
font-size: 28rpx;
color: #708694;
margin-left: 15rpx;
}
.s-text-color-gray {
color: #999;
}
.s-text-color-black {
color: #222;
}
.s-text-color-brown {
color: #816354;
}
.s-text-color-red {
color: #e40613;
}
.s-text-color-orange {
color: #f1a852;
}
.s-text-color-green {
color: #9cce06;
}
.s-text-color-blue {
color: #0dc5b9;
}
.comment {
padding: 40rpx 36rpx 0;
background: #f7f7f7;
}
.comment .c-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.comment .c-header .count {
height: 80rpx;
color: #636972;
font-size: 28rpx;
display: flex;
align-items: center;
}
.comment .c-header .write-btn {
padding: 16rpx;
border: 2rpx solid #6D8697;
border-radius: 6rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
}
.comment .c-header .write-btn text {
color: #6D8697;
}
.comment .c-body {
margin-top: 18.10rpx;
}
.comment .c-footer {
margin-bottom: 74rpx;
}
.comment .c-footer .check-more-comment {
height: 86rpx;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid #6D8697;
border-radius: 12rpx;
color: #6D8697;
font-size: 32rpx;
font-weight: 500;
}
.empty-comment-wrapper {
position: relative;
}
.empty-comment-wrapper image {
display: none;
}
.empty-comment-wrapper .add-comment-enter {
text-align: center;
padding: 50rpx 0 120rpx;
}
.empty-comment-wrapper .add-comment-enter text {
display: block;
font-size: 28rpx;
line-height: 1.64;
color: #999999;
}
.empty-comment-wrapper .add-comment-enter .write-btn {
width: 328rpx;
height: 80rpx;
border-radius: 12rpx;
background-color: #6D8697;
border: solid 2rpx #6D8697;
margin: 30rpx auto;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
}
.comment-img-box .comment-picture {
display: flex;
flex-grow: 30%;
flex-wrap: wrap;
justify-content: space-between;
margin-top: 10rpx;
align-items: flex-start;
overflow: hidden;
}
.comment-img-box .comment-picture .comment-img {
display: flex;
align-items: center;
margin-bottom: 12rpx;
}
.comment-img-box .comment-picture li:empty {
height: 0;
border: none;
}
.comment-img-box .more-image {
position: relative;
}
.comment-img-box .more-image .overlay {
background: rgba(0, 0, 0, 0.8);
position: absolute;
border-radius: 15rpx;
right: 10rpx;
bottom: 20rpx;
z-index: 500;
color: #ffffff;
font-size: 22rpx;
text-align: center;
line-height: 33rpx;
}
<import src="root/utils/wxParse/wxParse.wxml" />
<template name="post-item-quote">
<view class="post-item-quote post-item-wrapper">
<view class="post-item-quote-content">
<template is="wxParse" data="{{nodes}}" />
</view>
</view>
</template>
<template name="post-item-image">
<view class="post-item-wrapper post-item-image">
<image mode="widthFix" src="{{imageUrl}}" bindtap="showImage" data-image-url="{{imageUrl}}" style="width: {{(w ? (w + 'px') : '100%')}}" />
</view>
</template>
<template name="post-item-video">
<view class="post-item-wrapper post-item-video">
<video src="{{url}}" />
</view>
</template>
<template name="post-item-title">
<view class="post-item-wrapper">
<view class="post-item-title">
<template is="wxParse" data="{{nodes}}" />
</view>
</view>
</template>
<template name="post-item-text">
<view class="post-item-wrapper">
<view class="post-item-text">
<template is="wxParse" data="{{nodes}}" />
</view>
</view>
</template>
<template name="post-item-separator">
<view class="post-item-wrapper post-item-separator">
<view class="separator" />
</view>
</template>
.post-item-wrapper {
padding: 0 40rpx 60rpx;
}
.post-item-image {
text-align: center;
}
.post-item-image image {
max-width: 100%;
}
.post-item-video video {
width: 100%;
height: 418.75rpx;
}
.post-item-quote .post-item-quote-content {
border-left: 4rpx solid #333437;
padding-left: 46rpx;
}
.post-item-separator .separator {
background: #ddd;
height: 4rpx;
width: 260rpx;
margin: 0 auto;
}
.post-item-text {
min-height: 58rpx;
}
.post-item-text,
.post-item-quote-content {
word-break: break-word;
}
\ No newline at end of file
import { connect } from 'root/wmp-redux'
import compose from 'ramda/src/compose'
import { fetchPosts, likePost } from 'root/actions/blog/post'
import urls from 'root/constants/blog/urls'
import mixinUrls from 'root/constants/ecommerceMixinBlog/urls'
import iconPaths from 'root/constants/blog/iconPaths'
import { setComments } from 'root/actions/blog/comment'
import { fetchPostSharing } from 'root/actions/blog/sharing'
import { setError } from 'root/actions/common/error'
import { getError } from 'root/selectors/common/error'
import { getCategories } from 'root/selectors/blog/category'
import { getPosts } from 'root/selectors/blog/post'
import { getBanners, getShortcuts } from 'root/selectors/blog/setting'
import { getUserInfo } from 'root/selectors/common/global'
import {
getStyle,
getAttr,
getNavigationBar,
} from 'root/selectors/common/global'
import { getMixLayout } from 'root/utils/helpers/helper'
import { getComponents } from 'root/selectors/common/componentsSelector'
import {
login,
bindGetUserInfoHandler,
} from 'wechat_common/utils/wrappedWXTool'
import { trackUI } from 'wechat_common/tracker/index.bs'
import shareView from 'root/templates/shareView/shareView'
import { getTeamMemberId } from 'root/selectors/presentation/teamMemberSelector'
const { iconComment, iconLike, iconSearch, iconSearchWhite } = iconPaths
const page = {
data: {
currentTab: 'post',
dotPosition: 0,
iconComment,
iconLike,
iconSearch,
iconSearchWhite,
shareLoading: false,
selectedPostId: '',
currentComment: '',
isCommenting: false,
},
onReady() {
const { mix, navigationBar } = this.data
if (Array.isArray(mix) && mix[1] == 'blog') {
if (!wx.isWept) {
wx.setNavigationBarTitle({
title: navigationBar[urls.PAGES.POST_INDEX] || '故事',
})
}
} else {
wx.setNavigationBarTitle({ title: this.data.name })
}
},
handleTab(event) {
this.setData({
currentTab: event.currentTarget.dataset.id,
})
},
handleSearch() {
let { mix } = this.data,
url = urls.PAGES.POST_SEARCH
if (mix && mix.length == 2) {
if (mix[0] == 'store') {
switch (mix[1]) {
case 'blog':
url = mixinUrls.PAGES.SEARCH
break
default:
break
}
}
}
wx.navigateTo({ url })
},
handlePost(e) {
const { id } = e.currentTarget.dataset
wx.navigateTo({ url: `${urls.PAGES.POST_DETAIL}?postId=${id}` })
},
handleProduct(e) {
this.handlePost(e)
},
handleCategory(e) {
const { id, name } = e.currentTarget.dataset
wx.navigateTo({
url: `${urls.PAGES.POST_LIST}?category=${name}&categoryId=${id}`,
})
},
handlePage(e) {
const { id } = e.currentTarget.dataset
const navigateUrl = [
'/pages/blog/about/about',
'/pages/ecommerce/about/about',
]
if (navigateUrl.includes(id)) {
wx.navigateTo({ url: id })
} else {
wx.switchTab({ url: id })
}
},
handleSliderChange(e) {
this.setData({
dotPosition: e.detail.current,
})
},
makePhoneCall(e) {
if (wx.isWept) {
this.setError('预览模式下暂不支持拨打电话,请手机预览')
} else {
const { phone = '' } = e.currentTarget.dataset
wx.makePhoneCall({
phoneNumber: phone,
})
}
},
liveChat() {
if (wx.isWept) {
this.setError('预览模式下暂不支持客服功能,请手机预览')
}
},
share() {
if (wx.isWept) {
this.setError('预览模式下暂不支持分享功能,请手机预览')
}
},
navigation(e) {
if (wx.isWept) {
this.setError('预览模式下暂不支持,请手机预览')
} else {
const addressDetail = e.currentTarget.dataset.address
const addressLocation = e.currentTarget.dataset.coordinate
if (addressLocation) {
const latitude = addressLocation.lat
const longitude = addressLocation.lng
wx.openLocation({
latitude,
longitude,
scale: 18,
address: addressDetail,
})
}
}
},
loadMorePosts() {
const { paginationPosts, siteId } = this.data
if (paginationPosts.nextPage) {
this.fetchPosts(siteId, 'all', paginationPosts.nextPage)
}
},
handleCard(e) {
const { post } = e.currentTarget.dataset
this.setData({ currentPost: post })
},
onShareAppMessage() {
const { currentPost = {}, showShareVariation } = this.data
const { title, id } = currentPost
if (id && showShareVariation) {
return {
title,
path: `${urls.PAGES.POST_DETAIL}?postId=${id}`,
}
} else {
return {
path: `${urls.PAGES.POST_INDEX}`,
}
}
},
sharePicture() {
const { siteId, currentPost } = this.data
const { id: postId } = currentPost
if (wx.isWept) {
this.setError('预览模式下暂不支持,请手机预览')
} else {
this.setData({
shareLoading: true,
})
this.fetchPostSharing(siteId, {
scene: postId.toString(),
page: 'pages/blog/postDetail/postDetail',
}).then(res => {
trackUI(
'shareImageBlogPost',
postId,
JSON.stringify({
team_member_id: this.data.teamMemberId || -1,
key: `shareImageBlogPost${postId}`,
}),
)
this.setData({
shareLoading: false,
})
if (res.success) {
wx.navigateTo({
url: `${urls.PAGES.POST_SHARE}?postId=${postId}`,
})
} else {
wx.showModal({
title: '网络错误',
content: '请稍后重试',
showCancel: false,
success: res => {
if (res.confirm) {
this.closeShareView()
}
},
})
}
})
}
},
handleLike(e) {
const { isLikingPost } = this.data
const { postId, isLiked } = e.currentTarget.dataset
const cb = userInfo => {
const { nickName, avatarUrl } = userInfo
this.setData({
selectedPostId: '',
})
if (!isLikingPost) {
this.likePost(!isLiked, postId, nickName, avatarUrl)
if (!isLiked) {
trackUI(
'likeBlogPost',
postId,
JSON.stringify({
team_member_id: this.data.teamMemberId || -1,
key: `likeBlogPost${postId}`,
}),
)
}
}
}
bindGetUserInfoHandler(e, cb)
},
switchOperation(e) {
const { post } = e.currentTarget.dataset
const { selectedPostId } = this.data
this.setData({
isCommenting: false,
currentComment: '',
selectedPostId:
selectedPostId && selectedPostId == post.id ? '' : post.id,
currentPost: post,
})
},
handleComment(e) {
const cb = () => {
this.setData({ isCommenting: true, selectedPostId: '' })
}
bindGetUserInfoHandler(e, cb)
},
handleInputComment(e) {
this.setData({ currentComment: e.detail.value })
},
handleSendComment(e) {
const { currentComment } = this.data
if (!currentComment || !currentComment.trim()) {
this.setError('评论内容不能为空')
} else if (wx.isWept) {
this.setError('预览模式下暂不支持,请手机预览')
} else {
this.setData({ isCommenting: false })
this.createComment(e.detail.formId)
}
},
createComment(formId) {
const { nickName, avatarUrl, currentPost, currentComment } = this.data
const that = this
this.setData({
submitting: true,
})
const sendData = {
content: currentComment,
nickname: nickName,
wechat_photo: [avatarUrl],
settings: {
form_id: formId,
},
}
login({
success: loginRes => {
sendData.code = loginRes.code
that.setComments(
currentPost.id,
sendData,
() => {
that.setData({
submitting: false,
currentComment: '',
})
wx.showToast({
title: '评论成功,审核通过后显示在留言列表',
icon: 'none',
duration: 2000,
})
trackUI(
'commentBlogPost',
currentPost.id,
JSON.stringify({
team_member_id: this.data.teamMemberId || -1,
key: `commentBlogPost${currentPost.id}`,
}),
)
},
() => {
that.setData({ submitting: false })
},
)
},
fail() {
wx.showModal({
content: '请求失败,请重试',
})
that.setData({
submitting: false,
})
},
})
},
handleGlobalTab() {
this.setData({
selectedPostId: '',
isCommenting: false,
currentComment: '',
})
},
onPullDownRefresh() {
this.fetchPosts(this.data.siteId, 'all', 1).then(res =>
wx.stopPullDownRefresh(),
)
},
}
function mapStateToProps(state) {
const { list: categories, isFetching: isFetchingCategories } = getCategories(
state,
),
{
list: posts,
isFetching: isFetchingPosts,
pagination: paginationPosts,
isLikingPost,
} = getPosts(state, urls.PAGES.POST_INDEX),
{ mainBackground: background, isWhiteBackground } = getStyle(state),
{ siteId, layout, searchEnable, mix, companyName, name, logoUrl } = getAttr(
state,
),
{ nickName, avatarUrl } = getUserInfo(state),
{ navigationBar } = getNavigationBar(state),
{
shortcuts,
shortcutsFirstLine,
shortcutsSecondLine,
shortcutsSecondLineWrapperClass,
} = getShortcuts(state),
{ errorMessage } = getError(state),
tabs = [{ id: 'post', name: '文章' }]
if (categories && categories.length) {
tabs.push({ id: 'category', name: '分类' })
}
const showContactBtn = state.getIn(['globalData', 'attr', 'showContactBtn'])
let blogLayout = getMixLayout(mix, layout, 'blog')
let ifInPageDesign = false
let components = []
let shortcutsLayout = ''
if (state.get('components') !== null) {
components = getComponents(state, 'blog')
ifInPageDesign = true
const blogAndCategoryComp = components.find(
comp => comp.type === 'blogAndCategory',
)
const shortcutsComp = components.find(comp => comp.type === 'shortcuts')
if (blogAndCategoryComp && blogAndCategoryComp.settings) {
blogLayout = blogAndCategoryComp.settings.layout || 'a'
}
if (shortcutsComp && shortcutsComp.settings) {
shortcutsLayout = shortcutsComp.settings.layout || 'circle'
}
}
// hack temporarily, will be removed in v2
if (Array.isArray(mix) && mix[0] === 'presentation') {
ifInPageDesign = false
}
const enableComments = state.getIn([
'blog',
'setting',
'settings',
'enableComments',
])
const teamMemberId = getTeamMemberId(state)
return {
posts,
isFetchingPosts,
paginationPosts,
enableComments,
isLikingPost,
categories,
isFetchingCategories,
teamMemberId,
siteId,
layout: blogLayout,
searchEnable: searchEnable || false,
background,
isWhiteBackground,
companyName,
name,
logoUrl,
errorMessage,
nickName,
avatarUrl,
shortcuts: shortcuts.map(
item =>
shortcutsLayout
? Object.assign({}, item, { iconType: shortcutsLayout })
: item,
),
shortcutsFirstLine: shortcutsFirstLine.map(
item =>
shortcutsLayout
? Object.assign({}, item, { iconType: shortcutsLayout })
: item,
),
shortcutsSecondLine: shortcutsSecondLine.map(
item =>
shortcutsLayout
? Object.assign({}, item, { iconType: shortcutsLayout })
: item,
),
shortcutsSecondLineWrapperClass,
banners: getBanners(state),
tabs,
mix,
navigationBar,
showContactBtn: showContactBtn || false,
showSlider:
!ifInPageDesign ||
Boolean(components.find(comp => comp.type === 'slider')),
showShortcuts:
!ifInPageDesign ||
Boolean(components.find(comp => comp.type === 'shortcuts')),
showBlogPosts:
!ifInPageDesign ||
Boolean(components.find(comp => comp.type === 'blogAndCategory')),
ifInPageDesign,
isWept: wx.isWept || false,
}
}
function mapDispatchToProps(dispatch) {
return {
fetchPosts: (siteId, category, pageNum) =>
dispatch(fetchPosts(urls.PAGES.POST_INDEX, siteId, category, pageNum)),
setError: errorMessage => dispatch(setError(errorMessage)),
fetchPostSharing: (siteId, data) =>
dispatch(fetchPostSharing(siteId, data)),
setComments: (postId, setData, successCb, failCb) =>
dispatch(setComments(postId, setData, successCb, failCb)),
likePost: (status, postId, nickName, avatarUrl) =>
dispatch(likePost(status, postId, nickName, avatarUrl)),
}
}
const enhance = compose(shareView, connect(mapStateToProps, mapDispatchToProps))
Page(enhance(page))
{
"enablePullDownRefresh": true
}
@import "/styles/main.less";
@import '/templates/tabs/tabs.less';
@import '/templates/loaderPage/loaderPage.less';
@import '/templates/loaderBar/loaderBar.less';
@import '/templates/slider/slider.less';
@import '/templates/shortcuts/shortcuts.less';
@import '/templates/postCard/postCard.less';
@import '/templates/categoryCard/categoryCard.less';
@import "/templates/messageModal/messageModal.less";
@import "/templates/shareView/shareView.less";
@light-background: #f4f4f4;
.post-index {
display: flex;
flex-direction: column;
height: 100vh;
text {
font-size: 17px;
color: #fff;
}
scroll-view {
height: 100vh;
flex: 1 1 0%;
}
.posts {
padding: 26rpx;
}
.categories {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
justify-content: space-between;
padding: 30rpx 18rpx;
background: @light-background;
}
}
.comment-form {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 15rpx 30rpx;
border: 2rpx solid #e2e4e7;
background-color: #f4f4f4;
.component-placeholder {
font-size: 28rpx;
color: #c6c9cd;
}
.comment-input {
width: 100%;
height: 68rpx;
line-height: 70rpx;
padding-left: 18rpx;
font-size: 28rpx;
color: black;
border: 2rpx solid #e2e4e7;
border-radius: 8rpx;
background-color: white;
}
.comment-btn {
display: inline-block;
height: 70rpx;
width: 150rpx;
line-height: 70rpx;
margin-left: 15rpx;
font-size: 28rpx;
text-align: center;
color: white;
background-color: #62b900;
&.disabled{
background-color: #f6f6f6;
color: #e2e4e7;
}
}
}
<import src="root/templates/postCard/postCard.wxml"/>
<import src="root/templates/categoryCard/categoryCard.wxml"/>
<import src="root/templates/tabs/tabs.wxml"/>
<import src="root/templates/loaderPage/loaderPage.wxml"/>
<import src="root/templates/loaderBar/loaderBar.wxml"/>
<import src="root/templates/slider/slider.wxml"/>
<import src="root/templates/shortcuts/shortcuts.wxml"/>
<import src="root/templates/messageModal/messageModal.wxml"/>
<import src="root/templates/shareView/shareView.wxml"/>
<view class="post-index">
<block wx:if="{{showBlogPosts}}">
<template
is='tabs'
data="{{background, isWhiteBackground, currentTab, hasSearch: searchEnable, iconSearchWhite, iconSearch, tabs, handleTab, handleSearch}}"
/>
</block>
<scroll-view wx:if="{{!isFetchingPosts || posts.length > 0}}"
style="height: {{categories.length ? 'calc(100vh - 80rpx)' : '100vh'}}; box-sizing: border-box; background-color: {{layout === 'd' ? 'white': '#f4f4f4'}}"
scroll-y="true" hidden="{{currentTab == 'category'}}" lower-threshold="80"
bindscrolltolower="loadMorePosts"
bindtap="handleGlobalTab"
>
<block wx:if="{{showSlider}}">
<block wx:if="{{banners && banners.length}}">
<template
is="slider"
data="{{ sliders: banners, dotPosition, hasVideoBanner, handleProduct, handleCategory, handlePage, handleSliderChange }}"
/>
</block>
<block wx:else>
<view wx:if="{{isWept && ifInPageDesign}}" class="component-placeholder" style="height: 300rpx;margin-bottom: 30rpx;">
您还未添加轮播图
</view>
</block>
</block>
<block wx:if="{{showShortcuts}}">
<block wx:if="{{shortcuts && shortcuts.length}}">
<template
is="shortcuts"
data="{{ shortcuts, shortcutsFirstLine, shortcutsSecondLine, shortcutsSecondLineWrapperClass, handleProduct, handleCategory, handlePage, makePhoneCall, showContactBtn }}"
/>
</block>
<block wx:else>
<view wx:if="{{isWept && ifInPageDesign}}" class="component-placeholder" style="height: 224rpx;margin-bottom: 30rpx;">
您还未添加快捷按钮
</view>
</block>
</block>
<block wx:if="{{showBlogPosts}}">
<view class="posts">
<block wx:for="{{posts}}" wx:key="id" wx:for-item="item">
<template wx:if="{{layout === 'a'}}" is="post-card-a" data="{{...item, handlePost}}"/>
<template wx:if="{{layout === 'b'}}" is="post-card-b" data="{{...item, handlePost}}"/>
<template wx:if="{{layout === 'c'}}" is="post-card-c" data="{{...item, handlePost}}"/>
<template wx:if="{{layout === 'd'}}" is="post-card-moment" data="{{item, index, companyName, logoUrl, name, handleCard, handlePost, handleLike, handleComment, openShareView, selectedPostId, enableComments}}"/>
</block>
</view>
<template
is='loader-bar'
data="{{isLoading: posts.length > 0 && paginationPosts.nextPage, emptyText: posts && posts.length === 0 && '- 暂无文章 -', noMoreText:posts && posts.length !== 0 && paginationPosts.currentPage === paginationPosts.totalPages && '- 无更多文章 -', style:'margin: 0 0 40rpx 0'}}"
/>
</block>
</scroll-view>
<template wx:else is='loader-page'/>
<scroll-view style="height: {{categories.length ? 'calc(100vh - 80rpx)' : '100vh'}}; box-sizing: border-box;"
scroll-y="true" hidden="{{currentTab == 'post'}}" bindtap="handleGlobalTab">
<view class="categories">
<block wx:for="{{categories}}" wx:key="id" wx:for-item="item">
<template is="category-card" data="{{...item, handleCategory}}"/>
</block>
</view>
</scroll-view>
<form bindsubmit="handleSendComment">
<view class="comment-form" wx:if="{{isCommenting}}">
<input placeholder-class="comment-placeholder" class="comment-input" placeholder="评论" value="{{currentComment}}" bindinput="handleInputComment" cursor-spacing="8" focus="{{true}}"/>
<button class="comment-btn {{currentComment ? '' : 'disabled'}}" formType="submit">发送</button>
</view>
</form>
</view>
<view wx:if="{{errorMessage}}">
<template is="message-modal" data="{{message: errorMessage}}" />
</view>
<template is="share-view" data="{{shareLoading, sharePicture, shareAnimation, showShareVariation, closeShareView, openShareView}}"/>
import { connect } from 'root/wmp-redux'
import compose from 'ramda/src/compose'
import { fetchPosts, likePost } from 'root/actions/blog/post'
import urls from 'root/constants/blog/urls'
import iconPaths from 'root/constants/blog/iconPaths'
import { setComments } from 'root/actions/blog/comment'
import { fetchPostSharing } from 'root/actions/blog/sharing'
import { getPosts } from 'root/selectors/blog/post'
import { getAttr, getUserInfo } from 'root/selectors/common/global'
import { getComponents } from 'root/selectors/common/componentsSelector'
import {
login,
bindGetUserInfoHandler,
} from 'wechat_common/utils/wrappedWXTool'
import { getMixLayout } from 'root/utils/helpers/helper'
import shareView from 'root/templates/shareView/shareView'
import { trackUI } from 'wechat_common/tracker/index.bs'
import { getTeamMemberId } from 'root/selectors/presentation/teamMemberSelector'
const { iconComment, iconLike } = iconPaths
const page = {
data: {
currentCategory: 'all',
iconComment,
iconLike,
shareLoading: false,
selectedPostId: '',
currentComment: '',
isCommenting: false,
},
onLoad(options) {
wx.setNavigationBarTitle({
title: options.category,
})
this.setData({
isFetchingPosts: true,
currentCategory: options.category,
})
const { paginationPosts, siteId } = this.data
if (!paginationPosts) {
this.fetchPosts(siteId, options.category, 1)
}
},
handlePost(e) {
const { id } = e.currentTarget.dataset
wx.navigateTo({ url: `${urls.PAGES.POST_DETAIL}?postId=${id}` })
},
loadMorePosts() {
const { paginationPosts, siteId, currentCategory } = this.data
if (paginationPosts.nextPage) {
this.fetchPosts(siteId, currentCategory, paginationPosts.nextPage)
}
},
handleCard(e) {
const { post } = e.currentTarget.dataset
this.setData({ currentPost: post })
},
onShareAppMessage() {
const { currentPost = {}, showShareVariation, currentCategory } = this.data
const { title, id } = currentPost
if (id && showShareVariation) {
return {
title,
path: `${urls.PAGES.POST_DETAIL}?postId=${id}`,
}
} else {
return {
path: `${urls.PAGES.POST_LIST}?category=${currentCategory}`,
}
}
},
sharePicture() {
const { siteId, currentPost } = this.data
const { id: postId } = currentPost
if (wx.isWept) {
this.setError('预览模式下暂不支持,请手机预览')
} else {
this.setData({
shareLoading: true,
})
this.fetchPostSharing(siteId, {
scene: postId.toString(),
page: 'pages/blog/postDetail/postDetail',
}).then(res => {
trackUI(
'shareImageBlogPost',
postId,
JSON.stringify({
team_member_id: this.data.teamMemberId || -1,
key: `shareImageBlogPost${postId}`,
}),
)
this.setData({
shareLoading: false,
})
if (res.success) {
wx.navigateTo({
url: `${urls.PAGES.POST_SHARE}?postId=${postId}`,
})
} else {
wx.showModal({
title: '网络错误',
content: '请稍后重试',
showCancel: false,
success: res => {
if (res.confirm) {
this.closeShareView()
}
},
})
}
})
}
},
handleLike(e) {
const { postId, isLiked } = e.currentTarget.dataset
const cb = userInfo => {
const { nickName, avatarUrl } = userInfo
this.setData({ selectedPostId: '' })
if (!isLiked) {
trackUI(
'likeBlogPost',
postId,
JSON.stringify({
team_member_id: this.data.teamMemberId || -1,
key: `likeBlogPost${postId}`,
}),
)
}
this.likePost(!isLiked, postId, nickName, avatarUrl)
}
bindGetUserInfoHandler(e, cb)
},
switchOperation(e) {
const { post } = e.currentTarget.dataset
const { selectedPostId } = this.data
this.setData({
isCommenting: false,
currentComment: '',
selectedPostId:
selectedPostId && selectedPostId == post.id ? '' : post.id,
currentPost: post,
})
},
handleComment(e) {
const cb = () => {
this.setData({ isCommenting: true, selectedPostId: '' })
}
bindGetUserInfoHandler(e, cb)
},
handleInputComment(e) {
this.setData({ currentComment: e.detail.value })
},
handleSendComment(e) {
const { currentComment } = this.data
if (!currentComment || !currentComment.trim()) {
this.setError('评论内容不能为空')
} else if (wx.isWept) {
this.setError('预览模式下暂不支持,请手机预览')
} else {
this.setData({ isCommenting: false })
this.createComment(e.detail.formId)
}
},
createComment(formId) {
const { currentPost, currentComment, nickName, avatarUrl } = this.data
const that = this
this.setData({
submitting: true,
})
const sendData = {
content: currentComment,
nickname: nickName,
wechat_photo: [avatarUrl],
settings: {
form_id: formId,
},
}
login({
success: loginRes => {
sendData.code = loginRes.code
that.setComments(
currentPost.id,
sendData,
() => {
that.setData({
submitting: false,
currentComment: '',
})
wx.showToast({
title: '评论成功,审核通过后显示在留言列表',
icon: 'none',
duration: 2000,
})
trackUI(
'commentBlogPost',
currentPost.id,
JSON.stringify({
team_member_id: this.data.teamMemberId || -1,
key: `commentBlogPost${currentPost.id}`,
}),
)
},
() => {
that.setData({ submitting: false })
},
)
},
fail() {
wx.showModal({
content: '请求失败,请重试',
})
that.setData({
submitting: false,
})
},
})
},
handleGlobalTab() {
this.setData({
selectedPostId: '',
isCommenting: false,
currentComment: '',
})
},
onPullDownRefresh() {
const { siteId, currentCategory } = this.data
this.fetchPosts(siteId, currentCategory, 1).then(res =>
wx.stopPullDownRefresh(),
)
},
}
function mapStateToProps(state, options) {
const {
list: posts,
isFetching: isFetchingPosts,
pagination: paginationPosts,
} = getPosts(state, `${urls.PAGES.POST_LIST}_${options.category}`),
{ siteId, layout, mix, companyName, name, logoUrl } = getAttr(state),
enableComments = state.getIn([
'blog',
'setting',
'settings',
'enableComments',
])
const { nickName, avatarUrl } = getUserInfo(state)
let blogLayout = getMixLayout(mix, layout, 'blog')
let components = []
if (state.get('components') !== null) {
components = getComponents(state, 'blog')
const blogAndCategoryComp = components.find(
comp => comp.type === 'blogAndCategory',
)
if (blogAndCategoryComp && blogAndCategoryComp.settings) {
blogLayout = blogAndCategoryComp.settings.layout || 'a'
}
}
const teamMemberId = getTeamMemberId(state)
return {
posts,
isFetchingPosts,
paginationPosts,
enableComments,
teamMemberId,
siteId,
layout: blogLayout,
logoUrl,
companyName,
name,
nickName,
avatarUrl,
}
}
function mapDispatchToProps(dispatch) {
return {
fetchPosts: (siteId, category, pageNum) =>
dispatch(
fetchPosts(
`${urls.PAGES.POST_LIST}_${category}`,
siteId,
category,
pageNum,
),
),
fetchPostSharing: (siteId, data) =>
dispatch(fetchPostSharing(siteId, data)),
setComments: (postId, setData, successCb, failCb) =>
dispatch(setComments(postId, setData, successCb, failCb)),
likePost: (status, postId, nickName, avatarUrl) =>
dispatch(likePost(status, postId, nickName, avatarUrl)),
}
}
const enhance = compose(shareView, connect(mapStateToProps, mapDispatchToProps))
Page(enhance(page))
{
"enablePullDownRefresh": true
}
<import src="root/templates/postCard/postCard.wxml"/>
<import src="root/templates/loaderPage/loaderPage.wxml"/>
<import src="root/templates/loaderBar/loaderBar.wxml"/>
<import src="root/templates/shareView/shareView.wxml"/>
<view class="post-index">
<block wx:if="{{!isFetchingPosts || posts.length > 0}}">
<scroll-view style="height: 100vh; background-color:#fff;" scroll-y="true" lower-threshold="80" bindscrolltolower="loadMorePosts" bindtap="handleGlobalTab">
<view class="posts">
<block wx:for="{{posts}}" wx:key="id" wx:for-item="item">
<template wx:if="{{layout === 'a'}}" is="post-card-a" data="{{...item, handlePost}}"/>
<template wx:if="{{layout === 'b'}}" is="post-card-b" data="{{...item, handlePost}}"/>
<template wx:if="{{layout === 'c'}}" is="post-card-c" data="{{...item, handlePost}}"/>
<template wx:if="{{layout === 'd'}}" is="post-card-moment" data="{{item, index, companyName, logoUrl, name, handleCard, handlePost, handleLike, handleComment, openShareView, selectedPostId, enableComments}}"/>
</block>
</view>
<template
is='loader-bar'
data="{{isLoading: posts.length > 0 && paginationPosts.nextPage, emptyText: posts && posts.length === 0 && '- 暂无文章 -', noMoreText:posts && posts.length !== 0 && paginationPosts.currentPage === paginationPosts.totalPages && '- 无更多文章 -', style:'margin: 0 0 40rpx 0'}}"
/>
</scroll-view>
<form bindsubmit="handleSendComment">
<view class="comment-form" wx:if="{{isCommenting}}">
<input placeholder-class="comment-placeholder" class="comment-input" placeholder="评论" value="{{currentComment}}" bindinput="handleInputComment" cursor-spacing="8" focus="{{true}}"/>
<button class="comment-btn {{currentComment ? '' : 'disabled'}}" formType="submit">发送</button>
</view>
</form>
</block>
<template wx:else is='loader-page'/>
<template is="share-view" data="{{shareLoading, sharePicture, shareAnimation, showShareVariation, closeShareView, openShareView}}"/>
</view>
@import '/components/blog/blog/blog.wxss';
@import "/styles/main.wxss";
import { debounce } from 'wechat_common/utils/utils'
import { connect } from 'root/wmp-redux'
import { afterClearData } from 'root/actions/common/tools'
import {
fetchPostsSearch,
clearPostsSearch,
} from 'root/actions/blog/postSearch'
import urls from 'root/constants/blog/urls.js'
import iconPaths from 'root/constants/blog/iconPaths'
import { getPostsSearch } from 'root/selectors/blog/postSearch'
import { getStyle, getAttr } from 'root/selectors/common/global'
const { iconClear, iconLink, iconNoResult, iconClearWhite } = iconPaths
const page = {
data: {
keywords: '',
isFirst: true,
iconClear,
iconClearWhite,
iconLink,
iconNoResult,
},
handlePost(e) {
const { id } = e.currentTarget.dataset
wx.navigateTo({ url: `${urls.PAGES.POST_DETAIL}?postId=${id}` })
},
handleInput(event) {
const value = event.detail.value
this.setData({ isFirst: false, keywords: value })
const { siteId, keywords } = this.data
this.fetchPostsAfterClear(siteId, keywords, 1)
},
handleClear() {
this.setData({ keywords: '' })
this.clearPosts()
},
loadMorePosts() {
let {
paginationPosts: { pages, currPage },
siteId,
keywords,
isFetchingPosts,
} = this.data
if (!isFetchingPosts && currPage < pages) {
this.fetchPosts(siteId, keywords, ++currPage)
}
},
}
function mapStateToProps(state) {
const {
list: posts,
isFetching: isFetchingPosts,
pagination: paginationPosts,
} = getPostsSearch(state, urls.PAGES.POST_SEARCH),
{ mainBackground: background, isWhiteBackground } = getStyle(state),
{ siteId } = getAttr(state)
return {
posts,
isFetchingPosts,
paginationPosts,
siteId,
background,
isWhiteBackground,
}
}
function mapDispatchToProps(dispatch) {
const PAGE = urls.PAGES.POST_SEARCH
return {
clearPosts: () => dispatch(clearPostsSearch(PAGE)),
fetchPosts: (siteId, keywords, pageNum) =>
dispatch(fetchPostsSearch(PAGE, siteId, keywords, pageNum)),
fetchPostsAfterClear: debounce(
(siteId, keywords, pageNum) =>
dispatch(
afterClearData(
clearPostsSearch(PAGE),
fetchPostsSearch(PAGE, siteId, keywords, pageNum),
),
),
600,
),
}
}
const connectedPage = connect(page, mapStateToProps, mapDispatchToProps)
Page(connectedPage)
{
"navigationBarTitleText": "搜索"
}
\ No newline at end of file
<import src="root/templates/searchBar/searchBar.wxml"/>
<import src="root/templates/loaderBar/loaderBar.wxml"/>
<import src="root/templates/loaderPage/loaderPage.wxml"/>
<import src="root/templates/resultPage/resultPage.wxml"/>
<import src="root/templates/postCard/postCard.wxml"/>
<view class="post-search" style="height: 100vh; overflow:hidden;">
<template
is="search-bar"
data="{{background, isWhiteBackground, value:keywords, placeholder:'搜索文章', iconClear, iconClearWhite, handleInput, handleClear}}"
/>
<template wx:if="{{isFirst || !keywords}}" is='result-page'
data="{{height: 'calc(100vh - 112rpx)', text: '结果将会显示在这里'}}"/>
<block wx:if="{{posts.length}}">
<block wx:if="{{keywords}}">
<view class="search-result" style="width:calc(100vw - 80rpx)">
<text class="search-result-text">{{paginationPosts.totalCount}}条关于"{{keywords}}"的结果</text>
</view>
<scroll-view class="posts-content" lower-threshold="60" scroll-y="true" style="height:calc(100vh - 202rpx)"
bindscrolltolower="loadMorePosts">
<block wx:for="{{posts}}" wx:key="{{item.id}}" wx:for-index="index" wx:for-item="item">
<template is="post-card-d" data="{{...item, handlePost, icon: iconLink}}"/>
</block>
<template
is='loader-bar'
data="{{isLoading: isFetchingPosts, noMoreText: !isFetchingPosts&& paginationPosts.currPage === paginationPosts.pages && '- 无更多结果 -', style:'margin: 56rpx 0 40rpx 0'}}"
/>
</scroll-view>
</block>
</block>
<block wx:else>
<template wx:if="{{isFetchingPosts}}" is='loader-page' data="{{height: 'calc(100vh - 112rpx)'}}"/>
<template wx:else is='result-page'
data="{{height: 'calc(100vh - 109rpx)', icon: iconNoResult, text: '未找到相关结果'}}"/>
</block>
</view>
@import '/templates/searchBar/searchBar.wxss';
@import '/templates/loaderBar/loaderBar.wxss';
@import '/templates/loaderPage/loaderPage.wxss';
@import '/templates/resultPage/resultPage.wxss';
@import '/templates/postCard/postCard.wxss';
@import "/styles/main.wxss";
.post-search .search-result {
padding: 28rpx 16rpx 28rpx 40rpx;
width: 100vw;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.post-search .search-result .search-result-text {
height: 40rpx;
font-size: 28rpx;
color: #b6b6b6;
}
import { connect } from 'root/wmp-redux'
import iconPaths from 'root/constants/blog/iconPaths'
import { cutString, saveCanvasImage } from 'root/utils/helpers/helper'
import { getImageInfo } from 'wechat_common/utils/helpers/shareHelper'
import { getSharing } from 'root/selectors/blog/sharing'
import { replaceQnDomain } from 'wechat_common/utils/helpers/imageHelper'
import { formatProtocol } from 'wechat_common/utils/utils'
import urls from 'root/constants/blog/urls'
import { getPosts } from 'root/selectors/blog/post'
import { getPostDetail } from 'root/selectors/blog/postDetail'
const { iconDownload } = iconPaths
const TITLE_PER_LINE_WORDS = 16
const TITILE_MAX_LINES = 2
const CONTENT_PER_LINE_WORDS = 19
const CONTENT_MAX_LINES = 6
const NAME_PER_LINE_WORDS = 9
const NAME_MAX_LINES = 1
const page = {
data: {
iconDownload,
hasDrawn: false,
},
onLoad(option) {
const { postList } = this.data
const { postId } = option
const { blurb, title: headerTitle, iconUrl: backgroundUrl } = postList.find(
item => item.id == postId,
)
const { officialName = '', officialIconUrl, sceneCodeUrl } = this.data
const title = cutString(headerTitle, TITLE_PER_LINE_WORDS * 2)
const content = cutString(blurb, CONTENT_PER_LINE_WORDS * 2)
const name = cutString(officialName, NAME_PER_LINE_WORDS * 2)
const { screenWidth, screenHeight } = wx.getSystemInfoSync()
const promiseList = [backgroundUrl, officialIconUrl].map(url =>
getImageInfo(formatProtocol(replaceQnDomain(url))),
)
const ctx = wx.createCanvasContext('share-image')
Promise.all(promiseList)
.then(([backgroundUrl, officialIconUrl]) => {
ctx.scale(screenWidth / 375, screenWidth / 375)
ctx.setFillStyle('white')
ctx.fillRect(0, 0, 329, 476)
ctx.drawImage(backgroundUrl.path, 0, 0, 329, 189.5)
ctx.setFillStyle('rgba(0, 0, 0, 0.2)')
ctx.fillRect(0, 0, 329, 189.5)
ctx.setFontSize(18)
ctx.setFillStyle('#ffffff')
ctx.setTextAlign('left')
title.slice(0, TITILE_MAX_LINES).forEach((item, index) => {
if (
title.length > TITILE_MAX_LINES &&
index === TITILE_MAX_LINES - 1
) {
item = `${item.slice(0, item.length - 1)}...`
}
ctx.fillText(item, 18, 155 + index * 22)
})
ctx.setFontSize(15)
ctx.setFillStyle('#222222')
content.slice(0, CONTENT_MAX_LINES).forEach((item, index) => {
if (
content.length > CONTENT_MAX_LINES &&
index === CONTENT_MAX_LINES - 1
) {
item = `${item.slice(0, item.length - 1)}...`
}
ctx.fillText(item, 18, 220 + index * 24)
})
const grd = ctx.createLinearGradient(0, 245, 0, 355)
grd.addColorStop(0, 'rgba(255, 255, 255, 0)')
grd.addColorStop(1, 'white')
ctx.setFillStyle(grd)
ctx.fillRect(0, 245, 329, 100)
ctx.save()
ctx.beginPath()
ctx.arc(43, 401, 23, 0, 2 * Math.PI)
ctx.clip()
ctx.drawImage(officialIconUrl.path, 20, 378, 46, 46)
ctx.restore()
ctx.setFillStyle('#222222')
ctx.setTextAlign('left')
ctx.setFontSize(14)
name.slice(0, NAME_MAX_LINES).forEach((item, index) => {
if (name.length > NAME_MAX_LINES && index === NAME_MAX_LINES - 1) {
item = `${item.slice(0, item.length - 1)}...`
}
ctx.fillText(item, 72, 395)
})
ctx.setFontSize(12)
ctx.setFillStyle('#bbbbbb')
ctx.fillText('扫码查看完整文章', 72, 417)
return getImageInfo(replaceQnDomain(sceneCodeUrl))
})
.then(sceneCodeUrl => {
ctx.drawImage(sceneCodeUrl.path, 201, 350, 110, 110)
ctx.draw()
this.setData({
hasDrawn: true,
})
})
.catch(e => {
ctx.draw()
this.setData({
hasDrawn: true,
})
wx.showModal({
title: '网络错误',
content: '图片生成失败',
showCancel: false,
})
})
},
saveImage() {
saveCanvasImage('share-image')
},
}
function mapStateToProps(state, ownProps) {
const { officialName, officialIconUrl, sceneCodeUrl } = getSharing(state)
const { list: postList } = getPosts(state, urls.PAGES.POST_INDEX)
const { headerTitleString, backgroundUrl } = getPostDetail(state)
return {
officialName,
officialIconUrl,
sceneCodeUrl,
headerTitleString,
backgroundUrl,
postList,
}
}
const connectedPage = connect(page, mapStateToProps, {})
Page(connectedPage)
{
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#292940"
}
<view class="share-container">
<canvas canvas-id="share-image" class="share-canvas" style="display: {{hasDrawn ? 'flex' : 'none' }}"></canvas>
<view class="flex-row flex-col center-align-item" style="width: 658rpx;height: 952rpx; margin-top: 48rpx; background: #fff" wx:if="{{!hasDrawn}}">
<view class="loader"/>
</view>
<view class="share-save" bindtap="saveImage">
<image src="{{iconDownload}}" />
<text>保存图片</text>
</view>
</view>
@import "/styles/main.wxss";
@import "/templates/loader/loader.wxss";
.share-container {
width: 100vw;
height: 100vh;
background: #292940;
display: flex;
justify-content: center;
flex-wrap: wrap;
}
.share-canvas {
width: 658rpx;
height: 952rpx;
margin-top: 48rpx;
background: transparent;
}
.share-save {
display: flex;
width: 656rpx;
height: 96rpx;
background: rgba(255, 255, 255, 0.18);
justify-content: center;
align-items: center;
border: solid 2rpx rgba(255, 255, 255, 0.17);
border-radius: 10rpx;
image {
width: 48rpx;
height: 48rpx;
margin-right: 16rpx;
}
text {
color: rgba(255, 255, 255, 0.8);
font-size: 32rpx;
}
}
button {
font-size: 16px;
}
.flex-row {
display: flex;
flex-direction: row;
align-items: center;
}
.flex-row.flex-start {
align-items: flex-start;
}
.flex-row.space-between {
justify-content: space-between;
}
.flex-row.center-align-item {
justify-content: center;
}
.flex-col {
display: flex;
flex-direction: column;
}
.left-align {
text-align: left;
}
.right-align {
text-align: right;
}
.auto-flex {
flex: 0 0 auto;
}
.normal-flex {
flex: 1 1 0%;
}
.gray-text {
color: #666;
}
.light-text {
color: #999;
}
scroll-view {
overflow: scroll;
}
page {
background: #f4f4f4;
line-height: 1.2;
font-family: -apple-system-font, 'Helvetica Neue', Helvetica, 'Microsoft YaHei', sans-serif;
}
scroll-view::-webkit-scrollbar {
width: 0;
height: 0;
color: transparent;
}
@-webkit-keyframes load3 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes load3 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
\ No newline at end of file
.loader {
font-size: 10px;
margin: 0;
text-indent: -9999em;
width: 20px;
height: 20px;
display: inline-block;
border-radius: 50%;
background: #797979;
background: -moz-linear-gradient(left, #797979 10%, rgba(121, 121, 121, 0) 42%);
background: -webkit-linear-gradient(left, #797979 10%, rgba(121, 121, 121, 0) 42%);
background: -o-linear-gradient(left, #797979 10%, rgba(121, 121, 121, 0) 42%);
background: -ms-linear-gradient(left, #797979 10%, rgba(121, 121, 121, 0) 42%);
background: linear-gradient(to right, #797979 10%, rgba(121, 121, 121, 0) 42%);
position: relative;
-webkit-animation: load3 1.4s infinite linear;
animation: load3 1.4s infinite linear;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
.loader:before {
width: 50%;
height: 50%;
background: #797979;
border-radius: 100% 0 0 0;
position: absolute;
top: 0;
left: 0;
content: '';
}
.loader:after {
background: #f4f4f4;
width: 75%;
height: 75%;
border-radius: 50%;
content: '';
position: absolute;
left: 50%;
top: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
\ No newline at end of file
Page()
export const data = {
height: '100vh',
icon: '',
text: '',
}
<template name="result-page">
<view class="result-page" style="height: {{height || '100vh'}}">
<image wx:if="{{icon}}" class="content-icon" src="{{icon}}" mode="aspectFit"/>
<text class="content-text">{{text}}</text>
</view>
</template>
.result-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.result-page .content-icon {
width: 86rpx;
height: 86rpx;
display: block;
margin: 0 auto 26rpx auto;
}
.result-page .content-text {
height: 45rpx;
line-height: 45rpx;
font-size: 32rpx;
text-align: center;
color: #b6b6b6;
}
\ No newline at end of file
Page()
export const data = {
background: '#ffffff',
isWhiteBackground: true,
placeholder: '',
value: '',
iconClear: '',
iconClearWhite: '',
}
export function handleInput(event) {}
export function handleClear() {}
<template name="search-bar">
<view class="search-bar" style="{{'background-color:' + background}}">
<block wx:if="{{isWhiteBackground}}">
<input class="search-input" placeholder="{{placeholder || '搜索'}}"
placeholder-style="color:rgba(0, 0, 0, 0.5)"
focus="{{true}}" bindinput="handleInput"
style="{{'color:black; background-color:'+ background}}"
value="{{value}}"/>
</block>
<block wx:else>
<input class="search-input" placeholder="{{placeholder || '搜索'}}"
placeholder-style="color:rgba(255, 255, 255, 0.5)"
focus="{{true}}" bindinput="handleInput"
style="{{'color:white; background-color:'+ background}}"
value="{{value}}"/>
</block>
<block wx:if="{{value}}">
<image class="clear-icon" src="{{isWhiteBackground ? iconClear: iconClearWhite}}" mode="aspectFit"
bindtap="handleClear"/>
</block>
</view>
</template>
<template name="search-bar-new">
<view class="search-bar-new" style="background-color:{{background}}">
<view class="search-wrapper">
<image class="search-icon" src="{{iconSearch}}" mode="aspectFit"/>
<input class="search-input"
placeholder="{{value ? '' : placeholder || '搜索'}}"
focus="{{true}}"
bindinput="handleInput"
value="{{value}}"
style="color: black; background-color: #f6f6f6"
placeholder-style="color:rgba(0, 0, 0, 0.6)"
/>
<image wx:if="{{value}}" class="clear-icon" src="{{iconClear}}" mode="aspectFit" bindtap="handleClear"/>
</view>
</view>
</template>
.search-bar {
display: flex;
justify-content: space-between;
padding: 18rpx 16rpx 18rpx 24rpx;
background-color: #ffffff;
box-shadow: 0 2rpx 4rpx 0 rgba(0, 0, 0, 0.12);
}
.search-bar .search-input {
width: 100%;
line-height: 45rpx;
padding: 15rpx 0 24rpx 16rpx;
border-radius: 6rpx;
font-size: 32rpx;
font-weight: normal;
color: #212121;
}
.search-bar .search-input::placeholder {
color: #BDC1C5;
}
.search-bar .clear-icon {
width: 45rpx;
height: 45rpx;
padding: 15rpx 24rpx 24rpx 16rpx;
}
.search-bar-new {
padding: 0 30rpx 24rpx;
background-color: #ffffff;
box-shadow: 0 2rpx 4rpx 0 rgba(0, 0, 0, 0.12);
}
.search-bar-new .search-wrapper {
background: #f6f6f6;
border-radius: 4px;
overflow: hidden;
}
.search-bar-new .search-wrapper .search-input {
height: 64rpx;
font-size: 24rpx;
padding: 0 70rpx;
color: rgba(255, 255, 255, 0.4);
background-color: #f6f6f6;
}
.search-bar-new .search-wrapper .search-input::placeholder {
color: #BDC1C5;
}
.search-bar-new .search-wrapper .search-icon {
position: absolute;
width: 32rpx;
height: 32rpx;
top: 16rpx;
left: 50rpx;
}
.search-bar-new .search-wrapper .clear-icon {
position: absolute;
width: 32rpx;
height: 32rpx;
padding: 15rpx 25px;
top: 0;
right: 0;
}
\ No newline at end of file
import {
doAuthorizeGet as doGet,
doPut,
doPost,
doDelete,
poller,
} from 'wechat_common/utils/request'
import { ADMINTOOL_API_URL } from 'root/constants/admintool/urlConstants'
const customHeader = {
headerName: 'Authorization',
}
function getVerifyCode(phone) {
return doPost({
url: ADMINTOOL_API_URL.GET_VERIFY_CODE(),
data: {
phone,
},
})
}
function authorize(phone, verifyCode) {
return doPost({
url: ADMINTOOL_API_URL.LOGIN(),
data: {
phone,
verify_code: verifyCode,
},
})
}
function fetchWmpList() {
return doGet({
url: ADMINTOOL_API_URL.WMPLIST(),
customHeader,
})
}
function fetchUnreadCount() {
return doGet({
url: ADMINTOOL_API_URL.GET_UNREAD_COUNT(),
customHeader,
})
}
function fetchSummaryInfo(appId, storefrontId) {
return doGet({
url: ADMINTOOL_API_URL.SUMMARY_INFO(appId, storefrontId),
customHeader,
})
}
function fetchAnalytics(options) {
return doGet({
url: options,
customHeader,
})
}
function fetchMemberList(appId, orderBy, page) {
return doGet({
url: ADMINTOOL_API_URL.MEMBERLIST(appId, { order_by: orderBy, page }),
customHeader,
})
}
function fetchMemberDetail(appId, memberId) {
return doGet({
url: ADMINTOOL_API_URL.MEMBERLIST(appId, memberId),
customHeader,
})
}
function fetchMiniprogramPaymentOrders(options) {
return doGet({
url: options,
customHeader,
})
}
function fetchOrderDetail(appType, appId, orderNum, orderType) {
const url = ADMINTOOL_API_URL.ORDERDETAIL(appType, appId, orderType, orderNum)
return doGet({
url,
customHeader,
})
}
function updateOrderDetail(appType, appId, orderNum, orderType, remark, status) {
return doPut({
url: ADMINTOOL_API_URL.ORDERDETAIL(appType, appId, orderType, orderNum),
data: {
remark,
status,
},
customHeader,
})
}
function setOrderStatus(appId, orderNum, status, shipping_notes) {
if(status === 'shipped'){
return doPost({
url: ADMINTOOL_API_URL.ORDERDSTATUS(appId, orderNum, status),
data: {
status,
deliveryMemberPhone: shipping_notes,
},
customHeader,
})
}else{
return doPut({
url: ADMINTOOL_API_URL.ORDERDSTATUS(appId, orderNum, status),
data: {status},
customHeader,
})
}
}
function fetchVisitorData(options) {
return new Promise((resolve, reject) => {
poller(options, resolve, reject, customHeader)
})
}
function fetchComments(appId, status, page, storefrontId) {
let options = {}
if (storefrontId) {
options = {
page,
storefrontId,
}
} else {
options = {
page,
}
}
return doGet({
url: ADMINTOOL_API_URL.COMMENTLIST(appId, status, options),
customHeader,
})
}
function approveComment(appId, commentId) {
return doPut({
url: ADMINTOOL_API_URL.APPROVECOMMENT(appId, commentId),
data: {
id: commentId,
},
customHeader,
})
}
function deleteComment(appId, commentId) {
return doDelete({
url: ADMINTOOL_API_URL.DELETECOMMENT(appId, commentId),
data: {
id: commentId,
},
customHeader,
})
}
function replyComment(appId, commentId, content, replyStatus) {
return doPost({
url: ADMINTOOL_API_URL.REPLYCOMMENT(appId),
data: {
id: commentId,
content,
status: replyStatus ? 4 : 3,
},
customHeader,
})
}
function approveAllComments(appId, storefrontId) {
return doPost({
url: ADMINTOOL_API_URL.APPROVEALL(appId, storefrontId),
data: storefrontId
? {
storefrontId,
}
: null,
customHeader,
})
}
function markMemberAsRead(appId, memberId) {
return doPost({
url: ADMINTOOL_API_URL.MARK_MEMBER_AS_READ(appId, memberId),
data: {
id: memberId,
},
customHeader,
})
}
function markOrderAsRead(appId, orderNumber) {
return doPost({
url: ADMINTOOL_API_URL.MARK_ORDER_AS_READ(appId, orderNumber),
data: {
orderNumber,
},
customHeader,
})
}
function markCommentAsRead(appId, commentId) {
return doPost({
url: ADMINTOOL_API_URL.MARK_COMMENT_AS_READ(appId, commentId),
data: {
id: commentId,
},
customHeader,
})
}
function createTicket(appId, content, attachments) {
return doPost({
url: ADMINTOOL_API_URL.CREATE_TICKET(appId),
data: {
id: appId,
content,
attachments,
},
customHeader,
})
}
function upload(options) {
wx.uploadFile({
url: options.url,
filePath: options.filePath,
name: 'file',
formData: {
token: options.token,
},
success: res => {
if (options.success) {
options.success(res)
}
},
})
}
function getUploadInfo(appId) {
return doPost({
url: ADMINTOOL_API_URL.GET_UPLOAD_INFO(appId),
})
}
function fetchOrders({appType, appId, orderType, storefrontId, nextPage, status}) {
let options = { page: nextPage }
if (storefrontId) options.storefrontId = storefrontId
if (status) options.status = status
if (appType === 'restaurant'){
options.type = orderType === 'dinein' ? 'dine_in' : orderType
}
//options.per = 8
return doGet({
url: ADMINTOOL_API_URL.ORDERLIST(appType, appId, orderType, options),
customHeader,
}).then(
res => {
switch(appType) {
case 'restaurant':
res.data.total = res.data.paginationMeta.totalCount || 0
res.data.unread = res.data.unreadCount || 0
break;
case 'showcase':
switch (orderType) {
case 'sales':
res.data.orders = res.data.sales
res.data.total = res.data.totalSalesOrders || 0
res.data.unread = res.data.salesOrdersUnread || 0
break;
case 'recharge':
res.data.orders = res.data.recharges
res.data.total = res.data.totalRechargeOrders || 0
res.data.unread = res.data.rechargeOrdersUnread || 0
break;
case 'groupon':
res.data.orders = res.data.grouponOrders
res.data.total = res.data.paginationMeta.totalCount || 0
res.data.unread = res.data.grouponOrdersUnread || 0
break;
default:
break;
}
break;
default:
break;
}
return res
}
)
}
function fetchDocuments() {
return doGet({
url: ADMINTOOL_API_URL.DOCUMENTLIST(),
customHeader,
})
}
function fetchDocumentUnreadCount() {
return doGet({
url: ADMINTOOL_API_URL.DOCUMENT_UNREAD_COUNT(),
customHeader,
})
}
function fetchDocumentContent(id) {
return doGet({
url: ADMINTOOL_API_URL.DOCUMENT_CONTENT(id),
customHeader,
})
}
const admintoolApi = {
fetchWmpList,
fetchSummaryInfo,
getVerifyCode,
authorize,
fetchComments,
approveComment,
fetchUnreadCount,
fetchAnalytics,
fetchMiniprogramPaymentOrders,
fetchVisitorData,
fetchMemberList,
fetchMemberDetail,
fetchOrderDetail,
deleteComment,
approveAllComments,
replyComment,
markMemberAsRead,
markOrderAsRead,
markCommentAsRead,
createTicket,
upload,
getUploadInfo,
fetchOrders,
updateOrderDetail,
fetchDocuments,
fetchDocumentUnreadCount,
fetchDocumentContent,
setOrderStatus,
}
export default admintoolApi
import { doGet, doPut } from 'wechat_common/utils/request'
import UrlConstants from 'root/constants/ecommerce/urlConstants'
export function getAdvertisePopup(siteId, code) {
const url = UrlConstants.ECOMMERCE.GET_ADVERTISE_POPUP(siteId, code)
return doGet({ url })
}
export function hideAdvertisePopup(siteId, code) {
const url = UrlConstants.ECOMMERCE.UPDATE_ADVERTISE_POPUP(siteId)
return doPut({
url,
data: { code },
})
}
import { doGet } from 'wechat_common/utils/request'
import { FETCH_ANNOUNCEMENTS } from 'root/constants/common/urlConstants'
export function fetchAnnouncements(siteId) {
return doGet({
url: FETCH_ANNOUNCEMENTS(siteId),
})
}
import { doGet, doPost, doPut, doDelete } from 'wechat_common/utils/request'
import { bindSiteId, bindCode } from 'wechat_common/utils/context'
import Urls from 'root/constants/blog/urls'
import compose from 'ramda/src/compose'
import {
bindRetry,
bindShowToast,
} from 'wechat_common/networkErrorManager'
const getPostDetail = compose(bindCode, bindSiteId)(
(siteId, code, { params: { postId }, ...options }) =>
doGet(
bindRetry({
url: Urls.REQUESTS.GET_POST(code, postId),
...options,
}),
),
)
const getPosts = compose(bindCode, bindSiteId)(
(siteId, code, { params: { category, pageNum }, ...options }) =>
doGet(
bindRetry({
url: Urls.REQUESTS.GET_POSTS(siteId, code, category, pageNum),
...options,
}),
),
)
const likePost = compose(bindCode, bindSiteId)(
(
siteId,
code,
{ params: { status, postId, nickname, wechat_photo }, ...options },
) => {
const params = {
url: status
? Urls.REQUESTS.SET_LIKE_POST(postId)
: Urls.REQUESTS.DELETE_LIKE_POST(postId),
data: { code, nickname, wechat_photo },
...options,
}
if (status) {
doPost(bindShowToast(params))
} else {
doDelete(bindShowToast(params))
}
},
)
export default {
likePost,
getPosts,
getPostDetail,
}
export function upload(options) {
wx.uploadFile({
url: options.url,
filePath: options.filePath,
name: 'file',
formData: {
token: options.token,
},
success: res => {
if (options.success) {
options.success(res)
}
},
})
}
import { doGet, doPost, doPut, poller } from 'wechat_common/utils/request'
import { bindSiteId, bindCode } from 'wechat_common/utils/context'
import UrlConstants from 'root/constants/ecommerce/urlConstants'
import compose from 'ramda/src/compose'
import { bindRefresh, bindRetry, bindShowToast } from 'wechat_common/networkErrorManager'
export function getCategories(options) {
return doGet(bindRetry(options))
}
export function getProducts(url) {
return doGet(bindRefresh({ url }))
}
export const getFlashSaleProducts = bindSiteId((siteId, status, pageNum) => {
const url = UrlConstants.ECOMMERCE.GET_FLASH_SALE_PRODUCTS(
siteId,
status,
pageNum,
)
return doGet(bindRetry({ url }))
})
export const getGroupBuyProducts = bindSiteId(
(siteId, status, pageNum, code) => {
const url = UrlConstants.ECOMMERCE.GET_GROUP_BUY_PRODUCTS(
siteId,
status,
pageNum,
code,
)
return doGet(bindRetry({ url }))
},
)
export function getGroupBuyDetail(options) {
return doGet(bindRetry(options))
}
export function getProduct(options) {
return doGet(bindRetry(options))
}
export function getShare(options) {
return doPost(bindShowToast(options))
}
export function getSettings(options) {
return doGet(bindRetry(options))
}
export function convertCoupon(options) {
return doPost(bindShowToast(options))
}
export function getOptimal(options) {
return doPost(bindShowToast(options))
}
export const getCoupons = compose(bindCode, bindSiteId)((siteId, code) =>
doGet(bindRetry({ url: UrlConstants.ECOMMERCE.GET_COUPON_LIST_BY_SITE(siteId, code) })),
)
export const getUserCoupons = compose(bindCode, bindSiteId)(
(siteId, code, status, page, size) =>
doGet(bindRetry({
url: UrlConstants.ECOMMERCE.GET_COUPON_LIST_BY_USER(
siteId,
code,
status,
page,
size,
),
})),
)
export function search(options) {
return doPost(bindRetry(options))
}
export const getMembershipCard = compose(bindCode, bindSiteId)((siteId, code) =>
doGet(bindRetry({ url: UrlConstants.ECOMMERCE.GET_MEMBERSHIP_CARD(siteId, code) })),
)
export function getSignature(url) {
return doGet(bindRetry({ url }))
}
export function getAffiliateSettings(url) {
return doGet(bindRetry({ url }))
}
export function getAffiliateMpCode(url) {
return doGet(bindRetry({ url }))
}
export function fetchCommissionDetail(url) {
return doGet(bindRetry({ url }))
}
export function fetchCommissionHistory(url) {
return doGet(bindRetry({ url }))
}
export function fetchCommissionOrder(url) {
return doGet(bindRetry({ url }))
}
export function applyAffiliate(options) {
return doPost(bindShowToast(options))
}
export function updateAffiliate(options) {
return doPut(bindShowToast(options))
}
export function createWithdrawal(options) {
return doPost(bindShowToast(options))
}
export function createOrder(options) {
doPost(bindShowToast({
url: options.url,
data: options.data,
success(res) {
if (res.status === 200) {
const pollUrl = `/r/v1/tasks/${res.data.type}/${res.data.id}.jsm?v=2`
poller(pollUrl, options.success, options.fail)
} else if (options.success) {
options.success(res)
}
},
fail() {
if (options.fail) {
options.fail()
}
},
}))
}
export function createGroupBuyOrder(options) {
doPost(bindShowToast({
url: options.url,
data: options.data,
success(res) {
if (res.status === 200) {
const pollUrl = `/r/v1/tasks/${res.data.type}/${res.data.id}.jsm?v=2`
poller(pollUrl, options.success, options.fail)
} else if (options.success) {
options.success(res)
}
},
fail(err) {
if (options.fail) {
options.fail(err)
}
},
}))
}
export function getOrders(options) {
doGet(bindRetry(options))
}
export function updateOrder(options) {
doPut(bindShowToast(options))
}
export function getSliders(options) {
doGet(bindRetry(options))
}
export function receviceCoupons(options) {
doPost(bindShowToast(options))
}
const getOrderById = compose(bindCode, bindSiteId)(
(siteId, code, { params: { orderId }, ...options }) => {
doGet(bindRetry({
url: UrlConstants.ECOMMERCE.GET_ORDERS_BY_ID({ siteId, code, orderId }),
...options,
}))
},
)
export function upload(options) {
wx.uploadFile({
url: options.url,
filePath: options.filePath,
name: 'file',
formData: {
token: options.token,
},
success: res => {
if (options.success) {
options.success(res)
}
},
})
}
export function middleWareFunction(options) {
return doPost(bindShowToast(options))
}
export function fetchComments(options) {
return doGet(bindRetry(options))
}
export function fetchPointsRule(url) {
return doGet(bindRetry({ url }))
}
export const fetchPointRecords = compose(bindCode, bindSiteId)(
(siteId, code, page) =>
doGet(bindRetry({
url: UrlConstants.ECOMMERCE.FETCH_POINT_RECORDS(siteId, code, page),
})),
)
export const fetchNewCouponList = compose(bindCode, bindSiteId)(
(siteId, code) =>
new Promise((resolve, reject) => {
poller(
UrlConstants.ECOMMERCE.FETCH_NEW_COUPON_LIST(siteId, code),
resolve,
reject,
)
}),
)
const cancelOrder = compose(bindCode, bindSiteId)(
(siteId, code, { params: { orderId }, ...options }) =>
doPut(bindShowToast({
url: UrlConstants.ECOMMERCE.CANCEL_ORDER(siteId, orderId, code),
...options,
})),
)
const getUserInfo = compose(bindCode, bindSiteId)((siteId, code, options) =>
doGet(bindRetry({
url: UrlConstants.ECOMMERCE.GET_USER_INFO(siteId, code),
...options,
})),
)
export const fetchRechargeSetting = bindSiteId(siteId =>
doGet(bindRetry({
url: UrlConstants.ECOMMERCE.FETCH_RECHARGE_SETTING(siteId),
})),
)
export const getStoreValueCardInfo = compose(bindCode, bindSiteId)(
(siteId, code) =>
doGet(bindRetry({
url: UrlConstants.ECOMMERCE.GET_STORE_VALUE_CARD_INFO(siteId, code),
})),
)
export const recharge = compose(bindCode, bindSiteId)(
(siteId, code, rechargeRuleId) =>
doPost(bindShowToast({
url: UrlConstants.ECOMMERCE.RECHARGE(siteId),
data: {
code,
rechargeRuleId,
},
})).then(res => {
if (res.status === 200) {
const pollUrl = `/r/v1/tasks/${res.data.type}/${res.data.id}.jsm?v=2`
return new Promise(resolve => {
poller(pollUrl, response => {
resolve(response)
})
})
} else {
return Promise.reject(res)
}
}),
)
export const fetchRechargeRecords = compose(bindCode, bindSiteId)(
(siteId, code, page) =>
doGet(bindRetry({
url: UrlConstants.ECOMMERCE.FETCH_RECHARGE_RECORDS(siteId, code, page),
})),
)
export default {
getCategories,
getProducts,
getProduct,
getShare,
getSettings,
convertCoupon,
search,
getMembershipCard,
getSignature,
createOrder,
createGroupBuyOrder,
getOrders,
updateOrder,
getSliders,
getFlashSaleProducts,
getGroupBuyProducts,
getGroupBuyDetail,
getCoupons,
getOptimal,
receviceCoupons,
getOrderById,
upload,
middleWareFunction,
fetchComments,
fetchPointsRule,
fetchPointRecords,
fetchNewCouponList,
cancelOrder,
getUserInfo,
getStoreValueCardInfo,
fetchRechargeSetting,
recharge,
fetchRechargeRecords,
}
import { doGet, doPost, doPut, poller } from 'wechat_common/utils/request'
import URLS from 'root/constants/giftcard/urls'
export function getProducts(url) {
return doGet({ url })
}
export function getProduct(url) {
return doGet({ url })
}
export function getCards(url) {
return doGet({ url })
}
export function createOrder(options) {
return new Promise((resolve, reject) => {
doPost({
url: options.url,
data: options.data,
success(res) {
if (res.status === 200) {
const { type, id } = res.data
const pollUrl = URLS.POLL_ORDER(type, id)
poller(pollUrl, resolve, reject)
} else {
resolve(res)
}
},
fail() {
reject()
},
})
})
}
export function getPackets(url) {
return doGet({ url })
}
export function getPacket(url) {
return doGet({ url })
}
export function updatePacket(options) {
return doPut(options)
}
export function getSettings(url) {
return doGet({ url })
}
import { doGet, doPost, doPut } from 'wechat_common/utils/request'
import { PRESENTATION_API } from 'root/constants/presentation/urlConstants'
import compose from 'ramda/src/compose'
import { bindSiteId, bindCode } from 'wechat_common/utils/context'
import { bindRetry, bindShowToast } from 'wechat_common/networkErrorManager'
function fetchSettings(options) {
return doGet(bindRetry(options))
}
function fetchPortfolioSettings(options) {
return doGet(bindRetry(options))
}
function getProducts(url) {
return doGet(bindRetry({ url }))
}
function getCategories(options) {
return doGet(bindRetry(options))
}
function getProduct(options) {
return doGet(bindRetry(options))
}
function getCode(options) {
return doPost(bindRetry(options))
}
function verify(options) {
return doPost(bindRetry(options))
}
function registerTeamMember(options) {
return doPost(bindRetry(options))
}
export const fetchGroups = compose(bindSiteId, bindCode)((code, siteId) =>
doGet({
url: PRESENTATION_API.FETCH_GROUP_LIST(siteId, code),
}),
)
export const fetchGroupSummaryDataById = compose(bindSiteId, bindCode)(
(code, siteId, teamId) =>
doGet({
url: PRESENTATION_API.FETCH_GROUP_SUMMARY_DATA_BY_DI(
siteId,
code,
teamId,
),
}),
)
function createFeedback(options) {
return doPost(options)
}
export const fetchGroupMembers = compose(bindSiteId, bindCode)(
(code, siteId, groupId, page, rankingType) =>
doGet({
url: PRESENTATION_API.FETCH_GROUP_MEMBERS(
siteId,
code,
groupId,
page,
rankingType,
),
}),
)
export const bindCustomerRelationship = compose(bindSiteId, bindCode)(
(code, siteId, teamMemberId) =>
doPost(
bindRetry({
url: PRESENTATION_API.BIND_CUSTOMER_RELATIONSHIP(siteId),
data: {
code,
teamMemberId,
},
}),
),
)
export const getTeamMemberInfoById = compose(bindSiteId, bindCode)(
(code, siteId, teamMemberId) =>
doGet(
bindRetry({
url: PRESENTATION_API.FETCH_TEAM_MEMBER_CARD_INFO(
siteId,
code,
teamMemberId,
),
}),
),
)
export const fetchTeamMemberCards = compose(bindSiteId, bindCode)(
(code, siteId) =>
doGet(
bindRetry({
url: PRESENTATION_API.FETCH_TEAM_MEMBER_CARDS(siteId, code),
}),
),
)
export const updateCustomerRelationship = compose(bindSiteId, bindCode)(
(code, siteId, currentTeamMemberId, targetTeamMemberId) =>
doPost(
bindShowToast({
url: PRESENTATION_API.UPDATE_CUSTOMER_RELATIONSHIP(siteId),
data: {
memberId: currentTeamMemberId,
targetTeamMemberId,
code,
},
}),
),
)
export const fetchRadarUnreadCount = compose(bindSiteId, bindCode)(
(code, siteId) =>
doGet(
bindRetry({
url: PRESENTATION_API.FETCH_RADAR_UNREAD_COUNT(siteId, code),
}),
),
)
export const fetchGroupChartAnalytics = compose(bindSiteId, bindCode)(
(code, siteId, teamId, startDate, endDate) =>
doGet({
url: PRESENTATION_API.FETCH_GROUP_CHART_ANALYTICS(
siteId,
code,
teamId,
startDate,
endDate,
),
}),
)
const fetchGroupAnalyticsReports = compose(bindSiteId, bindCode)(
(code, siteId, teamId, startDate, endDate) =>
doGet({
url: PRESENTATION_API.FETCH_GROUP_ANALYTICS_REPORTS(
siteId,
code,
teamId,
startDate,
endDate,
),
}),
)
const fetchMemberList = compose(bindSiteId, bindCode)(
(code, siteId, value, memberId, groupId, needFakeData) =>
doGet(
bindRetry({
url: PRESENTATION_API.FETCH_MEMBER_LIST(
code,
siteId,
value,
memberId,
groupId,
needFakeData,
),
}),
),
)
const fetchVisitorDetail = compose(bindCode, bindSiteId)(
(siteId, code, memberId, startDate, endDate, teamMemberId) =>
doGet(
bindRetry({
url: PRESENTATION_API.FETCH_VISITOR_DETAIL(
siteId,
code,
memberId,
startDate,
endDate,
teamMemberId,
),
}),
),
)
const fetchVisitorInfo = compose(bindSiteId)((siteId, memberId) =>
doGet(
bindRetry({
url: PRESENTATION_API.FETCH_VISITOR_INFO(siteId, memberId),
}),
),
)
const fetchVisitorAnalytics = compose(bindSiteId)((siteId, memberId, options) =>
doGet({
url: PRESENTATION_API.FETCH_VISITOR_ANALYTICS(siteId, memberId, options),
}),
)
const fetchVisitorGeneralReport = compose(bindSiteId)(
(siteId, memberId, options) =>
doGet({
url: PRESENTATION_API.FETCH_VISITOR_GENERAL_REPORT(
siteId,
memberId,
options,
),
}),
)
const updateVisitorInfo = compose(bindSiteId)(
(siteId, code, memberId, contactInfo) =>
doPut(
bindShowToast({
url: PRESENTATION_API.UPDATE_VISITOR_INFO(siteId, memberId),
data: {
code,
contactInfo,
},
}),
),
)
const fetchContactForms = compose(bindSiteId, bindCode)((code, siteId) =>
doGet(
bindRetry({
url: PRESENTATION_API.FETCH_CONTACT_FORMS(siteId, code),
}),
),
)
const updateContactFormsReadMark = compose(bindSiteId, bindCode)(
(code, siteId, messageId) =>
doPost({
url: PRESENTATION_API.UPDATE_CONTACT_FORMS_READ_MARK(siteId),
data: {
code,
contact_form_ids: [messageId],
},
}),
)
function createContactForm(options) {
return doPost(options)
}
function fetchFormSettings(options) {
return doGet(bindRetry(options))
}
function getShare(options) {
return doPost(bindRetry(options))
}
const updateMemberCardInfo = compose(bindSiteId, bindCode)(
(code, siteId, value, memberId) =>
doPut(
bindShowToast({
url: PRESENTATION_API.UPDATE_MEMBER_CARD_INFO(siteId, memberId),
data: Object.assign({}, {code}, value),
}),
),
)
const updateTeamMember = compose(bindCode, bindSiteId)(
(siteId, code, memberId, data) =>
doPut(
bindShowToast({
url: PRESENTATION_API.UPDATE_TEAM_MEMBER(siteId, memberId, code),
data,
}),
),
)
const updateMemberLikes = compose(bindSiteId, bindCode)(
(code, siteId, likeState, teamMemberId) =>
doPut(
bindShowToast({
url: PRESENTATION_API.UPDATE_MEMBER_LIKES(siteId),
data: {
code,
likeState,
teamMemberId,
},
}),
),
)
const getChannelList = compose(bindCode, bindSiteId)(
(siteId, code, memberId, groupId) => {
const url = PRESENTATION_API.GET_CHANNEL_LIST(
siteId,
code,
memberId,
groupId,
)
return doGet(
bindRetry({
url,
}),
)
},
)
const getChannelDetail = compose(bindCode, bindSiteId)(
(siteId, code, channelId, memberId, groupId, serviceNotification) => {
const url = PRESENTATION_API.GET_CHANNEL_DETAIL(
siteId,
code,
channelId,
memberId,
groupId,
serviceNotification,
)
return doGet({
url,
})
},
)
const getChannelUnreadCount = compose(bindCode, bindSiteId)(
(siteId, code, channelId) => {
const url = PRESENTATION_API.GET_CHANNEL_UNREAD(siteId, code, channelId)
return doGet({
url,
})
},
)
const postChannelDetail = compose(bindCode, bindSiteId)(
(siteId, code, channelId, data) => {
const url = PRESENTATION_API.POST_CHANNEL_DETAIL(siteId, code)
data.channelId = channelId
return doPost({
url,
data,
})
},
)
const getBroadcasts = compose(bindCode, bindSiteId)((siteId, code) => {
const url = PRESENTATION_API.GET_BROADCASTS(siteId, code)
return doGet(
bindRetry({
url,
}),
)
})
const getSystemNotifications = compose(bindSiteId)(
(siteId, teamMemberId, startDate, endDate) => {
const url = PRESENTATION_API.GET_SYSTEM_NOTIFICATIONS(
siteId,
teamMemberId,
startDate,
endDate,
)
return doGet(
bindRetry({
url,
}),
)
},
)
const createBroadcast = compose(bindCode, bindSiteId)((siteId, code, data) => {
const url = PRESENTATION_API.CREATE_BROADCAST(siteId, code)
return doPost({
url,
data,
})
})
const getChannelListUnread = compose(bindCode, bindSiteId)((siteId, code) => {
const url = PRESENTATION_API.GET_CHANNEL_LIST_UNREAD(siteId, code)
return doGet(
bindRetry({
url,
}),
)
})
const fetchTrackEvents = compose(bindCode, bindSiteId)(
(siteId, code, startDate, endDate) => {
const url = PRESENTATION_API.FETCH_TRACK_EVENTS(
siteId,
code,
startDate,
endDate,
)
return doGet(
bindRetry({
url,
}),
)
},
)
const fetchProductDetail = compose(bindSiteId)((siteId, productId) => {
const url = PRESENTATION_API.GET_PRODUCT_DETAIL(siteId, productId)
return doGet({
url,
})
})
const fetchAnalyticsReport = compose(bindCode, bindSiteId)(
(siteId, code, teamMemberId, startDate, endDate, interval) => {
const url = PRESENTATION_API.FETCH_ANALYTICS_REPORTS(
siteId,
code,
teamMemberId,
startDate,
endDate,
interval,
)
return doGet({
url,
})
},
)
const getServiceMessageInfo = compose(bindSiteId)(
(siteId, clientId, teamMemberId) => {
const url = PRESENTATION_API.GET_SERVICE_MESSAGE_INFO(
siteId,
clientId,
teamMemberId,
)
return doGet({
url,
})
},
)
const fetchProductShare = compose(bindSiteId)(
(siteId, productId, page, affiliateOpenId) => {
const url = PRESENTATION_API.GET_PRODUCT_SHARE(
siteId,
productId,
page,
affiliateOpenId,
)
return doPost({
url,
})
},
)
const fetchAppList = code => {
const url = PRESENTATION_API.GET_APP_LIST(code)
return doGet(bindRetry({ url }))
}
const postFormId = compose(bindCode, bindSiteId)((siteId, code, formId) => {
const url = PRESENTATION_API.POST_VALID_FORM_ID(siteId)
const data = {
code,
formId,
}
return doPost({
url,
data,
})
})
const sendServiceMessage = compose(bindCode, bindSiteId)(
(siteId, code, clientid, type) => {
const url = PRESENTATION_API.POST_SEND_SERVICE(siteId)
const data = {
code,
stCoreClientId: Number(clientid),
notificationType: type,
}
return doPost({
url,
data,
})
},
)
const putServiceCount = compose(bindSiteId)((siteId, memberId) => {
const url = PRESENTATION_API.PUT_SERVICE_NOTIFICATION_COUNT(siteId)
const data = {
member_id: memberId,
}
return doPut({
url,
data,
})
})
const getServiceNotifyPopup = compose(bindCode, bindSiteId)((siteId, code) => {
const url = PRESENTATION_API.FETCH_SERVICE_POPUP_STATUS(siteId, code)
return doGet({
url,
})
})
const presentationApi = {
fetchAnalyticsReport,
fetchAppList,
fetchSettings,
getProducts,
fetchPortfolioSettings,
getCategories,
getProduct,
getCode,
verify,
registerTeamMember,
fetchGroupMembers,
fetchGroups,
createFeedback,
createContactForm,
fetchFormSettings,
fetchMemberList,
fetchVisitorDetail,
updateVisitorInfo,
fetchVisitorInfo,
fetchVisitorAnalytics,
fetchVisitorGeneralReport,
fetchContactForms,
updateContactFormsReadMark,
updateMemberCardInfo,
updateTeamMember,
updateMemberLikes,
getChannelList,
getChannelDetail,
getChannelUnreadCount,
postChannelDetail,
getChannelListUnread,
getBroadcasts,
createBroadcast,
fetchTrackEvents,
fetchProductDetail,
fetchProductShare,
getShare,
getSystemNotifications,
fetchRadarUnreadCount,
fetchGroupChartAnalytics,
fetchGroupAnalyticsReports,
fetchGroupSummaryDataById,
postFormId,
getServiceMessageInfo,
sendServiceMessage,
putServiceCount,
getServiceNotifyPopup,
}
export default presentationApi
import EmojiObj from './emojimap'
const emoji = EmojiObj.emojiList.emoji
const prefix = EmojiObj.emojiSxlUrl
/* eslint-disable max-statements */
function generateRichTextNode(text) {
let tempStr = text
const richTextNode = []
let leftBracketIndex = tempStr.indexOf('[')
let rightBracketIndex = 0
// 没有emoji
if (leftBracketIndex === -1) {
richTextNode.push({
type: 'text',
text: tempStr,
})
return richTextNode
}
while (tempStr.length !== 0) {
// 最前面是文本
if (leftBracketIndex !== 0) {
// 最后全是文字
if (leftBracketIndex === -1) {
richTextNode.push({
type: 'text',
text: tempStr.slice(0, tempStr.length),
})
tempStr = ''
} else {
richTextNode.push({
type: 'text',
text: tempStr.slice(0, leftBracketIndex),
})
tempStr = tempStr.substring(leftBracketIndex, tempStr.length + 1)
}
} else {
// 前面是[
rightBracketIndex = tempStr.indexOf(']')
const emojiName = tempStr.slice(0, rightBracketIndex + 1)
if (emoji[emojiName]) {
richTextNode.push({
name: 'img',
attrs: {
width: '20rpx',
height: '20rpx',
src: emoji[emojiName].img,
},
})
} else {
richTextNode.push({
type: 'text',
text: `${emojiName}`,
})
}
tempStr = tempStr.substring(rightBracketIndex + 1, tempStr.length)
}
leftBracketIndex = tempStr.indexOf('[')
if (tempStr.indexOf(']') === -1) {
leftBracketIndex = -1
}
}
return richTextNode
}
/* eslint-enable max-statements */
function generateImageNode(fileStr) {
let file = {}
try {
file = JSON.parse(fileStr)
} catch (e) {
console.info('JSON parse error')
}
let width = 0
let height = 0
if (file.w > 150) {
width = 100
height = file.h / (file.w / 100)
} else {
width = file.w
height = file.h
}
const richTextNode = []
richTextNode.push({
name: 'img',
attrs: {
width: `${width}rpx`,
height: `${height}rpx`,
src: file.url,
},
})
return richTextNode
}
function generateBigEmojiImageFile(messageStr) {
let content = {}
try {
content = JSON.parse(messageStr)
} catch (e) {
console.info('JSON parse error')
}
const file = { w: content.w, h: content.h, url: '' }
file.url = `${prefix}/${content.catalog}/${content.name}`
return JSON.stringify(file)
}
export const convertChatItem = item => {
if (item.messageType === 'welcome_message') {
return {
...item,
customMessage: item.customMessageChanged
? item.customMessageChanged
: JSON.parse(item.customMessage),
messageType: 'welcome_message',
}
} else if (item.messageType === 'service_notification') {
return {
...item,
messageType: 'service_notification',
}
}
if (item.message) {
return {
...item,
messageType: 'text',
nodes: generateRichTextNode(item.message),
}
} else if (item.messageType === 'image') {
return {
...item,
messageType: 'image',
nodes: generateImageNode(item.customMessage),
}
} else if (item.messageType === 'big_emoji') {
return {
...item,
messageType: 'image',
nodes: generateImageNode(generateBigEmojiImageFile(item.customMessage)),
}
} else {
return {
...item,
messageType: 'text',
}
}
}
const emojiSxlUrl = `https://user-assets.sxlcdn.com/livechat/emoji`
const iconEmojiDelete =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAMAAABHPGVmAAAAk1BMVEUAAADAwMC3t7e/v7/Gxsa/v7+/v7/AwMC/v7+/v7+/v7++vr7CwsK/v7+/v7+/v7++vr6/v7+/v7+/v7+/v7+/v7/AwMCbm5u/v7+/v7+/v7+/v7/AwMC/v7+/v7+/v7+/v7+/v7+/v7++vr7AwMC9vb2/v7+/v7/CwsLAwMDHx8fExMTIyMjLy8vJycnKysrPz8+3r5/kAAAAKHRSTlMA8Q39A/vcIwf34B0P1uNJIuzPjHFqFwVFmls4J761rMiFoVJ6XTNYKpTiKQAAAylJREFUeF7tmEl3ozAQhNt0ywYM3rfETuJMltEC2P//1w3IVg555olmHjfVlcMnAVUtFfgVFBQUFBQUFBREXAAR8BVnEUNZzF8YwTYdsZQ/Pcc8CsGmNEpyZMoqn/MYzxoFomIIhTDFujtjD3OlEFEyVENqzHUN1HUfx0QiKjliKJEVIorioxuF4LQ0AkW53mddf7Asmh5eNaLQeUfGLtUN4w8wtbaULlshiFZVw3gF7g8Jr4UQ5ZMfQkAvl5pRnWO+teYGhT53YMBbWTP0OAMCrqaJQjmLwKunhmGWkx4MiEZSyZEPQvDeMGRy7MOAbCTRCyHYFqqx+Rxi6KHIC7FhYrCW/gKCQSAuTBRWGwaDCSE4CmmNDtRhJhFxIS5M2o1uweQZmw7iC5N2o58O9EMhyDabjAdxYdJudIINytXiTiGY5tfrKuJBXJjo8a6FsUiMuIynQDfGuEqScgvEgLgwaTU6wVEJFEVDsYxCoOBBXJjI5LvdIOeyoaRTiGGaNowqZ7wuFybYYnT3vlJ928t9H3o2AeoOIdhUHqNbykzb9R/y6jfDDyH40g3jYo3uo6CUEh2jM4TgYE8mv4z+kJIWeFORToDn+FUhEI1/ohNMxtIy5NgyGJBspBDlMvKnYtzMV1SIxo6CYXeiZG69P8g3mbhvIqrGlQyIG1Xi8ukobYzb32UkOu93hrihi6i3QB5G48aD9aKl8B2vlPyAvYehZyc4WZqjMLMLFR5aKdHN5+kC4nvClGcA4Kdwe0ISbMs7g35yTB6B+swTs1wAtUCSpLBZcnt3lTBJTew1GXX+eGrVj69X5/OGkhu5Aeo54y+rGOjh489Py3drmn//z2nlBR5TPKcV5rnr7SEF9nvGuct3HW2uMVzxz8KifAcaDOICRhVboMEg7kiB5m/f+4nyQ34CRtmAGWAnLmD637V2I2Vn7I+8AXPiF0sHiWjyGDrf43W663WPv7wBdW8kqlXUp5Eonn9BfAGTRYxuZX7vVmJGSyQQTcJpiUxLS+QJGKW4fRfavovX3Clmc6f1mtlBXuSQHaSjLAdoU4fvhYdvuIOCgoKCgoL+AXU1fW2kbFGNAAAAAElFTkSuQmCC'
const albumArr = []
const emojiList = {
emoji: {
'[大笑]': { file: 'emoji_0.png' },
'[可爱]': { file: 'emoji_01.png' },
'[色]': { file: 'emoji_02.png' },
'[嘘]': { file: 'emoji_03.png' },
'[亲]': { file: 'emoji_04.png' },
'[呆]': { file: 'emoji_05.png' },
'[口水]': { file: 'emoji_06.png' },
'[汗]': { file: 'emoji_145.png' },
'[呲牙]': { file: 'emoji_07.png' },
'[鬼脸]': { file: 'emoji_08.png' },
'[害羞]': { file: 'emoji_09.png' },
'[偷笑]': { file: 'emoji_10.png' },
'[调皮]': { file: 'emoji_11.png' },
'[可怜]': { file: 'emoji_12.png' },
'[敲]': { file: 'emoji_13.png' },
'[惊讶]': { file: 'emoji_14.png' },
'[流感]': { file: 'emoji_15.png' },
'[委屈]': { file: 'emoji_16.png' },
'[流泪]': { file: 'emoji_17.png' },
'[嚎哭]': { file: 'emoji_18.png' },
'[惊恐]': { file: 'emoji_19.png' },
'[怒]': { file: 'emoji_20.png' },
'[酷]': { file: 'emoji_21.png' },
'[不说]': { file: 'emoji_22.png' },
'[鄙视]': { file: 'emoji_23.png' },
'[阿弥陀佛]': { file: 'emoji_24.png' },
'[奸笑]': { file: 'emoji_25.png' },
'[睡着]': { file: 'emoji_26.png' },
'[口罩]': { file: 'emoji_27.png' },
'[努力]': { file: 'emoji_28.png' },
'[抠鼻孔]': { file: 'emoji_29.png' },
'[疑问]': { file: 'emoji_30.png' },
'[怒骂]': { file: 'emoji_31.png' },
'[晕]': { file: 'emoji_32.png' },
'[呕吐]': { file: 'emoji_33.png' },
'[拜一拜]': { file: 'emoji_160.png' },
'[惊喜]': { file: 'emoji_161.png' },
'[流汗]': { file: 'emoji_162.png' },
'[卖萌]': { file: 'emoji_163.png' },
'[默契眨眼]': { file: 'emoji_164.png' },
'[烧香拜佛]': { file: 'emoji_165.png' },
'[晚安]': { file: 'emoji_166.png' },
'[强]': { file: 'emoji_34.png' },
'[弱]': { file: 'emoji_35.png' },
'[OK]': { file: 'emoji_36.png' },
'[拳头]': { file: 'emoji_37.png' },
'[胜利]': { file: 'emoji_38.png' },
'[鼓掌]': { file: 'emoji_39.png' },
'[握手]': { file: 'emoji_200.png' },
'[发怒]': { file: 'emoji_40.png' },
'[骷髅]': { file: 'emoji_41.png' },
'[便便]': { file: 'emoji_42.png' },
'[火]': { file: 'emoji_43.png' },
'[溜]': { file: 'emoji_44.png' },
'[爱心]': { file: 'emoji_45.png' },
'[心碎]': { file: 'emoji_46.png' },
'[钟情]': { file: 'emoji_47.png' },
'[唇]': { file: 'emoji_48.png' },
'[戒指]': { file: 'emoji_49.png' },
'[钻石]': { file: 'emoji_50.png' },
'[太阳]': { file: 'emoji_51.png' },
'[有时晴]': { file: 'emoji_52.png' },
'[多云]': { file: 'emoji_53.png' },
'[雷]': { file: 'emoji_54.png' },
'[雨]': { file: 'emoji_55.png' },
'[雪花]': { file: 'emoji_56.png' },
'[爱人]': { file: 'emoji_57.png' },
'[帽子]': { file: 'emoji_58.png' },
'[皇冠]': { file: 'emoji_59.png' },
'[篮球]': { file: 'emoji_60.png' },
'[足球]': { file: 'emoji_61.png' },
'[垒球]': { file: 'emoji_62.png' },
'[网球]': { file: 'emoji_63.png' },
'[台球]': { file: 'emoji_64.png' },
'[咖啡]': { file: 'emoji_65.png' },
'[啤酒]': { file: 'emoji_66.png' },
'[干杯]': { file: 'emoji_67.png' },
'[柠檬汁]': { file: 'emoji_68.png' },
'[餐具]': { file: 'emoji_69.png' },
'[汉堡]': { file: 'emoji_70.png' },
'[鸡腿]': { file: 'emoji_71.png' },
'[面条]': { file: 'emoji_72.png' },
'[冰淇淋]': { file: 'emoji_73.png' },
'[沙冰]': { file: 'emoji_74.png' },
'[生日蛋糕]': { file: 'emoji_75.png' },
'[蛋糕]': { file: 'emoji_76.png' },
'[糖果]': { file: 'emoji_77.png' },
'[葡萄]': { file: 'emoji_78.png' },
'[西瓜]': { file: 'emoji_79.png' },
'[光碟]': { file: 'emoji_80.png' },
'[手机]': { file: 'emoji_81.png' },
'[电话]': { file: 'emoji_82.png' },
'[电视]': { file: 'emoji_83.png' },
'[声音开启]': { file: 'emoji_84.png' },
'[声音关闭]': { file: 'emoji_85.png' },
'[铃铛]': { file: 'emoji_86.png' },
'[锁头]': { file: 'emoji_87.png' },
'[放大镜]': { file: 'emoji_88.png' },
'[灯泡]': { file: 'emoji_89.png' },
'[锤头]': { file: 'emoji_90.png' },
'[烟]': { file: 'emoji_91.png' },
'[炸弹]': { file: 'emoji_92.png' },
'[枪]': { file: 'emoji_93.png' },
'[刀]': { file: 'emoji_94.png' },
'[药]': { file: 'emoji_95.png' },
'[打针]': { file: 'emoji_96.png' },
'[钱袋]': { file: 'emoji_97.png' },
'[钞票]': { file: 'emoji_98.png' },
'[银行卡]': { file: 'emoji_99.png' },
'[手柄]': { file: 'emoji_100.png' },
'[麻将]': { file: 'emoji_101.png' },
'[调色板]': { file: 'emoji_102.png' },
'[电影]': { file: 'emoji_103.png' },
'[麦克风]': { file: 'emoji_104.png' },
'[耳机]': { file: 'emoji_105.png' },
'[音乐]': { file: 'emoji_106.png' },
'[吉他]': { file: 'emoji_107.png' },
'[火箭]': { file: 'emoji_108.png' },
'[飞机]': { file: 'emoji_109.png' },
'[火车]': { file: 'emoji_110.png' },
'[公交]': { file: 'emoji_111.png' },
'[轿车]': { file: 'emoji_112.png' },
'[出租车]': { file: 'emoji_113.png' },
'[警车]': { file: 'emoji_114.png' },
'[自行车]': { file: 'emoji_115.png' },
},
}
for (const emoji in emojiList) {
const emojiItem = emojiList[emoji]
for (const key in emojiItem) {
const item = emojiItem[key]
item.img = `${emojiSxlUrl}/${emoji}/${item.file}`
}
}
emojiList.jgz = {}
for (let i = 1; i <= 31; i++) {
const key = `jgz${i >= 10 ? i : `0${i}`}`
emojiList.jgz[key] = { file: `${key}.gif` }
}
// 内容
for (const emoji in emojiList) {
const emojiItem = emojiList[emoji]
for (const key in emojiItem) {
const item = emojiItem[key]
item.img = `${emojiSxlUrl}/${emoji}/${item.file}`
}
// 封面
albumArr.push({
album: emoji,
img: emojiItem[Object.keys(emojiItem)[0]].img,
})
}
// 添加删除按钮
emojiList.emoji['[删除]'] = {}
emojiList.emoji['[删除]'].img = iconEmojiDelete
export default {
emojiList,
albumArr,
emojiSxlUrl,
}
import { dispatch } from 'root/wmp-redux'
import { fetchSettings } from 'root/reducers/ecommerce/settings'
import {
fetchProducts,
fetchFlashSaleProducts,
fetchGroupBuyProducts,
} from 'root/actions/ecommerce/productActions'
import { forceUpdate } from 'root/reducers/ecommerce/global'
import { getMembershipCard } from 'root/actions/ecommerce/membership/cardActions'
export function refreshData(siteId, code, doNotFetchProducts) {
if (!doNotFetchProducts) {
dispatch(fetchProducts(siteId, 'all', 1))
}
dispatch(fetchFlashSaleProducts('all', 1))
code && dispatch(fetchGroupBuyProducts('all', 1, code))
dispatch(fetchSettings(siteId))
dispatch(getMembershipCard())
}
import { PAGES } from 'root/constants/admintool/urlConstants'
export function handleRequestSuccess() {
wx.hideLoading()
}
export function handleRequestError(err) {
wx.hideLoading()
if (err.data.code === 200000) {
wx.showModal({
title: '身份验证失败',
content: '请重新登录',
showCancel: false,
success: () => {
wx.setStorageSync('admin_tool_token', null)
wx.reLaunch({
url: PAGES.LOGIN,
})
},
})
} else if (err.data.code === 200001) {
wx.showModal({
title: '手机号不存在',
content: '请确认您输入的手机号已在员工授权管理中录入',
showCancel: false,
success: () => {
wx.setStorageSync('admin_tool_token', null)
wx.reLaunch({
url: PAGES.LOGIN,
})
},
})
} else if (err.data.code === 200002) {
wx.showModal({
title: '重新登录',
content: '权限发生变更,已被强行登出。请重新登录,或联系管理员。',
showCancel: false,
success: () => {
wx.setStorageSync('admin_tool_token', null)
wx.reLaunch({
url: PAGES.LOGIN,
})
},
})
} else {
wx.showModal({
title: '获取失败',
content: '请重新获取',
showCancel: false,
})
}
}
import {formatProtocol as _formatProtocol} from './tools'
export function formatProtocol(url) {
if (!url) {
return ''
}
return _formatProtocol(url)
}
export function convertPostLikes(post, nickname){
const { isLiked, likes = [] } = post
if(isLiked){
let likedIndex = -1
let isFirst = true
for(let i = 0; i < likes.length; i++){
if(isFirst && likes[i] === nickname){
likedIndex = i
isFirst = false
}
}
post.likes = likes.filter((item, index) => index !== likedIndex)
} else {
(post.likes || []).push(nickname)
}
post.isLiked = !post.isLiked
return post
}
\ No newline at end of file
export const targetMap = {
broadcast: {
className: 'broadcast',
title: '新建群发',
topHint: '您将给所有客户发送该条消息',
field: 'message',
maxLength: -1,
submitText: '发送',
},
welcome: {
className: 'welcome',
title: '设置欢迎语',
placeholder: '请输入欢迎语',
topHint: '当访客进入名片时在聊天窗口显示',
field: 'welcomeMessage',
switchField: 'openHotKey',
maxLength: 200,
},
autoReply: {
className: 'auto-reply',
title: '设置自动回复',
topHint: '当客户发送新消息时,自动回复:',
field: 'autoReply',
maxLength: 200,
},
}
import curry from 'ramda/src/curry'
export function getCacheTime(duration) {
return new Date().getTime() + duration
}
export const commonAnimation = wx.createAnimation({
duration: 400,
timingFunction: 'ease',
})
export const parseScene = scene => {
const params = {}
decodeURIComponent(scene)
.split('&')
.forEach((item, index) => {
const itemArr = item.split('=')
if (itemArr.length === 2) {
params[itemArr[0]] = itemArr[1]
} else {
params[index] = itemArr[0]
}
})
return params
}
// blog ecommerce presentation
export const baseFilterRelationData = curry(
(baseType, data, relationData = {}) => {
const filterData = data.filter(item => {
const type = item.category || baseType
if (item.type === 'blogPost' || item.type === 'product') {
if (
relationData[`${type}ProductIdList`] &&
relationData[`${type}ProductIdList`].find(
value => value.toString() === item.value.toString(),
)
) {
return false
}
} else if (item.type === 'category') {
if (
relationData[`${type}CategoryIdList`] &&
relationData[`${type}CategoryIdList`].find(
value => value.toString() === item.value.id.toString(),
)
) {
return false
}
}
return true
})
return filterData
},
)
// hexToRgba('#fff', 0.1) => rgba(255, 255, 255, 0.1)
export const hexToRgba = function(hex, alpha = 1) {
let c
if (/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)) {
c = hex.substring(1).split('')
if (c.length === 3) {
c = [c[0], c[0], c[1], c[1], c[2], c[2]]
}
c = `0x${c.join('')}`
return `rgba(${(c >> 16) & 255}, ${(c >> 8) & 255}, ${c & 255}, ${alpha})`
}
return c
}
// 1600 => 1.6k
export const KFormatter = num =>
num > 999 ? `${(num / 1000).toFixed(1)}k` : num
import { iconPath } from 'root/constants/presentation/iconPath'
import {
getRoundImage,
replaceWithDownloadFileDomain,
} from 'root/utils/helpers/presentationHelper'
import { showModal } from 'wechat_common/utils/wrappedWXTool'
export const getScoreText = score => {
const map = {
1: '很差',
2: '不太满意',
3: '一般',
4: '比较满意',
5: '非常满意',
}
return map[score]
}
const getImageInfo = url =>
new Promise((resolve, reject) => {
wx.getImageInfo({
src: url,
success: res => {
resolve(res)
},
fail: e => {
reject(e)
},
})
})
const rpxToPx = size => {
const { windowWidth } = wx.getSystemInfoSync()
return windowWidth * size / 750
}
function drawCloseIcon() {
const ctx = wx.createCanvasContext('closeIcon')
ctx.setFillStyle('#ffffff')
ctx.drawImage(
iconPath.ICON_CANVAS_CLOSE,
rpxToPx(710) - rpxToPx(104),
15,
30,
30,
)
ctx.draw()
}
export const drawContactCard = (
teamMember,
logoUrl,
isWhiteBackground,
companyName,
) =>
new Promise((resolve, reject) => {
const ctx = wx.createCanvasContext('contactCard')
const { profile } = teamMember
// bg
ctx.setFillStyle('#ffffff')
ctx.fillRect(0, 0, rpxToPx(710), rpxToPx(950))
const { avatarUrl, roundAvatarUrl } = profile
const qrCodeUrl = teamMember.qrCode
const imageUrlList = [qrCodeUrl, roundAvatarUrl]
const promiseList = imageUrlList.map(url => getImageInfo(url))
Promise.all(promiseList)
.catch(() => {
showModal({
content: '图片加载失败,请稍后重试',
showCancel: false,
})
reject()
})
.then(([verifyQrCodeInfo, roundAvatarInfo]) => {
const verifyQrCodePath = verifyQrCodeInfo.path
const roundAvatarPath = roundAvatarInfo.path
const cardOrigin = {
x: rpxToPx(43),
y: rpxToPx(220),
}
const avatarData = {
w: rpxToPx(200),
h: rpxToPx(200),
r: rpxToPx(100), // 头像半径
}
const drawImage = (imagePath, beginX, beginY, width, height) => {
ctx.drawImage(imagePath, beginX, beginY, width, height)
}
// 画用户头像
drawImage(roundAvatarPath, 20, 30, avatarData.w, avatarData.h)
// 画用户信息
drawProfile()
// 画下方二维码
drawBottomBar()
ctx.draw()
resolve()
function drawProfile() {
const profileOrigin = {
m: rpxToPx(353), // 中点
l: rpxToPx(43), // 左起点
r: rpxToPx(650), // 右终点
y: rpxToPx(300), // 高度起点
}
const iconData = {
w: rpxToPx(36),
h: rpxToPx(36),
}
drawProfileBackground()
ctx.setFontSize(rpxToPx(30))
drawPhone()
if (profile.wechatAccount) {
drawWechatAccount()
}
if (profile.email) {
drawEmail()
}
function drawProfileBackground() {
// name
ctx.setFontSize(rpxToPx(40))
ctx.setFillStyle('#000000')
ctx.fillText(profile.name, 140, 70)
drawPosition()
if (companyName) {
drawCompany()
}
}
function drawPosition() {
// position 职位背景,icon,职位名称
ctx.setFontSize(rpxToPx(30))
const positionMetrics = ctx.measureText(profile.position).width
ctx.setFillStyle(
profile.color !== '#ffffff' ? profile.color : '#F4F4F4',
)
ctx.fillRect(140, 82, positionMetrics + 8, 24)
// position
ctx.setFillStyle(
profile.color === '#ffffff' ? '#000000' : '#ffffff',
)
ctx.fillText(profile.position, 144, 100)
}
function drawCompany() {
// company
ctx.setFillStyle('#000000')
const companyMetrics = ctx.measureText(companyName).width
ctx.fillText(companyName, 22, profileOrigin.y + rpxToPx(22))
// icon
drawImage(
iconPath.ICON_VERIFY,
companyMetrics + 24,
profileOrigin.y - 2,
15,
15,
)
}
function drawPhone() {
ctx.drawImage(
iconPath.ICON_CANVAS_PHONE,
profileOrigin.l,
profileOrigin.y + rpxToPx(158) - rpxToPx(20),
iconData.w,
iconData.h,
)
ctx.setFillStyle('#999999')
ctx.fillText(
'手机',
profileOrigin.l + iconData.w + rpxToPx(10),
profileOrigin.y + rpxToPx(165),
)
const phoneMetrics = ctx.measureText(profile.phone).width
ctx.setFillStyle('#666666')
ctx.fillText(
profile.phone,
profileOrigin.r - phoneMetrics,
profileOrigin.y + rpxToPx(165),
)
ctx.setStrokeStyle('#e8e8e8')
ctx.setLineWidth(0.5)
ctx.moveTo(profileOrigin.l, profileOrigin.y + rpxToPx(190))
ctx.lineTo(profileOrigin.r, profileOrigin.y + rpxToPx(190))
ctx.stroke()
}
function drawWechatAccount() {
ctx.drawImage(
iconPath.ICON_CANVAS_WECHAT,
profileOrigin.l,
profileOrigin.y + rpxToPx(82) - rpxToPx(20),
iconData.w,
iconData.h,
)
ctx.setFillStyle('#999999')
ctx.fillText(
'微信',
profileOrigin.l + iconData.w + rpxToPx(10),
profileOrigin.y + rpxToPx(89),
)
const wechatMetrics = ctx.measureText(profile.wechatAccount).width
ctx.setFillStyle('#666666')
ctx.fillText(
profile.wechatAccount,
profileOrigin.r - wechatMetrics,
profileOrigin.y + rpxToPx(89),
)
ctx.setStrokeStyle('#e8e8e8')
ctx.setLineWidth(0.5)
ctx.moveTo(profileOrigin.l, profileOrigin.y + rpxToPx(116))
ctx.lineTo(profileOrigin.r, profileOrigin.y + rpxToPx(116))
ctx.stroke()
}
function drawEmail() {
ctx.drawImage(
iconPath.ICON_CANVAS_EMAIL,
profileOrigin.l,
profileOrigin.y + rpxToPx(230) - rpxToPx(20),
iconData.w,
iconData.h,
)
ctx.setFillStyle('#999999')
ctx.fillText(
'邮箱',
profileOrigin.l + iconData.w + rpxToPx(10),
profileOrigin.y + rpxToPx(237),
)
const emailMetrics = ctx.measureText(profile.email).width
ctx.setFillStyle('#666666')
ctx.fillText(
profile.email,
profileOrigin.r - emailMetrics,
profileOrigin.y + rpxToPx(237),
)
}
}
function drawBottomBar() {
ctx.setFontSize(rpxToPx(32))
ctx.setFillStyle('#636972')
ctx.fillText('长按识别二维码', rpxToPx(67), rpxToPx(689))
ctx.fillText('进入小程序了解更多', rpxToPx(67), rpxToPx(744))
ctx.drawImage(
verifyQrCodePath,
rpxToPx(416),
rpxToPx(590),
rpxToPx(219),
rpxToPx(219),
)
ctx.drawImage(
roundAvatarPath,
rpxToPx(416) + rpxToPx(64),
rpxToPx(590) + rpxToPx(64),
rpxToPx(92),
rpxToPx(92),
)
}
drawCloseIcon()
})
.catch(() => {
showModal({
content: '图片渲染失败',
showCancel: false,
})
reject()
})
})
import Accounting from 'accounting'
import curry from 'ramda/src/curry'
import moment from 'wechat_common/lib/moment'
const format = {
code: 'CNY',
symbol: '¥',
decimal: '.',
thousand: '',
precision: 2,
name: 'Chinese Yuan',
}
const AMOUNT_UNIT = 100
const PHONE_REGEX = /^[+\d-\(\)]+$/
const MOBILE_REGEX = /^1[0-9]{10}$/
const EMAIL_REGEX = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,10}(\s*;\s*[\w-\.]+@([\w-]+\.)+[\w-]{2,10})*(\s*;\s*)?$/
function checkMobile(mobile) {
return MOBILE_REGEX.test(mobile)
}
function checkEmail(email) {
return EMAIL_REGEX.test(email)
}
function getFormattedPrice(price) {
return Accounting.formatMoney(price, format)
}
function getShippingFeeNum(cartData, settings) {
const shippingRegionsData = settings.shippingRegions.default
? settings.shippingRegions.default
: settings.shippingRegions.cn
const { feePerOrder, feePerAdditionalItem } = shippingRegionsData
const items = cartData.items
if (!items.some(item => item.product.shippingInfo)) {
return 0
}
const additionalFee = items.reduce((total, next) => {
const fee = next.product.shippingInfo
? feePerAdditionalItem * next.orderItem.quantity
: 0
return total + fee
}, 0)
return feePerOrder - feePerAdditionalItem + additionalFee
}
function getTotalPriceNum(cartData) {
let totalPrice = 0
cartData.items.forEach(
item => (totalPrice += item.orderItem.quantity * item.orderItem.price),
)
return totalPrice
}
function getTotalPriceNumWithoutFlashSale(cartData) {
let totalPrice = 0
cartData.items.forEach(item => {
const canUseCoupon = !(
item.product.flashSale && !item.product.flashSale.coupon
)
if (canUseCoupon) {
totalPrice += item.orderItem.quantity * item.orderItem.price
}
})
return totalPrice
}
function getDiscountNum(cartData, coupon, settings) {
let number = 0
if (coupon.category) {
if (coupon.option.condition.productId) {
const found = cartData.items.find(
item =>
Number(item.productId) === Number(coupon.option.condition.productId),
)
if (found) {
const canUseCoupon = !(
found.product.flashSale && !found.product.flashSale.coupon
)
if (canUseCoupon) {
number =
found.orderItem.price *
found.orderItem.quantity *
coupon.option.amount /
100
}
}
} else {
switch (coupon.category) {
case 'free_shipping':
number = getShippingFeeNum(cartData, settings)
return number
case 'flat':
number = coupon.option.amount
break
case 'percentage':
number =
getTotalPriceNumWithoutFlashSale(cartData) *
coupon.option.amount /
100
break
// no default
}
}
}
if (number >= getTotalPriceNum(cartData)) {
number = getTotalPriceNum(cartData)
}
return number
}
function getPriceScope(prices) {
if (!prices.length) {
return '¥--'
}
let maxPrice = Math.max(...prices)
let minPrice = Math.min(...prices)
maxPrice =
maxPrice % 1 === 0 ? maxPrice : getFormattedPrice(maxPrice).slice(1) // remove the currency symbol
minPrice =
minPrice % 1 === 0 ? minPrice : getFormattedPrice(minPrice).slice(1)
if (maxPrice > minPrice) {
return ${minPrice} - ${maxPrice}`
}
return ${minPrice}`
}
function getMinPrice(prices) {
if (!prices.length) {
return '¥--'
}
const minPrice = Math.min(...prices)
if (minPrice >= 10000) {
return `¥${Number(Math.floor(minPrice / 1000) / 10).toFixed(1)}万`
} else {
return minPrice % 1 === 0 ? ${minPrice}` : getFormattedPrice(minPrice)
}
}
function getItemsNum(cartData) {
let total = 0
cartData.items.forEach(item => (total += item.orderItem.quantity))
return total
}
function getPointsDiscountAmount(cartData) {
if (!cartData.points) {
return 0
}
const { isUsePointsDeductible, pointsDeductibleAmount } = cartData.points
if (isUsePointsDeductible && pointsDeductibleAmount) {
return pointsDeductibleAmount * AMOUNT_UNIT
} else {
return 0
}
}
function getCouponBaseAmount(cartData, couponSaveMoney, settings) {
let amount = 0
amount = getTotalPriceNum(cartData) - couponSaveMoney
if (amount < 0) {
amount = 0
}
return amount
}
function getFinalAmount(shippingFeeBaseAmount) {
let finalAmount = 0
if (shippingFeeBaseAmount < 0) {
finalAmount = 0
} else {
finalAmount = shippingFeeBaseAmount
}
return finalAmount / 100
}
function getPointsBaseAmount(
shippingFeeBaseAmount,
discountAmount,
isOpenDeduct,
) {
let pointsBaseAmount = 0
const expandedDiscountAmount = discountAmount * AMOUNT_UNIT
if (isOpenDeduct) {
pointsBaseAmount = shippingFeeBaseAmount - expandedDiscountAmount
} else {
pointsBaseAmount = shippingFeeBaseAmount
}
if (pointsBaseAmount < 0) {
pointsBaseAmount = 0
}
return pointsBaseAmount
}
function getShippingFeeBaseAmount(
cartData,
settings,
couponBaseAmount,
isNotNeedExpressFee,
) {
let shippingFeeBaseAmount = 0
const { coupon } = cartData
const totalShippingFeeAmount = getShippingFeeNum(cartData, settings)
if ((coupon && coupon.category === 'free_shipping') || isNotNeedExpressFee) {
shippingFeeBaseAmount = couponBaseAmount
} else {
shippingFeeBaseAmount = couponBaseAmount + totalShippingFeeAmount
}
return shippingFeeBaseAmount
}
function canUseCoupon(cartData, coupon) {
if (coupon.category) {
if (coupon.option.condition.orderOver) {
return getTotalPriceNum(cartData) >= coupon.option.condition.orderOver
} else if (coupon.option.condition.productId) {
return cartData.items.find(
item =>
Number(item.productId) === Number(coupon.option.condition.productId),
)
}
return true
}
}
function leftItemsNum(cartData, variation, product) {
if (variation.quantity === -1) {
return -1
}
const variationId = variation.id
const foundItem = cartData.items.find(
item => Number(item.orderItem.id) === Number(variationId),
)
let totalQuantity = variation.quantity
// flash sale not started or expired, normal quantity should excludes flash quantity
// make sure flash quantiy is enough
const isFlashSaleStartedAndNotExpired =
product &&
product.flashSale &&
!product.flashSale.started &&
!product.flashSale.expired
if (isFlashSaleStartedAndNotExpired) {
const foundFlashVariation = product.flashSale.settings.find(
item => item.lineItemId === variationId,
)
if (foundFlashVariation) {
totalQuantity -= foundFlashVariation.flashQuantity
}
}
// group buy not started or expired, normal quantity should excludes group buy quantity
// make sure group buy quantiy is enough
const isGroupBuyStartedAndNotExpired =
product &&
product.groupBuy &&
!product.groupBuy.started &&
!product.groupBuy.expired
if (isGroupBuyStartedAndNotExpired) {
const foundGroupBuyVariation = product.groupBuy.settings.find(
item => item.lineItemId === variationId,
)
if (foundGroupBuyVariation) {
totalQuantity -= foundGroupBuyVariation.groupBuyQuantity
}
}
if (foundItem) {
return totalQuantity - foundItem.orderItem.quantity
}
return totalQuantity
}
function selectedItemsNumForSameProduct(cartData, variation) {
const { productId } = variation
const foundItems =
cartData.items.filter(
item => Number(item.productId) === Number(productId),
) || []
return foundItems.reduce((acc, next) => acc + next.orderItem.quantity, 0)
}
const sortWithOrderList = curry((orderList, data) => {
data.sort((a, b) => {
const aIndex = orderList[a.id] || -a.id
const bIndex = orderList[b.id] || -b.id
return aIndex - bIndex
})
return data
})
function sortWithOrder(orderList = {}) {
return function(a, b) {
const aIndex = orderList[a.id] || -a.id
const bIndex = orderList[b.id] || -b.id
return aIndex - bIndex
}
}
function checkShippingInfo(data, key) {
const shippingInfo = data.shippingInfo
const errors = {}
if (!shippingInfo.firstName) {
errors.firstName = '姓名不能为空'
}
if (!PHONE_REGEX.test(shippingInfo.phone)) {
errors.phone = '手机格式不正确'
}
if (shippingInfo.email && !EMAIL_REGEX.test(shippingInfo.email)) {
errors.email = '邮箱格式不正确'
}
if (data.needShippingInfo) {
const { provinceIndex, provinceArray } = data
if (
provinceArray[provinceIndex].name !== '海外' &&
(data.provinceIndex === -1 || data.cityIndex === -1)
) {
errors.district = '地区不能为空'
}
if (!shippingInfo.line1) {
errors.line1 = '请填写详细地址'
}
}
return !key ? errors[Object.keys(errors)[0]] : errors[key]
}
function getTime(date) {
const time = new Date(date)
return `${time.getFullYear()}${time.getMonth() +
1}${time.getDate()}${time.getHours()}:${time.getMinutes()}`
}
function formatProtocol(url) {
if (!url) {
return 'https://assets.sxlcdn.com/images/ecommerce/ecommerce-default-image.png'
}
const _url = url.replace('http:', '').replace('https:', '')
return `https:${_url}`
}
function getNeedShippingInfo(products = []) {
return products.some(product => product.shippingInfo)
}
function formatProductData(list, cartData) {
list.forEach(product => {
product.priceScope = getPriceScope(
product.variations.map(variation => variation.price / 100),
)
product.outOfStock = !product.variations.some(
variation => leftItemsNum(cartData, variation) !== 0,
)
if (product.picture.length > 0) {
product.picture[0].thumbnailUrl = formatProtocol(
product.picture[0].thumbnailUrl,
)
} else {
product.picture = [
{
thumbnailUrl: formatProtocol(),
},
]
}
})
return list
}
function getCacheTime(duration) {
return new Date().getTime() + duration
}
function formatFactory(method, key = 'url') {
let assertError = ''
if (typeof method !== 'function') {
assertError = new TypeError(
`method pass into formatFactory must be a function, but get ${typeof method}`,
)
}
return function(obj) {
if (assertError) {
throw assertError
}
return Object.assign({}, obj, {
[key]: method(obj[key]),
})
}
}
function caculateShortcuts(shortcuts) {
let shortcutsFirstLine
let shortcutsSecondLine
let shortcutsSecondLineWrapperClass
if (shortcuts && shortcuts.length > 5) {
const center = Math.ceil(shortcuts.length / 2)
shortcutsFirstLine = shortcuts.slice(0, center)
shortcutsSecondLine = shortcuts.slice(center)
const firstLineLength = shortcutsFirstLine.length
const secondLineLength = shortcutsSecondLine.length
if (firstLineLength !== secondLineLength) {
if (secondLineLength === 3) {
shortcutsSecondLineWrapperClass = 'shortcut-placeholder-junior'
} else if (secondLineLength === 4) {
shortcutsSecondLineWrapperClass = 'shortcut-placeholder-senior'
}
} else {
shortcutsSecondLineWrapperClass = ' '
}
} else {
shortcutsFirstLine = []
shortcutsSecondLine = []
shortcutsSecondLineWrapperClass = ' '
}
return [
shortcutsFirstLine,
shortcutsSecondLine,
shortcutsSecondLineWrapperClass,
]
}
function caculateShortcutsGroup(shortcuts, type) {
if (type === 'circle' || type === 'square') {
if (shortcuts && shortcuts.length > 5) {
const center = Math.ceil(shortcuts.length / 2)
return [shortcuts.slice(0, center), shortcuts.slice(center)]
}
} else if (type === 'frame') {
if (shortcuts) {
if (shortcuts.length > 4 && shortcuts.length < 9) {
const center = Math.ceil(shortcuts.length / 2)
return [shortcuts.slice(0, center), shortcuts.slice(center)]
} else if (shortcuts.length >= 9) {
const middle = Math.ceil(shortcuts.length / 3)
return [
shortcuts.slice(0, middle),
shortcuts.slice(middle, middle + 3),
shortcuts.slice(middle + 3),
]
}
}
}
return []
}
const trimMoney = money => {
if (money && typeof money === 'number' && !isNaN(money)) {
const moneyString = `${money}`
return Number(moneyString.substring(0, moneyString.length - 2))
}
return money
}
const trimMoneyWithPercentage = money => {
if (money && typeof money === 'number' && !isNaN(money)) {
return (100 - money) / 10
}
return money
}
const formatCouponForNewTemplate = coupon => {
const { category } = coupon
const preMappedCoupon = formatCouponDate(formatCoupon(coupon))
// TODO: bad style
return Object.assign({}, preMappedCoupon, {
couponName: mapCouponName(category),
condition: mapCondition(preMappedCoupon),
})
}
const mapCouponName = category => {
switch (category) {
case 'flat':
return '代金券'
case 'free_shipping':
return '包邮券'
case 'percentage':
return '折扣券'
default:
return '优惠券'
}
}
const mapCondition = coupon => {
if (coupon.option.condition.orderOver) {
return `满${coupon.option.condition.orderOver}可用`
} else if (coupon.option.condition.productId) {
return '特定商品可用'
} else {
return '所有订单可用'
}
}
const formatCoupon = coupon => {
const { option, category } = coupon
let couponName, conditionDescription, amount, sendMethod
if (category === 'flat') {
couponName = option.couponName || '抵扣券'
amount = trimMoney(option.amount)
} else if (category === 'free_shipping') {
couponName = option.couponName || '免邮优惠'
amount = '免邮'
} else {
couponName = option.couponName || '打折券'
amount = trimMoneyWithPercentage(option.amount)
}
if (option.condition.orderOver) {
conditionDescription = `消费满${trimMoney(
option.condition.orderOver,
)}元可使用`
} else if (option.condition.productId) {
conditionDescription = '特定商品可使用'
} else if (option.condition.productCategoryId) {
conditionDescription = '特定商品分类可使用'
} else {
conditionDescription = '适用所有商品'
}
const condition = Object.assign({}, option.condition, {
orderOver: trimMoney(option.condition.orderOver),
conditionDescription,
})
if (coupon.isReceivedWhenOrderPlaced) {
sendMethod = 'isReceivedWhenOrderPlaced'
} else if (coupon.isReceivedWhenBeingMember) {
sendMethod = 'isReceivedWhenBeingMember'
}
const optionResult = Object.assign({}, option, {
couponName,
amount,
condition,
})
return Object.assign({}, coupon, {
option: optionResult,
sendMethod,
})
}
const formatDate = date => {
if (!date) {
return ''
}
return moment(date).format('YYYY.MM.DD')
}
const formatCouponDate = coupon => {
const { startsAt, endsAt } = coupon
const startFormat = formatDate(startsAt)
const endFormat = formatDate(endsAt)
return Object.assign({}, coupon, {
startsAt: startFormat,
endsAt: endFormat,
})
}
const caculateSaveMoney = (currentShipping, couponId) => {
let couponSaveMoney = 0
let couponSaveMoneyToShow = ''
const discountInfo = currentShipping.validCoupon[couponId]
if (!currentShipping.optimalDiscountCoupon && !couponId) {
couponSaveMoneyToShow = '无可用优惠券'
} else if (discountInfo === 'free_shipping') {
couponSaveMoneyToShow = '免邮'
} else if (typeof discountInfo === 'number' && !isNaN(discountInfo)) {
couponSaveMoneyToShow = `- ¥${discountInfo / 100}`
couponSaveMoney = discountInfo
} else {
couponSaveMoneyToShow = '请手动选择优惠券'
}
return {
couponSaveMoney,
couponSaveMoneyToShow,
}
}
const commonError = () => {
wx.showModal({
title: '网络错误',
content: '请刷新重试',
})
}
const ERROR_MSG = {
expired: '优惠券已过期',
'coupon not find': '优惠券未找到',
'user not vip': '仅 VIP 用户可以领取',
'has been used': '优惠券已被领取',
'has been received': '优惠券仅可被兑换一次',
'count not enough': '优惠券不足,无法领取',
'not available to receive': '优惠券使用后方可继续兑换',
}
const showCouponStatus = state => {
const { isFetching } = state.getIn(['coupon', 'user']).toJS()
if (
isFetching &&
isFetching !== 'success' &&
wx.showLoading &&
wx.hideLoading
) {
wx.showLoading()
} else if (wx.hideLoading) {
wx.hideLoading()
}
}
const splitDate = date => {
if (!date) {
return ''
}
return new Date(date)
.toLocaleString()
.substr(0, 10)
.split('/')
.join('-')
}
const formatNumber = (number, precise) => Number(number).toFixed(precise)
const parseScene = scene => {
const params = {}
decodeURIComponent(scene)
.split('&')
.forEach((item, index) => {
const itemArr = item.split('=')
if (itemArr.length === 2) {
params[itemArr[0]] = itemArr[1]
} else {
params[index] = itemArr[0]
}
})
return params
}
const isEmptyDimension = dimension => {
if (
dimension === undefined ||
dimension.name === '' ||
dimension.options === undefined ||
dimension.options === null ||
dimension.options.length === 0
) {
return true
} else {
return false
}
}
const hasMutipleDimensions = dimensions => {
if (!dimensions) {
return false
}
const { dimension1, dimension2 } = dimensions
return !isEmptyDimension(dimension1) && !isEmptyDimension(dimension2)
}
/**
* according to the user's current consumption amount,
* calculate how many points and points can be deducted.
* Parameter:
* points: 2000
* confirmAmount: 600
* costBonusUnit: 100
* reduceMoney: 200
* max-reduce-amount => (2000 * 200 / 100)
*
* Method:
* calculatePointsDiscountData function
*
* conditions:
* 1. the current confirm amount of the user is greater than
* the max deductible amount of the current points of the user
*
* 2. the current confirm amount of the user is less than
* the max deductible amount of the current points of the user
*
* Result:
* usePoints: the user can use the current consumption of points
* pointsDeductibleAmount: points can be used to offset the money
*
*/
export function calculateCanbeUsedPoints(
amount,
costBonusUnit,
reduceMoneyUnit,
) {
return Math.ceil(amount * costBonusUnit / reduceMoneyUnit)
}
export function calculateMaxDeductibleAmount(
totalPoints,
reduceMoneyUnit,
costBonusUnit,
) {
return Math.floor(totalPoints * reduceMoneyUnit / costBonusUnit)
}
export function calculateCanbeDeductibleAmount(
maxCanbeUsedPoints,
costBonusUnit,
reduceMoneyUnit,
) {
return Math.ceil(maxCanbeUsedPoints * reduceMoneyUnit / costBonusUnit)
}
export function calculatePointsDiscountData(
costBonusUnit,
reduceMoney,
costAmount,
totalPoints,
) {
let usePoints = 0
let pointsDeductibleAmount = 0
const maxDeductibleAmount = calculateMaxDeductibleAmount(
totalPoints,
reduceMoney,
costBonusUnit,
)
if (maxDeductibleAmount - costAmount >= 0) {
usePoints = calculateCanbeUsedPoints(costAmount, costBonusUnit, reduceMoney)
pointsDeductibleAmount = costAmount
} else {
const maxCanbeUsedPoints = calculateCanbeUsedPoints(
maxDeductibleAmount,
costBonusUnit,
reduceMoney,
)
if (totalPoints - maxCanbeUsedPoints >= 0) {
pointsDeductibleAmount = calculateCanbeDeductibleAmount(
maxCanbeUsedPoints,
costBonusUnit,
reduceMoney,
)
usePoints = maxCanbeUsedPoints
} else {
pointsDeductibleAmount = maxDeductibleAmount
usePoints = calculateCanbeUsedPoints(
maxDeductibleAmount,
costBonusUnit,
reduceMoney,
)
}
}
return {
usePoints,
expandedPointsDeductibleAmount: parseFloat(
pointsDeductibleAmount / AMOUNT_UNIT,
).toFixed(2),
}
}
export function getRestTime(time, interval, unit) {
return moment(time)
.add(interval, unit)
.from(new Date())
}
export {
checkMobile,
checkEmail,
getFormattedPrice,
getPriceScope,
getMinPrice,
getItemsNum,
getDiscountNum,
getTotalPriceNum,
getFinalAmount,
getCouponBaseAmount,
getPointsBaseAmount,
getShippingFeeBaseAmount,
getShippingFeeNum,
canUseCoupon,
leftItemsNum,
selectedItemsNumForSameProduct,
sortWithOrderList,
checkShippingInfo,
getTime,
formatProtocol,
getNeedShippingInfo,
formatProductData,
getCacheTime,
formatFactory,
sortWithOrder,
caculateShortcuts,
caculateShortcutsGroup,
formatCoupon,
formatCouponDate,
formatCouponForNewTemplate,
caculateSaveMoney,
commonError,
ERROR_MSG,
splitDate,
showCouponStatus,
formatNumber,
parseScene,
isEmptyDimension,
hasMutipleDimensions,
}
import { formatProtocol } from 'root/utils/helpers/ecommerceHelper'
import moment from 'wechat_common/lib/moment'
import compose from 'ramda/src/compose'
export function getQnUrl(image) {
if (image.url && image.url !== '!') {
return image.url
}
return `https://user-assets.sxlcdn.com/${image.storageKey}`
}
export function formattedPrice(price) {
if (price > 0) {
return Number(price / 100).toFixed(2)
} else {
return 0
}
}
export function getAmount(cart) {
let result = 0
for (const id in cart) {
result += cart[id].quantity * cart[id].price
}
return formattedPrice(result)
}
export function getQuantity(cart) {
let result = 0
for (const id in cart) {
result += cart[id].quantity
}
return result
}
export function getOrderItems(cart) {
const result = []
for (const id in cart) {
result.push({
id,
quantity: cart[id].quantity,
})
}
return result
}
export function formatPack(target) {
function addPacketName(packet) {
const { cards } = packet
const quantity = cards.length
if (quantity === 1) {
packet.name = `${cards[0].title}`
} else if (quantity > 1) {
packet.name = `${cards[0].title}等多份`
} else {
packet.name = ''
}
return packet
}
function fotmatPacketBackground(packet) {
packet.background = formatProtocol(getQnUrl(packet.background))
return packet
}
function _formatTime(time) {
return moment(time).format('YYYY年MM月DD日 HH:mm')
}
function formatPacketTime(packet) {
packet.created = _formatTime(packet.created)
packet.updated = _formatTime(packet.updated)
return packet
}
function formattedPackPrice(packet) {
packet.price = formattedPrice(packet.price)
return packet
}
return compose(
addPacketName,
fotmatPacketBackground,
formatPacketTime,
formattedPackPrice,
)(target)
}
export function getCardList(packet) {
const { cards } = packet
return cards.map(card => {
const newCard = {
cardId: card.cardId,
}
return newCard
})
}
import { getQnUrl } from 'wechat_common/utils/helpers/imageHelper'
export function getMixLayout(mix, layout, type, isMain) {
const index = isMain ? 0 : 1
if(Array.isArray(mix) && Array.isArray(layout) && mix[index] === type){
return layout[index]
}
return layout
}
// Chinese character、full Angle symbol takes up two widths
// English character、half - Angle symbol takes up one widths
export function cutString(str, width) {
const re = /[^\x00-\xff]/g
const result = []
let cnlen = 0
if (str.match(re)) {
cnlen = str.match(re).length
}
let strlen = Number(str.length) + cnlen
while (strlen > width) {
let tmplen = 0
let index = 0
for (let i = 0; i < str.length; i++) {
if (str[i].match(re)) {
tmplen += 2
} else {
tmplen++
}
if (tmplen > width) {
index = i
break
}
}
result.push(str.substring(0, index))
str = str.substring(index)
if (str.match(re)) {
cnlen = str.match(re).length
}
strlen = Number(str.length) + cnlen
}
result.push(str)
return result
}
// save the canvas as a image
export function saveCanvasImage(canvasId) {
wx.canvasToTempFilePath({
canvasId: canvasId,
success(res) {
const filePath = res.tempFilePath
wx.getSetting({
success(res) {
if (!res.authSetting['scope.writePhotosAlbum']) {
wx.authorize({
scope: 'scope.writePhotosAlbum',
success() {
wx.saveImageToPhotosAlbum({
filePath,
success(result) {
wx.showToast({
title: '保存成功',
icon: 'success',
duration: 2000,
})
},
fail(result) {
console.log(result)
},
})
},
fail() {
wx.showModal({
title: '提示',
content: '保存图片需要进行授权操作,请前往小程序设置页完成授权。',
showCancel: false,
})
},
})
} else {
wx.saveImageToPhotosAlbum({
filePath,
success(result) {
wx.showToast({
title: '保存成功',
icon: 'success',
duration: 2000,
})
},
})
}
},
})
},
})
}
// canvas draw image need use this api to get image local path
export const getImageInfo = function(url) {
return new Promise((resolve, reject) => {
wx.getImageInfo({
src: url,
success: res => resolve(res),
fail: e => reject(e),
})
})
}
export function parseUploadImage(upload) {
const uploadImage = []
const previewImage = []
if (upload) {
upload.forEach(image => {
uploadImage.push({
productId: image.productId,
hash: image.hash,
image: getQnUrl(image),
})
previewImage.push({
productId: image.productId,
url: getQnUrl(image)
})
})
}
return {
uploadImage,
previewImage,
}
}
export function extendMethod(...fns) {
return function(...args) {
fns.forEach(
fn => (typeof fn === 'function' ? fn.bind(this)(...args) : null),
)
}
}
export function getQnUrl(image) {
function isUnsplash(url) {
return url.indexOf('unsplash') !== -1
}
if (!image.storageKey) {
return ''
}
if (isUnsplash(image.storageKey)) {
return `${image.storageKey}?h=1500&w=2000&q=40&fit=clip&fm=jpg`
}
return `https://user-assets.sxlcdn.com/${
image.storageKey
}?imageMogr2/strip/thumbnail/1200x9000>/format/${image.format}`
}
import wrappedWXTool from 'wechat_common/utils/wrappedWXTool'
import { setGlobalData } from 'root/reducers/ecommerce/global'
import { dispatch } from 'root/wmp-redux'
export default function login(cb) {
wrappedWXTool.login({
success(loginRes) {
const { code } = loginRes
dispatch(
setGlobalData({
code,
}),
)
if (cb && typeof cb === 'function') {
cb(loginRes)
}
wrappedWXTool.wxGetuserInfo().then(getInfoRes => {
dispatch(
setGlobalData({
userInfo: getInfoRes.userInfo,
}),
)
})
},
})
}
import curry from 'ramda/src/curry'
import { html2json } from 'wechat_common/wxParse/html2json'
import { getProductList } from 'root/selectors/presentation/productSelector'
import { getStatusByPath } from 'wechat_common/selectors/entityStatusSelector'
import { iconPath } from 'root/constants/presentation/iconPath'
import moment from 'wechat_common/lib/moment'
const DAYS = [1, 7, 30, 90]
const DATE_TYPE = 'YYYY-MM-DD'
const RENDER_COLOR = ['#B6628E', '#CE605D', '#66B67A', '#DCBD73', '#5E77AF']
export function getCacheTime(duration) {
return new Date().getTime() + duration
}
export function getQnUrl(image, options) {
if (!image) {
return null
}
if (image.url && image.url !== '!') {
return image.url
}
if (image.storageKey && image.storageKey.charAt(0) === '/') {
image.storageKey = image.storageKey.slice(1)
}
const _options = options
? options
: `imageMogr2/strip/thumbnail/1200x9000>/format/${image.format}`
if (NODE_ENV !== 'production') {
return `https://okzg4jwn8.qnssl.com/${image.storageKey}?${_options}`
}
return `https://user-assets.sxlcdn.com/${image.storageKey}?${_options}`
}
export function formatProtocol(url) {
let _url = url
if (!url) {
return 'https://assets.sxlcdn.com/images/ecommerce/ecommerce-default-image.png'
}
if (typeof url === 'object') {
_url = getQnUrl(url)
}
_url = _url.replace('http:', '').replace('https:', '')
return `https:${_url}`
}
export function formatFactory(method, key = 'url') {
let assertError = ''
if (typeof method !== 'function') {
assertError = new TypeError(
`method pass into formatFactory must be a function, but get ${typeof method}`,
)
}
return function(obj) {
if (assertError) {
throw assertError
}
return Object.assign({}, obj, {
[key]: method(obj[key]),
})
}
}
export function caculateShortcuts(shortcuts) {
let shortcutsFirstLine
let shortcutsSecondLine
let shortcutsSecondLineWrapperClass
if (shortcuts && shortcuts.length > 5) {
const center = Math.ceil(shortcuts.length / 2)
shortcutsFirstLine = shortcuts.slice(0, center)
shortcutsSecondLine = shortcuts.slice(center)
const firstLineLength = shortcutsFirstLine.length
const secondLineLength = shortcutsSecondLine.length
if (firstLineLength !== secondLineLength) {
if (secondLineLength === 3) {
shortcutsSecondLineWrapperClass = 'shortcut-placeholder-junior'
} else if (secondLineLength === 4) {
shortcutsSecondLineWrapperClass = 'shortcut-placeholder-senior'
}
} else {
shortcutsSecondLineWrapperClass = ' '
}
} else {
shortcutsFirstLine = []
shortcutsSecondLine = []
shortcutsSecondLineWrapperClass = ' '
}
return [
shortcutsFirstLine,
shortcutsSecondLine,
shortcutsSecondLineWrapperClass,
]
}
function strNumDiscode(str) {
str = str.replace(/&forall;/g, '∀')
str = str.replace(/&part;/g, '∂')
str = str.replace(/&exists;/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(/&cap;/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;&nbsp;/g, ' ')
str = str.replace(/&yen;/g, '¥')
str = str.replace(/&quot;/g, "'")
str = str.replace(/&amp;/g, '&')
// str = str.replace(/&lt;/g, '‹');
// str = str.replace(/&gt;/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
}
export function generateEditorContentHTML(items) {
return items
.map(item => {
if (item.type === 'Image') {
return `<img
src="${getQnUrl(item)}"
width="${item.w}"
height="${item.h}" />`
} else if (item.type === 'RichText') {
return `<p>${item.value}</p>`
} else if (item.type === 'Video') {
const attrs = Object.entries(item)
.map(([key, value]) => `${key}="${value}"`)
.join(' ')
return `<video ${attrs}></video>`
}
})
.join('')
}
export function parseString(str) {
str = strNumDiscode(str)
str = strGreeceDiscode(str)
str = strcharacterDiscode(str)
str = strOtherDiscode(str)
return str
}
export function sortWithOrder(orderList = {}) {
return function(a, b) {
const aIndex = orderList[a.id] || -a.id
const bIndex = orderList[b.id] || -b.id
return aIndex - bIndex
}
}
export const sortWithOrderList = curry((orderList, data) => {
data.sort((a, b) => {
const aIndex = orderList[a.id] || -a.id
const bIndex = orderList[b.id] || -b.id
return aIndex - bIndex
})
return data
})
export const checkProtocol = product => {
if (product.picture.length > 0) {
product.picture[0].thumbnailUrl = formatProtocol(
product.picture[0].thumbnailUrl,
)
} else {
product.picture = [
{
thumbnailUrl: formatProtocol(),
},
]
}
return product
}
export const matchNameInProduct = (productList, name) =>
productList.filter(product => (product.categories || []).includes(name))
export const convertProductDetailData = product => {
if (product.detailEnabled && product.detail) {
const { items } = product.detail
if (items) {
const parsedHtml = generateEditorContentHTML(items)
const contentTemp = parsedHtml.replace(
/&nbsp;/g,
'<em class="dump">z</em>',
)
const content = html2json(parseString(contentTemp))
product.detail.content = content
product.detailNodes = content.nodes
}
}
return product
}
export const getFirstProduct = products => {
const state = wx.store.getState()
const { ids } = getStatusByPath(state, ['product', 'category', 'all'])
const productList = getProductList(state, ids)
if (productList && productList.length > 0) {
return productList[0]
} else {
return null
}
}
// there is no route in the return of getCurrentPages in wept
// so using pageName to predicate current page
export const isInProductsPage = () =>
getCurrentPages().pop().data.pageName === 'productIndex'
// there is no route in the return of getCurrentPages in wept
// so using pageName to predicate current page
export const isInProductDetailPage = () =>
getCurrentPages().pop().data.pageName === 'productDetail'
export function getTeamMemberAvatar(image) {
if (!image) {
return `${iconPath.DEFAULT_AVATAR}&random=${Math.random()}`
}
return getQnUrl(image, `imageView2/1/w/300/h/400/format/${image.format}`)
}
export function getRoundTeamMemberAvatar(image) {
if (!image) {
return `${iconPath.DEFAULT_ROUND_AVATAR}&random=${Math.random()}`
}
return getQnUrl(
image,
`imageView2/1/w/300/h/300/format/${image.format}|roundPic/radius/!50p`,
)
}
export function getRoundImage(imageUrl) {
return `${imageUrl}|roundPic/radius/!50p`
}
export function replaceWithDownloadFileDomain(url) {
return url && url.replace('nzr2ybsda.qnssl.com', 'user-assets.sxlcdn.com')
}
export const getPeriodTimeByNumber = days => {
const startDate = moment()
.add(-days, 'days')
.format(DATE_TYPE)
const endDate = moment().format(DATE_TYPE)
return {
startDate,
endDate,
}
}
export const formatChartsData = charts => {
let expandedCharts = []
if (charts.length > 0) {
expandedCharts = charts.map((item, index) => {
item.data = parseFloat(item.data) || 0
item.stroke = true
item.color = RENDER_COLOR[index]
return item
})
}
return expandedCharts
}
export const formatGroupChartData = chartsData => {
const { groupArticleAnalytics, groupProductAnalytics } = chartsData
const articleCharts = formatChartsData(groupArticleAnalytics)
const productCharts = formatChartsData(groupProductAnalytics)
return {
groupArticleAnalytics: articleCharts,
groupProductAnalytics: productCharts,
}
}
export const filterEnableChartList = chartsList => {
const enableRenderChartList = []
if (chartsList.length) {
chartsList.forEach(item => {
if (item.series && item.series.length > 0) {
enableRenderChartList.push(item)
}
})
}
return enableRenderChartList
}
export const getDaysByTabStatus = tabStatus => {
let days
switch (tabStatus) {
case 'yesterday':
days = DAYS[0]
break
case '7days':
days = DAYS[1]
break
case '30days':
days = DAYS[2]
break
case '90days':
days = DAYS[3]
break
default:
}
return days
}
const resetItem = (item, typeMap) => {
item.label = item.label ? item.label : ''
if (item.label.match(/【.*】/)) {
item.label = item.label.match(/【.*】/)[0].replace(/【|】/g, '')
}
let path = item.subtype
if (item.type === 'ui' && item.subtype === 'sharePage') {
try {
const pathParam = JSON.parse(item.extraParam)
path = pathParam.path
for (const key in typeMap) {
const regEx = new RegExp(key)
if (regEx.test(path)) {
if (typeMap[key].isShare) {
return Object.assign(
{},
item,
{ isHiddenRadar: false },
typeMap[key],
)
} else {
return Object.assign(
{},
item,
{ isHiddenRadar: true },
typeMap[key],
)
}
}
}
} catch (err) {
throw new Error('pathParam parse error')
}
}
for (const key in typeMap) {
const regEx = new RegExp(key)
if (regEx.test(path)) {
return Object.assign(
{},
item,
{
isHiddenRadar: false,
isStayTime: typeMap[key].isStayTime,
},
typeMap[key],
)
}
}
return Object.assign({}, item, {
name: item.label,
isHiddenRadar: true,
})
}
export const formatRadar = item => {
// fix label value is null or `点赞了 【博客】`
item.label = item.label ? item.label : ''
const radarUiTypeShow = [
'chat',
'leaveContact',
'submitContactForm',
'likeBlogPost',
'likeContactCard',
'shareContactCard',
'sharePostPage',
'shareProductPage',
'shareImageBlogPost',
'shareImageProduct',
'saveAsImage',
'saveToContact',
'copyWechatId',
'viewQrcode',
'copyEmail',
'enterApp',
'leaveApp',
]
if (item.label.match(/【.*】/)) {
item.label = item.label.match(/【.*】/)[0].replace(/【|】/g, '')
}
const radarPvTypeShow = {
'productDetail/productDetail': {
pageName: '产品',
name: `【${item.label}】`,
highLight: true,
showTimes: true,
isShare: true,
isStayTime: true,
},
'product/product': {
pageName: '产品分类',
name: `【${item.label}】`,
highLight: true,
showTimes: true,
},
'postList/postList': {
pageName: '文章分类',
name: `【${item.label}】`,
highLight: true,
showTimes: true,
},
'postDetail/postDetail': {
pageName: '文章',
name: `【${item.label}】`,
highLight: true,
showTimes: true,
isShare: true,
isStayTime: true,
},
'presentation/index/index': {
pageName: '',
name: `首页`,
highLight: true,
showTimes: true,
isShare: false,
},
'contact/contact': {
isShare: true,
pageName: '您的',
name: `名片`,
highLight: true,
showTimes: true,
isHiddenRadar: true,
},
}
// fix the pv include blog's and product's catgary and detail
if (item.type === 'pv') {
item = resetItem(item, radarPvTypeShow)
} else if (item.type === 'ui') {
if (radarUiTypeShow.indexOf(item.subtype) > -1) {
item.name = `【${item.label}】`
item.isHiddenRadar = false
} else if (item.subtype === 'sharePage') {
item = resetItem(item, radarPvTypeShow)
} else {
item.isHiddenRadar = true
}
}
// the personalized push text
if (item.subtype === 'chat') {
item.welcomeText = item.repeatTimes >= 2 ? '' : '快去回复吧'
} else if (item.subtype === 'submitContactForm') {
item.welcomeText = item.repeatTimes >= 2 ? '' : '快去看看他说了什么吧'
} else if (item.subtype === 'likeContactCard') {
item.welcomeText = item.repeatTimes >= 2 ? '' : '似乎对您有好感,快联系ta吧'
} else if (item.subtype === 'viewContactCard') {
item.showTimes = true
item.welcomeText =
item.repeatTimes >= 2 ? '看来成交在望,快联系ta吧!' : '初识在此刻'
} else if (item.subtype === 'shareContactCard') {
item.showTimes = true
item.welcomeText =
item.repeatTimes >= 2
? '看来成交在望,快联系ta吧!'
: '您的人脉正在扩散!'
} else if (item.subtype === 'saveAsImage') {
item.welcomeText =
item.repeatTimes >= 2 ? '' : '近期可能会联系您,密切关注哦'
} else if (item.subtype === 'copyWechatId') {
item.welcomeText = item.repeatTimes >= 2 ? '' : '可能会加您好友,请留意哦'
} else if (item.subtype === 'shareImageProduct') {
item.welcomeText =
item.repeatTimes >= 2
? '看来成交在望,快联系ta吧'
: '看来还挺感兴趣的,快联系ta吧'
} else if (item.subtype === 'copyEmail') {
item.welcomeText =
item.repeatTimes >= 2 ? '' : '可能会发送邮件给您,请留意哦'
} else if (item.subtype === 'sharePage') {
let path
try {
const pathParam = JSON.parse(item.extraParam)
path = pathParam.path
} catch (err) {
throw new Error('pathParam parse error')
}
if (path.includes('contact/contact')) {
item.welcomeText =
item.repeatTimes >= 2
? '看来成交在望,快联系ta吧!'
: '您的人脉正在扩散!'
}
} else if (item.subtype === 'likeBlogPost') {
item.welcomeText = item.repeatTimes >= 2 ? '' : '看来很认可呢,要不要联系下'
} else if (item.type === 'pv') {
if (item.subtype.includes('postList?category')) {
item.welcomeText =
item.repeatTimes >= 2
? '看来成交在望,快联系ta吧'
: '看来对这类文章还挺感兴趣的,快联系ta介绍更多吧'
} else if (item.subtype.includes('product?category')) {
item.welcomeText =
item.repeatTimes >= 2
? '看来成交在望,快联系ta吧'
: '看来对这类产品还挺感兴趣的,快联系ta介绍更多吧'
} else if (item.subtype.includes('postId')) {
item.welcomeText =
item.repeatTimes >= 2
? '看来成交在望,快联系ta吧'
: '看来对公司还挺感兴趣的,快联系ta吧'
} else if (item.subtype.includes('productId')) {
item.welcomeText =
item.repeatTimes >= 2
? '看来成交在望,快联系ta吧'
: '看来还挺感兴趣的,快联系ta吧'
} else if (item.subtype.includes('presentation/index/index')) {
item.isStayTime = true
item.welcomeText =
item.repeatTimes >= 2
? '看来对公司还挺感兴趣的,快联系ta吧'
: '这是个重要情报哦'
} else if (item.subtype.includes('contact/contact')) {
item.isStayTime = true
item.welcomeText =
item.repeatTimes >= 2 ? '看来成交在望,快联系ta吧' : '初识在此刻'
} else if (item.subtype.includes('businessCard/businessCard')) {
item.highLight = false
item.pageName = '您的'
item.name = '名片夹'
item.showTimes = true
}
}
if (item.subtype === 'leaveContact') {
const str1 = item.label.split(',')[0]
const str2 = str1.split('留下了电话:')
item.phone = str2[1]
item.name = str2[0]
item.nickname = str2[0]
}
if (item.subtype === 'shareProductPage') {
item.welcomeText =
item.repeatTimes >= 2
? '看来成交在望,快联系ta吧!'
: '看来还挺感兴趣的,快联系ta吧'
}
if (item.subtype === 'sharePostPage') {
item.welcomeText =
item.repeatTimes >= 2
? '看来成交在望,快联系ta吧!'
: '看来还挺感兴趣的,快联系ta吧'
}
const hourTime = parseInt(item.dwellTime / 3600)
? `${parseInt(item.dwellTime / 3600)}小时`
: ``
const minuteTime = parseInt((item.dwellTime % 3600) / 60)
? `${parseInt((item.dwellTime % 3600) / 60)}分钟`
: ``
const secondTime = `${
(item.dwellTime % 3600) % 60 && item.type === 'pv'
? (item.dwellTime % 3600) % 60
: (item.dwellTime % 3600) % 60 + 1
}秒`
item.dwellTime = hourTime + minuteTime + secondTime
return item
}
import {doGet, dePostWithPromise} from 'wechat_common/utils/request'
export default function REQUEST(options, bindType) {
switch (options.method) {
case "GET":
return bindType ? doGet(bindType(options)) : doGet(options)
case "POST":
return bindType ? dePostWithPromise(bindType(options)) : dePostWithPromise(options)
default :
return bindType ? doGet(bindType(options)) : doGet(options)
}
}
const lastStateContainer = {}
function _observable(store, keyPath, callback) {
return store.subscribe(() => {
const newState = store.getState().getIn(keyPath)
const keyPathStr = keyPath.join('')
if (lastStateContainer[keyPathStr] !== newState) {
const _lastState = lastStateContainer[keyPathStr]
lastStateContainer[keyPathStr] = newState
callback(newState, _lastState)
}
})
}
export default function createObserveble(store, keyPath) {
let subscribeId = 0
return {
subscribe: callback => {
subscribeId = _observable(store, keyPath, callback)
},
unSubscribe: () => {
store.unsubscribe(subscribeId)
},
}
}
import moment from 'wechat_common/lib/moment'
import { getQnUrl } from 'wechat_common/utils/helpers/imageHelper'
export function reducerHelper(state, action, handlers) {
const handler = handlers[action.type]
return handler ? handler(state, action) : state
}
export function formatFactory(method, key = 'url') {
let assertError = ''
if (typeof method !== 'function') {
assertError = new TypeError(
`method pass into formatFactory must be a function, but get ${typeof method}`,
)
}
return function(obj) {
if (assertError) {
throw assertError
}
return Object.assign({}, obj, {
[key]: method(obj[key]),
})
}
}
export function formatProtocol(url) {
const _url = url.replace('http:', '').replace('https:', '')
return `https:${_url}`
}
// 判断十六进制色值是浅色还是深色 return true: 浅色,false:深色
const isLightFunc = hexColor => {
const color = hexColor.toLowerCase()
const sColorChange = []
for (let i = 1; i < 7; i += 2) {
sColorChange.push(parseInt(`0x${color.slice(i, i + 2)}`, 0))
}
if (
sColorChange[0] * 0.299 +
sColorChange[1] * 0.578 +
sColorChange[2] * 0.114 >=
192
) {
return true
} else {
return false
}
}
export function isWhite(color) {
if (
!color ||
color === '' ||
color === '#fff' ||
color === '#ffffff' ||
color === 'white'
) {
return true
} else if (color === 'black') {
return false
}
return isLightFunc(color)
}
export function getDateDiff(dateTimeStamp) {
const monthCount = moment().diff(dateTimeStamp, 'months')
const weekCount = moment().diff(dateTimeStamp, 'weeks')
const dayCount = moment().diff(dateTimeStamp, 'days')
const hourCount = moment().diff(dateTimeStamp, 'hours')
const minCount = moment().diff(dateTimeStamp, 'minutes')
let result = ''
if (monthCount >= 1) {
result = `${String(parseInt(monthCount, 0))}月前`
} else if (weekCount >= 1) {
result = `${String(parseInt(weekCount, 0))}周前`
} else if (dayCount >= 1) {
result = `${String(parseInt(dayCount, 0))}天前`
} else if (hourCount >= 1) {
result = `${String(parseInt(hourCount, 0))}小时前`
} else if (minCount >= 1) {
result = `${String(parseInt(minCount, 0))}分钟前`
} else {
result = '刚刚'
}
return result
}
export function validatePhoneNumber(phoneNumer) {
return /^1[34578]\d{9}$/.test(phoneNumer)
}
export function formatAmount(amount) {
return (Number(amount) / 100).toFixed(2)
}
export function formatGrouponStatus(status) {
switch (status) {
case 'refund_pending':
return { name: '待退款', color: '#fb7d2b', type: 'effective' }
case 'enabled':
return { name: '未使用', color: '#93b719', type: 'effective' }
case 'used':
return { name: '已核销', color: '#9298a0', type: 'used' }
case 'refunded':
return { name: '已退款', color: '#9298a0', type: 'refunded' }
case 'expired':
return { name: '已过期', color: '#9298a0', type: 'expired' }
case 'disabled':
return { name: '已失效', color: '#e64751', type: 'disabled' }
default:
return ''
}
}
// parse the comment pictures and split comment to show or hide.
export function updateComment(comments) {
let lessCommentImg = []
let moreCommentImg = []
let lessComments = []
let holdPictures = []
let newComments = []
if (comments) {
comments.forEach(value => {
if (value.content && value.content.length > 82) {
value.shortContent = `${value.content.substring(0, 82)}...`
}
value.createdAt = moment(value.createdAt)
.locale('zh-cn')
.fromNow()
if (value.pictures && value.pictures.length > 0) {
value.pictures.map(img => {
if (img.image) {
holdPictures.push(img.image)
} else {
holdPictures.push(getQnUrl(img))
}
})
value.pictures = holdPictures
holdPictures = []
} else {
value.pictures = []
}
if (value.pictures && value.pictures.length > 3) {
lessCommentImg = value.pictures.slice(0, 2)
moreCommentImg = value.pictures.slice(2, 3)
} else if (value.pictures && value.pictures.length <= 3) {
lessCommentImg = value.pictures
}
value.lessCommentImg = lessCommentImg || []
value.moreCommentImg = moreCommentImg || []
})
newComments = [...comments]
lessComments = newComments.slice(0, 1)
}
return {
comments: newComments,
lessComments,
}
}
export function compare(property) {
return (a, b) => {
const value1 = a[property]
const value2 = b[property]
return value2 - value1
}
}
export function simpleString(str = '', length = 5) {
return str.length > length ? `${str.substr(0, length)}...` : str
}
/*
* return the main region of a detailed address
* @param {string} addr detailed address
* @return {string}
*/
export const getRegionFromAddress = addr => {
const regEx = /^(.+[省|自治区])?(.+[地区|自治州|市])?(.+[县|区])?/
const rslt = (addr || '').match(regEx)
return rslt || ['']
}
/*
* get the type of the input value
* @param {string} addr detailed address
* @return {string}
*/
export const getType = param => {
if (Array.isArray(param)) {
return 'array'
}
if (param instanceof Date) {
return 'date'
}
if (param === null) {
return 'null'
}
if (param instanceof RegExp) {
return 'regExp'
}
if (param instanceof Error) {
return 'error'
}
return typeof param
}
/*
* compare the first input value to another,
* return a boolean value if they are as same as each other
* @param {Object} a the first object which would be used to compare to another
* @return {Object} b another object
*/
export const isCongruence = (a, b) => {
const typeFirst = getType(a)
if (typeFirst !== getType(b)) {
return false
}
const TYPE_METHODS_MAP = {
array: (x, y) => {
if (x === y) {
return true
}
if (x.length !== y.length) {
return false
}
const ifEqual = (pre, nex) => {
const compareFirst = isCongruence(pre[0], nex[0])
if (pre.length <= 1) {
return compareFirst
}
return compareFirst && TYPE_METHODS_MAP.array(pre.slice(1), nex.slice(1))
}
return ifEqual(x, y)
},
function: (x, y) => {
if (x === y) {
return true
}
return String(x) === String(y)
},
object: (x, y) => {
if (x === y) {
return true
}
const xKeys = Object.keys(x)
const yKeys = Object.keys(y)
if (xKeys.length !== yKeys.length) {
return false
}
const ifKeysEqual = TYPE_METHODS_MAP.array(xKeys, yKeys)
const ifEqual = keys => {
if (keys.length <= 0) {
return true
}
const compareFirst = isCongruence(x[keys[0]], y[keys[0]])
if (keys.length <= 1) {
return compareFirst
}
return compareFirst && ifEqual(keys.slice(1))
}
return ifKeysEqual && ifEqual(xKeys)
},
otherwise: (x, y) => x === y,
}
return (TYPE_METHODS_MAP[typeFirst] || TYPE_METHODS_MAP.otherwise)(a, b)
}
/**
* This middleware can only work in wmp
* @returns {undefined}
*/
export function errorModalMiddleware({ getState }) {
return next => action => {
next(action)
const state = getState()
const { message } = state.getIn(['error']).toJS()
if (message) {
wx.showModal({
content: message,
showCancel: false,
})
}
}
}
export default {
onLoad(options) {},
}
/*
* charts for WeChat small app v1.0
*
* https://github.com/xiaolin3303/wx-charts
* 2016-11-28
*
* Designed and built with all the love of Web
*/
const config = {
yAxisWidth: 15,
yAxisSplit: 5,
xAxisHeight: 15,
xAxisLineHeight: 15,
legendHeight: 15,
yAxisTitleWidth: 15,
padding: 12,
columePadding: 3,
fontSize: 10,
dataPointShape: ['diamond', 'circle', 'triangle', 'rect'],
colors: ['#7cb5ec', '#f7a35c', '#434348', '#90ed7d', '#f15c80', '#8085e9'],
pieChartLinePadding: 25,
pieChartTextPadding: 15,
xAxisTextPadding: 3,
titleColor: '#333333',
titleFontSize: 20,
subtitleColor: '#999999',
subtitleFontSize: 15,
toolTipPadding: 3,
toolTipBackground: '#000000',
toolTipOpacity: 0.7,
toolTipLineHeight: 14,
radarGridCount: 3,
radarLabelTextMargin: 15,
}
// Object.assign polyfill
// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
function assign(target, varArgs) {
if (target == null) {
// TypeError if undefined or null
throw new TypeError('Cannot convert undefined or null to object')
}
const to = Object(target)
for (let index = 1; index < arguments.length; index++) {
const nextSource = arguments[index]
if (nextSource != null) {
// Skip over if undefined or null
for (const nextKey in nextSource) {
// Avoid bugs when hasOwnProperty is shadowed
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey]
}
}
}
}
return to
}
const util = {
toFixed: function toFixed(num, limit) {
limit = limit || 2
if (this.isFloat(num)) {
num = num.toFixed(limit)
}
return num
},
isFloat: function isFloat(num) {
return num % 1 !== 0
},
approximatelyEqual: function approximatelyEqual(num1, num2) {
return Math.abs(num1 - num2) < 1e-10
},
isSameSign: function isSameSign(num1, num2) {
return (
(Math.abs(num1) === num1 && Math.abs(num2) === num2) ||
(Math.abs(num1) !== num1 && Math.abs(num2) !== num2)
)
},
isSameXCoordinateArea: function isSameXCoordinateArea(p1, p2) {
return this.isSameSign(p1.x, p2.x)
},
isCollision: function isCollision(obj1, obj2) {
obj1.end = {}
obj1.end.x = obj1.start.x + obj1.width
obj1.end.y = obj1.start.y - obj1.height
obj2.end = {}
obj2.end.x = obj2.start.x + obj2.width
obj2.end.y = obj2.start.y - obj2.height
const flag =
obj2.start.x > obj1.end.x ||
obj2.end.x < obj1.start.x ||
obj2.end.y > obj1.start.y ||
obj2.start.y < obj1.end.y
return !flag
},
}
function findRange(num, type, limit) {
if (isNaN(num)) {
throw new Error('[wxCharts] unvalid series data!')
}
limit = limit || 10
type = type ? type : 'upper'
let multiple = 1
while (limit < 1) {
limit *= 10
multiple *= 10
}
if (type === 'upper') {
num = Math.ceil(num * multiple)
} else {
num = Math.floor(num * multiple)
}
while (num % limit !== 0) {
if (type === 'upper') {
num++
} else {
num--
}
}
return num / multiple
}
function calValidDistance(distance, chartData, config, opts) {
const dataChartAreaWidth =
opts.width - config.padding - chartData.xAxisPoints[0]
const dataChartWidth = chartData.eachSpacing * opts.categories.length
let validDistance = distance
if (distance >= 0) {
validDistance = 0
} else if (Math.abs(distance) >= dataChartWidth - dataChartAreaWidth) {
validDistance = dataChartAreaWidth - dataChartWidth
}
return validDistance
}
function isInAngleRange(angle, startAngle, endAngle) {
function adjust(angle) {
while (angle < 0) {
angle += 2 * Math.PI
}
while (angle > 2 * Math.PI) {
angle -= 2 * Math.PI
}
return angle
}
angle = adjust(angle)
startAngle = adjust(startAngle)
endAngle = adjust(endAngle)
if (startAngle > endAngle) {
endAngle += 2 * Math.PI
if (angle < startAngle) {
angle += 2 * Math.PI
}
}
return angle >= startAngle && angle <= endAngle
}
function calRotateTranslate(x, y, h) {
const xv = x
const yv = h - y
let transX = xv + (h - yv - xv) / Math.sqrt(2)
transX *= -1
const transY = (h - yv) * (Math.sqrt(2) - 1) - (h - yv - xv) / Math.sqrt(2)
return {
transX,
transY,
}
}
function createCurveControlPoints(points, i) {
function isNotMiddlePoint(points, i) {
if (points[i - 1] && points[i + 1]) {
return (
points[i].y >= Math.max(points[i - 1].y, points[i + 1].y) ||
points[i].y <= Math.min(points[i - 1].y, points[i + 1].y)
)
} else {
return false
}
}
const a = 0.2
const b = 0.2
let pAx = null
let pAy = null
let pBx = null
let pBy = null
if (i < 1) {
pAx = points[0].x + (points[1].x - points[0].x) * a
pAy = points[0].y + (points[1].y - points[0].y) * a
} else {
pAx = points[i].x + (points[i + 1].x - points[i - 1].x) * a
pAy = points[i].y + (points[i + 1].y - points[i - 1].y) * a
}
if (i > points.length - 3) {
const last = points.length - 1
pBx = points[last].x - (points[last].x - points[last - 1].x) * b
pBy = points[last].y - (points[last].y - points[last - 1].y) * b
} else {
pBx = points[i + 1].x - (points[i + 2].x - points[i].x) * b
pBy = points[i + 1].y - (points[i + 2].y - points[i].y) * b
}
// fix issue https://github.com/xiaolin3303/wx-charts/issues/79
if (isNotMiddlePoint(points, i + 1)) {
pBy = points[i + 1].y
}
if (isNotMiddlePoint(points, i)) {
pAy = points[i].y
}
return {
ctrA: { x: pAx, y: pAy },
ctrB: { x: pBx, y: pBy },
}
}
function convertCoordinateOrigin(x, y, center) {
return {
x: center.x + x,
y: center.y - y,
}
}
function avoidCollision(obj, target) {
if (target) {
// is collision test
while (util.isCollision(obj, target)) {
if (obj.start.x > 0) {
obj.start.y--
} else if (obj.start.x < 0) {
obj.start.y++
} else if (obj.start.y > 0) {
obj.start.y++
} else {
obj.start.y--
}
}
}
return obj
}
function fillSeriesColor(series, config) {
let index = 0
return series.map(item => {
if (!item.color) {
item.color = config.colors[index]
index = (index + 1) % config.colors.length
}
return item
})
}
function getDataRange(minData, maxData) {
let limit = 0
const range = maxData - minData
if (range >= 10000) {
limit = 1000
} else if (range >= 1000) {
limit = 100
} else if (range >= 100) {
limit = 10
} else if (range >= 10) {
limit = 5
} else if (range >= 1) {
limit = 1
} else if (range >= 0.1) {
limit = 0.1
} else {
limit = 0.01
}
return {
minRange: findRange(minData, 'lower', limit),
maxRange: findRange(maxData, 'upper', limit),
}
}
function measureText(text) {
const fontSize =
arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 10
// wx canvas 未实现measureText方法, 此处自行实现
text = String(text)
var text = text.split('')
let width = 0
text.forEach(item => {
if (/[a-zA-Z]/.test(item)) {
width += 7
} else if (/[0-9]/.test(item)) {
width += 5.5
} else if (/\./.test(item)) {
width += 2.7
} else if (/-/.test(item)) {
width += 3.25
} else if (/[\u4e00-\u9fa5]/.test(item)) {
width += 10
} else if (/\(|\)/.test(item)) {
width += 3.73
} else if (/\s/.test(item)) {
width += 2.5
} else if (/%/.test(item)) {
width += 8
} else {
width += 10
}
})
return width * fontSize / 10
}
function dataCombine(series) {
return series.reduce((a, b) => (a.data ? a.data : a).concat(b.data), [])
}
function getSeriesDataItem(series, index) {
const data = []
series.forEach(item => {
if (item.data[index] !== null && typeof item.data[index] !== 'undefined') {
const seriesItem = {}
seriesItem.color = item.color
seriesItem.name = item.name
seriesItem.data = item.format
? item.format(item.data[index])
: item.data[index]
data.push(seriesItem)
}
})
return data
}
function getMaxTextListLength(list) {
const lengthList = list.map(item => measureText(item))
return Math.max.apply(null, lengthList)
}
function getRadarCoordinateSeries(length) {
const eachAngle = 2 * Math.PI / length
const CoordinateSeries = []
for (let i = 0; i < length; i++) {
CoordinateSeries.push(eachAngle * i)
}
return CoordinateSeries.map(item => -1 * item + Math.PI / 2)
}
function getToolTipData(seriesData, calPoints, index, categories) {
const option =
arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {}
const textList = seriesData.map(item => ({
text: option.format
? option.format(item, categories[index])
: `${item.name}: ${item.data}`,
color: item.color,
}))
const validCalPoints = []
const offset = {
x: 0,
y: 0,
}
calPoints.forEach(points => {
if (typeof points[index] !== 'undefined' && points[index] !== null) {
validCalPoints.push(points[index])
}
})
validCalPoints.forEach(item => {
offset.x = Math.round(item.x)
offset.y += item.y
})
offset.y /= validCalPoints.length
return { textList, offset }
}
function findCurrentIndex(currentPoints, xAxisPoints, opts, config) {
const offset =
arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 0
let currentIndex = -1
if (isInExactChartArea(currentPoints, opts, config)) {
xAxisPoints.forEach((item, index) => {
if (currentPoints.x + offset > item) {
currentIndex = index
}
})
}
return currentIndex
}
function isInExactChartArea(currentPoints, opts, config) {
return (
currentPoints.x < opts.width - config.padding &&
currentPoints.x >
config.padding + config.yAxisWidth + config.yAxisTitleWidth &&
currentPoints.y > config.padding &&
currentPoints.y <
opts.height - config.legendHeight - config.xAxisHeight - config.padding
)
}
function findRadarChartCurrentIndex(currentPoints, radarData, count) {
const eachAngleArea = 2 * Math.PI / count
let currentIndex = -1
if (
isInExactPieChartArea(currentPoints, radarData.center, radarData.radius)
) {
const fixAngle = function fixAngle(angle) {
if (angle < 0) {
angle += 2 * Math.PI
}
if (angle > 2 * Math.PI) {
angle -= 2 * Math.PI
}
return angle
}
let angle = Math.atan2(
radarData.center.y - currentPoints.y,
currentPoints.x - radarData.center.x,
)
angle = -1 * angle
if (angle < 0) {
angle += 2 * Math.PI
}
const angleList = radarData.angleList.map(item => {
item = fixAngle(-1 * item)
return item
})
angleList.forEach((item, index) => {
const rangeStart = fixAngle(item - eachAngleArea / 2)
let rangeEnd = fixAngle(item + eachAngleArea / 2)
if (rangeEnd < rangeStart) {
rangeEnd += 2 * Math.PI
}
if (
(angle >= rangeStart && angle <= rangeEnd) ||
(angle + 2 * Math.PI >= rangeStart && angle + 2 * Math.PI <= rangeEnd)
) {
currentIndex = index
}
})
}
return currentIndex
}
function findPieChartCurrentIndex(currentPoints, pieData) {
let currentIndex = -1
if (isInExactPieChartArea(currentPoints, pieData.center, pieData.radius)) {
let angle = Math.atan2(
pieData.center.y - currentPoints.y,
currentPoints.x - pieData.center.x,
)
angle = -angle
for (let i = 0, len = pieData.series.length; i < len; i++) {
const item = pieData.series[i]
if (
isInAngleRange(
angle,
item._start_,
item._start_ + item._proportion_ * 2 * Math.PI,
)
) {
currentIndex = i
break
}
}
}
return currentIndex
}
function isInExactPieChartArea(currentPoints, center, radius) {
return (
Math.pow(currentPoints.x - center.x, 2) +
Math.pow(currentPoints.y - center.y, 2) <=
Math.pow(radius, 2)
)
}
function splitPoints(points) {
const newPoints = []
let items = []
points.forEach((item, index) => {
if (item !== null) {
items.push(item)
} else {
if (items.length) {
newPoints.push(items)
}
items = []
}
})
if (items.length) {
newPoints.push(items)
}
return newPoints
}
function calLegendData(series, opts, config) {
if (opts.legend === false) {
return {
legendList: [],
legendHeight: 0,
}
}
const padding = 5
const marginTop = 8
const shapeWidth = 15
const legendList = []
let widthCount = 0
let currentRow = []
series.forEach(item => {
const itemWidth =
3 * padding + shapeWidth + measureText(item.name || 'undefined')
if (widthCount + itemWidth > opts.width) {
legendList.push(currentRow)
widthCount = itemWidth
currentRow = [item]
} else {
widthCount += itemWidth
currentRow.push(item)
}
})
if (currentRow.length) {
legendList.push(currentRow)
}
return {
legendList,
legendHeight: legendList.length * (config.fontSize + marginTop) + padding,
}
}
function calCategoriesData(categories, opts, config) {
const result = {
angle: 0,
xAxisHeight: config.xAxisHeight,
}
let _getXAxisPoints = getXAxisPoints(categories, opts, config),
eachSpacing = _getXAxisPoints.eachSpacing
// get max length of categories text
const categoriesTextLenth = categories.map(item => measureText(item))
const maxTextLength = Math.max.apply(this, categoriesTextLenth)
if (
!opts.xAxis.noRotate &&
maxTextLength + 2 * config.xAxisTextPadding > eachSpacing
) {
result.angle = 45 * Math.PI / 180
result.xAxisHeight =
2 * config.xAxisTextPadding + maxTextLength * Math.sin(result.angle)
}
return result
}
function getRadarDataPoints(angleList, center, radius, series, opts) {
const process =
arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 1
const radarOption = opts.extra.radar || {}
radarOption.max = radarOption.max || 0
const maxData = Math.max(
radarOption.max,
Math.max.apply(null, dataCombine(series)),
)
const data = []
series.forEach(each => {
const listItem = {}
listItem.color = each.color
listItem.data = []
each.data.forEach((item, index) => {
const tmp = {}
tmp.angle = angleList[index]
tmp.proportion = item / maxData
tmp.position = convertCoordinateOrigin(
radius * tmp.proportion * process * Math.cos(tmp.angle),
radius * tmp.proportion * process * Math.sin(tmp.angle),
center,
)
listItem.data.push(tmp)
})
data.push(listItem)
})
return data
}
function getPieDataPoints(series) {
const process =
arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1
let count = 0
let _start_ = 0
series.forEach(item => {
item.data = item.data === null ? 0 : item.data
count += item.data
})
series.forEach(item => {
item.data = item.data === null ? 0 : item.data
item._proportion_ = item.data / count * process
})
series.forEach(item => {
item._start_ = _start_
_start_ += 2 * item._proportion_ * Math.PI
})
return series
}
function getPieTextMaxLength(series) {
series = getPieDataPoints(series)
let maxLength = 0
series.forEach(item => {
const text = item.format
? item.format(Number(item._proportion_.toFixed(2)))
: `${util.toFixed(item._proportion_ * 100)}%`
maxLength = Math.max(maxLength, measureText(text))
})
return maxLength
}
function fixColumeData(points, eachSpacing, columnLen, index, config, opts) {
return points.map(item => {
if (item === null) {
return null
}
item.width = (eachSpacing - 2 * config.columePadding) / columnLen
if (
opts.extra.column &&
opts.extra.column.width &&
Number(opts.extra.column.width) > 0
) {
// customer column width
item.width = Math.min(item.width, Number(opts.extra.column.width))
} else {
// default width should less tran 25px
// don't ask me why, I don't know
item.width = Math.min(item.width, 25)
}
item.x += (index + 0.5 - columnLen / 2) * item.width
return item
})
}
function getXAxisPoints(categories, opts, config) {
const yAxisTotalWidth = config.yAxisWidth + config.yAxisTitleWidth
const spacingValid = opts.justified
? opts.width
: opts.width - 2 * config.padding - yAxisTotalWidth
const dataCount = opts.enableScroll
? Math.min(5, categories.length)
: categories.length
const eachSpacing = spacingValid / dataCount
const xAxisPoints = []
const startX = opts.justified
? config.padding
: config.padding + yAxisTotalWidth
const endX = opts.width - config.padding
categories.forEach((item, index) => {
xAxisPoints.push(startX + index * eachSpacing)
})
if (opts.enableScroll === true) {
xAxisPoints.push(startX + categories.length * eachSpacing)
} else {
xAxisPoints.push(endX)
}
return {
xAxisPoints,
startX,
endX,
eachSpacing,
}
}
function getDataPoints(
data,
minRange,
maxRange,
xAxisPoints,
eachSpacing,
opts,
config,
) {
const process =
arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : 1
const points = []
const validHeight =
opts.height - 2 * config.padding - config.xAxisHeight - config.legendHeight
data.forEach((item, index) => {
if (item === null) {
points.push(null)
} else {
const point = {}
point.x = xAxisPoints[index] + Math.round(eachSpacing / 2)
let height = validHeight * (item - minRange) / (maxRange - minRange)
height *= process
point.y =
opts.height -
config.xAxisHeight -
config.legendHeight -
Math.round(height) -
config.padding
points.push(point)
}
})
return points
}
function getYAxisTextList(series, opts, config) {
let data = dataCombine(series)
// remove null from data
data = data.filter(item => item !== null)
let minData = Math.min.apply(this, data)
let maxData = Math.max.apply(this, data)
if (typeof opts.yAxis.min === 'number') {
minData = Math.min(opts.yAxis.min, minData)
}
if (typeof opts.yAxis.max === 'number') {
maxData = Math.max(opts.yAxis.max, maxData)
}
// fix issue https://github.com/xiaolin3303/wx-charts/issues/9
if (minData === maxData) {
const rangeSpan = maxData || 1
// when all the data is 0 , set minData = 0
if (!maxData) {
minData = 0
maxData = 1
} else {
minData -= rangeSpan
maxData += rangeSpan
}
}
const dataRange = getDataRange(minData, maxData)
const minRange = dataRange.minRange
const maxRange = dataRange.maxRange
const range = []
const eachRange = (maxRange - minRange) / config.yAxisSplit
for (let i = 0; i <= config.yAxisSplit; i++) {
range.push(minRange + eachRange * i)
}
return range.reverse()
}
function calYAxisData(series, opts, config) {
const ranges = getYAxisTextList(series, opts, config)
let yAxisWidth = config.yAxisWidth
const rangesFormat = ranges.map(item => {
item = util.toFixed(item, 2)
item = opts.yAxis.format ? opts.yAxis.format(Number(item)) : item
yAxisWidth = Math.max(yAxisWidth, measureText(item) + 5)
return item
})
if (opts.yAxis.disabled === true) {
yAxisWidth = 0
}
return { rangesFormat, ranges, yAxisWidth }
}
function drawPointShape(points, color, shape, context) {
context.beginPath()
context.setStrokeStyle('#ffffff')
context.setLineWidth(1)
context.setFillStyle(color)
if (shape === 'diamond') {
points.forEach((item, index) => {
if (item !== null) {
context.moveTo(item.x, item.y - 4.5)
context.lineTo(item.x - 4.5, item.y)
context.lineTo(item.x, item.y + 4.5)
context.lineTo(item.x + 4.5, item.y)
context.lineTo(item.x, item.y - 4.5)
}
})
} else if (shape === 'circle') {
points.forEach((item, index) => {
if (item !== null) {
context.moveTo(item.x + 3.5, item.y)
context.arc(item.x, item.y, 4, 0, 2 * Math.PI, false)
}
})
} else if (shape === 'rect') {
points.forEach((item, index) => {
if (item !== null) {
context.moveTo(item.x - 3.5, item.y - 3.5)
context.rect(item.x - 3.5, item.y - 3.5, 7, 7)
}
})
} else if (shape === 'triangle') {
points.forEach((item, index) => {
if (item !== null) {
context.moveTo(item.x, item.y - 4.5)
context.lineTo(item.x - 4.5, item.y + 4.5)
context.lineTo(item.x + 4.5, item.y + 4.5)
context.lineTo(item.x, item.y - 4.5)
}
})
}
context.closePath()
context.fill()
context.stroke()
}
function drawRingTitle(opts, config, context) {
const titlefontSize = opts.title.fontSize || config.titleFontSize
const subtitlefontSize = opts.subtitle.fontSize || config.subtitleFontSize
const title = opts.title.name || ''
const subtitle = opts.subtitle.name || ''
const titleFontColor = opts.title.color || config.titleColor
const subtitleFontColor = opts.subtitle.color || config.subtitleColor
const titleHeight = title ? titlefontSize : 0
const subtitleHeight = subtitle ? subtitlefontSize : 0
const margin = 5
if (subtitle) {
const textWidth = measureText(subtitle, subtitlefontSize)
const startX = (opts.width - textWidth) / 2 + (opts.subtitle.offsetX || 0)
let startY = (opts.height - config.legendHeight + subtitlefontSize) / 2
if (title) {
startY -= (titleHeight + margin) / 2
}
context.beginPath()
context.setFontSize(subtitlefontSize)
context.setFillStyle(subtitleFontColor)
context.fillText(subtitle, startX, startY)
context.stroke()
context.closePath()
}
if (title) {
const _textWidth = measureText(title, titlefontSize)
const _startX = (opts.width - _textWidth) / 2 + (opts.title.offsetX || 0)
let _startY = (opts.height - config.legendHeight + titlefontSize) / 2
if (subtitle) {
_startY += (subtitleHeight + margin) / 2
}
context.beginPath()
context.setFontSize(titlefontSize)
context.setFillStyle(titleFontColor)
context.fillText(title, _startX, _startY)
context.stroke()
context.closePath()
}
}
function drawPointText(points, series, config, context) {
// 绘制数据文案
const data = series.data
context.beginPath()
context.setFontSize(config.fontSize)
context.setFillStyle('#666666')
points.forEach((item, index) => {
if (item !== null) {
const formatVal = series.format ? series.format(data[index]) : data[index]
context.fillText(
formatVal,
item.x - measureText(formatVal) / 2,
item.y - 2,
)
}
})
context.closePath()
context.stroke()
}
function drawRadarLabel(
angleList,
radius,
centerPosition,
opts,
config,
context,
) {
const radarOption = opts.extra.radar || {}
radius += config.radarLabelTextMargin
context.beginPath()
context.setFontSize(config.fontSize)
context.setFillStyle(radarOption.labelColor || '#666666')
angleList.forEach((angle, index) => {
const pos = {
x: radius * Math.cos(angle),
y: radius * Math.sin(angle),
}
const posRelativeCanvas = convertCoordinateOrigin(
pos.x,
pos.y,
centerPosition,
)
let startX = posRelativeCanvas.x
const startY = posRelativeCanvas.y
if (util.approximatelyEqual(pos.x, 0)) {
startX -= measureText(opts.categories[index] || '') / 2
} else if (pos.x < 0) {
startX -= measureText(opts.categories[index] || '')
}
context.fillText(
opts.categories[index] || '',
startX,
startY + config.fontSize / 2,
)
})
context.stroke()
context.closePath()
}
function drawPieText(series, opts, config, context, radius, center) {
const lineRadius = radius + config.pieChartLinePadding
const textObjectCollection = []
let lastTextObject = null
const seriesConvert = series.map(item => {
const arc =
2 * Math.PI - (item._start_ + 2 * Math.PI * item._proportion_ / 2)
const text = item.format
? item.format(Number(item._proportion_.toFixed(2)))
: `${util.toFixed(item._proportion_ * 100)}%`
const color = item.color
return { arc, text, color }
})
seriesConvert.forEach(item => {
// line end
const orginX1 = Math.cos(item.arc) * lineRadius
const orginY1 = Math.sin(item.arc) * lineRadius
// line start
const orginX2 = Math.cos(item.arc) * radius
const orginY2 = Math.sin(item.arc) * radius
// text start
let orginX3 =
orginX1 >= 0
? orginX1 + config.pieChartTextPadding
: orginX1 - config.pieChartTextPadding
const orginY3 = orginY1
const textWidth = measureText(item.text)
let startY = orginY3
if (
lastTextObject &&
util.isSameXCoordinateArea(lastTextObject.start, { x: orginX3 })
) {
if (orginX3 > 0) {
startY = Math.min(orginY3, lastTextObject.start.y)
} else if (orginX1 < 0) {
startY = Math.max(orginY3, lastTextObject.start.y)
} else if (orginY3 > 0) {
startY = Math.max(orginY3, lastTextObject.start.y)
} else {
startY = Math.min(orginY3, lastTextObject.start.y)
}
}
if (orginX3 < 0) {
orginX3 -= textWidth
}
const textObject = {
lineStart: {
x: orginX2,
y: orginY2,
},
lineEnd: {
x: orginX1,
y: orginY1,
},
start: {
x: orginX3,
y: startY,
},
width: textWidth,
height: config.fontSize,
text: item.text,
color: item.color,
}
lastTextObject = avoidCollision(textObject, lastTextObject)
textObjectCollection.push(lastTextObject)
})
textObjectCollection.forEach(item => {
const lineStartPoistion = convertCoordinateOrigin(
item.lineStart.x,
item.lineStart.y,
center,
)
const lineEndPoistion = convertCoordinateOrigin(
item.lineEnd.x,
item.lineEnd.y,
center,
)
const textPosition = convertCoordinateOrigin(
item.start.x,
item.start.y,
center,
)
context.setLineWidth(1)
context.setFontSize(config.fontSize)
context.beginPath()
context.setStrokeStyle(item.color)
context.setFillStyle(item.color)
context.moveTo(lineStartPoistion.x, lineStartPoistion.y)
const curveStartX =
item.start.x < 0 ? textPosition.x + item.width : textPosition.x
const textStartX =
item.start.x < 0 ? textPosition.x - 10 : textPosition.x + 10
context.quadraticCurveTo(
lineEndPoistion.x,
lineEndPoistion.y,
curveStartX,
textPosition.y,
)
context.moveTo(lineStartPoistion.x, lineStartPoistion.y)
context.stroke()
context.closePath()
context.beginPath()
context.moveTo(textPosition.x + item.width, textPosition.y)
context.arc(curveStartX, textPosition.y, 2, 0, 2 * Math.PI)
context.closePath()
context.fill()
context.beginPath()
context.setFontSize(opts.fontSize)
context.setFillStyle(item.color || '#666666')
context.fillText(item.text, textStartX - 6, textPosition.y + 5)
context.closePath()
context.stroke()
context.closePath()
})
}
function drawToolTipSplitLine(offsetX, opts, config, context) {
const startY = config.padding
const endY =
opts.height - config.padding - config.xAxisHeight - config.legendHeight
context.beginPath()
context.setStrokeStyle('#cccccc')
context.setLineWidth(1)
context.moveTo(offsetX, startY)
context.lineTo(offsetX, endY)
context.stroke()
context.closePath()
}
function drawToolTip(textList, offset, opts, config, context) {
const legendWidth = 4
const legendMarginRight = 5
const arrowWidth = 8
let isOverRightBorder = false
offset = assign(
{
x: 0,
y: 0,
},
offset,
)
offset.y -= 8
const textWidth = textList.map(item => measureText(item.text))
const toolTipWidth =
legendWidth +
legendMarginRight +
4 * config.toolTipPadding +
Math.max.apply(null, textWidth)
const toolTipHeight =
2 * config.toolTipPadding + textList.length * config.toolTipLineHeight
// if beyond the right border
if (
offset.x - Math.abs(opts._scrollDistance_) + arrowWidth + toolTipWidth >
opts.width
) {
isOverRightBorder = true
}
// draw background rect
context.beginPath()
// hacker tooltip grd
const grd = context.createLinearGradient(0, 150, 0, 50)
grd.addColorStop(0, config.toolTipBackground)
grd.addColorStop(1, config.toolTipBackground)
context.setFillStyle(grd)
// context.setFillStyle(
// opts.tooltip.option.background || config.toolTipBackground,
// )
context.setGlobalAlpha(config.toolTipOpacity)
if (isOverRightBorder) {
context.moveTo(offset.x, offset.y + 10)
context.lineTo(offset.x - arrowWidth, offset.y + 10 - 5)
context.lineTo(offset.x - arrowWidth, offset.y + 10 + 5)
context.moveTo(offset.x, offset.y + 10)
context.fillRect(
offset.x - toolTipWidth - arrowWidth,
offset.y,
toolTipWidth,
toolTipHeight,
)
} else {
context.moveTo(offset.x, offset.y + 10)
context.lineTo(offset.x + arrowWidth, offset.y + 10 - 5)
context.lineTo(offset.x + arrowWidth, offset.y + 10 + 5)
context.moveTo(offset.x, offset.y + 10)
context.fillRect(
offset.x + arrowWidth,
offset.y,
toolTipWidth,
toolTipHeight,
)
}
context.closePath()
context.fill()
context.setGlobalAlpha(1)
// draw legend
textList.forEach((item, index) => {
context.beginPath()
const tooltipTextGrd = context.createLinearGradient(0, 150, 0, 50)
tooltipTextGrd.addColorStop(0, item.color)
tooltipTextGrd.addColorStop(1, item.color)
context.setFillStyle(tooltipTextGrd)
let startX = offset.x + arrowWidth + 2 * config.toolTipPadding
const startY =
offset.y +
(config.toolTipLineHeight - config.fontSize) / 2 +
config.toolTipLineHeight * index +
config.toolTipPadding
if (isOverRightBorder) {
startX = offset.x - toolTipWidth - arrowWidth + 2 * config.toolTipPadding
}
context.fillRect(startX, startY, legendWidth, config.fontSize)
context.closePath()
})
// draw text list
context.beginPath()
context.setFontSize(config.fontSize)
context.setFillStyle('#ffffff')
textList.forEach((item, index) => {
let startX =
offset.x +
arrowWidth +
2 * config.toolTipPadding +
legendWidth +
legendMarginRight
if (isOverRightBorder) {
startX =
offset.x -
toolTipWidth -
arrowWidth +
2 * config.toolTipPadding +
Number(legendWidth) +
legendMarginRight
}
const startY =
offset.y +
(config.toolTipLineHeight - config.fontSize) / 2 +
config.toolTipLineHeight * index +
config.toolTipPadding
context.fillText(item.text, startX, startY + config.fontSize)
})
context.stroke()
context.closePath()
}
function drawYAxisTitle(title, opts, config, context) {
const startX =
config.xAxisHeight +
(opts.height - config.xAxisHeight - measureText(title)) / 2
context.save()
context.beginPath()
context.setFontSize(config.fontSize)
context.setFillStyle(opts.yAxis.titleFontColor || '#333333')
context.translate(0, opts.height)
context.rotate(-90 * Math.PI / 180)
context.fillText(title, startX, config.padding + 0.5 * config.fontSize)
context.stroke()
context.closePath()
context.restore()
}
function drawColumnDataPoints(series, opts, config, context) {
const process =
arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1
let _calYAxisData = calYAxisData(series, opts, config),
ranges = _calYAxisData.ranges
let _getXAxisPoints = getXAxisPoints(opts.categories, opts, config),
xAxisPoints = _getXAxisPoints.xAxisPoints,
eachSpacing = _getXAxisPoints.eachSpacing
const minRange = ranges.pop()
const maxRange = ranges.shift()
context.save()
if (
opts._scrollDistance_ &&
opts._scrollDistance_ !== 0 &&
opts.enableScroll === true
) {
context.translate(opts._scrollDistance_, 0)
}
series.forEach((eachSeries, seriesIndex) => {
const data = eachSeries.data
let points = getDataPoints(
data,
minRange,
maxRange,
xAxisPoints,
eachSpacing,
opts,
config,
process,
)
points = fixColumeData(
points,
eachSpacing,
series.length,
seriesIndex,
config,
opts,
)
// 绘制柱状数据图
context.beginPath()
context.setFillStyle(eachSeries.color)
points.forEach((item, index) => {
if (item !== null) {
const startX = item.x - item.width / 2 + 1
const height =
opts.height -
item.y -
config.padding -
config.xAxisHeight -
config.legendHeight
context.moveTo(startX, item.y)
context.rect(startX, item.y, item.width - 2, height)
}
})
context.closePath()
context.fill()
})
series.forEach((eachSeries, seriesIndex) => {
const data = eachSeries.data
let points = getDataPoints(
data,
minRange,
maxRange,
xAxisPoints,
eachSpacing,
opts,
config,
process,
)
points = fixColumeData(
points,
eachSpacing,
series.length,
seriesIndex,
config,
opts,
)
if (opts.dataLabel !== false && process === 1) {
drawPointText(points, eachSeries, config, context)
}
})
context.restore()
return {
xAxisPoints,
eachSpacing,
}
}
function drawAreaDataPoints(series, opts, config, context) {
const process =
arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1
let _calYAxisData2 = calYAxisData(series, opts, config),
ranges = _calYAxisData2.ranges
let _getXAxisPoints2 = getXAxisPoints(opts.categories, opts, config),
xAxisPoints = _getXAxisPoints2.xAxisPoints,
eachSpacing = _getXAxisPoints2.eachSpacing
const minRange = ranges.pop()
const maxRange = ranges.shift()
const endY =
opts.height - config.padding - config.xAxisHeight - config.legendHeight
const calPoints = []
context.save()
if (
opts._scrollDistance_ &&
opts._scrollDistance_ !== 0 &&
opts.enableScroll === true
) {
context.translate(opts._scrollDistance_, 0)
}
if (
opts.tooltip &&
opts.tooltip.textList &&
opts.tooltip.textList.length &&
process === 1
) {
drawToolTipSplitLine(opts.tooltip.offset.x, opts, config, context)
}
series.forEach((eachSeries, seriesIndex) => {
const data = eachSeries.data
const points = getDataPoints(
data,
minRange,
maxRange,
xAxisPoints,
eachSpacing,
opts,
config,
process,
)
calPoints.push(points)
const splitPointList = splitPoints(points)
splitPointList.forEach(points => {
// 绘制区域数据
context.beginPath()
context.setStrokeStyle(eachSeries.color)
// 添加线性渐变
if (eachSeries.gradient) {
const grd = context.createLinearGradient(0, 150, 0, 50)
grd.addColorStop(0, eachSeries.color)
grd.addColorStop(1, eachSeries.endColor)
context.setFillStyle(grd)
} else {
context.setFillStyle(eachSeries.color)
}
context.setGlobalAlpha(eachSeries.globalAlpha || 0.6)
context.setLineWidth(2)
if (points.length > 1) {
const firstPoint = points[0]
const lastPoint = points[points.length - 1]
context.moveTo(firstPoint.x, firstPoint.y)
if (opts.extra.lineStyle === 'curve') {
points.forEach((item, index) => {
if (index > 0) {
const ctrlPoint = createCurveControlPoints(points, index - 1)
context.bezierCurveTo(
ctrlPoint.ctrA.x,
ctrlPoint.ctrA.y,
ctrlPoint.ctrB.x,
ctrlPoint.ctrB.y,
item.x,
item.y,
)
}
})
} else {
points.forEach((item, index) => {
if (index > 0) {
context.lineTo(item.x, item.y)
}
})
}
if (eachSeries.outlineColor) {
context.setGlobalAlpha(1)
context.setStrokeStyle(eachSeries.outlineColor)
context.stroke()
context.setGlobalAlpha(eachSeries.globalAlpha || 0.6)
context.setStrokeStyle(eachSeries.color)
}
context.lineTo(lastPoint.x, endY)
context.lineTo(firstPoint.x, endY)
context.lineTo(firstPoint.x, firstPoint.y)
} else {
const item = points[0]
context.moveTo(item.x - eachSpacing / 2, item.y)
context.lineTo(item.x + eachSpacing / 2, item.y)
context.lineTo(item.x + eachSpacing / 2, endY)
context.lineTo(item.x - eachSpacing / 2, endY)
context.moveTo(item.x - eachSpacing / 2, item.y)
}
context.closePath()
context.fill()
context.setGlobalAlpha(1)
// hacker grd
const grd = context.createLinearGradient(0, 150, 0, 50)
grd.addColorStop(0, opts.background)
grd.addColorStop(1, opts.background)
context.setFillStyle(grd)
})
if (opts.dataPointShape !== false) {
const shape =
config.dataPointShape[seriesIndex % config.dataPointShape.length]
drawPointShape(points, eachSeries.color, shape, context)
}
})
if (opts.dataLabel !== false && process === 1) {
series.forEach((eachSeries, seriesIndex) => {
const data = eachSeries.data
const points = getDataPoints(
data,
minRange,
maxRange,
xAxisPoints,
eachSpacing,
opts,
config,
process,
)
drawPointText(points, eachSeries, config, context)
})
}
context.restore()
return {
xAxisPoints,
calPoints,
eachSpacing,
}
}
function drawLineDataPoints(series, opts, config, context) {
const process =
arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1
let _calYAxisData3 = calYAxisData(series, opts, config),
ranges = _calYAxisData3.ranges
let _getXAxisPoints3 = getXAxisPoints(opts.categories, opts, config),
xAxisPoints = _getXAxisPoints3.xAxisPoints,
eachSpacing = _getXAxisPoints3.eachSpacing
const minRange = ranges.pop()
const maxRange = ranges.shift()
const calPoints = []
context.save()
if (
opts._scrollDistance_ &&
opts._scrollDistance_ !== 0 &&
opts.enableScroll === true
) {
context.translate(opts._scrollDistance_, 0)
}
if (
opts.tooltip &&
opts.tooltip.textList &&
opts.tooltip.textList.length &&
process === 1
) {
drawToolTipSplitLine(opts.tooltip.offset.x, opts, config, context)
}
series.forEach((eachSeries, seriesIndex) => {
const data = eachSeries.data
const points = getDataPoints(
data,
minRange,
maxRange,
xAxisPoints,
eachSpacing,
opts,
config,
process,
)
calPoints.push(points)
const splitPointList = splitPoints(points)
splitPointList.forEach((points, index) => {
context.beginPath()
context.setStrokeStyle(eachSeries.color)
context.setLineWidth(2)
if (points.length === 1) {
context.moveTo(points[0].x, points[0].y)
context.arc(points[0].x, points[0].y, 1, 0, 2 * Math.PI)
} else {
context.moveTo(points[0].x, points[0].y)
if (opts.extra.lineStyle === 'curve') {
points.forEach((item, index) => {
if (index > 0) {
const ctrlPoint = createCurveControlPoints(points, index - 1)
context.bezierCurveTo(
ctrlPoint.ctrA.x,
ctrlPoint.ctrA.y,
ctrlPoint.ctrB.x,
ctrlPoint.ctrB.y,
item.x,
item.y,
)
}
})
} else {
points.forEach((item, index) => {
if (index > 0) {
context.lineTo(item.x, item.y)
}
})
}
context.moveTo(points[0].x, points[0].y)
}
context.closePath()
context.stroke()
})
if (opts.dataPointShape !== false) {
const shape =
config.dataPointShape[seriesIndex % config.dataPointShape.length]
drawPointShape(points, eachSeries.color, shape, context)
}
})
if (opts.dataLabel !== false && process === 1) {
series.forEach((eachSeries, seriesIndex) => {
const data = eachSeries.data
const points = getDataPoints(
data,
minRange,
maxRange,
xAxisPoints,
eachSpacing,
opts,
config,
process,
)
drawPointText(points, eachSeries, config, context)
})
}
context.restore()
return {
xAxisPoints,
calPoints,
eachSpacing,
}
}
function drawToolTipBridge(opts, config, context, process) {
context.save()
if (
opts._scrollDistance_ &&
opts._scrollDistance_ !== 0 &&
opts.enableScroll === true
) {
context.translate(opts._scrollDistance_, 0)
}
if (
opts.tooltip &&
opts.tooltip.textList &&
opts.tooltip.textList.length &&
process === 1
) {
drawToolTip(
opts.tooltip.textList,
opts.tooltip.offset,
opts,
config,
context,
)
}
context.restore()
}
function drawXAxis(categories, opts, config, context) {
let _getXAxisPoints4 = getXAxisPoints(categories, opts, config),
xAxisPoints = _getXAxisPoints4.xAxisPoints,
startX = _getXAxisPoints4.startX,
endX = _getXAxisPoints4.endX,
eachSpacing = _getXAxisPoints4.eachSpacing
const startY =
opts.height - config.padding - config.xAxisHeight - config.legendHeight
const endY = startY + config.xAxisLineHeight
context.save()
if (opts._scrollDistance_ && opts._scrollDistance_ !== 0) {
context.translate(opts._scrollDistance_, 0)
}
context.beginPath()
context.setStrokeStyle(opts.xAxis.gridColor || '#cccccc')
if (opts.xAxis.disableGrid !== true) {
if (opts.xAxis.type === 'calibration') {
xAxisPoints.forEach((item, index) => {
if (index > 0) {
context.moveTo(item - eachSpacing / 2, startY)
context.lineTo(item - eachSpacing / 2, startY + 4)
}
})
} else {
xAxisPoints.forEach((item, index) => {
context.moveTo(item, startY)
context.lineTo(item, endY)
})
}
}
context.closePath()
context.stroke()
// 对X轴列表做抽稀处理
const validWidth =
opts.width - 2 * config.padding - config.yAxisWidth - config.yAxisTitleWidth
const maxXAxisListLength = Math.min(
categories.length,
Math.ceil(validWidth / config.fontSize / 1.5),
)
const ratio = Math.ceil(
categories.length /
(opts.xAxis.maxPoints && opts.xAxis.maxPoints < maxXAxisListLength - 1
? opts.xAxis.maxPoints
: maxXAxisListLength),
)
categories = categories.map(
(item, index) =>
index % ratio !== 0 && index !== categories.length - 1 ? '' : item,
)
if (opts.xAxis.noRotate || config._xAxisTextAngle_ === 0) {
context.beginPath()
context.setFontSize(config.fontSize)
context.setFillStyle(opts.xAxis.fontColor || '#666666')
categories.forEach((item, index) => {
const offset =
eachSpacing / 2 -
measureText(item) / 2 -
(index === categories.length - 1 ? 4 : 0)
context.fillText(
item,
xAxisPoints[index] + offset,
startY + config.fontSize + 5,
)
})
context.closePath()
context.stroke()
} else {
categories.forEach((item, index) => {
context.save()
context.beginPath()
context.setFontSize(config.fontSize)
context.setFillStyle(opts.xAxis.fontColor || '#666666')
const textWidth = measureText(item)
const offset = eachSpacing / 2 - textWidth
let _calRotateTranslate = calRotateTranslate(
xAxisPoints[index] + eachSpacing / 2,
startY + config.fontSize / 2 + 5,
opts.height,
),
transX = _calRotateTranslate.transX,
transY = _calRotateTranslate.transY
context.rotate(-1 * config._xAxisTextAngle_)
context.translate(transX, transY)
// 不绘制x轴
if (!opts.xAxis.disabled) {
context.fillText(
item,
xAxisPoints[index] + offset,
startY + config.fontSize + 5,
)
}
context.closePath()
context.stroke()
context.restore()
})
}
context.restore()
}
function drawYAxisGrid(opts, config, context) {
const spacingValid =
opts.height - 2 * config.padding - config.xAxisHeight - config.legendHeight
const eachSpacing = Math.floor(spacingValid / config.yAxisSplit)
const yAxisTotalWidth = config.yAxisWidth + config.yAxisTitleWidth
const startX = config.padding + yAxisTotalWidth
const endX = opts.width - config.padding
const points = []
for (let i = 0; i < config.yAxisSplit; i++) {
points.push(config.padding + eachSpacing * i)
}
points.push(config.padding + eachSpacing * config.yAxisSplit + 2)
context.beginPath()
context.setStrokeStyle(opts.yAxis.gridColor || '#cccccc')
context.setLineWidth(1)
points.forEach((item, index) => {
context.moveTo(startX, item)
context.lineTo(endX, item)
})
context.closePath()
context.stroke()
// to draw the yAxis the first line
context.beginPath()
context.setStrokeStyle(opts.yAxis.gridColor || '#cccccc')
context.moveTo(startX, points[5])
context.lineTo(endX, points[5])
context.closePath()
context.stroke()
}
function drawYAxis(series, opts, config, context) {
if (opts.yAxis.disabled === true) {
return
}
let _calYAxisData4 = calYAxisData(series, opts, config),
rangesFormat = _calYAxisData4.rangesFormat
const yAxisTotalWidth = config.yAxisWidth + config.yAxisTitleWidth
const spacingValid =
opts.height - 2 * config.padding - config.xAxisHeight - config.legendHeight
const eachSpacing = Math.floor(spacingValid / config.yAxisSplit)
const startX = config.padding + yAxisTotalWidth
const endX = opts.width - config.padding
const endY =
opts.height - config.padding - config.xAxisHeight - config.legendHeight
// set YAxis background
context.setFillStyle(opts.background || '#ffffff')
if (opts._scrollDistance_ < 0) {
context.fillRect(0, 0, startX, endY + config.xAxisHeight + 5)
}
context.fillRect(endX, 0, opts.width, endY + config.xAxisHeight + 5)
const points = []
for (let i = 0; i <= config.yAxisSplit; i++) {
points.push(config.padding + eachSpacing * i)
}
context.stroke()
context.beginPath()
context.setFontSize(config.fontSize)
context.setFillStyle(opts.yAxis.fontColor || '#666666')
rangesFormat.forEach((item, index) => {
const pos = points[index] ? points[index] : endY
context.fillText(
item,
config.padding + config.yAxisTitleWidth,
pos + config.fontSize / 2,
)
})
context.closePath()
context.stroke()
if (opts.yAxis.title) {
drawYAxisTitle(opts.yAxis.title, opts, config, context)
}
}
function drawLegend(series, opts, config, context) {
if (!opts.legend) {
return
}
// each legend shape width 15px
// the spacing between shape and text in each legend is the `padding`
// each legend spacing is the `padding`
// legend margin top `config.padding`
let _calLegendData = calLegendData(series, opts, config),
legendList = _calLegendData.legendList
const padding = 5
const marginTop = 8
const shapeWidth = 15
legendList.forEach((itemList, listIndex) => {
let width = 0
itemList.forEach(item => {
item.name = item.name || 'undefined'
width += 3 * padding + measureText(item.name) + shapeWidth
})
let startX = (opts.width - width) / 2 + padding
const startY =
opts.height -
config.padding -
config.legendHeight +
listIndex * (config.fontSize + marginTop) +
padding +
marginTop
context.setFontSize(config.fontSize)
itemList.forEach(item => {
switch (opts.type) {
case 'line':
context.beginPath()
context.setLineWidth(1)
context.setStrokeStyle(item.color)
context.moveTo(startX - 2, startY + 5)
context.lineTo(startX + 17, startY + 5)
context.stroke()
context.closePath()
context.beginPath()
context.setLineWidth(1)
context.setStrokeStyle('#ffffff')
context.setFillStyle(item.color)
context.moveTo(startX + 7.5, startY + 5)
context.arc(startX + 7.5, startY + 5, 4, 0, 2 * Math.PI)
context.fill()
context.stroke()
context.closePath()
break
case 'pie':
case 'ring':
context.beginPath()
context.setFillStyle(item.color)
context.moveTo(startX + 7.5, startY + 5)
context.arc(startX + 7.5, startY + 5, 7, 0, 2 * Math.PI)
context.closePath()
context.fill()
break
default:
context.beginPath()
context.setFillStyle(item.color)
context.moveTo(startX, startY)
context.rect(startX, startY, 15, 10)
context.closePath()
context.fill()
}
startX += padding + shapeWidth
context.beginPath()
context.setFillStyle(opts.extra.legendTextColor || '#333333')
context.fillText(item.name, startX, startY + 9)
context.closePath()
context.stroke()
startX += measureText(item.name) + 2 * padding
})
})
}
function drawPieDataPoints(series, opts, config, context) {
const process =
arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1
const pieOption = opts.extra.pie || {}
series = getPieDataPoints(series, process)
const centerPosition = {
x: opts.width / 2,
y: (opts.height - config.legendHeight) / 2,
}
let radius = Math.min(
centerPosition.x -
config.pieChartLinePadding -
config.pieChartTextPadding -
config._pieTextMaxLength_,
centerPosition.y - config.pieChartLinePadding - config.pieChartTextPadding,
)
if (opts.dataLabel) {
radius -= 10
} else {
radius -= 2 * config.padding
}
series = series.map(eachSeries => {
eachSeries._start_ += (pieOption.offsetAngle || 0) * Math.PI / 180
return eachSeries
})
series.forEach(eachSeries => {
context.beginPath()
context.setLineWidth(2)
context.setStrokeStyle('#ffffff')
context.setFillStyle(eachSeries.color)
context.moveTo(centerPosition.x, centerPosition.y)
context.arc(
centerPosition.x,
centerPosition.y,
radius,
eachSeries._start_,
eachSeries._start_ + 2 * eachSeries._proportion_ * Math.PI,
)
context.closePath()
context.fill()
if (opts.disablePieStroke !== true) {
context.stroke()
}
})
if (opts.type === 'ring') {
let innerPieWidth = radius * 0.6
if (typeof opts.extra.ringWidth === 'number' && opts.extra.ringWidth > 0) {
innerPieWidth = Math.max(0, radius - opts.extra.ringWidth)
}
context.beginPath()
context.setFillStyle(opts.background || '#ffffff')
context.moveTo(centerPosition.x, centerPosition.y)
context.arc(
centerPosition.x,
centerPosition.y,
innerPieWidth,
0,
2 * Math.PI,
)
context.closePath()
context.fill()
}
if (opts.dataLabel !== false && process === 1) {
// fix https://github.com/xiaolin3303/wx-charts/issues/132
let valid = false
for (let i = 0, len = series.length; i < len; i++) {
if (series[i].data > 0) {
valid = true
break
}
}
if (valid) {
drawPieText(series, opts, config, context, radius, centerPosition)
}
}
if (process === 1 && opts.type === 'ring') {
drawRingTitle(opts, config, context)
}
return {
center: centerPosition,
radius,
series,
}
}
function drawRadarDataPoints(series, opts, config, context) {
const process =
arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1
const radarOption = opts.extra.radar || {}
const coordinateAngle = getRadarCoordinateSeries(opts.categories.length)
const centerPosition = {
x: opts.width / 2,
y: (opts.height - config.legendHeight) / 2,
}
let radius = Math.min(
centerPosition.x -
(getMaxTextListLength(opts.categories) + config.radarLabelTextMargin),
centerPosition.y - config.radarLabelTextMargin,
)
radius -= config.padding
// draw grid
context.beginPath()
context.setLineWidth(1)
context.setStrokeStyle(radarOption.gridColor || '#cccccc')
coordinateAngle.forEach(angle => {
const pos = convertCoordinateOrigin(
radius * Math.cos(angle),
radius * Math.sin(angle),
centerPosition,
)
context.moveTo(centerPosition.x, centerPosition.y)
context.lineTo(pos.x, pos.y)
})
context.stroke()
context.closePath()
// draw split line grid
const _loop = function _loop(i) {
let startPos = {}
context.beginPath()
context.setLineWidth(1)
context.setStrokeStyle(radarOption.gridColor || '#cccccc')
coordinateAngle.forEach((angle, index) => {
const pos = convertCoordinateOrigin(
radius / config.radarGridCount * i * Math.cos(angle),
radius / config.radarGridCount * i * Math.sin(angle),
centerPosition,
)
if (index === 0) {
startPos = pos
context.moveTo(pos.x, pos.y)
} else {
context.lineTo(pos.x, pos.y)
}
})
context.lineTo(startPos.x, startPos.y)
context.stroke()
context.closePath()
}
for (let i = 1; i <= config.radarGridCount; i++) {
_loop(i)
}
const radarDataPoints = getRadarDataPoints(
coordinateAngle,
centerPosition,
radius,
series,
opts,
process,
)
radarDataPoints.forEach((eachSeries, seriesIndex) => {
// 绘制区域数据
context.beginPath()
context.setFillStyle(eachSeries.color)
context.setGlobalAlpha(0.6)
eachSeries.data.forEach((item, index) => {
if (index === 0) {
context.moveTo(item.position.x, item.position.y)
} else {
context.lineTo(item.position.x, item.position.y)
}
})
context.closePath()
context.fill()
context.setGlobalAlpha(1)
if (opts.dataPointShape !== false) {
const shape =
config.dataPointShape[seriesIndex % config.dataPointShape.length]
const points = eachSeries.data.map(item => item.position)
drawPointShape(points, eachSeries.color, shape, context)
}
})
// draw label text
drawRadarLabel(coordinateAngle, radius, centerPosition, opts, config, context)
return {
center: centerPosition,
radius,
angleList: coordinateAngle,
}
}
function drawCanvas(opts, context) {
context.draw()
}
const Timing = {
easeIn: function easeIn(pos) {
return Math.pow(pos, 3)
},
easeOut: function easeOut(pos) {
return Math.pow(pos - 1, 3) + 1
},
easeInOut: function easeInOut(pos) {
if ((pos /= 0.5) < 1) {
return 0.5 * Math.pow(pos, 3)
} else {
return 0.5 * (Math.pow(pos - 2, 3) + 2)
}
},
linear: function linear(pos) {
return pos
},
}
function Animation(opts) {
this.isStop = false
opts.duration = typeof opts.duration === 'undefined' ? 1000 : opts.duration
opts.timing = opts.timing || 'linear'
const delay = 17
const createAnimationFrame = function createAnimationFrame() {
if (typeof requestAnimationFrame !== 'undefined') {
return requestAnimationFrame
} else if (typeof setTimeout !== 'undefined') {
return function(step, delay) {
setTimeout(() => {
const timeStamp = Number(new Date())
step(timeStamp)
}, delay)
}
} else {
return function(step) {
step(null)
}
}
}
const animationFrame = createAnimationFrame()
let startTimeStamp = null
var _step = function step(timestamp) {
if (timestamp === null || this.isStop === true) {
opts.onProcess && opts.onProcess(1)
opts.onAnimationFinish && opts.onAnimationFinish()
return
}
if (startTimeStamp === null) {
startTimeStamp = timestamp
}
if (timestamp - startTimeStamp < opts.duration) {
let process = (timestamp - startTimeStamp) / opts.duration
const timingFunction = Timing[opts.timing]
process = timingFunction(process)
opts.onProcess && opts.onProcess(process)
animationFrame(_step, delay)
} else {
opts.onProcess && opts.onProcess(1)
opts.onAnimationFinish && opts.onAnimationFinish()
}
}
_step = _step.bind(this)
animationFrame(_step, delay)
}
// stop animation immediately
// and tigger onAnimationFinish
Animation.prototype.stop = function() {
this.isStop = true
}
function drawCharts(type, opts, config, context) {
// context.setFillStyle(opts.background)
// context.fillRect(0, 0, opts.width, opts.height)
const _this = this
let series = opts.series
const categories = opts.categories
series = fillSeriesColor(series, config)
let _calLegendData = calLegendData(series, opts, config),
legendHeight = _calLegendData.legendHeight
config.legendHeight = legendHeight
let _calYAxisData = calYAxisData(series, opts, config),
yAxisWidth = _calYAxisData.yAxisWidth
config.yAxisWidth = yAxisWidth
if (categories && categories.length) {
let _calCategoriesData = calCategoriesData(categories, opts, config),
xAxisHeight = _calCategoriesData.xAxisHeight,
angle = _calCategoriesData.angle
config.xAxisHeight = xAxisHeight
config._xAxisTextAngle_ = angle
}
if (type === 'pie' || type === 'ring') {
config._pieTextMaxLength_ =
opts.dataLabel === false ? 0 : getPieTextMaxLength(series)
}
const duration = opts.animation ? 1000 : 0
this.animationInstance && this.animationInstance.stop()
switch (type) {
case 'line':
this.animationInstance = new Animation({
timing: 'easeIn',
duration,
onProcess: function onProcess(process) {
drawYAxisGrid(opts, config, context)
let _drawLineDataPoints = drawLineDataPoints(
series,
opts,
config,
context,
process,
),
xAxisPoints = _drawLineDataPoints.xAxisPoints,
calPoints = _drawLineDataPoints.calPoints,
eachSpacing = _drawLineDataPoints.eachSpacing
_this.chartData.xAxisPoints = xAxisPoints
_this.chartData.calPoints = calPoints
_this.chartData.eachSpacing = eachSpacing
// drawXAxis(categories, opts, config, context)
drawLegend(opts.series, opts, config, context)
drawYAxis(series, opts, config, context)
drawToolTipBridge(opts, config, context, process)
drawCanvas(opts, context)
},
onAnimationFinish: function onAnimationFinish() {
_this.event.trigger('renderComplete')
},
})
break
case 'column':
this.animationInstance = new Animation({
timing: 'easeIn',
duration,
onProcess: function onProcess(process) {
drawYAxisGrid(opts, config, context)
let _drawColumnDataPoints = drawColumnDataPoints(
series,
opts,
config,
context,
process,
),
xAxisPoints = _drawColumnDataPoints.xAxisPoints,
eachSpacing = _drawColumnDataPoints.eachSpacing
_this.chartData.xAxisPoints = xAxisPoints
_this.chartData.eachSpacing = eachSpacing
drawXAxis(categories, opts, config, context)
drawLegend(opts.series, opts, config, context)
drawYAxis(series, opts, config, context)
drawCanvas(opts, context)
},
onAnimationFinish: function onAnimationFinish() {
_this.event.trigger('renderComplete')
},
})
break
case 'area':
this.animationInstance = new Animation({
timing: 'easeIn',
duration,
onProcess: function onProcess(process) {
drawYAxisGrid(opts, config, context)
let _drawAreaDataPoints = drawAreaDataPoints(
series,
opts,
config,
context,
process,
),
xAxisPoints = _drawAreaDataPoints.xAxisPoints,
calPoints = _drawAreaDataPoints.calPoints,
eachSpacing = _drawAreaDataPoints.eachSpacing
_this.chartData.xAxisPoints = xAxisPoints
_this.chartData.calPoints = calPoints
_this.chartData.eachSpacing = eachSpacing
drawXAxis(categories, opts, config, context)
drawLegend(opts.series, opts, config, context)
drawYAxis(series, opts, config, context)
drawToolTipBridge(opts, config, context, process)
drawCanvas(opts, context)
},
onAnimationFinish: function onAnimationFinish() {
_this.event.trigger('renderComplete')
},
})
break
case 'ring':
case 'pie':
this.animationInstance = new Animation({
timing: 'easeInOut',
duration,
onProcess: function onProcess(process) {
_this.chartData.pieData = drawPieDataPoints(
series,
opts,
config,
context,
process,
)
drawLegend(opts.series, opts, config, context)
drawCanvas(opts, context)
},
onAnimationFinish: function onAnimationFinish() {
_this.event.trigger('renderComplete')
},
})
break
case 'radar':
this.animationInstance = new Animation({
timing: 'easeInOut',
duration,
onProcess: function onProcess(process) {
_this.chartData.radarData = drawRadarDataPoints(
series,
opts,
config,
context,
process,
)
drawLegend(opts.series, opts, config, context)
drawCanvas(opts, context)
},
onAnimationFinish: function onAnimationFinish() {
_this.event.trigger('renderComplete')
},
})
break
}
}
// simple event implement
function Event() {
this.events = {}
}
Event.prototype.addEventListener = function(type, listener) {
this.events[type] = this.events[type] || []
this.events[type].push(listener)
}
Event.prototype.trigger = function() {
for (
var _len = arguments.length, args = Array(_len), _key = 0;
_key < _len;
_key++
) {
args[_key] = arguments[_key]
}
const type = args[0]
const params = args.slice(1)
if (this.events[type]) {
this.events[type].forEach(listener => {
try {
listener(...params)
} catch (e) {
console.error(e)
}
})
}
}
const Charts = function Charts(opts) {
opts.title = opts.title || {}
opts.subtitle = opts.subtitle || {}
opts.yAxis = opts.yAxis || {}
opts.xAxis = opts.xAxis || {}
opts.extra = opts.extra || {}
opts.legend = opts.legend !== false
opts.animation = opts.animation !== false
const config$$1 = assign({}, config)
config$$1.yAxisTitleWidth =
opts.yAxis.disabled !== true && opts.yAxis.title
? config$$1.yAxisTitleWidth
: 0
config$$1.pieChartLinePadding =
opts.dataLabel === false ? 0 : config$$1.pieChartLinePadding
config$$1.pieChartTextPadding =
opts.dataLabel === false ? 0 : config$$1.pieChartTextPadding
this.opts = opts
this.config = config$$1
this.context = wx.createCanvasContext(opts.canvasId)
// this.context.setFillStyle(grd)
// this.context.fillRect(10, 10, 150, 80)
// store calcuated chart data
// such as chart point coordinate
this.chartData = {}
this.event = new Event()
this.scrollOption = {
currentOffset: 0,
startTouchX: 0,
distance: 0,
}
drawCharts.call(this, opts.type, opts, config$$1, this.context)
}
Charts.prototype.updateData = function() {
const data =
arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}
this.opts.series = data.series || this.opts.series
this.opts.categories = data.categories || this.opts.categories
this.opts.height = data.height || this.opts.height
this.opts.title = assign({}, this.opts.title, data.title || {})
this.opts.subtitle = assign({}, this.opts.subtitle, data.subtitle || {})
drawCharts.call(this, this.opts.type, this.opts, this.config, this.context)
}
Charts.prototype.stopAnimation = function() {
this.animationInstance && this.animationInstance.stop()
}
Charts.prototype.addEventListener = function(type, listener) {
this.event.addEventListener(type, listener)
}
Charts.prototype.getCurrentDataIndex = function(e) {
const touches = e.touches && e.touches.length ? e.touches : e.changedTouches
if (touches && touches.length) {
let _touches$ = touches[0],
x = _touches$.x,
y = _touches$.y
if (this.opts.type === 'pie' || this.opts.type === 'ring') {
return findPieChartCurrentIndex({ x, y }, this.chartData.pieData)
} else if (this.opts.type === 'radar') {
return findRadarChartCurrentIndex(
{ x, y },
this.chartData.radarData,
this.opts.categories.length,
)
} else {
return findCurrentIndex(
{ x, y },
this.chartData.xAxisPoints,
this.opts,
this.config,
Math.abs(this.scrollOption.currentOffset),
)
}
}
return -1
}
Charts.prototype.showToolTip = function(e) {
const option =
arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}
if (this.opts.type === 'line' || this.opts.type === 'area') {
const index = this.getCurrentDataIndex(e)
const currentOffset = this.scrollOption.currentOffset
const opts = assign({}, this.opts, {
_scrollDistance_: currentOffset,
animation: false,
})
if (index > -1) {
const seriesData = getSeriesDataItem(this.opts.series, index)
if (seriesData.length !== 0) {
let _getToolTipData = getToolTipData(
seriesData,
this.chartData.calPoints,
index,
this.opts.categories,
option,
),
textList = _getToolTipData.textList,
offset = _getToolTipData.offset
opts.tooltip = {
textList,
offset,
option,
}
}
}
drawCharts.call(this, opts.type, opts, this.config, this.context)
}
}
Charts.prototype.scrollStart = function(e) {
if (e.touches[0] && this.opts.enableScroll === true) {
this.scrollOption.startTouchX = e.touches[0].x
}
}
Charts.prototype.scroll = function(e) {
// TODO throtting...
if (e.touches[0] && this.opts.enableScroll === true) {
let _distance = e.touches[0].x - this.scrollOption.startTouchX
const currentOffset = this.scrollOption.currentOffset
const validDistance = calValidDistance(
currentOffset + _distance,
this.chartData,
this.config,
this.opts,
)
this.scrollOption.distance = _distance = validDistance - currentOffset
const opts = assign({}, this.opts, {
_scrollDistance_: currentOffset + _distance,
animation: false,
})
drawCharts.call(this, opts.type, opts, this.config, this.context)
}
}
Charts.prototype.scrollEnd = function(e) {
if (this.opts.enableScroll === true) {
let _scrollOption = this.scrollOption,
currentOffset = _scrollOption.currentOffset,
distance = _scrollOption.distance
this.scrollOption.currentOffset = currentOffset + distance
this.scrollOption.distance = 0
}
}
module.exports = Charts
/**
* 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
*/
var __placeImgeUrlHttps = "https";
var __emojisReg = '';
var __emojisBaseSrc = '';
var __emojis = {};
var wxDiscode = require('./wxDiscode.js');
var HTMLParser = require('./htmlparser.js');
// Empty Elements - HTML 5
var empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr");
// Block Elements - HTML 5
var block = makeMap("br,a,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
var inline = makeMap("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)
var closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr");
// Attributes that have their values filled in disabled="disabled"
var fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected");
// Special Elements (can contain anything)
var special = makeMap("wxxxcode-style,script,style,view,scroll-view,block");
function makeMap(str) {
var obj = {}, items = str.split(",");
for (var i = 0; i < items.length; i++)
obj[items[i]] = true;
return obj;
}
function q(v) {
return '"' + v + '"';
}
function removeDOCTYPE(html) {
return html
.replace(/<\?xml.*\?>\n/, '')
.replace(/<.*!doctype.*\>\n/, '')
.replace(/<.*!DOCTYPE.*\>\n/, '');
}
function trimHtml(html) {
return html
.replace(/\r?\n+/g, '')
.replace(/<!--.*?-->/ig, '')
.replace(/\/\*.*?\*\//ig, '')
.replace(/[ ]+</ig, '<')
}
function html2json(html, bindName) {
//处理字符串
html = removeDOCTYPE(html);
html = trimHtml(html);
html = wxDiscode.strDiscode(html);
//生成node节点
var bufArray = [];
var results = {
node: bindName,
nodes: [],
images:[],
imageUrls:[]
};
var index = 0;
HTMLParser(html, {
start: function (tag, attrs, unary) {
//debug(tag, attrs, unary);
// node for this element
var node = {
node: 'element',
tag: tag,
};
if (bufArray.length === 0) {
node.index = index.toString()
index += 1
} else {
var parent = bufArray[0];
if (parent.nodes === undefined) {
parent.nodes = [];
}
node.index = parent.index + '.' + parent.nodes.length
}
if (block[tag]) {
node.tagType = "block";
} else if (inline[tag]) {
node.tagType = "inline";
} else if (closeSelf[tag]) {
node.tagType = "closeSelf";
}
if (attrs.length !== 0) {
node.attr = attrs.reduce(function (pre, attr) {
var name = attr.name;
var value = attr.value;
if (name == 'class') {
console.dir(value);
// value = value.join("")
node.classStr = value;
}
// has multi attibutes
// make it array of attribute
if (name == 'style') {
console.dir(value);
// value = value.join("")
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;
}, {});
}
//对img添加额外数据
if (node.tag === 'img') {
node.imgIndex = results.images.length;
var imgUrl = node.attr.src;
if (imgUrl[0] == '') {
imgUrl.splice(0, 1);
}
imgUrl = wxDiscode.urlToHttpUrl(imgUrl, __placeImgeUrlHttps);
node.attr.src = imgUrl;
node.from = bindName;
results.images.push(node);
results.imageUrls.push(imgUrl);
}
// 处理font标签样式属性
if (node.tag === 'font') {
var fontSize = ['x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', '-webkit-xxx-large'];
var styleAttrs = {
'color': 'color',
'face': 'font-family',
'size': 'font-size'
};
if (!node.attr.style) node.attr.style = [];
if (!node.styleStr) node.styleStr = '';
for (var key in styleAttrs) {
if (node.attr[key]) {
var value = key === 'size' ? fontSize[node.attr[key]-1] : node.attr[key];
node.attr.style.push(styleAttrs[key]);
node.attr.style.push(value);
node.styleStr += styleAttrs[key] + ': ' + value + ';';
}
}
}
//临时记录source资源
if(node.tag === 'source'){
results.source = node.attr.src;
}
if (unary) {
// if this tag dosen't have end tag
// like <img src="hoge.png"/>
// add to parents
var parent = bufArray[0] || results;
if (parent.nodes === undefined) {
parent.nodes = [];
}
parent.nodes.push(node);
} else {
bufArray.unshift(node);
}
},
end: function (tag) {
//debug(tag);
// merge into parent tag
var 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 (bufArray.length === 0) {
results.nodes.push(node);
} else {
var parent = bufArray[0];
if (parent.nodes === undefined) {
parent.nodes = [];
}
parent.nodes.push(node);
}
},
chars: function (text) {
//debug(text);
var node = {
node: 'text',
text: text,
textArray:transEmojiStr(text)
};
if (bufArray.length === 0) {
node.index = index.toString()
index += 1
results.nodes.push(node);
} else {
var parent = bufArray[0];
if (parent.nodes === undefined) {
parent.nodes = [];
}
node.index = parent.index + '.' + parent.nodes.length
parent.nodes.push(node);
}
},
comment: function (text) {
//debug(text);
// var node = {
// node: 'comment',
// text: text,
// };
// var parent = bufArray[0];
// if (parent.nodes === undefined) {
// parent.nodes = [];
// }
// parent.nodes.push(node);
},
});
return results;
};
function transEmojiStr(str){
// var eReg = new RegExp("["+__reg+' '+"]");
// str = str.replace(/\[([^\[\]]+)\]/g,':$1:')
var emojiObjs = [];
//如果正则表达式为空
if(__emojisReg.length == 0 || !__emojis){
var emojiObj = {}
emojiObj.node = "text";
emojiObj.text = str;
array = [emojiObj];
return array;
}
//这个地方需要调整
str = str.replace(/\[([^\[\]]+)\]/g,':$1:')
var eReg = new RegExp("[:]");
var array = str.split(eReg);
for(var i = 0; i < array.length; i++){
var ele = array[i];
var emojiObj = {};
if(__emojis[ele]){
emojiObj.node = "element";
emojiObj.tag = "emoji";
emojiObj.text = __emojis[ele];
emojiObj.baseSrc= __emojisBaseSrc;
}else{
emojiObj.node = "text";
emojiObj.text = ele;
}
emojiObjs.push(emojiObj);
}
return emojiObjs;
}
function emojisInit(reg='',baseSrc="/wxParse/emojis/",emojis){
__emojisReg = reg;
__emojisBaseSrc=baseSrc;
__emojis=emojis;
}
module.exports = {
html2json: html2json,
emojisInit:emojisInit
};
/**
*
* 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
var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/,
endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/,
attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g;
// Empty Elements - HTML 5
var empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr");
// Block Elements - HTML 5
var block = makeMap("a,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
var inline = makeMap("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)
var closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr");
// Attributes that have their values filled in disabled="disabled"
var fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected");
// Special Elements (can contain anything)
var special = makeMap("wxxxcode-style,script,style,view,scroll-view,block");
function HTMLParser(html, handler) {
var index, chars, match, stack = [], last = html;
stack.last = function () {
return this[this.length - 1];
};
while (html) {
chars = true;
// Make sure we're not in a script or style element
if (!stack.last() || !special[stack.last()]) {
// Comment
if (html.indexOf("<!--") == 0) {
index = html.indexOf("-->");
if (index >= 0) {
if (handler.comment)
handler.comment(html.substring(4, index));
html = html.substring(index + 3);
chars = false;
}
// end tag
} else 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("<");
var 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);
}
} else {
html = html.replace(new RegExp("([\\s\\S]*?)<\/" + stack.last() + "[^>]*>"), function (all, text) {
text = text.replace(/<!--([\s\S]*?)-->|<!\[CDATA\[([\s\S]*?)]]>/g, "$1$2");
if (handler.chars)
handler.chars(text);
return "";
});
parseEndTag("", stack.last());
}
if (html == last)
throw "Parse Error: " + html;
last = html;
}
// Clean up any remaining tags
parseEndTag();
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) {
var attrs = [];
rest.replace(attr, function (match, name) {
var value = arguments[2] ? arguments[2] :
arguments[3] ? arguments[3] :
arguments[4] ? arguments[4] :
fillAttrs[name] ? name : "";
attrs.push({
name: name,
value: value,
escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') //"
});
});
if (handler.start) {
handler.start(tagName, attrs, unary);
}
}
}
function parseEndTag(tag, tagName) {
// If no tag name is provided, clean shop
if (!tagName)
var pos = 0;
// Find the closest opened tag of the same type
else {
tagName = tagName.toLowerCase();
for (var pos = stack.length - 1; pos >= 0; pos--)
if (stack[pos] == tagName)
break;
}
if (pos >= 0) {
// Close all the open elements, up the stack
for (var i = stack.length - 1; i >= pos; i--)
if (handler.end)
handler.end(stack[i]);
// Remove the open elements from the stack
stack.length = pos;
}
}
};
function makeMap(str) {
var obj = {}, items = str.split(",");
for (var i = 0; i < items.length; i++)
obj[items[i]] = true;
return obj;
}
module.exports = HTMLParser;
/**
*
* showdown: https://github.com/showdownjs/showdown
*
* 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
*/
function getDefaultOpts(simple) {
'use strict';
var defaultOptions = {
omitExtraWLInCodeBlocks: {
defaultValue: false,
describe: 'Omit the default extra whiteline added to code blocks',
type: 'boolean'
},
noHeaderId: {
defaultValue: false,
describe: 'Turn on/off generated header id',
type: 'boolean'
},
prefixHeaderId: {
defaultValue: false,
describe: 'Specify a prefix to generated header ids',
type: 'string'
},
headerLevelStart: {
defaultValue: false,
describe: 'The header blocks level start',
type: 'integer'
},
parseImgDimensions: {
defaultValue: false,
describe: 'Turn on/off image dimension parsing',
type: 'boolean'
},
simplifiedAutoLink: {
defaultValue: false,
describe: 'Turn on/off GFM autolink style',
type: 'boolean'
},
literalMidWordUnderscores: {
defaultValue: false,
describe: 'Parse midword underscores as literal underscores',
type: 'boolean'
},
strikethrough: {
defaultValue: false,
describe: 'Turn on/off strikethrough support',
type: 'boolean'
},
tables: {
defaultValue: false,
describe: 'Turn on/off tables support',
type: 'boolean'
},
tablesHeaderId: {
defaultValue: false,
describe: 'Add an id to table headers',
type: 'boolean'
},
ghCodeBlocks: {
defaultValue: true,
describe: 'Turn on/off GFM fenced code blocks support',
type: 'boolean'
},
tasklists: {
defaultValue: false,
describe: 'Turn on/off GFM tasklist support',
type: 'boolean'
},
smoothLivePreview: {
defaultValue: false,
describe: 'Prevents weird effects in live previews due to incomplete input',
type: 'boolean'
},
smartIndentationFix: {
defaultValue: false,
description: 'Tries to smartly fix identation in es6 strings',
type: 'boolean'
}
};
if (simple === false) {
return JSON.parse(JSON.stringify(defaultOptions));
}
var ret = {};
for (var opt in defaultOptions) {
if (defaultOptions.hasOwnProperty(opt)) {
ret[opt] = defaultOptions[opt].defaultValue;
}
}
return ret;
}
/**
* Created by Tivie on 06-01-2015.
*/
// Private properties
var showdown = {},
parsers = {},
extensions = {},
globalOptions = getDefaultOpts(true),
flavor = {
github: {
omitExtraWLInCodeBlocks: true,
prefixHeaderId: 'user-content-',
simplifiedAutoLink: true,
literalMidWordUnderscores: true,
strikethrough: true,
tables: true,
tablesHeaderId: true,
ghCodeBlocks: true,
tasklists: true
},
vanilla: getDefaultOpts(true)
};
/**
* helper namespace
* @type {{}}
*/
showdown.helper = {};
/**
* TODO LEGACY SUPPORT CODE
* @type {{}}
*/
showdown.extensions = {};
/**
* Set a global option
* @static
* @param {string} key
* @param {*} value
* @returns {showdown}
*/
showdown.setOption = function (key, value) {
'use strict';
globalOptions[key] = value;
return this;
};
/**
* Get a global option
* @static
* @param {string} key
* @returns {*}
*/
showdown.getOption = function (key) {
'use strict';
return globalOptions[key];
};
/**
* Get the global options
* @static
* @returns {{}}
*/
showdown.getOptions = function () {
'use strict';
return globalOptions;
};
/**
* Reset global options to the default values
* @static
*/
showdown.resetOptions = function () {
'use strict';
globalOptions = getDefaultOpts(true);
};
/**
* Set the flavor showdown should use as default
* @param {string} name
*/
showdown.setFlavor = function (name) {
'use strict';
if (flavor.hasOwnProperty(name)) {
var preset = flavor[name];
for (var option in preset) {
if (preset.hasOwnProperty(option)) {
globalOptions[option] = preset[option];
}
}
}
};
/**
* Get the default options
* @static
* @param {boolean} [simple=true]
* @returns {{}}
*/
showdown.getDefaultOptions = function (simple) {
'use strict';
return getDefaultOpts(simple);
};
/**
* Get or set a subParser
*
* subParser(name) - Get a registered subParser
* subParser(name, func) - Register a subParser
* @static
* @param {string} name
* @param {function} [func]
* @returns {*}
*/
showdown.subParser = function (name, func) {
'use strict';
if (showdown.helper.isString(name)) {
if (typeof func !== 'undefined') {
parsers[name] = func;
} else {
if (parsers.hasOwnProperty(name)) {
return parsers[name];
} else {
throw Error('SubParser named ' + name + ' not registered!');
}
}
}
};
/**
* Gets or registers an extension
* @static
* @param {string} name
* @param {object|function=} ext
* @returns {*}
*/
showdown.extension = function (name, ext) {
'use strict';
if (!showdown.helper.isString(name)) {
throw Error('Extension \'name\' must be a string');
}
name = showdown.helper.stdExtName(name);
// Getter
if (showdown.helper.isUndefined(ext)) {
if (!extensions.hasOwnProperty(name)) {
throw Error('Extension named ' + name + ' is not registered!');
}
return extensions[name];
// Setter
} else {
// Expand extension if it's wrapped in a function
if (typeof ext === 'function') {
ext = ext();
}
// Ensure extension is an array
if (!showdown.helper.isArray(ext)) {
ext = [ext];
}
var validExtension = validate(ext, name);
if (validExtension.valid) {
extensions[name] = ext;
} else {
throw Error(validExtension.error);
}
}
};
/**
* Gets all extensions registered
* @returns {{}}
*/
showdown.getAllExtensions = function () {
'use strict';
return extensions;
};
/**
* Remove an extension
* @param {string} name
*/
showdown.removeExtension = function (name) {
'use strict';
delete extensions[name];
};
/**
* Removes all extensions
*/
showdown.resetExtensions = function () {
'use strict';
extensions = {};
};
/**
* Validate extension
* @param {array} extension
* @param {string} name
* @returns {{valid: boolean, error: string}}
*/
function validate(extension, name) {
'use strict';
var errMsg = (name) ? 'Error in ' + name + ' extension->' : 'Error in unnamed extension',
ret = {
valid: true,
error: ''
};
if (!showdown.helper.isArray(extension)) {
extension = [extension];
}
for (var i = 0; i < extension.length; ++i) {
var baseMsg = errMsg + ' sub-extension ' + i + ': ',
ext = extension[i];
if (typeof ext !== 'object') {
ret.valid = false;
ret.error = baseMsg + 'must be an object, but ' + typeof ext + ' given';
return ret;
}
if (!showdown.helper.isString(ext.type)) {
ret.valid = false;
ret.error = baseMsg + 'property "type" must be a string, but ' + typeof ext.type + ' given';
return ret;
}
var type = ext.type = ext.type.toLowerCase();
// normalize extension type
if (type === 'language') {
type = ext.type = 'lang';
}
if (type === 'html') {
type = ext.type = 'output';
}
if (type !== 'lang' && type !== 'output' && type !== 'listener') {
ret.valid = false;
ret.error = baseMsg + 'type ' + type + ' is not recognized. Valid values: "lang/language", "output/html" or "listener"';
return ret;
}
if (type === 'listener') {
if (showdown.helper.isUndefined(ext.listeners)) {
ret.valid = false;
ret.error = baseMsg + '. Extensions of type "listener" must have a property called "listeners"';
return ret;
}
} else {
if (showdown.helper.isUndefined(ext.filter) && showdown.helper.isUndefined(ext.regex)) {
ret.valid = false;
ret.error = baseMsg + type + ' extensions must define either a "regex" property or a "filter" method';
return ret;
}
}
if (ext.listeners) {
if (typeof ext.listeners !== 'object') {
ret.valid = false;
ret.error = baseMsg + '"listeners" property must be an object but ' + typeof ext.listeners + ' given';
return ret;
}
for (var ln in ext.listeners) {
if (ext.listeners.hasOwnProperty(ln)) {
if (typeof ext.listeners[ln] !== 'function') {
ret.valid = false;
ret.error = baseMsg + '"listeners" property must be an hash of [event name]: [callback]. listeners.' + ln +
' must be a function but ' + typeof ext.listeners[ln] + ' given';
return ret;
}
}
}
}
if (ext.filter) {
if (typeof ext.filter !== 'function') {
ret.valid = false;
ret.error = baseMsg + '"filter" must be a function, but ' + typeof ext.filter + ' given';
return ret;
}
} else if (ext.regex) {
if (showdown.helper.isString(ext.regex)) {
ext.regex = new RegExp(ext.regex, 'g');
}
if (!ext.regex instanceof RegExp) {
ret.valid = false;
ret.error = baseMsg + '"regex" property must either be a string or a RegExp object, but ' + typeof ext.regex + ' given';
return ret;
}
if (showdown.helper.isUndefined(ext.replace)) {
ret.valid = false;
ret.error = baseMsg + '"regex" extensions must implement a replace string or function';
return ret;
}
}
}
return ret;
}
/**
* Validate extension
* @param {object} ext
* @returns {boolean}
*/
showdown.validateExtension = function (ext) {
'use strict';
var validateExtension = validate(ext, null);
if (!validateExtension.valid) {
console.warn(validateExtension.error);
return false;
}
return true;
};
/**
* showdownjs helper functions
*/
if (!showdown.hasOwnProperty('helper')) {
showdown.helper = {};
}
/**
* Check if var is string
* @static
* @param {string} a
* @returns {boolean}
*/
showdown.helper.isString = function isString(a) {
'use strict';
return (typeof a === 'string' || a instanceof String);
};
/**
* Check if var is a function
* @static
* @param {string} a
* @returns {boolean}
*/
showdown.helper.isFunction = function isFunction(a) {
'use strict';
var getType = {};
return a && getType.toString.call(a) === '[object Function]';
};
/**
* ForEach helper function
* @static
* @param {*} obj
* @param {function} callback
*/
showdown.helper.forEach = function forEach(obj, callback) {
'use strict';
if (typeof obj.forEach === 'function') {
obj.forEach(callback);
} else {
for (var i = 0; i < obj.length; i++) {
callback(obj[i], i, obj);
}
}
};
/**
* isArray helper function
* @static
* @param {*} a
* @returns {boolean}
*/
showdown.helper.isArray = function isArray(a) {
'use strict';
return a.constructor === Array;
};
/**
* Check if value is undefined
* @static
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is `undefined`, else `false`.
*/
showdown.helper.isUndefined = function isUndefined(value) {
'use strict';
return typeof value === 'undefined';
};
/**
* Standardidize extension name
* @static
* @param {string} s extension name
* @returns {string}
*/
showdown.helper.stdExtName = function (s) {
'use strict';
return s.replace(/[_-]||\s/g, '').toLowerCase();
};
function escapeCharactersCallback(wholeMatch, m1) {
'use strict';
var charCodeToEscape = m1.charCodeAt(0);
return '~E' + charCodeToEscape + 'E';
}
/**
* Callback used to escape characters when passing through String.replace
* @static
* @param {string} wholeMatch
* @param {string} m1
* @returns {string}
*/
showdown.helper.escapeCharactersCallback = escapeCharactersCallback;
/**
* Escape characters in a string
* @static
* @param {string} text
* @param {string} charsToEscape
* @param {boolean} afterBackslash
* @returns {XML|string|void|*}
*/
showdown.helper.escapeCharacters = function escapeCharacters(text, charsToEscape, afterBackslash) {
'use strict';
// First we have to escape the escape characters so that
// we can build a character class out of them
var regexString = '([' + charsToEscape.replace(/([\[\]\\])/g, '\\$1') + '])';
if (afterBackslash) {
regexString = '\\\\' + regexString;
}
var regex = new RegExp(regexString, 'g');
text = text.replace(regex, escapeCharactersCallback);
return text;
};
var rgxFindMatchPos = function (str, left, right, flags) {
'use strict';
var f = flags || '',
g = f.indexOf('g') > -1,
x = new RegExp(left + '|' + right, 'g' + f.replace(/g/g, '')),
l = new RegExp(left, f.replace(/g/g, '')),
pos = [],
t, s, m, start, end;
do {
t = 0;
while ((m = x.exec(str))) {
if (l.test(m[0])) {
if (!(t++)) {
s = x.lastIndex;
start = s - m[0].length;
}
} else if (t) {
if (!--t) {
end = m.index + m[0].length;
var obj = {
left: {start: start, end: s},
match: {start: s, end: m.index},
right: {start: m.index, end: end},
wholeMatch: {start: start, end: end}
};
pos.push(obj);
if (!g) {
return pos;
}
}
}
}
} while (t && (x.lastIndex = s));
return pos;
};
/**
* matchRecursiveRegExp
*
* (c) 2007 Steven Levithan <stevenlevithan.com>
* MIT License
*
* Accepts a string to postSearch, a left and right format delimiter
* as regex patterns, and optional regex flags. Returns an array
* of matches, allowing nested instances of left/right delimiters.
* Use the "g" flag to return all matches, otherwise only the
* first is returned. Be careful to ensure that the left and
* right format delimiters produce mutually exclusive matches.
* Backreferences are not supported within the right delimiter
* due to how it is internally combined with the left delimiter.
* When matching strings whose format delimiters are unbalanced
* to the left or right, the output is intentionally as a
* conventional regex library with recursion support would
* produce, e.g. "<<x>" and "<x>>" both produce ["x"] when using
* "<" and ">" as the delimiters (both strings contain a single,
* balanced instance of "<x>").
*
* examples:
* matchRecursiveRegExp("test", "\\(", "\\)")
* returns: []
* matchRecursiveRegExp("<t<<e>><s>>t<>", "<", ">", "g")
* returns: ["t<<e>><s>", ""]
* matchRecursiveRegExp("<div id=\"x\">test</div>", "<div\\b[^>]*>", "</div>", "gi")
* returns: ["test"]
*/
showdown.helper.matchRecursiveRegExp = function (str, left, right, flags) {
'use strict';
var matchPos = rgxFindMatchPos (str, left, right, flags),
results = [];
for (var i = 0; i < matchPos.length; ++i) {
results.push([
str.slice(matchPos[i].wholeMatch.start, matchPos[i].wholeMatch.end),
str.slice(matchPos[i].match.start, matchPos[i].match.end),
str.slice(matchPos[i].left.start, matchPos[i].left.end),
str.slice(matchPos[i].right.start, matchPos[i].right.end)
]);
}
return results;
};
/**
*
* @param {string} str
* @param {string|function} replacement
* @param {string} left
* @param {string} right
* @param {string} flags
* @returns {string}
*/
showdown.helper.replaceRecursiveRegExp = function (str, replacement, left, right, flags) {
'use strict';
if (!showdown.helper.isFunction(replacement)) {
var repStr = replacement;
replacement = function () {
return repStr;
};
}
var matchPos = rgxFindMatchPos(str, left, right, flags),
finalStr = str,
lng = matchPos.length;
if (lng > 0) {
var bits = [];
if (matchPos[0].wholeMatch.start !== 0) {
bits.push(str.slice(0, matchPos[0].wholeMatch.start));
}
for (var i = 0; i < lng; ++i) {
bits.push(
replacement(
str.slice(matchPos[i].wholeMatch.start, matchPos[i].wholeMatch.end),
str.slice(matchPos[i].match.start, matchPos[i].match.end),
str.slice(matchPos[i].left.start, matchPos[i].left.end),
str.slice(matchPos[i].right.start, matchPos[i].right.end)
)
);
if (i < lng - 1) {
bits.push(str.slice(matchPos[i].wholeMatch.end, matchPos[i + 1].wholeMatch.start));
}
}
if (matchPos[lng - 1].wholeMatch.end < str.length) {
bits.push(str.slice(matchPos[lng - 1].wholeMatch.end));
}
finalStr = bits.join('');
}
return finalStr;
};
/**
* POLYFILLS
*/
if (showdown.helper.isUndefined(console)) {
console = {
warn: function (msg) {
'use strict';
alert(msg);
},
log: function (msg) {
'use strict';
alert(msg);
},
error: function (msg) {
'use strict';
throw msg;
}
};
}
/**
* Created by Estevao on 31-05-2015.
*/
/**
* Showdown Converter class
* @class
* @param {object} [converterOptions]
* @returns {Converter}
*/
showdown.Converter = function (converterOptions) {
'use strict';
var
/**
* Options used by this converter
* @private
* @type {{}}
*/
options = {},
/**
* Language extensions used by this converter
* @private
* @type {Array}
*/
langExtensions = [],
/**
* Output modifiers extensions used by this converter
* @private
* @type {Array}
*/
outputModifiers = [],
/**
* Event listeners
* @private
* @type {{}}
*/
listeners = {};
_constructor();
/**
* Converter constructor
* @private
*/
function _constructor() {
converterOptions = converterOptions || {};
for (var gOpt in globalOptions) {
if (globalOptions.hasOwnProperty(gOpt)) {
options[gOpt] = globalOptions[gOpt];
}
}
// Merge options
if (typeof converterOptions === 'object') {
for (var opt in converterOptions) {
if (converterOptions.hasOwnProperty(opt)) {
options[opt] = converterOptions[opt];
}
}
} else {
throw Error('Converter expects the passed parameter to be an object, but ' + typeof converterOptions +
' was passed instead.');
}
if (options.extensions) {
showdown.helper.forEach(options.extensions, _parseExtension);
}
}
/**
* Parse extension
* @param {*} ext
* @param {string} [name='']
* @private
*/
function _parseExtension(ext, name) {
name = name || null;
// If it's a string, the extension was previously loaded
if (showdown.helper.isString(ext)) {
ext = showdown.helper.stdExtName(ext);
name = ext;
// LEGACY_SUPPORT CODE
if (showdown.extensions[ext]) {
console.warn('DEPRECATION WARNING: ' + ext + ' is an old extension that uses a deprecated loading method.' +
'Please inform the developer that the extension should be updated!');
legacyExtensionLoading(showdown.extensions[ext], ext);
return;
// END LEGACY SUPPORT CODE
} else if (!showdown.helper.isUndefined(extensions[ext])) {
ext = extensions[ext];
} else {
throw Error('Extension "' + ext + '" could not be loaded. It was either not found or is not a valid extension.');
}
}
if (typeof ext === 'function') {
ext = ext();
}
if (!showdown.helper.isArray(ext)) {
ext = [ext];
}
var validExt = validate(ext, name);
if (!validExt.valid) {
throw Error(validExt.error);
}
for (var i = 0; i < ext.length; ++i) {
switch (ext[i].type) {
case 'lang':
langExtensions.push(ext[i]);
break;
case 'output':
outputModifiers.push(ext[i]);
break;
}
if (ext[i].hasOwnProperty(listeners)) {
for (var ln in ext[i].listeners) {
if (ext[i].listeners.hasOwnProperty(ln)) {
listen(ln, ext[i].listeners[ln]);
}
}
}
}
}
/**
* LEGACY_SUPPORT
* @param {*} ext
* @param {string} name
*/
function legacyExtensionLoading(ext, name) {
if (typeof ext === 'function') {
ext = ext(new showdown.Converter());
}
if (!showdown.helper.isArray(ext)) {
ext = [ext];
}
var valid = validate(ext, name);
if (!valid.valid) {
throw Error(valid.error);
}
for (var i = 0; i < ext.length; ++i) {
switch (ext[i].type) {
case 'lang':
langExtensions.push(ext[i]);
break;
case 'output':
outputModifiers.push(ext[i]);
break;
default:// should never reach here
throw Error('Extension loader error: Type unrecognized!!!');
}
}
}
/**
* Listen to an event
* @param {string} name
* @param {function} callback
*/
function listen(name, callback) {
if (!showdown.helper.isString(name)) {
throw Error('Invalid argument in converter.listen() method: name must be a string, but ' + typeof name + ' given');
}
if (typeof callback !== 'function') {
throw Error('Invalid argument in converter.listen() method: callback must be a function, but ' + typeof callback + ' given');
}
if (!listeners.hasOwnProperty(name)) {
listeners[name] = [];
}
listeners[name].push(callback);
}
function rTrimInputText(text) {
var rsp = text.match(/^\s*/)[0].length,
rgx = new RegExp('^\\s{0,' + rsp + '}', 'gm');
return text.replace(rgx, '');
}
/**
* Dispatch an event
* @private
* @param {string} evtName Event name
* @param {string} text Text
* @param {{}} options Converter Options
* @param {{}} globals
* @returns {string}
*/
this._dispatch = function dispatch (evtName, text, options, globals) {
if (listeners.hasOwnProperty(evtName)) {
for (var ei = 0; ei < listeners[evtName].length; ++ei) {
var nText = listeners[evtName][ei](evtName, text, this, options, globals);
if (nText && typeof nText !== 'undefined') {
text = nText;
}
}
}
return text;
};
/**
* Listen to an event
* @param {string} name
* @param {function} callback
* @returns {showdown.Converter}
*/
this.listen = function (name, callback) {
listen(name, callback);
return this;
};
/**
* Converts a markdown string into HTML
* @param {string} text
* @returns {*}
*/
this.makeHtml = function (text) {
//check if text is not falsy
if (!text) {
return text;
}
var globals = {
gHtmlBlocks: [],
gHtmlMdBlocks: [],
gHtmlSpans: [],
gUrls: {},
gTitles: {},
gDimensions: {},
gListLevel: 0,
hashLinkCounts: {},
langExtensions: langExtensions,
outputModifiers: outputModifiers,
converter: this,
ghCodeBlocks: []
};
// attacklab: Replace ~ with ~T
// This lets us use tilde as an escape char to avoid md5 hashes
// The choice of character is arbitrary; anything that isn't
// magic in Markdown will work.
text = text.replace(/~/g, '~T');
// attacklab: Replace $ with ~D
// RegExp interprets $ as a special character
// when it's in a replacement string
text = text.replace(/\$/g, '~D');
// Standardize line endings
text = text.replace(/\r\n/g, '\n'); // DOS to Unix
text = text.replace(/\r/g, '\n'); // Mac to Unix
if (options.smartIndentationFix) {
text = rTrimInputText(text);
}
// Make sure text begins and ends with a couple of newlines:
//text = '\n\n' + text + '\n\n';
text = text;
// detab
text = showdown.subParser('detab')(text, options, globals);
// stripBlankLines
text = showdown.subParser('stripBlankLines')(text, options, globals);
//run languageExtensions
showdown.helper.forEach(langExtensions, function (ext) {
text = showdown.subParser('runExtension')(ext, text, options, globals);
});
// run the sub parsers
text = showdown.subParser('hashPreCodeTags')(text, options, globals);
text = showdown.subParser('githubCodeBlocks')(text, options, globals);
text = showdown.subParser('hashHTMLBlocks')(text, options, globals);
text = showdown.subParser('hashHTMLSpans')(text, options, globals);
text = showdown.subParser('stripLinkDefinitions')(text, options, globals);
text = showdown.subParser('blockGamut')(text, options, globals);
text = showdown.subParser('unhashHTMLSpans')(text, options, globals);
text = showdown.subParser('unescapeSpecialChars')(text, options, globals);
// attacklab: Restore dollar signs
text = text.replace(/~D/g, '$$');
// attacklab: Restore tildes
text = text.replace(/~T/g, '~');
// Run output modifiers
showdown.helper.forEach(outputModifiers, function (ext) {
text = showdown.subParser('runExtension')(ext, text, options, globals);
});
return text;
};
/**
* Set an option of this Converter instance
* @param {string} key
* @param {*} value
*/
this.setOption = function (key, value) {
options[key] = value;
};
/**
* Get the option of this Converter instance
* @param {string} key
* @returns {*}
*/
this.getOption = function (key) {
return options[key];
};
/**
* Get the options of this Converter instance
* @returns {{}}
*/
this.getOptions = function () {
return options;
};
/**
* Add extension to THIS converter
* @param {{}} extension
* @param {string} [name=null]
*/
this.addExtension = function (extension, name) {
name = name || null;
_parseExtension(extension, name);
};
/**
* Use a global registered extension with THIS converter
* @param {string} extensionName Name of the previously registered extension
*/
this.useExtension = function (extensionName) {
_parseExtension(extensionName);
};
/**
* Set the flavor THIS converter should use
* @param {string} name
*/
this.setFlavor = function (name) {
if (flavor.hasOwnProperty(name)) {
var preset = flavor[name];
for (var option in preset) {
if (preset.hasOwnProperty(option)) {
options[option] = preset[option];
}
}
}
};
/**
* Remove an extension from THIS converter.
* Note: This is a costly operation. It's better to initialize a new converter
* and specify the extensions you wish to use
* @param {Array} extension
*/
this.removeExtension = function (extension) {
if (!showdown.helper.isArray(extension)) {
extension = [extension];
}
for (var a = 0; a < extension.length; ++a) {
var ext = extension[a];
for (var i = 0; i < langExtensions.length; ++i) {
if (langExtensions[i] === ext) {
langExtensions[i].splice(i, 1);
}
}
for (var ii = 0; ii < outputModifiers.length; ++i) {
if (outputModifiers[ii] === ext) {
outputModifiers[ii].splice(i, 1);
}
}
}
};
/**
* Get all extension of THIS converter
* @returns {{language: Array, output: Array}}
*/
this.getAllExtensions = function () {
return {
language: langExtensions,
output: outputModifiers
};
};
};
/**
* Turn Markdown link shortcuts into XHTML <a> tags.
*/
showdown.subParser('anchors', function (text, options, globals) {
'use strict';
text = globals.converter._dispatch('anchors.before', text, options, globals);
var writeAnchorTag = function (wholeMatch, m1, m2, m3, m4, m5, m6, m7) {
if (showdown.helper.isUndefined(m7)) {
m7 = '';
}
wholeMatch = m1;
var linkText = m2,
linkId = m3.toLowerCase(),
url = m4,
title = m7;
if (!url) {
if (!linkId) {
// lower-case and turn embedded newlines into spaces
linkId = linkText.toLowerCase().replace(/ ?\n/g, ' ');
}
url = '#' + linkId;
if (!showdown.helper.isUndefined(globals.gUrls[linkId])) {
url = globals.gUrls[linkId];
if (!showdown.helper.isUndefined(globals.gTitles[linkId])) {
title = globals.gTitles[linkId];
}
} else {
if (wholeMatch.search(/\(\s*\)$/m) > -1) {
// Special case for explicit empty url
url = '';
} else {
return wholeMatch;
}
}
}
url = showdown.helper.escapeCharacters(url, '*_', false);
var result = '<a href="' + url + '"';
if (title !== '' && title !== null) {
title = title.replace(/"/g, '&quot;');
title = showdown.helper.escapeCharacters(title, '*_', false);
result += ' title="' + title + '"';
}
result += '>' + linkText + '</a>';
return result;
};
// First, handle reference-style links: [link text] [id]
/*
text = text.replace(/
( // wrap whole match in $1
\[
(
(?:
\[[^\]]*\] // allow brackets nested one level
|
[^\[] // or anything else
)*
)
\]
[ ]? // one optional space
(?:\n[ ]*)? // one optional newline followed by spaces
\[
(.*?) // id = $3
\]
)()()()() // pad remaining backreferences
/g,_DoAnchors_callback);
*/
text = text.replace(/(\[((?:\[[^\]]*]|[^\[\]])*)][ ]?(?:\n[ ]*)?\[(.*?)])()()()()/g, writeAnchorTag);
//
// Next, inline-style links: [link text](url "optional title")
//
/*
text = text.replace(/
( // wrap whole match in $1
\[
(
(?:
\[[^\]]*\] // allow brackets nested one level
|
[^\[\]] // or anything else
)
)
\]
\( // literal paren
[ \t]*
() // no id, so leave $3 empty
<?(.*?)>? // href = $4
[ \t]*
( // $5
(['"]) // quote char = $6
(.*?) // Title = $7
\6 // matching quote
[ \t]* // ignore any spaces/tabs between closing quote and )
)? // title is optional
\)
)
/g,writeAnchorTag);
*/
text = text.replace(/(\[((?:\[[^\]]*]|[^\[\]])*)]\([ \t]*()<?(.*?(?:\(.*?\).*?)?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,
writeAnchorTag);
//
// Last, handle reference-style shortcuts: [link text]
// These must come last in case you've also got [link test][1]
// or [link test](/foo)
//
/*
text = text.replace(/
( // wrap whole match in $1
\[
([^\[\]]+) // link text = $2; can't contain '[' or ']'
\]
)()()()()() // pad rest of backreferences
/g, writeAnchorTag);
*/
text = text.replace(/(\[([^\[\]]+)])()()()()()/g, writeAnchorTag);
text = globals.converter._dispatch('anchors.after', text, options, globals);
return text;
});
showdown.subParser('autoLinks', function (text, options, globals) {
'use strict';
text = globals.converter._dispatch('autoLinks.before', text, options, globals);
var simpleURLRegex = /\b(((https?|ftp|dict):\/\/|www\.)[^'">\s]+\.[^'">\s]+)(?=\s|$)(?!["<>])/gi,
delimUrlRegex = /<(((https?|ftp|dict):\/\/|www\.)[^'">\s]+)>/gi,
simpleMailRegex = /(?:^|[ \n\t])([A-Za-z0-9!#$%&'*+-/=?^_`\{|}~\.]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)(?:$|[ \n\t])/gi,
delimMailRegex = /<(?:mailto:)?([-.\w]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi;
text = text.replace(delimUrlRegex, replaceLink);
text = text.replace(delimMailRegex, replaceMail);
// simpleURLRegex = /\b(((https?|ftp|dict):\/\/|www\.)[-.+~:?#@!$&'()*,;=[\]\w]+)\b/gi,
// Email addresses: <address@domain.foo>
if (options.simplifiedAutoLink) {
text = text.replace(simpleURLRegex, replaceLink);
text = text.replace(simpleMailRegex, replaceMail);
}
function replaceLink(wm, link) {
var lnkTxt = link;
if (/^www\./i.test(link)) {
link = link.replace(/^www\./i, 'http://www.');
}
return '<a href="' + link + '">' + lnkTxt + '</a>';
}
function replaceMail(wholeMatch, m1) {
var unescapedStr = showdown.subParser('unescapeSpecialChars')(m1);
return showdown.subParser('encodeEmailAddress')(unescapedStr);
}
text = globals.converter._dispatch('autoLinks.after', text, options, globals);
return text;
});
/**
* These are all the transformations that form block-level
* tags like paragraphs, headers, and list items.
*/
showdown.subParser('blockGamut', function (text, options, globals) {
'use strict';
text = globals.converter._dispatch('blockGamut.before', text, options, globals);
// we parse blockquotes first so that we can have headings and hrs
// inside blockquotes
text = showdown.subParser('blockQuotes')(text, options, globals);
text = showdown.subParser('headers')(text, options, globals);
// Do Horizontal Rules:
var key = showdown.subParser('hashBlock')('<hr />', options, globals);
text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm, key);
text = text.replace(/^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$/gm, key);
text = text.replace(/^[ ]{0,2}([ ]?_[ ]?){3,}[ \t]*$/gm, key);
text = showdown.subParser('lists')(text, options, globals);
text = showdown.subParser('codeBlocks')(text, options, globals);
text = showdown.subParser('tables')(text, options, globals);
// We already ran _HashHTMLBlocks() before, in Markdown(), but that
// was to escape raw HTML in the original Markdown source. This time,
// we're escaping the markup we've just created, so that we don't wrap
// <p> tags around block-level tags.
text = showdown.subParser('hashHTMLBlocks')(text, options, globals);
text = showdown.subParser('paragraphs')(text, options, globals);
text = globals.converter._dispatch('blockGamut.after', text, options, globals);
return text;
});
showdown.subParser('blockQuotes', function (text, options, globals) {
'use strict';
text = globals.converter._dispatch('blockQuotes.before', text, options, globals);
/*
text = text.replace(/
( // Wrap whole match in $1
(
^[ \t]*>[ \t]? // '>' at the start of a line
.+\n // rest of the first line
(.+\n)* // subsequent consecutive lines
\n* // blanks
)+
)
/gm, function(){...});
*/
text = text.replace(/((^[ \t]{0,3}>[ \t]?.+\n(.+\n)*\n*)+)/gm, function (wholeMatch, m1) {
var bq = m1;
// attacklab: hack around Konqueror 3.5.4 bug:
// "----------bug".replace(/^-/g,"") == "bug"
bq = bq.replace(/^[ \t]*>[ \t]?/gm, '~0'); // trim one level of quoting
// attacklab: clean up hack
bq = bq.replace(/~0/g, '');
bq = bq.replace(/^[ \t]+$/gm, ''); // trim whitespace-only lines
bq = showdown.subParser('githubCodeBlocks')(bq, options, globals);
bq = showdown.subParser('blockGamut')(bq, options, globals); // recurse
bq = bq.replace(/(^|\n)/g, '$1 ');
// These leading spaces screw with <pre> content, so we need to fix that:
bq = bq.replace(/(\s*<pre>[^\r]+?<\/pre>)/gm, function (wholeMatch, m1) {
var pre = m1;
// attacklab: hack around Konqueror 3.5.4 bug:
pre = pre.replace(/^ /mg, '~0');
pre = pre.replace(/~0/g, '');
return pre;
});
return showdown.subParser('hashBlock')('<blockquote>\n' + bq + '\n</blockquote>', options, globals);
});
text = globals.converter._dispatch('blockQuotes.after', text, options, globals);
return text;
});
/**
* Process Markdown `<pre><code>` blocks.
*/
showdown.subParser('codeBlocks', function (text, options, globals) {
'use strict';
text = globals.converter._dispatch('codeBlocks.before', text, options, globals);
/*
text = text.replace(text,
/(?:\n\n|^)
( // $1 = the code block -- one or more lines, starting with a space/tab
(?:
(?:[ ]{4}|\t) // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
.*\n+
)+
)
(\n*[ ]{0,3}[^ \t\n]|(?=~0)) // attacklab: g_tab_width
/g,function(){...});
*/
// attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
text += '~0';
var pattern = /(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g;
text = text.replace(pattern, function (wholeMatch, m1, m2) {
var codeblock = m1,
nextChar = m2,
end = '\n';
codeblock = showdown.subParser('outdent')(codeblock);
codeblock = showdown.subParser('encodeCode')(codeblock);
codeblock = showdown.subParser('detab')(codeblock);
codeblock = codeblock.replace(/^\n+/g, ''); // trim leading newlines
codeblock = codeblock.replace(/\n+$/g, ''); // trim trailing newlines
if (options.omitExtraWLInCodeBlocks) {
end = '';
}
codeblock = '<pre><code>' + codeblock + end + '</code></pre>';
return showdown.subParser('hashBlock')(codeblock, options, globals) + nextChar;
});
// attacklab: strip sentinel
text = text.replace(/~0/, '');
text = globals.converter._dispatch('codeBlocks.after', text, options, globals);
return text;
});
/**
*
* * Backtick quotes are used for <code></code> spans.
*
* * You can use multiple backticks as the delimiters if you want to
* include literal backticks in the code span. So, this input:
*
* Just type ``foo `bar` baz`` at the prompt.
*
* Will translate to:
*
* <p>Just type <code>foo `bar` baz</code> at the prompt.</p>
*
* There's no arbitrary limit to the number of backticks you
* can use as delimters. If you need three consecutive backticks
* in your code, use four for delimiters, etc.
*
* * You can use spaces to get literal backticks at the edges:
*
* ... type `` `bar` `` ...
*
* Turns to:
*
* ... type <code>`bar`</code> ...
*/
showdown.subParser('codeSpans', function (text, options, globals) {
'use strict';
text = globals.converter._dispatch('codeSpans.before', text, options, globals);
/*
text = text.replace(/
(^|[^\\]) // Character before opening ` can't be a backslash
(`+) // $2 = Opening run of `
( // $3 = The code block
[^\r]*?
[^`] // attacklab: work around lack of lookbehind
)
\2 // Matching closer
(?!`)
/gm, function(){...});
*/
if (typeof(text) === 'undefined') {
text = '';
}
text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,
function (wholeMatch, m1, m2, m3) {
var c = m3;
c = c.replace(/^([ \t]*)/g, ''); // leading whitespace
c = c.replace(/[ \t]*$/g, ''); // trailing whitespace
c = showdown.subParser('encodeCode')(c);
return m1 + '<code>' + c + '</code>';
}
);
text = globals.converter._dispatch('codeSpans.after', text, options, globals);
return text;
});
/**
* Convert all tabs to spaces
*/
showdown.subParser('detab', function (text) {
'use strict';
// expand first n-1 tabs
text = text.replace(/\t(?=\t)/g, ' '); // g_tab_width
// replace the nth with two sentinels
text = text.replace(/\t/g, '~A~B');
// use the sentinel to anchor our regex so it doesn't explode
text = text.replace(/~B(.+?)~A/g, function (wholeMatch, m1) {
var leadingText = m1,
numSpaces = 4 - leadingText.length % 4; // g_tab_width
// there *must* be a better way to do this:
for (var i = 0; i < numSpaces; i++) {
leadingText += ' ';
}
return leadingText;
});
// clean up sentinels
text = text.replace(/~A/g, ' '); // g_tab_width
text = text.replace(/~B/g, '');
return text;
});
/**
* Smart processing for ampersands and angle brackets that need to be encoded.
*/
showdown.subParser('encodeAmpsAndAngles', function (text) {
'use strict';
// Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:
// http://bumppo.net/projects/amputator/
text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g, '&amp;');
// Encode naked <'s
text = text.replace(/<(?![a-z\/?\$!])/gi, '&lt;');
return text;
});
/**
* Returns the string, with after processing the following backslash escape sequences.
*
* attacklab: The polite way to do this is with the new escapeCharacters() function:
*
* text = escapeCharacters(text,"\\",true);
* text = escapeCharacters(text,"`*_{}[]()>#+-.!",true);
*
* ...but we're sidestepping its use of the (slow) RegExp constructor
* as an optimization for Firefox. This function gets called a LOT.
*/
showdown.subParser('encodeBackslashEscapes', function (text) {
'use strict';
text = text.replace(/\\(\\)/g, showdown.helper.escapeCharactersCallback);
text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g, showdown.helper.escapeCharactersCallback);
return text;
});
/**
* Encode/escape certain characters inside Markdown code runs.
* The point is that in code, these characters are literals,
* and lose their special Markdown meanings.
*/
showdown.subParser('encodeCode', function (text) {
'use strict';
// Encode all ampersands; HTML entities are not
// entities within a Markdown code span.
text = text.replace(/&/g, '&amp;');
// Do the angle bracket song and dance:
text = text.replace(/</g, '&lt;');
text = text.replace(/>/g, '&gt;');
// Now, escape characters that are magic in Markdown:
text = showdown.helper.escapeCharacters(text, '*_{}[]\\', false);
// jj the line above breaks this:
//---
//* Item
// 1. Subitem
// special char: *
// ---
return text;
});
/**
* Input: an email address, e.g. "foo@example.com"
*
* Output: the email address as a mailto link, with each character
* of the address encoded as either a decimal or hex entity, in
* the hopes of foiling most address harvesting spam bots. E.g.:
*
* <a href="&#x6D;&#97;&#105;&#108;&#x74;&#111;:&#102;&#111;&#111;&#64;&#101;
* x&#x61;&#109;&#x70;&#108;&#x65;&#x2E;&#99;&#111;&#109;">&#102;&#111;&#111;
* &#64;&#101;x&#x61;&#109;&#x70;&#108;&#x65;&#x2E;&#99;&#111;&#109;</a>
*
* Based on a filter by Matthew Wickline, posted to the BBEdit-Talk
* mailing list: <http://tinyurl.com/yu7ue>
*
*/
showdown.subParser('encodeEmailAddress', function (addr) {
'use strict';
var encode = [
function (ch) {
return '&#' + ch.charCodeAt(0) + ';';
},
function (ch) {
return '&#x' + ch.charCodeAt(0).toString(16) + ';';
},
function (ch) {
return ch;
}
];
addr = 'mailto:' + addr;
addr = addr.replace(/./g, function (ch) {
if (ch === '@') {
// this *must* be encoded. I insist.
ch = encode[Math.floor(Math.random() * 2)](ch);
} else if (ch !== ':') {
// leave ':' alone (to spot mailto: later)
var r = Math.random();
// roughly 10% raw, 45% hex, 45% dec
ch = (
r > 0.9 ? encode[2](ch) : r > 0.45 ? encode[1](ch) : encode[0](ch)
);
}
return ch;
});
addr = '<a href="' + addr + '">' + addr + '</a>';
addr = addr.replace(/">.+:/g, '">'); // strip the mailto: from the visible part
return addr;
});
/**
* Within tags -- meaning between < and > -- encode [\ ` * _] so they
* don't conflict with their use in Markdown for code, italics and strong.
*/
showdown.subParser('escapeSpecialCharsWithinTagAttributes', function (text) {
'use strict';
// Build a regex to find HTML tags and comments. See Friedl's
// "Mastering Regular Expressions", 2nd Ed., pp. 200-201.
var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|<!(--.*?--\s*)+>)/gi;
text = text.replace(regex, function (wholeMatch) {
var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g, '$1`');
tag = showdown.helper.escapeCharacters(tag, '\\`*_', false);
return tag;
});
return text;
});
/**
* Handle github codeblocks prior to running HashHTML so that
* HTML contained within the codeblock gets escaped properly
* Example:
* ```ruby
* def hello_world(x)
* puts "Hello, #{x}"
* end
* ```
*/
showdown.subParser('githubCodeBlocks', function (text, options, globals) {
'use strict';
// early exit if option is not enabled
if (!options.ghCodeBlocks) {
return text;
}
text = globals.converter._dispatch('githubCodeBlocks.before', text, options, globals);
text += '~0';
text = text.replace(/(?:^|\n)```(.*)\n([\s\S]*?)\n```/g, function (wholeMatch, language, codeblock) {
var end = (options.omitExtraWLInCodeBlocks) ? '' : '\n';
// First parse the github code block
codeblock = showdown.subParser('encodeCode')(codeblock);
codeblock = showdown.subParser('detab')(codeblock);
codeblock = codeblock.replace(/^\n+/g, ''); // trim leading newlines
codeblock = codeblock.replace(/\n+$/g, ''); // trim trailing whitespace
codeblock = '<pre><code' + (language ? ' class="' + language + ' language-' + language + '"' : '') + '>' + codeblock + end + '</code></pre>';
codeblock = showdown.subParser('hashBlock')(codeblock, options, globals);
// Since GHCodeblocks can be false positives, we need to
// store the primitive text and the parsed text in a global var,
// and then return a token
return '\n\n~G' + (globals.ghCodeBlocks.push({text: wholeMatch, codeblock: codeblock}) - 1) + 'G\n\n';
});
// attacklab: strip sentinel
text = text.replace(/~0/, '');
return globals.converter._dispatch('githubCodeBlocks.after', text, options, globals);
});
showdown.subParser('hashBlock', function (text, options, globals) {
'use strict';
text = text.replace(/(^\n+|\n+$)/g, '');
return '\n\n~K' + (globals.gHtmlBlocks.push(text) - 1) + 'K\n\n';
});
showdown.subParser('hashElement', function (text, options, globals) {
'use strict';
return function (wholeMatch, m1) {
var blockText = m1;
// Undo double lines
blockText = blockText.replace(/\n\n/g, '\n');
blockText = blockText.replace(/^\n/, '');
// strip trailing blank lines
blockText = blockText.replace(/\n+$/g, '');
// Replace the element text with a marker ("~KxK" where x is its key)
blockText = '\n\n~K' + (globals.gHtmlBlocks.push(blockText) - 1) + 'K\n\n';
return blockText;
};
});
showdown.subParser('hashHTMLBlocks', function (text, options, globals) {
'use strict';
var blockTags = [
'pre',
'div',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'blockquote',
'table',
'dl',
'ol',
'ul',
'script',
'noscript',
'form',
'fieldset',
'iframe',
'math',
'style',
'section',
'header',
'footer',
'nav',
'article',
'aside',
'address',
'audio',
'canvas',
'figure',
'hgroup',
'output',
'video',
'p'
],
repFunc = function (wholeMatch, match, left, right) {
var txt = wholeMatch;
// check if this html element is marked as markdown
// if so, it's contents should be parsed as markdown
if (left.search(/\bmarkdown\b/) !== -1) {
txt = left + globals.converter.makeHtml(match) + right;
}
return '\n\n~K' + (globals.gHtmlBlocks.push(txt) - 1) + 'K\n\n';
};
for (var i = 0; i < blockTags.length; ++i) {
text = showdown.helper.replaceRecursiveRegExp(text, repFunc, '^(?: |\\t){0,3}<' + blockTags[i] + '\\b[^>]*>', '</' + blockTags[i] + '>', 'gim');
}
// HR SPECIAL CASE
text = text.replace(/(\n[ ]{0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,
showdown.subParser('hashElement')(text, options, globals));
// Special case for standalone HTML comments:
text = text.replace(/(<!--[\s\S]*?-->)/g,
showdown.subParser('hashElement')(text, options, globals));
// PHP and ASP-style processor instructions (<?...?> and <%...%>)
text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,
showdown.subParser('hashElement')(text, options, globals));
return text;
});
/**
* Hash span elements that should not be parsed as markdown
*/
showdown.subParser('hashHTMLSpans', function (text, config, globals) {
'use strict';
var matches = showdown.helper.matchRecursiveRegExp(text, '<code\\b[^>]*>', '</code>', 'gi');
for (var i = 0; i < matches.length; ++i) {
text = text.replace(matches[i][0], '~L' + (globals.gHtmlSpans.push(matches[i][0]) - 1) + 'L');
}
return text;
});
/**
* Unhash HTML spans
*/
showdown.subParser('unhashHTMLSpans', function (text, config, globals) {
'use strict';
for (var i = 0; i < globals.gHtmlSpans.length; ++i) {
text = text.replace('~L' + i + 'L', globals.gHtmlSpans[i]);
}
return text;
});
/**
* Hash span elements that should not be parsed as markdown
*/
showdown.subParser('hashPreCodeTags', function (text, config, globals) {
'use strict';
var repFunc = function (wholeMatch, match, left, right) {
// encode html entities
var codeblock = left + showdown.subParser('encodeCode')(match) + right;
return '\n\n~G' + (globals.ghCodeBlocks.push({text: wholeMatch, codeblock: codeblock}) - 1) + 'G\n\n';
};
text = showdown.helper.replaceRecursiveRegExp(text, repFunc, '^(?: |\\t){0,3}<pre\\b[^>]*>\\s*<code\\b[^>]*>', '^(?: |\\t){0,3}</code>\\s*</pre>', 'gim');
return text;
});
showdown.subParser('headers', function (text, options, globals) {
'use strict';
text = globals.converter._dispatch('headers.before', text, options, globals);
var prefixHeader = options.prefixHeaderId,
headerLevelStart = (isNaN(parseInt(options.headerLevelStart))) ? 1 : parseInt(options.headerLevelStart),
// Set text-style headers:
// Header 1
// ========
//
// Header 2
// --------
//
setextRegexH1 = (options.smoothLivePreview) ? /^(.+)[ \t]*\n={2,}[ \t]*\n+/gm : /^(.+)[ \t]*\n=+[ \t]*\n+/gm,
setextRegexH2 = (options.smoothLivePreview) ? /^(.+)[ \t]*\n-{2,}[ \t]*\n+/gm : /^(.+)[ \t]*\n-+[ \t]*\n+/gm;
text = text.replace(setextRegexH1, function (wholeMatch, m1) {
var spanGamut = showdown.subParser('spanGamut')(m1, options, globals),
hID = (options.noHeaderId) ? '' : ' id="' + headerId(m1) + '"',
hLevel = headerLevelStart,
hashBlock = '<h' + hLevel + hID + '>' + spanGamut + '</h' + hLevel + '>';
return showdown.subParser('hashBlock')(hashBlock, options, globals);
});
text = text.replace(setextRegexH2, function (matchFound, m1) {
var spanGamut = showdown.subParser('spanGamut')(m1, options, globals),
hID = (options.noHeaderId) ? '' : ' id="' + headerId(m1) + '"',
hLevel = headerLevelStart + 1,
hashBlock = '<h' + hLevel + hID + '>' + spanGamut + '</h' + hLevel + '>';
return showdown.subParser('hashBlock')(hashBlock, options, globals);
});
// atx-style headers:
// # Header 1
// ## Header 2
// ## Header 2 with closing hashes ##
// ...
// ###### Header 6
//
text = text.replace(/^(#{1,6})[ \t]*(.+?)[ \t]*#*\n+/gm, function (wholeMatch, m1, m2) {
var span = showdown.subParser('spanGamut')(m2, options, globals),
hID = (options.noHeaderId) ? '' : ' id="' + headerId(m2) + '"',
hLevel = headerLevelStart - 1 + m1.length,
header = '<h' + hLevel + hID + '>' + span + '</h' + hLevel + '>';
return showdown.subParser('hashBlock')(header, options, globals);
});
function headerId(m) {
var title, escapedId = m.replace(/[^\w]/g, '').toLowerCase();
if (globals.hashLinkCounts[escapedId]) {
title = escapedId + '-' + (globals.hashLinkCounts[escapedId]++);
} else {
title = escapedId;
globals.hashLinkCounts[escapedId] = 1;
}
// Prefix id to prevent causing inadvertent pre-existing style matches.
if (prefixHeader === true) {
prefixHeader = 'section';
}
if (showdown.helper.isString(prefixHeader)) {
return prefixHeader + title;
}
return title;
}
text = globals.converter._dispatch('headers.after', text, options, globals);
return text;
});
/**
* Turn Markdown image shortcuts into <img> tags.
*/
showdown.subParser('images', function (text, options, globals) {
'use strict';
text = globals.converter._dispatch('images.before', text, options, globals);
var inlineRegExp = /!\[(.*?)]\s?\([ \t]*()<?(\S+?)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(['"])(.*?)\6[ \t]*)?\)/g,
referenceRegExp = /!\[([^\]]*?)] ?(?:\n *)?\[(.*?)]()()()()()/g;
function writeImageTag (wholeMatch, altText, linkId, url, width, height, m5, title) {
var gUrls = globals.gUrls,
gTitles = globals.gTitles,
gDims = globals.gDimensions;
linkId = linkId.toLowerCase();
if (!title) {
title = '';
}
if (url === '' || url === null) {
if (linkId === '' || linkId === null) {
// lower-case and turn embedded newlines into spaces
linkId = altText.toLowerCase().replace(/ ?\n/g, ' ');
}
url = '#' + linkId;
if (!showdown.helper.isUndefined(gUrls[linkId])) {
url = gUrls[linkId];
if (!showdown.helper.isUndefined(gTitles[linkId])) {
title = gTitles[linkId];
}
if (!showdown.helper.isUndefined(gDims[linkId])) {
width = gDims[linkId].width;
height = gDims[linkId].height;
}
} else {
return wholeMatch;
}
}
altText = altText.replace(/"/g, '&quot;');
altText = showdown.helper.escapeCharacters(altText, '*_', false);
url = showdown.helper.escapeCharacters(url, '*_', false);
var result = '<img src="' + url + '" alt="' + altText + '"';
if (title) {
title = title.replace(/"/g, '&quot;');
title = showdown.helper.escapeCharacters(title, '*_', false);
result += ' title="' + title + '"';
}
if (width && height) {
width = (width === '*') ? 'auto' : width;
height = (height === '*') ? 'auto' : height;
result += ' width="' + width + '"';
result += ' height="' + height + '"';
}
result += ' />';
return result;
}
// First, handle reference-style labeled images: ![alt text][id]
text = text.replace(referenceRegExp, writeImageTag);
// Next, handle inline images: ![alt text](url =<width>x<height> "optional title")
text = text.replace(inlineRegExp, writeImageTag);
text = globals.converter._dispatch('images.after', text, options, globals);
return text;
});
showdown.subParser('italicsAndBold', function (text, options, globals) {
'use strict';
text = globals.converter._dispatch('italicsAndBold.before', text, options, globals);
if (options.literalMidWordUnderscores) {
//underscores
// Since we are consuming a \s character, we need to add it
text = text.replace(/(^|\s|>|\b)__(?=\S)([\s\S]+?)__(?=\b|<|\s|$)/gm, '$1<strong>$2</strong>');
text = text.replace(/(^|\s|>|\b)_(?=\S)([\s\S]+?)_(?=\b|<|\s|$)/gm, '$1<em>$2</em>');
//asterisks
text = text.replace(/(\*\*)(?=\S)([^\r]*?\S[*]*)\1/g, '<strong>$2</strong>');
text = text.replace(/(\*)(?=\S)([^\r]*?\S)\1/g, '<em>$2</em>');
} else {
// <strong> must go first:
text = text.replace(/(\*\*|__)(?=\S)([^\r]*?\S[*_]*)\1/g, '<strong>$2</strong>');
text = text.replace(/(\*|_)(?=\S)([^\r]*?\S)\1/g, '<em>$2</em>');
}
text = globals.converter._dispatch('italicsAndBold.after', text, options, globals);
return text;
});
/**
* Form HTML ordered (numbered) and unordered (bulleted) lists.
*/
showdown.subParser('lists', function (text, options, globals) {
'use strict';
text = globals.converter._dispatch('lists.before', text, options, globals);
/**
* Process the contents of a single ordered or unordered list, splitting it
* into individual list items.
* @param {string} listStr
* @param {boolean} trimTrailing
* @returns {string}
*/
function processListItems (listStr, trimTrailing) {
// The $g_list_level global keeps track of when we're inside a list.
// Each time we enter a list, we increment it; when we leave a list,
// we decrement. If it's zero, we're not in a list anymore.
//
// We do this because when we're not inside a list, we want to treat
// something like this:
//
// I recommend upgrading to version
// 8. Oops, now this line is treated
// as a sub-list.
//
// As a single paragraph, despite the fact that the second line starts
// with a digit-period-space sequence.
//
// Whereas when we're inside a list (or sub-list), that line will be
// treated as the start of a sub-list. What a kludge, huh? This is
// an aspect of Markdown's syntax that's hard to parse perfectly
// without resorting to mind-reading. Perhaps the solution is to
// change the syntax rules such that sub-lists must start with a
// starting cardinal number; e.g. "1." or "a.".
globals.gListLevel++;
// trim trailing blank lines:
listStr = listStr.replace(/\n{2,}$/, '\n');
// attacklab: add sentinel to emulate \z
listStr += '~0';
var rgx = /(\n)?(^[ \t]*)([*+-]|\d+[.])[ \t]+((\[(x|X| )?])?[ \t]*[^\r]+?(\n{1,2}))(?=\n*(~0|\2([*+-]|\d+[.])[ \t]+))/gm,
isParagraphed = (/\n[ \t]*\n(?!~0)/.test(listStr));
listStr = listStr.replace(rgx, function (wholeMatch, m1, m2, m3, m4, taskbtn, checked) {
checked = (checked && checked.trim() !== '');
var item = showdown.subParser('outdent')(m4, options, globals),
bulletStyle = '';
// Support for github tasklists
if (taskbtn && options.tasklists) {
bulletStyle = ' class="task-list-item" style="list-style-type: none;"';
item = item.replace(/^[ \t]*\[(x|X| )?]/m, function () {
var otp = '<input type="checkbox" disabled style="margin: 0px 0.35em 0.25em -1.6em; vertical-align: middle;"';
if (checked) {
otp += ' checked';
}
otp += '>';
return otp;
});
}
// m1 - Leading line or
// Has a double return (multi paragraph) or
// Has sublist
if (m1 || (item.search(/\n{2,}/) > -1)) {
item = showdown.subParser('githubCodeBlocks')(item, options, globals);
item = showdown.subParser('blockGamut')(item, options, globals);
} else {
// Recursion for sub-lists:
item = showdown.subParser('lists')(item, options, globals);
item = item.replace(/\n$/, ''); // chomp(item)
if (isParagraphed) {
item = showdown.subParser('paragraphs')(item, options, globals);
} else {
item = showdown.subParser('spanGamut')(item, options, globals);
}
}
item = '\n<li' + bulletStyle + '>' + item + '</li>\n';
return item;
});
// attacklab: strip sentinel
listStr = listStr.replace(/~0/g, '');
globals.gListLevel--;
if (trimTrailing) {
listStr = listStr.replace(/\s+$/, '');
}
return listStr;
}
/**
* Check and parse consecutive lists (better fix for issue #142)
* @param {string} list
* @param {string} listType
* @param {boolean} trimTrailing
* @returns {string}
*/
function parseConsecutiveLists(list, listType, trimTrailing) {
// check if we caught 2 or more consecutive lists by mistake
// we use the counterRgx, meaning if listType is UL we look for UL and vice versa
var counterRxg = (listType === 'ul') ? /^ {0,2}\d+\.[ \t]/gm : /^ {0,2}[*+-][ \t]/gm,
subLists = [],
result = '';
if (list.search(counterRxg) !== -1) {
(function parseCL(txt) {
var pos = txt.search(counterRxg);
if (pos !== -1) {
// slice
result += '\n\n<' + listType + '>' + processListItems(txt.slice(0, pos), !!trimTrailing) + '</' + listType + '>\n\n';
// invert counterType and listType
listType = (listType === 'ul') ? 'ol' : 'ul';
counterRxg = (listType === 'ul') ? /^ {0,2}\d+\.[ \t]/gm : /^ {0,2}[*+-][ \t]/gm;
//recurse
parseCL(txt.slice(pos));
} else {
result += '\n\n<' + listType + '>' + processListItems(txt, !!trimTrailing) + '</' + listType + '>\n\n';
}
})(list);
for (var i = 0; i < subLists.length; ++i) {
}
} else {
result = '\n\n<' + listType + '>' + processListItems(list, !!trimTrailing) + '</' + listType + '>\n\n';
}
return result;
}
// attacklab: add sentinel to hack around khtml/safari bug:
// http://bugs.webkit.org/show_bug.cgi?id=11231
text += '~0';
// Re-usable pattern to match any entire ul or ol list:
var wholeList = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm;
if (globals.gListLevel) {
text = text.replace(wholeList, function (wholeMatch, list, m2) {
var listType = (m2.search(/[*+-]/g) > -1) ? 'ul' : 'ol';
return parseConsecutiveLists(list, listType, true);
});
} else {
wholeList = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm;
//wholeList = /(\n\n|^\n?)( {0,3}([*+-]|\d+\.)[ \t]+[\s\S]+?)(?=(~0)|(\n\n(?!\t| {2,}| {0,3}([*+-]|\d+\.)[ \t])))/g;
text = text.replace(wholeList, function (wholeMatch, m1, list, m3) {
var listType = (m3.search(/[*+-]/g) > -1) ? 'ul' : 'ol';
return parseConsecutiveLists(list, listType);
});
}
// attacklab: strip sentinel
text = text.replace(/~0/, '');
text = globals.converter._dispatch('lists.after', text, options, globals);
return text;
});
/**
* Remove one level of line-leading tabs or spaces
*/
showdown.subParser('outdent', function (text) {
'use strict';
// attacklab: hack around Konqueror 3.5.4 bug:
// "----------bug".replace(/^-/g,"") == "bug"
text = text.replace(/^(\t|[ ]{1,4})/gm, '~0'); // attacklab: g_tab_width
// attacklab: clean up hack
text = text.replace(/~0/g, '');
return text;
});
/**
*
*/
showdown.subParser('paragraphs', function (text, options, globals) {
'use strict';
text = globals.converter._dispatch('paragraphs.before', text, options, globals);
// Strip leading and trailing lines:
text = text.replace(/^\n+/g, '');
text = text.replace(/\n+$/g, '');
var grafs = text.split(/\n{2,}/g),
grafsOut = [],
end = grafs.length; // Wrap <p> tags
for (var i = 0; i < end; i++) {
var str = grafs[i];
// if this is an HTML marker, copy it
if (str.search(/~(K|G)(\d+)\1/g) >= 0) {
grafsOut.push(str);
} else {
str = showdown.subParser('spanGamut')(str, options, globals);
str = str.replace(/^([ \t]*)/g, '<p>');
str += '</p>';
grafsOut.push(str);
}
}
/** Unhashify HTML blocks */
end = grafsOut.length;
for (i = 0; i < end; i++) {
var blockText = '',
grafsOutIt = grafsOut[i],
codeFlag = false;
// if this is a marker for an html block...
while (grafsOutIt.search(/~(K|G)(\d+)\1/) >= 0) {
var delim = RegExp.$1,
num = RegExp.$2;
if (delim === 'K') {
blockText = globals.gHtmlBlocks[num];
} else {
// we need to check if ghBlock is a false positive
if (codeFlag) {
// use encoded version of all text
blockText = showdown.subParser('encodeCode')(globals.ghCodeBlocks[num].text);
} else {
blockText = globals.ghCodeBlocks[num].codeblock;
}
}
blockText = blockText.replace(/\$/g, '$$$$'); // Escape any dollar signs
grafsOutIt = grafsOutIt.replace(/(\n\n)?~(K|G)\d+\2(\n\n)?/, blockText);
// Check if grafsOutIt is a pre->code
if (/^<pre\b[^>]*>\s*<code\b[^>]*>/.test(grafsOutIt)) {
codeFlag = true;
}
}
grafsOut[i] = grafsOutIt;
}
text = grafsOut.join('\n\n');
// Strip leading and trailing lines:
text = text.replace(/^\n+/g, '');
text = text.replace(/\n+$/g, '');
return globals.converter._dispatch('paragraphs.after', text, options, globals);
});
/**
* Run extension
*/
showdown.subParser('runExtension', function (ext, text, options, globals) {
'use strict';
if (ext.filter) {
text = ext.filter(text, globals.converter, options);
} else if (ext.regex) {
// TODO remove this when old extension loading mechanism is deprecated
var re = ext.regex;
if (!re instanceof RegExp) {
re = new RegExp(re, 'g');
}
text = text.replace(re, ext.replace);
}
return text;
});
/**
* These are all the transformations that occur *within* block-level
* tags like paragraphs, headers, and list items.
*/
showdown.subParser('spanGamut', function (text, options, globals) {
'use strict';
text = globals.converter._dispatch('spanGamut.before', text, options, globals);
text = showdown.subParser('codeSpans')(text, options, globals);
text = showdown.subParser('escapeSpecialCharsWithinTagAttributes')(text, options, globals);
text = showdown.subParser('encodeBackslashEscapes')(text, options, globals);
// Process anchor and image tags. Images must come first,
// because ![foo][f] looks like an anchor.
text = showdown.subParser('images')(text, options, globals);
text = showdown.subParser('anchors')(text, options, globals);
// Make links out of things like `<http://example.com/>`
// Must come after _DoAnchors(), because you can use < and >
// delimiters in inline links like [this](<url>).
text = showdown.subParser('autoLinks')(text, options, globals);
text = showdown.subParser('encodeAmpsAndAngles')(text, options, globals);
text = showdown.subParser('italicsAndBold')(text, options, globals);
text = showdown.subParser('strikethrough')(text, options, globals);
// Do hard breaks:
text = text.replace(/ +\n/g, ' <br />\n');
text = globals.converter._dispatch('spanGamut.after', text, options, globals);
return text;
});
showdown.subParser('strikethrough', function (text, options, globals) {
'use strict';
if (options.strikethrough) {
text = globals.converter._dispatch('strikethrough.before', text, options, globals);
text = text.replace(/(?:~T){2}([\s\S]+?)(?:~T){2}/g, '<del>$1</del>');
text = globals.converter._dispatch('strikethrough.after', text, options, globals);
}
return text;
});
/**
* Strip any lines consisting only of spaces and tabs.
* This makes subsequent regexs easier to write, because we can
* match consecutive blank lines with /\n+/ instead of something
* contorted like /[ \t]*\n+/
*/
showdown.subParser('stripBlankLines', function (text) {
'use strict';
return text.replace(/^[ \t]+$/mg, '');
});
/**
* Strips link definitions from text, stores the URLs and titles in
* hash references.
* Link defs are in the form: ^[id]: url "optional title"
*
* ^[ ]{0,3}\[(.+)\]: // id = $1 attacklab: g_tab_width - 1
* [ \t]*
* \n? // maybe *one* newline
* [ \t]*
* <?(\S+?)>? // url = $2
* [ \t]*
* \n? // maybe one newline
* [ \t]*
* (?:
* (\n*) // any lines skipped = $3 attacklab: lookbehind removed
* ["(]
* (.+?) // title = $4
* [")]
* [ \t]*
* )? // title is optional
* (?:\n+|$)
* /gm,
* function(){...});
*
*/
showdown.subParser('stripLinkDefinitions', function (text, options, globals) {
'use strict';
var regex = /^ {0,3}\[(.+)]:[ \t]*\n?[ \t]*<?(\S+?)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n+|(?=~0))/gm;
// attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
text += '~0';
text = text.replace(regex, function (wholeMatch, linkId, url, width, height, blankLines, title) {
linkId = linkId.toLowerCase();
globals.gUrls[linkId] = showdown.subParser('encodeAmpsAndAngles')(url); // Link IDs are case-insensitive
if (blankLines) {
// Oops, found blank lines, so it's not a title.
// Put back the parenthetical statement we stole.
return blankLines + title;
} else {
if (title) {
globals.gTitles[linkId] = title.replace(/"|'/g, '&quot;');
}
if (options.parseImgDimensions && width && height) {
globals.gDimensions[linkId] = {
width: width,
height: height
};
}
}
// Completely remove the definition from the text
return '';
});
// attacklab: strip sentinel
text = text.replace(/~0/, '');
return text;
});
showdown.subParser('tables', function (text, options, globals) {
'use strict';
if (!options.tables) {
return text;
}
var tableRgx = /^[ \t]{0,3}\|?.+\|.+\n[ \t]{0,3}\|?[ \t]*:?[ \t]*(?:-|=){2,}[ \t]*:?[ \t]*\|[ \t]*:?[ \t]*(?:-|=){2,}[\s\S]+?(?:\n\n|~0)/gm;
function parseStyles(sLine) {
if (/^:[ \t]*--*$/.test(sLine)) {
return ' style="text-align:left;"';
} else if (/^--*[ \t]*:[ \t]*$/.test(sLine)) {
return ' style="text-align:right;"';
} else if (/^:[ \t]*--*[ \t]*:$/.test(sLine)) {
return ' style="text-align:center;"';
} else {
return '';
}
}
function parseHeaders(header, style) {
var id = '';
header = header.trim();
if (options.tableHeaderId) {
id = ' id="' + header.replace(/ /g, '_').toLowerCase() + '"';
}
header = showdown.subParser('spanGamut')(header, options, globals);
return '<th' + id + style + '>' + header + '</th>\n';
}
function parseCells(cell, style) {
var subText = showdown.subParser('spanGamut')(cell, options, globals);
return '<td' + style + '>' + subText + '</td>\n';
}
function buildTable(headers, cells) {
var tb = '<table>\n<thead>\n<tr>\n',
tblLgn = headers.length;
for (var i = 0; i < tblLgn; ++i) {
tb += headers[i];
}
tb += '</tr>\n</thead>\n<tbody>\n';
for (i = 0; i < cells.length; ++i) {
tb += '<tr>\n';
for (var ii = 0; ii < tblLgn; ++ii) {
tb += cells[i][ii];
}
tb += '</tr>\n';
}
tb += '</tbody>\n</table>\n';
return tb;
}
text = globals.converter._dispatch('tables.before', text, options, globals);
text = text.replace(tableRgx, function (rawTable) {
var i, tableLines = rawTable.split('\n');
// strip wrong first and last column if wrapped tables are used
for (i = 0; i < tableLines.length; ++i) {
if (/^[ \t]{0,3}\|/.test(tableLines[i])) {
tableLines[i] = tableLines[i].replace(/^[ \t]{0,3}\|/, '');
}
if (/\|[ \t]*$/.test(tableLines[i])) {
tableLines[i] = tableLines[i].replace(/\|[ \t]*$/, '');
}
}
var rawHeaders = tableLines[0].split('|').map(function (s) { return s.trim();}),
rawStyles = tableLines[1].split('|').map(function (s) { return s.trim();}),
rawCells = [],
headers = [],
styles = [],
cells = [];
tableLines.shift();
tableLines.shift();
for (i = 0; i < tableLines.length; ++i) {
if (tableLines[i].trim() === '') {
continue;
}
rawCells.push(
tableLines[i]
.split('|')
.map(function (s) {
return s.trim();
})
);
}
if (rawHeaders.length < rawStyles.length) {
return rawTable;
}
for (i = 0; i < rawStyles.length; ++i) {
styles.push(parseStyles(rawStyles[i]));
}
for (i = 0; i < rawHeaders.length; ++i) {
if (showdown.helper.isUndefined(styles[i])) {
styles[i] = '';
}
headers.push(parseHeaders(rawHeaders[i], styles[i]));
}
for (i = 0; i < rawCells.length; ++i) {
var row = [];
for (var ii = 0; ii < headers.length; ++ii) {
if (showdown.helper.isUndefined(rawCells[i][ii])) {
}
row.push(parseCells(rawCells[i][ii], styles[ii]));
}
cells.push(row);
}
return buildTable(headers, cells);
});
text = globals.converter._dispatch('tables.after', text, options, globals);
return text;
});
/**
* Swap back in all the special characters we've hidden.
*/
showdown.subParser('unescapeSpecialChars', function (text) {
'use strict';
text = text.replace(/~E(\d+)E/g, function (wholeMatch, m1) {
var charCodeToReplace = parseInt(m1);
return String.fromCharCode(charCodeToReplace);
});
return text;
});
module.exports = showdown;
// HTML 支持的数学符号
function strNumDiscode(str){
str = str.replace(/&forall;/g, '∀');
str = str.replace(/&part;/g, '∂');
str = str.replace(/&exists;/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(/&cap;/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(/&quot;/g, "'");
str = str.replace(/&amp;/g, '&');
// str = str.replace(/&lt;/g, '‹');
// str = str.replace(/&gt;/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 strMoreDiscode(str){
str = str.replace(/\r\n/g,"");
str = str.replace(/\n/g,"");
str = str.replace(/code/g,"wxxxcode-style");
return str;
}
function strDiscode(str){
str = strNumDiscode(str);
str = strGreeceDiscode(str);
str = strcharacterDiscode(str);
str = strOtherDiscode(str);
str = strMoreDiscode(str);
return str;
}
function urlToHttpUrl(url,rep){
var patt1 = new RegExp("^//");
var result = patt1.test(url);
if(result){
url = rep+":"+url;
}
return url;
}
module.exports = {
strDiscode:strDiscode,
urlToHttpUrl:urlToHttpUrl
}
\ No newline at end of file
/**
* 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
*/
/**
* utils函数引入
**/
import showdown from './showdown.js';
import HtmlToJson from './html2json.js';
/**
* 配置及公有属性
**/
var realWindowWidth = 0;
var realWindowHeight = 0;
wx.getSystemInfo({
success: function (res) {
realWindowWidth = res.windowWidth
realWindowHeight = res.windowHeight
}
})
/**
* 主函数入口区
**/
function wxParse(bindName = 'wxParseData', type='html', data='<div class="color:red;">数据不能为空</div>', target,imagePadding) {
var that = target;
var transData = {};//存放转化后的数据
if (type == 'html') {
transData = HtmlToJson.html2json(data, bindName);
} else if (type == 'md' || type == 'markdown') {
var converter = new showdown.Converter();
var html = converter.makeHtml(data);
transData = HtmlToJson.html2json(html, bindName);
}
transData.view = {};
transData.view.imagePadding = 0;
if(typeof(imagePadding) != 'undefined'){
transData.view.imagePadding = imagePadding
}
that.wxParseImgLoad = wxParseImgLoad;
that.wxParseImgTap = wxParseImgTap;
return transData
}
// 图片点击事件
function wxParseImgTap(e) {
var that = this;
var nowImgUrl = e.target.dataset.src;
var tagFrom = e.target.dataset.from;
if (typeof (tagFrom) != 'undefined' && tagFrom.length > 0) {
wx.previewImage({
current: nowImgUrl, // 当前显示图片的http链接
urls: that.data[tagFrom].imageUrls // 需要预览的图片http链接列表
})
}
}
/**
* 图片视觉宽高计算函数区
**/
function wxParseImgLoad(e) {
var that = this;
var tagFrom = e.target.dataset.from;
var idx = e.target.dataset.idx;
if (typeof (tagFrom) != 'undefined' && tagFrom.length > 0) {
calMoreImageInfo(e, idx, that, tagFrom)
}
}
// 假循环获取计算图片视觉最佳宽高
function calMoreImageInfo(e, idx, that, bindName) {
var temData = that.data[bindName];
if (!temData || temData.images.length == 0) {
return;
}
var temImages = temData.images;
//因为无法获取view宽度 需要自定义padding进行计算,稍后处理
var recal = wxAutoImageCal(e.detail.width, e.detail.height,that,bindName);
// temImages[idx].width = recal.imageWidth;
// temImages[idx].height = recal.imageheight;
// temData.images = temImages;
// var bindData = {};
// bindData[bindName] = temData;
// that.setData(bindData);
var index = temImages[idx].index
var key = `${bindName}`
for (var i of index.split('.')) key+=`.nodes[${i}]`
var keyW = key + '.width'
var keyH = key + '.height'
that.setData({
[keyW]: recal.imageWidth,
[keyH]: recal.imageheight,
})
}
// 计算视觉优先的图片宽高
function wxAutoImageCal(originalWidth, originalHeight,that,bindName) {
//获取图片的原始长宽
var windowWidth = 0, windowHeight = 0;
var autoWidth = 0, autoHeight = 0;
var results = {};
var padding = that.data[bindName].view.imagePadding;
windowWidth = realWindowWidth-2*padding;
windowHeight = realWindowHeight;
//判断按照那种方式进行缩放
// console.log("windowWidth" + windowWidth);
if (originalWidth > windowWidth) {//在图片width大于手机屏幕width时候
autoWidth = windowWidth;
// console.log("autoWidth" + autoWidth);
autoHeight = (autoWidth * originalHeight) / originalWidth;
// console.log("autoHeight" + autoHeight);
results.imageWidth = autoWidth;
results.imageheight = autoHeight;
} else {//否则展示原来的数据
results.imageWidth = originalWidth;
results.imageheight = originalHeight;
}
return results;
}
function wxParseTemArray(temArrayName,bindNameReg,total,that){
var array = [];
var temData = that.data;
var obj = null;
for(var i = 0; i < total; i++){
var simArr = temData[bindNameReg+i].nodes;
array.push(simArr);
}
temArrayName = temArrayName || 'wxParseTemArray';
obj = JSON.parse('{"'+ temArrayName +'":""}');
obj[temArrayName] = array;
that.setData(obj);
}
/**
* 配置emojis
*
*/
function emojisInit(reg='',baseSrc="/wxParse/emojis/",emojis){
HtmlToJson.emojisInit(reg,baseSrc,emojis);
}
export default {
wxParse: wxParse,
wxParseTemArray:wxParseTemArray,
emojisInit:emojisInit
}
<!--**
* 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
*/-->
<!--基础元素-->
<template name="wxParseVideo">
<!--增加video标签支持,并循环添加-->
<view class="{{item.classStr}} wxParse-{{item.tag}}" style="{{item.styleStr}}">
<video class="{{item.classStr}} wxParse-{{item.tag}}-video" src="{{item.attr.src}}"></video>
</view>
</template>
<template name="wxParseImg">
<image class="{{item.classStr}} wxParse-{{item.tag}}" data-from="{{item.from}}" data-src="{{item.attr.src}}" data-idx="{{item.imgIndex}}" src="{{item.attr.src}}" mode="aspectFit" bindload="wxParseImgLoad" bindtap="wxParseImgTap" mode="widthFix" style="width:{{item.width}}px;"
/>
</template>
<template name="WxEmojiView">
<view class="WxEmojiView wxParse-inline" style="{{item.styleStr}}">
<block wx:for="{{item.textArray}}" wx:key="">
<text class="{{item.text == '\\n' ? 'wxParse-hide':''}}" wx:if="{{item.node == 'text'}}" selectable="{{true}}">{{item.text}}</text>
<block wx:elif="{{item.node == 'element'}}">
<image class="wxEmoji" src="{{item.baseSrc}}{{item.text}}" />
</block>
</block>
</view>
</template>
<template name="WxParseBr">
<text>\n</text>
</template>
<!--入口模版-->
<template name="wxParse">
<block wx:for="{{nodes}}" wx:key="">
<template is="wxParse0" data="{{item}}" />
</block>
</template>
<!--循环模版-->
<template name="wxParse0">
<!--<template is="wxParse1" data="{{item}}" />-->
<!--判断是否是标签节点-->
<block wx:if="{{item.node == 'element'}}">
<block wx:if="{{item.tag == 'button'}}">
<button type="default" size="mini">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse1" data="{{item}}" />
</block>
</button>
</block>
<!--li类型-->
<block wx:elif="{{item.tag == 'li'}}">
<view class="{{item.classStr}} wxParse-li" style="{{item.styleStr}}">
<view class="{{item.classStr}} wxParse-li-inner">
<view class="{{item.classStr}} wxParse-li-text">
<view class="{{item.classStr}} wxParse-li-circle"></view>
</view>
<view class="{{item.classStr}} wxParse-li-text">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse1" data="{{item}}" />
</block>
</view>
</view>
</view>
</block>
<!--video类型-->
<block wx:elif="{{item.tag == 'video'}}">
<template is="wxParseVideo" data="{{item}}" />
</block>
<!--img类型-->
<block wx:elif="{{item.tag == 'img'}}">
<template is="wxParseImg" data="{{item}}" />
</block>
<!--a类型-->
<block wx:elif="{{item.tag == 'a'}}">
<view bindtap="wxParseTagATap" class="wxParse-inline {{item.classStr}} wxParse-{{item.tag}}" data-src="{{item.attr.href}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse1" data="{{item}}" />
</block>
</view>
</block>
<block wx:elif="{{item.tag == 'table'}}">
<view class="{{item.classStr}} wxParse-{{item.tag}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse1" data="{{item}}" />
</block>
</view>
</block>
<block wx:elif="{{item.tag == 'br'}}">
<template is="WxParseBr"></template>
</block>
<!--其他块级标签-->
<block wx:elif="{{item.tagType == 'block'}}">
<view class="{{item.classStr}} wxParse-{{item.tag}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse1" data="{{item}}" />
</block>
</view>
</block>
<!--内联标签-->
<view wx:else class="{{item.classStr}} wxParse-{{item.tag}} wxParse-{{item.tagType}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse1" data="{{item}}" />
</block>
</view>
</block>
<!--判断是否是文本节点-->
<block wx:elif="{{item.node == 'text'}}">
<!--如果是,直接进行-->
<template is="WxEmojiView" data="{{item}}" />
</block>
</template>
<!--循环模版-->
<template name="wxParse1">
<!--<template is="wxParse2" data="{{item}}" />-->
<!--判断是否是标签节点-->
<block wx:if="{{item.node == 'element'}}">
<block wx:if="{{item.tag == 'button'}}">
<button type="default" size="mini">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse2" data="{{item}}" />
</block>
</button>
</block>
<!--li类型-->
<block wx:elif="{{item.tag == 'li'}}">
<view class="{{item.classStr}} wxParse-li" style="{{item.styleStr}}">
<view class="{{item.classStr}} wxParse-li-inner">
<view class="{{item.classStr}} wxParse-li-text">
<view class="{{item.classStr}} wxParse-li-circle"></view>
</view>
<view class="{{item.classStr}} wxParse-li-text">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse2" data="{{item}}" />
</block>
</view>
</view>
</view>
</block>
<!--video类型-->
<block wx:elif="{{item.tag == 'video'}}">
<template is="wxParseVideo" data="{{item}}" />
</block>
<!--img类型-->
<block wx:elif="{{item.tag == 'img'}}">
<template is="wxParseImg" data="{{item}}" />
</block>
<!--a类型-->
<block wx:elif="{{item.tag == 'a'}}">
<view bindtap="wxParseTagATap" class="wxParse-inline {{item.classStr}} wxParse-{{item.tag}}" data-src="{{item.attr.href}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse2" data="{{item}}" />
</block>
</view>
</block>
<block wx:elif="{{item.tag == 'br'}}">
<template is="WxParseBr"></template>
</block>
<!--其他块级标签-->
<block wx:elif="{{item.tagType == 'block'}}">
<view class="{{item.classStr}} wxParse-{{item.tag}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse2" data="{{item}}" />
</block>
</view>
</block>
<!--内联标签-->
<view wx:else class="{{item.classStr}} wxParse-{{item.tag}} wxParse-{{item.tagType}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse2" data="{{item}}" />
</block>
</view>
</block>
<!--判断是否是文本节点-->
<block wx:elif="{{item.node == 'text'}}">
<!--如果是,直接进行-->
<template is="WxEmojiView" data="{{item}}" />
</block>
</template>
<!--循环模版-->
<template name="wxParse2">
<!--<template is="wxParse3" data="{{item}}" />-->
<!--判断是否是标签节点-->
<block wx:if="{{item.node == 'element'}}">
<block wx:if="{{item.tag == 'button'}}">
<button type="default" size="mini">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse3" data="{{item}}" />
</block>
</button>
</block>
<!--li类型-->
<block wx:elif="{{item.tag == 'li'}}">
<view class="{{item.classStr}} wxParse-li" style="{{item.styleStr}}">
<view class="{{item.classStr}} wxParse-li-inner">
<view class="{{item.classStr}} wxParse-li-text">
<view class="{{item.classStr}} wxParse-li-circle"></view>
</view>
<view class="{{item.classStr}} wxParse-li-text">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse3" data="{{item}}" />
</block>
</view>
</view>
</view>
</block>
<!--video类型-->
<block wx:elif="{{item.tag == 'video'}}">
<template is="wxParseVideo" data="{{item}}" />
</block>
<!--img类型-->
<block wx:elif="{{item.tag == 'img'}}">
<template is="wxParseImg" data="{{item}}" />
</block>
<!--a类型-->
<block wx:elif="{{item.tag == 'a'}}">
<view bindtap="wxParseTagATap" class="wxParse-inline {{item.classStr}} wxParse-{{item.tag}}" data-src="{{item.attr.href}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse3" data="{{item}}" />
</block>
</view>
</block>
<block wx:elif="{{item.tag == 'br'}}">
<template is="WxParseBr"></template>
</block>
<!--其他块级标签-->
<block wx:elif="{{item.tagType == 'block'}}">
<view class="{{item.classStr}} wxParse-{{item.tag}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse3" data="{{item}}" />
</block>
</view>
</block>
<!--内联标签-->
<view wx:else class="{{item.classStr}} wxParse-{{item.tag}} wxParse-{{item.tagType}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse3" data="{{item}}" />
</block>
</view>
</block>
<!--判断是否是文本节点-->
<block wx:elif="{{item.node == 'text'}}">
<!--如果是,直接进行-->
<template is="WxEmojiView" data="{{item}}" />
</block>
</template>
<!--循环模版-->
<template name="wxParse3">
<!--<template is="wxParse4" data="{{item}}" />-->
<!--判断是否是标签节点-->
<block wx:if="{{item.node == 'element'}}">
<block wx:if="{{item.tag == 'button'}}">
<button type="default" size="mini">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse4" data="{{item}}" />
</block>
</button>
</block>
<!--li类型-->
<block wx:elif="{{item.tag == 'li'}}">
<view class="{{item.classStr}} wxParse-li" style="{{item.styleStr}}">
<view class="{{item.classStr}} wxParse-li-inner">
<view class="{{item.classStr}} wxParse-li-text">
<view class="{{item.classStr}} wxParse-li-circle"></view>
</view>
<view class="{{item.classStr}} wxParse-li-text">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse4" data="{{item}}" />
</block>
</view>
</view>
</view>
</block>
<!--video类型-->
<block wx:elif="{{item.tag == 'video'}}">
<template is="wxParseVideo" data="{{item}}" />
</block>
<!--img类型-->
<block wx:elif="{{item.tag == 'img'}}">
<template is="wxParseImg" data="{{item}}" />
</block>
<!--a类型-->
<block wx:elif="{{item.tag == 'a'}}">
<view bindtap="wxParseTagATap" class="wxParse-inline {{item.classStr}} wxParse-{{item.tag}}" data-src="{{item.attr.href}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse4" data="{{item}}" />
</block>
</view>
</block>
<block wx:elif="{{item.tag == 'br'}}">
<template is="WxParseBr"></template>
</block>
<!--其他块级标签-->
<block wx:elif="{{item.tagType == 'block'}}">
<view class="{{item.classStr}} wxParse-{{item.tag}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse4" data="{{item}}" />
</block>
</view>
</block>
<!--内联标签-->
<view wx:else class="{{item.classStr}} wxParse-{{item.tag}} wxParse-{{item.tagType}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse4" data="{{item}}" />
</block>
</view>
</block>
<!--判断是否是文本节点-->
<block wx:elif="{{item.node == 'text'}}">
<!--如果是,直接进行-->
<template is="WxEmojiView" data="{{item}}" />
</block>
</template>
<!--循环模版-->
<template name="wxParse4">
<!--<template is="wxParse5" data="{{item}}" />-->
<!--判断是否是标签节点-->
<block wx:if="{{item.node == 'element'}}">
<block wx:if="{{item.tag == 'button'}}">
<button type="default" size="mini">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse5" data="{{item}}" />
</block>
</button>
</block>
<!--li类型-->
<block wx:elif="{{item.tag == 'li'}}">
<view class="{{item.classStr}} wxParse-li" style="{{item.styleStr}}">
<view class="{{item.classStr}} wxParse-li-inner">
<view class="{{item.classStr}} wxParse-li-text">
<view class="{{item.classStr}} wxParse-li-circle"></view>
</view>
<view class="{{item.classStr}} wxParse-li-text">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse5" data="{{item}}" />
</block>
</view>
</view>
</view>
</block>
<!--video类型-->
<block wx:elif="{{item.tag == 'video'}}">
<template is="wxParseVideo" data="{{item}}" />
</block>
<!--img类型-->
<block wx:elif="{{item.tag == 'img'}}">
<template is="wxParseImg" data="{{item}}" />
</block>
<!--a类型-->
<block wx:elif="{{item.tag == 'a'}}">
<view bindtap="wxParseTagATap" class="wxParse-inline {{item.classStr}} wxParse-{{item.tag}}" data-src="{{item.attr.href}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse5" data="{{item}}" />
</block>
</view>
</block>
<block wx:elif="{{item.tag == 'br'}}">
<template is="WxParseBr"></template>
</block>
<!--其他块级标签-->
<block wx:elif="{{item.tagType == 'block'}}">
<view class="{{item.classStr}} wxParse-{{item.tag}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse5" data="{{item}}" />
</block>
</view>
</block>
<!--内联标签-->
<view wx:else class="{{item.classStr}} wxParse-{{item.tag}} wxParse-{{item.tagType}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse5" data="{{item}}" />
</block>
</view>
</block>
<!--判断是否是文本节点-->
<block wx:elif="{{item.node == 'text'}}">
<!--如果是,直接进行-->
<template is="WxEmojiView" data="{{item}}" />
</block>
</template>
<!--循环模版-->
<template name="wxParse5">
<!--<template is="wxParse6" data="{{item}}" />-->
<!--判断是否是标签节点-->
<block wx:if="{{item.node == 'element'}}">
<block wx:if="{{item.tag == 'button'}}">
<button type="default" size="mini">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse6" data="{{item}}" />
</block>
</button>
</block>
<!--li类型-->
<block wx:elif="{{item.tag == 'li'}}">
<view class="{{item.classStr}} wxParse-li" style="{{item.styleStr}}">
<view class="{{item.classStr}} wxParse-li-inner">
<view class="{{item.classStr}} wxParse-li-text">
<view class="{{item.classStr}} wxParse-li-circle"></view>
</view>
<view class="{{item.classStr}} wxParse-li-text">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse6" data="{{item}}" />
</block>
</view>
</view>
</view>
</block>
<!--video类型-->
<block wx:elif="{{item.tag == 'video'}}">
<template is="wxParseVideo" data="{{item}}" />
</block>
<!--img类型-->
<block wx:elif="{{item.tag == 'img'}}">
<template is="wxParseImg" data="{{item}}" />
</block>
<!--a类型-->
<block wx:elif="{{item.tag == 'a'}}">
<view bindtap="wxParseTagATap" class="wxParse-inline {{item.classStr}} wxParse-{{item.tag}}" data-src="{{item.attr.href}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse6" data="{{item}}" />
</block>
</view>
</block>
<block wx:elif="{{item.tag == 'br'}}">
<template is="WxParseBr"></template>
</block>
<!--其他块级标签-->
<block wx:elif="{{item.tagType == 'block'}}">
<view class="{{item.classStr}} wxParse-{{item.tag}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse6" data="{{item}}" />
</block>
</view>
</block>
<!--内联标签-->
<view wx:else class="{{item.classStr}} wxParse-{{item.tag}} wxParse-{{item.tagType}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse6" data="{{item}}" />
</block>
</view>
</block>
<!--判断是否是文本节点-->
<block wx:elif="{{item.node == 'text'}}">
<!--如果是,直接进行-->
<template is="WxEmojiView" data="{{item}}" />
</block>
</template>
<!--循环模版-->
<template name="wxParse6">
<!--<template is="wxParse7" data="{{item}}" />-->
<!--判断是否是标签节点-->
<block wx:if="{{item.node == 'element'}}">
<block wx:if="{{item.tag == 'button'}}">
<button type="default" size="mini">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse7" data="{{item}}" />
</block>
</button>
</block>
<!--li类型-->
<block wx:elif="{{item.tag == 'li'}}">
<view class="{{item.classStr}} wxParse-li" style="{{item.styleStr}}">
<view class="{{item.classStr}} wxParse-li-inner">
<view class="{{item.classStr}} wxParse-li-text">
<view class="{{item.classStr}} wxParse-li-circle"></view>
</view>
<view class="{{item.classStr}} wxParse-li-text">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse7" data="{{item}}" />
</block>
</view>
</view>
</view>
</block>
<!--video类型-->
<block wx:elif="{{item.tag == 'video'}}">
<template is="wxParseVideo" data="{{item}}" />
</block>
<!--img类型-->
<block wx:elif="{{item.tag == 'img'}}">
<template is="wxParseImg" data="{{item}}" />
</block>
<!--a类型-->
<block wx:elif="{{item.tag == 'a'}}">
<view bindtap="wxParseTagATap" class="wxParse-inline {{item.classStr}} wxParse-{{item.tag}}" data-src="{{item.attr.href}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse7" data="{{item}}" />
</block>
</view>
</block>
<block wx:elif="{{item.tag == 'br'}}">
<template is="WxParseBr"></template>
</block>
<!--其他块级标签-->
<block wx:elif="{{item.tagType == 'block'}}">
<view class="{{item.classStr}} wxParse-{{item.tag}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse7" data="{{item}}" />
</block>
</view>
</block>
<!--内联标签-->
<view wx:else class="{{item.classStr}} wxParse-{{item.tag}} wxParse-{{item.tagType}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse7" data="{{item}}" />
</block>
</view>
</block>
<!--判断是否是文本节点-->
<block wx:elif="{{item.node == 'text'}}">
<!--如果是,直接进行-->
<template is="WxEmojiView" data="{{item}}" />
</block>
</template>
<!--循环模版-->
<template name="wxParse7">
<!--<template is="wxParse8" data="{{item}}" />-->
<!--判断是否是标签节点-->
<block wx:if="{{item.node == 'element'}}">
<block wx:if="{{item.tag == 'button'}}">
<button type="default" size="mini">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse8" data="{{item}}" />
</block>
</button>
</block>
<!--li类型-->
<block wx:elif="{{item.tag == 'li'}}">
<view class="{{item.classStr}} wxParse-li" style="{{item.styleStr}}">
<view class="{{item.classStr}} wxParse-li-inner">
<view class="{{item.classStr}} wxParse-li-text">
<view class="{{item.classStr}} wxParse-li-circle"></view>
</view>
<view class="{{item.classStr}} wxParse-li-text">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse8" data="{{item}}" />
</block>
</view>
</view>
</view>
</block>
<!--video类型-->
<block wx:elif="{{item.tag == 'video'}}">
<template is="wxParseVideo" data="{{item}}" />
</block>
<!--img类型-->
<block wx:elif="{{item.tag == 'img'}}">
<template is="wxParseImg" data="{{item}}" />
</block>
<!--a类型-->
<block wx:elif="{{item.tag == 'a'}}">
<view bindtap="wxParseTagATap" class="wxParse-inline {{item.classStr}} wxParse-{{item.tag}}" data-src="{{item.attr.href}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse8" data="{{item}}" />
</block>
</view>
</block>
<block wx:elif="{{item.tag == 'br'}}">
<template is="WxParseBr"></template>
</block>
<!--其他块级标签-->
<block wx:elif="{{item.tagType == 'block'}}">
<view class="{{item.classStr}} wxParse-{{item.tag}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse8" data="{{item}}" />
</block>
</view>
</block>
<!--内联标签-->
<view wx:else class="{{item.classStr}} wxParse-{{item.tag}} wxParse-{{item.tagType}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse8" data="{{item}}" />
</block>
</view>
</block>
<!--判断是否是文本节点-->
<block wx:elif="{{item.node == 'text'}}">
<!--如果是,直接进行-->
<template is="WxEmojiView" data="{{item}}" />
</block>
</template>
<!--循环模版-->
<template name="wxParse8">
<!--<template is="wxParse9" data="{{item}}" />-->
<!--判断是否是标签节点-->
<block wx:if="{{item.node == 'element'}}">
<block wx:if="{{item.tag == 'button'}}">
<button type="default" size="mini">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse9" data="{{item}}" />
</block>
</button>
</block>
<!--li类型-->
<block wx:elif="{{item.tag == 'li'}}">
<view class="{{item.classStr}} wxParse-li" style="{{item.styleStr}}">
<view class="{{item.classStr}} wxParse-li-inner">
<view class="{{item.classStr}} wxParse-li-text">
<view class="{{item.classStr}} wxParse-li-circle"></view>
</view>
<view class="{{item.classStr}} wxParse-li-text">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse9" data="{{item}}" />
</block>
</view>
</view>
</view>
</block>
<!--video类型-->
<block wx:elif="{{item.tag == 'video'}}">
<template is="wxParseVideo" data="{{item}}" />
</block>
<!--img类型-->
<block wx:elif="{{item.tag == 'img'}}">
<template is="wxParseImg" data="{{item}}" />
</block>
<!--a类型-->
<block wx:elif="{{item.tag == 'a'}}">
<view bindtap="wxParseTagATap" class="wxParse-inline {{item.classStr}} wxParse-{{item.tag}}" data-src="{{item.attr.href}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse9" data="{{item}}" />
</block>
</view>
</block>
<block wx:elif="{{item.tag == 'br'}}">
<template is="WxParseBr"></template>
</block>
<!--其他块级标签-->
<block wx:elif="{{item.tagType == 'block'}}">
<view class="{{item.classStr}} wxParse-{{item.tag}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse9" data="{{item}}" />
</block>
</view>
</block>
<!--内联标签-->
<view wx:else class="{{item.classStr}} wxParse-{{item.tag}} wxParse-{{item.tagType}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse9" data="{{item}}" />
</block>
</view>
</block>
<!--判断是否是文本节点-->
<block wx:elif="{{item.node == 'text'}}">
<!--如果是,直接进行-->
<template is="WxEmojiView" data="{{item}}" />
</block>
</template>
<!--循环模版-->
<template name="wxParse9">
<!--<template is="wxParse10" data="{{item}}" />-->
<!--判断是否是标签节点-->
<block wx:if="{{item.node == 'element'}}">
<block wx:if="{{item.tag == 'button'}}">
<button type="default" size="mini">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse10" data="{{item}}" />
</block>
</button>
</block>
<!--li类型-->
<block wx:elif="{{item.tag == 'li'}}">
<view class="{{item.classStr}} wxParse-li" style="{{item.styleStr}}">
<view class="{{item.classStr}} wxParse-li-inner">
<view class="{{item.classStr}} wxParse-li-text">
<view class="{{item.classStr}} wxParse-li-circle"></view>
</view>
<view class="{{item.classStr}} wxParse-li-text">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse10" data="{{item}}" />
</block>
</view>
</view>
</view>
</block>
<!--video类型-->
<block wx:elif="{{item.tag == 'video'}}">
<template is="wxParseVideo" data="{{item}}" />
</block>
<!--img类型-->
<block wx:elif="{{item.tag == 'img'}}">
<template is="wxParseImg" data="{{item}}" />
</block>
<!--a类型-->
<block wx:elif="{{item.tag == 'a'}}">
<view bindtap="wxParseTagATap" class="wxParse-inline {{item.classStr}} wxParse-{{item.tag}}" data-src="{{item.attr.href}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse10" data="{{item}}" />
</block>
</view>
</block>
<block wx:elif="{{item.tag == 'br'}}">
<template is="WxParseBr"></template>
</block>
<!--其他块级标签-->
<block wx:elif="{{item.tagType == 'block'}}">
<view class="{{item.classStr}} wxParse-{{item.tag}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse10" data="{{item}}" />
</block>
</view>
</block>
<!--内联标签-->
<view wx:else class="{{item.classStr}} wxParse-{{item.tag}} wxParse-{{item.tagType}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse10" data="{{item}}" />
</block>
</view>
</block>
<!--判断是否是文本节点-->
<block wx:elif="{{item.node == 'text'}}">
<!--如果是,直接进行-->
<template is="WxEmojiView" data="{{item}}" />
</block>
</template>
<!--循环模版-->
<template name="wxParse10">
<!--<template is="wxParse11" data="{{item}}" />-->
<!--判断是否是标签节点-->
<block wx:if="{{item.node == 'element'}}">
<block wx:if="{{item.tag == 'button'}}">
<button type="default" size="mini">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse11" data="{{item}}" />
</block>
</button>
</block>
<!--li类型-->
<block wx:elif="{{item.tag == 'li'}}">
<view class="{{item.classStr}} wxParse-li" style="{{item.styleStr}}">
<view class="{{item.classStr}} wxParse-li-inner">
<view class="{{item.classStr}} wxParse-li-text">
<view class="{{item.classStr}} wxParse-li-circle"></view>
</view>
<view class="{{item.classStr}} wxParse-li-text">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse11" data="{{item}}" />
</block>
</view>
</view>
</view>
</block>
<!--video类型-->
<block wx:elif="{{item.tag == 'video'}}">
<template is="wxParseVideo" data="{{item}}" />
</block>
<!--img类型-->
<block wx:elif="{{item.tag == 'img'}}">
<template is="wxParseImg" data="{{item}}" />
</block>
<!--a类型-->
<block wx:elif="{{item.tag == 'a'}}">
<view bindtap="wxParseTagATap" class="wxParse-inline {{item.classStr}} wxParse-{{item.tag}}" data-src="{{item.attr.href}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse11" data="{{item}}" />
</block>
</view>
</block>
<block wx:elif="{{item.tag == 'br'}}">
<template is="WxParseBr"></template>
</block>
<!--其他块级标签-->
<block wx:elif="{{item.tagType == 'block'}}">
<view class="{{item.classStr}} wxParse-{{item.tag}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse11" data="{{item}}" />
</block>
</view>
</block>
<!--内联标签-->
<view wx:else class="{{item.classStr}} wxParse-{{item.tag}} wxParse-{{item.tagType}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse11" data="{{item}}" />
</block>
</view>
</block>
<!--判断是否是文本节点-->
<block wx:elif="{{item.node == 'text'}}">
<!--如果是,直接进行-->
<template is="WxEmojiView" data="{{item}}" />
</block>
</template>
<!--循环模版-->
<template name="wxParse11">
<!--<template is="wxParse12" data="{{item}}" />-->
<!--判断是否是标签节点-->
<block wx:if="{{item.node == 'element'}}">
<block wx:if="{{item.tag == 'button'}}">
<button type="default" size="mini">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse12" data="{{item}}" />
</block>
</button>
</block>
<!--li类型-->
<block wx:elif="{{item.tag == 'li'}}">
<view class="{{item.classStr}} wxParse-li" style="{{item.styleStr}}">
<view class="{{item.classStr}} wxParse-li-inner">
<view class="{{item.classStr}} wxParse-li-text">
<view class="{{item.classStr}} wxParse-li-circle"></view>
</view>
<view class="{{item.classStr}} wxParse-li-text">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse12" data="{{item}}" />
</block>
</view>
</view>
</view>
</block>
<!--video类型-->
<block wx:elif="{{item.tag == 'video'}}">
<template is="wxParseVideo" data="{{item}}" />
</block>
<!--img类型-->
<block wx:elif="{{item.tag == 'img'}}">
<template is="wxParseImg" data="{{item}}" />
</block>
<!--a类型-->
<block wx:elif="{{item.tag == 'a'}}">
<view bindtap="wxParseTagATap" class="wxParse-inline {{item.classStr}} wxParse-{{item.tag}}" data-src="{{item.attr.href}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse12" data="{{item}}" />
</block>
</view>
</block>
<block wx:elif="{{item.tag == 'br'}}">
<template is="WxParseBr"></template>
</block>
<!--其他块级标签-->
<block wx:elif="{{item.tagType == 'block'}}">
<view class="{{item.classStr}} wxParse-{{item.tag}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse12" data="{{item}}" />
</block>
</view>
</block>
<!--内联标签-->
<view wx:else class="{{item.classStr}} wxParse-{{item.tag}} wxParse-{{item.tagType}}" style="{{item.styleStr}}">
<block wx:for="{{item.nodes}}" wx:for-item="item" wx:key="">
<template is="wxParse12" data="{{item}}" />
</block>
</view>
</block>
<!--判断是否是文本节点-->
<block wx:elif="{{item.node == 'text'}}">
<!--如果是,直接进行-->
<template is="WxEmojiView" data="{{item}}" />
</block>
</template>
\ No newline at end of file
/**
* 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{
margin: 0 5px;
font-family: Helvetica,sans-serif;
font-size: 28rpx;
color: #666;
line-height: 1.8;
}
view{
word-break:break-all;
}
.wxParse-inline{
display: inline;
margin: 0;
padding: 0;
}
/*//标题 */
.wxParse-div{margin: 0;padding: 0;}
.wxParse-h1{ font-size:2em; margin: .67em 0 }
.wxParse-h2{ font-size:1.5em; margin: .75em 0 }
.wxParse-h3{ font-size:1.17em; margin: .83em 0 }
.wxParse-h4{ margin: 1.12em 0}
.wxParse-h5 { font-size:.83em; margin: 1.5em 0 }
.wxParse-h6{ font-size:.75em; margin: 1.67em 0 }
.wxParse-h1 {
font-size: 18px;
font-weight: 400;
margin-bottom: .9em;
}
.wxParse-h2 {
font-size: 16px;
font-weight: 400;
margin-bottom: .34em;
}
.wxParse-h3 {
font-weight: 400;
font-size: 15px;
margin-bottom: .34em;
}
.wxParse-h4 {
font-weight: 400;
font-size: 14px;
margin-bottom: .24em;
}
.wxParse-h5 {
font-weight: 400;
font-size: 13px;
margin-bottom: .14em;
}
.wxParse-h6 {
font-weight: 400;
font-size: 12px;
margin-bottom: .04em;
}
.wxParse-h1, .wxParse-h2, .wxParse-h3, .wxParse-h4, .wxParse-h5, .wxParse-h6, .wxParse-b, .wxParse-strong { font-weight: bolder }
.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{white-space:pre}
.wxParse-big{font-size:1.17em}
.wxParse-small,.wxParse-sub,.wxParse-sup{font-size:.83em}
.wxParse-sub{vertical-align:sub}
.wxParse-sup{vertical-align:super}
.wxParse-s,.wxParse-strike,.wxParse-del{text-decoration:line-through}
/*wxparse-自定义个性化的css样式*/
/*增加video的css样式*/
.wxParse-strong,.wxParse-s{display: inline}
.wxParse-a{
color: deepskyblue;
word-break:break-all;
overflow:auto;
}
.wxParse-video{
text-align: center;
margin: 10px 0;
}
.wxParse-video-video{
width:100%;
}
.wxParse-img{
/*background-color: #efefef;*/
overflow: hidden;
}
.wxParse-blockquote {
margin: 0;
padding:10px 0 10px 5px;
font-family:Courier, Calibri,"宋体";
background:#f5f5f5;
border-left: 3px solid #dbdbdb;
}
.wxParse-code,.wxParse-wxxxcode-style{
display: inline;
background:#f5f5f5;
}
.wxParse-ul{
margin: 20rpx 10rpx;
}
.wxParse-li,.wxParse-li-inner{
display: flex;
align-items: baseline;
margin: 10rpx 0;
}
.wxParse-li-text{
align-items: center;
line-height: 20px;
}
.wxParse-li-circle{
display: inline-flex;
width: 5px;
height: 5px;
background-color: #333;
margin-right: 5px;
}
.wxParse-li-square{
display: inline-flex;
width: 10rpx;
height: 10rpx;
background-color: #333;
margin-right: 5px;
}
.wxParse-li-ring{
display: inline-flex;
width: 10rpx;
height: 10rpx;
border: 2rpx solid #333;
border-radius: 50%;
background-color: #fff;
margin-right: 5px;
}
/*.wxParse-table{
width: 100%;
height: 400px;
}
.wxParse-thead,.wxParse-tfoot,.wxParse-tr{
display: flex;
flex-direction: row;
}
.wxParse-th,.wxParse-td{
display: flex;
width: 580px;
overflow: auto;
}*/
.wxParse-u {
text-decoration: underline;
}
.wxParse-hide{
display: none;
}
.WxEmojiView{
align-items: center;
}
.wxEmoji{
width: 16px;
height:16px;
}
.wxParse-tr{
display: flex;
border-right:1px solid #e0e0e0;
border-bottom:1px solid #e0e0e0;
border-top:1px solid #e0e0e0;
}
.wxParse-th,
.wxParse-td{
flex:1;
padding:5px;
font-size:28rpx;
border-left:1px solid #e0e0e0;
word-break: break-all;
}
.wxParse-td:last{
border-top:1px solid #e0e0e0;
}
.wxParse-th{
background:#f0f0f0;
border-top:1px solid #e0e0e0;
}
.wxParse-del{
display: inline;
}
.wxParse-figure {
overflow: hidden;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment