sendMsgSopOpenTool.vue 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130
  1. <template>
  2. <div class="app-container">
  3. <el-dialog :title="sendMsgOpen.title" :visible.sync="sendMsgOpen.open" width="1000px" append-to-body>
  4. <el-alert
  5. type="error"
  6. :closable="false"
  7. show-icon>
  8. <template #title>
  9. <span style="font-size: 25px; line-height: 1.5;">
  10. 此功能用于给 选中的 SOP营期 内【所有的】客户发送 消息
  11. </span>
  12. </template>
  13. </el-alert>
  14. <el-form ref="msgForm" :model="msgForm" :rules="msgRules" label-width="100px">
  15. <el-form-item label="策略" prop="draftStrategy">
  16. <el-radio-group v-model="msgForm.draftStrategy">
  17. <el-radio :label="1">正常群发</el-radio>
  18. <el-radio :label="2">清除草稿</el-radio>
  19. <el-radio :label="3">发送草稿</el-radio>
  20. </el-radio-group>
  21. </el-form-item>
  22. <el-form-item label="选择课程" v-if="msgForm.draftStrategy==1" >
  23. <el-select v-model="msgForm.courseId" placeholder="请选择课程" style=" margin-right: 10px;" size="mini" filterable clearable @change="courseChange()">
  24. <el-option
  25. v-for="dict in courseList"
  26. :key="dict.dictValue"
  27. :label="dict.dictLabel"
  28. :value="parseInt(dict.dictValue)"
  29. />
  30. </el-select>
  31. <el-select v-model="msgForm.videoId" placeholder="请选择小节" size="mini" style=" margin-right: 10px;" filterable clearable @change="videoIdChange()" >
  32. <el-option
  33. v-for="dict in videoList"
  34. :key="dict.dictValue"
  35. :label="dict.dictLabel"
  36. :value="parseInt(dict.dictValue)"
  37. />
  38. </el-select>
  39. <el-select v-model="msgForm.courseType" placeholder="请选择消息类型" size="mini" style=" margin-right: 10px;" filterable clearable>
  40. <el-option
  41. v-for="dict in sysFsSopWatchStatus"
  42. :key="dict.dictValue"
  43. :label="dict.dictLabel"
  44. :value="parseInt(dict.dictValue)"
  45. />
  46. </el-select>
  47. </el-form-item>
  48. <el-form-item label="规则" prop="setting" v-if="msgForm.draftStrategy==1" >
  49. <div v-for="(item, index) in setting" :key="index" style="background-color: #fdfdfd; border: 1px solid #e6e6e6; margin-bottom: 20px;">
  50. <el-row>
  51. <el-col :span="22">
  52. <el-form :model="item" label-width="70px">
  53. <el-form-item label="内容类别" style="margin: 2%">
  54. <el-radio-group v-model="item.contentType">
  55. <el-radio
  56. v-for="typeDict in sysQwSopAiContentType"
  57. :key="typeDict.dictValue"
  58. :label="typeDict.dictValue"
  59. @change="handleContentTypeChange()"
  60. >{{ typeDict.dictLabel }}</el-radio>
  61. </el-radio-group>
  62. </el-form-item>
  63. <el-form-item label="内容" style="margin-bottom: 2%" >
  64. <el-input
  65. v-if="item.contentType == 1 || item.contentType == 15"
  66. v-model="item.value"
  67. type="textarea"
  68. :rows="3"
  69. placeholder="内容"
  70. style="width: 90%; margin-top: 10px;"
  71. @keydown.native="handleKeydown($event, index)"
  72. :ref="`textarea-${index}`"
  73. >
  74. </el-input>
  75. <el-link
  76. v-if="item.contentType == 1 || item.contentType == 15"
  77. type="primary"
  78. @click="toggleSalesCall(index)"
  79. style="margin-top: 10px;"
  80. >
  81. {{ item.isSalesCallAdded ? '移除#销售称呼#' : '添加#销售称呼#' }}
  82. </el-link>
  83. <el-link
  84. v-if="item.contentType == 1 || item.contentType == 15"
  85. type="primary"
  86. @click="toggleSalesCallCustomer(index)"
  87. style="margin-top: 10px;margin-left: 2%"
  88. >
  89. {{ item.isSalesCallCustomerAdded ? '移除#客户称呼#' : '添加#客户称呼#' }}
  90. </el-link>
  91. <ImageUpload v-if="item.contentType == 2 " v-model="item.imgUrl" type="image" :num="1" :width="150" :height="150" />
  92. <div v-if="isLinkH5ContentType(item.contentType)">
  93. <el-card class="box-card">
  94. <el-form-item label="链接标题:" label-width="100px">
  95. <el-input v-model="item.linkTitle" placeholder="请输入链接标题" style="width: 90%;"/>
  96. </el-form-item>
  97. <el-form-item label="链接描述:" label-width="100px" >
  98. <el-input type="textarea" :rows="3" v-model="item.linkDescribe" placeholder="请输入链接描述" style="width: 90%;margin-top: 1%;"/>
  99. </el-form-item>
  100. <el-form-item label="链接封面:" label-width="100px">
  101. <ImageUpload v-model="item.linkImageUrl" type="image" :num="1" :file-size="2" :width="150" :height="150" style="margin-top: 1%;" />
  102. </el-form-item>
  103. <el-form-item label="链接地址:" label-width="100px" >
  104. <el-tag type="warning" v-model="item.isBindUrl=1">选择的课程小节 即为卡片链接地址</el-tag>
  105. </el-form-item>
  106. </el-card>
  107. </div>
  108. <div v-if="isAppContentType(item.contentType)">
  109. <el-card class="box-card">
  110. <el-form-item label="消息标题" label-width="100px">
  111. <el-input v-model="item.linkTitle" placeholder="APP 消息标题(短链/推送展示)" style="width: 90%;"/>
  112. </el-form-item>
  113. <el-form-item label="描述" label-width="100px">
  114. <el-input type="textarea" :rows="3" v-model="item.linkDescribe" placeholder="请输入描述" style="width: 90%;margin-top: 1%;"/>
  115. </el-form-item>
  116. <el-form-item label="封面" label-width="100px">
  117. <ImageUpload v-model="item.linkImageUrl" type="image" :num="1" :file-size="2" :width="150" :height="150" style="margin-top: 1%;" />
  118. </el-form-item>
  119. <el-form-item label="链接说明" label-width="100px">
  120. <el-tag type="info">可点击链接由服务端根据上方所选课程、课节生成,无需填写短链</el-tag>
  121. </el-form-item>
  122. <el-form-item label="链接有效天数" label-width="100px">
  123. <el-input-number v-model="item.expiresDays" :min="0" :max="365" :precision="0" placeholder="可选"/>
  124. <span class="tip" style="margin-left: 8px;">不填或 0 表示使用课程/系统默认过期时间</span>
  125. </el-form-item>
  126. </el-card>
  127. </div>
  128. <div v-if="isFeishuContentType(item.contentType)">
  129. <el-card class="box-card">
  130. <el-form-item label="消息标题" label-width="100px">
  131. <el-input v-model="item.linkTitle" placeholder="飞书链接卡片标题" style="width: 90%;"/>
  132. </el-form-item>
  133. <el-form-item label="描述" label-width="100px">
  134. <el-input type="textarea" :rows="3" v-model="item.linkDescribe" placeholder="请输入描述" style="width: 90%;margin-top: 1%;"/>
  135. </el-form-item>
  136. <el-form-item label="封面" label-width="100px">
  137. <ImageUpload v-model="item.linkImageUrl" type="image" :num="1" :file-size="2" :width="150" :height="150" style="margin-top: 1%;" />
  138. </el-form-item>
  139. <el-form-item label="链接说明" label-width="100px">
  140. <el-tag type="info">飞书看课链接由服务端根据上方所选课程、课节自动生成</el-tag>
  141. </el-form-item>
  142. <el-form-item label="客户授权" label-width="100px">
  143. <el-radio-group v-model="item.feishuNeedAuth">
  144. <el-radio :label="1">需要授权</el-radio>
  145. <el-radio :label="0">免授权</el-radio>
  146. </el-radio-group>
  147. <div style="color: #909399; font-size: 12px; margin-top: 4px;">免授权:客户可直接看课</div>
  148. </el-form-item>
  149. </el-card>
  150. </div>
  151. <div v-if="item.contentType == 4 || item.contentType == 10 ">
  152. <el-card class="box-card">
  153. <el-form-item label="标题" prop="miniprogramTitle">
  154. <el-input v-model="item.miniprogramTitle" placeholder="请输入小程序消息标题,最长为64字" />
  155. </el-form-item>
  156. <el-form-item label="封面" prop="miniprogramPicUrl">
  157. <ImageUpload v-model="item.miniprogramPicUrl" type="image" :num="10" :width="150" :height="150" />
  158. </el-form-item>
  159. <el-form-item label="appid" prop="miniprogramAppid" v-show="false" >
  160. <el-input v-model="item.miniprogramAppid='wx73f85f8d62769119' " disabled />
  161. </el-form-item>
  162. <el-form-item label="page路径" prop="miniprogramPage" v-show="item.contentType == 10" label-width="100px" style="margin-left: -30px" >
  163. <el-input v-model="item.miniprogramPage" placeholder="小程序消息打开后的路径" type="textarea" :rows="3" />
  164. </el-form-item>
  165. </el-card>
  166. </div>
  167. <div v-if="item.contentType == 5 ">
  168. <el-form-item label="上传文件:" prop="fileUrl" label-width="100px">
  169. <el-upload
  170. v-model="item.fileUrl"
  171. class="avatar-uploader"
  172. :action="uploadUrl"
  173. :show-file-list="false"
  174. :on-success="(res, file) => handleAvatarSuccessFile(res, file, item)"
  175. :before-upload="beforeAvatarUploadFile">
  176. <i class="el-icon-plus avatar-uploader-icon"></i>
  177. </el-upload>
  178. <el-link v-if="item.fileUrl" type="primary" :href="downloadUrl(item.fileUrl)" download>
  179. {{item.fileUrl}}
  180. </el-link>
  181. </el-form-item>
  182. </div>
  183. <div v-if="item.contentType == 6 ">
  184. <el-form-item label="上传视频:" prop="videoUrl" label-width="100px">
  185. <el-upload
  186. v-model="item.videoUrl"
  187. class="avatar-uploader"
  188. :action="uploadUrl"
  189. :show-file-list="false"
  190. :on-success="(res, file) => handleAvatarSuccessVideo(res, file, item)"
  191. :before-upload="beforeAvatarUploadVideo">
  192. <i class="el-icon-plus avatar-uploader-icon"></i>
  193. </el-upload>
  194. <video v-if="item.videoUrl"
  195. :src="item.videoUrl"
  196. controls style="width: 200px;height: 100px">
  197. </video>
  198. </el-form-item>
  199. </div>
  200. <div v-if="item.contentType == 7 || item.contentType == 16">
  201. <el-form-item label="输入方式" style="margin-bottom: 10px;">
  202. <el-radio-group v-model="item.voiceInputMode" size="small">
  203. <el-radio-button label="dropdown">选择语音</el-radio-button>
  204. <el-radio-button label="input">手动输入</el-radio-button>
  205. </el-radio-group>
  206. </el-form-item>
  207. <!-- 下拉框选择语音 -->
  208. <div v-if="item.voiceInputMode === 'dropdown' || !item.voiceInputMode">
  209. <el-select
  210. v-model="item.value"
  211. placeholder="请选择语音内容"
  212. filterable
  213. clearable
  214. size="small" style="width: 100%;"
  215. v-loading="voiceLoading"
  216. @focus="loadVoiceOptions()"
  217. >
  218. <el-option
  219. v-for="voice in voiceList"
  220. :key="voice.id"
  221. :label="voice.voiceTxt || voice.name"
  222. :value="voice.voiceTxt"
  223. />
  224. </el-select>
  225. </div>
  226. <!-- 手动输入语音 -->
  227. <div v-if="item.voiceInputMode === 'input'">
  228. <el-input
  229. v-model="item.value"
  230. type="textarea"
  231. :rows="3"
  232. maxlength="66"
  233. show-word-limit
  234. placeholder="输入要转为语音的内容"
  235. style="width: 100%; margin-top: 5px;"
  236. @input="handleInputVideoText(item.value,item)"/>
  237. </div>
  238. </div>
  239. <div v-if="item.contentType == 8">
  240. <el-button type="primary"
  241. style="margin-bottom: 1%"
  242. @click="hanldeSelectVideoNum(setting,index)">
  243. 选择视频号
  244. </el-button>
  245. <el-card class="box-card" v-if="item.coverUrl">
  246. <el-form-item label="封面标题:" label-width="100px">
  247. <el-input v-model="item.nickname"
  248. style="width: 90%;margin-bottom: 1%" disabled/>
  249. </el-form-item>
  250. <el-form-item label="头像:" label-width="100px">
  251. <el-image
  252. v-if="item.avatar != null"
  253. :src="item.avatar"
  254. :preview-src-list="[item.avatar]"
  255. :style="{ width: '50px', height: '50px' }"
  256. ></el-image>
  257. </el-form-item>
  258. <el-form-item label="封面:" label-width="100px">
  259. <el-image
  260. v-if="item.coverUrl != null"
  261. :src="item.coverUrl"
  262. :preview-src-list="[item.coverUrl]"
  263. :style="{ width: '200px', height: '200px' }"
  264. ></el-image>
  265. </el-form-item>
  266. <el-form-item label="简介:" label-width="100px">
  267. <el-input type="textarea" :rows="3"
  268. v-model="item.desc"
  269. style="width: 90%;margin-top: 1%;" disabled/>
  270. </el-form-item>
  271. <el-form-item label="视频地址:" label-width="100px"
  272. style="margin-top: 1%">
  273. <el-input v-model="item.url"
  274. style="width: 90%;" disabled/>
  275. </el-form-item>
  276. </el-card>
  277. </div>
  278. <div v-if="item.contentType == 12">
  279. <el-card class="box-card">
  280. <el-form-item label="直播间" >
  281. <el-select v-model="item.liveId"
  282. filterable
  283. placeholder="请选择直播间" size="mini"
  284. @change="liveChange(item)" >
  285. <el-option
  286. v-for="dict in liveList"
  287. :key="dict.liveId"
  288. :label="dict.liveName"
  289. :value="dict.liveId"
  290. />
  291. </el-select>
  292. </el-form-item>
  293. <el-form-item label="标题" prop="miniprogramTitle">
  294. <el-input v-model="item.miniprogramTitle" placeholder="请输入小程序消息标题,最长为64字节" :rows="2" maxlength="64"
  295. type="textarea" />
  296. </el-form-item>
  297. <el-form-item label="封面" prop="miniprogramPicUrl">
  298. <ImageUpload v-model="item.miniprogramPicUrl" type="image" :num="10" :width="150" :height="150" />
  299. </el-form-item>
  300. <el-form-item label="appid" prop="miniprogramAppid" v-show="false">
  301. <el-input v-model="item.miniprogramAppid='wx503cf8ab31f83dd4' " disabled />
  302. </el-form-item>
  303. <el-form-item label="page路径" prop="miniprogramPage" v-show="false" label-width="100px" style="margin-left: -30px">
  304. <el-input v-model="item.miniprogramPage" placeholder="小程序消息打开后的路径" disabled />
  305. </el-form-item>
  306. </el-card>
  307. </div>
  308. <div v-if="item.contentType == 13">
  309. <el-card class="box-card">
  310. <el-form-item label="标题" prop="miniprogramTitle">
  311. <el-input
  312. v-model="item.miniprogramTitle"
  313. placeholder="请输入小程序消息标题,最长为64字"
  314. />
  315. </el-form-item>
  316. <el-form-item label="封面" prop="miniprogramPicUrl">
  317. <ImageUpload
  318. v-model="item.miniprogramPicUrl"
  319. type="image"
  320. :num="10"
  321. :width="150"
  322. :height="150"
  323. />
  324. </el-form-item>
  325. <el-form-item label="文章链接" prop="articleUrl">
  326. <el-input
  327. v-model="item.articleUrl"
  328. placeholder="请输入订阅号文章链接"
  329. />
  330. </el-form-item>
  331. </el-card>
  332. </div>
  333. <div v-if="item.contentType == 14">
  334. <el-card class="box-card">
  335. <el-form-item label="福袋名称" >
  336. <el-select v-model="item.luckyBagId"
  337. placeholder="请选择福袋" size="mini"
  338. @change="getLuckyBagStatus(item)" >
  339. <el-option
  340. v-for="dict in luckyBagList"
  341. :key="dict.id"
  342. :label="dict.name"
  343. :value="dict.id"
  344. />
  345. </el-select>
  346. </el-form-item>
  347. <el-form-item label="福袋状态" >
  348. <el-tag :type="item.luckyBagDataStatus == 1 ? 'success' : 'danger'">
  349. {{ item.luckyBagDataStatus == 1 ? '启用' : '禁用' }}
  350. </el-tag>
  351. </el-form-item>
  352. </el-card>
  353. </div>
  354. </el-form-item>
  355. <el-form-item label="添加短链" v-if="item.contentType == 1 || item.contentType == 15" >
  356. <el-tooltip content="请先根据课程选定课程小节之后再添加" effect="dark" :disabled="!!msgForm.videoId">
  357. <el-switch
  358. v-model="item.isBindUrl"
  359. :disabled="!msgForm.videoId"
  360. active-color="#13ce66"
  361. inactive-color="#DCDFE6"
  362. active-value="1"
  363. inactive-value="2">
  364. </el-switch>
  365. </el-tooltip>
  366. <span v-if="item.isBindUrl == '1'" style="margin-left: 10px; color: #13ce66">添加URL</span>
  367. <span v-if="item.isBindUrl == '2'" style="margin-left: 10px; color: #b1b4ba">不加URL</span>
  368. </el-form-item>
  369. <el-form-item label="课节过期时间" v-if="item.isBindUrl == '1'
  370. && item.contentType != 2
  371. && item.contentType != 5
  372. && item.contentType != 6
  373. && item.contentType != 8
  374. && !isAppContentType(item.contentType)"
  375. style="margin-top: 1%" label-width="100px">
  376. <el-row>
  377. <el-input-number v-model="item.expiresDays" :min="1" :max="100" ></el-input-number>
  378. (天)
  379. </el-row>
  380. <el-row>
  381. <span class="tip">填写0或不填时,默认为系统配置的默认时间</span>
  382. </el-row>
  383. </el-form-item>
  384. </el-form>
  385. </el-col>
  386. <el-col :span="1" :offset="1">
  387. <i class="el-icon-delete" @click="delSetList(index)" style="margin-top: 20px;" v-if="setting.length>1"></i>
  388. </el-col>
  389. </el-row>
  390. </div>
  391. <el-link type="primary" class="el-icon-plus" :underline="false" @click='addSetList()' >添加内容</el-link>
  392. </el-form-item>
  393. <el-form-item label="发送时间" prop="sendTime" v-if="msgForm.draftStrategy==1" >
  394. <el-time-picker
  395. class="custom-input"
  396. v-model="msgForm.sendTime"
  397. value-format="HH:mm"
  398. format="HH:mm"
  399. placeholder="时间"
  400. style="width: 100px;height: 20px;">
  401. </el-time-picker>
  402. <span class="tip" style="margin-left: 2%"> 不填时,默认为系统当前时间(立即发送)</span>
  403. </el-form-item>
  404. </el-form>
  405. <div slot="footer" class="dialog-footer">
  406. <el-button type="primary" @click="submitMsgForm">确 定</el-button>
  407. <el-button @click="cancelMsgForm">取 消</el-button>
  408. </div>
  409. </el-dialog>
  410. <el-dialog :title="videoNumOptions.title" :visible.sync="videoNumOptions.open" width="1500px" append-to-body>
  411. <userVideo ref="QwUserVideo" @videoResult="qwUserVideoResult"></userVideo>
  412. </el-dialog>
  413. </div>
  414. </template>
  415. <script>
  416. import { sendMsg,} from "../../../api/qw/sopUserLogsInfo";
  417. import ImageUpload from "@/views/qw/sop/ImageUpload.vue";
  418. import { courseList, getVoiceList, getVoiceListByQwUserIds, videoList } from '@/api/qw/sop'
  419. import userVideo from "@/views/qw/userVideo/userVideo";
  420. import {listReward} from "@/api/qw/luckyBag";
  421. export default {
  422. name: "sendMsgSopOpenTool",
  423. components: {ImageUpload,userVideo},
  424. data() {
  425. return {
  426. voiceList: [],
  427. voiceOptions: [], // 语音选项数据
  428. queryParams1: {
  429. pageNum: 1,
  430. pageSize: 10,
  431. dataStatus: '1',
  432. name: null,
  433. type: null
  434. },
  435. luckyBagList: [],
  436. projectFrom:process.env.VUE_APP_PROJECT_FROM,
  437. videoNumOptions: {
  438. title: '选择视频号',
  439. open: false,
  440. content: null,
  441. contentIndex: null,
  442. },
  443. //上传语音的遮罩层
  444. voiceLoading :false,
  445. uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS",
  446. uploadUrlByVoice:process.env.VUE_APP_BASE_API+"/common/uploadOSSByHOOKVoice",
  447. // 遮罩层
  448. loading: true,
  449. // 导出遮罩层
  450. exportLoading: false,
  451. // 选中数组
  452. ids: [],
  453. // 非单个禁用
  454. single: true,
  455. // 非多个禁用
  456. multiple: true,
  457. // 显示搜索条件
  458. showSearch: true,
  459. // 总条数
  460. total: 0,
  461. // sopUserLogsInfo表格数据
  462. sopUserLogsInfoList: [],
  463. sysFsSopWatchStatus: [],
  464. isSalesCallAdded:false,
  465. isSalesCallCustomerAdded:false,
  466. tagList:[],
  467. selectTags:[],
  468. // 弹出层标题
  469. title: "",
  470. // 是否显示弹出层
  471. open: false,
  472. updateOpen:false,
  473. // 查询参数
  474. queryParams: {
  475. pageNum: 1,
  476. pageSize: 10,
  477. tagIds:null,
  478. remark:null,
  479. sopId: null,
  480. userLogsId: null,
  481. userIdParam:null,
  482. startTimeParam:null,
  483. externalContactId: null,
  484. qwUserId: null,
  485. corpId: null,
  486. externalId: null,
  487. fsUserId: null,
  488. externalUserName: null,
  489. createTime: null,
  490. },
  491. courseList:[],
  492. videoList:[],
  493. //插件版
  494. sysQwSopAiContentType:[],
  495. sendMsgOpen:{
  496. title:'一键批量群发',
  497. open:false,
  498. ids:null,
  499. },
  500. // 表单参数
  501. form: {},
  502. setting:[{contentType:'1', value: '',}],
  503. msgForm:{
  504. videoId:null,
  505. courseId:null,
  506. courseType:null,
  507. userIdParam:null,
  508. setting:null,
  509. ids:null,
  510. type:null,
  511. corpId:null,
  512. sopId: null,
  513. sopIds: null,
  514. startTime: null,
  515. sendTime: null,
  516. draftStrategy:1,
  517. qwUserIds: null,
  518. },
  519. // 表单校验
  520. rules: {},
  521. batchRules:{
  522. paramTime: [
  523. { required: true, message: '选择的时间不能为空', trigger: 'blur' }
  524. ],
  525. },
  526. msgRules:{},
  527. };
  528. },
  529. created() {
  530. this.getDicts("sys_qwSopAi_contentType").then(response => {
  531. this.sysQwSopAiContentType = response.data;
  532. });
  533. this.getDicts("sys_fs_sop_watch_status").then(response => {
  534. this.sysFsSopWatchStatus = response.data;
  535. });
  536. courseList().then(response => {
  537. this.courseList = response.list;
  538. });
  539. listReward(this.queryParams1).then(response => {
  540. this.luckyBagList = response.rows;
  541. });
  542. },
  543. methods: {
  544. loadVoiceOptions() {
  545. const qwUserIds = this.msgForm.qwUserIds; // string数组
  546. const corpId = this.msgForm.corpId;
  547. const params ={
  548. qwUserIds: JSON.stringify(qwUserIds), // 转为 "[xxx,xxx]" 格式
  549. corpId
  550. }
  551. if (!this.voiceList) {
  552. this.voiceList = []; // 如果未定义,立即初始化
  553. }
  554. // 如果已加载过语音选项,则不再重复加载
  555. if (this.voiceList.length > 0) {
  556. return;
  557. }
  558. this.voiceLoading = true;
  559. if (!qwUserIds || (Array.isArray(qwUserIds) && qwUserIds.length === 0)) {
  560. this.$message.warning('无个人qw_user_id,无法加载语音选项');
  561. return; // 提前返回,不继续执行
  562. }
  563. // 调用接口获取语音列表
  564. getVoiceListByQwUserIds(params).then(response => {
  565. this.voiceList = response.rows || [];
  566. // 如果语音列表为空,给出提示
  567. if (this.voiceList.length === 0) {
  568. this.$message.info('暂无可用的语音内容');
  569. }
  570. }).catch(error => {
  571. console.error('加载语音选项失败:', error);
  572. this.$message.error('加载语音选项失败');
  573. this.voiceList = []; // 确保列表为空
  574. }).finally(() => {
  575. this.voiceLoading = false;
  576. });
  577. },
  578. getLuckyBagStatus(content) {
  579. const selectedLuckyBag = this.luckyBagList.find(item => item.id === content.luckyBagId);
  580. if (selectedLuckyBag) {
  581. content.luckyBagDataStatus = selectedLuckyBag.dataStatus; // 自动填充标题
  582. } else {
  583. // 若未找到对应直播间,清空标题和封面(可选)
  584. content.luckyBagDataStatus = null;
  585. }
  586. },
  587. qwUserVideoResult(val) {
  588. // 根据选中的内容,将返回的数据更新到相应的表单项
  589. const content = this.videoNumOptions.content;
  590. const setList = content[this.videoNumOptions.contentIndex];
  591. setList.nickname = val.nickname;
  592. setList.avatar = val.avatar;
  593. setList.coverUrl = val.coverUrl;
  594. setList.thumbUrl = val.thumbUrl;
  595. setList.desc = val.desc;
  596. setList.url = val.url;
  597. setList.extras = val.extras;
  598. setList.videoId = val.id;
  599. console.info(setList)
  600. this.videoNumOptions.open = false;
  601. },
  602. oneClickGroupSending(val,type,corpId,isMine,qwUserIds){
  603. this.sendMsgOpen.open= true;
  604. this.msgForm.sopIds = val;
  605. this.msgForm.type = type;
  606. this.msgForm.corpId = corpId;
  607. this.msgForm.isMine = isMine;
  608. this.msgForm.qwUserIds = qwUserIds;
  609. },
  610. /** 内容类别 APP,与后端 contentType 字符串 "9" 对齐(兼容字典 number / string) */
  611. isAppContentType(ct) {
  612. if (ct === undefined || ct === null || ct === '') {
  613. return false;
  614. }
  615. return String(ct).trim() === '9';
  616. },
  617. isFeishuContentType(ct) {
  618. if (ct === undefined || ct === null || ct === '') {
  619. return false;
  620. }
  621. return String(ct).trim() === '17';
  622. },
  623. isLinkH5ContentType(ct) {
  624. if (ct === undefined || ct === null || ct === '') {
  625. return false;
  626. }
  627. return String(ct).trim() === '3';
  628. },
  629. /**
  630. * 提交前规范化 setting:contentType 统一为字符串;APP 项固定为 "9",空/0 的 expiresDays 不传
  631. */
  632. buildSettingPayload() {
  633. return this.setting.map(row => {
  634. const o = { ...row };
  635. o.contentType = o.contentType !== undefined && o.contentType !== null ? String(o.contentType) : o.contentType;
  636. if (this.isAppContentType(row.contentType)) {
  637. o.contentType = '9';
  638. const d = o.expiresDays;
  639. if (d === undefined || d === null || d === '' || Number(d) === 0) {
  640. delete o.expiresDays;
  641. }
  642. }
  643. if (this.isFeishuContentType(row.contentType)) {
  644. o.contentType = '17';
  645. if (o.feishuNeedAuth === undefined || o.feishuNeedAuth === null || o.feishuNeedAuth === '') {
  646. o.feishuNeedAuth = 1;
  647. }
  648. }
  649. return o;
  650. });
  651. },
  652. courseChange() {
  653. if (this.msgForm.courseId != null ) {
  654. const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === this.msgForm.courseId);
  655. for (let i = 0; i < this.setting.length; i++) {
  656. //响应式直接给链接的标题/封面上值
  657. if (selectedCourse && this.msgForm.courseId != null) {
  658. if ( this.setting[i].contentType == 3 || this.setting[i].contentType == 9 || this.setting[i].contentType == 17 ){
  659. this.$set(this.setting[i], 'linkTitle', selectedCourse.dictLabel);
  660. this.$set(this.setting[i], 'linkImageUrl', selectedCourse.dictImgUrl);
  661. }
  662. if ( this.setting[i].contentType == 4 || this.setting[i].contentType == 10 ){
  663. this.$set(this.setting[i], 'miniprogramPicUrl', selectedCourse.dictImgUrl);
  664. }
  665. }
  666. }
  667. }
  668. videoList(this.msgForm.courseId).then(response => {
  669. this.videoList=response.list;
  670. });
  671. this.msgForm.videoId = null
  672. },
  673. videoIdChange() {
  674. if (this.msgForm.videoId != null ) {
  675. // 查找选中的课节对应的 label
  676. const selectedVideo = this.videoList.find(course => parseInt(course.dictValue) === this.msgForm.videoId);
  677. for (let i = 0; i < this.setting.length; i++) {
  678. //响应式直接给链接的描述上值
  679. if (selectedVideo && this.msgForm.videoId != null) {
  680. if (this.setting[i].contentType == 3 || this.setting[i].contentType == 9 || this.setting[i].contentType == 17 ){
  681. this.$set(this.setting[i], 'linkDescribe', selectedVideo.dictLabel);
  682. if (this.projectFrom == 'sxjz' && selectedVideo.dictImgUrl) {
  683. this.$set(this.setting[i], 'linkImageUrl', selectedVideo.dictImgUrl);
  684. }
  685. }
  686. if (this.setting[i].contentType == 4 || this.setting[i].contentType == 10 ){
  687. this.$set(this.setting[i], 'miniprogramTitle', selectedVideo.dictLabel);
  688. if (this.projectFrom == 'sxjz' && selectedVideo.dictImgUrl) {
  689. this.$set(this.setting[i], 'miniprogramPicUrl', selectedVideo.dictImgUrl);
  690. }
  691. }
  692. }
  693. }
  694. }
  695. },
  696. handleAvatarSuccessFile(res, file, item) {
  697. if (res.code === 200) {
  698. // 使用 $set 确保响应式更新
  699. this.$set(item, 'fileUrl', res.url);
  700. } else {
  701. this.msgError(res.msg);
  702. }
  703. },
  704. beforeAvatarUploadFile(file){
  705. const isLt1M = file.size / 1024 / 1024 < 10;
  706. if (!isLt1M) {
  707. this.$message.error('上传大小不能超过 10MB!');
  708. }
  709. return isLt1M;
  710. },
  711. //下载文件
  712. downloadUrl(materialUrl) {
  713. // 直接返回文件 URL
  714. return materialUrl;
  715. },
  716. handleAvatarSuccessVideo(res, file, item) {
  717. if(res.code==200){
  718. // 使用 $set 确保响应式更新
  719. this.$set(item, 'videoUrl', res.url);
  720. }
  721. else{
  722. this.msgError(res.msg);
  723. }
  724. },
  725. beforeAvatarUploadVideo(file){
  726. const isLt30M = file.size / 1024 / 1024 < 10;
  727. const isMP4 = file.type === 'video/mp4';
  728. if (!isMP4) {
  729. this.$message.error('仅支持上传 MP4 格式的视频文件!');
  730. return false;
  731. }
  732. if (!isLt30M) {
  733. this.$message.error('上传大小不能超过 10MB!');
  734. return false;
  735. }
  736. return true;
  737. },
  738. handleInputVideoText(value,content){
  739. // 允许的字符:中文、英文(大小写)、数字和指定标点符号(,。!?)
  740. const regex = /^[\u4e00-\u9fa5,。!?,!?]+$/;
  741. // 删除不符合条件的字符
  742. const filteredValue = value.split('').filter(char => regex.test(char)).join('');
  743. this.$set(content, 'value', filteredValue);
  744. },
  745. delSetList(index){
  746. this.setting.splice(index,1)
  747. },
  748. addSetList(){
  749. const newSetting = {
  750. contentType:'1',
  751. value: '',
  752. };
  753. // 将新设置项添加到 content.setting 数组中
  754. this.setting.push(newSetting);
  755. },
  756. //选择视频号
  757. hanldeSelectVideoNum(content, index) {
  758. this.videoNumOptions.content = content;
  759. this.videoNumOptions.contentIndex = index;
  760. this.videoNumOptions.open = true;
  761. },
  762. handleKeydown(event, index) {
  763. const item = this.setting[index];
  764. const textarea = this.$refs[`textarea-${index}`][0].$refs.textarea;
  765. const cursorPosition = textarea.selectionStart;
  766. // 检查是否按下了 Backspace 或 Delete 键
  767. if (event.key === 'Backspace' || event.key === 'Delete') {
  768. const tags = ['#销售称呼#', '#客户称呼#']; // 需要检查的标签
  769. const value = item.value;
  770. // 遍历标签,检查是否需要删除
  771. for (const tag of tags) {
  772. let start, end;
  773. if (event.key === 'Backspace') {
  774. // 检查光标前是否是当前标签的一部分
  775. start = cursorPosition - tag.length;
  776. if (start >= 0 && value.slice(start, cursorPosition) === tag) {
  777. // 删除整个标签
  778. item.value = value.slice(0, start) + value.slice(cursorPosition);
  779. // 更新光标位置
  780. this.$nextTick(() => {
  781. textarea.setSelectionRange(start, start);
  782. });
  783. // 更新状态
  784. if (tag === '#销售称呼#') item.isSalesCallAdded = false;
  785. if (tag === '#客户称呼#') item.isSalesCallCustomerAdded = false;
  786. event.preventDefault(); // 阻止默认删除行为
  787. break; // 找到匹配的标签后退出循环
  788. }
  789. } else if (event.key === 'Delete') {
  790. // 检查光标后是否是当前标签的一部分
  791. end = cursorPosition + tag.length;
  792. if (end <= value.length && value.slice(cursorPosition, end) === tag) {
  793. // 删除整个标签
  794. item.value = value.slice(0, cursorPosition) + value.slice(end);
  795. // 更新状态
  796. if (tag === '#销售称呼#') item.isSalesCallAdded = false;
  797. if (tag === '#客户称呼#') item.isSalesCallCustomerAdded = false;
  798. event.preventDefault(); // 阻止默认删除行为
  799. break; // 找到匹配的标签后退出循环
  800. }
  801. }
  802. // 检查光标是否位于标签的中间
  803. for (let i = 0; i <= tag.length; i++) {
  804. const tagStart = cursorPosition - i;
  805. const tagEnd = tagStart + tag.length;
  806. if (
  807. tagStart >= 0 &&
  808. tagEnd <= value.length &&
  809. value.slice(tagStart, tagEnd) === tag
  810. ) {
  811. // 删除整个标签
  812. item.value = value.slice(0, tagStart) + value.slice(tagEnd);
  813. // 更新光标位置
  814. this.$nextTick(() => {
  815. textarea.setSelectionRange(tagStart, tagStart);
  816. });
  817. // 更新状态
  818. if (tag === '#销售称呼#') item.isSalesCallAdded = false;
  819. if (tag === '#客户称呼#') item.isSalesCallCustomerAdded = false;
  820. event.preventDefault(); // 阻止默认删除行为
  821. break; // 找到匹配的标签后退出循环
  822. }
  823. }
  824. }
  825. }
  826. },
  827. // 切换添加销售称呼按钮点击事件
  828. toggleSalesCall(index) {
  829. const item = this.setting[index];
  830. const salesCall = '#销售称呼#';
  831. const textarea = this.$refs[`textarea-${index}`][0].$refs.textarea;
  832. // 获取当前光标位置
  833. const cursorPosition = textarea.selectionStart;
  834. if (item.isSalesCallAdded) {
  835. // 移除所有的 #销售称呼#
  836. item.value = item.value.replace(new RegExp(salesCall, 'g'), '');
  837. } else {
  838. // 添加 #销售称呼#
  839. item.value = item.value.slice(0, cursorPosition) + salesCall + item.value.slice(cursorPosition);
  840. }
  841. // 切换状态
  842. item.isSalesCallAdded = !item.isSalesCallAdded;
  843. // 保持光标位置
  844. this.$nextTick(() => {
  845. textarea.setSelectionRange(cursorPosition, cursorPosition);
  846. });
  847. },
  848. toggleSalesCallCustomer(index) {
  849. const item = this.setting[index];
  850. const salesCall = '#客户称呼#';
  851. const textarea = this.$refs[`textarea-${index}`][0].$refs.textarea;
  852. // 获取当前光标位置
  853. const cursorPosition = textarea.selectionStart;
  854. if (item.isSalesCallCustomerAdded) {
  855. // 移除所有的 #销售称呼#
  856. item.value = item.value.replace(new RegExp(salesCall, 'g'), '');
  857. } else {
  858. // 添加 #客户称呼#
  859. item.value = item.value.slice(0, cursorPosition) + salesCall + item.value.slice(cursorPosition);
  860. }
  861. // 切换状态
  862. item.isSalesCallCustomerAdded = !item.isSalesCallCustomerAdded;
  863. // 保持光标位置
  864. this.$nextTick(() => {
  865. textarea.setSelectionRange(cursorPosition, cursorPosition);
  866. });
  867. },
  868. handleContentTypeChange() {
  869. //如果是链接的才上
  870. if (this.msgForm.courseId != null ) {
  871. const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === this.msgForm.courseId);
  872. for (let i = 0; i < this.setting.length; i++) {
  873. //响应式直接给链接的标题/封面上值
  874. if (selectedCourse && this.msgForm.courseId != null) {
  875. if (this.setting[i].contentType == 3 || this.setting[i].contentType == 9 || this.setting[i].contentType == 17){
  876. this.$set(this.setting[i], 'linkTitle', selectedCourse.dictLabel);
  877. this.$set(this.setting[i], 'linkImageUrl', selectedCourse.dictImgUrl);
  878. }
  879. if (this.setting[i].contentType == 4 || this.setting[i].contentType == 10){
  880. this.$set(this.setting[i], 'miniprogramPicUrl', selectedCourse.dictImgUrl);
  881. }
  882. }
  883. }
  884. }
  885. if (this.msgForm.videoId != null ) {
  886. // 查找选中的课节对应的 label
  887. const selectedVideo = this.videoList.find(course => parseInt(course.dictValue) === this.msgForm.videoId);
  888. for (let i = 0; i < this.setting.length; i++) {
  889. //响应式直接给链接的描述上值
  890. if (selectedVideo && this.msgForm.videoId != null) {
  891. if (this.setting[i].contentType == 3 || this.setting[i].contentType == 9 || this.setting[i].contentType == 17){
  892. this.$set(this.setting[i], 'linkDescribe', selectedVideo.dictLabel);
  893. if (this.projectFrom == 'sxjz' && selectedVideo.dictImgUrl) {
  894. this.$set(this.setting[i], 'linkImageUrl', selectedVideo.dictImgUrl);
  895. }
  896. }
  897. if (this.setting[i].contentType == 4 || this.setting[i].contentType == 10){
  898. this.$set(this.setting[i], 'miniprogramTitle', selectedVideo.dictLabel);
  899. if (this.projectFrom == 'sxjz' && selectedVideo.dictImgUrl) {
  900. this.$set(this.setting[i], 'miniprogramPicUrl', selectedVideo.dictImgUrl);
  901. }
  902. }
  903. }
  904. }
  905. }
  906. },
  907. resetSendMsgSop() {
  908. this.msgForm = {
  909. videoId:null,
  910. courseId:null,
  911. courseType:null,
  912. setting:null,
  913. sendTime:null,
  914. draftStrategy:1,
  915. };
  916. this.resetForm("msgForm");
  917. },
  918. // 多选框选中数据
  919. handleSelectionChange(selection) {
  920. this.ids = selection.map(item => item.id)
  921. this.single = selection.length!==1
  922. this.multiple = !selection.length
  923. },
  924. submitMsgForm(){
  925. this.$refs["msgForm"].validate(valid => {
  926. if (valid) {
  927. if (this.msgForm.draftStrategy==1){
  928. if (this.setting.length <= 0) {
  929. return this.$message.error("请添加规则")
  930. }
  931. if (this.msgForm.courseId===null || this.msgForm.courseId===''){
  932. return this.$message.error("课程不能为空")
  933. }
  934. if (this.msgForm.videoId===null || this.msgForm.videoId===''){
  935. return this.$message.error("课节不能为空")
  936. }
  937. if (this.msgForm.courseType===null || this.msgForm.courseType===''){
  938. return this.$message.error("消息类型不能为空")
  939. }
  940. const hasAppBlock = this.setting.some(s => this.isAppContentType(s.contentType));
  941. if (hasAppBlock && (this.msgForm.corpId === null || this.msgForm.corpId === undefined || this.msgForm.corpId === '')) {
  942. return this.$message.error("企业编号不能为空")
  943. }
  944. for (let i = 0; i < this.setting.length; i++) {
  945. if (this.setting[i].contentType == 1 && (this.setting[i].value == null || this.setting[i].value == "")) {
  946. return this.$message.error("内容不能为空")
  947. }
  948. if (this.setting[i].contentType == 2 && (this.setting[i].imgUrl == null || this.setting[i].imgUrl == "")) {
  949. return this.$message.error("图片不能为空")
  950. }
  951. if ((this.isLinkH5ContentType(this.setting[i].contentType) || this.isAppContentType(this.setting[i].contentType) || this.isFeishuContentType(this.setting[i].contentType)) && (this.setting[i].linkTitle == null || this.setting[i].linkTitle == "")) {
  952. return this.$message.error(this.isAppContentType(this.setting[i].contentType) ? "APP 消息标题不能为空" : (this.isFeishuContentType(this.setting[i].contentType) ? "飞书消息标题不能为空" : "链接标题不能为空"))
  953. }
  954. if ((this.isLinkH5ContentType(this.setting[i].contentType) || this.isAppContentType(this.setting[i].contentType) || this.isFeishuContentType(this.setting[i].contentType)) && (this.setting[i].linkDescribe == null || this.setting[i].linkDescribe == "")) {
  955. return this.$message.error(this.isAppContentType(this.setting[i].contentType) ? "APP 描述不能为空" : (this.isFeishuContentType(this.setting[i].contentType) ? "飞书描述不能为空" : "链接描述不能为空"))
  956. }
  957. if ((this.isLinkH5ContentType(this.setting[i].contentType) || this.isAppContentType(this.setting[i].contentType) || this.isFeishuContentType(this.setting[i].contentType)) && (this.setting[i].linkImageUrl == null || this.setting[i].linkImageUrl == "")) {
  958. return this.$message.error(this.isAppContentType(this.setting[i].contentType) ? "APP 封面不能为空" : (this.isFeishuContentType(this.setting[i].contentType) ? "飞书封面不能为空" : "链接图片不能为空"))
  959. }
  960. if (this.isLinkH5ContentType(this.setting[i].contentType) && this.setting[i].type == 1 && (this.setting[i].linkUrl == null || this.setting[i].linkUrl == "")) {
  961. return this.$message.error("链接地址不能为空")
  962. }
  963. if ((this.setting[i].contentType == 4 || this.setting[i].contentType == 10) && (this.setting[i].miniprogramTitle == null || this.setting[i].miniprogramTitle == "")) {
  964. return this.$message.error("小程序消息标题不能为空")
  965. }
  966. if ((this.setting[i].contentType == 4 || this.setting[i].contentType == 10) && (this.setting[i].miniprogramPicUrl == null || this.setting[i].miniprogramPicUrl == "")) {
  967. return this.$message.error("小程序封面地址不能为空")
  968. }
  969. if (this.setting[i].contentType == 10 && (this.setting[i].miniprogramPage == null || this.setting[i].miniprogramPage == "")) {
  970. return this.$message.error("小程序page地址不能为空")
  971. }
  972. if (this.setting[i].contentType == 5 && (this.setting[i].fileUrl == null || this.setting[i].fileUrl == "")) {
  973. return this.$message.error("文件不能为空")
  974. }
  975. if (this.setting[i].contentType == 6 && (this.setting[i].videoUrl == null || this.setting[i].videoUrl == "")) {
  976. return this.$message.error("视频不能为空")
  977. }
  978. if (this.setting[i].contentType == 7 && (this.setting[i].value == null || this.setting[i].value == "")) {
  979. return this.$message.error("语音不能为空")
  980. }
  981. if (this.setting[i].contentType == 13 && (this.setting[i].miniprogramTitle == null || this.setting[i].miniprogramTitle == "")) {
  982. return this.$message.error("订阅号文章标题不能为空")
  983. }
  984. if (this.setting[i].contentType == 13 && (this.setting[i].miniprogramPicUrl == null || this.setting[i].miniprogramPicUrl == "")) {
  985. return this.$message.error("订阅号文章封面不能为空")
  986. }
  987. if (this.setting[i].contentType == 13 && (this.setting[i].articleUrl == null || this.setting[i].articleUrl == "")) {
  988. return this.$message.error("订阅号文章链接不能为空")
  989. }
  990. }
  991. this.msgForm.setting = JSON.stringify(this.buildSettingPayload())
  992. }
  993. this.sendMsgOpen.open = false;
  994. const loading = this.$loading({
  995. lock: true,
  996. text: '正在执行中请稍后~~请不要刷新页面!!',
  997. spinner: 'el-icon-loading',
  998. background: 'rgba(0, 0, 0, 0.7)'
  999. });
  1000. sendMsg(this.msgForm).then(response => {
  1001. this.msgSuccess("一键群发成功");
  1002. loading.close();
  1003. this.setting=[];
  1004. this.resetSendMsgSop();
  1005. }).finally(()=>{
  1006. loading.close();
  1007. });
  1008. }
  1009. });
  1010. },
  1011. cancelMsgForm(){
  1012. this.sendMsgOpen.open = false;
  1013. this.resetSendMsgSop();
  1014. },
  1015. }
  1016. };
  1017. </script>