diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java index 758a1814..0d56f4b2 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java @@ -380,6 +380,7 @@ public class WorkcaseServiceImpl implements WorkcaseService { workcase.setStatus("cancelled"); workcaseMapper.updateWorkcase(workcase); } + workcaseProcess.setCreator(LoginUtil.getCurrentUserId()); int rows = workcaseProcessMapper.insertWorkcaseProcess(workcaseProcess); if (rows > 0) { diff --git a/urbanLifelineWeb/packages/shared/src/api/file/file.ts b/urbanLifelineWeb/packages/shared/src/api/file/file.ts index 8f88500a..4ee8bd36 100644 --- a/urbanLifelineWeb/packages/shared/src/api/file/file.ts +++ b/urbanLifelineWeb/packages/shared/src/api/file/file.ts @@ -48,6 +48,26 @@ export const fileAPI = { return response.data; }, + /** + * 根据ID获取文件信息 + * @param fileId 文件ID + * @returns Promise> + */ + async getFileById(fileId: string): Promise> { + const response = await api.get(`${this.baseUrl}/${fileId}`); + return response.data; + }, + + /** + * 批量获取文件信息 + * @param fileIds 文件ID列表 + * @returns Promise> + */ + async getFilesByIds(fileIds: string[]): Promise> { + const response = await api.post(`${this.baseUrl}/list`, { fileIds }); + return response.data; + }, + /** * 下载文件 * @param fileId 文件ID diff --git a/urbanLifelineWeb/packages/shared/src/components/file/fileupload/FileUpload.vue b/urbanLifelineWeb/packages/shared/src/components/file/fileupload/FileUpload.vue index 4234a277..8ca9daf0 100644 --- a/urbanLifelineWeb/packages/shared/src/components/file/fileupload/FileUpload.vue +++ b/urbanLifelineWeb/packages/shared/src/components/file/fileupload/FileUpload.vue @@ -98,7 +98,8 @@
-
+
@@ -109,38 +110,37 @@
{{ getUploadTip() }}
+ +
+
+
+
上传中...
+
+
+ - -
-

待上传文件:

-
+ +
+
- - {{ getFileTypeIcon(file) }} + + {{ getUploadedFileTypeIcon(file) }}
-
{{ file.name }}
-
{{ formatFileSize(file.size) }}
+
{{ file.name || file.fileName || '未知文件' }}
+
{{ file.size ? formatFileSize(file.size) : '' }}
- + 删除
- - -
- - {{ uploading ? '上传中...' : '确定上传' }} - -
@@ -203,6 +203,12 @@ const selectedFiles = ref([]) const isDragover = ref(false) const coverImageClass = ref('') +// 内部文件列表(包含本地预览URL) +interface InternalFile extends TbSysFileDTO { + localPreviewUrl?: string // 本地预览URL,避免重新下载 +} +const internalFileList = ref([]) + // 文件输入引用 const fileInputRef = ref() @@ -212,6 +218,56 @@ const currentCoverImg = computed({ set: (value: string) => emit('update:coverImg', value) }) +// 当前文件列表(优先使用内部列表,否则使用props) +const currentFileList = computed(() => { + return internalFileList.value.length > 0 ? internalFileList.value : props.fileList +}) + +// 判断已上传文件是否为图片 +const isUploadedImageFile = (file: InternalFile): boolean => { + if (file.localPreviewUrl) return true // 有本地预览说明是图片 + const mimeType = file.mimeType || file.extension || '' + return mimeType.includes('image') || /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name || '') +} + +// 获取已上传文件的显示URL(优先使用本地预览) +const getUploadedFileUrl = (file: InternalFile): string => { + if (file.localPreviewUrl) return file.localPreviewUrl + if (file.url) return file.url + if (file.fileId) return `${FILE_DOWNLOAD_URL}${file.fileId}` + return '' +} + +// 获取已上传文件的类型图标 +const getUploadedFileTypeIcon = (file: InternalFile): string => { + const ext = file.extension || file.name?.split('.').pop() || '' + const iconMap: Record = { + pdf: '📄', + doc: '📝', + docx: '📝', + xls: '📊', + xlsx: '📊', + ppt: '📽️', + pptx: '📽️', + zip: '📦', + rar: '📦', + txt: '📃' + } + return iconMap[ext.toLowerCase()] || '📎' +} + +// 删除已上传的文件 +const removeUploadedFile = (index: number) => { + const newList = [...internalFileList.value] + const removed = newList.splice(index, 1)[0] + // 释放本地预览URL + if (removed?.localPreviewUrl) { + URL.revokeObjectURL(removed.localPreviewUrl) + } + internalFileList.value = newList + emit('update:fileList', newList) +} + // 触发文件输入 const triggerFileInput = () => { fileInputRef.value?.click() @@ -292,18 +348,23 @@ const addFiles = (files: File[]) => { return } - // 检查是否重复 + // 检查是否重复(包括已上传的文件) if (selectedFiles.value.some(f => f.name === file.name && f.size === file.size)) { emit('upload-error', `文件 ${file.name} 已添加`) return } + if (internalFileList.value.some(f => f.name === file.name)) { + emit('upload-error', `文件 ${file.name} 已上传`) + return + } // 如果不允许多选或封面模式,清空之前的文件 if (props.maxCount === 1 || props.mode === 'cover') { selectedFiles.value = [file] } else { - // 检查数量限制 - if (selectedFiles.value.length >= props.maxCount) { + // 检查数量限制(已上传 + 待上传) + const totalCount = internalFileList.value.length + selectedFiles.value.length + if (totalCount >= props.maxCount) { emit('upload-error', `最多只能上传 ${props.maxCount} 个文件`) return } @@ -391,7 +452,15 @@ const uploadFiles = async () => { // 默认上传逻辑 uploading.value = true - const uploadedFilesList: TbSysFileDTO[] = [] + const uploadedFilesList: InternalFile[] = [] + + // 为图片文件创建本地预览URL的映射 + const localPreviewMap = new Map() + selectedFiles.value.forEach(file => { + if (file.type.startsWith('image/')) { + localPreviewMap.set(file.name, URL.createObjectURL(file)) + } + }) try { @@ -404,12 +473,19 @@ const uploadFiles = async () => { }) if (result.success && result.data) { - uploadedFilesList.push(result.data) + // 添加本地预览URL + const uploadedFile: InternalFile = { + ...result.data, + localPreviewUrl: localPreviewMap.get(file.name) + } + uploadedFilesList.push(uploadedFile) if (props.mode === 'cover' && result.data.url) { emit('update:coverImg', result.data.url) } } else { + // 释放预览URL + localPreviewMap.forEach(url => URL.revokeObjectURL(url)) emit('upload-error', result.message || '上传失败,请重试') return } @@ -422,20 +498,33 @@ const uploadFiles = async () => { if (result.success) { const files = result.dataList || [] - uploadedFilesList.push(...files) + // 为每个上传成功的文件添加本地预览URL + files.forEach(f => { + const uploadedFile: InternalFile = { + ...f, + localPreviewUrl: localPreviewMap.get(f.name || '') + } + uploadedFilesList.push(uploadedFile) + }) } else { + // 释放预览URL + localPreviewMap.forEach(url => URL.revokeObjectURL(url)) emit('upload-error', result.message || '上传失败,请重试') return } } - // 上传成功 + // 上传成功 - 更新内部文件列表 if (uploadedFilesList.length > 0) { + // content模式:追加到内部列表 + if (props.mode === 'content') { + internalFileList.value = [...internalFileList.value, ...uploadedFilesList] + } emit('upload-success', uploadedFilesList) emit('update:fileList', [...props.fileList, ...uploadedFilesList]) } - // 清空文件列表 + // 清空待上传文件列表 selectedFiles.value = [] if (props.mode === 'dialog') { closeDialog() @@ -453,6 +542,8 @@ const uploadFiles = async () => { defineExpose({ // 当前选中的文件列表 selectedFiles, + // 已上传的文件列表 + internalFileList, // 上传状态 uploading, // 弹窗显示状态 @@ -463,8 +554,17 @@ defineExpose({ addFiles, // 移除指定文件 removeFile, - // 清空所有文件 - clearFiles: () => { selectedFiles.value = [] }, + // 清空所有文件(包括已上传的) + clearFiles: () => { + // 释放本地预览URL + internalFileList.value.forEach(f => { + if (f.localPreviewUrl) { + URL.revokeObjectURL(f.localPreviewUrl) + } + }) + selectedFiles.value = [] + internalFileList.value = [] + }, // 手动触发上传 uploadFiles, // 关闭弹窗 diff --git a/urbanLifelineWeb/packages/workcase/src/components/index.ts b/urbanLifelineWeb/packages/workcase/src/components/index.ts index e69de29b..0fdcf4b2 100644 --- a/urbanLifelineWeb/packages/workcase/src/components/index.ts +++ b/urbanLifelineWeb/packages/workcase/src/components/index.ts @@ -0,0 +1 @@ +export { default as WorkcaseAssign } from './workcase/WorkcaseAssign.vue' diff --git a/urbanLifelineWeb/packages/workcase/src/components/workcase/WorkcaseAssign.vue b/urbanLifelineWeb/packages/workcase/src/components/workcase/WorkcaseAssign.vue new file mode 100644 index 00000000..14be9563 --- /dev/null +++ b/urbanLifelineWeb/packages/workcase/src/components/workcase/WorkcaseAssign.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/urbanLifelineWeb/packages/workcase/src/config/index.ts b/urbanLifelineWeb/packages/workcase/src/config/index.ts index ed2be281..8d5eab0d 100644 --- a/urbanLifelineWeb/packages/workcase/src/config/index.ts +++ b/urbanLifelineWeb/packages/workcase/src/config/index.ts @@ -212,6 +212,7 @@ console.log('[配置] 当前配置', config); // 单独导出常用配置项 export const API_BASE_URL = config.api.baseUrl; export const FILE_DOWNLOAD_URL = config.file.downloadUrl; +export const FILE_UPLOAD_URL = config.file.uploadUrl; export const PUBLIC_IMG_PATH = config.publicImgPath; export const PUBLIC_WEB_PATH = config.publicWebPath; diff --git a/urbanLifelineWeb/packages/workcase/src/types/shared.d.ts b/urbanLifelineWeb/packages/workcase/src/types/shared.d.ts index e7bf387f..2d4c54cf 100644 --- a/urbanLifelineWeb/packages/workcase/src/types/shared.d.ts +++ b/urbanLifelineWeb/packages/workcase/src/types/shared.d.ts @@ -61,8 +61,6 @@ declare module 'shared/api' { export const api: ApiInstance export const TokenManager: any - export const authAPI: any - export const fileAPI: any } declare module 'shared/api/auth' { diff --git a/urbanLifelineWeb/packages/workcase/src/views/admin/workcase/WorkcaseView.vue b/urbanLifelineWeb/packages/workcase/src/views/admin/workcase/WorkcaseView.vue index f8462fb0..10b760c5 100644 --- a/urbanLifelineWeb/packages/workcase/src/views/admin/workcase/WorkcaseView.vue +++ b/urbanLifelineWeb/packages/workcase/src/views/admin/workcase/WorkcaseView.vue @@ -158,6 +158,15 @@ @cancel="showDetailDialog = false" /> + + + @@ -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({}) const loading = ref(false) +// 指派弹窗相关 +const showAssignDialog = ref(false) +const assignWorkcaseId = ref('') +const assignCurrentProcessor = ref('') + const formData = ref({ 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() } /** diff --git a/urbanLifelineWeb/packages/workcase/src/views/public/workcase/WorkcaseDetail/WorkcaseDetail.scss b/urbanLifelineWeb/packages/workcase/src/views/public/workcase/WorkcaseDetail/WorkcaseDetail.scss index b174d843..224ed31b 100644 --- a/urbanLifelineWeb/packages/workcase/src/views/public/workcase/WorkcaseDetail/WorkcaseDetail.scss +++ b/urbanLifelineWeb/packages/workcase/src/views/public/workcase/WorkcaseDetail/WorkcaseDetail.scss @@ -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; diff --git a/urbanLifelineWeb/packages/workcase/src/views/public/workcase/WorkcaseDetail/WorkcaseDetail.vue b/urbanLifelineWeb/packages/workcase/src/views/public/workcase/WorkcaseDetail/WorkcaseDetail.vue index 09350021..0b2a50a0 100644 --- a/urbanLifelineWeb/packages/workcase/src/views/public/workcase/WorkcaseDetail/WorkcaseDetail.vue +++ b/urbanLifelineWeb/packages/workcase/src/views/public/workcase/WorkcaseDetail/WorkcaseDetail.vue @@ -145,8 +145,14 @@
-
- 处理记录 +
+
+ 处理记录 +
+ + + 添加处理记录 +
@@ -161,6 +167,22 @@ {{ getActionText(item.action) }}: {{ item.message }}
+ +
@@ -171,25 +193,100 @@
{{ mode === 'view' ? '关闭' : '取消' }} 创建工单 - 保存修改 - 指派工程师 - 完成工单 + + + + 撤销工单 + + + + + 指派工程师 + + + 转派工程师 + + + 完成工单 +
+ + + +
+
+ + +
+
+ + +
+
+ +
+ + +