Преглед изворни кода

feat(risk): 风险警示模块按设计稿实现

- warning/index.vue 重写:
  * 列表加序号列
  * 警示等级按设计稿配色(重大=红/较大=黄/一般=蓝)
  * 字典 value 改用中文(安全/食安/社会 + 重大/较大/一般)
  * 删除多余的附件上传(MinIO),严格按 UI
  * 修 <style scoped> 缺点的 bug(.warning-page)
- system/dict/index.vue: 卡片内 flex 撑满视口,el-table height=100% 表头固定可独立滚动
- personnel/index.vue: getJobTypeTag 改用新 6 项人员类型字典
yangjingjing пре 1 дан
родитељ
комит
7d167fa5e4
3 измењених фајлова са 72 додато и 232 уклоњено
  1. 20 4
      src/views/risk/personnel/index.vue
  2. 21 226
      src/views/risk/warning/index.vue
  3. 31 2
      src/views/system/dict/index.vue

+ 20 - 4
src/views/risk/personnel/index.vue

@@ -56,7 +56,9 @@
         <el-table-column type="index" label="序号" align="center" width="60" />
         <el-table-column label="类型" align="center" prop="jobType" width="100">
           <template #default="scope">
-            <el-tag :type="getJobTypeTag(scope.row.jobType)">{{ scope.row.jobType || '-' }}</el-tag>
+            <el-tag :type="getJobTypeTag(scope.row.jobType)">
+              {{ getDictLabel(personTypeOptions, scope.row.jobType) || scope.row.jobType || '-' }}
+            </el-tag>
           </template>
         </el-table-column>
         <el-table-column label="姓名" align="center" prop="personnelName" width="100" />
@@ -515,15 +517,29 @@ const getInsuranceTag = (row: any) => {
   return map[label] || 'info';
 };
 
+// 人员类型 tag 颜色(按字典 value 映射,与设计稿一致)
+// 人员类型 tag 颜色(按字典 value 映射,与设计稿一致)
 const getJobTypeTag = (type: string) => {
   const map: Record<string, string> = {
-    '1': 'primary',
-    '2': 'warning',
-    '3': 'danger'
+    防损人员: 'primary',             // 蓝色
+    施工人员: 'warning',             // 橙色
+    特岗人员: 'danger',              // 红色
+    食品相关岗位人员: 'success',     // 绿色
+    检测员: 'info',                  // 灰色
+    兼职品控人员: 'primary'          // 蓝色
   };
   return map[type] || 'info';
 };
 
+// 字典值 -> 中文 label(用于列表渲染)
+const getDictLabel = (
+  opts: Array<{ label: string; value: string }>,
+  val: string
+) => {
+  if (!val) return ''
+  return opts.find((o) => o.value === val)?.label || val
+}
+
 const getList = async () => {
   loading.value = true;
   const res = await listPersonnel(queryParams.value as RiskPersonnelBo);

+ 21 - 226
src/views/risk/warning/index.vue

@@ -38,7 +38,7 @@
             <el-option
               v-for="d in warningLevelOptions"
               :key="d.value"
-              :label="getWarningLevelDisplay(d.value).label"
+              :label="d.label"
               :value="d.value"
             />
           </el-select>
@@ -74,11 +74,12 @@
     <!-- 列表 -->
     <el-card shadow="hover" class="list-card">
       <div class="list-toolbar">
-        <span class="list-total">共 {{ total }} 条警示信息</span>
+        <span class="list-total">共 {{ total }} 条风险警示信息</span>
         <el-button type="primary" :icon="Plus" @click="handleAdd">发布警示</el-button>
       </div>
 
       <el-table v-loading="loading" :data="warningList" border stripe>
+        <el-table-column type="index" label="序号" align="center" width="60" />
         <el-table-column prop="warningTitle" label="警示标题" align="center" :show-overflow-tooltip="true" min-width="220" />
         <el-table-column label="警示类型" align="center" prop="warningType" width="130">
           <template #default="{ row }">{{ getDictLabel(warningTypeOptions, row.warningType) }}</template>
@@ -86,7 +87,7 @@
         <el-table-column label="警示等级" align="center" width="120">
           <template #default="{ row }">
             <el-tag :type="getWarningLevelTag(row.warningLevel)" effect="light" round>
-              {{ getWarningLevelDisplay(row.warningLevel).label }}
+              {{ getDictLabel(warningLevelOptions, row.warningLevel) }}
             </el-tag>
           </template>
         </el-table-column>
@@ -146,7 +147,7 @@
             <el-option
               v-for="d in warningLevelOptions"
               :key="d.value"
-              :label="getWarningLevelDisplay(d.value).label"
+              :label="d.label"
               :value="d.value"
             />
           </el-select>
@@ -173,36 +174,6 @@
             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">
@@ -219,7 +190,7 @@
         <el-descriptions-item label="警示类型">{{ getDictLabel(warningTypeOptions, viewForm.warningType) }}</el-descriptions-item>
         <el-descriptions-item label="警示等级">
           <el-tag :type="getWarningLevelTag(viewForm.warningLevel)">
-            {{ getWarningLevelDisplay(viewForm.warningLevel).label }}
+            {{ getDictLabel(warningLevelOptions, viewForm.warningLevel) }}
           </el-tag>
         </el-descriptions-item>
         <el-descriptions-item label="发布人">{{ viewForm.releaserName }}</el-descriptions-item>
@@ -228,18 +199,6 @@
           <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>
@@ -253,11 +212,9 @@ import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue';
 import { useUserStore } from '@/store/modules/user';
 import { listWarning, getWarning, addWarning, delWarning } from '@/api/risk/warning';
 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 { uploadFile } from '@/api/risk/file';
-import { ElMessage, FormInstance, UploadUserFile } from 'element-plus';
-import { Document, Plus, UploadFilled } from '@element-plus/icons-vue';
+import { FormInstance } from 'element-plus';
+import { Plus } from '@element-plus/icons-vue';
 import type { ComponentInternalInstance } from 'vue';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -269,29 +226,21 @@ 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>();
 
 /**
- * 等级字典值 → 需求显示文字 + el-tag 颜色
- * 字典值 urgent / important / normal
- * 需求显示:重大风险(red) / 较大风险(yellow) / 一般风险(blue)
+ * 警示等级中文 value → el-tag 颜色
+ * 字典 value 与设计稿对应:重大风险(红) / 较大风险(黄) / 一般风险(蓝)
  */
-const LEVEL_DISPLAY: Record<string, { label: string; tag: 'danger' | 'warning' | 'primary' }> = {
-  urgent: { label: '重大风险', tag: 'danger' },
-  important: { label: '较大风险', tag: 'warning' },
-  normal: { label: '一般风险', tag: 'primary' }
-};
-
-const getWarningLevelDisplay = (val: string) => {
-  return LEVEL_DISPLAY[val] || { label: val || '-', tag: 'info' };
+const LEVEL_TAG: Record<string, 'danger' | 'warning' | 'primary'> = {
+  '重大风险': 'danger',
+  '较大风险': 'warning',
+  '一般风险': 'primary'
 };
 
-const getWarningLevelTag = (val: string): any => {
-  return LEVEL_DISPLAY[val]?.tag || 'info';
-};
+const getWarningLevelTag = (val: string) => LEVEL_TAG[val] || 'info';
 
 const getDictLabel = (opts: Array<{ label: string; value: string }>, val: string) => {
   if (!val) return '';
@@ -338,9 +287,7 @@ const data = reactive<any>({
     expireTime: '',
     releaserName: '',
     publishScope: '全员',
-    status: '已发布',
-    attachmentUrl: '',
-    attachmentName: ''
+    status: '已发布'
   } as any,
   queryParams: {
     pageNum: 1,
@@ -442,19 +389,9 @@ const handleDelete = async (row: RiskWarningVo) => {
 const submitForm = () => {
   formRef.value?.validate(async (valid: boolean) => {
     if (valid) {
-      // 自动填充发布人
-      form.value.releaserName = form.value.releaserName || userStore.nickname || 'admin';
+      // 自动填充发布人(从当前登录用户取)
+      form.value.releaserName = form.value.releaserName || userStore.nickname || userStore.name || '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;
@@ -481,11 +418,8 @@ const reset = () => {
     expireTime: '',
     releaserName: '',
     publishScope: '全员',
-    status: '已发布',
-    attachmentUrl: '',
-    attachmentName: ''
+    status: '已发布'
   } as any;
-  attachmentFileList.value = [];
   formRef.value?.resetFields();
 };
 
@@ -494,91 +428,6 @@ 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);
@@ -594,7 +443,7 @@ onMounted(async () => {
 </script>
 
 <style scoped>
-  warning-page {
+.warning-page {
   padding: 16px;
   height: 100%;
   overflow-y: auto;
@@ -635,58 +484,4 @@ 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>
+</style>

+ 31 - 2
src/views/system/dict/index.vue

@@ -54,6 +54,7 @@
               ref="typeTableRef"
               v-loading="typeLoading"
               border
+              height="100%"
               :data="typeList"
               highlight-current-row
               @row-click="handleTypeRowClick"
@@ -155,7 +156,7 @@
           </div>
 
           <div class="dict-table-wrap">
-            <el-table v-loading="dataLoading" border :data="dataList" @selection-change="handleDataSelectionChange">
+            <el-table v-loading="dataLoading" border height="100%" :data="dataList" @selection-change="handleDataSelectionChange">
               <el-table-column type="selection" width="55" align="center" />
               <el-table-column v-if="false" label="字典编码" align="center" prop="dictCode" />
               <el-table-column label="字典标签" align="center" prop="dictLabel" width="80">
@@ -612,8 +613,34 @@ onMounted(() => {
 </script>
 
 <style lang="scss" scoped>
+.dict-page {
+  /* 让两列卡片撑满视口高度,内部表格独立滚动(不依赖浏览器整体滚动) */
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
 .dict-grid {
   row-gap: 16px;
+  height: 100%;
+}
+
+.dict-grid :deep(.el-col) {
+  height: 100%;
+}
+
+.dict-card {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.dict-card :deep(.el-card__body) {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
 }
 
 .dict-card__header {
@@ -671,6 +698,8 @@ onMounted(() => {
 }
 
 .dict-table-wrap {
-  overflow-x: auto;
+  flex: 1;
+  min-height: 0;
+  overflow: auto;
 }
 </style>