|
@@ -173,6 +173,36 @@
|
|
|
style="width: 100%"
|
|
style="width: 100%"
|
|
|
/>
|
|
/>
|
|
|
</el-form-item>
|
|
</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>
|
|
</el-form>
|
|
|
<template #footer>
|
|
<template #footer>
|
|
|
<div class="dialog-footer">
|
|
<div class="dialog-footer">
|
|
@@ -198,6 +228,18 @@
|
|
|
<div class="multi-line">{{ viewForm.warningContent || '—' }}</div>
|
|
<div class="multi-line">{{ viewForm.warningContent || '—' }}</div>
|
|
|
</el-descriptions-item>
|
|
</el-descriptions-item>
|
|
|
<el-descriptions-item label="备注">{{ viewForm.remark || '—' }}</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>
|
|
</el-descriptions>
|
|
|
<template #footer>
|
|
<template #footer>
|
|
|
<el-button @click="viewDialog.visible = false">关闭</el-button>
|
|
<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 { getDicts } from '@/api/system/dict/data';
|
|
|
import type { DictDataVO } from '@/api/system/dict/data/types';
|
|
import type { DictDataVO } from '@/api/system/dict/data/types';
|
|
|
import type { RiskWarningBo, RiskWarningVo } from '@/api/risk/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';
|
|
import type { ComponentInternalInstance } from 'vue';
|
|
|
|
|
|
|
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
@@ -226,6 +269,7 @@ const total = ref(0);
|
|
|
const dateRange = ref<string[]>([]);
|
|
const dateRange = ref<string[]>([]);
|
|
|
const warningTypeOptions = ref<Array<{ label: string; value: string }>>([]);
|
|
const warningTypeOptions = ref<Array<{ label: string; value: string }>>([]);
|
|
|
const warningLevelOptions = ref<Array<{ label: string; value: string }>>([]);
|
|
const warningLevelOptions = ref<Array<{ label: string; value: string }>>([]);
|
|
|
|
|
+const attachmentFileList = ref<UploadUserFile[]>([]);
|
|
|
|
|
|
|
|
const queryFormRef = ref<FormInstance>();
|
|
const queryFormRef = ref<FormInstance>();
|
|
|
const formRef = ref<FormInstance>();
|
|
const formRef = ref<FormInstance>();
|
|
@@ -257,7 +301,7 @@ const getDictLabel = (opts: Array<{ label: string; value: string }>, val: string
|
|
|
|
|
|
|
|
const mapDictData = (list: DictDataVO[]) => {
|
|
const mapDictData = (list: DictDataVO[]) => {
|
|
|
return (list || [])
|
|
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))
|
|
.sort((a, b) => (a.dictSort || 0) - (b.dictSort || 0))
|
|
|
.map((d) => ({ label: d.dictLabel, value: d.dictValue }));
|
|
.map((d) => ({ label: d.dictLabel, value: d.dictValue }));
|
|
|
};
|
|
};
|
|
@@ -288,7 +332,9 @@ const data = reactive<any>({
|
|
|
expireTime: '',
|
|
expireTime: '',
|
|
|
releaserName: '',
|
|
releaserName: '',
|
|
|
publishScope: '全员',
|
|
publishScope: '全员',
|
|
|
- status: '已发布'
|
|
|
|
|
|
|
+ status: '已发布',
|
|
|
|
|
+ attachmentUrl: '',
|
|
|
|
|
+ attachmentName: ''
|
|
|
} as any,
|
|
} as any,
|
|
|
queryParams: {
|
|
queryParams: {
|
|
|
pageNum: 1,
|
|
pageNum: 1,
|
|
@@ -393,6 +439,16 @@ const submitForm = () => {
|
|
|
// 自动填充发布人
|
|
// 自动填充发布人
|
|
|
form.value.releaserName = form.value.releaserName || userStore.nickname || 'admin';
|
|
form.value.releaserName = form.value.releaserName || userStore.nickname || 'admin';
|
|
|
form.value.publishScope = '全员';
|
|
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);
|
|
await addWarning(form.value as RiskWarningBo);
|
|
|
proxy?.$modal.msgSuccess('发布成功');
|
|
proxy?.$modal.msgSuccess('发布成功');
|
|
|
dialog.visible = false;
|
|
dialog.visible = false;
|
|
@@ -419,8 +475,11 @@ const reset = () => {
|
|
|
expireTime: '',
|
|
expireTime: '',
|
|
|
releaserName: '',
|
|
releaserName: '',
|
|
|
publishScope: '全员',
|
|
publishScope: '全员',
|
|
|
- status: '已发布'
|
|
|
|
|
|
|
+ status: '已发布',
|
|
|
|
|
+ attachmentUrl: '',
|
|
|
|
|
+ attachmentName: ''
|
|
|
} as any;
|
|
} as any;
|
|
|
|
|
+ attachmentFileList.value = [];
|
|
|
formRef.value?.resetFields();
|
|
formRef.value?.resetFields();
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -429,6 +488,91 @@ const closeDialog = () => {
|
|
|
reset();
|
|
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 => {
|
|
const formatDateTime = (val: any): string => {
|
|
|
if (!val) return '';
|
|
if (!val) return '';
|
|
|
const d = new Date(val);
|
|
const d = new Date(val);
|
|
@@ -485,4 +629,58 @@ onMounted(async () => {
|
|
|
word-break: break-all;
|
|
word-break: break-all;
|
|
|
line-height: 1.6;
|
|
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>
|
|
</style>
|