微信小程序工单处理

This commit is contained in:
2025-12-28 13:35:10 +08:00
parent 7de30b1b36
commit 7eddf00705
3 changed files with 475 additions and 7 deletions

View File

@@ -60,7 +60,7 @@ export const fileAPI = {
* @returns Promise<ResultDomain<TbSysFileDTO>>
*/
getFileById(fileId: string): Promise<ResultDomain<TbSysFileDTO>> {
return request<TbSysFileDTO>({ url: `${BASE_URL}${this.baseUrl}/${fileId}`, method: 'GET' })
return request<TbSysFileDTO>({ url: `${this.baseUrl}/${fileId}`, method: 'GET' })
},
/**

View File

@@ -534,6 +534,50 @@
margin-left: 8rpx;
}
// 处理记录附件
.timeline-files {
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx dashed #e5e7eb;
}
.files-label {
font-size: 24rpx;
color: #9ca3af;
margin-bottom: 8rpx;
display: block;
}
.files-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 12rpx;
}
.file-link {
display: flex;
flex-direction: row;
align-items: center;
gap: 8rpx;
padding: 8rpx 16rpx;
background: #f3f4f6;
border-radius: 8rpx;
}
.file-icon {
font-size: 28rpx;
}
.file-name {
font-size: 24rpx;
color: #4b87ff;
max-width: 200rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// 底部占位
.footer-placeholder {
height: 120rpx;
@@ -588,3 +632,210 @@
color: #6b7280;
font-weight: 500;
}
// 弹窗样式
.process-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.process-modal {
width: 90%;
max-width: 600rpx;
background: #fff;
border-radius: 16rpx;
overflow: hidden;
}
.modal-header {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 32rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.modal-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.modal-close {
position: absolute;
right: 24rpx;
top: 50%;
transform: translateY(-50%);
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 40rpx;
color: #999;
line-height: 1;
}
}
.modal-body {
padding: 24rpx;
max-height: 60vh;
overflow-y: auto;
}
.modal-footer {
display: flex;
flex-direction: row;
border-top: 1rpx solid #f0f0f0;
}
.modal-btn {
flex: 1;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: #666;
background: #fff;
border: none;
border-radius: 0;
&.primary {
color: #fff;
background: #4b87ff;
}
&::after {
border: none;
}
}
// picker 箭头样式
.picker-arrow {
font-size: 28rpx;
color: #9ca3af;
margin-left: 16rpx;
}
.picker-arrow-icon {
width: 16rpx;
height: 16rpx;
border-right: 4rpx solid #9ca3af;
border-bottom: 4rpx solid #9ca3af;
transform: rotate(-45deg);
margin-left: 16rpx;
flex-shrink: 0;
}
.picker-text.placeholder {
color: #9ca3af;
}
// 加载提示
.loading-tip {
padding: 16rpx 0;
text {
font-size: 24rpx;
color: #9ca3af;
}
}
// 字数统计
.char-count {
display: block;
text-align: right;
font-size: 22rpx;
color: #9ca3af;
margin-top: 8rpx;
}
// 文件上传区域
.file-upload-section {
margin-top: 16rpx;
}
.file-list {
margin-bottom: 16rpx;
}
.file-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 16rpx;
background: #f9fafb;
border-radius: 8rpx;
margin-bottom: 8rpx;
}
.file-item .file-name {
flex: 1;
font-size: 26rpx;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-delete {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 32rpx;
color: #999;
}
}
.upload-btn {
font-size: 24rpx;
}
// 添加处理记录按钮区域
.add-process-section {
display: flex;
justify-content: flex-end;
margin-bottom: 16rpx;
}
.add-process-btn {
font-size: 24rpx;
}
// 危险/警告按钮
.action-button.danger {
background: #ef4444;
border-color: #ef4444;
.button-text {
color: #fff;
}
}
.action-button.warning {
background: #f59e0b;
border-color: #f59e0b;
.button-text {
color: #fff;
}
}

View File

@@ -184,8 +184,20 @@
<text class="timeline-action" >{{ getActionText(item.action) }}:</text>
<text class="timeline-desc">{{ item.message }}</text>
</view>
<view class="timeline-files">
<!-- 附件列表 -->
<view class="timeline-files" v-if="item.files && item.files.length > 0">
<text class="files-label">附件:</text>
<view class="files-list">
<view
class="file-link"
v-for="fileId in item.files"
:key="fileId"
@tap="previewOrDownloadFile(fileId)"
>
<text class="file-icon">{{ getFileIcon(fileId) }}</text>
<text class="file-name">{{ getFileName(fileId) }}</text>
</view>
</view>
</view>
</view>
</view>
@@ -312,7 +324,7 @@
<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 class="picker-arrow-icon"></view>
</view>
</picker>
<view v-if="loadingEngineers" class="loading-tip">
@@ -328,6 +340,28 @@
maxlength="200"
/>
</view>
<view class="form-item">
<text class="form-label">附件(可选)</text>
<view class="file-upload-section">
<view class="file-list" v-if="assignForm.files.length > 0">
<view class="file-item" v-for="(file, index) in assignForm.files" :key="index">
<text class="file-name">{{ file.name }}</text>
<view class="file-delete" @tap="deleteAssignFile(index)">
<text>×</text>
</view>
</view>
</view>
<button
class="upload-btn"
size="mini"
@tap="chooseAssignFile"
v-if="assignForm.files.length < 5"
>
选择文件
</button>
<text class="upload-tip">最多上传5个文件</text>
</view>
</view>
</view>
<view class="modal-footer">
<button class="modal-btn" @tap="closeAssignDialog">取消</button>
@@ -369,6 +403,9 @@ const workcase = reactive<TbWorkcaseDTO>({})
// 处理记录
const processList = reactive<TbWorkcaseProcessDTO[]>([])
// 文件信息缓存
const fileInfoCache = reactive<Map<string, any>>(new Map())
// 判断是否是处理人
const isProcessor = computed(() => {
return workcase.processor === userId
@@ -398,7 +435,8 @@ const selectedEngineerIndex = ref(-1)
const assignForm = reactive({
processor: '',
processorName: '',
message: ''
message: '',
files: [] as Array<{ name: string; fileId: string }>
})
// 获取图片 URL通过 fileId
@@ -513,12 +551,121 @@ async function loadProcessList(workcaseId: string) {
processList.length = 0
processList.push(...res.dataList)
console.log('处理记录:', processList)
// 加载文件信息
await loadFilesInfo(res.dataList)
}
} catch (error) {
console.error('加载处理记录失败:', error)
}
}
// 加载处理记录中的文件信息
async function loadFilesInfo(processes: TbWorkcaseProcessDTO[]) {
// 收集所有文件ID
const fileIds: string[] = []
processes.forEach(p => {
if (p.files) {
const filesArray = Array.isArray(p.files) ? p.files : [p.files]
filesArray.forEach(id => {
if (id && !fileInfoCache.has(id)) {
fileIds.push(id)
}
})
}
})
if (fileIds.length === 0) return
// 逐个查询文件信息
for (const fileId of fileIds) {
try {
const res = await fileAPI.getFileById(fileId)
if (res.success && res.data) {
fileInfoCache.set(fileId, res.data)
}
} catch (error) {
console.error(`加载文件信息失败: ${fileId}`, error)
}
}
}
// 获取文件名
function getFileName(fileId: string): string {
const fileInfo = fileInfoCache.get(fileId)
return fileInfo?.name || fileId.substring(0, 8) + '...'
}
// 获取文件图标
function getFileIcon(fileId: string): string {
const fileInfo = fileInfoCache.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: '📊'
}
return iconMap[ext] || '📎'
}
// 预览或下载文件
function previewOrDownloadFile(fileId: string) {
const fileInfo = fileInfoCache.get(fileId)
const fileUrl = fileAPI.getDownloadUrl(fileId)
// 如果是图片,预览
if (fileInfo) {
const ext = (fileInfo.extension || fileInfo.name?.split('.').pop() || '').toLowerCase()
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) {
uni.previewImage({
urls: [fileUrl],
current: 0
})
return
}
}
// 其他文件,打开文档或复制链接
uni.showActionSheet({
itemList: ['复制下载链接', '打开文档'],
success: (res) => {
if (res.tapIndex === 0) {
uni.setClipboardData({
data: fileUrl,
success: () => {
uni.showToast({ title: '链接已复制', icon: 'success' })
}
})
} else if (res.tapIndex === 1) {
uni.downloadFile({
url: fileUrl,
success: (downloadRes) => {
if (downloadRes.statusCode === 200) {
uni.openDocument({
filePath: downloadRes.tempFilePath,
fail: () => {
uni.showToast({ title: '无法打开此文件', icon: 'none' })
}
})
}
},
fail: () => {
uni.showToast({ title: '下载失败', icon: 'none' })
}
})
}
}
})
}
function getTime(datetime: string): string{
return datetime.split(" ")[1]
}
@@ -646,6 +793,7 @@ async function openAssignDialog() {
assignForm.processor = ''
assignForm.processorName = ''
assignForm.message = ''
assignForm.files = []
selectedEngineerIndex.value = -1
showAssignDialog.value = true
await loadAvailableEngineers()
@@ -656,6 +804,74 @@ function closeAssignDialog() {
showAssignDialog.value = false
}
// 选择指派/转派附件
function chooseAssignFile() {
uni.chooseMessageFile({
count: 5 - assignForm.files.length,
type: 'all',
success: async (res) => {
uni.showLoading({ title: '上传中...' })
try {
for (const file of res.tempFiles) {
const uploadRes = await fileAPI.uploadFile(file.path, {
module: 'workcase',
optsn: workcase.workcaseId || 'temp'
})
if (uploadRes.success && uploadRes.data?.fileId) {
assignForm.files.push({
name: file.name,
fileId: uploadRes.data.fileId
})
}
}
uni.hideLoading()
uni.showToast({ title: '上传成功', icon: 'success' })
} catch (error) {
uni.hideLoading()
console.error('上传文件失败:', error)
uni.showToast({ title: '上传失败', icon: 'none' })
}
},
fail: () => {
// 如果 chooseMessageFile 不支持,尝试使用 chooseImage
uni.chooseImage({
count: 5 - assignForm.files.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (imgRes) => {
uni.showLoading({ title: '上传中...' })
try {
for (const filePath of imgRes.tempFilePaths) {
const uploadRes = await fileAPI.uploadFile(filePath, {
module: 'workcase',
optsn: workcase.workcaseId || 'temp'
})
if (uploadRes.success && uploadRes.data?.fileId) {
const fileName = filePath.split('/').pop() || '图片'
assignForm.files.push({
name: fileName,
fileId: uploadRes.data.fileId
})
}
}
uni.hideLoading()
uni.showToast({ title: '上传成功', icon: 'success' })
} catch (error) {
uni.hideLoading()
console.error('上传文件失败:', error)
uni.showToast({ title: '上传失败', icon: 'none' })
}
}
})
}
})
}
// 删除指派/转派附件
function deleteAssignFile(index: number) {
assignForm.files.splice(index, 1)
}
// 加载可用工程师列表(排除当前处理人)
async function loadAvailableEngineers() {
loadingEngineers.value = true
@@ -707,7 +923,8 @@ async function submitAssign() {
workcaseId: workcase.workcaseId,
action: assignAction.value,
processor: assignForm.processor,
message: assignForm.message || (assignAction.value === 'assign' ? '工单指派' : '工单转派')
message: assignForm.message || (assignAction.value === 'assign' ? '工单指派' : '工单转派'),
files: assignForm.files.map(f => f.fileId)
}
const res = await workcaseAPI.createWorkcaseProcess(params)