|
|
@@ -0,0 +1,293 @@
|
|
|
+<template>
|
|
|
+ <div class="container">
|
|
|
+ <Breadcrumb :items="['工单管理', '工单详情']" />
|
|
|
+ <a-card title="工单详情" :loading="loading">
|
|
|
+ <a-descriptions :data="info" :column="2" />
|
|
|
+ <div class="buttonGroup">
|
|
|
+ <a-button type="primary" size="large" :loading="buttonLoading" @click="CloseOrder()"
|
|
|
+ :disabled="data.state === 2">{{ data.state === 2 ? '已关闭' :
|
|
|
+ '关闭工单' }}</a-button>
|
|
|
+ </div>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <a-card title="沟通记录" style="margin-top: 15px; padding-bottom: 20px;">
|
|
|
+ <div class="message" v-for="(msg, index) in data.msg">
|
|
|
+ <a-divider v-if="index !== 0" />
|
|
|
+ <div class="head">
|
|
|
+ <a-avatar :size="40">
|
|
|
+ <img :src="data.userInfo[msg.uuid]?.avatar ?? ''" />
|
|
|
+ </a-avatar>
|
|
|
+ <div class="right">
|
|
|
+ <div class="username">
|
|
|
+ {{ data.userInfo[msg.uuid]?.username }}
|
|
|
+ <a-tag color="gray" v-if="msg.type === 'system'">
|
|
|
+ <template #icon>
|
|
|
+ <icon-desktop />
|
|
|
+ </template>
|
|
|
+ 系统回复
|
|
|
+ </a-tag>
|
|
|
+ <a-tag color="orangered" v-else-if="msg.type === 'user'">
|
|
|
+ <template #icon>
|
|
|
+ <icon-user />
|
|
|
+ </template>
|
|
|
+ 用户回复
|
|
|
+ </a-tag>
|
|
|
+ <a-tag color="blue" v-else-if="msg.type === 'server'">
|
|
|
+ <template #icon>
|
|
|
+ <icon-customer-service />
|
|
|
+ </template>
|
|
|
+ 客服回复
|
|
|
+ </a-tag>
|
|
|
+ </div>
|
|
|
+ <div class="time">
|
|
|
+ {{ stramptoTime(msg.time) }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="content" :style="{ whiteSpace: 'pre-wrap' }">
|
|
|
+ {{ msg.content }}
|
|
|
+ </div>
|
|
|
+ <div class="filebox">
|
|
|
+ <a-upload :default-file-list="msg.files" :show-upload-button="false" :show-remove-button="false">
|
|
|
+ <template #success-icon>
|
|
|
+ </template>
|
|
|
+ </a-upload>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <a-card title="回复工单" style="margin-top: 15px;" v-if="data.state !== 2">
|
|
|
+ <a-form :model="form" :rules="rules" layout="vertical" :style="{ width: '600px' }"
|
|
|
+ @submit-success="handleSubmit">
|
|
|
+ <a-form-item field="content" label="内容">
|
|
|
+ <a-textarea v-model="form.content" placeholder="请详细说明您遇到的问题,详细的阐述有助于我们快速为您解决问题..." :max-length="200"
|
|
|
+ :auto-size="{
|
|
|
+ minRows: 6,
|
|
|
+ maxRows: 20
|
|
|
+ }" allow-clear show-word-limit />
|
|
|
+ </a-form-item>
|
|
|
+
|
|
|
+ <a-form-item field="files" label="上传附件">
|
|
|
+ <a-upload action="/cloud/api.php" :file-list="form.files" @change="handleFileChange" />
|
|
|
+ </a-form-item>
|
|
|
+
|
|
|
+ <a-form-item>
|
|
|
+ <a-button html-type="submit" :loading="formLoading">提交</a-button>
|
|
|
+ </a-form-item>
|
|
|
+ </a-form>
|
|
|
+ </a-card>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
|
|
+import { adminOrderDetail, closeOrder, adminReplyOrder } from '@/api/workOrder'
|
|
|
+import { Notification } from '@arco-design/web-vue'
|
|
|
+import { useRoute } from 'vue-router'
|
|
|
+
|
|
|
+const route = useRoute()
|
|
|
+
|
|
|
+const loading = ref(false)
|
|
|
+const formLoading = ref(false)
|
|
|
+const buttonLoading = ref(false)
|
|
|
+const data = ref({})
|
|
|
+const info = ref([])
|
|
|
+
|
|
|
+const form = reactive({
|
|
|
+ content: '',
|
|
|
+ files: [],
|
|
|
+})
|
|
|
+
|
|
|
+const rules = {
|
|
|
+ content: [
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: '请详细说明您遇到的问题',
|
|
|
+ },
|
|
|
+ ],
|
|
|
+}
|
|
|
+
|
|
|
+const handleFileChange = (fileList) => {
|
|
|
+ fileList.forEach(f => {
|
|
|
+ if (f.status === 'done' && f.response?.downurl) {
|
|
|
+ f.url = f.response?.viewurl ?? f.response.downurl
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ form.files = fileList // 更新到表单数据中
|
|
|
+}
|
|
|
+
|
|
|
+function getState(state) {
|
|
|
+ switch (state) {
|
|
|
+ case 0:
|
|
|
+ return '待处理'
|
|
|
+ case 1:
|
|
|
+ return '已回复'
|
|
|
+ case 2:
|
|
|
+ return '已关闭'
|
|
|
+ }
|
|
|
+ return '未知'
|
|
|
+}
|
|
|
+
|
|
|
+const handleSubmit = async () => {
|
|
|
+ try {
|
|
|
+ formLoading.value = true
|
|
|
+ const data = {
|
|
|
+ id: route.params.id,
|
|
|
+ ...form
|
|
|
+ }
|
|
|
+ const res = await adminReplyOrder(data)
|
|
|
+ if (!res || res.code !== 0)
|
|
|
+ return Notification.error({
|
|
|
+ title: '回复工单失败!',
|
|
|
+ content: res?.msg ?? '请稍后再试'
|
|
|
+ })
|
|
|
+ Notification.success({
|
|
|
+ title: '回复工单成功!'
|
|
|
+ })
|
|
|
+ form.content = ''
|
|
|
+ form.files = []
|
|
|
+ getOrderDetail()
|
|
|
+ } catch (error) {
|
|
|
+ Notification.error({
|
|
|
+ title: '回复工单失败!',
|
|
|
+ content: error.message || '请稍后再试'
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ formLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const CloseOrder = async () => {
|
|
|
+ try {
|
|
|
+ buttonLoading.value = true
|
|
|
+ const res = await closeOrder({ id: route.params.id })
|
|
|
+ if (!res || res.code !== 0)
|
|
|
+ return Notification.error({
|
|
|
+ title: '关闭工单失败!',
|
|
|
+ content: res?.msg ?? '请稍后再试'
|
|
|
+ })
|
|
|
+ Notification.success({
|
|
|
+ title: '关闭工单成功!'
|
|
|
+ })
|
|
|
+ getOrderDetail()
|
|
|
+ } catch (error) {
|
|
|
+ Notification.error({
|
|
|
+ title: '关闭工单失败!',
|
|
|
+ content: error.message || '请稍后再试'
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ buttonLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const getOrderDetail = async () => {
|
|
|
+ try {
|
|
|
+ const res = await adminOrderDetail({ id: route.params.id })
|
|
|
+ if (!res || res.code !== 0)
|
|
|
+ return Notification.error({
|
|
|
+ title: '获取工单详情失败!',
|
|
|
+ content: res?.msg ?? '请稍后再试'
|
|
|
+ })
|
|
|
+
|
|
|
+ data.value = res.data
|
|
|
+
|
|
|
+ info.value = [
|
|
|
+ { label: '工单标题', value: res.data.title },
|
|
|
+ { label: '工单ID', value: res.data.id },
|
|
|
+ { label: '通知邮箱', value: res.data.email },
|
|
|
+ { label: '创建时间', value: stramptoTime(res.data.create_time) },
|
|
|
+ { label: '最后更新时间', value: stramptoTime(res.data.update_time) },
|
|
|
+ { label: '当前状态', value: getState(res.data.state) }
|
|
|
+ ]
|
|
|
+ } catch (error) {
|
|
|
+ Notification.error({
|
|
|
+ title: '获取路径数据失败!',
|
|
|
+ content: error.message || '请稍后再试'
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+let timer = null
|
|
|
+
|
|
|
+// 轮询
|
|
|
+const startPolling = () => {
|
|
|
+ if (!timer) {
|
|
|
+ timer = setInterval(async () => {
|
|
|
+ await getOrderDetail()
|
|
|
+ }, 2000)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 停止轮询
|
|
|
+const stopPolling = () => {
|
|
|
+ if (timer) {
|
|
|
+ clearInterval(timer)
|
|
|
+ timer = null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ loading.value = true
|
|
|
+ await getOrderDetail()
|
|
|
+ loading.value = false
|
|
|
+ startPolling()
|
|
|
+})
|
|
|
+
|
|
|
+// 组件销毁时停止轮询
|
|
|
+onUnmounted(() => {
|
|
|
+ stopPolling()
|
|
|
+})
|
|
|
+
|
|
|
+const stramptoTime = (time) => {
|
|
|
+ return new Date(time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="less">
|
|
|
+.container {
|
|
|
+ padding: 0 20px 20px 20px;
|
|
|
+
|
|
|
+ .buttonGroup {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 20px;
|
|
|
+ margin: 15px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.message {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+
|
|
|
+ .head {
|
|
|
+ display: flex;
|
|
|
+
|
|
|
+ .right {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ margin-left: 10px;
|
|
|
+
|
|
|
+ .username {
|
|
|
+ font-size: 1.2em;
|
|
|
+ display: flex;
|
|
|
+ gap: 10px
|
|
|
+ }
|
|
|
+
|
|
|
+ .time {
|
|
|
+ font-size: 0.9em;
|
|
|
+ color: #777;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .content {
|
|
|
+ margin-top: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .filebox {
|
|
|
+ margin-top: -10px;
|
|
|
+ max-width: 500px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|