web工单处理
This commit is contained in:
@@ -164,6 +164,12 @@
|
||||
<view class="title-bar"></view>
|
||||
<text class="title-text">处理记录</text>
|
||||
</view>
|
||||
<!-- 处理人记录处理过程 -->
|
||||
<view v-if="isProcessor" class="add-process-section">
|
||||
<button class="add-process-btn" type="primary" size="mini" @tap="navigateToAddProcess">
|
||||
添加处理记录
|
||||
</button>
|
||||
</view>
|
||||
<view class="timeline">
|
||||
<view class="timeline-item" v-for="(item, index) in processList" :key="index">
|
||||
<view class="timeline-dot" :class="getTimelineDotClass(item.status)"></view>
|
||||
@@ -198,13 +204,139 @@
|
||||
<view class="action-button primary" v-if="mode === 'create'" @tap="submitWorkcase">
|
||||
<text class="button-text">提交工单</text>
|
||||
</view>
|
||||
<view class="action-button primary" v-if="mode === 'view' && workcase.status === 'pending'" @tap="handleAssign">
|
||||
|
||||
<!-- 来客视角:只能撤销自己创建的工单 -->
|
||||
<view
|
||||
class="action-button danger"
|
||||
v-if="mode !== 'create' && isCreator && workcase.status !== 'done'"
|
||||
@tap="handleRevoke"
|
||||
>
|
||||
<text class="button-text">撤销工单</text>
|
||||
</view>
|
||||
|
||||
<!-- 员工视角:可以指派、转派、完成工单 -->
|
||||
<view
|
||||
class="action-button warning"
|
||||
v-if="mode !== 'create' && !isCreator && workcase.status === 'pending'"
|
||||
@tap="handleAssign"
|
||||
>
|
||||
<text class="button-text">指派工程师</text>
|
||||
</view>
|
||||
<view class="action-button success" v-if="mode === 'view' && workcase.status === 'processing'" @tap="handleComplete">
|
||||
<view
|
||||
class="action-button warning"
|
||||
v-if="mode !== 'create' && !isCreator && workcase.status === 'processing'"
|
||||
@tap="handleRedeployBtn"
|
||||
>
|
||||
<text class="button-text">转派工程师</text>
|
||||
</view>
|
||||
<view
|
||||
class="action-button success"
|
||||
v-if="mode !== 'create' && !isCreator && workcase.status === 'processing'"
|
||||
@tap="handleComplete"
|
||||
>
|
||||
<text class="button-text">完成工单</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 添加处理记录弹窗(使用 v-if 控制显示) -->
|
||||
<view v-if="showAddProcessDialog" class="process-modal-overlay" @tap="closeAddProcessDialog">
|
||||
<view class="process-modal" @tap.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">添加处理记录</text>
|
||||
<view class="modal-close" @tap="closeAddProcessDialog">
|
||||
<text>×</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="form-item">
|
||||
<text class="form-label">处理内容</text>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
v-model="processForm.message"
|
||||
placeholder="请详细描述处理过程、发现的问题或解决方案..."
|
||||
maxlength="500"
|
||||
/>
|
||||
<text class="char-count">{{ processForm.message.length }}/500</text>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">附件(可选)</text>
|
||||
<view class="file-upload-section">
|
||||
<view class="file-list">
|
||||
<view class="file-item" v-for="(file, index) in processForm.files" :key="index">
|
||||
<text class="file-name">{{ file.name }}</text>
|
||||
<view class="file-delete" @tap="deleteProcessFile(index)">
|
||||
<text>×</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<button
|
||||
class="upload-btn"
|
||||
size="mini"
|
||||
@tap="chooseProcessFile"
|
||||
v-if="processForm.files.length < 5"
|
||||
>
|
||||
选择文件
|
||||
</button>
|
||||
<text class="upload-tip">最多上传5个文件</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="modal-btn" @tap="closeAddProcessDialog">取消</button>
|
||||
<button class="modal-btn primary" type="primary" @tap="submitProcessRecord" :loading="submittingProcess">
|
||||
提交
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 指派/转派工程师弹窗 -->
|
||||
<view v-if="showAssignDialog" class="process-modal-overlay" @tap="closeAssignDialog">
|
||||
<view class="process-modal" @tap.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{ assignDialogTitle }}</text>
|
||||
<view class="modal-close" @tap="closeAssignDialog">
|
||||
<text>×</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="form-item">
|
||||
<text class="form-label">选择工程师</text>
|
||||
<picker
|
||||
class="form-picker"
|
||||
:value="selectedEngineerIndex"
|
||||
:range="availableEngineers"
|
||||
range-key="username"
|
||||
@change="onEngineerChange"
|
||||
>
|
||||
<view class="picker-content">
|
||||
<text class="picker-text" v-if="assignForm.processorName">{{ assignForm.processorName }}</text>
|
||||
<text class="picker-text placeholder" v-else>请选择工程师</text>
|
||||
<text class="picker-arrow">></text>
|
||||
</view>
|
||||
</picker>
|
||||
<view v-if="loadingEngineers" class="loading-tip">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">备注说明(可选)</text>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
v-model="assignForm.message"
|
||||
placeholder="请输入指派/转派说明..."
|
||||
maxlength="200"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="modal-btn" @tap="closeAssignDialog">取消</button>
|
||||
<button class="modal-btn primary" type="primary" @tap="submitAssign" :loading="submittingAssign">
|
||||
确定
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #ifdef APP -->
|
||||
</scroll-view>
|
||||
@@ -212,10 +344,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { ref, onMounted, reactive, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import type { TbWorkcaseDTO, TbWorkcaseProcessDTO } from '@/types/workcase'
|
||||
import { workcaseAPI, fileAPI } from '@/api'
|
||||
import type { CustomerServiceVO } from '@/types/workcase/chatRoom'
|
||||
import { workcaseAPI, fileAPI, workcaseChatAPI } from '@/api'
|
||||
|
||||
// 响应式数据
|
||||
const headerPaddingTop = ref<number>(44)
|
||||
@@ -228,6 +361,7 @@ const faultTypes = ref<string[]>(['电气系统故障', '机械故障', '控制
|
||||
const typeIndex = ref<number>(0)
|
||||
const emergencies = ref<string[]>(['普通', '紧急'])
|
||||
const emergencyIndex = ref<number>(0)
|
||||
const userId = JSON.parse(uni.getStorageSync('loginDomain')).userInfo.userId
|
||||
|
||||
// 工单数据
|
||||
const workcase = reactive<TbWorkcaseDTO>({})
|
||||
@@ -235,6 +369,38 @@ const workcase = reactive<TbWorkcaseDTO>({})
|
||||
// 处理记录
|
||||
const processList = reactive<TbWorkcaseProcessDTO[]>([])
|
||||
|
||||
// 判断是否是处理人
|
||||
const isProcessor = computed(() => {
|
||||
return workcase.processor === userId
|
||||
})
|
||||
|
||||
// 判断是否是创建人
|
||||
const isCreator = computed(() => {
|
||||
return workcase.creator === userId
|
||||
})
|
||||
|
||||
// 添加处理记录相关
|
||||
const showAddProcessDialog = ref(false)
|
||||
const submittingProcess = ref(false)
|
||||
const processForm = reactive({
|
||||
message: '',
|
||||
files: [] as Array<{ name: string; fileId: string }>
|
||||
})
|
||||
|
||||
// 指派/转派相关
|
||||
const showAssignDialog = ref(false)
|
||||
const assignDialogTitle = ref('指派工程师')
|
||||
const assignAction = ref<'assign' | 'redeploy'>('assign')
|
||||
const submittingAssign = ref(false)
|
||||
const loadingEngineers = ref(false)
|
||||
const availableEngineers = ref<CustomerServiceVO[]>([])
|
||||
const selectedEngineerIndex = ref(-1)
|
||||
const assignForm = reactive({
|
||||
processor: '',
|
||||
processorName: '',
|
||||
message: ''
|
||||
})
|
||||
|
||||
// 获取图片 URL(通过 fileId)
|
||||
function getImageUrl(fileId: string): string {
|
||||
// 如果已经是完整 URL,直接返回
|
||||
@@ -431,11 +597,9 @@ function handleViewChat() {
|
||||
|
||||
// 指派工程师
|
||||
function handleAssign() {
|
||||
uni.showToast({
|
||||
title: '指派工程师',
|
||||
icon: 'none'
|
||||
})
|
||||
// TODO: 实现指派逻辑
|
||||
assignAction.value = 'assign'
|
||||
assignDialogTitle.value = '指派工程师'
|
||||
openAssignDialog()
|
||||
}
|
||||
|
||||
// 完成工单
|
||||
@@ -443,18 +607,131 @@ function handleComplete() {
|
||||
uni.showModal({
|
||||
title: '完成确认',
|
||||
content: '确认完成该工单?',
|
||||
success: (res) => {
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
// TODO: 调用 API 完成工单
|
||||
uni.showToast({
|
||||
title: '工单已完成',
|
||||
icon: 'success'
|
||||
})
|
||||
try {
|
||||
const params: TbWorkcaseProcessDTO = {
|
||||
workcaseId: workcase.workcaseId,
|
||||
action: 'finish',
|
||||
message: '工单完成'
|
||||
}
|
||||
const result = await workcaseAPI.createWorkcaseProcess(params)
|
||||
if (result.success) {
|
||||
uni.showToast({ title: '工单已完成', icon: 'success' })
|
||||
// 重新加载工单详情
|
||||
if (workcase.workcaseId) {
|
||||
await loadWorkcaseDetail(workcase.workcaseId)
|
||||
}
|
||||
} else {
|
||||
uni.showToast({ title: result.message || '操作失败', icon: 'none' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('完成工单失败:', error)
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 转派工程师
|
||||
function handleRedeploy() {
|
||||
assignAction.value = 'redeploy'
|
||||
assignDialogTitle.value = '转派工程师'
|
||||
openAssignDialog()
|
||||
}
|
||||
|
||||
// 打开指派/转派弹窗
|
||||
async function openAssignDialog() {
|
||||
assignForm.processor = ''
|
||||
assignForm.processorName = ''
|
||||
assignForm.message = ''
|
||||
selectedEngineerIndex.value = -1
|
||||
showAssignDialog.value = true
|
||||
await loadAvailableEngineers()
|
||||
}
|
||||
|
||||
// 关闭指派/转派弹窗
|
||||
function closeAssignDialog() {
|
||||
showAssignDialog.value = false
|
||||
}
|
||||
|
||||
// 加载可用工程师列表(排除当前处理人)
|
||||
async function loadAvailableEngineers() {
|
||||
loadingEngineers.value = true
|
||||
try {
|
||||
const res = await workcaseChatAPI.getCustomerServicePage({
|
||||
filter: {},
|
||||
pageParam: { page: 1, pageSize: 100 }
|
||||
})
|
||||
if (res.success && res.dataList) {
|
||||
// 排除当前处理人
|
||||
availableEngineers.value = res.dataList.filter(
|
||||
(engineer: CustomerServiceVO) => engineer.userId !== workcase.processor
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载工程师列表失败:', error)
|
||||
uni.showToast({ title: '加载工程师列表失败', icon: 'none' })
|
||||
} finally {
|
||||
loadingEngineers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 选择工程师
|
||||
function onEngineerChange(e: any) {
|
||||
const index = e.detail.value
|
||||
selectedEngineerIndex.value = index
|
||||
if (index >= 0 && index < availableEngineers.value.length) {
|
||||
const engineer = availableEngineers.value[index]
|
||||
assignForm.processor = engineer.userId || ''
|
||||
assignForm.processorName = engineer.username || ''
|
||||
}
|
||||
}
|
||||
|
||||
// 提交指派/转派
|
||||
async function submitAssign() {
|
||||
if (!assignForm.processor) {
|
||||
uni.showToast({ title: '请选择工程师', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!workcase.workcaseId) {
|
||||
uni.showToast({ title: '工单ID不存在', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
submittingAssign.value = true
|
||||
try {
|
||||
const params: TbWorkcaseProcessDTO = {
|
||||
workcaseId: workcase.workcaseId,
|
||||
action: assignAction.value,
|
||||
processor: assignForm.processor,
|
||||
message: assignForm.message || (assignAction.value === 'assign' ? '工单指派' : '工单转派')
|
||||
}
|
||||
|
||||
const res = await workcaseAPI.createWorkcaseProcess(params)
|
||||
if (res.success) {
|
||||
uni.showToast({
|
||||
title: assignAction.value === 'assign' ? '指派成功' : '转派成功',
|
||||
icon: 'success'
|
||||
})
|
||||
closeAssignDialog()
|
||||
// 重新加载工单详情
|
||||
if (workcase.workcaseId) {
|
||||
await loadWorkcaseDetail(workcase.workcaseId)
|
||||
}
|
||||
} else {
|
||||
uni.showToast({ title: res.message || '操作失败', icon: 'none' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('指派/转派失败:', error)
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
} finally {
|
||||
submittingAssign.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 表单选择器事件
|
||||
function onTypeChange(e: any) {
|
||||
typeIndex.value = e.detail.value
|
||||
@@ -649,6 +926,127 @@ async function submitWorkcase() {
|
||||
}
|
||||
}
|
||||
|
||||
// 撤销工单
|
||||
function handleRevoke() {
|
||||
uni.showModal({
|
||||
title: '撤销确认',
|
||||
content: '确认撤销该工单?撤销后无法恢复',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// TODO: 调用 API 撤销工单
|
||||
uni.showToast({
|
||||
title: '工单已撤销',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 转派工程师
|
||||
function handleRedeployBtn() {
|
||||
handleRedeploy()
|
||||
}
|
||||
|
||||
// 导航到添加处理记录(或直接显示弹窗)
|
||||
function navigateToAddProcess() {
|
||||
showAddProcessDialog.value = true
|
||||
}
|
||||
|
||||
// 关闭添加处理记录弹窗
|
||||
function closeAddProcessDialog() {
|
||||
showAddProcessDialog.value = false
|
||||
}
|
||||
|
||||
// 选择处理记录附件
|
||||
async function chooseProcessFile() {
|
||||
uni.chooseFile({
|
||||
count: 5 - processForm.files.length,
|
||||
extension: ['.jpg', '.jpeg', '.png', '.pdf', '.doc', '.docx', '.xls', '.xlsx'],
|
||||
success: async (res) => {
|
||||
uni.showLoading({ title: '上传中...' })
|
||||
try {
|
||||
const uploadPromises = res.tempFiles.map(async (file) => {
|
||||
const result = await fileAPI.uploadFile(file.path, {
|
||||
module: 'workcase',
|
||||
optsn: workcase.workcaseId || 'temp'
|
||||
})
|
||||
if (result.success && result.data?.fileId) {
|
||||
return {
|
||||
name: file.name,
|
||||
fileId: result.data.fileId
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const results = await Promise.all(uploadPromises)
|
||||
results.forEach(result => {
|
||||
if (result) {
|
||||
processForm.files.push(result)
|
||||
}
|
||||
})
|
||||
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '上传成功', icon: 'success' })
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
console.error('上传文件失败:', error)
|
||||
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除处理记录附件
|
||||
function deleteProcessFile(index: number) {
|
||||
processForm.files.splice(index, 1)
|
||||
}
|
||||
|
||||
// 提交处理记录
|
||||
async function submitProcessRecord() {
|
||||
if (!processForm.message.trim()) {
|
||||
uni.showToast({ title: '请输入处理内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!workcase.workcaseId) {
|
||||
uni.showToast({ title: '工单ID不存在', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
submittingProcess.value = true
|
||||
try {
|
||||
const fileIds = processForm.files.map(f => f.fileId).join(',')
|
||||
const params: TbWorkcaseProcessDTO = {
|
||||
workcaseId: workcase.workcaseId,
|
||||
action: 'info',
|
||||
message: processForm.message,
|
||||
files: fileIds || undefined
|
||||
}
|
||||
|
||||
const res = await workcaseAPI.addWorkcaseProcess(params)
|
||||
if (res.success) {
|
||||
uni.showToast({ title: '处理记录添加成功', icon: 'success' })
|
||||
closeAddProcessDialog()
|
||||
// 重置表单
|
||||
processForm.message = ''
|
||||
processForm.files = []
|
||||
// 重新加载处理记录
|
||||
if (workcase.workcaseId) {
|
||||
await loadProcessList(workcase.workcaseId)
|
||||
}
|
||||
} else {
|
||||
uni.showToast({ title: res.message || '添加失败', icon: 'none' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加处理记录失败:', error)
|
||||
uni.showToast({ title: '添加失败', icon: 'none' })
|
||||
} finally {
|
||||
submittingProcess.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
|
||||
@@ -320,16 +320,15 @@ export interface MarkReadParam {
|
||||
*/
|
||||
export interface TbCustomerServiceDTO extends BaseDTO {
|
||||
userId?: string
|
||||
userName?: string
|
||||
username?: string
|
||||
userCode?: string
|
||||
status?: string
|
||||
maxConcurrentChats?: number
|
||||
currentChatCount?: number
|
||||
totalServedCount?: number
|
||||
skillTags?: string[]
|
||||
maxConcurrent?: number
|
||||
currentWorkload?: number
|
||||
totalServed?: number
|
||||
avgResponseTime?: number
|
||||
avgRating?: number
|
||||
skills?: string[]
|
||||
priority?: number
|
||||
lastOnlineTime?: string
|
||||
satisfactionScore?: number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -337,19 +336,17 @@ export interface TbCustomerServiceDTO extends BaseDTO {
|
||||
*/
|
||||
export interface CustomerServiceVO extends BaseVO {
|
||||
userId?: string
|
||||
userName?: string
|
||||
userAvatar?: string
|
||||
username?: string
|
||||
userCode?: string
|
||||
avatar?: string
|
||||
status?: string
|
||||
statusName?: string
|
||||
maxConcurrentChats?: number
|
||||
currentChatCount?: number
|
||||
totalServedCount?: number
|
||||
skillTags?: string[]
|
||||
maxConcurrent?: number
|
||||
currentWorkload?: number
|
||||
totalServed?: number
|
||||
avgResponseTime?: number
|
||||
avgResponseTimeFormatted?: string
|
||||
avgRating?: number
|
||||
skills?: string[]
|
||||
skillNames?: string[]
|
||||
priority?: number
|
||||
lastOnlineTime?: string
|
||||
satisfactionScore?: number
|
||||
isAvailable?: boolean
|
||||
}
|
||||
Reference in New Issue
Block a user