diff --git a/PREVIEW_FEATURE_GUIDE.md b/PREVIEW_FEATURE_GUIDE.md new file mode 100644 index 0000000..6f57aad --- /dev/null +++ b/PREVIEW_FEATURE_GUIDE.md @@ -0,0 +1,88 @@ +# 商务数据库文件预览功能说明 + +## 功能概述 + +商务数据库信息模块的文件预览功能已经实现,支持多种文件类型的在线预览,为用户提供便捷的文件查看体验。 + +## 支持的文件类型 + +### 1. 图片文件 +- **支持格式**: JPG, JPEG, PNG, GIF, BMP, WebP +- **预览方式**: 在模态窗口中直接显示图片 +- **特性**: + - 自适应大小,最大高度为70vh + - 圆角和阴影效果提升视觉体验 + - 显示文件名信息 + +### 2. PDF文件 +- **支持格式**: PDF +- **预览方式**: 在新窗口中打开PDF文件 +- **特性**: 利用浏览器原生PDF查看器 + +### 3. 文本文件 +- **支持格式**: TXT, MD, JSON, XML, CSV +- **预览方式**: 在模态窗口中显示文本内容 +- **特性**: + - 等宽字体显示,保持格式 + - 400px高度的滚动区域 + - 保留原文本格式和换行 + +### 4. Office文档 +- **支持格式**: DOC, DOCX, XLS, XLSX, PPT, PPTX +- **预览方式**: 提示用户下载查看 +- **说明**: 由于Office文档需要特殊渲染,建议下载后使用相应软件打开 + +### 5. 其他文件类型 +- **处理方式**: 询问用户是否下载查看 +- **说明**: 对于不支持在线预览的文件类型,系统会友好提示用户 + +## 功能特性 + +### 用户体验优化 +1. **加载提示**: 预览过程中显示"正在加载预览..."提示 +2. **错误处理**: 针对不同错误状态提供详细的错误信息 +3. **资源管理**: 自动清理预览使用的内存资源 + +### 错误处理机制 +- **404错误**: "文件不存在或已被删除" +- **403错误**: "没有权限访问该文件" +- **500错误**: "服务器内部错误,请稍后重试" +- **网络错误**: "预览文件失败,请检查网络连接" + +### 安全性 +- 使用Blob URL进行文件预览,确保安全性 +- 10秒后自动释放URL资源,防止内存泄漏 + +## 使用方法 + +1. 在文件列表中找到要预览的文件 +2. 点击文件行中的"眼睛"图标按钮 +3. 系统会根据文件类型自动选择合适的预览方式 +4. 对于不支持的文件类型,系统会提供下载选项 + +## 技术实现 + +### API接口 +- **接口地址**: `/businessData/file/preview` +- **请求方式**: GET +- **参数**: `fileId` (必需) +- **响应类型**: Blob + +### 核心函数 +- `handlePreview(file)`: 主预览函数 +- `showImagePreview(url, fileName)`: 图片预览组件 +- `showTextPreview(blob, fileName)`: 文本预览组件 + +## 注意事项 + +1. **文件大小限制**: 大文件可能需要较长加载时间 +2. **浏览器兼容性**: 确保浏览器支持Blob和URL.createObjectURL +3. **网络要求**: 稳定的网络连接确保预览效果 +4. **权限控制**: 需要有相应的文件访问权限 + +## 后续优化建议 + +1. 添加文件预览缓存机制 +2. 支持更多文件格式的在线预览 +3. 添加图片预览的缩放和旋转功能 +4. 支持文档的分页预览功能 diff --git a/src/apis/equipment/approval.ts b/src/apis/equipment/approval.ts index 7867d6e..8ce061b 100644 --- a/src/apis/equipment/approval.ts +++ b/src/apis/equipment/approval.ts @@ -1,49 +1,70 @@ import http from '@/utils/http' -import type { EquipmentApprovalReq, EquipmentApprovalResp, EquipmentApprovalListReq } from './type' +import type { EquipmentApprovalListReq, EquipmentApprovalResp } from './type' /** - * 设备审批管理API + * 设备审批API */ export const equipmentApprovalApi = { /** * 分页查询待审批的设备采购申请 */ - getPendingApprovals: (params: EquipmentApprovalListReq) => { - return http.get>>('/equipment/approval/pending', { params }) + getPendingApprovals(params: EquipmentApprovalListReq) { + return http.get('/equipment/approval/pending', params) }, /** * 分页查询已审批的设备采购申请 */ - getApprovedApprovals: (params: EquipmentApprovalListReq) => { - return http.get>>('/equipment/approval/approved', { params }) + getApprovedApprovals(params: EquipmentApprovalListReq) { + return http.get('/equipment/approval/approved', params) }, /** * 审批通过 */ - approve: (approvalId: string, data: EquipmentApprovalReq) => { - return http.post>(`/equipment/approval/${approvalId}/approve`, data) + approve(approvalId: string, data: any) { + return http.post(`/equipment/approval/${approvalId}/approve`, data) }, /** * 审批拒绝 */ - reject: (approvalId: string, data: EquipmentApprovalReq) => { - return http.post>(`/equipment/approval/${approvalId}/reject`, data) + reject(approvalId: string, data: any) { + return http.post(`/equipment/approval/${approvalId}/reject`, data) }, /** * 获取审批详情 */ - getApprovalDetail: (approvalId: string) => { - return http.get>(`/equipment/approval/${approvalId}`) + getApprovalDetail(approvalId: string) { + return http.get(`/equipment/approval/${approvalId}`) }, /** * 获取审批统计信息 */ - getApprovalStats: () => { - return http.get>('/equipment/approval/stats') + getApprovalStats() { + return http.get('/equipment/approval/stats') + }, + + /** + * 提交采购申请 + */ + submitProcurementApplication(data: any) { + return http.post('/equipment/approval/procurement/apply', data) + }, + + /** + * 获取我的采购申请 + */ + getMyProcurementApplications(params: EquipmentApprovalListReq) { + return http.get('/equipment/approval/procurement/my-applications', params) + }, + + /** + * 撤回采购申请 + */ + withdrawProcurementApplication(approvalId: string) { + return http.post(`/equipment/approval/procurement/${approvalId}/withdraw`) } } diff --git a/src/layout/components/HeaderRightBar/index.vue b/src/layout/components/HeaderRightBar/index.vue index ecc4a36..64719de 100644 --- a/src/layout/components/HeaderRightBar/index.vue +++ b/src/layout/components/HeaderRightBar/index.vue @@ -19,8 +19,13 @@ :content-style="{ marginTop: '-5px', padding: 0, border: 'none' }" :arrow-style="{ width: 0, height: 0 }" > - - + + @@ -74,9 +79,9 @@ diff --git a/src/types/env.d.ts b/src/types/env.d.ts index acbbf80..61bb774 100644 --- a/src/types/env.d.ts +++ b/src/types/env.d.ts @@ -4,6 +4,7 @@ interface ImportMetaEnv { readonly VITE_API_PREFIX: string readonly VITE_API_BASE_URL: string + readonly VITE_API_WS_URL: string readonly VITE_BASE: string readonly VITE_APP_SETTING: string readonly VITE_CLIENT_ID: string diff --git a/src/utils/http.ts b/src/utils/http.ts index 5894b18..70adfa3 100644 --- a/src/utils/http.ts +++ b/src/utils/http.ts @@ -225,7 +225,3 @@ export default { requestRaw, download, } - -export const updateContract = (contractId, contractData) => { - return http.put(`/contract/${contractId}`, contractData) -} diff --git a/src/views/bussiness-data/bussiness.vue b/src/views/bussiness-data/bussiness.vue index b8cdb6b..08a65a2 100644 --- a/src/views/bussiness-data/bussiness.vue +++ b/src/views/bussiness-data/bussiness.vue @@ -409,10 +409,12 @@ @@ -1079,9 +1081,26 @@ const handleFileChange = (info) => { console.log('文件列表:', fileList); console.log('文件列表长度:', fileList.length); - // 每次选择文件时,强制清空所有状态 - fileListTemp.value = []; - console.log('已清空之前的文件列表'); + // 检查是否是组件内部状态触发的(可能是之前的状态) + if (fileList.length === 0) { + console.log('⚠️ 文件列表为空,可能是组件内部状态,跳过处理'); + return; + } + + // 检查是否是对话框刚打开时的触发(可能是之前的状态) + if (!uploadDialogVisible.value) { + console.log('⚠️ 对话框未显示,可能是之前的状态触发,跳过处理'); + return; + } + + // 获取当前已存在的文件UID列表,用于去重 + const existingUids = fileListTemp.value.map(f => f.uid); + console.log('已存在的文件UID:', existingUids); + + // 获取当前文件夹中已存在的文件名列表,用于检查重复 + const currentFolderFiles = fileList.value || []; + const existingFileNames = currentFolderFiles.map(f => f.fileName || f.name); + console.log('当前文件夹中的文件:', existingFileNames); // 强制重置上传组件状态 if (uploadRef.value) { @@ -1093,7 +1112,7 @@ const handleFileChange = (info) => { } } - // 处理新选择的文件 - 只处理当前选择的文件 + // 处理新选择的文件 - 支持多文件选择,同时避免重复 fileList.forEach((file, index) => { console.log(`处理第${index + 1}个文件:`, file); console.log('文件名称:', file.name); @@ -1101,6 +1120,12 @@ const handleFileChange = (info) => { console.log('文件UID:', file.uid); console.log('文件对象结构:', Object.keys(file)); + // 检查文件是否已存在(去重) + if (existingUids.includes(file.uid)) { + console.log('⚠️ 文件已存在,跳过:', file.name); + return; + } + // 确保文件对象有正确的属性 const fileObj = { uid: file.uid, @@ -1114,21 +1139,28 @@ const handleFileChange = (info) => { console.log('开始验证新文件...'); + // 检查文件是否已存在于当前文件夹中 + if (existingFileNames.includes(fileObj.name)) { + fileObj.error = '文件已存在于当前文件夹中'; + console.log('⚠️ 文件已存在于文件夹中:', fileObj.name); + // 显示友好的提示信息 + Message.warning(`文件 "${fileObj.name}" 已存在于当前文件夹中,已跳过`); + return; + } + // 验证文件 const isValid = validateFile(fileObj); console.log('文件验证结果:', isValid); if (isValid) { - // 确保只添加当前选择的文件 - fileListTemp.value = [fileObj]; // 只保留当前文件 - console.log('✅ 成功设置文件到列表:', fileObj.name); + // 支持多文件:添加到列表 + fileListTemp.value.push(fileObj); + console.log('✅ 成功添加文件到列表:', fileObj.name); } else { console.log('❌ 文件验证失败:', fileObj.name, '错误:', fileObj.error); } }); - // 文件列表已在上方清空并重新填充,无需额外清理 - console.log('=== 当前文件列表状态 ==='); console.log('fileListTemp长度:', fileListTemp.value.length); console.log('fileListTemp内容:', fileListTemp.value); @@ -1415,267 +1447,26 @@ const handlePreview = async (file) => { // 显示图片预览 const showImagePreview = (url, fileName) => { - const imageScale = ref(1); - const imageRotation = ref(0); - - const container = h('div', { - style: { - width: '100%', - height: '100%', - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - padding: '24px', - boxSizing: 'border-box', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', - borderRadius: '16px', - position: 'relative', - overflow: 'hidden' - } - }, [ - // 背景装饰 - h('div', { + Modal.info({ + title: '图片预览', + content: h('div', { style: { - position: 'absolute', - top: '-50%', - left: '-50%', - width: '200%', - height: '200%', - background: 'radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%)', - animation: 'float 6s ease-in-out infinite' - } - }), - - // 顶部工具栏 - h('div', { - style: { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - gap: '16px', - marginBottom: '24px', - padding: '16px 24px', - backgroundColor: 'rgba(255, 255, 255, 0.95)', - borderRadius: '16px', - boxShadow: '0 8px 32px rgba(0,0,0,0.15)', - backdropFilter: 'blur(10px)', - border: '1px solid rgba(255,255,255,0.2)', - position: 'relative', - zIndex: 10 - } - }, [ - h('button', { - onClick: () => imageScale.value = Math.max(0.3, imageScale.value - 0.2), - style: { - padding: '10px 16px', - border: 'none', - borderRadius: '12px', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', - color: 'white', - cursor: 'pointer', - fontSize: '14px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '8px', - transition: 'all 0.3s ease', - boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)' - }, - onMouseenter: (e) => { - e.target.style.transform = 'translateY(-2px)'; - e.target.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.4)'; - }, - onMouseleave: (e) => { - e.target.style.transform = 'translateY(0)'; - e.target.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.3)'; - } - }, [ - h('span', { style: { fontSize: '18px' } }, '🔍'), - '缩小' - ]), - h('button', { - onClick: () => imageScale.value = Math.min(4, imageScale.value + 0.2), - style: { - padding: '10px 16px', - border: 'none', - borderRadius: '12px', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', - color: 'white', - cursor: 'pointer', - fontSize: '14px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '8px', - transition: 'all 0.3s ease', - boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)' - }, - onMouseenter: (e) => { - e.target.style.transform = 'translateY(-2px)'; - e.target.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.4)'; - }, - onMouseleave: (e) => { - e.target.style.transform = 'translateY(0)'; - e.target.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.3)'; - } - }, [ - h('span', { style: { fontSize: '18px' } }, '🔍'), - '放大' - ]), - h('button', { - onClick: () => imageRotation.value += 90, - style: { - padding: '10px 16px', - border: 'none', - borderRadius: '12px', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', - color: 'white', - cursor: 'pointer', - fontSize: '14px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '8px', - transition: 'all 0.3s ease', - boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)' - }, - onMouseenter: (e) => { - e.target.style.transform = 'translateY(-2px)'; - e.target.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.4)'; - }, - onMouseleave: (e) => { - e.target.style.transform = 'translateY(0)'; - e.target.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.3)'; - } - }, [ - h('span', { style: { fontSize: '18px' } }, '🔄'), - '旋转' - ]), - h('button', { - onClick: () => { - imageScale.value = 1; - imageRotation.value = 0; - }, - style: { - padding: '10px 16px', - border: 'none', - borderRadius: '12px', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', - color: 'white', - cursor: 'pointer', - fontSize: '14px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '8px', - transition: 'all 0.3s ease', - boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)' - }, - onMouseenter: (e) => { - e.target.style.transform = 'translateY(-2px)'; - e.target.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.4)'; - }, - onMouseleave: (e) => { - e.target.style.transform = 'translateY(0)'; - e.target.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.3)'; - } - }, [ - h('span', { style: { fontSize: '18px' } }, '🏠'), - '重置' - ]) - ]), - - // 图片容器 - h('div', { - style: { - flex: 1, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - overflow: 'auto', - backgroundColor: 'rgba(255, 255, 255, 0.95)', - borderRadius: '20px', - padding: '32px', - boxShadow: '0 16px 48px rgba(0,0,0,0.2)', - minHeight: '500px', - backdropFilter: 'blur(10px)', - border: '1px solid rgba(255,255,255,0.3)', - position: 'relative', - zIndex: 5 + textAlign: 'center', + padding: '20px' } }, [ h('img', { src: url, style: { maxWidth: '100%', - maxHeight: '100%', - objectFit: 'contain', - borderRadius: '16px', - transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)', - transform: `scale(${imageScale.value}) rotate(${imageRotation.value}deg)`, - boxShadow: '0 8px 24px rgba(0,0,0,0.15)', - border: '2px solid rgba(255,255,255,0.8)' + maxHeight: '70vh', + objectFit: 'contain' } }) ]), - - // 文件名 - h('div', { - style: { - marginTop: '20px', - fontSize: '16px', - color: 'white', - textAlign: 'center', - wordBreak: 'break-all', - padding: '12px 24px', - backgroundColor: 'rgba(255, 255, 255, 0.15)', - borderRadius: '12px', - boxShadow: '0 4px 16px rgba(0,0,0,0.1)', - backdropFilter: 'blur(10px)', - border: '1px solid rgba(255,255,255,0.2)', - fontWeight: '500', - position: 'relative', - zIndex: 5 - } - }, fileName) - ]); - - Modal.info({ - title: h('div', { - style: { - display: 'flex', - alignItems: 'center', - gap: '12px', - fontSize: '20px', - fontWeight: '600', - color: '#2c3e50', - textAlign: 'center', - width: '100%', - justifyContent: 'center' - } - }, [ - h('span', { style: { fontSize: '24px' } }, '🖼️'), - '图片预览' - ]), - content: container, - width: '95%', - style: { - maxWidth: '1600px', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - borderRadius: '20px', - overflow: 'hidden', - boxShadow: '0 32px 96px rgba(0,0,0,0.3)', - border: '3px solid rgba(255,255,255,0.2)' - }, - mask: true, - maskClosable: true, + width: '80%', footer: null, - closable: true, - okText: null, - cancelText: null + closable: true }); }; @@ -1683,208 +1474,37 @@ const showImagePreview = (url, fileName) => { const showTextPreview = async (blob, fileName) => { try { const text = await blob.text(); - const fontSize = ref(16); - const container = h('div', { - style: { - width: '100%', - height: '100%', - display: 'flex', - flexDirection: 'column', - padding: '24px', - boxSizing: 'border-box', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', - borderRadius: '16px', - position: 'relative', - overflow: 'hidden' - } - }, [ - // 背景装饰 - h('div', { - style: { - position: 'absolute', - top: '-50%', - left: '-50%', - width: '200%', - height: '200%', - background: 'radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%)', - animation: 'float 6s ease-in-out infinite' - } - }), - - // 顶部工具栏 - h('div', { - style: { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - gap: '16px', - marginBottom: '24px', - padding: '16px 24px', - backgroundColor: 'rgba(255, 255, 255, 0.95)', - borderRadius: '16px', - boxShadow: '0 8px 32px rgba(0,0,0,0.15)', - backdropFilter: 'blur(10px)', - border: '1px solid rgba(255,255,255,0.2)', - position: 'relative', - zIndex: 10 - } - }, [ - h('button', { - onClick: () => fontSize.value = Math.max(12, fontSize.value - 2), - style: { - padding: '10px 16px', - border: 'none', - borderRadius: '12px', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', - color: 'white', - cursor: 'pointer', - fontSize: '14px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '8px', - transition: 'all 0.3s ease', - boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)' - }, - onMouseenter: (e) => { - e.target.style.transform = 'translateY(-2px)'; - e.target.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.4)'; - }, - onMouseleave: (e) => { - e.target.style.transform = 'translateY(0)'; - e.target.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.3)'; - } - }, [ - h('span', { style: { fontSize: '18px' } }, '🔍'), - '缩小字体' - ]), - h('button', { - onClick: () => fontSize.value = Math.min(28, fontSize.value + 2), - style: { - padding: '10px 16px', - border: 'none', - borderRadius: '12px', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', - color: 'white', - cursor: 'pointer', - fontSize: '14px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '8px', - transition: 'all 0.3s ease', - boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)' - }, - onMouseenter: (e) => { - e.target.style.transform = 'translateY(-2px)'; - e.target.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.4)'; - }, - onMouseleave: (e) => { - e.target.style.transform = 'translateY(0)'; - e.target.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.3)'; - } - }, [ - h('span', { style: { fontSize: '18px' } }, '🔍'), - '放大字体' - ]), - h('button', { - onClick: () => { - const textArea = document.createElement('textarea'); - textArea.value = text; - document.body.appendChild(textArea); - textArea.select(); - document.execCommand('copy'); - document.body.removeChild(textArea); - Message.success('文本已复制到剪贴板'); - }, - style: { - padding: '10px 16px', - border: 'none', - borderRadius: '12px', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', - color: 'white', - cursor: 'pointer', - fontSize: '14px', - fontWeight: '500', - display: 'flex', - alignItems: 'center', - gap: '8px', - transition: 'all 0.3s ease', - boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)' - }, - onMouseenter: (e) => { - e.target.style.transform = 'translateY(-2px)'; - e.target.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.4)'; - }, - onMouseleave: (e) => { - e.target.style.transform = 'translateY(0)'; - e.target.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.3)'; - } - }, [ - h('span', { style: { fontSize: '18px' } }, '📋'), - '复制文本' - ]) - ]), - - // 文本内容 - h('div', { - style: { - flex: 1, - backgroundColor: 'rgba(255, 255, 255, 0.95)', - borderRadius: '20px', - padding: '32px', - boxShadow: '0 16px 48px rgba(0,0,0,0.2)', - overflow: 'auto', - fontFamily: "'JetBrains Mono', 'Fira Code', 'Courier New', 'Monaco', 'Menlo', monospace", - fontSize: `${fontSize.value}px`, - lineHeight: '1.8', - whiteSpace: 'pre-wrap', - wordBreak: 'break-word', - color: '#2c3e50', - border: '1px solid rgba(255,255,255,0.3)', - backdropFilter: 'blur(10px)', - position: 'relative', - zIndex: 5 - } - }, text) - ]); - Modal.info({ - title: h('div', { + title: '文本预览', + content: h('div', { style: { - display: 'flex', - alignItems: 'center', - gap: '12px', - fontSize: '20px', - fontWeight: '600', - color: '#2c3e50', textAlign: 'center', - width: '100%', - justifyContent: 'center' + padding: '20px' } }, [ - h('span', { style: { fontSize: '24px' } }, '📄'), - `文本预览 - ${fileName}` + h('div', { + style: { + maxWidth: '100%', + maxHeight: '70vh', + overflow: 'auto', + backgroundColor: '#f8f9fa', + border: '1px solid #e9ecef', + borderRadius: '8px', + padding: '20px', + fontFamily: "'Consolas', 'Monaco', 'Courier New', monospace", + fontSize: '14px', + lineHeight: '1.6', + whiteSpace: 'pre-wrap', + wordBreak: 'break-word', + color: '#333', + textAlign: 'left' + } + }, text) ]), - content: container, - width: '90%', - style: { - maxWidth: '1400px', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - borderRadius: '20px', - overflow: 'hidden', - boxShadow: '0 32px 96px rgba(0,0,0,0.3)', - border: '3px solid rgba(255,255,255,0.2)' - }, - mask: true, - maskClosable: true, + width: '80%', footer: null, - closable: true, - okText: null, - cancelText: null + closable: true }); } catch (error) { console.error('文本预览失败:', error); @@ -1895,6 +1515,7 @@ const showTextPreview = async (blob, fileName) => { // 显示视频预览 const showVideoPreview = (url, fileName) => { const container = h('div', { + class: 'preview-container', style: { width: '100%', height: '100%', @@ -1902,40 +1523,24 @@ const showVideoPreview = (url, fileName) => { flexDirection: 'column', alignItems: 'center', justifyContent: 'center', - padding: '24px', + padding: '20px', boxSizing: 'border-box', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', - borderRadius: '16px', - position: 'relative', - overflow: 'hidden' + background: '#f8fafc !important' } }, [ - // 背景装饰 - h('div', { - style: { - position: 'absolute', - top: '-50%', - left: '-50%', - width: '200%', - height: '200%', - background: 'radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%)', - animation: 'float 6s ease-in-out infinite' - } - }), - // 视频容器 h('div', { style: { width: '100%', maxWidth: '1000px', - backgroundColor: 'rgba(255, 255, 255, 0.95)', - borderRadius: '20px', - padding: '32px', - boxShadow: '0 16px 48px rgba(0,0,0,0.2)', - backdropFilter: 'blur(10px)', - border: '1px solid rgba(255,255,255,0.3)', - position: 'relative', - zIndex: 5 + backgroundColor: '#ffffff', + borderRadius: '8px', + padding: '20px', + boxShadow: '0 2px 8px rgba(0,0,0,0.1)', + border: '1px solid #e2e8f0', + display: 'flex', + flexDirection: 'column', + alignItems: 'center' } }, [ h('video', { @@ -1944,34 +1549,41 @@ const showVideoPreview = (url, fileName) => { style: { width: '100%', maxHeight: '70vh', - borderRadius: '16px', + borderRadius: '8px', background: '#000', - boxShadow: '0 12px 32px rgba(0,0,0,0.3)', - border: '3px solid rgba(255,255,255,0.8)', - transition: 'all 0.3s ease' + boxShadow: '0 4px 12px rgba(0,0,0,0.15)', + border: '1px solid #e2e8f0' } }) ]), - // 文件名 + // 文件信息栏 h('div', { style: { - marginTop: '20px', - fontSize: '16px', - color: 'white', - wordBreak: 'break-all', - textAlign: 'center', - padding: '12px 24px', - backgroundColor: 'rgba(255, 255, 255, 0.15)', - borderRadius: '12px', - boxShadow: '0 4px 16px rgba(0,0,0,0.1)', - backdropFilter: 'blur(10px)', - border: '1px solid rgba(255,255,255,0.2)', - fontWeight: '500', - position: 'relative', - zIndex: 5 + marginTop: '16px', + padding: '12px 16px', + backgroundColor: '#ffffff', + borderRadius: '8px', + boxShadow: '0 2px 8px rgba(0,0,0,0.1)', + border: '1px solid #e2e8f0', + width: '100%', + maxWidth: '1000px', + display: 'flex', + alignItems: 'center', + gap: '16px', + fontSize: '14px', + color: '#2c3e50' } - }, fileName) + }, [ + h('span', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, [ + h('span', { style: { fontSize: '16px' } }, '🎬'), + fileName + ]), + h('span', { style: { color: '#666', fontSize: '13px' } }, '|'), + h('span', { style: { color: '#666', fontSize: '13px' } }, '视频文件'), + h('span', { style: { color: '#666', fontSize: '13px' } }, '|'), + h('span', { style: { color: '#666', fontSize: '13px' } }, new Date().toLocaleString('zh-CN')) + ]) ]); Modal.info({ @@ -1979,32 +1591,41 @@ const showVideoPreview = (url, fileName) => { style: { display: 'flex', alignItems: 'center', - gap: '12px', - fontSize: '20px', + gap: '8px', + fontSize: '16px', fontWeight: '600', - color: '#2c3e50', - textAlign: 'center', - width: '100%', - justifyContent: 'center' + color: '#2c3e50' } }, [ - h('span', { style: { fontSize: '24px' } }, '🎬'), + h('span', { style: { fontSize: '18px' } }, '🎬'), '视频预览' ]), content: container, width: '90%', style: { - maxWidth: '1400px', + maxWidth: '1200px', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', - borderRadius: '20px', + borderRadius: '8px', overflow: 'hidden', - boxShadow: '0 32px 96px rgba(0,0,0,0.3)', - border: '3px solid rgba(255,255,255,0.2)' + boxShadow: '0 8px 24px rgba(0,0,0,0.15)', + border: '1px solid #e2e8f0', + zIndex: 10000 }, mask: true, maskClosable: true, + maskStyle: { + backgroundColor: 'rgba(0, 0, 0, 0.6)', + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + width: '100vw', + height: '100vh', + zIndex: 9999 + }, footer: null, closable: true, okText: null, @@ -2015,6 +1636,7 @@ const showVideoPreview = (url, fileName) => { // 显示音频预览 const showAudioPreview = (url, fileName) => { const container = h('div', { + class: 'preview-container', style: { width: '100%', height: '100%', @@ -2022,49 +1644,30 @@ const showAudioPreview = (url, fileName) => { flexDirection: 'column', alignItems: 'center', justifyContent: 'center', - padding: '24px', + padding: '20px', boxSizing: 'border-box', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', - borderRadius: '16px', - position: 'relative', - overflow: 'hidden' + background: '#f8fafc !important' } }, [ - // 背景装饰 - h('div', { - style: { - position: 'absolute', - top: '-50%', - left: '-50%', - width: '200%', - height: '200%', - background: 'radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%)', - animation: 'float 6s ease-in-out infinite' - } - }), - // 音频容器 h('div', { style: { width: '100%', maxWidth: '500px', - backgroundColor: 'rgba(255, 255, 255, 0.95)', - borderRadius: '20px', - padding: '40px', - boxShadow: '0 16px 48px rgba(0,0,0,0.2)', - backdropFilter: 'blur(10px)', - border: '1px solid rgba(255,255,255,0.3)', - position: 'relative', - zIndex: 5, + backgroundColor: '#ffffff', + borderRadius: '8px', + padding: '32px', + boxShadow: '0 2px 8px rgba(0,0,0,0.1)', + border: '1px solid #e2e8f0', textAlign: 'center' } }, [ // 音频图标 h('div', { style: { - fontSize: '64px', - marginBottom: '24px', - animation: 'pulse 2s ease-in-out infinite' + fontSize: '48px', + marginBottom: '20px', + color: '#165DFF' } }, '🎵'), @@ -2074,26 +1677,54 @@ const showAudioPreview = (url, fileName) => { controls: true, style: { width: '100%', - height: '50px', - borderRadius: '12px', - marginBottom: '20px' + height: '40px', + borderRadius: '6px', + marginBottom: '16px' } }), // 文件名 h('div', { style: { - fontSize: '16px', + fontSize: '14px', color: '#2c3e50', wordBreak: 'break-all', textAlign: 'center', - padding: '12px 16px', - backgroundColor: 'rgba(102, 126, 234, 0.1)', - borderRadius: '12px', + padding: '8px 12px', + backgroundColor: '#f8fafc', + borderRadius: '6px', fontWeight: '500', - border: '1px solid rgba(102, 126, 234, 0.2)' + border: '1px solid #e2e8f0' } }, fileName) + ]), + + // 文件信息栏 + h('div', { + style: { + marginTop: '16px', + padding: '12px 16px', + backgroundColor: '#ffffff', + borderRadius: '8px', + boxShadow: '0 2px 8px rgba(0,0,0,0.1)', + border: '1px solid #e2e8f0', + width: '100%', + maxWidth: '500px', + display: 'flex', + alignItems: 'center', + gap: '16px', + fontSize: '14px', + color: '#2c3e50' + } + }, [ + h('span', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, [ + h('span', { style: { fontSize: '16px' } }, '🎵'), + fileName + ]), + h('span', { style: { color: '#666', fontSize: '13px' } }, '|'), + h('span', { style: { color: '#666', fontSize: '13px' } }, '音频文件'), + h('span', { style: { color: '#666', fontSize: '13px' } }, '|'), + h('span', { style: { color: '#666', fontSize: '13px' } }, new Date().toLocaleString('zh-CN')) ]) ]); @@ -2102,16 +1733,13 @@ const showAudioPreview = (url, fileName) => { style: { display: 'flex', alignItems: 'center', - gap: '12px', - fontSize: '20px', + gap: '8px', + fontSize: '16px', fontWeight: '600', - color: '#2c3e50', - textAlign: 'center', - width: '100%', - justifyContent: 'center' + color: '#2c3e50' } }, [ - h('span', { style: { fontSize: '24px' } }, '🎵'), + h('span', { style: { fontSize: '18px' } }, '🎵'), '音频预览' ]), content: container, @@ -2121,13 +1749,25 @@ const showAudioPreview = (url, fileName) => { top: '50%', left: '50%', transform: 'translate(-50%, -50%)', - borderRadius: '20px', + borderRadius: '8px', overflow: 'hidden', - boxShadow: '0 32px 96px rgba(0,0,0,0.3)', - border: '3px solid rgba(255,255,255,0.2)' + boxShadow: '0 8px 24px rgba(0,0,0,0.15)', + border: '1px solid #e2e8f0', + zIndex: 10000 }, mask: true, maskClosable: true, + maskStyle: { + backgroundColor: 'rgba(0, 0, 0, 0.6)', + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + width: '100vw', + height: '100vh', + zIndex: 9999 + }, footer: null, closable: true, okText: null, @@ -2341,6 +1981,25 @@ const handleCreateFolder = () => { // 打开上传文件对话框 const handleUploadFile = () => { + // 清空文件列表,避免显示之前上传的文件 + fileListTemp.value = []; + + // 重置上传组件状态 + if (uploadRef.value) { + try { + uploadRef.value.reset(); + // 强制清空组件的内部文件列表 + if (uploadRef.value.fileList) { + uploadRef.value.fileList = []; + } + if (uploadRef.value.fileListTemp) { + uploadRef.value.fileListTemp = []; + } + } catch (error) { + console.log('重置上传组件时出错:', error); + } + } + uploadForm.folderId = currentFolderId.value || ''; uploadDialogVisible.value = true; }; @@ -2361,6 +2020,41 @@ watch(currentFolderId, (newId) => { } }); +// 监听上传对话框显示状态,确保文件列表清空 +watch(uploadDialogVisible, (visible) => { + if (visible) { + console.log('=== 上传对话框已显示,确保文件列表清空 ==='); + // 立即清空文件列表 + fileListTemp.value = []; + console.log('✅ 已清空文件列表'); + + // 强制重置上传组件 + if (uploadRef.value) { + try { + uploadRef.value.reset(); + // 强制清空组件的内部文件列表 + if (uploadRef.value.fileList) { + uploadRef.value.fileList = []; + } + if (uploadRef.value.fileListTemp) { + uploadRef.value.fileListTemp = []; + } + console.log('✅ 已重置上传组件'); + } catch (error) { + console.log('❌ 重置上传组件时出错:', error); + } + } + + // 延迟再次清空,确保处理完所有可能的触发 + setTimeout(() => { + fileListTemp.value = []; + console.log('✅ 延迟清空文件列表'); + }, 100); + } +}); + + + // 初始化加载 onMounted(() => { initData(); @@ -3072,57 +2766,106 @@ onMounted(() => { /* 预览弹窗样式增强 */ :deep(.arco-modal) { - animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1); + animation: slideIn 0.2s ease-out; + z-index: 10000 !important; } +/* 确保遮罩覆盖整个屏幕 */ :deep(.arco-modal-mask) { - animation: fadeIn 0.3s ease; - backdrop-filter: blur(8px); + animation: fadeIn 0.2s ease; + background-color: rgba(0, 0, 0, 0.6) !important; + position: fixed !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + width: 100vw !important; + height: 100vh !important; + z-index: 9999 !important; + margin: 0 !important; + padding: 0 !important; + border: none !important; + outline: none !important; + box-sizing: border-box !important; } :deep(.arco-modal-content) { - border-radius: 20px !important; + border-radius: 8px !important; overflow: hidden; - box-shadow: 0 32px 96px rgba(0,0,0,0.3) !important; - border: 3px solid rgba(255,255,255,0.2) !important; + box-shadow: 0 8px 24px rgba(0,0,0,0.15) !important; + border: 1px solid #e2e8f0 !important; + background: #ffffff !important; +} + +:deep(.arco-modal-body) { + background: #f8fafc !important; + padding: 0 !important; +} + +/* 强制覆盖所有可能的背景样式 */ +:deep(.arco-modal-body > div) { + background: #f8fafc !important; +} + +:deep(.arco-modal-body > div > div) { + background: #f8fafc !important; +} + +/* 确保预览弹窗内容区域使用正确的背景 */ +:deep(.arco-modal-body .preview-container) { + background: #f8fafc !important; +} + +/* 强制覆盖所有预览弹窗的背景 */ +:deep(.arco-modal-body) { + background: #f8fafc !important; +} + +:deep(.arco-modal-body > *) { + background: #f8fafc !important; +} + +/* 确保没有渐变背景 */ +:deep(.arco-modal-body *[style*="linear-gradient"]) { + background: #f8fafc !important; } :deep(.arco-modal-header) { - background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); - border-bottom: 1px solid rgba(255,255,255,0.2); - padding: 20px 24px; + background: #ffffff; + border-bottom: 1px solid #e2e8f0; + padding: 16px 20px; } :deep(.arco-modal-title) { font-weight: 600; color: #2c3e50; + font-size: 16px; } :deep(.arco-modal-close) { - background: rgba(255,255,255,0.9); + background: #f8fafc; border-radius: 50%; - width: 32px; - height: 32px; + width: 28px; + height: 28px; display: flex; align-items: center; justify-content: center; - transition: all 0.3s ease; - box-shadow: 0 2px 8px rgba(0,0,0,0.1); + transition: all 0.2s ease; + border: 1px solid #e2e8f0; &:hover { - background: rgba(255,255,255,1); - transform: scale(1.1); - box-shadow: 0 4px 12px rgba(0,0,0,0.15); + background: #e2e8f0; + color: #165DFF; } } /* 按钮悬停效果增强 */ :deep(.arco-btn) { - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + transition: all 0.2s ease; &:hover { - transform: translateY(-2px); - box-shadow: 0 6px 16px rgba(0,0,0,0.15); + border-color: #165DFF; + color: #165DFF; } &:active { diff --git a/src/views/operation-platform/data-processing/data-storage/index.vue b/src/views/operation-platform/data-processing/data-storage/index.vue index 1c8b3f9..e881a02 100644 --- a/src/views/operation-platform/data-processing/data-storage/index.vue +++ b/src/views/operation-platform/data-processing/data-storage/index.vue @@ -83,8 +83,8 @@ - - + + @@ -92,8 +92,12 @@ import { ref, reactive, computed, onMounted } from 'vue' import { Message } from '@arco-design/web-vue' import type { TableColumnData } from '@arco-design/web-vue' +<<<<<<< HEAD import PreviewModal from './components/PreviewModal.vue' import rawData from './components/raw-data.vue' +======= + +>>>>>>> 16743cfc7f885acdff70e08f20ddb7795d638a83 import { getProjectList, getTurbineList, @@ -107,7 +111,8 @@ import { } from '@/apis/industrial-image' import DeformationTap from './components/DeformationTap.vue' -const previewModal = ref() +// 预览弹窗引用(待重新设计) +// const previewModal = ref() // 活动选项卡 const activeTab = ref('image') @@ -323,18 +328,23 @@ const getImageUrl = (imagePath: string): string => { return `${baseUrl}${imagePath}` } -// 预览文件 +// 预览文件(待重新设计预览弹窗) const previewFile = (file: any) => { +<<<<<<< HEAD /* previewFileData.value = file previewModalVisible.value = true*/ +======= +>>>>>>> 16743cfc7f885acdff70e08f20ddb7795d638a83 const fileObj = { id: file.id, name: file.name, - url: getImageUrl(file.imagePath), - audios: file.audioList + url: getImageUrl(file.imagePath) } - previewModal.value.openPreview(fileObj) + // TODO: 重新设计预览弹窗后取消注释 + // previewModal.value.openPreview(fileObj) + console.log('预览文件:', fileObj) + // 暂时禁用预览功能,等待重新设计 } // 删除文件 diff --git a/src/views/project-management/contract/expense-contract/ContractDetail.vue b/src/views/project-management/contract/expense-contract/ContractDetail.vue new file mode 100644 index 0000000..dbba383 --- /dev/null +++ b/src/views/project-management/contract/expense-contract/ContractDetail.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/src/views/project-management/contract/expense-contract/ContractEdit.vue b/src/views/project-management/contract/expense-contract/ContractEdit.vue new file mode 100644 index 0000000..4ced636 --- /dev/null +++ b/src/views/project-management/contract/expense-contract/ContractEdit.vue @@ -0,0 +1,122 @@ + + + diff --git a/src/views/project-management/contract/expense-contract/index.vue b/src/views/project-management/contract/expense-contract/index.vue index 871f4c1..a0c3eaf 100644 --- a/src/views/project-management/contract/expense-contract/index.vue +++ b/src/views/project-management/contract/expense-contract/index.vue @@ -1,28 +1,28 @@ + + diff --git a/src/views/system-resource/device-management/procurement/index.vue b/src/views/system-resource/device-management/procurement/index.vue index d07edab..6dc9934 100644 --- a/src/views/system-resource/device-management/procurement/index.vue +++ b/src/views/system-resource/device-management/procurement/index.vue @@ -175,6 +175,19 @@ 编辑 + + + 申请采购 + + + + {{ getApprovalStatusText(record.approvalStatus) }} + + + + @@ -213,7 +233,9 @@ import { import message from '@arco-design/web-vue/es/message' import ProcurementModal from './components/ProcurementModal.vue' import ProcurementSearch from './components/ProcurementSearch.vue' +import ProcurementApplicationModal from './components/ProcurementApplicationModal.vue' import { equipmentProcurementApi } from '@/apis/equipment/procurement' +import { equipmentApprovalApi } from '@/apis/equipment/approval' import type { EquipmentListReq, EquipmentResp } from '@/apis/equipment/type' defineOptions({ name: 'EquipmentProcurement' }) @@ -243,6 +265,10 @@ const modalVisible = ref(false) const currentProcurement = ref(null) const modalMode = ref<'add' | 'edit' | 'view'>('add') +// 采购申请弹窗控制 +const applicationModalVisible = ref(false) +const currentApplicationData = ref(null) + // 表格选择 const selectedRowKeys = ref([]) const rowSelection = reactive({ @@ -611,6 +637,12 @@ const handleEdit = (record: EquipmentResp) => { modalVisible.value = true } +// 申请采购 +const handleApplyProcurement = (record: EquipmentResp) => { + currentApplicationData.value = { ...record } + applicationModalVisible.value = true +} + // 删除 const handleDelete = async (record: EquipmentResp) => { try { @@ -629,6 +661,12 @@ const handleModalSuccess = () => { loadData(currentSearchParams.value) } +// 采购申请成功回调 +const handleApplicationSuccess = () => { + applicationModalVisible.value = false + loadData(currentSearchParams.value) +} + // 刷新数据 const refreshData = () => { loadData(currentSearchParams.value) @@ -674,6 +712,36 @@ const getTotalAmount = () => { return formatPrice(total) } +// 检查是否可以申请采购 +const canApplyProcurement = (record: EquipmentResp) => { + // 检查是否有审批状态,如果没有或者状态为待申请,则可以申请 + return !record.approvalStatus || record.approvalStatus === 'PENDING_APPLICATION' +} + +// 获取审批状态颜色 +const getApprovalStatusColor = (status: string) => { + const colorMap: Record = { + 'PENDING_APPLICATION': 'blue', + 'PENDING': 'orange', + 'APPROVED': 'green', + 'REJECTED': 'red', + 'WITHDRAWN': 'gray' + } + return colorMap[status] || 'blue' +} + +// 获取审批状态文本 +const getApprovalStatusText = (status: string) => { + const textMap: Record = { + 'PENDING_APPLICATION': '待申请', + 'PENDING': '待审批', + 'APPROVED': '已通过', + 'REJECTED': '已拒绝', + 'WITHDRAWN': '已撤回' + } + return textMap[status] || '未知' +} + onMounted(() => { loadData() }) diff --git a/vite.config.ts b/vite.config.ts index a38aa21..2c2fbf8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,6 +5,11 @@ import createVitePlugins from './config/plugins' export default defineConfig(({ command, mode }) => { const env = loadEnv(mode, process.cwd()) as ImportMetaEnv + // 设置默认的WebSocket URL + if (!env.VITE_API_WS_URL) { + env.VITE_API_WS_URL = 'ws://localhost:8888' + } + return { // 开发或生产环境服务的公共基础路径 base: env.VITE_BASE,