508 lines
14 KiB
Vue
508 lines
14 KiB
Vue
<template>
|
||
<view>
|
||
<uni-popup ref="smsPopup" type="center" @change="popupChange" :is-mask-click="false">
|
||
<view class="sms-manager">
|
||
<view class="sms-manager--header mb">群发短信</view>
|
||
<uni-forms :label-width="100" :modelValue="smsDataModel" ref="smsForm">
|
||
<uni-forms-item v-if="toType === 'user' && !isSelectedReceiver" label="目标对象" name="smsPreset" :rules="[{ required: true, errorMessage: '请选择目标对象' }]" required>
|
||
<uni-data-select class="type m" placeholder="预设条件" size="mini" :clear="false"
|
||
:localdata="smsPresetList" v-model="smsDataModel.smsPreset">
|
||
</uni-data-select>
|
||
<view class="sms-data-tip">如需给指定用户发送,请在列表选择要发送的用户。</view>
|
||
</uni-forms-item>
|
||
<uni-forms-item label="目标对象" v-else-if="toType === 'user' && isSelectedReceiver">
|
||
<view>当前已选择{{ receiver.length }}人</view>
|
||
</uni-forms-item>
|
||
<uni-forms-item label="目标对象" v-else-if="toType === 'userTags' ">
|
||
<view>当前已选择{{ receiver.length }}个标签</view>
|
||
<view class="sms-data-tip">如标签关联的用户没有绑定手机号,将不会发送短信。</view>
|
||
</uni-forms-item>
|
||
|
||
<uni-forms-item label="跨分页选择" v-if="isSelectedReceiver && hasCondition">
|
||
<checkbox-group @change="smsFilteredChange">
|
||
<checkbox style="transform: scale(.9)" :checked="smsDataModel.filtered"></checkbox>
|
||
</checkbox-group>
|
||
<view class="sms-data-tip">对用户进行了筛选后,可能存在分页无法全部选中时,请勾选跨分页选中</view>
|
||
</uni-forms-item>
|
||
<uni-forms-item label="任务名称" name="name" required
|
||
:rules="[{ required: true, errorMessage: '请输入任务名称' }]">
|
||
<uni-easyinput v-model="smsDataModel.name" placeholder="请输入任务名称,例如 “7日内未登录用户召回”"/>
|
||
</uni-forms-item>
|
||
<uni-forms-item required label="短信模板" name="templateId"
|
||
:rules="[{ required: true, errorMessage: '请选择短信模板' }]">
|
||
<template v-if="!smsTemplateLoading">
|
||
<view v-if="smsTemplate.length">
|
||
<uni-data-select class="type m" placeholder="请选择短信模板" size="mini" :clear="false"
|
||
:localdata="smsTemplate" v-model="smsDataModel.templateId"
|
||
@change="onSmsTemplateSelected">
|
||
</uni-data-select>
|
||
<view class="sms-data-tip">
|
||
导入短信模版参考<a class="a-link" href="https://uniapp.dcloud.net.cn/uniCloud/admin.html#群发短信"
|
||
target="_blank">教程</a>;若有新的短信模版,可
|
||
<text @click="chooseFile"
|
||
class="a-link">点此导入
|
||
</text>
|
||
</view>
|
||
</view>
|
||
<view v-else>
|
||
<button @click="chooseFile" type="primary" style="width: 120px;"
|
||
size="mini">上传短信模板
|
||
</button>
|
||
<view class="sms-data-tip">当前未导入短信模板,请从dev.dcloud.net.cn的短信-<a
|
||
href="https://dev.dcloud.net.cn/pages/sms/template" target="_blank">模板配置</a>中导出短信模版,并在此导入。教程<a
|
||
href="https://uniapp.dcloud.net.cn/uniCloud/admin.html#batch-sms" target="_blank">详见</a></view>
|
||
</view>
|
||
</template>
|
||
<template v-else>
|
||
模板加载中...
|
||
</template>
|
||
</uni-forms-item>
|
||
<uni-forms-item label="短信内容" v-if="smsTemplateContent">
|
||
<view class="form-item-flex-center">{{ smsTemplateContent }}</view>
|
||
</uni-forms-item>
|
||
<uni-forms-item label="模板变量配置" :error-message="smsTemplateDataErrorMessage"
|
||
v-if="smsDataModel.templateData.length">
|
||
<view class="sms-data-item" :key="template.field"
|
||
v-for="(template, index) in smsDataModel.templateData">
|
||
<uni-easyinput class="field m" v-model="template.field" placeholder="字段" :clearable="false"
|
||
:disabled="true" style="width: 120px;flex:none;"/>
|
||
<uni-easyinput class="value m" v-model="template.value"
|
||
placeholder="例 {uni-id-users.username}" :clearable="false"/>
|
||
</view>
|
||
<view class="sms-data-tip">
|
||
短信变量支持固定值和数据表查询两种方式;固定值如:各位同事,数据表查询如:{uni-id-users.username};请注意,若使用数据表查询方式,目前仅支持查询
|
||
uni-id-users 表;并注意确保数据库中查询字段值不为空,否则短信将发送失败。
|
||
</view>
|
||
</uni-forms-item>
|
||
</uni-forms>
|
||
<view class="uni-group">
|
||
<button @click="sendSms(true)" class="uni-button">预览</button>
|
||
<button @click="sendSms()" class="uni-button" type="primary">提交</button>
|
||
</view>
|
||
</view>
|
||
<uni-icons type="closeempty" size="24" class="close" @click="close"></uni-icons>
|
||
</uni-popup>
|
||
<uni-popup ref="previewPopup" type="center" :is-mask-click="false">
|
||
<view class="sms-manager preview">
|
||
<view class="sms-manager--header mb">
|
||
<view>短信预览</view>
|
||
<view class="sub-title">仅预览第一条短信内容</view>
|
||
<view class="sub-title">预计送达 <text style="color: red">{{smsSendUserCount}}</text> 位用户</view>
|
||
</view>
|
||
<view class="content">
|
||
<view v-for="(content,index) of smsPreviewContent" :key="index">{{ content }}</view>
|
||
<view class="length">短信字数:
|
||
<text class="num">{{ smsPreviewContent.length ? smsPreviewContent[0].length : 0 }}</text>
|
||
字
|
||
</view>
|
||
</view>
|
||
<view class="tip">
|
||
<view>说明:</view>
|
||
<view>若从数据表中查询,字段内容长度会影响总字数,短信字数=短信签名字数+短信内容字数。</view>
|
||
<view>短信长度不超过70个字,按照一条短信计费;超过70个字,按照67字/条拆分成多条计费。</view>
|
||
</view>
|
||
<view class="uni-group">
|
||
<button @click="$refs.previewPopup.close()" class="uni-button">关闭</button>
|
||
</view>
|
||
</view>
|
||
</uni-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
const uniSmsCo = uniCloud.importObject('uni-sms-co')
|
||
|
||
export default {
|
||
name: 'batchSms',
|
||
props: {
|
||
// 发送类型 user|userTags
|
||
toType: String,
|
||
// 接收者 user=user._id, userTags=tag.id
|
||
receiver: {
|
||
type: Array,
|
||
default() {
|
||
return []
|
||
}
|
||
},
|
||
// 条件;跨分页选择时需要
|
||
condition: {
|
||
type: Object,
|
||
default () {
|
||
return {}
|
||
}
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
smsTemplateLoading: false,
|
||
smsPresetList: [{
|
||
value: 'all',
|
||
text: '全部用户',
|
||
},{
|
||
value: '7-day-offline-users',
|
||
text: '7天内未登录用户',
|
||
},{
|
||
value: '15-day-offline-users',
|
||
text: '15天内未登录用户',
|
||
},{
|
||
value: '30-day-offline-users',
|
||
text: '30天内未登录用户',
|
||
}],
|
||
smsTemplate: [],
|
||
smsTemplateDataErrorMessage: '',
|
||
smsDataModel: {
|
||
name: '',
|
||
templateId: '',
|
||
templateData: [],
|
||
smsPreset: '',
|
||
filtered: false
|
||
},
|
||
smsTemplateContent: '',
|
||
smsPreviewContent: [],
|
||
smsSendUserCount: 0
|
||
}
|
||
},
|
||
computed: {
|
||
isSelectedReceiver() {
|
||
return !!this.receiver.length
|
||
},
|
||
sendAll() {
|
||
return this.smsDataModel.smsPreset === 'all' || this.toType === 'userTags'
|
||
},
|
||
hasCondition () {
|
||
return !!Object.keys(this.condition).length
|
||
}
|
||
},
|
||
watch: {
|
||
smsDataModel: {
|
||
handler(smsDataModel) {
|
||
if (!smsDataModel.templateId) return ''
|
||
|
||
const template = this.smsTemplate.find(template => template.value === smsDataModel.templateId)
|
||
let content = smsDataModel.templateData.reduce((res, param) => {
|
||
const reg = new RegExp(`\\$\\{${param.field}\\}`)
|
||
return res.replace(reg, ($1) => param.value || $1)
|
||
}, template.content)
|
||
|
||
this.smsTemplateContent = `【${template.sign}】${content}`
|
||
},
|
||
deep: true
|
||
}
|
||
},
|
||
methods: {
|
||
smsFilteredChange () {
|
||
this.smsDataModel.filtered = !this.smsDataModel.filtered
|
||
},
|
||
popupChange(e) {
|
||
if (!e.show) this.reset()
|
||
},
|
||
open() {
|
||
this.$refs.smsPopup.open()
|
||
this.loadSmsTemplate()
|
||
},
|
||
close() {
|
||
this.reset()
|
||
this.$refs.smsPopup.close()
|
||
},
|
||
async loadSmsTemplate() {
|
||
if (this.smsTemplate.length > 0 || this.smsTemplateLoading) return
|
||
|
||
this.smsTemplateLoading = true
|
||
|
||
try {
|
||
const uniSmsCo = uniCloud.importObject('uni-sms-co', {customUI: true})
|
||
const res = await uniSmsCo.template()
|
||
this.smsTemplate = res.map(item => ({
|
||
...item,
|
||
value: item._id,
|
||
text: item.name,
|
||
}))
|
||
} finally {
|
||
this.smsTemplateLoading = false
|
||
}
|
||
},
|
||
onSmsTemplateSelected(templateId) {
|
||
const current = this.smsTemplate.find(template => template.value === templateId)
|
||
|
||
if (!current) return
|
||
|
||
const reg = new RegExp(/\$\{(.*?)\}/g)
|
||
let templateVars = []
|
||
let _execResult
|
||
|
||
while (_execResult = reg.exec(current.content)) {
|
||
const param = _execResult[1]
|
||
|
||
if (param) {
|
||
templateVars.push({
|
||
field: param,
|
||
value: ''
|
||
})
|
||
}
|
||
}
|
||
|
||
this.smsDataModel.templateData = templateVars
|
||
},
|
||
async sendSms(isPreview = false) {
|
||
const values = await this.$refs.smsForm.validate()
|
||
const receiver = this.receiver
|
||
|
||
for (const template of this.smsDataModel.templateData) {
|
||
if (!template.value) {
|
||
this.smsTemplateDataErrorMessage = '字段/值不可为空'
|
||
return
|
||
}
|
||
}
|
||
this.smsTemplateDataErrorMessage = ''
|
||
|
||
const to = {
|
||
type: this.toType,
|
||
receiver,
|
||
}
|
||
|
||
if (this.smsDataModel.filtered || this.smsDataModel.smsPreset) {
|
||
to.condition = this.smsDataModel.smsPreset || this.condition
|
||
}
|
||
|
||
if (isPreview) {
|
||
const res = await uniSmsCo.preview(
|
||
to,
|
||
values.templateId,
|
||
this.smsDataModel.templateData
|
||
)
|
||
|
||
if (res.errCode === 0) {
|
||
this.smsPreviewContent = res.list
|
||
this.$refs.previewPopup.open()
|
||
this.smsSendUserCount = res.total
|
||
return
|
||
}
|
||
}
|
||
|
||
uni.showModal({
|
||
title: '发送确认',
|
||
content: `短信${this.sendAll ? '将发送给所有用户' : this.smsSendUserCount ? `预计发送${this.smsSendUserCount}人`: `将发送给符合条件的用户`},确定发送?`,
|
||
success: async (e) => {
|
||
this.smsSendUserCount = 0
|
||
|
||
if (e.cancel) return
|
||
|
||
const res = await uniSmsCo.createSmsTask(
|
||
to,
|
||
values.templateId,
|
||
this.smsDataModel.templateData, {
|
||
taskName: values.name
|
||
}
|
||
)
|
||
|
||
if (res.taskId) {
|
||
uni.showModal({
|
||
content: '短信任务已提交,您可在DCloud开发者后台查看短信发送记录',
|
||
confirmText: '立即查看',
|
||
cancelText: '关闭',
|
||
success: (e) => {
|
||
if (e.cancel) {
|
||
this.reset()
|
||
this.$refs.smsPopup.close()
|
||
} else {
|
||
// #ifdef H5
|
||
window.open('https://dev.dcloud.net.cn/#/pages/sms/sendLog', '_blank')
|
||
// #endif
|
||
// ifndef H5
|
||
this.reset()
|
||
this.$refs.smsPopup.close()
|
||
// endif
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
})
|
||
},
|
||
chooseFile() {
|
||
if (typeof window.FileReader === 'undefined') {
|
||
uni.showModal({
|
||
content: '当前浏览器不支持文件上传,请升级浏览器重试',
|
||
showCancel: false
|
||
})
|
||
return
|
||
}
|
||
|
||
uni.chooseFile({
|
||
count: 1,
|
||
extension: ['.json'],
|
||
success: ({tempFiles}) => {
|
||
if (tempFiles.length <= 0) return
|
||
const [file] = tempFiles
|
||
const reader = new FileReader()
|
||
|
||
reader.readAsText(file)
|
||
reader.onload = () => this.parserFileJson(null, reader.result)
|
||
reader.onerror = () => this.parserFileJson(reader.error)
|
||
},
|
||
fail: () => {
|
||
uni.showModal({
|
||
content: '打开选择文件框失败',
|
||
showCancel: false
|
||
})
|
||
}
|
||
})
|
||
},
|
||
async parserFileJson(error, fileContent) {
|
||
if (error) {
|
||
console.error(error)
|
||
uni.showModal({
|
||
content: '文件读取失败,请重新上传文件',
|
||
showCancel: false
|
||
})
|
||
return
|
||
}
|
||
|
||
let templates = []
|
||
try {
|
||
templates = JSON.parse(fileContent)
|
||
} catch (e) {
|
||
console.error(e)
|
||
uni.showModal({
|
||
content: '短信模板解析失败,请检查模板格式',
|
||
showCancel: false
|
||
})
|
||
return
|
||
}
|
||
|
||
const res = await uniSmsCo.updateTemplates(templates)
|
||
if (res.errCode === 0) {
|
||
uni.showModal({
|
||
content: '短信模板更新成功',
|
||
showCancel: false,
|
||
success: () => {
|
||
this.loadSmsTemplate()
|
||
}
|
||
})
|
||
}
|
||
},
|
||
reset() {
|
||
this.smsDataModel.name = ''
|
||
this.smsDataModel.smsPreset = ''
|
||
this.smsDataModel.templateId = ''
|
||
this.smsDataModel.templateData = []
|
||
this.smsPreviewContent = []
|
||
this.smsTemplateContent = ''
|
||
this.smsSendUserCount = 0
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
@import '@/uni_modules/uni-scss/variables.scss';
|
||
|
||
.a-link {
|
||
cursor: pointer;
|
||
color: $uni-primary;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.close {
|
||
position: absolute;
|
||
right: 20px;
|
||
top: 20px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.sms-manager {
|
||
width: 570px;
|
||
background: #fff;
|
||
padding: 30px;
|
||
border-radius: 5px;
|
||
|
||
&.preview {
|
||
width: 550px;
|
||
}
|
||
|
||
&--header {
|
||
text-align: center;
|
||
font-size: 22px;
|
||
|
||
&.mb {
|
||
margin-bottom: 50px;
|
||
}
|
||
|
||
.sub-title {
|
||
margin-top: 5px;
|
||
font-size: 16px;
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.content {
|
||
margin-top: 20px;
|
||
font-size: 16px;
|
||
line-height: 1.5;
|
||
|
||
.length {
|
||
text-align: right;
|
||
font-size: 13px;
|
||
margin-top: 20px;
|
||
|
||
.num {
|
||
color: red;
|
||
}
|
||
}
|
||
}
|
||
|
||
.tip {
|
||
border-top: #ccc solid 1px;
|
||
padding-top: 20px;
|
||
margin-top: 20px;
|
||
line-height: 1.7;
|
||
font-size: 13px;
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.sms-data-item {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-top: 10px;
|
||
|
||
&:first-child {
|
||
margin-top: 0;
|
||
}
|
||
|
||
.m {
|
||
margin: 0 5px;
|
||
|
||
&:first-child {
|
||
margin-left: 0;
|
||
}
|
||
|
||
&:last-child {
|
||
margin-right: 0;
|
||
}
|
||
}
|
||
|
||
.type {
|
||
width: 100px;
|
||
flex: none;
|
||
}
|
||
|
||
.add,
|
||
.minus {
|
||
cursor: pointer;
|
||
}
|
||
}
|
||
|
||
.sms-data-tip {
|
||
color: $uni-info;
|
||
font-size: 12px;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
.form-item-flex-center {
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
</style>
|