Browse Source

feat(risk-warning): 发布弹窗加附件上传 + 详情支持下载(MinIO)

- types.ts: RiskWarningBo/Vo 加 attachmentUrl + attachmentName 字段
- warning/index.vue:
  - 发布弹窗新增附件上传 form-item(el-upload + customUpload 走 axios baseURL)
  - 提交前提取附件 URL 与原始文件名
  - 详情弹窗新增附件下载行(显示文件名 + window.open 触发下载)
  - 上传前校验扩展名与 10MB 大小限制
  - reset/closeDialog 清空附件列表

后端字段: org.dromara.risk.domain.RiskWarning.attachmentUrl/Name
yangjingjing 2 ngày trước cách đây
mục cha
commit
06159693fa
2 tập tin đã thay đổi với 211 bổ sung5 xóa
  1. 8 0
      src/api/risk/types.ts
  2. 203 5
      src/views/risk/warning/index.vue

+ 8 - 0
src/api/risk/types.ts

@@ -382,6 +382,10 @@ export interface RiskWarningBo {
   status?: string;
   readCount?: number;
   remark?: string;
+  /** 附件访问URL(MinIO对象存储路径) */
+  attachmentUrl?: string;
+  /** 附件原始文件名 */
+  attachmentName?: string;
   params?: any;
 }
 
@@ -403,6 +407,10 @@ export interface RiskWarningVo {
   status: string;
   readCount: number;
   remark: string;
+  /** 附件访问URL(MinIO对象存储路径) */
+  attachmentUrl: string;
+  /** 附件原始文件名 */
+  attachmentName: string;
   createBy: string;
   createTime: string;
   updateBy: string;

+ 203 - 5
src/views/risk/warning/index.vue

@@ -173,6 +173,36 @@
             style="width: 100%"
           />
         </el-form-item>
+
+        <el-form-item label="附件上传">
+          <el-upload
+            v-model:file-list="attachmentFileList"
+            :http-request="customUpload"
+            :limit="1"
+            :accept="'.pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png'"
+            :before-upload="beforeAttachmentUpload"
+            :on-success="handleAttachmentSuccess"
+            :on-error="handleUploadError"
+            :on-exceed="handleAttachmentExceed"
+            :on-remove="handleAttachmentRemove"
+          >
+            <template #trigger>
+              <div class="upload-trigger-area">
+                <div class="upload-action-row">
+                  <el-button type="primary" plain>选择文件</el-button>
+                  <span class="upload-placeholder">{{ attachmentFileList.length > 0 ? '已选择 1 个文件' : '未选择任何文件' }}</span>
+                </div>
+                <div class="upload-tip-box">
+                  <div class="upload-tip-icon">
+                    <el-icon><UploadFilled /></el-icon>
+                  </div>
+                  <div class="upload-tip-text">点击上传或拖拽文件到此区域</div>
+                  <div class="upload-tip-desc">支持 PDF、Word、Excel、图片格式,单个文件不超过10MB</div>
+                </div>
+              </div>
+            </template>
+          </el-upload>
+        </el-form-item>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
@@ -198,6 +228,18 @@
           <div class="multi-line">{{ viewForm.warningContent || '—' }}</div>
         </el-descriptions-item>
         <el-descriptions-item label="备注">{{ viewForm.remark || '—' }}</el-descriptions-item>
+        <el-descriptions-item v-if="viewForm.attachmentUrl" label="附件">
+          <a
+            class="attachment-link"
+            :href="viewForm.attachmentUrl"
+            target="_blank"
+            rel="noopener noreferrer"
+            @click.prevent="handleDownloadAttachment(viewForm.attachmentUrl)"
+          >
+            <el-icon><Document /></el-icon>
+            <span>{{ viewForm.attachmentName || '下载附件' }}</span>
+          </a>
+        </el-descriptions-item>
       </el-descriptions>
       <template #footer>
         <el-button @click="viewDialog.visible = false">关闭</el-button>
@@ -213,8 +255,9 @@ import { listWarning, getWarning, addWarning, delWarning } from '@/api/risk/warn
 import { getDicts } from '@/api/system/dict/data';
 import type { DictDataVO } from '@/api/system/dict/data/types';
 import type { RiskWarningBo, RiskWarningVo } from '@/api/risk/types';
-import { ElMessage, FormInstance } from 'element-plus';
-import { Plus } from '@element-plus/icons-vue';
+import { uploadFile } from '@/api/risk/file';
+import { ElMessage, FormInstance, UploadUserFile } from 'element-plus';
+import { Document, Plus, UploadFilled } from '@element-plus/icons-vue';
 import type { ComponentInternalInstance } from 'vue';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -226,6 +269,7 @@ const total = ref(0);
 const dateRange = ref<string[]>([]);
 const warningTypeOptions = ref<Array<{ label: string; value: string }>>([]);
 const warningLevelOptions = ref<Array<{ label: string; value: string }>>([]);
+const attachmentFileList = ref<UploadUserFile[]>([]);
 
 const queryFormRef = ref<FormInstance>();
 const formRef = ref<FormInstance>();
@@ -257,7 +301,7 @@ const getDictLabel = (opts: Array<{ label: string; value: string }>, val: string
 
 const mapDictData = (list: DictDataVO[]) => {
   return (list || [])
-    .filter((d) => d.status === '0' || d.status === undefined)
+    .filter((d: any) => d.status === '0' || d.status === undefined)
     .sort((a, b) => (a.dictSort || 0) - (b.dictSort || 0))
     .map((d) => ({ label: d.dictLabel, value: d.dictValue }));
 };
@@ -288,7 +332,9 @@ const data = reactive<any>({
     expireTime: '',
     releaserName: '',
     publishScope: '全员',
-    status: '已发布'
+    status: '已发布',
+    attachmentUrl: '',
+    attachmentName: ''
   } as any,
   queryParams: {
     pageNum: 1,
@@ -393,6 +439,16 @@ const submitForm = () => {
       // 自动填充发布人
       form.value.releaserName = form.value.releaserName || userStore.nickname || 'admin';
       form.value.publishScope = '全员';
+      // 提取附件 URL 与原始文件名
+      const attach: any = attachmentFileList.value.find((f: any) => f.status === 'success');
+      if (attach) {
+        form.value.attachmentUrl =
+          attach.url || attach.response?.data?.url || attach.response?.data?.ossId || '';
+        form.value.attachmentName = attach.name || attach.response?.data?.originalName || '';
+      } else {
+        form.value.attachmentUrl = '';
+        form.value.attachmentName = '';
+      }
       await addWarning(form.value as RiskWarningBo);
       proxy?.$modal.msgSuccess('发布成功');
       dialog.visible = false;
@@ -419,8 +475,11 @@ const reset = () => {
     expireTime: '',
     releaserName: '',
     publishScope: '全员',
-    status: '已发布'
+    status: '已发布',
+    attachmentUrl: '',
+    attachmentName: ''
   } as any;
+  attachmentFileList.value = [];
   formRef.value?.resetFields();
 };
 
@@ -429,6 +488,91 @@ const closeDialog = () => {
   reset();
 };
 
+// ============ 附件上传(MinIO) ============
+const ALLOWED_EXTS = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'jpg', 'jpeg', 'png'];
+const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
+
+/**
+ * 自定义上传(走 axios baseURL,避免跨域/405)
+ */
+const customUpload = async (options: any) => {
+  try {
+    const res: any = await uploadFile(options.file);
+    if (res && res.code === 200 && res.data) {
+      // 把后端返回的 url 写入 file 对象,方便 onSuccess 后回填
+      options.file.url = res.data.url;
+      options.onSuccess(res, options.file);
+    } else {
+      options.onError(new Error(res?.msg || '上传失败'));
+    }
+  } catch (err: any) {
+    options.onError(err);
+  }
+};
+
+/**
+ * 上传前校验扩展名 + 大小
+ */
+const beforeAttachmentUpload = (file: any) => {
+  const ext = (file.name.split('.').pop() || '').toLowerCase();
+  if (!ALLOWED_EXTS.includes(ext)) {
+    ElMessage.warning(`不支持的文件类型 .${ext},仅允许: ${ALLOWED_EXTS.join('/')}`);
+    return false;
+  }
+  if (file.size > MAX_FILE_SIZE) {
+    ElMessage.warning(`文件 ${file.name} 超过 10MB 上限`);
+    return false;
+  }
+  return true;
+};
+
+/**
+ * 单个文件上传成功
+ */
+const handleAttachmentSuccess = (response: any, file: any) => {
+  if (response && response.code === 200) {
+    ElMessage.success(`附件 ${file.name} 上传成功`);
+  } else {
+    ElMessage.error(`附件 ${file.name} 上传失败: ${response?.msg || '未知错误'}`);
+  }
+};
+
+/**
+ * 上传失败
+ */
+const handleUploadError = (error: any) => {
+  console.error('附件上传失败', error);
+  ElMessage.error(`附件上传失败: ${error?.message || '网络异常'}`);
+};
+
+/**
+ * 超过上传数量限制
+ */
+const handleAttachmentExceed = () => {
+  ElMessage.warning('最多只能上传 1 个附件,如需更换请先删除已有附件');
+};
+
+/**
+ * 移除附件
+ */
+const handleAttachmentRemove = () => {
+  form.value.attachmentUrl = '';
+  form.value.attachmentName = '';
+  ElMessage.success('已移除附件');
+};
+
+/**
+ * 下载附件(详情弹窗点击触发)
+ */
+const handleDownloadAttachment = (url?: string) => {
+  if (!url) {
+    ElMessage.warning('该警示未上传附件');
+    return;
+  }
+  // MinIO public 桶,直接打开即可下载
+  window.open(url, '_blank');
+};
+
 const formatDateTime = (val: any): string => {
   if (!val) return '';
   const d = new Date(val);
@@ -485,4 +629,58 @@ onMounted(async () => {
   word-break: break-all;
   line-height: 1.6;
 }
+.attachment-link {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  color: #409eff;
+  text-decoration: none;
+  font-size: 13px;
+  cursor: pointer;
+  transition: color 0.2s;
+}
+.attachment-link:hover {
+  color: #66b1ff;
+  text-decoration: underline;
+}
+.attachment-link .el-icon {
+  font-size: 16px;
+}
+.upload-trigger-area {
+  width: 100%;
+}
+.upload-action-row {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-bottom: 8px;
+}
+.upload-placeholder {
+  font-size: 13px;
+  color: #909399;
+}
+.upload-tip-box {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 16px 0;
+  border: 1px dashed #d9d9d9;
+  border-radius: 4px;
+  background: #fafafa;
+}
+.upload-tip-icon {
+  color: #c0c4cc;
+  font-size: 24px;
+  margin-bottom: 6px;
+}
+.upload-tip-text {
+  font-size: 13px;
+  color: #606266;
+}
+.upload-tip-desc {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 2px;
+}
 </style>