orderDetail.vue 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. <template>
  2. <div class="container">
  3. <Breadcrumb :items="['工单管理', '工单详情']" />
  4. <a-card title="工单详情" :loading="loading">
  5. <a-descriptions :data="info" :column="2" />
  6. <div class="buttonGroup">
  7. <a-button type="primary" size="large" :loading="buttonLoading" @click="CloseOrder()"
  8. :disabled="data.state === 2">{{ data.state === 2 ? '已关闭' :
  9. '关闭工单' }}</a-button>
  10. </div>
  11. </a-card>
  12. <a-card title="沟通记录" style="margin-top: 15px; padding-bottom: 20px;">
  13. <div class="message" v-for="(msg, index) in data.msg">
  14. <a-divider v-if="index !== 0" />
  15. <div class="head">
  16. <a-avatar>
  17. <IconUser v-if="!data.userInfo[msg.uuid]?.avatar" />
  18. <img alt="" :src="data.userInfo[msg.uuid]?.avatar" v-else />
  19. </a-avatar>
  20. <div class="right">
  21. <div class="username">
  22. {{ data.userInfo[msg.uuid]?.username }}
  23. <a-tag color="gray" v-if="msg.type === 'system'">
  24. <template #icon>
  25. <icon-desktop />
  26. </template>
  27. 系统回复
  28. </a-tag>
  29. <a-tag color="orangered" v-else-if="msg.type === 'user'">
  30. <template #icon>
  31. <icon-user />
  32. </template>
  33. 用户回复
  34. </a-tag>
  35. <a-tag color="blue" v-else-if="msg.type === 'server'">
  36. <template #icon>
  37. <icon-customer-service />
  38. </template>
  39. 客服回复
  40. </a-tag>
  41. </div>
  42. <div class="time">
  43. {{ stramptoTime(msg.time) }}
  44. </div>
  45. </div>
  46. </div>
  47. <div class="content" :style="{ whiteSpace: 'pre-wrap' }">
  48. {{ msg.content }}
  49. </div>
  50. <div class="filebox">
  51. <a-upload :default-file-list="msg.files" :show-upload-button="false" :show-remove-button="false">
  52. <template #success-icon>
  53. </template>
  54. </a-upload>
  55. </div>
  56. </div>
  57. </a-card>
  58. <a-card title="回复工单" style="margin-top: 15px;" v-if="data.state !== 2">
  59. <a-form :model="form" :rules="rules" layout="vertical" :style="{ width: '600px' }"
  60. @submit-success="handleSubmit">
  61. <a-form-item field="content" label="内容">
  62. <a-textarea v-model="form.content" placeholder="请详细说明您遇到的问题,详细的阐述有助于我们快速为您解决问题..." :max-length="200"
  63. :auto-size="{
  64. minRows: 6,
  65. maxRows: 20
  66. }" allow-clear show-word-limit />
  67. </a-form-item>
  68. <a-form-item field="files" label="上传附件">
  69. <a-upload action="/cloud/api.php" :file-list="form.files" @change="handleFileChange" />
  70. </a-form-item>
  71. <a-form-item>
  72. <a-button html-type="submit" :loading="formLoading">提交</a-button>
  73. </a-form-item>
  74. </a-form>
  75. </a-card>
  76. </div>
  77. </template>
  78. <script setup>
  79. import { ref, reactive, onMounted, onUnmounted } from 'vue'
  80. import { adminOrderDetail, closeOrder, adminReplyOrder } from '@/api/workOrder'
  81. import { Notification } from '@arco-design/web-vue'
  82. import { useRoute } from 'vue-router'
  83. const route = useRoute()
  84. const loading = ref(false)
  85. const formLoading = ref(false)
  86. const buttonLoading = ref(false)
  87. const data = ref({})
  88. const info = ref([])
  89. const form = reactive({
  90. content: '',
  91. files: [],
  92. })
  93. const rules = {
  94. content: [
  95. {
  96. required: true,
  97. message: '请详细说明您遇到的问题',
  98. },
  99. ],
  100. }
  101. const handleFileChange = (fileList) => {
  102. fileList.forEach(f => {
  103. if (f.status === 'done' && f.response?.downurl) {
  104. f.url = f.response?.viewurl ?? f.response.downurl
  105. }
  106. })
  107. form.files = fileList // 更新到表单数据中
  108. }
  109. function getState(state) {
  110. switch (state) {
  111. case 0:
  112. return '待处理'
  113. case 1:
  114. return '已回复'
  115. case 2:
  116. return '已关闭'
  117. }
  118. return '未知'
  119. }
  120. const handleSubmit = async () => {
  121. try {
  122. formLoading.value = true
  123. const data = {
  124. id: route.params.id,
  125. ...form
  126. }
  127. const res = await adminReplyOrder(data)
  128. if (!res || res.code !== 0)
  129. return Notification.error({
  130. title: '回复工单失败!',
  131. content: res?.msg ?? '请稍后再试'
  132. })
  133. Notification.success({
  134. title: '回复工单成功!'
  135. })
  136. form.content = ''
  137. form.files = []
  138. getOrderDetail()
  139. } catch (error) {
  140. Notification.error({
  141. title: '回复工单失败!',
  142. content: error.message || '请稍后再试'
  143. })
  144. } finally {
  145. formLoading.value = false
  146. }
  147. }
  148. const CloseOrder = async () => {
  149. try {
  150. buttonLoading.value = true
  151. const res = await closeOrder({ id: route.params.id })
  152. if (!res || res.code !== 0)
  153. return Notification.error({
  154. title: '关闭工单失败!',
  155. content: res?.msg ?? '请稍后再试'
  156. })
  157. Notification.success({
  158. title: '关闭工单成功!'
  159. })
  160. getOrderDetail()
  161. } catch (error) {
  162. Notification.error({
  163. title: '关闭工单失败!',
  164. content: error.message || '请稍后再试'
  165. })
  166. } finally {
  167. buttonLoading.value = false
  168. }
  169. }
  170. const getOrderDetail = async () => {
  171. try {
  172. const res = await adminOrderDetail({ id: route.params.id })
  173. if (!res || res.code !== 0)
  174. return Notification.error({
  175. title: '获取工单详情失败!',
  176. content: res?.msg ?? '请稍后再试'
  177. })
  178. data.value = res.data
  179. info.value = [
  180. { label: '工单标题', value: res.data.title },
  181. { label: '工单ID', value: res.data.id },
  182. { label: '发起人', value: res.data.userInfo[res.data.create_user]?.username},
  183. { label: '通知邮箱', value: res.data.email },
  184. { label: '创建时间', value: stramptoTime(res.data.create_time) },
  185. { label: '最后更新时间', value: stramptoTime(res.data.update_time) },
  186. { label: '当前状态', value: getState(res.data.state) }
  187. ]
  188. } catch (error) {
  189. Notification.error({
  190. title: '获取路径数据失败!',
  191. content: error.message || '请稍后再试'
  192. })
  193. }
  194. }
  195. let timer = null
  196. // 轮询
  197. const startPolling = () => {
  198. if (!timer) {
  199. timer = setInterval(async () => {
  200. await getOrderDetail()
  201. }, 2000)
  202. }
  203. }
  204. // 停止轮询
  205. const stopPolling = () => {
  206. if (timer) {
  207. clearInterval(timer)
  208. timer = null
  209. }
  210. }
  211. onMounted(async () => {
  212. loading.value = true
  213. await getOrderDetail()
  214. loading.value = false
  215. startPolling()
  216. })
  217. // 组件销毁时停止轮询
  218. onUnmounted(() => {
  219. stopPolling()
  220. })
  221. const stramptoTime = (time) => {
  222. return new Date(time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })
  223. }
  224. </script>
  225. <style scoped lang="less">
  226. .container {
  227. padding: 0 20px 20px 20px;
  228. .buttonGroup {
  229. display: flex;
  230. justify-content: center;
  231. gap: 20px;
  232. margin: 15px;
  233. }
  234. }
  235. .message {
  236. display: flex;
  237. flex-direction: column;
  238. margin-bottom: 5px;
  239. .head {
  240. display: flex;
  241. .right {
  242. display: flex;
  243. flex-direction: column;
  244. margin-left: 10px;
  245. .username {
  246. font-size: 1.2em;
  247. display: flex;
  248. gap: 10px
  249. }
  250. .time {
  251. font-size: 0.9em;
  252. color: #777;
  253. }
  254. }
  255. }
  256. .content {
  257. margin-top: 10px;
  258. }
  259. .filebox {
  260. margin-top: -10px;
  261. max-width: 500px;
  262. }
  263. }
  264. </style>