web工单处理

This commit is contained in:
2025-12-28 13:18:28 +08:00
parent 1148e3368d
commit 7de30b1b36
12 changed files with 1209 additions and 95 deletions

View File

@@ -158,6 +158,15 @@
@cancel="showDetailDialog = false"
/>
</el-dialog>
<!-- 指派工单弹窗 -->
<WorkcaseAssign
v-model="showAssignDialog"
:workcase-id="assignWorkcaseId"
:current-processor="assignCurrentProcessor"
action="assign"
@success="handleAssignSuccess"
/>
</AdminLayout>
</template>
@@ -168,6 +177,7 @@ import { Plus, Search } from 'lucide-vue-next'
import { ElMessage, ElMessageBox } from 'element-plus'
import { workcaseAPI } from '@/api/workcase'
import WorkcaseDetail from '@/views/public/workcase/WorkcaseDetail/WorkcaseDetail.vue'
import { WorkcaseAssign } from '@/components'
import type { TbWorkcaseDTO, TbWorkcaseProcessDTO } from '@/types/workcase'
import type { PageRequest, PageParam } from 'shared/types'
@@ -183,6 +193,11 @@ const showDetailDialog = ref(false)
const currentWorkcase = ref<TbWorkcaseDTO>({})
const loading = ref(false)
// 指派弹窗相关
const showAssignDialog = ref(false)
const assignWorkcaseId = ref('')
const assignCurrentProcessor = ref('')
const formData = ref<TbWorkcaseDTO>({
username: '',
phone: '',
@@ -254,30 +269,19 @@ const createWorkcaseAPI = async () => {
}
/**
* 指派工单 - API调用
* 指派工单 - 打开指派弹窗
*/
const assignWorkcaseAPI = async (workcase: TbWorkcaseDTO) => {
const { value: processor } = await ElMessageBox.prompt('请输入处理人ID', '指派工单', {
confirmButtonText: '确定',
cancelButtonText: '取消'
}).catch(() => ({ value: '' }))
if (!processor) return
const process: TbWorkcaseProcessDTO = {
workcaseId: workcase.workcaseId,
action: 'assign',
processor: processor,
message: '工单指派'
}
const res = await workcaseAPI.createWorkcaseProcess(process)
if (res.success) {
ElMessage.success('指派成功')
loadWorkcases()
} else {
ElMessage.error(res.message || '指派失败')
}
assignWorkcaseId.value = workcase.workcaseId || ''
assignCurrentProcessor.value = workcase.processor || ''
showAssignDialog.value = true
}
/**
* 指派成功回调
*/
const handleAssignSuccess = () => {
loadWorkcases()
}
/**

View File

@@ -170,11 +170,17 @@
.section-title {
display: flex;
align-items: center;
gap: 8px;
justify-content: space-between;
margin-bottom: 16px;
color: #111827;
font-weight: 700;
font-size: 16px;
.title-left {
display: flex;
align-items: center;
gap: 8px;
}
}
.title-icon {
@@ -303,6 +309,52 @@
margin-bottom: 4px;
}
.timeline-files {
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed #e5e7eb;
.files-label {
font-size: 12px;
color: #9ca3af;
margin-bottom: 4px;
}
.files-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.file-link {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: #f3f4f6;
border-radius: 4px;
font-size: 12px;
color: #4b87ff;
text-decoration: none;
transition: background 0.2s;
&:hover {
background: #e5e7eb;
}
.file-icon {
font-size: 14px;
}
.file-name {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.timeline-time {
font-size: 12px;
color: #9ca3af;

View File

@@ -145,8 +145,14 @@
<!-- 处理记录 (仅查看模式) -->
<div class="timeline-section" v-if="mode === 'view' && processList.length > 0">
<div class="section-title">
<div class="title-bar"></div>
处理记录
<div class="title-left">
<div class="title-bar"></div>
处理记录
</div>
<!-- 工单处理人记录工单过程-->
<ElButton v-if="isProcessor" type="primary" size="small" @click="showAddProcessDialog = true">
添加处理记录
</ElButton>
</div>
<div class="timeline-content">
<div v-for="(item, index) in processList" :key="index" class="timeline-item">
@@ -161,6 +167,22 @@
<span class="timeline-action">{{ getActionText(item.action) }}:</span>
<span class="timeline-desc">{{ item.message }}</span>
</div>
<!-- 附件列表 -->
<div v-if="item.files && item.files.length > 0" class="timeline-files">
<div class="files-label">附件</div>
<div class="files-list">
<a
v-for="fileId in item.files"
:key="fileId"
:href="getFileDownloadUrl(fileId)"
target="_blank"
class="file-link"
>
<span class="file-icon">{{ getFileIcon(fileId) }}</span>
<span class="file-name">{{ getFileName(fileId) }}</span>
</a>
</div>
</div>
</div>
</div>
</div>
@@ -171,25 +193,100 @@
<footer class="detail-footer">
<ElButton @click="handleCancel">{{ mode === 'view' ? '关闭' : '取消' }}</ElButton>
<ElButton v-if="mode === 'create'" type="primary" @click="handleSubmit">创建工单</ElButton>
<ElButton v-if="mode === 'edit'" type="primary" @click="handleSubmit">保存修改</ElButton>
<ElButton v-if="mode === 'view' && formData.status === 'pending'" type="warning" @click="handleAssign">指派工程师</ElButton>
<ElButton v-if="mode === 'view' && formData.status === 'processing'" type="success" @click="handleComplete">完成工单</ElButton>
<!-- 来客视角只能撤销自己创建的工单 -->
<ElButton
v-if="mode === 'view' && isCreator && formData.status !== 'done'"
type="danger"
>
撤销工单
</ElButton>
<!-- 员工视角可以指派转派完成工单 -->
<ElButton
v-if="mode === 'view' && !isCreator && formData.status === 'pending'"
type="warning"
@click="handleAssign"
>
指派工程师
</ElButton>
<ElButton
v-if="mode === 'view' && !isCreator && formData.status === 'processing'"
type="warning"
@click="handleRedeploy"
>
转派工程师
</ElButton>
<ElButton
v-if="mode === 'view' && !isCreator && formData.status === 'processing'"
type="success"
@click="handleComplete"
>
完成工单
</ElButton>
</footer>
<!-- ChatMessage Dialog -->
<ElDialog v-model="showChatMessage" title="对话详情" width="800px" class="chat-dialog">
<ChatMessage v-if="showChatMessage" :room-id="currentRoomId" />
</ElDialog>
<!-- Add Process Record Dialog -->
<ElDialog v-model="showAddProcessDialog" title="添加处理记录" width="600px">
<div class="add-process-form">
<div class="form-item">
<label class="form-label">处理内容</label>
<ElInput
v-model="processForm.message"
type="textarea"
:rows="4"
placeholder="请详细描述处理过程、发现的问题或解决方案..."
maxlength="500"
show-word-limit
/>
</div>
<div class="form-item" style="margin-top: 16px;">
<label class="form-label">附件可选</label>
<FileUpload
ref="processFileUploadRef"
mode="content"
:max-count="5"
:max-size="10 * 1024 * 1024"
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx"
v-model:file-list="processUploadedFiles"
@upload-success="handleProcessUploadSuccess"
@upload-error="handleProcessUploadError"
/>
</div>
</div>
<template #footer>
<ElButton @click="handleCloseProcessDialog">取消</ElButton>
<ElButton type="primary" @click="submitProcessRecord" :loading="submittingProcess">提交</ElButton>
</template>
</ElDialog>
<!-- Assign/Redeploy Dialog -->
<WorkcaseAssign
v-model="showAssignDialog"
:workcase-id="formData.workcaseId || ''"
:current-processor="formData.processor || ''"
:action="assignAction"
@success="handleAssignSuccess"
/>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import { ref, watch, onMounted, computed } from 'vue'
import { ChatMessage } from '@/views/public/ChatRoom/'
import { ElButton, ElInput, ElSelect, ElOption, ElDialog, ElMessage, ElLoading } from 'element-plus'
import { MessageSquare, ImageIcon as ImageIcon, Plus } from 'lucide-vue-next'
import type { TbWorkcaseDTO, TbWorkcaseProcessDTO } from '@/types/workcase/workcase'
import type { TbSysFileDTO } from 'shared/types'
import { workcaseAPI } from '@/api/workcase'
import { fileAPI } from 'shared/api/file'
import { FileUpload } from 'shared/components'
import { WorkcaseAssign } from '@/components'
import { FILE_DOWNLOAD_URL } from '@/config'
interface Props {
@@ -226,6 +323,35 @@ const showChatMessage = ref(false)
const currentRoomId = ref<string>('')
const processList = ref<TbWorkcaseProcessDTO[]>([])
// 文件信息缓存 (fileId -> TbSysFileDTO)
const fileInfoCache = ref<Map<string, TbSysFileDTO>>(new Map())
// 当前登录用户ID
const userId = ref<string>('')
// 判断是否是处理人
const isProcessor = computed(() => {
return formData.value.processor === userId.value
})
// 判断是否是创建人
const isCreator = computed(() => {
return formData.value.creator === userId.value
})
// 添加处理记录相关
const showAddProcessDialog = ref(false)
const submittingProcess = ref(false)
const processForm = ref({
message: '',
fileIds: [] as string[]
})
// 已上传的文件列表(用于显示)
const processUploadedFiles = ref<TbSysFileDTO[]>([])
// 指派/转派相关
const showAssignDialog = ref(false)
const assignAction = ref<'assign' | 'redeploy'>('assign')
// 加载工单详情
const loadWorkcaseDetail = async (workcaseId: string) => {
if (!workcaseId) return
@@ -255,12 +381,54 @@ const loadProcessList = async (workcaseId: string) => {
const res = await workcaseAPI.getWorkcaseProcessList({ workcaseId })
if (res.success && res.dataList) {
processList.value = res.dataList
// 加载所有文件信息
await loadFilesInfo(res.dataList)
}
} catch (error) {
console.error('加载处理记录失败:', error)
}
}
// 加载处理记录中的文件信息
const loadFilesInfo = async (processes: TbWorkcaseProcessDTO[]) => {
// 收集所有文件ID
const fileIds: string[] = []
processes.forEach(p => {
if (p.files) {
// 处理 files 可能是字符串或数组的情况
const filesArray = Array.isArray(p.files) ? p.files : [p.files]
filesArray.forEach(id => {
if (id && !fileInfoCache.value.has(id)) {
fileIds.push(id)
}
})
}
})
console.log('需要查询的文件ID:', fileIds)
if (fileIds.length === 0) return
// 逐个查询文件信息
for (const fileId of fileIds) {
try {
console.log('查询文件信息:', fileId)
const res = await fileAPI.getFileById(fileId)
console.log('文件信息结果:', res)
if (res.success && res.data) {
fileInfoCache.value.set(fileId, res.data)
}
} catch (error) {
console.error(`加载文件信息失败: ${fileId}`, error)
}
}
}
// 获取文件信息
const getFileInfo = (fileId: string): TbSysFileDTO | undefined => {
return fileInfoCache.value.get(fileId)
}
// 获取时间线圆点样式类
const getTimelineDotClass = (action?: string): string => {
switch (action) {
@@ -312,8 +480,50 @@ function getImageUrl(fileId: string): string {
return `${FILE_DOWNLOAD_URL}${fileId}`
}
// 获取文件下载URL
function getFileDownloadUrl(fileId: string): string {
if (!fileId) return ''
return `${FILE_DOWNLOAD_URL}${fileId}`
}
// 获取文件名
function getFileName(fileId: string): string {
const fileInfo = fileInfoCache.value.get(fileId)
return fileInfo?.name || fileId.substring(0, 8) + '...'
}
// 获取文件图标
function getFileIcon(fileId: string): string {
const fileInfo = fileInfoCache.value.get(fileId)
if (!fileInfo) return '📎'
const ext = (fileInfo.extension || fileInfo.name?.split('.').pop() || '').toLowerCase()
const iconMap: Record<string, string> = {
jpg: '🖼️',
jpeg: '🖼️',
png: '🖼️',
gif: '🖼️',
webp: '🖼️',
pdf: '📄',
doc: '📝',
docx: '📝',
xls: '📊',
xlsx: '📊',
ppt: '📽️',
pptx: '📽️',
zip: '📦',
rar: '📦',
txt: '📃'
}
return iconMap[ext] || '📎'
}
// 初始化
onMounted(() => {
// 获取当前登录用户ID
const loginDomain = JSON.parse(localStorage.getItem('loginDomain') || '{}')
userId.value = loginDomain?.userInfo?.userId || ''
if (props.mode === 'view' || props.mode === 'edit') {
// 查看/编辑模式:通过 workcaseId 加载数据
if (props.workcaseId) {
@@ -328,12 +538,11 @@ onMounted(() => {
}
} else if (props.mode === 'create') {
// 创建模式:初始化空表单,设置 roomId
const loginDomain = JSON.parse(localStorage.getItem('loginDomain') || '{}')
formData.value = {
roomId: props.roomId,
username: loginDomain?.userInfo?.username || '',
phone: loginDomain?.user?.phone || '',
userId: loginDomain?.user?.userId || '',
userId: userId.value,
emergency: 'normal',
imgs: []
}
@@ -455,12 +664,100 @@ const handleSubmit = async () => {
}
const handleAssign = () => {
emit('assign', formData.value.workcaseId!)
assignAction.value = 'assign'
showAssignDialog.value = true
}
const handleComplete = () => {
emit('complete', formData.value.workcaseId!)
}
// 处理转派
const handleRedeploy = () => {
assignAction.value = 'redeploy'
showAssignDialog.value = true
}
// 指派/转派成功回调
const handleAssignSuccess = async () => {
// 重新加载工单详情
if (formData.value.workcaseId) {
await loadWorkcaseDetail(formData.value.workcaseId)
}
// 触发事件通知父组件
if (assignAction.value === 'assign') {
emit('assign', formData.value.workcaseId!)
}
}
// 处理记录文件上传组件引用
const processFileUploadRef = ref<InstanceType<typeof FileUpload>>()
// 处理记录上传成功回调
const handleProcessUploadSuccess = (files: TbSysFileDTO[]) => {
console.log('文件上传成功:', files)
}
// 处理记录上传失败回调
const handleProcessUploadError = (error: string) => {
ElMessage.error(error)
}
// 关闭处理记录弹窗
const handleCloseProcessDialog = () => {
showAddProcessDialog.value = false
processForm.value = { message: '', fileIds: [] }
processUploadedFiles.value = []
if (processFileUploadRef.value) {
processFileUploadRef.value.clearFiles()
}
}
// 提交处理记录
const submitProcessRecord = async () => {
if (!processForm.value.message.trim()) {
ElMessage.warning('请输入处理内容')
return
}
if (!formData.value.workcaseId) {
ElMessage.error('工单ID不存在')
return
}
submittingProcess.value = true
try {
// 从已上传文件列表获取文件ID
const fileIds = processUploadedFiles.value
.map(f => f.fileId)
.filter((id): id is string => !!id)
// 提交处理记录
const params: TbWorkcaseProcessDTO = {
workcaseId: formData.value.workcaseId,
action: 'info',
message: processForm.value.message,
files: fileIds.length > 0 ? fileIds : undefined
}
const res = await workcaseAPI.createWorkcaseProcess(params)
if (res.success) {
ElMessage.success('处理记录添加成功')
handleCloseProcessDialog()
// 重新加载处理记录
if (formData.value.workcaseId) {
await loadProcessList(formData.value.workcaseId)
}
} else {
ElMessage.error(res.message || '添加失败')
}
} catch (error) {
console.error('添加处理记录失败:', error)
ElMessage.error('添加处理记录失败')
} finally {
submittingProcess.value = false
}
}
</script>
<style scoped lang="scss">