telControl.vue 61 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893
  1. <template>
  2. <div class="phoneControls" v-loading="state.loading">
  3. <!-- 电话状态 -->
  4. <div class="infos">
  5. <div class="pt5" :class="talkTime ? '' : 'mt8'"><span>分机号:</span>{{ telStatusInfo.telsNo }}</div>
  6. <div class="pt5" :class="talkTime ? '' : 'mt8'">
  7. <span>状态:</span><b class="dutyOn_status">{{ currentStatusText }}</b>
  8. </div>
  9. <div class="pt5" v-if="talkTime">
  10. <span>通话时长:</span> <el-text tag="b" type="danger">{{ formatDuration(talkTime, false) }}</el-text>
  11. </div>
  12. </div>
  13. <!-- 按钮列表 -->
  14. <div class="btn-container">
  15. <!-- 签入 -->
  16. <template v-if="telStatusInfo.isDutyOn">
  17. <!-- 签出可用 -->
  18. <div
  19. class="item active"
  20. @click="onControlClick('dutyOff')"
  21. v-if="activeArr.includes('dutyOff')"
  22. @mouseenter="onHover('dutyOffSrc', 'phoneControls/dutyOff_white.png')"
  23. @mouseleave="onHover('dutyOffSrc', 'phoneControls/dutyOff_blue.png')"
  24. title="签出"
  25. >
  26. <img :src="state.dutyOffSrc" alt="" />
  27. <span>签出</span>
  28. </div>
  29. <!-- 签出不可用 -->
  30. <div class="item disabled" v-else title="签出">
  31. <img :src="getImageUrl('phoneControls/dutyOff_grey.png')" alt="" />
  32. <span>签出</span>
  33. </div>
  34. </template>
  35. <!-- 灰色签入不可用 -->
  36. <template v-else>
  37. <div
  38. class="item active"
  39. @click="onControlClick('dutyOn')"
  40. title="签入"
  41. @mouseenter="onHover('dutyOnSrc', 'phoneControls/dutyOn_white.png')"
  42. @mouseleave="onHover('dutyOnSrc', 'phoneControls/dutyOn_blue.png')"
  43. >
  44. <img :src="state.dutyOnSrc" alt="" />
  45. <span>签入</span>
  46. </div>
  47. </template>
  48. <!-- &lt;!&ndash; 外呼模式和取消外呼模式 可用 &ndash;&gt;
  49. <template v-if="telStatusInfo.isDutyOn && activeArr.includes('callOut')">
  50. <div
  51. class="item active"
  52. :title="telStatusInfo.isHold ? '取消外呼模式' : '外呼模式'"
  53. @click="onControlClick(telStatusInfo.isCallOut ? 'unCallOut' : 'callOut')"
  54. @mouseenter="onHover('callOutSrc', 'phoneControls/evaluate_white.png')"
  55. @mouseleave="onHover('callOutSrc', 'phoneControls/evaluate_blue.png')"
  56. >
  57. <img :src="state.callOutSrc" alt="" />
  58. <span>{{ telStatusInfo.isCallOut ? '取消外呼模式' : '外呼模式' }}</span>
  59. </div>
  60. </template>
  61. &lt;!&ndash; 灰色外呼不可用 &ndash;&gt;
  62. <template v-else>
  63. <div class="item disabled" title="外呼模式">
  64. <img :src="getImageUrl('phoneControls/evaluate_grey.png')" alt="" />
  65. <span>外呼模式</span>
  66. </div>
  67. </template>-->
  68. <!-- 可用挂断 -->
  69. <template v-if="telStatusInfo.isDutyOn && activeArr.includes('hangup')">
  70. <div
  71. class="item active"
  72. :class="state.active.includes('hangup') ? 'active' : ''"
  73. @click="onControlClick('hangup')"
  74. title="挂断"
  75. @mouseenter="onHover('hangupSrc', 'phoneControls/hangup_white.png')"
  76. @mouseleave="onHover('hangupSrc', 'phoneControls/hangup_blue.png')"
  77. >
  78. <img :src="state.hangupSrc" alt="" />
  79. <span>挂断</span>
  80. </div>
  81. </template>
  82. <!-- 灰色挂断 不可用 -->
  83. <template v-else>
  84. <div class="item disabled" title="挂断">
  85. <img :src="getImageUrl('phoneControls/hangup_grey.png')" alt="" />
  86. <span>挂断</span>
  87. </div>
  88. </template>
  89. <!-- 小休和结束休息 可用 -->
  90. <template v-if="telStatusInfo.isDutyOn && activeArr.includes('rest')">
  91. <div
  92. class="item active"
  93. @click="onControlClick('restEnd')"
  94. v-if="telStatusInfo.isRest === 'resting'"
  95. title="结束小休"
  96. @mouseenter="onHover('restSrc', 'phoneControls/rest_white.png')"
  97. @mouseleave="onHover('restSrc', 'phoneControls/rest_blue.png')"
  98. >
  99. <img :src="state.restSrc" alt="" />
  100. <span
  101. >结束<span v-if="restReason">({{ restReason }})</span></span
  102. >
  103. </div>
  104. <div
  105. class="item active"
  106. @click="onControlClick('rest')"
  107. v-else-if="telStatusInfo.isRest === 'unRest'"
  108. title="小休"
  109. @mouseenter="onHover('restSrc', 'phoneControls/rest_white.png')"
  110. @mouseleave="onHover('restSrc', 'phoneControls/rest_blue.png')"
  111. >
  112. <img :src="state.restSrc" alt="" />
  113. <span>小休 </span>
  114. </div>
  115. <div class="item disabled" title="审批中" v-else-if="telStatusInfo.isRest === 'InReview'">
  116. <img :src="getImageUrl('phoneControls/rest_grey.png')" alt="" />
  117. <span>审批中</span>
  118. </div>
  119. </template>
  120. <!-- 灰色小休不可用 -->
  121. <template v-else>
  122. <div class="item disabled" title="小休">
  123. <img :src="getImageUrl('phoneControls/rest_grey.png')" alt="" />
  124. <span>小休</span>
  125. </div>
  126. </template>
  127. <!-- 保持和取消保持 可用 -->
  128. <template v-if="telStatusInfo.isDutyOn && activeArr.includes('hold')">
  129. <div
  130. class="item active"
  131. :title="telStatusInfo.isHold ? '取消保持' : '保持'"
  132. @click="onControlClick(telStatusInfo.isHold ? 'unHold' : 'hold')"
  133. @mouseenter="onHover('holdSrc', 'phoneControls/hold_white.png')"
  134. @mouseleave="onHover('holdSrc', 'phoneControls/hold_blue.png')"
  135. >
  136. <img :src="state.holdSrc" alt="" />
  137. <span>{{ telStatusInfo.isHold ? '取消保持' : '保持' }}</span>
  138. </div>
  139. </template>
  140. <!-- 灰色保持不可用 -->
  141. <template v-else>
  142. <div class="item disabled" title="保持">
  143. <img :src="getImageUrl('phoneControls/hold_grey.png')" alt="" />
  144. <span>保持</span>
  145. </div>
  146. </template>
  147. <!-- 话后整理和取消话后整理中 可用-->
  148. <template v-if="telStatusInfo.isDutyOn && activeArr.includes('TalkingDeal')">
  149. <div
  150. class="item active"
  151. @click="onControlClick(telStatusInfo.isTalkingDeal ? 'unTalkingDeal' : 'TalkingDeal')"
  152. @mouseenter="onHover('talkingDealSrc', 'phoneControls/talkingDeal_white.png')"
  153. :title="telStatusInfo.isTalkingDeal ? '取消话后整理' : '话后整理'"
  154. @mouseleave="onHover('talkingDealSrc', 'phoneControls/talkingDeal_blue.png')"
  155. >
  156. <img :src="state.talkingDealSrc" alt="" />
  157. <span>{{ telStatusInfo.isTalkingDeal ? '取消话后整理' : '话后整理' }}</span>
  158. </div>
  159. </template>
  160. <!-- 话后整理中不可用 -->
  161. <template v-else>
  162. <div class="item disabled" title="话后整理">
  163. <img :src="getImageUrl('phoneControls/talkingDeal_grey.png')" alt="" />
  164. <span>话后整理</span>
  165. </div>
  166. </template>
  167. <!-- 转接 可用 -->
  168. <template v-if="telStatusInfo.isDutyOn && activeArr.includes('transfer')">
  169. <div
  170. class="item active"
  171. @click="onControlClick('transfer')"
  172. title="保持"
  173. @mouseenter="onHover('transferSrc', 'phoneControls/transfer_white.png')"
  174. @mouseleave="onHover('transferSrc', 'phoneControls/transfer_blue.png')"
  175. >
  176. <img :src="state.transferSrc" alt="" />
  177. <span>转接</span>
  178. </div>
  179. </template>
  180. <!-- 转接不可用 -->
  181. <template v-else>
  182. <div class="item disabled" title="保持">
  183. <img :src="getImageUrl('phoneControls/transfer_grey.png')" alt="" />
  184. <span>转接</span>
  185. </div>
  186. </template>
  187. <!-- 三方会议 可用(当前处于通话中)-->
  188. <template v-if="telStatusInfo.isDutyOn && activeArr.includes('conference') && onCallArr.length !== 1">
  189. <div
  190. class="item active"
  191. @mouseenter="onHover('conferenceSrc', 'phoneControls/conference_white.png')"
  192. title="三方会议"
  193. @mouseleave="onHover('conferenceSrc', 'phoneControls/conference_blue.png')"
  194. @click="onControlClick('conference')"
  195. >
  196. <img :src="state.conferenceSrc" alt="" />
  197. <span>三方会议</span>
  198. </div>
  199. </template>
  200. <!-- 三方会议并且处于三方会议中 -->
  201. <template v-else-if="telStatusInfo.isDutyOn && activeArr.includes('conference') && onCallArr.length === 1">
  202. <el-popover :width="130 * onCallArr.length" :offset="0" v-model:visible="threeWayVisible" trigger="hover" popper-class="hangup-popover">
  203. <template #reference>
  204. <div
  205. class="item active"
  206. @mouseenter="onHover('conferenceSrc', 'phoneControls/conference_white.png')"
  207. title="三方会议"
  208. @mouseleave="onHover('conferenceSrc', 'phoneControls/conference_blue.png')"
  209. >
  210. <img :src="state.conferenceSrc" alt="" />
  211. <span>三方会议({{ onCallArr.length }})</span>
  212. </div>
  213. </template>
  214. <div class="hangup-container">
  215. <div class="hangup-item" v-for="(item, index) in onCallArr" :key="index">
  216. <p class="hangup-item-phoneNumber">{{ item.telNo }}</p>
  217. <el-button size="small" @click="kickOut(item)" class="default-button">踢出</el-button>
  218. </div>
  219. </div>
  220. </el-popover>
  221. </template>
  222. <!-- 三方会议 不可用 -->
  223. <template v-else>
  224. <div class="item disabled" title="三方会议">
  225. <img :src="getImageUrl('phoneControls/conference_grey.png')" alt="" />
  226. <span>三方会议</span>
  227. </div>
  228. </template>
  229. <!-- 呼叫 可用-->
  230. <template v-if="telStatusInfo.isDutyOn && activeArr.includes('outbound')">
  231. <div
  232. class="item active"
  233. @mouseenter="onHover('outboundSrc', 'phoneControls/outbound_white.png')"
  234. title="呼叫"
  235. @mouseleave="onHover('outboundSrc', 'phoneControls/outbound_blue.png')"
  236. @click="onControlClick('outbound')"
  237. >
  238. <img :src="state.outboundSrc" alt="" />
  239. <span>呼叫</span>
  240. </div>
  241. </template>
  242. <!-- 呼叫 不可用 -->
  243. <template v-else>
  244. <div class="item disabled" title="呼叫">
  245. <img :src="getImageUrl('phoneControls/outbound_grey.png')" alt="" />
  246. <span>呼叫</span>
  247. </div>
  248. </template>
  249. </div>
  250. <!-- 签入时长 -->
  251. <div class="duty-on-time">
  252. <span class="duty-on-time-label">签入时长</span>
  253. <el-text class="duty-on-time-time" tag="b" type="danger" v-if="onDutyTime">{{ formatDuration(onDutyTime) }}</el-text>
  254. </div>
  255. </div>
  256. <!-- 占位标签 -->
  257. <div class="seizeSeat-box"></div>
  258. <!-- 功能 -->
  259. <!-- 签入弹窗 -->
  260. <el-dialog v-model="state.dutyDialogVisible" draggable title="签入" width="500px" :show-close="false">
  261. <el-form :model="state.dutyForm" label-width="80px" ref="dutyFormRef">
  262. <el-form-item
  263. label="分机"
  264. prop="telNo"
  265. :rules="[{ required: true, message: '请选择需要签入的分机', trigger: 'change' }]"
  266. v-if="AppConfigInfo.isNeedTelNo"
  267. >
  268. <el-select-v2
  269. v-model="state.dutyForm.telNo"
  270. :options="state.telsList"
  271. :props="{
  272. label: 'telNo',
  273. value: 'telNo',
  274. }"
  275. placeholder="选择要签入的分机"
  276. filterable
  277. class="w100"
  278. />
  279. </el-form-item>
  280. <!-- 是否需要输入分机密码 -->
  281. <el-form-item
  282. label="分机密码"
  283. prop="password"
  284. :rules="[{ required: true, message: '请输入分机密码', trigger: 'blur' }]"
  285. v-if="AppConfigInfo.isTelNeedVerify"
  286. >
  287. <el-input v-model="state.dutyForm.password" placeholder="请输入分机密码" />
  288. </el-form-item>
  289. </el-form>
  290. <template #footer>
  291. <span class="dialog-footer">
  292. <el-button @click="state.dutyDialogVisible = false" class="default-button" :loading="state.loading">取 消</el-button>
  293. <el-button type="primary" @click="clickOnDuty(dutyFormRef)" :loading="state.loading">确 定</el-button>
  294. </span>
  295. </template>
  296. </el-dialog>
  297. <!-- 小休弹窗 -->
  298. <el-dialog
  299. v-model="state.restDialogVisible"
  300. ref="dialogRestRef"
  301. draggable
  302. title="小休申请"
  303. :width="AppConfigInfo.isRestApproval ? '60%' : '500px'"
  304. @mouseup="mouseup"
  305. :style="'transform: ' + state.transform + ';'"
  306. @close="restFormOpened"
  307. >
  308. <!-- 需要审核 -->
  309. <template v-if="AppConfigInfo.isRestApproval">
  310. <el-form :model="state.restForm" label-width="100px" ref="restFormRef">
  311. <el-row :gutter="10">
  312. <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8">
  313. <el-form-item label="小休" prop="reason" :rules="[{ required: true, message: '请选择小休原因', trigger: 'change' }]">
  314. <el-select v-model="state.restForm.reason" placeholder="请选择小休原因" class="w100" clearable>
  315. <el-option v-for="item in state.restReasonOptions" :key="item.dicDataValue" :label="item.dicDataName" :value="item.dicDataValue" />
  316. </el-select>
  317. </el-form-item>
  318. </el-col>
  319. <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8">
  320. <el-form-item label="下一环节" prop="nextStepCode" :rules="[{ required: true, message: '请选择下一环节', trigger: 'change' }]">
  321. <el-select v-model="state.restForm.nextStepCode" placeholder="请选择下一环节" class="w100" @change="selectNextStep">
  322. <el-option v-for="item in state.nextStepOptions" :key="item.key" :label="item.value" :value="item.key" />
  323. </el-select>
  324. </el-form-item>
  325. </el-col>
  326. <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8">
  327. <el-form-item label="处理人" prop="nextHandlers" :rules="[{ required: false, message: '请选择处理人', trigger: 'change' }]">
  328. <el-select
  329. v-model="state.restForm.nextHandlers"
  330. multiple
  331. filterable
  332. placeholder="请选择处理人"
  333. class="w100"
  334. @change="selectHandlers"
  335. value-key="key"
  336. >
  337. <el-option v-for="item in state.handlerOptions" :key="item.key" :label="item.value" :value="item" />
  338. </el-select>
  339. </el-form-item>
  340. </el-col>
  341. <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8" v-if="state.restForm.nextHandlers.length > 1">
  342. <el-form-item
  343. label="主办"
  344. prop="nextMainHandler"
  345. multiple
  346. filterable
  347. :rules="[{ required: false, message: '请选择主办', trigger: 'change' }]"
  348. >
  349. <el-select v-model="state.restForm.nextMainHandler" placeholder="请选择主办" class="w100">
  350. <el-option v-for="item in state.handlerMainOptions" :key="item.key" :label="item.value" :value="item.key" />
  351. </el-select>
  352. </el-form-item>
  353. </el-col>
  354. <el-col :xs="24" :sm="12" :md="8" :lg="8" :xl="8">
  355. <el-form-item label="" prop="acceptSms">
  356. <el-checkbox v-model="state.restForm.acceptSms" label="短信通知" />
  357. </el-form-item>
  358. </el-col>
  359. <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8">
  360. <el-form-item
  361. label="是否发起会签"
  362. prop="isStartCountersign"
  363. :rules="[{ required: false, message: '请选择是否发起会签', trigger: 'change' }]"
  364. >
  365. <el-switch v-model="state.restForm.isStartCountersign" inline-prompt active-text="是" inactive-text="否" />
  366. </el-form-item>
  367. </el-col>
  368. <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
  369. <el-form-item label="办理意见" prop="opinion" :rules="[{ required: true, message: '请填写小休办理意见', trigger: 'blur' }]">
  370. <common-advice
  371. @chooseAdvice="chooseAdvice"
  372. v-model="state.restForm.opinion"
  373. placeholder="请填写小休办理意见"
  374. :loading="state.loading"
  375. :commonEnum="commonEnum.RestReason"
  376. />
  377. </el-form-item>
  378. </el-col>
  379. <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
  380. <el-form-item label="附件" prop="remark" :rules="[{ required: false, message: '请填写诉求内容', trigger: 'change' }]">
  381. <annex-list name="小休附件" businessId="" classify="小休上传" />
  382. </el-form-item>
  383. </el-col>
  384. </el-row>
  385. </el-form>
  386. </template>
  387. <!-- 不需要审核 -->
  388. <template v-else>
  389. <el-form :model="state.restForm" label-width="90px" ref="restFormRef">
  390. <el-row :gutter="10">
  391. <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
  392. <el-form-item label="小休" prop="reason" :rules="[{ required: true, message: '请选择小休原因', trigger: 'change' }]">
  393. <el-select v-model="state.restForm.reason" placeholder="请选择小休原因" class="w100" clearable>
  394. <el-option v-for="item in state.restReasonOptions" :key="item.dicDataValue" :label="item.dicDataName" :value="item.dicDataValue" />
  395. </el-select>
  396. </el-form-item>
  397. </el-col>
  398. </el-row>
  399. </el-form>
  400. </template>
  401. <template #footer>
  402. <span class="dialog-footer">
  403. <el-button @click="state.restDialogVisible = false" class="default-button" :loading="state.loading">取 消</el-button>
  404. <el-button type="primary" @click="clickOnRest(restFormRef)" :loading="state.loading">提 交</el-button>
  405. </span>
  406. </template>
  407. </el-dialog>
  408. <!-- 转接弹窗 -->
  409. <el-dialog v-model="state.transferDialogVisible" draggable title="转接" width="500px">
  410. <el-form :model="state.transferForm" label-width="100px" ref="transferFormRef">
  411. <el-form-item label="转接号码" prop="telNo" :rules="[{ required: true, message: '请选择转接分机或输入外部电话', trigger: 'blur' }]">
  412. <el-select-v2
  413. v-model="state.transferForm.telNo"
  414. :options="state.telsList"
  415. placeholder="请选择转接分机或输入外部电话"
  416. filterable
  417. class="w100"
  418. allow-create
  419. default-first-option
  420. :props="{
  421. label: 'telNo',
  422. value: 'telNo',
  423. }"
  424. />
  425. </el-form-item>
  426. </el-form>
  427. <template #footer>
  428. <span class="dialog-footer">
  429. <el-button @click="state.transferDialogVisible = false" class="default-button" :loading="state.loading">取 消</el-button>
  430. <el-button type="primary" @click="clickOnTransfer(transferFormRef)" :loading="state.loading">确 定</el-button>
  431. </span>
  432. </template>
  433. </el-dialog>
  434. <!-- 呼叫弹窗 -->
  435. <el-dialog v-model="state.outboundDialogVisible" draggable title="呼叫" width="450px">
  436. <el-form :model="state.outboundForm" label-width="80px" ref="outboundFormRef">
  437. <el-form-item label="呼叫号码" prop="telNo" :rules="[{ required: true, message: '请选择或输入呼叫号码', trigger: 'blur' }]">
  438. <el-select-v2
  439. v-model="state.outboundForm.telNo"
  440. :options="state.telsList"
  441. placeholder="请选择或输入呼叫号码"
  442. filterable
  443. class="w100"
  444. allow-create
  445. default-first-option
  446. :props="{
  447. label: 'telNo',
  448. value: 'telNo',
  449. }"
  450. />
  451. </el-form-item>
  452. </el-form>
  453. <template #footer>
  454. <span class="dialog-footer">
  455. <el-button @click="state.outboundDialogVisible = false" class="default-button" :loading="state.loading">取 消</el-button>
  456. <el-button type="primary" @click="clickOnOutbound(outboundFormRef)" :loading="state.loading">确 定</el-button>
  457. </span>
  458. </template>
  459. </el-dialog>
  460. <!-- 三方通话弹窗 -->
  461. <el-dialog v-model="state.threeWayDialogVisible" draggable title="三方会议" width="450px">
  462. <el-form :model="state.threeWayForm" label-width="120px" ref="threeWayFormRef">
  463. <el-form-item label="三方通话号码" prop="telNo" :rules="[{ required: true, message: '请选择或输入三方通话号码', trigger: 'blur' }]">
  464. <el-select-v2
  465. v-model="state.threeWayForm.telNo"
  466. :options="state.telsList"
  467. placeholder="请选择或输入三方通话号码"
  468. filterable
  469. class="w100"
  470. allow-create
  471. default-first-option
  472. :props="{
  473. label: 'telNo',
  474. value: 'telNo',
  475. }"
  476. />
  477. </el-form-item>
  478. </el-form>
  479. <template #footer>
  480. <span class="dialog-footer">
  481. <el-button @click="state.threeWayDialogVisible = false" class="default-button" :loading="state.loading">取 消</el-button>
  482. <el-button type="primary" @click="clickOnThreeWay(threeWayFormRef)" :loading="state.loading">确 定</el-button>
  483. </span>
  484. </template>
  485. </el-dialog>
  486. </template>
  487. <script setup lang="ts" name="telControl">
  488. import { reactive, ref, computed, defineAsyncComponent, onMounted, onBeforeUnmount } from 'vue';
  489. import { ElMessageBox, ElNotification, ElMessage, FormInstance } from 'element-plus';
  490. import { storeToRefs } from 'pinia';
  491. import { useTelStatus, TelStates, RestStates } from '@/stores/telStatus';
  492. import { useUserInfo } from '@/stores/userInfo';
  493. import { useAppConfig } from '@/stores/appConfig';
  494. import { getImageUrl } from '@/utils/tools';
  495. import { formatDuration } from '@/utils/formatTime';
  496. import { commonEnum } from '@/utils/constants';
  497. import other from '@/utils/other';
  498. import { workflowStepOptions } from '@/api/system/workflow';
  499. import {
  500. restFlowStart,
  501. restFlowDel,
  502. restFlowStartWex,
  503. getTelList,
  504. telRestBaseData,
  505. dutyOff,
  506. dutyOn,
  507. busyOff,
  508. busyOn,
  509. queryBlacklist,
  510. } from '@/api/public/wex';
  511. import signalR from '@/utils/signalR';
  512. import { Local } from '@/utils/storage';
  513. import { ola } from '@/utils/ola_api';
  514. import { useRouter } from 'vue-router';
  515. import { useSocket } from '@/utils/websocket';
  516. import mittBus from '@/utils/mitt';
  517. import { voiceAssistant } from '@/api/todo/voiceAssistant';
  518. import { submitLog } from '@/api/public/log';
  519. // 引入组件
  520. const CommonAdvice = defineAsyncComponent(() => import('@/components/CommonAdvice/index.vue')); // 常用意见
  521. const AnnexList = defineAsyncComponent(() => import('@/components/AnnexList/index.vue'));
  522. const state = reactive<any>({
  523. active: <EmptyArrayType>[], // 当前选中
  524. currentStatus: '', //当前通话状态
  525. dutyDialogVisible: false, //签入选分机弹窗
  526. dutyForm: {
  527. //签入选分机表单
  528. telNo: null, //分机号
  529. },
  530. telsList: <EmptyArrayType>[], // 分机列表
  531. loading: false,
  532. showHangupList: false, //是否展示挂断列表
  533. restDialogVisible: false, //小休弹窗
  534. restForm: {
  535. //小休表单
  536. opinion: '', //小休原因
  537. nextStepCode: '', //下一步
  538. nextHandlers: [], //下一步处理人
  539. acceptSms: false, //是否接收短信
  540. nextMainHandler: '', //下一步主办人
  541. reason: '', //小休原因
  542. expiredTime: '', //期满时间
  543. isStartCountersign: false, //是否发起会签
  544. },
  545. handlerMainOptions: [], // 主办
  546. transferDialogVisible: false, // 转接弹窗
  547. transferForm: {
  548. //转接表单
  549. telNo: null,
  550. },
  551. outboundDialogVisible: false, //外呼弹窗
  552. outboundForm: {
  553. // 外呼表单
  554. telNo: null, //外呼号码
  555. },
  556. dutyOnSrc: getImageUrl('phoneControls/dutyOn_blue.png'), //签入图片
  557. dutyOffSrc: getImageUrl('phoneControls/dutyOff_blue.png'), //签出图片
  558. hangupSrc: getImageUrl('phoneControls/hangup_blue.png'), //挂断图片
  559. restSrc: getImageUrl('phoneControls/rest_blue.png'), //小休图片
  560. holdSrc: getImageUrl('phoneControls/hold_blue.png'), //保持图片
  561. talkingDealSrc: getImageUrl('phoneControls/talkingDeal_blue.png'), //话后整理图片
  562. transferSrc: getImageUrl('phoneControls/transfer_blue.png'), //转接图片
  563. conferenceSrc: getImageUrl('phoneControls/conference_blue.png'), //三方会议图片
  564. outboundSrc: getImageUrl('phoneControls/outbound_blue.png'), //外呼图片
  565. callOutSrc: getImageUrl('phoneControls/evaluate_blue.png'), // 外呼模式图片
  566. restReasonOptions: [], // 小休原因
  567. nextStepOptions: [], // 下一个环节
  568. handlerOptions: [], // 处理人
  569. handleId: '', // 流程处理ID
  570. transform: 'translate(0px, 0px)',
  571. fileList: [], // 文件上传列表
  572. threeWayDialogVisible: false, // 三方会议弹窗
  573. threeWayForm: {
  574. // 三方会议表单
  575. telNo: null, // 三方会议号码
  576. },
  577. whiteList: [], // 白名单
  578. });
  579. const useTelStatusStore = useTelStatus();
  580. const { telStatusInfo } = storeToRefs(useTelStatusStore); // 电话状态
  581. const appConfigStore = useAppConfig();
  582. const { AppConfigInfo } = storeToRefs(appConfigStore); // 系统配置信息
  583. const storesUserInfo = useUserInfo();
  584. const { userInfos } = storeToRefs(storesUserInfo); // 用户信息
  585. const talkTime = ref<any>(0); // 通话时长
  586. const talkTimer = ref<any>(null); // 通话时长定时器
  587. // 开始通话计时
  588. const startTime = () => {
  589. let localTalkTime = Local.get('talkTime');
  590. if (talkTime.value) {
  591. talkTime.value = Number(localTalkTime);
  592. talkTimer.value = setInterval(() => {
  593. talkTime.value++;
  594. Local.set('talkTime', String(talkTime.value));
  595. }, 1000);
  596. } else {
  597. talkTimer.value = setInterval(() => {
  598. talkTime.value++;
  599. Local.set('talkTime', String(talkTime.value));
  600. }, 1000);
  601. }
  602. };
  603. // 结束通话计时
  604. const removeTimer = () => {
  605. talkTime.value = 0;
  606. Local.remove('talkTime');
  607. clearInterval(talkTimer.value);
  608. };
  609. // 开始签入时长
  610. const onDutyTime = ref<any>(0); // 签入时长
  611. const onDutyTimer = ref<any>(null); // 签入时长定时器
  612. const startDutyTimer = (second: any) => {
  613. if (second) {
  614. // 从后台获取签入时长
  615. if (second < 0) second = 0; // 防止后台返回的签入时间大于当前时间
  616. onDutyTime.value = second;
  617. onDutyTimer.value = setInterval(() => {
  618. onDutyTime.value++;
  619. }, 1000);
  620. } else {
  621. onDutyTimer.value = setInterval(() => {
  622. onDutyTime.value++;
  623. }, 1000);
  624. }
  625. };
  626. // 结束签入时长
  627. const removeTimerOnDuty = () => {
  628. onDutyTime.value = 0;
  629. clearInterval(onDutyTimer.value);
  630. };
  631. // 监听消息
  632. const signalRStart = async () => {
  633. mittBus.on('RestApplyPass', (data) => {
  634. // 小休审批通过消息
  635. console.log(data, '小休审批通过消息');
  636. RestApplyPassFn(data);
  637. });
  638. };
  639. // 检查用户状态
  640. // 设置当前可用的按钮
  641. const activeArr = computed(() => {
  642. const switchCases: any = {
  643. dutyOff: ['dutyOn'], // 签出状态
  644. dutyOn: ['dutyOff', 'callOut', 'rest', 'outbound'], // 已签入无通话状态
  645. onCallOut: ['callOut', 'outbound'], // 外呼模式中
  646. rest: ['rest'], // 小休中状态
  647. ring: ['hangup'], //振铃中
  648. onCall: ['hangup', 'hold', 'transfer', 'evaluate', 'conference'], // 单个通话中
  649. onHold: ['hangup', 'hold', 'transfer', 'evaluate'], // 保持中
  650. onTalkingDeal: ['dutyOff', 'rest', 'TalkingDeal'], // 话后整理中
  651. onConference: ['hangup'], // 三方会议中 只能挂断
  652. onThreeWay: ['hangup', 'conference'], // 三方会议呼出中 只能挂断和踢人
  653. };
  654. let arr = <EmptyArrayType>[];
  655. if (telStatusInfo.value.phoneControlState in switchCases) {
  656. arr = switchCases[telStatusInfo.value.phoneControlState];
  657. }
  658. return arr;
  659. });
  660. // 设置当前状态的值
  661. const currentStatusText = computed(() => {
  662. const statusMap: any = {
  663. dutyOff: '签出',
  664. dutyOn: '空闲',
  665. onCallOut: '外呼中',
  666. rest: '小休中',
  667. ring: '振铃中',
  668. onHold: '保持中',
  669. onCall: '通话中',
  670. onTalkingDeal: '整理中',
  671. onConference: '会议中',
  672. onThreeWay: '会议中',
  673. };
  674. return statusMap[telStatusInfo.value.phoneControlState] || '';
  675. });
  676. // 小休审批通过消息
  677. const RestApplyPassFn = (data: any) => {
  678. ElNotification({
  679. title: '成功',
  680. message: '小休审批通过,开始小休',
  681. type: 'success',
  682. });
  683. //不需要审核直接开始小休
  684. ola.go_break(state.restForm.reason); //设置忙碌
  685. console.log('调用示忙审核');
  686. };
  687. // 查询所有分机
  688. const getTelsLists = async () => {
  689. state.loading = true;
  690. try {
  691. const res: any = await getTelList();
  692. state.telsList = res?.result ?? [];
  693. state.loading = false;
  694. return state.telsList;
  695. } catch (err) {
  696. console.log(err);
  697. state.loading = false;
  698. }
  699. };
  700. // 鼠标移入移出改变图标
  701. const onHover = (val: string, path: string) => {
  702. state[val] = getImageUrl(path);
  703. };
  704. // 点击事件
  705. const onControlClick = (val: string) => {
  706. switch (val) {
  707. case 'dutyOn': //签入
  708. onDutyFn();
  709. break;
  710. case 'dutyOff': //签出
  711. offDutyFn();
  712. break;
  713. case 'unCallOut': //取消外呼模式
  714. onUnCallOut();
  715. break;
  716. case 'callOut': //外呼模式
  717. onCallOut();
  718. break;
  719. case 'hangup': //挂断
  720. onHangup();
  721. break;
  722. case 'rest': //小休
  723. onRest();
  724. break;
  725. case 'restEnd': //结束小休
  726. onRestEnd();
  727. break;
  728. case 'hold': //保持
  729. onHold();
  730. break;
  731. case 'unHold': //取消保持
  732. onUnHold();
  733. break;
  734. case 'TalkingDeal': //话后整理
  735. onTalkingDeal();
  736. break;
  737. case 'unTalkingDeal': // 取消话后整理
  738. unTalkingDeal();
  739. break;
  740. case 'transfer': //转接
  741. onTransfer();
  742. break;
  743. case 'conference': //三方会议
  744. onConference();
  745. break;
  746. case 'outbound': //外呼
  747. onOutbound();
  748. break;
  749. default:
  750. break;
  751. }
  752. };
  753. // 链接呼叫中心
  754. const websocket_connect = () => {
  755. console.log('链接呼叫中心');
  756. if (ola.ws) {
  757. // 如果已经连接 则先关闭
  758. ola.close();
  759. }
  760. ola.onConnect = onConnect;
  761. ola.onClose = onClose;
  762. ola.onMessage = onMessage;
  763. ola.connect(import.meta.env.VITE_CALLCENTER_SOCKET_URL, currentTel.value.telNo, currentTel.value.password);
  764. };
  765. // 呼叫中心链接
  766. const onConnect = () => {
  767. ola.subscribe('ola.agent.' + currentTel.value.telNo);
  768. ola.subscribe('ola.caller.' + currentTel.value.telNo);
  769. ola.get_agent_state(currentTel.value.telNo);
  770. pingTimer.value = setInterval(() => {
  771. ola.ping();
  772. }, 5000);
  773. // ola.logout(currentTel.value.telNo); //连接之后,先登出一次,防止其他地方已经登陆
  774. let array_ola_queue: EmptyArrayType = []; // 队列
  775. if (currentTel.value.queue) {
  776. let array = currentTel.value.queue.split(',');
  777. for (let i = 0; i < array.length; i++) {
  778. array_ola_queue[i] = array[i];
  779. }
  780. ola.login(array_ola_queue, currentTel.value.telNo, { type: 'onhook' });
  781. connectVoiceAssistant(currentTel.value.telNo); // 坐席助手开启
  782. }
  783. Local.set('telNo', currentTel.value.telNo);
  784. };
  785. // 业务系统发送消息
  786. const sendMsg = (msg: any) => {
  787. signalR.SR.invoke('SendSeatState', { telNo: telStatusInfo.value.telsNo, state: msg });
  788. };
  789. const router = useRouter();
  790. const talkDealTimer = ref<any>(null); // 话后整理定时器
  791. const pingTimer = ref<any>(null); // 心跳定时器
  792. const call_direction = ref<any>(''); // 呼叫方向
  793. // 呼叫中心消息
  794. const onMessage = async (event: any) => {
  795. const data = JSON.parse(event.data);
  796. if (data.event_type == 'agent_state') {
  797. if (data.agent_extn) {
  798. // 设置签入状态
  799. useTelStatusStore.setDutyState(true);
  800. // 设置分机号和坐席组
  801. useTelStatusStore.setCallInfo({ telsNo: data.agent_extn });
  802. }
  803. // 坐席状态
  804. if (data.state == 'login') {
  805. // 签入
  806. // 设置分机号和坐席组
  807. useTelStatusStore.setCallInfo({ telsNo: currentTel.value.telNo });
  808. state.loading = true;
  809. setTimeout(() => {
  810. console.log('isRest', `当前分机是否正在休息${isRest.value}`);
  811. if (isRest.value) {
  812. // 如果是小休状态
  813. ola.go_break(''); //设置忙碌
  814. } else {
  815. // 设置示闲状态
  816. ola.go_ready();
  817. console.log('呼叫中心:调用示闲');
  818. }
  819. }, 1000);
  820. console.log('呼叫中心:已签入');
  821. ElMessage.success('签入成功');
  822. sendMsg('login');
  823. } else if (data.state == 'logout') {
  824. // 签出
  825. // 重置所有状态
  826. useTelStatusStore.resetState();
  827. console.log('呼叫中心:已签出');
  828. ElMessage.success('签出成功');
  829. clearInterval(pingTimer.value); // 清除心跳定时器
  830. isReconnect.value = false; // 不需要重连
  831. seatAssistOff();
  832. } else if (data.state == 'ready') {
  833. // 结束计时
  834. removeTimer();
  835. // 设置分机号和坐席组
  836. useTelStatusStore.setCallInfo({ telsNo: data.agent_extn });
  837. // 设置签入状态
  838. useTelStatusStore.setDutyState(true);
  839. // 示闲中
  840. // 设置休息状态 设置未正常状态
  841. useTelStatusStore.setRest(RestStates.unRest);
  842. // 设置话机状态 结束休息改为签入状态
  843. useTelStatusStore.setPhoneControlState(TelStates.dutyOn);
  844. state.loading = false;
  845. // ElMessage.success('示闲中');
  846. console.log('呼叫中心:示闲中');
  847. sendMsg('ready');
  848. } else if (data.state == 'unready') {
  849. break_reason(data.private_data);
  850. sendMsg('unready'); // 发送消息 业务系统消息通知
  851. console.log('呼叫中心:示忙中,小休开始');
  852. // 示忙中
  853. useTelStatusStore.setPhoneControlState(TelStates.rest);
  854. useTelStatusStore.setRest(RestStates.resting);
  855. /*if (AppConfigInfo.value.isRestApproval) {
  856. // 如果小休需要审核
  857. telRestProcess()
  858. .then((res: any) => {
  859. console.log('小休申请成功', res);
  860. // 设置电话状态小休中
  861. useTelStatusStore.setPhoneControlState(TelStates.rest);
  862. useTelStatusStore.setRest(RestStates.resting);
  863. ElMessage.success('小休开始');
  864. })
  865. .catch((err: any) => {
  866. console.log('小休申请失败', err);
  867. restFlowDel().then(() => {
  868. // 删除小休流程
  869. });
  870. ola.go_ready();// 示闲
  871. });
  872. } else {
  873. // ElMessage.success('小休开始');
  874. // 设置电话状态小休中
  875. useTelStatusStore.setPhoneControlState(TelStates.rest);
  876. useTelStatusStore.setRest(RestStates.resting);
  877. // 添加小休记录
  878. telRestAdd({ reason: data.private_data })
  879. .then((res: any) => {
  880. console.log('小休记录添加成功 开始休息', res);
  881. ElMessage.success('小休开始');
  882. })
  883. .catch((err: any) => {
  884. console.log('小休记录添加失败 开始休息', err);
  885. });
  886. }*/
  887. } else if (data.state == 'acw') {
  888. console.log(call_direction.value, '呼入还是呼出');
  889. if (call_direction.value === 'inbound') {
  890. // 呼入需要进入话后整理
  891. // 设置分机号和坐席组
  892. useTelStatusStore.setCallInfo({ telsNo: data.agent_extn });
  893. // 话后整理中
  894. const time: number = AppConfigInfo.value.talkingDealTime * 1000; // 话后整理时间
  895. ElNotification({
  896. title: '自动开启话后整理成功',
  897. message: `${time / 1000}秒后自动结束话后整理,或者手动结束话后整理`,
  898. type: 'success',
  899. duration: 1000 * 10,
  900. });
  901. // 设置话后整理
  902. useTelStatusStore.setTalkingDeal(true);
  903. // 设置话机状态 设置为话后整理中
  904. useTelStatusStore.setPhoneControlState(TelStates.onTalkingDeal);
  905. talkDealTimer.value = setTimeout(() => {
  906. // 设置话后整理
  907. useTelStatusStore.setTalkingDeal(false);
  908. // 设置话机状态 取消话后整理修改为空闲状态
  909. useTelStatusStore.setPhoneControlState(TelStates.dutyOn);
  910. ola.go_ready(); // 示闲
  911. console.log('呼叫中心:调用示闲');
  912. clearTimeout(talkDealTimer.value); // 清除话后整理定时器
  913. }, time);
  914. console.log('呼叫中心:话后整理中');
  915. sendMsg('acw');
  916. } else {
  917. // 呼出直接调用示闲
  918. ola.go_ready(); // 示闲
  919. console.log('呼叫中心:调用示闲');
  920. }
  921. } else if (data.state == 'busy') {
  922. console.log(data.state, '其他状态');
  923. /* // 设置振铃中
  924. useTelStatusStore.setPhoneControlState(TelStates.ring);
  925. sendMsg('busy');
  926. console.log('呼叫中心:转接中....');*/
  927. } else {
  928. console.log(data.state, '其他状态1');
  929. }
  930. if (data.state == 'busy') {
  931. call_direction.value = data.call_direction; // 保存呼叫方向
  932. holdStatus(data.private_data); //处理保持
  933. if (data.private_data == 'monitoring') {
  934. // 三方来电振铃中
  935. useTelStatusStore.setPhoneControlState(TelStates.ring);
  936. console.log('呼叫中心:三方来电振铃中');
  937. } else if (data.private_data == 'three_way') {
  938. // 三方来电通话中
  939. // 开始计时
  940. startTime();
  941. // 设置电话状态 三方通话中
  942. useTelStatusStore.setPhoneControlState(TelStates.onConference);
  943. console.log('呼叫中心:三方来电通话中');
  944. sendMsg('busy');
  945. } else if (data.private_data == 'three_way_hangup') {
  946. // 三方来电挂断
  947. // 设置电话状态 通话中
  948. useTelStatusStore.setPhoneControlState(TelStates.onCall);
  949. onCallArr.value = [];
  950. sendMsg('busy');
  951. console.log('呼叫中心:三方来电挂断');
  952. } else if (data.private_data == 'three_way_ring') {
  953. // 三方通话呼出中
  954. // 设置振铃中
  955. useTelStatusStore.setPhoneControlState(TelStates.ring);
  956. sendMsg('busy');
  957. console.log('呼叫中心:三方通话呼出中');
  958. } else if (data.private_data == 'three_way_answered') {
  959. // 三方通话呼出接通
  960. onCallArr.value.push({ telNo: state.threeWayForm.telNo }); // 三方通话呼出接通
  961. // 设置电话状态 呼出三方通话中 可以踢人和挂断通话
  962. useTelStatusStore.setPhoneControlState(TelStates.onThreeWay);
  963. console.log('呼叫中心:三方通话呼出接通,推送一次');
  964. sendMsg('treeWay');
  965. } else if (data.call_direction == 'outbound') {
  966. // 呼出
  967. if (data.private_data == 'calling') {
  968. // 拨号中
  969. // 设置电话状态 振铃中
  970. useTelStatusStore.setPhoneControlState(TelStates.ring);
  971. sendMsg('busy');
  972. console.log('呼叫中心:呼出拨号中');
  973. } else if (data.private_data == 'answered') {
  974. //振铃中
  975. if (data.other_answered == false) {
  976. // 设置电话状态 振铃中
  977. useTelStatusStore.setPhoneControlState(TelStates.ring);
  978. console.log('呼叫中心:呼出振铃中');
  979. } else if (data.other_answered == true) {
  980. // 通话中
  981. // 开始计时
  982. startTime();
  983. // 设置电话状态 通话中
  984. useTelStatusStore.setPhoneControlState(TelStates.onCall);
  985. mittBus.emit('outboundConnect', data); // 外呼接通之后收到的消息
  986. console.log('呼叫中心:呼出通话中');
  987. }
  988. sendMsg('busy');
  989. }
  990. } else {
  991. // 呼入
  992. if (data.private_data == 'ring') {
  993. // 设置电话状态 振铃中
  994. useTelStatusStore.setPhoneControlState(TelStates.ring);
  995. console.log(data, '呼叫中心:来电弹单信息');
  996. // 来电才展示弹屏
  997. // 跳转到录入工单页面
  998. await getWithList(); // 获取白名单列表
  999. const isWhite = state.whiteList.filter((item: any) => item.phone === data.ani);
  1000. if (isWhite) {
  1001. // 如果来电电话在呼入白名单中 需要提示
  1002. ElNotification({
  1003. title: '来电提醒',
  1004. message: '该市民为白名单。',
  1005. type: 'success',
  1006. });
  1007. }
  1008. await router.push({
  1009. name: 'orderAccept',
  1010. state: {
  1011. createBy: 'tel',
  1012. fromTel: data.ani,
  1013. telGuid: data.call_accept,
  1014. transfer: data.gateway,
  1015. telArea: '',
  1016. },
  1017. params: {
  1018. callId: data.call_accept,
  1019. tagsViewName: '工单受理',
  1020. },
  1021. });
  1022. } else if (data.private_data == 'answered') {
  1023. // 开始计时
  1024. startTime();
  1025. // 设置电话状态 通话中
  1026. useTelStatusStore.setPhoneControlState(TelStates.onCall);
  1027. console.log('呼叫中心:呼入通话中');
  1028. }
  1029. sendMsg('busy');
  1030. }
  1031. } else if (data.old_state == 'busy') {
  1032. //挂机后系统可以返回两种状态:acw 话后整理状态 ready 示闲状态,如果不需要acw,可以联系我们后台修改配置,如果需要保留,如果需要再次
  1033. //拨打电话的话,需要手动点击示闲按钮
  1034. // 设置分机号和坐席组
  1035. useTelStatusStore.setCallInfo({ telsNo: data.agent_extn });
  1036. // 结束计时
  1037. removeTimer();
  1038. onCallArr.value = [];
  1039. console.log('呼叫中心:已挂机', onCallArr.value, data);
  1040. sendMsg('ready');
  1041. }
  1042. } else if (data.event_type == 'agent_caller_state') {
  1043. //通话状态
  1044. // special feature, never mind
  1045. if (data.action == 'in') {
  1046. console.log('呼叫中心:呼入', data.caller.cid_number, data.caller.iuud);
  1047. // ola.take_call(data.caller.uuid);
  1048. } else {
  1049. console.log('呼叫中心:呼出', data.caller.uuid);
  1050. }
  1051. } else if (data.event_type == 'command/reply') {
  1052. // 其他消息
  1053. // console.log('command/reply', data);
  1054. }
  1055. };
  1056. // 记录日志
  1057. const submitLogFn = async (event: any) => {
  1058. const telsNo = Local.get('telNo');
  1059. const name: string = `分机号:${telsNo}的websocket断开链接`;
  1060. const remark: string = `websocket 断开: 错误code:${event.code}, 错误原因:${event.reason}, 是否正常断开:${event.wasClean}`;
  1061. console.log(name, remark, event);
  1062. const request = {
  1063. creationTime: new Date(),
  1064. name,
  1065. remark,
  1066. executeUrl: import.meta.env.VITE_CALLCENTER_SOCKET_URL,
  1067. };
  1068. try {
  1069. await submitLog(request);
  1070. Local.remove('telNo');
  1071. } catch (error) {
  1072. console.log(error);
  1073. }
  1074. };
  1075. // 呼叫中心链接关闭
  1076. const isReconnect = ref(true); // 呼叫中心是否需要重连
  1077. const onClose = async (event: any) => {
  1078. removeTimerOnDuty(); // 移除签入时长定时器
  1079. removeTimer(); // 移除通话计时器
  1080. clearTimeout(talkDealTimer.value); // 清除话后整理定时器
  1081. clearInterval(pingTimer.value); // 清除心跳定时器
  1082. clearInterval(onDutyTimer.value); // 清除签入时长定时器
  1083. clearInterval(talkTimer.value); // 清除通话时长定时器
  1084. console.log('呼叫中心断开链接', isReconnect.value ? '需要重连' : '不需要重连');
  1085. if (isReconnect.value) {
  1086. await reConnect(); // 重新链接呼叫中心
  1087. Local.set('currentTelNo', currentTel.value.telNo);
  1088. }
  1089. // 重置所有状态
  1090. useTelStatusStore.resetState();
  1091. await submitLogFn(event);
  1092. };
  1093. // 重新链接呼叫中心
  1094. let reconnectAttempts = 0; // 重连次数
  1095. let maxReconnectAttempts = 99; // 最大重连次数
  1096. let reconnectInterval = 2; // 重连间隔
  1097. const reConnect = async () => {
  1098. ElNotification({
  1099. title: '重连提示',
  1100. message: `检测到与呼叫中心链接断开,${reconnectInterval}秒后将重新链接`,
  1101. type: 'warning',
  1102. duration: reconnectInterval * 1000,
  1103. });
  1104. console.log('开始重连', `已重连${reconnectAttempts}次,最大重连次数${maxReconnectAttempts}次,重连间隔${reconnectInterval}秒`);
  1105. if (reconnectAttempts < maxReconnectAttempts) {
  1106. setTimeout(() => {
  1107. reconnectAttempts++;
  1108. websocket_connect();
  1109. }, reconnectInterval * 1000);
  1110. } else {
  1111. ElNotification({
  1112. title: '呼叫中心重连失败',
  1113. message: '已到达重连次数最高,请手动刷新重连',
  1114. type: 'warning',
  1115. });
  1116. console.error('已到达重连次数最高,请手动刷新重连');
  1117. }
  1118. };
  1119. // 小休原因
  1120. const restReason = ref(''); // 小休原因
  1121. const break_reason = (reason: string) => {
  1122. switch (reason) {
  1123. case 'away':
  1124. restReason.value = '外出中';
  1125. break;
  1126. case 'rest':
  1127. restReason.value = '休息中';
  1128. break;
  1129. case 'conference':
  1130. restReason.value = '会议中';
  1131. break;
  1132. case 'train':
  1133. restReason.value = '培训中';
  1134. break;
  1135. case 'eat':
  1136. restReason.value = '用餐中';
  1137. break;
  1138. default:
  1139. restReason.value = '休息中';
  1140. break;
  1141. }
  1142. if (telStatusInfo.value.isRest !== 'resting' && !isRest.value) {
  1143. // 如果不在在小休中
  1144. const restReasons = state.restReasonOptions.find((item: any) => item.dicDataValue === reason);
  1145. busyOn({ reason: restReasons?.dicDataName ?? '' }) // 开始小休 设置示忙 业务系统统计需要
  1146. .then(() => {
  1147. console.log('业务系统调用示忙成功');
  1148. state.loading = false;
  1149. })
  1150. .catch((err) => {
  1151. console.log('业务系统调用示忙失败', err);
  1152. state.loading = false;
  1153. });
  1154. } else {
  1155. state.loading = false;
  1156. }
  1157. };
  1158. // 保持状态处理
  1159. const holdStatus = (holdStatus: string) => {
  1160. switch (holdStatus) {
  1161. case 'held':
  1162. // 设置电话状态
  1163. useTelStatusStore.setHold(true);
  1164. // 设置电话状态 保持中
  1165. useTelStatusStore.setPhoneControlState(TelStates.onHold);
  1166. sendMsg('held');
  1167. break;
  1168. case 'unheld':
  1169. // 设置电话状态 通话中
  1170. // 设置电话状态 取消单个保持为通话中
  1171. useTelStatusStore.setHold(false);
  1172. // 设置电话状态
  1173. useTelStatusStore.setPhoneControlState(TelStates.onCall);
  1174. break;
  1175. default:
  1176. break;
  1177. }
  1178. };
  1179. const dutyFormRef = ref<RefType>();
  1180. const currentTel = ref<any>({}); // 当前分机
  1181. const isRest = ref<boolean>(false); // 是否小休
  1182. //签入
  1183. const onDutyFn = async () => {
  1184. if (AppConfigInfo.value.isNeedTelNo || AppConfigInfo.value.isTelNeedVerify) {
  1185. // 需要选择分机号或者输入密码 打开弹窗选择分机号
  1186. dutyFormRef.value?.resetFields();
  1187. state.dutyDialogVisible = true;
  1188. } else {
  1189. ElMessageBox.confirm(`确定要签入,是否继续?`, '提示', {
  1190. confirmButtonText: '确认',
  1191. cancelButtonText: '取消',
  1192. type: 'warning',
  1193. draggable: true,
  1194. cancelButtonClass: 'default-button',
  1195. autofocus: false,
  1196. })
  1197. .then(() => {
  1198. state.loading = true;
  1199. dutyOn({ telNo: userInfos.value.defaultTelNo })
  1200. .then((res: any) => {
  1201. currentTel.value.password = res.result.telPwd;
  1202. currentTel.value.telNo = res.result.telNo;
  1203. currentTel.value.queue = res.result.queueId;
  1204. // 不需要选择分机号和密码 直接签入 传入默认分机号
  1205. websocket_connect(); //开启消息监听
  1206. startDutyTimer(res.result.second); // 开启计时 签入时长
  1207. isRest.value = res.result.isRest;
  1208. state.loading = false;
  1209. })
  1210. .catch(() => {})
  1211. .finally(() => {
  1212. state.loading = false;
  1213. });
  1214. })
  1215. .catch(() => {
  1216. state.loading = false;
  1217. });
  1218. }
  1219. };
  1220. // 确认签入
  1221. const clickOnDuty = (formEl: FormInstance | undefined) => {
  1222. if (!formEl) return;
  1223. formEl.validate((valid: boolean) => {
  1224. if (!valid) return;
  1225. state.loading = true;
  1226. let request = {};
  1227. if (AppConfigInfo.value.isNeedTelNo && AppConfigInfo.value.isTelNeedVerify) {
  1228. // 需要分机和密码
  1229. request = {
  1230. telNo: state.dutyForm.telNo,
  1231. telPwd: state.dutyForm.password,
  1232. };
  1233. } else if (AppConfigInfo.value.isNeedTelNo) {
  1234. //需要分机号
  1235. request = {
  1236. telNo: state.dutyForm.telNo,
  1237. };
  1238. } else if (AppConfigInfo.value.isTelNeedVerify) {
  1239. // 需要密码
  1240. request = {
  1241. telNo: state.dutyForm.telNo,
  1242. telPwd: userInfos.value.defaultTelNo,
  1243. };
  1244. }
  1245. dutyOn(request)
  1246. .then((res: any) => {
  1247. if (AppConfigInfo.value.isNeedTelNo && AppConfigInfo.value.isTelNeedVerify) {
  1248. // 需要分机和密码
  1249. currentTel.value.password = state.dutyForm.password;
  1250. currentTel.value.telNo = state.dutyForm.telNo;
  1251. currentTel.value.queue = res.result.queueId;
  1252. } else if (AppConfigInfo.value.isNeedTelNo) {
  1253. //需要分机号
  1254. currentTel.value.password = res.result.telPwd;
  1255. currentTel.value.telNo = res.result.telNo;
  1256. currentTel.value.queue = res.result.queueId;
  1257. } else if (AppConfigInfo.value.isTelNeedVerify) {
  1258. // 需要密码
  1259. currentTel.value.password = state.dutyForm.password;
  1260. currentTel.value.telNo = res.result.telNo;
  1261. currentTel.value.queue = res.result.queueId;
  1262. }
  1263. websocket_connect(); //开启消息监听
  1264. startDutyTimer(res.result.second); // 开启计时 签入时长
  1265. isRest.value = res.result.isRest;
  1266. state.loading = false;
  1267. state.dutyDialogVisible = false;
  1268. })
  1269. .catch(() => {
  1270. // dutyOff();
  1271. // 重置所有状态
  1272. useTelStatusStore.resetState();
  1273. console.log('呼叫中心:签入错误111');
  1274. })
  1275. .finally(() => {
  1276. state.loading = false;
  1277. state.dutyDialogVisible = false;
  1278. });
  1279. });
  1280. };
  1281. // 签出
  1282. const offDutyFn = () => {
  1283. ElMessageBox.confirm(`确定要签出,是否继续?`, '提示', {
  1284. confirmButtonText: '确认',
  1285. cancelButtonText: '取消',
  1286. type: 'warning',
  1287. draggable: true,
  1288. cancelButtonClass: 'default-button',
  1289. autofocus: false,
  1290. })
  1291. .then(() => {
  1292. state.loading = true;
  1293. dutyOff()
  1294. .then(() => {
  1295. console.log('业务系统:签出成功');
  1296. Local.set('isReconnect1', false);
  1297. sendMsg('logout');
  1298. ola.logout(currentTel.value.telNo); //签出
  1299. setTimeout(() => {
  1300. ola.close();
  1301. }, 500);
  1302. // 重置所有状态
  1303. useTelStatusStore.resetState();
  1304. removeTimerOnDuty(); // 移除签入时长定时器
  1305. removeTimer(); // 移除通话计时器
  1306. clearTimeout(talkDealTimer.value); // 清除话后整理定时器
  1307. clearInterval(pingTimer.value); // 清除心跳定时器
  1308. clearInterval(onDutyTimer.value); // 清除签入时长定时器
  1309. clearInterval(talkTimer.value); // 清除通话时长定时器
  1310. state.dutyOnSrc = getImageUrl('phoneControls/dutyOn_blue.png'); //签入图片
  1311. state.loading = false;
  1312. })
  1313. .catch(() => {})
  1314. .finally(() => {
  1315. state.loading = false;
  1316. });
  1317. })
  1318. .catch(() => {
  1319. state.loading = false;
  1320. });
  1321. };
  1322. // 开启外呼模式
  1323. const onCallOut = () => {
  1324. ElMessageBox.confirm(`确定要开启外呼模式,是否继续?`, '提示', {
  1325. confirmButtonText: '确认',
  1326. cancelButtonText: '取消',
  1327. type: 'warning',
  1328. draggable: true,
  1329. cancelButtonClass: 'default-button',
  1330. autofocus: false,
  1331. })
  1332. .then(() => {
  1333. // state.loading = true;
  1334. // 设置电话状态 外呼模式中
  1335. useTelStatusStore.setCallOut(true);
  1336. // 设置电话状态 保持中
  1337. useTelStatusStore.setPhoneControlState(TelStates.onCallOut);
  1338. sendMsg('held');
  1339. })
  1340. .catch(() => {});
  1341. };
  1342. // 关闭外呼模式
  1343. const onUnCallOut = () => {
  1344. ElMessageBox.confirm(`确定要关闭外呼模式,是否继续?`, '提示', {
  1345. confirmButtonText: '确认',
  1346. cancelButtonText: '取消',
  1347. type: 'warning',
  1348. draggable: true,
  1349. cancelButtonClass: 'default-button',
  1350. autofocus: false,
  1351. })
  1352. .then(() => {
  1353. // 设置电话状态 取消外呼模式
  1354. useTelStatusStore.setCallOut(false);
  1355. // 设置电话状态 保持中
  1356. useTelStatusStore.setPhoneControlState(TelStates.dutyOn);
  1357. // state.loading = true;
  1358. })
  1359. .catch(() => {});
  1360. };
  1361. // 挂断
  1362. const onHangup = () => {
  1363. ElMessageBox.confirm(`确定要挂断,是否继续?`, '提示', {
  1364. confirmButtonText: '确认',
  1365. cancelButtonText: '取消',
  1366. type: 'warning',
  1367. draggable: true,
  1368. cancelButtonClass: 'default-button',
  1369. autofocus: false,
  1370. })
  1371. .then(() => {
  1372. state.loading = true;
  1373. ola.hangup(); //挂断
  1374. state.loading = false;
  1375. })
  1376. .catch(() => {});
  1377. };
  1378. // 小休
  1379. const restFormRef = ref<RefType>(); //小休表单
  1380. const onRest = async () => {
  1381. if (AppConfigInfo.value.isRestApproval) {
  1382. // 如果小休需要审核
  1383. // 查询流程节点参数
  1384. const res: any = await restFlowStart();
  1385. state.nextStepOptions = res.result.steps;
  1386. state.handleId = res.result.id;
  1387. }
  1388. // 重置表单
  1389. restFormRef.value?.resetFields();
  1390. state.restDialogVisible = true;
  1391. };
  1392. // 打开弹窗清空表单
  1393. const restFormOpened = () => {
  1394. restFormRef.value?.resetFields();
  1395. restFormRef.value?.clearValidate();
  1396. };
  1397. // 小休流程选择下一个环节
  1398. const selectNextStep = (val: any) => {
  1399. const next = state.nextStepOptions.find((item: any) => item.key === val);
  1400. getNextStepOption(state.handleId, next.key);
  1401. };
  1402. // 查询流程下一节点参数
  1403. const getNextStepOption = async (DefineId: string, Code: string) => {
  1404. try {
  1405. const res: any = await workflowStepOptions({ DefineId, Code });
  1406. state.handlerOptions = res.result ?? [];
  1407. } catch (error) {
  1408. console.log(error);
  1409. }
  1410. };
  1411. // 小休选择处理人
  1412. const selectHandlers = () => {
  1413. restFormRef.value?.resetFields('nextMainHandler');
  1414. };
  1415. // 主办从处理人中选择
  1416. state.handlerMainOptions = computed(() => {
  1417. return state.restForm.nextHandlers;
  1418. });
  1419. // 确定小休(示忙)
  1420. const clickOnRest = (formEl: FormInstance | undefined) => {
  1421. if (!formEl) return;
  1422. formEl.validate((valid: boolean) => {
  1423. if (!valid) return;
  1424. state.loading = true;
  1425. if (AppConfigInfo.value.isRestApproval) {
  1426. //如果需要审核
  1427. state.restForm.additions = state.fileList;
  1428. let submitObj = other.deepClone(state.restForm);
  1429. if (submitObj.nextHandlers && submitObj.nextHandlers.length) {
  1430. submitObj.nextHandlers = submitObj.nextHandlers.map((item: any) => {
  1431. return {
  1432. id: item.key,
  1433. name: item.value,
  1434. };
  1435. });
  1436. if (submitObj.nextHandlers.length === 1) {
  1437. submitObj.nextMainHandler = submitObj.nextHandlers[0].id;
  1438. }
  1439. }
  1440. restFlowStartWex(submitObj)
  1441. .then(() => {
  1442. ElNotification({
  1443. title: '成功',
  1444. message: '申请小休成功',
  1445. type: 'success',
  1446. });
  1447. // 设置休息状态 审核中
  1448. useTelStatusStore.setRest(RestStates.InReview);
  1449. state.restDialogVisible = false;
  1450. state.loading = false;
  1451. })
  1452. .catch(() => {
  1453. restFlowDel().then(() => {
  1454. state.loading = false;
  1455. state.restDialogVisible = false;
  1456. });
  1457. });
  1458. } else {
  1459. //不需要审核直接开始小休
  1460. ola.go_break(state.restForm.reason); //设置忙碌
  1461. clearTimeout(talkDealTimer.value); // 清除话后整理定时器
  1462. console.log('呼叫中心:调用示忙');
  1463. state.restDialogVisible = false;
  1464. state.loading = false;
  1465. }
  1466. });
  1467. };
  1468. // 设置抽屉
  1469. const dialogRestRef = ref<RefType>(); // 小休申请弹窗
  1470. const mouseup = () => {
  1471. state.transform = dialogRestRef.value.dialogContentRef.$el.style.transform;
  1472. };
  1473. // 选择常用意见 填入填写框
  1474. const chooseAdvice = (item: any) => {
  1475. state.restForm.opinion += item.content;
  1476. };
  1477. // 小休结束
  1478. const onRestEnd = () => {
  1479. ElMessageBox.confirm(`确定要结束小休,是否继续?`, '提示', {
  1480. confirmButtonText: '确认',
  1481. cancelButtonText: '取消',
  1482. type: 'warning',
  1483. draggable: true,
  1484. cancelButtonClass: 'default-button',
  1485. autofocus: false,
  1486. })
  1487. .then(() => {
  1488. state.loading = true;
  1489. ola.go_ready(); // 示闲
  1490. busyOff(); // 结束小休(调用业务系统接口,统计需要)
  1491. console.log('呼叫中心:调用示闲');
  1492. state.loading = false;
  1493. })
  1494. .catch(() => {});
  1495. };
  1496. // 通话保持
  1497. const onHold = () => {
  1498. ElMessageBox.confirm(`确定要保持,是否继续?`, '提示', {
  1499. confirmButtonText: '确认',
  1500. cancelButtonText: '取消',
  1501. type: 'warning',
  1502. draggable: true,
  1503. cancelButtonClass: 'default-button',
  1504. autofocus: false,
  1505. })
  1506. .then(() => {
  1507. state.loading = true;
  1508. ola.hold(); //保持
  1509. state.loading = false;
  1510. })
  1511. .catch(() => {});
  1512. };
  1513. // 取消保持
  1514. const onUnHold = () => {
  1515. ElMessageBox.confirm(`确定要取消保持,是否继续?`, '提示', {
  1516. confirmButtonText: '确认',
  1517. cancelButtonText: '取消',
  1518. type: 'warning',
  1519. draggable: true,
  1520. cancelButtonClass: 'default-button',
  1521. autofocus: false,
  1522. })
  1523. .then(() => {
  1524. state.loading = true;
  1525. ola.unhold(); //取消保持
  1526. sendMsg('busy');
  1527. state.loading = false;
  1528. })
  1529. .catch(() => {});
  1530. };
  1531. // 话后整理(系统默认进入)
  1532. const onTalkingDeal = () => {};
  1533. // 取消话后整理
  1534. const unTalkingDeal = () => {
  1535. ElMessageBox.confirm(`确定要取消话后整理,是否继续?`, '提示', {
  1536. confirmButtonText: '确认',
  1537. cancelButtonText: '取消',
  1538. type: 'warning',
  1539. draggable: true,
  1540. cancelButtonClass: 'default-button',
  1541. autofocus: false,
  1542. })
  1543. .then(() => {
  1544. state.loading = true;
  1545. // 设置话后整理
  1546. useTelStatusStore.setTalkingDeal(false);
  1547. // 设置话机状态 取消话后整理修改为空闲状态
  1548. useTelStatusStore.setPhoneControlState(TelStates.dutyOn);
  1549. ola.go_ready(); // 示闲
  1550. clearTimeout(talkDealTimer.value); // 清除话后整理定时器
  1551. console.log('呼叫中心:调用示闲');
  1552. state.loading = false;
  1553. })
  1554. .catch(() => {});
  1555. };
  1556. // 打开转接弹窗
  1557. const transferFormRef = ref<RefType>(); // 转接表单
  1558. const onTransfer = () => {
  1559. // 重置表单
  1560. transferFormRef.value?.resetFields();
  1561. // 获取所有分机列表
  1562. state.transferDialogVisible = true;
  1563. };
  1564. // 确认转接
  1565. const clickOnTransfer = (formEl: FormInstance | undefined) => {
  1566. if (!formEl) return;
  1567. formEl.validate((valid: boolean) => {
  1568. if (!valid) return;
  1569. ola.transfer(state.transferForm.telNo); //转接
  1570. state.transferDialogVisible = false;
  1571. });
  1572. };
  1573. // 三方会议开始
  1574. const onConference = () => {
  1575. // 重置表单
  1576. transferFormRef.value?.resetFields();
  1577. // 获取所有分机列表
  1578. state.threeWayDialogVisible = true;
  1579. };
  1580. // 三方会议确定
  1581. const onCallArr = ref<EmptyArrayType>([]); // 三方会议选中的分机
  1582. const threeWayVisible = ref(false); // 三方会议弹窗
  1583. const threeWayFormRef = ref<RefType>(); // 三方会议表单
  1584. const clickOnThreeWay = (formEl: FormInstance | undefined) => {
  1585. if (!formEl) return;
  1586. formEl.validate((valid: boolean) => {
  1587. if (!valid) return;
  1588. ola.three_way(telStatusInfo.value.telsNo, state.threeWayForm.telNo); //三方会议
  1589. // 三方通话呼出接通
  1590. onCallArr.value.push({ telNo: state.threeWayForm.telNo }); // 三方通话呼出接通
  1591. // 设置电话状态 呼出三方通话中 可以踢人和挂断通话
  1592. useTelStatusStore.setPhoneControlState(TelStates.onThreeWay);
  1593. sendMsg('treeWay');
  1594. state.threeWayDialogVisible = false;
  1595. });
  1596. };
  1597. const kickOut = (item: any) => {
  1598. ElMessageBox.confirm(`确定确定要踢出${item.telNo},并恢复通话,是否继续?`, '提示', {
  1599. confirmButtonText: '确认',
  1600. cancelButtonText: '取消',
  1601. type: 'warning',
  1602. draggable: true,
  1603. cancelButtonClass: 'default-button',
  1604. autofocus: false,
  1605. })
  1606. .then(() => {
  1607. ola.exit_three_way(item.telNo); // 踢出第三方
  1608. // 恢复通话
  1609. useTelStatusStore.setPhoneControlState(TelStates.onCall);
  1610. onCallArr.value = [];
  1611. })
  1612. .catch(() => {});
  1613. };
  1614. // 外呼
  1615. const outboundFormRef = ref<RefType>(); //外呼
  1616. const onOutbound = () => {
  1617. // 重置表单
  1618. outboundFormRef.value?.resetFields();
  1619. // 获取所有分机列表
  1620. state.outboundDialogVisible = true;
  1621. };
  1622. // 外呼保存
  1623. const clickOnOutbound = (formEl: FormInstance | undefined) => {
  1624. if (!formEl) return;
  1625. formEl.validate((valid: boolean) => {
  1626. if (!valid) return;
  1627. state.loading = true;
  1628. setTimeout(() => {
  1629. ola.dial(state.outboundForm.telNo);
  1630. state.outboundDialogVisible = false;
  1631. state.loading = false;
  1632. }, 300);
  1633. });
  1634. };
  1635. // 坐席辅链接
  1636. const socket = ref<any>(null);
  1637. // 打开websocket链接
  1638. const connectVoiceAssistant = async (telNo: string) => {
  1639. if (!telNo || socket.value?.socket?.ws) {
  1640. socket.value?.close();
  1641. socket.value = null;
  1642. }
  1643. try {
  1644. const { result } = await voiceAssistant(telNo);
  1645. const uid = `8#User-${result.uid}`;
  1646. const subscribe = `/trans/${result.orgCode}/${result.groupUid}/${uid}`;
  1647. socket.value = useSocket(import.meta.env.VITE_VOICE_ASSISTANT_SOCKET_URL, { uid, subscribe });
  1648. socket.value.on('open', () => {
  1649. socket.value.send({ id: '', type: 1, from: uid, to: 'sys', timestamps: new Date().getTime(), body: result.userName });
  1650. console.log('坐席辅助连接成功');
  1651. ElMessage.success('坐席辅助连接成功');
  1652. });
  1653. socket.value.on('message', wsReceive);
  1654. } catch (err) {
  1655. console.log(err, '坐席辅助链接失败');
  1656. ElMessage.error('坐席辅助链接失败');
  1657. }
  1658. };
  1659. // 坐席辅助关闭
  1660. const seatAssistOff = () => {
  1661. console.log('坐席辅助:手动关闭,不再重连');
  1662. if (socket.value) {
  1663. socket.value.close();
  1664. socket.value = null;
  1665. }
  1666. };
  1667. // 推送消息
  1668. const wsReceive = (message: any) => {
  1669. try {
  1670. const data = JSON.parse(message.data);
  1671. const toTransfer = socket.value.socket.opts.subscribe;
  1672. if (data.to === toTransfer) {
  1673. // 判断当前转写消息是否是发送给当前用户的
  1674. mittBus.emit('wsReceive', message);
  1675. }
  1676. } catch (e) {
  1677. return;
  1678. }
  1679. };
  1680. // 刷新页面呼叫中心链接
  1681. const callCenterConnect = async () => {
  1682. const currentTelNo = Local.get('currentTelNo');
  1683. if (currentTelNo && isReconnect.value) {
  1684. // 重新链接
  1685. try {
  1686. const { result } = await dutyOn({ telNo: currentTelNo });
  1687. currentTel.value.password = result.telPwd;
  1688. currentTel.value.telNo = result.telNo;
  1689. currentTel.value.queue = result.queueId;
  1690. Local.remove('currentTelNo');
  1691. startDutyTimer(result.second); // 开启计时 签入时长
  1692. isRest.value = result.isRest;
  1693. state.loading = false;
  1694. } catch (e) {
  1695. console.log(e);
  1696. // await dutyOff();
  1697. // 重置所有状态
  1698. useTelStatusStore.resetState();
  1699. state.loading = false;
  1700. }
  1701. } else if (telStatusInfo.value.telsNo) {
  1702. // 刷新页面
  1703. try {
  1704. const { result } = await dutyOn({ telNo: telStatusInfo.value.telsNo });
  1705. currentTel.value.password = result.telPwd;
  1706. currentTel.value.telNo = result.telNo;
  1707. currentTel.value.queue = result.queueId;
  1708. websocket_connect(); //开启消息监听
  1709. startDutyTimer(result.second); // 开启计时 签入时长
  1710. isRest.value = result.isRest;
  1711. state.loading = false;
  1712. } catch (e) {
  1713. console.log(e);
  1714. // await dutyOff();
  1715. // 重置所有状态
  1716. useTelStatusStore.resetState();
  1717. state.loading = false;
  1718. }
  1719. } else {
  1720. // 重置所有状态
  1721. useTelStatusStore.resetState();
  1722. }
  1723. };
  1724. // 获取小休原因
  1725. const getReason = async () => {
  1726. try {
  1727. // 查询小休原因
  1728. const response: any = await telRestBaseData();
  1729. state.restReasonOptions = response.result?.restReason ?? [];
  1730. } catch (err) {
  1731. console.log(err);
  1732. }
  1733. };
  1734. // 获取白名单列表
  1735. const getWithList = async () => {
  1736. try {
  1737. const response: any = await queryBlacklist({ SpecialFlag: 1 });
  1738. state.whiteList = response.result;
  1739. } catch (err) {
  1740. console.log(err);
  1741. }
  1742. };
  1743. onMounted(async () => {
  1744. await getReason(); // 获取小休原因
  1745. await signalRStart(); //开启消息监听
  1746. removeTimerOnDuty(); // 移除签入时长定时器
  1747. removeTimer(); // 移除通话计时器
  1748. clearTimeout(talkDealTimer.value); // 清除话后整理定时器
  1749. clearInterval(pingTimer.value); // 清除心跳定时器
  1750. clearInterval(onDutyTimer.value); // 清除签入时长定时器
  1751. clearInterval(talkTimer.value); // 清除通话时长定时器
  1752. await getTelsLists(); // 查询所有分机
  1753. await callCenterConnect(); // 呼叫中心链接
  1754. // 加入分组
  1755. await signalR.joinGroup('CallCenter');
  1756. });
  1757. onBeforeUnmount(() => {
  1758. mittBus.off('RestApplyPass');
  1759. if (ola.ws) ola.close();
  1760. });
  1761. </script>
  1762. <style scoped lang="scss">
  1763. .seizeSeat-box {
  1764. display: none;
  1765. }
  1766. .phoneControls {
  1767. display: flex;
  1768. flex: 1;
  1769. background-color: var(--el-color-white) !important;
  1770. box-shadow: 0 1px 8px 0 rgba(0, 15, 49, 0.1);
  1771. border-bottom-left-radius: 90px;
  1772. border-bottom-right-radius: 90px;
  1773. padding: 0 18px 0 40px;
  1774. color: var(--hotline-color-text-main);
  1775. height: 100%;
  1776. .duty-on-time {
  1777. width: 100px;
  1778. margin-left: 10px;
  1779. &-label {
  1780. display: block;
  1781. margin-top: 13px;
  1782. }
  1783. &-time {
  1784. display: block;
  1785. margin-top: 15px;
  1786. }
  1787. }
  1788. .infos {
  1789. text-align: left;
  1790. width: 140px;
  1791. .dutyOn_status {
  1792. color: var(--el-color-primary);
  1793. font-weight: normal;
  1794. }
  1795. span {
  1796. display: inline-block;
  1797. width: 80px;
  1798. text-align: right;
  1799. }
  1800. }
  1801. // 按钮列表
  1802. .btn-container {
  1803. display: flex;
  1804. width: calc(100% - 120px);
  1805. justify-content: space-between;
  1806. .item {
  1807. text-align: center;
  1808. cursor: pointer;
  1809. width: 100%;
  1810. user-select: none;
  1811. height: calc(100% + 20px);
  1812. img {
  1813. display: block;
  1814. margin: 0 auto;
  1815. padding-top: 10px;
  1816. }
  1817. span {
  1818. margin-top: 5px;
  1819. display: inline-block;
  1820. }
  1821. &.disabled {
  1822. cursor: not-allowed;
  1823. overflow: hidden;
  1824. }
  1825. }
  1826. .active {
  1827. &:hover {
  1828. color: var(--hotline-color-white);
  1829. background-image: url('@/assets/images/phoneControls/active.png');
  1830. background-repeat: no-repeat;
  1831. background-size: 100% 100%;
  1832. }
  1833. }
  1834. }
  1835. }
  1836. </style>
  1837. <style lang="scss">
  1838. .el-popover.hangup-popover {
  1839. .hangup-container {
  1840. display: flex;
  1841. flex-wrap: nowrap;
  1842. .hangup-item {
  1843. text-align: center;
  1844. width: 120px;
  1845. border-right: 1px solid var(--el-border-color-darker);
  1846. &:last-child {
  1847. border-right: 0;
  1848. }
  1849. &-phoneNumber {
  1850. padding-top: 4px;
  1851. margin-bottom: 10px;
  1852. }
  1853. }
  1854. }
  1855. }
  1856. </style>