order-service.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. <template>
  2. <div class="hc-order-service">
  3. <div class="order-service-content">
  4. <el-scrollbar ref="scrollbarRef">
  5. <div class="content-box">
  6. <div class="comment-card-box" v-for="(item,index) in orderDataList" :key="item.id">
  7. <div class="user-avatar-box">
  8. <el-avatar :size="50" :src="item.avatar || avatarPng" />
  9. </div>
  10. <div class="card-content-box">
  11. <div class="user-info-box">
  12. <div class="text-lg">{{item['createUserName']||'用户名异常'}}</div>
  13. <div class="text-gray">{{item['createTime']}}</div>
  14. </div>
  15. <div class="desc_para" v-html="item['opinionContent']"></div>
  16. <div class="image_desc" v-if="item['returnFiles']?.length > 0">
  17. <div class="hc-image-box" v-for="(items,indexs) in item['returnFiles']">
  18. <HcImg class="hc-image" :src="items" :srcs="item['returnFiles']" :index="indexs"/>
  19. </div>
  20. </div>
  21. <div class="foot-tools-box">
  22. <div class="icon-box" :class="item['commentsNumber'] >= 1 ? 'active' : ''" @click="commentExpanded(item)">
  23. <HcIcon name="chat" class="icon"/>
  24. <span class="badge" v-if="item['commentsNumber'] >= 1">{{item['commentsNumber']}}</span>
  25. </div>
  26. <div class="icon-box" :class="item['currentUserGood'] ? 'active' : ''" :data-index="item['expandedName']" @click="likeClick(item)">
  27. <HcIcon name="thumb_up" class="icon"/>
  28. <span class="badge" v-if="item['goodNumber'] >= 1">{{item['goodNumber']}}</span>
  29. </div>
  30. </div>
  31. <el-collapse class="hc-collapse-box" v-model="item['expandedName']" accordion>
  32. <el-collapse-item title="" :name="`commentList-${item['id']}`">
  33. <div class="collapse-comment-box">
  34. <div class="comment-reply-content-box">
  35. <el-input autosize type="textarea" v-model="item['replyContent']" placeholder="我也说一句"/>
  36. <el-button type="primary" hc-btn @click="saveCommentClick(item)">评论</el-button>
  37. </div>
  38. <div class="user-comment-info-box" v-for="items in item['expandedCommentList']" :key="items.id">
  39. <el-avatar :size="50" :src="items.avatar || avatarPng" />
  40. <div class="user-comment-box">
  41. <div class="user-info-box">
  42. <span class="user-name">{{items['userName']||'用户名异常'}}</span>
  43. <span class="create-time">{{items['createTime']}}</span>
  44. </div>
  45. <div class="user-comment-content-box" v-html="items['replyContent']"></div>
  46. </div>
  47. </div>
  48. </div>
  49. </el-collapse-item>
  50. </el-collapse>
  51. </div>
  52. <div class="code-status-box" v-if="parseInt(item['isSolve']) === 1">
  53. <img :src="Web515Png" class="widget" alt=""/>
  54. </div>
  55. </div>
  56. </div>
  57. </el-scrollbar>
  58. <div class="page-top-btn" @click="scrollToTop">
  59. <HcIcon name="vertical_align_top" class="icon"/>
  60. </div>
  61. </div>
  62. <!--我的工单服务-->
  63. <div class="order-service-data" :style="'width:' + leftWidth + 'px;'">
  64. <HcCard :scrollbar="false">
  65. <template #header>
  66. <el-badge :value="2" class="item-badge">
  67. <div class="font-bold text-lg">我的工单服务进度</div>
  68. </el-badge>
  69. </template>
  70. <template #extra>
  71. <el-tooltip effect="dark" content="发起新工单服务" placement="top">
  72. <el-button type="primary" hc-btn class="hc-add-icon" @click="newOrderServiceClick">
  73. <HcIcon name="add"/>
  74. </el-button>
  75. </el-tooltip>
  76. </template>
  77. <div class="mb-5">
  78. <el-select v-model="nameSelectKey" block placeholder="工单名称" size="large" @change="nameSelectUpdate">
  79. <el-option v-for="item in nameSelectData" :key="item.id" :label="item?.title" :value="item?.id"/>
  80. </el-select>
  81. </div>
  82. <div class="time-line-box" :class="isCurrentBol?'time-height':''">
  83. <el-scrollbar>
  84. <el-timeline class="hc-time-line">
  85. <template v-for="(item,index) in orderFlowList" :key="index">
  86. <el-timeline-item :class="item['currentBol']?'success':item['current']?'primary':''" size="large">
  87. <div class="timeline-item-icon">
  88. <HcIcon name="done" class="check-icon" v-if="item['currentBol']"/>
  89. <span v-else>{{index + 1}}</span>
  90. </div>
  91. <div class="reply-name">{{item['replyName']}}</div>
  92. <div class="reply-content" v-html="item['replyContent']"></div>
  93. </el-timeline-item>
  94. </template>
  95. </el-timeline>
  96. </el-scrollbar>
  97. </div>
  98. <div class="evaluation-box" :class="isCurrentBol?'show':''">
  99. <div class="text-lg font-bold">评价</div>
  100. <div class="tip-box">请对工单处理评价,若是未解决问题,可进行投诉,平台核实情况,将对相关客服人员绩效考核,并且从新为您自动发起工单解决问题</div>
  101. <div class="radio-group-box">
  102. <el-radio-group class="radio-group" v-model="evaluationKey">
  103. <div class="radio-item" v-for="item in evaluationData" :key="item.value">
  104. <el-radio :label="item.value" size="large" class="size-xl">{{ item.label }}</el-radio>
  105. </div>
  106. </el-radio-group>
  107. </div>
  108. <div class="btn-box">
  109. <el-button type="primary" hc-btn @click="disposeUserFeedback">
  110. <HcIcon name="check_circle"/>
  111. <span>提交</span>
  112. </el-button>
  113. </div>
  114. </div>
  115. </HcCard>
  116. <!--左右拖动-->
  117. <div class="horizontal-drag-line" @mousedown="onmousedown"/>
  118. </div>
  119. <!--提交工单-->
  120. <el-dialog v-model="showModal" title="发起新工单服务" width="720px" custom-class="hc-modal-border" :before-close="handleModalClose">
  121. <div class="title">请选择您需要反馈的问题类型</div>
  122. <div class="hc-type-tabs my-5">
  123. <el-radio-group v-model="typeTabKey" size="large" @change="typeTabChange">
  124. <el-radio-button v-for="item in typeTab" :label="item?.dictValue">{{item?.dictValue}}</el-radio-button>
  125. </el-radio-group>
  126. </div>
  127. <div class="modal-checkbox-box">
  128. <el-checkbox-group v-model="typeCheckBox[typeTabIndex]">
  129. <div class="checkbox-item" v-for="item in typeTab[typeTabIndex]?.children" :key="item.id">
  130. <el-checkbox :label="item['dictValue']">{{item['dictValue']}}</el-checkbox>
  131. </div>
  132. </el-checkbox-group>
  133. </div>
  134. <div class="mt-5">
  135. <el-input v-model="opinionContent" :rows="3" type="textarea" placeholder="请输入你宝贵的建议,我们将会跟踪解决"/>
  136. </div>
  137. <div class="mt-3 upload-img" v-loading="spinShow">
  138. <el-upload v-model:file-list="uploadFileList" :action="uploadAction" :headers="getTokenHeader()" :limit="3" :accept="uploadAccept" list-type="picture-card" multiple
  139. :before-upload="beforeUpload" :on-change="uploadChange" :on-exceed="uploadExceed" :on-preview="handlePreview" :on-remove="removeUpload">
  140. <HcIcon name="add" class="hc-upload-icon"/>
  141. </el-upload>
  142. <el-image-viewer v-if="showViewer" :initial-index="initialIndex" :url-list="previewFileList" @close="showViewer = false"/>
  143. </div>
  144. <div class="mt-3">
  145. <el-alert title="请上传JPG、PNG格式的图片文件,最多上传 3 张图片,文件大小不超过30M" type="error" :closable="false"/>
  146. </div>
  147. <template #footer>
  148. <div class="dialog-footer">
  149. <el-button size="large" @click="handleModalClose">取消</el-button>
  150. <el-button type="primary" hc-btn @click="saveClick">提交</el-button>
  151. </div>
  152. </template>
  153. </el-dialog>
  154. <!--提示框-->
  155. <el-dialog v-model="showTipModal" title="感谢" width="600px" custom-class="hc-modal-border" :before-close="handleTipModalClose">
  156. <div class="tip-modal-icon-box">
  157. <HcIcon name="sentiment_very_satisfied"/>
  158. </div>
  159. <div class="tip-modal-text-box">感谢您的仗义直言,大恩不言谢,有事联系我们,我们随时都在</div>
  160. <template #footer>
  161. <div class="dialog-footer">
  162. <el-button size="large" @click="tipModalClick">下次不用感谢了</el-button>
  163. <el-button type="primary" hc-btn @click="handleTipModalClose">不客气</el-button>
  164. </div>
  165. </template>
  166. </el-dialog>
  167. </div>
  168. </template>
  169. <script setup>
  170. import {nextTick, onMounted, ref, watch} from "vue";
  171. import {useAppStore} from "~src/store/index";
  172. import orderServe from '~api/other/orderServe';
  173. import {getTokenHeader} from '~src/api/request/header';
  174. import avatarPng from '~src/assets/images/avatar.png';
  175. import Web515Png from '~src/assets/images/Web515.png';
  176. import {userConfigSave} from "~api/other";
  177. import {isType, isSize, base64ToFile, getIndex} from "vue-utils-plus"
  178. import oss from "~api/oss";
  179. //初始变量
  180. const { getArrValue, getObjValue, getObjNullValue } = isType()
  181. const useAppState = useAppStore()
  182. const projectId = ref(useAppState.getProjectId);
  183. const contractId = ref(useAppState.getContractId);
  184. const isScreenShort = ref(useAppState.getScreenShort)
  185. //是否弹出工单感谢, 0不弹出,1弹出
  186. const opinionView = ref(useAppState.getOrderServiceTipModal)
  187. //搜索和分页数据
  188. const searchForm = ref({current: 1, size: 20})
  189. const orderDataList = ref([])
  190. // 工单名称
  191. const nameSelectKey = ref(null)
  192. const nameSelectData = ref([])
  193. //监听
  194. watch(() => [
  195. useAppState.getScreenShort
  196. ], ([ScreenShort]) => {
  197. isScreenShort.value = ScreenShort
  198. if( ScreenShort ) {
  199. let base64 = window.sessionStorage.getItem('screenShort-base64') || '';
  200. if (base64) uploadImgFile(base64)
  201. }
  202. })
  203. nextTick(() => {
  204. //截图数据
  205. if(isScreenShort.value) {
  206. let base64 = window.sessionStorage.getItem('screenShort-base64') || '';
  207. if (base64) uploadImgFile(base64)
  208. }
  209. })
  210. onMounted(() => {
  211. //获取相关数据
  212. queryUserOpinionPage()
  213. queryCurrentUserOpinionList()
  214. })
  215. //获取列表数据
  216. const queryUserOpinionPage = async () => {
  217. const { error, code, data } = await orderServe.queryUserOpinionPage(searchForm.value)
  218. if (!error && code === 200) {
  219. orderDataList.value = getArrValue(data['records'])
  220. } else {
  221. orderDataList.value = []
  222. }
  223. }
  224. //获取工单服务下拉列表
  225. const queryCurrentUserOpinionList = async () => {
  226. const { error, code, data } = await orderServe.queryCurrentUserOpinionList({
  227. projectId: projectId.value
  228. })
  229. if (!error && code === 200) {
  230. const res = getArrValue(data)
  231. nameSelectData.value = res
  232. if (res.length > 0) {
  233. nameSelectKey.value = res[0].id
  234. queryUserFlowOpinion()
  235. } else {
  236. nameSelectKey.value = null
  237. }
  238. } else {
  239. nameSelectData.value = []
  240. nameSelectKey.value = null
  241. }
  242. }
  243. //获取当前工单的最新流程
  244. const isCurrentBol = ref(false)
  245. const orderFlowList = ref([])
  246. const queryUserFlowOpinion = async () => {
  247. let id = nameSelectKey.value || null;
  248. const { error, code, data } = await orderServe.queryUserFlowOpinion({userOpinionId: id})
  249. if (!error && code === 200) {
  250. const res = getArrValue(data)
  251. orderFlowList.value = res
  252. if (res.length > 0) {
  253. const {currentBol, evaluation} = res[res.length-1];
  254. isCurrentBol.value = !!(currentBol && parseInt(evaluation) === -1);
  255. }
  256. } else {
  257. orderFlowList.value = []
  258. isCurrentBol.value = false
  259. }
  260. }
  261. //我的工单被切换
  262. const nameSelectUpdate = () => {
  263. //获取当前工单的最新流程
  264. queryUserFlowOpinion().then()
  265. }
  266. //评论
  267. const expandedName = ref('')
  268. const commentExpanded = (item) => {
  269. if (item['expandedName']) {
  270. item['expandedName'] = ''
  271. } else {
  272. item['expandedName'] = `commentList-${item.id}`
  273. queryCommentsList(item)
  274. }
  275. }
  276. //获取评论列表
  277. const queryCommentsList = async (item) => {
  278. const { error, code, data } = await orderServe.queryCommentsList({
  279. userOpinionId: item.id
  280. })
  281. if (!error && code === 200) {
  282. item['expandedCommentList'] = getArrValue(data)
  283. } else {
  284. item['expandedCommentList'] = []
  285. }
  286. }
  287. //提交评论
  288. const saveCommentClick = async (item) => {
  289. if (!item['replyContent']) {
  290. window.$message?.warning('请先填写评论内容');
  291. } else {
  292. const { error, code } = await orderServe.saveUserComments({
  293. userOpinionId: item.id,
  294. replyContent: item['replyContent'],
  295. projectId: projectId.value,
  296. contractId: contractId.value,
  297. })
  298. if (!error && code === 200) {
  299. window.$message?.success('评论成功');
  300. item['replyContent'] = ''
  301. queryCommentsList(item)
  302. }
  303. }
  304. }
  305. //点赞
  306. const likeClick = async (item) => {
  307. if (item['currentUserGood']) {
  308. const { error, code } = await orderServe.cancelGood({
  309. userOpinionId: item.id
  310. })
  311. if (!error && code === 200) {
  312. item['currentUserGood'] = false
  313. item['goodNumber'] --
  314. }
  315. } else {
  316. const { error, code } = await orderServe.addGoodNumber({
  317. userOpinionId: item.id,
  318. good: 1
  319. })
  320. if (!error && code === 200) {
  321. item['currentUserGood'] = true
  322. item['goodNumber'] ++
  323. }
  324. }
  325. }
  326. //弹框
  327. const showModal = ref(false)
  328. //类型tab数据
  329. const typeTabKey = ref(null)
  330. const typeTab = ref([]);
  331. const typeTabIndex = ref(-1);
  332. const typeCheckBox = ref([]);
  333. const typeTabChange = (val) => {
  334. typeTabKey.value = val;
  335. typeTabIndex.value = typeTab.value.findIndex(item => item.dictValue === val);
  336. }
  337. //发起新工单服务
  338. const newOrderServiceClick = () => {
  339. queryDictBizList()
  340. showModal.value = true
  341. }
  342. //关闭
  343. const handleModalClose = () => {
  344. showModal.value = false
  345. }
  346. //获取字典信息
  347. const queryDictBizList = async () => {
  348. const { error, code, data } = await orderServe.queryDictBizList()
  349. if (!error && code === 200) {
  350. const res = getArrValue(data)
  351. typeTab.value = res
  352. if (res.length > 0) {
  353. typeTabIndex.value = 0
  354. typeTabKey.value = res[0]?.dictValue
  355. }
  356. } else {
  357. typeTab.value = []
  358. }
  359. }
  360. //建议内容
  361. const opinionContent = ref('')
  362. //上传
  363. const uploadFileList = ref([])
  364. const uploadAction = "/api/blade-resource/oss/endpoint/put-file"
  365. const uploadAccept = "image/png,image/jpg,image/jpeg"
  366. //上传前
  367. const beforeUpload = (res) => {
  368. if (isSize(res?.size,30)) {
  369. return true;
  370. } else {
  371. window?.$message?.warning('文件大小,不能过30M!');
  372. return false;
  373. }
  374. }
  375. //状态改变
  376. const uploadChange = (_, uploadFiles) => {
  377. console.log(uploadFiles)
  378. //暂时不知道怎么搞。。。
  379. }
  380. //超出限制时
  381. const uploadExceed = () => {
  382. window?.$message?.warning('请上传JPG、PNG格式的图片文件,最多上传 3 张图片,文件大小不超过30M');
  383. }
  384. //预览
  385. const showViewer = ref(false)
  386. const previewFileList = ref([])
  387. const initialIndex = ref(0)
  388. const handlePreview = (file) => {
  389. let fileArr = getUploadFileUrl()
  390. const fileList = uploadFileList.value ?? [];
  391. const index = getIndex(fileList, 'uid', file?.uid)
  392. previewFileList.value = fileArr
  393. initialIndex.value = index
  394. showViewer.value = true
  395. }
  396. //获取文件URL
  397. const getUploadFileUrl = () => {
  398. let fileArr = [], fileList = uploadFileList.value ?? [];
  399. fileList.forEach(item => {
  400. fileArr.push(item?.response?.data?.link)
  401. })
  402. return fileArr
  403. }
  404. //删除文件
  405. const removeUpload = async (file) => {
  406. const fileName = file?.response?.data?.name
  407. const { error, code } = await oss.removeFile({
  408. fileName: fileName
  409. })
  410. if (!error && code === 200) {
  411. return true
  412. } else {
  413. return false
  414. }
  415. }
  416. //上传截图文件
  417. const spinShow = ref(false)
  418. const uploadImgFile = async (base64) => {
  419. let fileOfBlob = base64ToFile(base64);
  420. let formData = new FormData();
  421. formData.append("file", fileOfBlob);
  422. //上传文件
  423. spinShow.value = true
  424. newOrderServiceClick()
  425. const { error, code, data } = await oss.putFile(formData, false)
  426. spinShow.value = false
  427. if (!error && code === 200) {
  428. let res = getObjValue(data)
  429. if (res?.link) {
  430. uploadFileList.value.push({
  431. url: res?.link,
  432. name: res?.name,
  433. response: {data: res}
  434. })
  435. }
  436. window.sessionStorage.removeItem('screenShort-base64');
  437. window.$message?.success('文件上传成功');
  438. spinShow.value = false
  439. } else {
  440. window.sessionStorage.removeItem('screenShort-base64');
  441. window.$message?.warning('文件上传失败');
  442. }
  443. }
  444. //提交工单反馈
  445. const saveClick = async () => {
  446. //拼接问题类型
  447. let problemType = typeTabKey.value, index = typeTabIndex.value, problemVal = '';
  448. const checkBoxVal = typeCheckBox.value[index] || [];
  449. checkBoxVal.forEach(item => {problemVal += `-${item}`})
  450. let filesUrl = getUploadFileUrl()
  451. //判断数据
  452. if (!problemVal) {
  453. window.$message?.warning('请先选择问题类型');
  454. } else {
  455. //请求接口
  456. const { error, code } = await orderServe.saveUserOpinion({
  457. projectId: projectId.value,
  458. contractId: contractId.value,
  459. problemType: problemType + problemVal,
  460. opinionContent: opinionContent.value,
  461. returnFiles: filesUrl
  462. })
  463. if (!error && code === 200) {
  464. window.$message?.success('提交成功');
  465. showModal.value = false;
  466. //重置表单
  467. typeCheckBox.value[index] = []
  468. opinionContent.value = ''
  469. uploadFileList.value = []
  470. previewFileList.value = []
  471. //更新数据
  472. queryUserOpinionPage()
  473. queryCurrentUserOpinionList()
  474. }
  475. }
  476. }
  477. //评价
  478. const showTipModal = ref(false)
  479. const evaluationKey = ref('1')
  480. const evaluationData = [
  481. {value: "1", label: "满意"},
  482. {value: "2", label: "不满意并再次提交解决"},
  483. {value: "3", label: "不满意且投诉"}
  484. ]
  485. const disposeUserFeedback = async () => {
  486. let oldEndFlow = orderFlowList.value[3]?.id || ''
  487. const { error, code } = await orderServe.disposeUserFeedback({
  488. oldEndFlow: oldEndFlow,
  489. type: evaluationKey.value || '',
  490. userOpinionId: nameSelectKey.value || ''
  491. })
  492. if (!error && code === 200) {
  493. window.$message?.success('提交成功');
  494. showTipModal.value = parseInt(opinionView.value) === 1
  495. queryCurrentUserOpinionList()
  496. }
  497. }
  498. //提示框
  499. const tipModalClick = async () => {
  500. await userConfigSave({opinionView: 0})
  501. showTipModal.value = false
  502. useAppState.setOrderServiceTipModal(0)
  503. opinionView.value = 0
  504. }
  505. const handleTipModalClose = () => {
  506. showTipModal.value = false
  507. }
  508. //滚动到顶部
  509. const scrollbarRef = ref(null)
  510. const scrollToTop = () => {
  511. scrollbarRef.value?.setScrollTop(0)
  512. }
  513. //左右拖动,改变树形结构宽度
  514. const leftWidth = ref(500);
  515. const onmousedown = () => {
  516. const clientWidth = document.body.clientWidth
  517. document.onmousemove = (ve) => {
  518. let diffVal = clientWidth - (ve.clientX + 24);
  519. if (diffVal >= 300 && diffVal <= 1000) {
  520. leftWidth.value = diffVal;
  521. }
  522. }
  523. document.onmouseup = () => {
  524. document.onmousemove = null;
  525. document.onmouseup = null;
  526. }
  527. }
  528. </script>
  529. <style lang="scss" scoped>
  530. @import "../../styles/other/order-service.scss";
  531. </style>
  532. <style lang="scss">
  533. .item-badge .el-badge__content.is-fixed {
  534. top: 4px;
  535. }
  536. .comment-card-box .card-content-box .hc-collapse-box.el-collapse {
  537. border: 0;
  538. .el-collapse-item {
  539. &:last-child {
  540. margin-bottom: 0;
  541. }
  542. .el-collapse-item__header {
  543. display: none;
  544. }
  545. .el-collapse-item__wrap {
  546. background-color: initial;
  547. border-bottom: 0;
  548. }
  549. }
  550. }
  551. .comment-reply-content-box {
  552. .el-textarea {
  553. min-height: 40px;
  554. margin-right: 10px;
  555. .el-textarea__inner {
  556. min-height: 40px !important;
  557. }
  558. }
  559. }
  560. </style>