@@ -15,28 +15,6 @@ App({ | |||
}, | |||
}) | |||
// 获取小程序AccessToken | |||
const getWxToken = secret => { | |||
const data = { | |||
grant_type: 'client_credential', | |||
appid: 'wxf288a9b8167ff2ca', | |||
secret: secret | |||
} | |||
wx.request({ | |||
url: 'https://api.weixin.qq.com/cgi-bin/stable_token', | |||
header: { | |||
"Content-Type": "application/json;charset=UTF-8", | |||
}, | |||
data: data, | |||
method: "POST", | |||
success: res => { | |||
console.log(res, 'getWxToken'); | |||
wx.setStorageSync('AccessToken', res.data.access_token) | |||
}, | |||
fail: err => { } | |||
}); | |||
} | |||
/** | |||
* @description 登录 | |||
* @param {*} loginData {code , appid} | |||
@@ -88,9 +66,15 @@ App({ | |||
res.data.validEndTime = res.data.validEndTime ? timestampToTime(res.data.validEndTime, 'YYYY-MM-DD hh:mm:ss') : '' | |||
const userInfo = res.data | |||
that.globalData.userInfo = userInfo | |||
// getWxToken(res.data.secret) | |||
// 请求完成后的回调,在index.js中调用防止异步 | |||
that.userInfoCallback(res) | |||
wx.setStorageSync('promotContentCount', res.data.promotContentCount) | |||
that.globalData.promotContentCount = res.data.promotContentCount | |||
wx.setStorageSync('completionContentCount', res.data.completionContentCount) | |||
that.globalData.completionContentCount = res.data.completionContentCount | |||
}).catch(err => { | |||
console.log(err, 'err'); | |||
}) | |||
@@ -5,7 +5,8 @@ | |||
"pages/payOrder/payOrder", | |||
"pages/payHistory/payHistory", | |||
"pages/chat/chat", | |||
"pages/orderDetail/orderDetail" | |||
"pages/orderDetail/orderDetail", | |||
"pages/userProtocol/userProtocol" | |||
], | |||
"window": { | |||
"backgroundTextStyle": "light", | |||
@@ -10,15 +10,18 @@ Page({ | |||
chatList: [], | |||
saysContentLength: 50, | |||
message: "", | |||
waitingMessage: "", | |||
isResponing: false, | |||
isLoading: false, | |||
chatOver: false, | |||
messageFlag: true, | |||
istouching: false, | |||
isHolding: false, | |||
scrollTop: 0, | |||
timer: null, | |||
clearTimer: null | |||
clearTimer: null, | |||
interval: null, | |||
promotContentCount: 0, | |||
completionContentCount: 0 | |||
}, | |||
/** | |||
@@ -27,53 +30,44 @@ Page({ | |||
onLoad() { | |||
this.socketLink() | |||
this.chatLimit() | |||
this.setData({ | |||
promotContentCount: wx.getStorageSync('promotContentCount'), | |||
completionContentCount: wx.getStorageSync('completionContentCount') | |||
}) | |||
}, | |||
// 敏感词检测 | |||
msgSec(content) { | |||
msgSecCheck(message) { | |||
const that = this | |||
return new Promise((resolve, reject) => { | |||
const AccessToken = wx.getStorageSync('AccessToken') | |||
const openId = wx.getStorageSync('openId') | |||
const data = { | |||
content, | |||
version: 2, | |||
scene: 2, // 场景枚举值(1 资料;2 评论;3 论坛;4 社交日志) | |||
openid: openId | |||
appId: 'wxf288a9b8167ff2ca', | |||
message | |||
} | |||
wx.request({ | |||
url: `https://api.weixin.qq.com/wxa/msg_sec_check?access_token=${AccessToken}`, | |||
header: { | |||
"Content-Type": "application/json;charset=UTF-8", | |||
}, | |||
data: data, | |||
method: "POST", | |||
success: res => { | |||
const label = res.data.result.label | |||
// 其他敏感词 | |||
if (label == 100) { | |||
resolve() | |||
} else if (label == 21000) { | |||
wx.showToast({ | |||
title: `文本涉及敏感词汇`, | |||
icon: 'none' | |||
}) | |||
reject() | |||
} else { | |||
// 枚举中的敏感词 | |||
const msg = keyWordsEnum.filter(item => item.label == label)[0].msg | |||
wx.showToast({ | |||
title: `文本涉及${msg}敏感词汇`, | |||
icon: 'none' | |||
}) | |||
reject() | |||
} | |||
}, | |||
fail: err => { } | |||
}); | |||
request.post({ | |||
url: '/api/chat/msgSecCheck', data | |||
}).then(res => { | |||
console.log(res, 'msgSecCheck'); | |||
if (res.code == 200) { | |||
resolve() | |||
} else { | |||
reject() | |||
wx.showToast({ | |||
title: `文本涉及敏感词!`, | |||
}) | |||
} | |||
}).catch(err => { | |||
console.log(err, 'err'); | |||
reject() | |||
wx.showToast({ | |||
title: `文本涉及敏感词!`, | |||
icon: 'error' | |||
}) | |||
}) | |||
}) | |||
}, | |||
chatLimit() { | |||
request.get({ | |||
url: '/api/chat/limit', | |||
@@ -101,44 +95,83 @@ Page({ | |||
}) | |||
}, | |||
preMessageSend(message) { | |||
const promotContentCount = this.data.promotContentCount | |||
const completionContentCount = this.data.completionContentCount | |||
let chatList = JSON.parse(JSON.stringify(this.data.chatList)) | |||
chatList.shift() | |||
console.log(chatList, 'chatListIII'); | |||
const len = chatList.length | |||
const msg = [] | |||
if (len == 0) { | |||
const obj = { | |||
role: 'user', | |||
content: message | |||
} | |||
msg.push(obj) | |||
} else if (len <= promotContentCount + completionContentCount) { | |||
let tempObj = null | |||
chatList.forEach(item => { | |||
tempObj = { | |||
role: 'user', | |||
content: item.message | |||
} | |||
msg.push(tempObj) | |||
}) | |||
} else if (len > promotContentCount + completionContentCount) { | |||
const index = promotContentCount + completionContentCount | |||
let tempObj = null | |||
const arr = chatList.slice(-1 * index) | |||
arr.forEach(item => { | |||
tempObj = { | |||
role: 'user', | |||
content: item.message | |||
} | |||
msg.push(tempObj) | |||
}) | |||
} | |||
console.log(JSON.stringify(msg), 'msg'); | |||
this.socketSend(JSON.stringify(msg)) | |||
}, | |||
// 向socket连接端发送数据 | |||
send() { | |||
if (!this.data.message) return | |||
this.msgSecCheck(this.data.message) | |||
.then(() => { | |||
console.log('非敏感词,放行'); | |||
// 当socket连接被关闭时,重新连接socket | |||
if (this.data.reLoadSocket) { | |||
this.socketLink(this.data.message) | |||
} | |||
// 当socket连接被关闭时,重新连接socket | |||
if (this.data.reLoadSocket) { | |||
this.socketLink() | |||
this.setData({ | |||
waitingMessage: this.data.message | |||
}) | |||
} | |||
const data = { | |||
message: this.data.message, | |||
id: 1 // 带id为用户发送的讯息,不带则为服务器返回的讯息 | |||
} | |||
const arr = this.data.chatList | |||
arr.push(data) | |||
const data = { | |||
message: this.data.message, | |||
id: 1 // 带id为用户发送的讯息,不带则为服务器返回的讯息 | |||
} | |||
// 非重载状态下向服务器发送讯息 | |||
if (!this.data.reLoadSocket) { | |||
this.socketSend(this.data.message) | |||
} | |||
const arr = this.data.chatList | |||
arr.push(data) | |||
this.setData({ | |||
chatList: arr, | |||
message: '', | |||
messageFlag: true | |||
}) | |||
console.log(this.data.chatList, 'chatList'); | |||
// 非重载状态下向服务器发送讯息 | |||
if (!this.data.reLoadSocket) { | |||
this.setData({ | |||
isLoading: true | |||
}) | |||
this.preMessageSend(this.data.message) | |||
} | |||
this.msgSec(this.data.message) | |||
.then(() => { | |||
console.log('非敏感词,放行'); | |||
}).catch(() => { | |||
console.log('触发敏感词!'); | |||
this.setData({ | |||
chatList: arr, | |||
message: '', | |||
messageFlag: true | |||
}) | |||
console.log(this.data.chatList, 'chatList'); | |||
}).catch((err) => { | |||
console.log('触发敏感词!', err); | |||
}) | |||
}, | |||
@@ -187,7 +220,8 @@ Page({ | |||
}, | |||
// 进行socket连接 | |||
socketLink() { | |||
socketLink(waitingMessage) { | |||
console.log('Link Start!'); | |||
const that = this | |||
wx.connectSocket({ | |||
url: 'wss://gptsockettest.malls.iformall.com:8010', | |||
@@ -195,20 +229,25 @@ Page({ | |||
'content-type': 'application/json' | |||
}, | |||
success: function (res) { | |||
console.log('Link Successed!'); | |||
console.log(res) | |||
if (!that.data.reLoadSocket) { | |||
if (!waitingMessage) { | |||
that.sayHello('Hello,我是小闲聊,你可以和我聊天,😘😘😘') | |||
} | |||
if (that.data.waitingMessage) { | |||
that.socketSend(that.data.waitingMessage) | |||
if (waitingMessage) { | |||
console.log('reLoadSocket,waitingMessage!'); | |||
setTimeout(() => { | |||
that.preMessageSend(waitingMessage) | |||
}, 2000); | |||
that.setData({ | |||
reLoadSocket: false | |||
reLoadSocket: false, | |||
isLoading: true | |||
}) | |||
} | |||
wx.onSocketClose(() => { | |||
console.log('Socket连接已关闭'); | |||
console.log('Link Over!'); | |||
that.scrollToBottom('scrollSingel') | |||
that.setData({ | |||
reLoadSocket: true | |||
@@ -216,10 +255,11 @@ Page({ | |||
}) | |||
}, | |||
fail: function (err) { | |||
console.log('Link fialed!'); | |||
console.log(err) | |||
}, | |||
complete: function (info) { | |||
console.log(info) | |||
// console.log(info) | |||
} | |||
}) | |||
}, | |||
@@ -227,16 +267,20 @@ Page({ | |||
socketSend(data) { | |||
const that = this | |||
const msgArr = [] | |||
clearInterval(that.data.interval) | |||
wx.sendSocketMessage({ | |||
data: data, | |||
success: function (res) { | |||
// 返回成功后清除轮询 | |||
clearInterval(that.data.interval) | |||
console.log(res) | |||
wx.onSocketMessage(data => { | |||
const msgBack = data.data | |||
console.log(msgBack); | |||
if (msgBack == '___talk_end___') { | |||
that.setData({ | |||
isResponing: false | |||
isResponing: false, | |||
isLoading: false | |||
}) | |||
// setTimeout(() => { | |||
// console.log('10s'); | |||
@@ -252,6 +296,12 @@ Page({ | |||
}, | |||
fail: function (err) { | |||
console.log(err) | |||
// 若socket连接尚未建立,则2s发一次轮询 | |||
that.setData({ | |||
interval: setInterval(() => { | |||
that.socketSend(data) | |||
}, 2000) | |||
}) | |||
}, | |||
complete: function (info) { | |||
console.log(info) | |||
@@ -169,6 +169,13 @@ page { | |||
} | |||
} | |||
} | |||
.loading { | |||
position: absolute; | |||
left: -60rpx; | |||
height: 40rpx; | |||
animation: rotation 2s linear infinite; | |||
} | |||
} | |||
.userAvatar { | |||
@@ -9,8 +9,11 @@ | |||
<image wx:if="{{item.id != 1}}" class="arrowWhite" src="../../asset/icon/arrowWhite.png" mode="heightFix" /> | |||
<view class="{{item.id == 1 ? 'message right' : 'message left'}}"> | |||
<text user-select="true">{{item.message}}</text> | |||
<image class="loading" wx:if="{{item.id == 1 && (index + 1 == chatList.length) && isLoading}}" src="../../asset/icon/loading.png" mode="heightFix" /> | |||
<!-- <view wx:if="{{item.id == 1}}" id="copyBox" class="{{isHolding && istouchingIndex == index ? 'copyBoxRight active' : 'copyBoxRight'}}" bindtap="copyMessage" data-message="{{item.message}}"> | |||
<view class="boxInside"> | |||
<view class="text">复制</view> | |||
@@ -27,13 +30,12 @@ | |||
</view> | |||
<image wx:if="{{item.id == 1}}" class="arrowGreen" src="../../asset/icon/arrowGreen.png" mode="heightFix" /> | |||
</view> | |||
<image class="userAvatar" wx:if="{{item.id && item.id == 1}}" src="../../asset/icon/user.png" mode="heightFix" /> | |||
</view> | |||
</view> | |||
<view class='over' wx:if="{{reLoadSocket}}">聊天已结束</view> | |||
<view class='over' wx:if="{{reLoadSocket}}">聊天已挂起</view> | |||
</view> | |||
<view class="chatBox"> | |||
@@ -14,7 +14,8 @@ Page({ | |||
holdingSwitch: false, | |||
logoActive: false, | |||
timer: null, | |||
touchFlag: "" | |||
touchFlag: "", | |||
isNotRead: true | |||
}, | |||
// 事件处理函数 | |||
bindViewTap() { | |||
@@ -46,6 +47,19 @@ Page({ | |||
}) | |||
}, | |||
agreedClick() { | |||
this.setData({ | |||
isNotRead: !this.data.isNotRead | |||
}) | |||
}, | |||
// 跳转至用户协议页面 | |||
checkUserProtocol() { | |||
wx.navigateTo({ | |||
url: '/pages/userProtocol/userProtocol', | |||
}) | |||
}, | |||
// 跳转至购买历史页面 | |||
checkPayHistory() { | |||
wx.navigateTo({ | |||
@@ -110,6 +110,40 @@ | |||
font-size: 25rpx; | |||
margin-top: 150rpx; | |||
.userProtocolModel { | |||
text-align: center; | |||
margin-bottom: 30rpx; | |||
.textInside { | |||
position: relative; | |||
.agreeButton { | |||
position: absolute; | |||
left: 115rpx; | |||
width: 30rpx; | |||
height: 30rpx; | |||
line-height: 35rpx; | |||
border: 1px solid #464646; | |||
border-radius: 50% 50%; | |||
color: #ffffff; | |||
text-align: center; | |||
&.active { | |||
border: 1px solid #07c160; | |||
background-color: #07c160; | |||
} | |||
} | |||
.userProtocol { | |||
text-align: center; | |||
color: #929292; | |||
text-decoration: underline; | |||
margin-bottom: 15rpx; | |||
} | |||
} | |||
} | |||
.userPhone { | |||
text-align: center; | |||
margin-bottom: 15rpx; | |||
@@ -25,20 +25,33 @@ | |||
<block wx:if="{{!isDevMode}}"> | |||
<block wx:if="{{!userInfo.phone}}"> | |||
<button type="primary" open-type="getPhoneNumber" bindgetphonenumber="getUserPhone">申请使用</button> | |||
<view class="userProtocolModel"> | |||
<view class="textInside"> | |||
<view class="{{isNotRead ? 'agreeButton' : 'agreeButton active'}}" bindtap="agreedClick">√</view> | |||
<text>我已阅读并同意</text> | |||
<text class="userProtocol" bindtap="checkUserProtocol">《用户协议及隐私政策》</text> | |||
</view> | |||
</view> | |||
<button type="primary" open-type="getPhoneNumber" bindgetphonenumber="getUserPhone" disabled="{{isNotRead}}">申请使用</button> | |||
</block> | |||
<block wx:if="{{userInfo.isMember == 1 && userInfo.isValid == 1}}"> | |||
<view class="userPhone">当前用户:{{userInfo.phone}}</view> | |||
<view class="userPhone" wx:if="{{userInfo.validEndTime}}">到期时间:{{userInfo.validEndTime}}</view> | |||
<view class="payHistory" bindtap="checkPayHistory">购买历史</view> | |||
<view class="payHistory"> | |||
<text style="margin-right: 20rpx;" bindtap="checkUserProtocol">用户协议</text> | |||
<text bindtap="checkPayHistory">购买历史</text> | |||
</view> | |||
<button type="primary" bindtap="goToChat">开始聊天</button> | |||
</block> | |||
<block wx:if="{{userInfo.isMember == 1 && userInfo.isValid == 0}}"> | |||
<view class="userPhone">当前用户:{{userInfo.phone}}</view> | |||
<view class="userPhone" wx:if="{{userInfo.validEndTime}}">到期时间:{{userInfo.validEndTime}}</view> | |||
<view class="payHistory" bindtap="checkPayHistory">购买历史</view> | |||
<view class="payHistory"> | |||
<text bindtap="checkUserProtocol">用户协议</text> | |||
<text bindtap="checkPayHistory">购买历史</text> | |||
</view> | |||
<button type="primary" bindtap="goToBuy">续费会员</button> | |||
</block> | |||
@@ -0,0 +1,66 @@ | |||
// pages/userProtocol/userProtocol.ts | |||
Page({ | |||
/** | |||
* 页面的初始数据 | |||
*/ | |||
data: { | |||
}, | |||
/** | |||
* 生命周期函数--监听页面加载 | |||
*/ | |||
onLoad() { | |||
}, | |||
/** | |||
* 生命周期函数--监听页面初次渲染完成 | |||
*/ | |||
onReady() { | |||
}, | |||
/** | |||
* 生命周期函数--监听页面显示 | |||
*/ | |||
onShow() { | |||
}, | |||
/** | |||
* 生命周期函数--监听页面隐藏 | |||
*/ | |||
onHide() { | |||
}, | |||
/** | |||
* 生命周期函数--监听页面卸载 | |||
*/ | |||
onUnload() { | |||
}, | |||
/** | |||
* 页面相关事件处理函数--监听用户下拉动作 | |||
*/ | |||
onPullDownRefresh() { | |||
}, | |||
/** | |||
* 页面上拉触底事件的处理函数 | |||
*/ | |||
onReachBottom() { | |||
}, | |||
/** | |||
* 用户点击右上角分享 | |||
*/ | |||
onShareAppMessage() { | |||
} | |||
}) |
@@ -0,0 +1,4 @@ | |||
{ | |||
"navigationBarTitleText": "用户协议", | |||
"usingComponents": {} | |||
} |
@@ -0,0 +1,25 @@ | |||
/* pages/userProtocol/userProtocol.wxss */ | |||
.content { | |||
padding: 30rpx; | |||
color: #525252; | |||
view { | |||
margin-bottom: 30rpx; | |||
} | |||
.title { | |||
font-weight: 600; | |||
font-size: 35rpx; | |||
text-align: center; | |||
} | |||
.paragraph { | |||
text-indent: 40rpx; | |||
} | |||
.item { | |||
.titleInside { | |||
font-weight: 600; | |||
} | |||
} | |||
} |
@@ -0,0 +1,115 @@ | |||
<!--pages/userProtocol/userProtocol.wxml--> | |||
<view class="content"> | |||
<view class="title">网络用户协议</view> | |||
<view class="paragraph"> | |||
欢迎使用小闲聊小程序! | |||
小闲聊小程序所有服务和产品均受以下条款和条件的约束。请仔细阅读本产品的用户协议(以下简称“协议”)并确保您完全理解并同意其中的条款和条件。如果您不同意本协议中的任何内容,您应立即停止使用本产品。 | |||
</view> | |||
<view class="item"> | |||
<view class="titleInside"> 1. 服务</view> | |||
本产品提供的服务仅供您个人使用。您不得将任何服务或产品用于商业目的。本产品有权随时更改或中断服务或产品的供应,并且不承担任何责任。 | |||
</view> | |||
<view class="item"> | |||
<view class="titleInside">2. 用户注册信息</view> | |||
<view> | |||
<view> | |||
2.1 用户应该注册真实的个人信息,包括但不限于姓名、地址、联系电话、电子邮件地址等。用户应该及时更新这些信息。 | |||
</view> | |||
<view> | |||
2.2 用户应该保护好自己的账户和密码,避免泄露。并承担您账户下的所有活动的责任。如果您发现未经授权使用您的账户或任何其他安全漏洞,您应立即通知我司。 | |||
</view> | |||
<view> | |||
2.3 如有任何违反本协议的行为,本产品有权随时停止或终止用户的账户。 | |||
</view> | |||
</view> | |||
</view> | |||
<view class="item"> | |||
<view class="titleInside">3. 用户行为</view> | |||
<view> | |||
<view> | |||
3.1 用户应该遵守法律法规和社会公德,不得在本产品发布任何违法内容。 | |||
</view> | |||
<view> | |||
3.2 用户应该尊重其他用户的权利和利益,不得侵犯他人的知识产权等合法权益。 | |||
</view> | |||
<view> | |||
3.3 用户应该保证自己发布的信息真实、准确、完整、不误导。 | |||
</view> | |||
<view> | |||
3.4 用户不得利用本产品进行任何商业活动或广告宣传行为。 | |||
</view> | |||
<view> | |||
3.5 您同意不会在本产品上从事任何非法、欺诈、骚扰、侵犯隐私、垃圾邮件或其他违法或侵权行为。如果本产品发现您从事这些行为,则有权立即终止您的账户或限制您的访问。 | |||
</view> | |||
</view> | |||
</view> | |||
<view class="item"> | |||
<view> | |||
<view class="titleInside">4. 知识产权</view> | |||
</view> | |||
<view> | |||
4.1 本产品上所有内容(包括但不限于文字、图片、音频、视频等)均为本产品所有。包括但不限于商标、版权和专利。您不得复制、修改、分发或创建派生作品。您同意不会挑战我们的所有权或控制权。 | |||
</view> | |||
<view> | |||
4.2 本产品允许用户发布或提交内容,包括但不限于文字、照片、视频、音频等。您保证对于您发布或提交的内容,您拥有必要的权利和许可,并且这些内容不会侵犯任何第三方的权利。我司有权删除或编辑用户提交的任何内容。 | |||
</view> | |||
<view> | |||
4.3 本产品上的所有内容,包括但不限于文字、图片、音频、视频、图形、商标和标识等,均受版权、商标和其他知识产权法律的保护。未经本网站或相关权利人书面许可,您不得复制、传播、转载、修改、发布、展示或以其他方式使用这些内容。 | |||
</view> | |||
</view> | |||
<view class="item"> | |||
<view class="titleInside">5. 付费声明</view> | |||
<view> | |||
<view> | |||
5.1 本产品拥有完全自主定价权,所有价格及相关免费、优惠政策,相关调整时间由我司自行决定,如果您继续使用本产品,则视为您接受修改后的价格政策。 | |||
</view> | |||
<view> | |||
5.2 本产品由于开通即可服务,一经购买不能退款,如果您继续使用本产品,则视为您接受该政策。 | |||
</view> | |||
</view> | |||
</view> | |||
<view class="item"> | |||
<view class="titleInside">6. 免责声明</view> | |||
本产品不对任何信息的准确性、完整性、可靠性或适用性作任何明示或暗示的保证。本产品不对由于使用或无法使用本产品而导致的任何损失或损害承担任何责任。 | |||
</view> | |||
<view class="item"> | |||
<view class="titleInside">7. 法律适用</view> | |||
本协议受中华人民共和国法律的管辖。任何因本协议产生的纠纷,应提交到有管辖权的法院解决。 | |||
</view> | |||
<view class="item"> | |||
<view class="titleInside">8. 协议变更</view> | |||
本产品有权随时修改本协议的条款和条件。一旦本协议的任何条款和条件发生变化,您必须遵守修改后的协议。如果您继续使用本产品,则视为您接受修改后的协议。 | |||
</view> | |||
<view class="item"> | |||
<view class="titleInside">9. 终止</view> | |||
本产品有权随时终止本协议,而无需事先通知您。如果您违反本协议中的任何条款和条件,本产品有权立即终止您的账户和访问权限。 | |||
</view> | |||
<view class="item"> | |||
<view class="titleInside">10. 其他</view> | |||
本协议构成您与本产品之间的完整协议,取代了一切先前的协议和谈判。如果本协议中的任何条款或条件被认为无效或不可执行,则该条款或条件将被视为与本协议的其余部分分离,不影响其有效性和可执行性。本产品未行使或强制执行本协议中规定的任何权利或条款,并不构成对该权利或条款的豁免。 | |||
</view> | |||
<view class="item"> | |||
<view class="titleInside">如果您对这些条款及条件有任何问题,请联系我们。</view> | |||
</view> | |||
</view> |
@@ -33,7 +33,7 @@ | |||
"miniprogram/pages/payOrder/payOrder.js", | |||
"miniprogram/pages/payHistory/payHistory.js", | |||
"miniprogram/pages/chat/chat.js", | |||
"miniprogram/pages/orderDetail/orderDetail.js", | |||
"miniprogram/pages/orderDetail/orderDetail.js", "miniprogram/pages/userProtocol/userProtocol.js", | |||
], | |||
"exclude": [ | |||
"node_modules" | |||