batch-sms.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. <template>
  2. <view>
  3. <uni-popup ref="smsPopup" type="center" @change="popupChange" :is-mask-click="false">
  4. <view class="sms-manager">
  5. <view class="sms-manager--header mb">群发短信</view>
  6. <uni-forms :label-width="100" :modelValue="smsDataModel" ref="smsForm">
  7. <uni-forms-item v-if="toType === 'user' && !isSelectedReceiver" label="目标对象" name="smsPreset" :rules="[{ required: true, errorMessage: '请选择目标对象' }]" required>
  8. <uni-data-select class="type m" placeholder="预设条件" size="mini" :clear="false"
  9. :localdata="smsPresetList" v-model="smsDataModel.smsPreset">
  10. </uni-data-select>
  11. <view class="sms-data-tip">如需给指定用户发送,请在列表选择要发送的用户。</view>
  12. </uni-forms-item>
  13. <uni-forms-item label="目标对象" v-else-if="toType === 'user' && isSelectedReceiver">
  14. <view>当前已选择{{ receiver.length }}人</view>
  15. </uni-forms-item>
  16. <uni-forms-item label="目标对象" v-else-if="toType === 'userTags' ">
  17. <view>当前已选择{{ receiver.length }}个标签</view>
  18. <view class="sms-data-tip">如标签关联的用户没有绑定手机号,将不会发送短信。</view>
  19. </uni-forms-item>
  20. <uni-forms-item label="跨分页选择" v-if="isSelectedReceiver && hasCondition">
  21. <checkbox-group @change="smsFilteredChange">
  22. <checkbox style="transform: scale(.9)" :checked="smsDataModel.filtered"></checkbox>
  23. </checkbox-group>
  24. <view class="sms-data-tip">对用户进行了筛选后,可能存在分页无法全部选中时,请勾选跨分页选中</view>
  25. </uni-forms-item>
  26. <uni-forms-item label="任务名称" name="name" required
  27. :rules="[{ required: true, errorMessage: '请输入任务名称' }]">
  28. <uni-easyinput v-model="smsDataModel.name" placeholder="请输入任务名称,例如 “7日内未登录用户召回”"/>
  29. </uni-forms-item>
  30. <uni-forms-item required label="短信模板" name="templateId"
  31. :rules="[{ required: true, errorMessage: '请选择短信模板' }]">
  32. <template v-if="!smsTemplateLoading">
  33. <view v-if="smsTemplate.length">
  34. <uni-data-select class="type m" placeholder="请选择短信模板" size="mini" :clear="false"
  35. :localdata="smsTemplate" v-model="smsDataModel.templateId"
  36. @change="onSmsTemplateSelected">
  37. </uni-data-select>
  38. <view class="sms-data-tip">
  39. 导入短信模版参考<a class="a-link" href="https://uniapp.dcloud.net.cn/uniCloud/admin.html#群发短信"
  40. target="_blank">教程</a>;若有新的短信模版,可
  41. <text @click="chooseFile"
  42. class="a-link">点此导入
  43. </text>
  44. </view>
  45. </view>
  46. <view v-else>
  47. <button @click="chooseFile" type="primary" style="width: 120px;"
  48. size="mini">上传短信模板
  49. </button>
  50. <view class="sms-data-tip">当前未导入短信模板,请从dev.dcloud.net.cn的短信-<a
  51. href="https://dev.dcloud.net.cn/pages/sms/template" target="_blank">模板配置</a>中导出短信模版,并在此导入。教程<a
  52. href="https://uniapp.dcloud.net.cn/uniCloud/admin.html#batch-sms" target="_blank">详见</a></view>
  53. </view>
  54. </template>
  55. <template v-else>
  56. 模板加载中...
  57. </template>
  58. </uni-forms-item>
  59. <uni-forms-item label="短信内容" v-if="smsTemplateContent">
  60. <view class="form-item-flex-center">{{ smsTemplateContent }}</view>
  61. </uni-forms-item>
  62. <uni-forms-item label="模板变量配置" :error-message="smsTemplateDataErrorMessage"
  63. v-if="smsDataModel.templateData.length">
  64. <view class="sms-data-item" :key="template.field"
  65. v-for="(template, index) in smsDataModel.templateData">
  66. <uni-easyinput class="field m" v-model="template.field" placeholder="字段" :clearable="false"
  67. :disabled="true" style="width: 120px;flex:none;"/>
  68. <uni-easyinput class="value m" v-model="template.value"
  69. placeholder="例 {uni-id-users.username}" :clearable="false"/>
  70. </view>
  71. <view class="sms-data-tip">
  72. 短信变量支持固定值和数据表查询两种方式;固定值如:各位同事,数据表查询如:{uni-id-users.username};请注意,若使用数据表查询方式,目前仅支持查询
  73. uni-id-users 表;并注意确保数据库中查询字段值不为空,否则短信将发送失败。
  74. </view>
  75. </uni-forms-item>
  76. </uni-forms>
  77. <view class="uni-group">
  78. <button @click="sendSms(true)" class="uni-button">预览</button>
  79. <button @click="sendSms()" class="uni-button" type="primary">提交</button>
  80. </view>
  81. </view>
  82. <uni-icons type="closeempty" size="24" class="close" @click="close"></uni-icons>
  83. </uni-popup>
  84. <uni-popup ref="previewPopup" type="center" :is-mask-click="false">
  85. <view class="sms-manager preview">
  86. <view class="sms-manager--header mb">
  87. <view>短信预览</view>
  88. <view class="sub-title">仅预览第一条短信内容</view>
  89. <view class="sub-title">预计送达 <text style="color: red">{{smsSendUserCount}}</text> 位用户</view>
  90. </view>
  91. <view class="content">
  92. <view v-for="(content,index) of smsPreviewContent" :key="index">{{ content }}</view>
  93. <view class="length">短信字数:
  94. <text class="num">{{ smsPreviewContent.length ? smsPreviewContent[0].length : 0 }}</text>
  95. </view>
  96. </view>
  97. <view class="tip">
  98. <view>说明:</view>
  99. <view>若从数据表中查询,字段内容长度会影响总字数,短信字数=短信签名字数+短信内容字数。</view>
  100. <view>短信长度不超过70个字,按照一条短信计费;超过70个字,按照67字/条拆分成多条计费。</view>
  101. </view>
  102. <view class="uni-group">
  103. <button @click="$refs.previewPopup.close()" class="uni-button">关闭</button>
  104. </view>
  105. </view>
  106. </uni-popup>
  107. </view>
  108. </template>
  109. <script>
  110. const uniSmsCo = uniCloud.importObject('uni-sms-co')
  111. export default {
  112. name: 'batchSms',
  113. props: {
  114. // 发送类型 user|userTags
  115. toType: String,
  116. // 接收者 user=user._id, userTags=tag.id
  117. receiver: {
  118. type: Array,
  119. default() {
  120. return []
  121. }
  122. },
  123. // 条件;跨分页选择时需要
  124. condition: {
  125. type: Object,
  126. default () {
  127. return {}
  128. }
  129. }
  130. },
  131. data() {
  132. return {
  133. smsTemplateLoading: false,
  134. smsPresetList: [{
  135. value: 'all',
  136. text: '全部用户',
  137. },{
  138. value: '7-day-offline-users',
  139. text: '7天内未登录用户',
  140. },{
  141. value: '15-day-offline-users',
  142. text: '15天内未登录用户',
  143. },{
  144. value: '30-day-offline-users',
  145. text: '30天内未登录用户',
  146. }],
  147. smsTemplate: [],
  148. smsTemplateDataErrorMessage: '',
  149. smsDataModel: {
  150. name: '',
  151. templateId: '',
  152. templateData: [],
  153. smsPreset: '',
  154. filtered: false
  155. },
  156. smsTemplateContent: '',
  157. smsPreviewContent: [],
  158. smsSendUserCount: 0
  159. }
  160. },
  161. computed: {
  162. isSelectedReceiver() {
  163. return !!this.receiver.length
  164. },
  165. sendAll() {
  166. return this.smsDataModel.smsPreset === 'all' || this.toType === 'userTags'
  167. },
  168. hasCondition () {
  169. return !!Object.keys(this.condition).length
  170. }
  171. },
  172. watch: {
  173. smsDataModel: {
  174. handler(smsDataModel) {
  175. if (!smsDataModel.templateId) return ''
  176. const template = this.smsTemplate.find(template => template.value === smsDataModel.templateId)
  177. let content = smsDataModel.templateData.reduce((res, param) => {
  178. const reg = new RegExp(`\\$\\{${param.field}\\}`)
  179. return res.replace(reg, ($1) => param.value || $1)
  180. }, template.content)
  181. this.smsTemplateContent = `【${template.sign}】${content}`
  182. },
  183. deep: true
  184. }
  185. },
  186. methods: {
  187. smsFilteredChange () {
  188. this.smsDataModel.filtered = !this.smsDataModel.filtered
  189. },
  190. popupChange(e) {
  191. if (!e.show) this.reset()
  192. },
  193. open() {
  194. this.$refs.smsPopup.open()
  195. this.loadSmsTemplate()
  196. },
  197. close() {
  198. this.reset()
  199. this.$refs.smsPopup.close()
  200. },
  201. async loadSmsTemplate() {
  202. if (this.smsTemplate.length > 0 || this.smsTemplateLoading) return
  203. this.smsTemplateLoading = true
  204. try {
  205. const uniSmsCo = uniCloud.importObject('uni-sms-co', {customUI: true})
  206. const res = await uniSmsCo.template()
  207. this.smsTemplate = res.map(item => ({
  208. ...item,
  209. value: item._id,
  210. text: item.name,
  211. }))
  212. } finally {
  213. this.smsTemplateLoading = false
  214. }
  215. },
  216. onSmsTemplateSelected(templateId) {
  217. const current = this.smsTemplate.find(template => template.value === templateId)
  218. if (!current) return
  219. const reg = new RegExp(/\$\{(.*?)\}/g)
  220. let templateVars = []
  221. let _execResult
  222. while (_execResult = reg.exec(current.content)) {
  223. const param = _execResult[1]
  224. if (param) {
  225. templateVars.push({
  226. field: param,
  227. value: ''
  228. })
  229. }
  230. }
  231. this.smsDataModel.templateData = templateVars
  232. },
  233. async sendSms(isPreview = false) {
  234. const values = await this.$refs.smsForm.validate()
  235. const receiver = this.receiver
  236. for (const template of this.smsDataModel.templateData) {
  237. if (!template.value) {
  238. this.smsTemplateDataErrorMessage = '字段/值不可为空'
  239. return
  240. }
  241. }
  242. this.smsTemplateDataErrorMessage = ''
  243. const to = {
  244. type: this.toType,
  245. receiver,
  246. }
  247. if (this.smsDataModel.filtered || this.smsDataModel.smsPreset) {
  248. to.condition = this.smsDataModel.smsPreset || this.condition
  249. }
  250. if (isPreview) {
  251. const res = await uniSmsCo.preview(
  252. to,
  253. values.templateId,
  254. this.smsDataModel.templateData
  255. )
  256. if (res.errCode === 0) {
  257. this.smsPreviewContent = res.list
  258. this.$refs.previewPopup.open()
  259. this.smsSendUserCount = res.total
  260. return
  261. }
  262. }
  263. uni.showModal({
  264. title: '发送确认',
  265. content: `短信${this.sendAll ? '将发送给所有用户' : this.smsSendUserCount ? `预计发送${this.smsSendUserCount}人`: `将发送给符合条件的用户`},确定发送?`,
  266. success: async (e) => {
  267. this.smsSendUserCount = 0
  268. if (e.cancel) return
  269. const res = await uniSmsCo.createSmsTask(
  270. to,
  271. values.templateId,
  272. this.smsDataModel.templateData, {
  273. taskName: values.name
  274. }
  275. )
  276. if (res.taskId) {
  277. uni.showModal({
  278. content: '短信任务已提交,您可在DCloud开发者后台查看短信发送记录',
  279. confirmText: '立即查看',
  280. cancelText: '关闭',
  281. success: (e) => {
  282. if (e.cancel) {
  283. this.reset()
  284. this.$refs.smsPopup.close()
  285. } else {
  286. // #ifdef H5
  287. window.open('https://dev.dcloud.net.cn/#/pages/sms/sendLog', '_blank')
  288. // #endif
  289. // ifndef H5
  290. this.reset()
  291. this.$refs.smsPopup.close()
  292. // endif
  293. }
  294. }
  295. })
  296. }
  297. }
  298. })
  299. },
  300. chooseFile() {
  301. if (typeof window.FileReader === 'undefined') {
  302. uni.showModal({
  303. content: '当前浏览器不支持文件上传,请升级浏览器重试',
  304. showCancel: false
  305. })
  306. return
  307. }
  308. uni.chooseFile({
  309. count: 1,
  310. extension: ['.json'],
  311. success: ({tempFiles}) => {
  312. if (tempFiles.length <= 0) return
  313. const [file] = tempFiles
  314. const reader = new FileReader()
  315. reader.readAsText(file)
  316. reader.onload = () => this.parserFileJson(null, reader.result)
  317. reader.onerror = () => this.parserFileJson(reader.error)
  318. },
  319. fail: () => {
  320. uni.showModal({
  321. content: '打开选择文件框失败',
  322. showCancel: false
  323. })
  324. }
  325. })
  326. },
  327. async parserFileJson(error, fileContent) {
  328. if (error) {
  329. console.error(error)
  330. uni.showModal({
  331. content: '文件读取失败,请重新上传文件',
  332. showCancel: false
  333. })
  334. return
  335. }
  336. let templates = []
  337. try {
  338. templates = JSON.parse(fileContent)
  339. } catch (e) {
  340. console.error(e)
  341. uni.showModal({
  342. content: '短信模板解析失败,请检查模板格式',
  343. showCancel: false
  344. })
  345. return
  346. }
  347. const res = await uniSmsCo.updateTemplates(templates)
  348. if (res.errCode === 0) {
  349. uni.showModal({
  350. content: '短信模板更新成功',
  351. showCancel: false,
  352. success: () => {
  353. this.loadSmsTemplate()
  354. }
  355. })
  356. }
  357. },
  358. reset() {
  359. this.smsDataModel.name = ''
  360. this.smsDataModel.smsPreset = ''
  361. this.smsDataModel.templateId = ''
  362. this.smsDataModel.templateData = []
  363. this.smsPreviewContent = []
  364. this.smsTemplateContent = ''
  365. this.smsSendUserCount = 0
  366. }
  367. }
  368. }
  369. </script>
  370. <style lang="scss">
  371. @import '@/uni_modules/uni-scss/variables.scss';
  372. .a-link {
  373. cursor: pointer;
  374. color: $uni-primary;
  375. text-decoration: none;
  376. }
  377. .close {
  378. position: absolute;
  379. right: 20px;
  380. top: 20px;
  381. cursor: pointer;
  382. }
  383. .sms-manager {
  384. width: 570px;
  385. background: #fff;
  386. padding: 30px;
  387. border-radius: 5px;
  388. &.preview {
  389. width: 550px;
  390. }
  391. &--header {
  392. text-align: center;
  393. font-size: 22px;
  394. &.mb {
  395. margin-bottom: 50px;
  396. }
  397. .sub-title {
  398. margin-top: 5px;
  399. font-size: 16px;
  400. color: #999;
  401. }
  402. }
  403. .content {
  404. margin-top: 20px;
  405. font-size: 16px;
  406. line-height: 1.5;
  407. .length {
  408. text-align: right;
  409. font-size: 13px;
  410. margin-top: 20px;
  411. .num {
  412. color: red;
  413. }
  414. }
  415. }
  416. .tip {
  417. border-top: #ccc solid 1px;
  418. padding-top: 20px;
  419. margin-top: 20px;
  420. line-height: 1.7;
  421. font-size: 13px;
  422. color: #999;
  423. }
  424. }
  425. .sms-data-item {
  426. display: flex;
  427. align-items: center;
  428. margin-top: 10px;
  429. &:first-child {
  430. margin-top: 0;
  431. }
  432. .m {
  433. margin: 0 5px;
  434. &:first-child {
  435. margin-left: 0;
  436. }
  437. &:last-child {
  438. margin-right: 0;
  439. }
  440. }
  441. .type {
  442. width: 100px;
  443. flex: none;
  444. }
  445. .add,
  446. .minus {
  447. cursor: pointer;
  448. }
  449. }
  450. .sms-data-tip {
  451. color: $uni-info;
  452. font-size: 12px;
  453. margin-top: 5px;
  454. }
  455. .form-item-flex-center {
  456. height: 100%;
  457. display: flex;
  458. align-items: center;
  459. }
  460. </style>