NGToolsAdmin/components/batch-sms/batch-sms.vue
2024-09-13 16:39:31 +08:00

508 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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