fix(material): 物料列表列加双重守卫,消除「有表头无数据」的空列

现象
----
收紧某字段读权限后,后端已把值抹成 null,但表格列照常渲染 —— 留下一列
全是「-」的空表头,白占屏幕宽度。反馈的「专业名称」即属此类。

两处前提与代码不符,先澄清
----
· 列展示设置里**已有**「专业名称」选项(list.vue:190),columns.commonName
  也**已在** columns 数组中(默认 visible: true)—— 无需补充。
  该选项与表格列都走 hasColPermission('commonName'),没有权限者看不到选项,
  与「没权限就看不见这列」的目标一致,并非缺陷。
· 真正的缺陷在同一处:**表格列只有 columns.X.visible,没有权限守卫**。
  16 个数据列里仅 isApprovalRequired 有(且用错了码)。

改动
----
一、给全部数据列补上 && hasColPermission('<key>')(两文件各 15 列;
    isApprovalRequired 原先写成 userStore.hasPermission('material_list:isApprovalRequired'),
    改为同一函数,避免复选框与表格列各用一套口径)。
二、修正 permissionMap 两处与后端读过滤的口径错位:
      isInspectionRequired: material_list:operation → material_list:isInspectionRequired
      isApprovalRequired:   material_list:operation → material_list:isApprovalRequired
    (以 app/utils/field_permissions.py 为准)

★ 为什么第 2 步是必须的:前端按 operation 判断会以为「有权限」,而后端其实
  按另一个码把字段抹成了 null —— 于是渲染出一列全是「-」的空表头。
  前端判定码必须与后端读过滤码**逐字段一致**,否则修了守卫也照样是空列。

验证
----
· 两个文件的数据列 100% 带权限守卫(grep 复核)
· 前端 permissionMap 与后端 STOCK_FIELD_RBAC_MAPPING 逐字段比对:
  16 项中 14 项完全一致;id / isEnabled 两项前端更严(后端为公开字段)——
  方向安全,不会产生空列
· 前端 vite build 通过
This commit is contained in:
yueli
2026-09-17 11:20:59 +08:00
parent 70071e111a
commit 2f7a81ff3d
2 changed files with 40 additions and 34 deletions

View File

@ -241,15 +241,15 @@
style="width: 100%; border-top: none;"
>
<el-table-column v-if="isBatchMode" type="selection" width="55" :reserve-selection="true" />
<el-table-column v-if="columns.id.visible" prop="id" label="ID" min-width="80" align="center" fixed="left" />
<el-table-column v-if="columns.id.visible && hasColPermission('id')" prop="id" label="ID" min-width="80" align="center" fixed="left" />
<el-table-column v-if="columns.companyName.visible" prop="companyName" label="所属公司" min-width="100" align="center" show-overflow-tooltip sortable="custom">
<el-table-column v-if="columns.companyName.visible && hasColPermission('companyName')" prop="companyName" label="所属公司" min-width="100" align="center" show-overflow-tooltip sortable="custom">
<template #default="scope">
<span>{{ scope.row.companyName || '-' }}</span>
</template>
</el-table-column>
<el-table-column v-if="columns.name.visible" label="名称" min-width="160" show-overflow-tooltip sortable="custom">
<el-table-column v-if="columns.name.visible && hasColPermission('name')" label="名称" min-width="160" show-overflow-tooltip sortable="custom">
<template #default="scope">
<span v-if="userStore.hasPermission('material_list:operation')" class="clickable-text" @click="handleEdit(scope.row)">
{{ scope.row.name }}
@ -258,35 +258,35 @@
</template>
</el-table-column>
<el-table-column v-if="columns.commonName.visible" prop="commonName" label="专业名称" min-width="140" show-overflow-tooltip sortable="custom">
<el-table-column v-if="columns.commonName.visible && hasColPermission('commonName')" prop="commonName" label="专业名称" min-width="140" show-overflow-tooltip sortable="custom">
<template #default="scope">
<span v-if="scope.row.commonName">{{ scope.row.commonName }}</span>
<span v-else style="color: #ccc;">-</span>
</template>
</el-table-column>
<el-table-column v-if="columns.category.visible" prop="category" label="类别" min-width="140" show-overflow-tooltip sortable="custom">
<el-table-column v-if="columns.category.visible && hasColPermission('category')" prop="category" label="类别" min-width="140" show-overflow-tooltip sortable="custom">
<template #default="scope">{{ scope.row.category || '-' }}</template>
</el-table-column>
<el-table-column v-if="columns.type.visible" prop="type" label="类型" min-width="120" align="center" show-overflow-tooltip sortable="custom">
<el-table-column v-if="columns.type.visible && hasColPermission('type')" prop="type" label="类型" min-width="120" align="center" show-overflow-tooltip sortable="custom">
<template #default="scope">{{ scope.row.type || '-' }}</template>
</el-table-column>
<el-table-column v-if="columns.spec.visible" prop="spec" label="规格型号" min-width="180" show-overflow-tooltip sortable="custom" />
<el-table-column v-if="columns.unit.visible" prop="unit" label="单位" min-width="80" align="center" sortable="custom" />
<el-table-column v-if="columns.spec.visible && hasColPermission('spec')" prop="spec" label="规格型号" min-width="180" show-overflow-tooltip sortable="custom" />
<el-table-column v-if="columns.unit.visible && hasColPermission('unit')" prop="unit" label="单位" min-width="80" align="center" sortable="custom" />
<el-table-column v-if="columns.inventory.visible" prop="inventoryCount" label="库存数" min-width="100" align="center" sortable="custom">
<el-table-column v-if="columns.inventory.visible && hasColPermission('inventory')" prop="inventoryCount" label="库存数" min-width="100" align="center" sortable="custom">
<template #default="{ row }">
<span>{{ row.inventoryCount }}</span>
</template>
</el-table-column>
<el-table-column v-if="columns.available.visible" prop="availableCount" label="可用数" min-width="100" align="center" sortable="custom">
<el-table-column v-if="columns.available.visible && hasColPermission('available')" prop="availableCount" label="可用数" min-width="100" align="center" sortable="custom">
<template #default="{ row }">
<span :style="{ fontWeight: 'bold', color: row.availableCount > 0 ? '#409EFF' : 'inherit' }">{{ row.availableCount }}</span>
</template>
</el-table-column>
<el-table-column v-if="columns.files.visible" label="资料" min-width="140" align="center">
<el-table-column v-if="columns.files.visible && hasColPermission('files')" label="资料" min-width="140" align="center">
<template #default="{ row }">
<div style="display: flex; gap: 8px; justify-content: center;">
<div v-if="getImagesOnly(row.generalImage).length > 0" class="file-preview-cell">
@ -330,7 +330,7 @@
</template>
</el-table-column>
<el-table-column v-if="columns.isEnabled.visible" prop="isEnabled" label="是否启用" min-width="100" align="center">
<el-table-column v-if="columns.isEnabled.visible && hasColPermission('isEnabled')" prop="isEnabled" label="是否启用" min-width="100" align="center">
<template #default="scope">
<el-switch
v-model="scope.row.isEnabled"
@ -342,20 +342,20 @@
/>
</template>
</el-table-column>
<el-table-column v-if="columns.isInspectionRequired.visible" prop="isInspectionRequired" label="强制质检" min-width="100" align="center">
<el-table-column v-if="columns.isInspectionRequired.visible && hasColPermission('isInspectionRequired')" prop="isInspectionRequired" label="强制质检" min-width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.isInspectionRequired ? 'danger' : 'info'" size="small">
{{ scope.row.isInspectionRequired ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column v-if="columns.referencePrice.visible" prop="referencePrice" label="参考价格" min-width="120" align="center" sortable="custom">
<el-table-column v-if="columns.referencePrice.visible && hasColPermission('referencePrice')" prop="referencePrice" label="参考价格" min-width="120" align="center" sortable="custom">
<template #default="scope">
<span v-if="scope.row.referencePrice != null" class="money-text">{{ scope.row.referencePrice?.toFixed(2) }}</span>
<span v-else style="color: #ccc;">-</span>
</template>
</el-table-column>
<el-table-column v-if="columns.warningStatus.visible" label="预警状态" width="120" align="center">
<el-table-column v-if="columns.warningStatus.visible && hasColPermission('warningStatus')" label="预警状态" width="120" align="center">
<template #default="{ row }">
<template v-if="row.warningStatus === 2">
<el-tag type="danger" size="small">红色预警</el-tag>
@ -1012,7 +1012,9 @@ const permissionMap: Record<string, string> = {
commonName: 'material_list:commonName', category: 'material_list:category', type: 'material_list:type',
spec: 'material_list:spec', unit: 'material_list:unit', inventory: 'material_list:inventoryCount',
available: 'material_list:availableCount', files: 'material_list:files', isEnabled: 'material_list:isEnabled',
isInspectionRequired: 'material_list:operation', referencePrice: 'material_list:referencePrice',
// ★ 与后端读过滤field_permissions.py对齐原先写 material_list:operation
// 会与后端口径不一致,渲染出一列全是「-」的空表头
isInspectionRequired: 'material_list:isInspectionRequired', referencePrice: 'material_list:referencePrice',
warningStatus: 'material_list:view_warning'
};

View File

@ -218,15 +218,15 @@
style="width: 100%; margin-top: 15px"
>
<el-table-column v-if="isBatchMode" type="selection" width="55" :reserve-selection="true" />
<el-table-column v-if="columns.id.visible" prop="id" label="ID" min-width="80" align="center" fixed="left" />
<el-table-column v-if="columns.id.visible && hasColPermission('id')" prop="id" label="ID" min-width="80" align="center" fixed="left" />
<el-table-column v-if="columns.companyName.visible" prop="companyName" label="所属公司" min-width="100" align="center" show-overflow-tooltip sortable="custom">
<el-table-column v-if="columns.companyName.visible && hasColPermission('companyName')" prop="companyName" label="所属公司" min-width="100" align="center" show-overflow-tooltip sortable="custom">
<template #default="scope">
<span>{{ scope.row.companyName || '-' }}</span>
</template>
</el-table-column>
<el-table-column v-if="columns.name.visible" label="名称" min-width="160" show-overflow-tooltip sortable="custom">
<el-table-column v-if="columns.name.visible && hasColPermission('name')" label="名称" min-width="160" show-overflow-tooltip sortable="custom">
<template #default="scope">
<span v-if="userStore.hasPermission('material_list:operation')" class="clickable-text" @click="handleEdit(scope.row)">
{{ scope.row.name }}
@ -235,35 +235,35 @@
</template>
</el-table-column>
<el-table-column v-if="columns.commonName.visible" prop="commonName" label="专业名称" min-width="140" show-overflow-tooltip sortable="custom">
<el-table-column v-if="columns.commonName.visible && hasColPermission('commonName')" prop="commonName" label="专业名称" min-width="140" show-overflow-tooltip sortable="custom">
<template #default="scope">
<span v-if="scope.row.commonName">{{ scope.row.commonName }}</span>
<span v-else style="color: #ccc;">-</span>
</template>
</el-table-column>
<el-table-column v-if="columns.category.visible" prop="category" label="类别" min-width="140" show-overflow-tooltip sortable="custom">
<el-table-column v-if="columns.category.visible && hasColPermission('category')" prop="category" label="类别" min-width="140" show-overflow-tooltip sortable="custom">
<template #default="scope">{{ scope.row.category || '-' }}</template>
</el-table-column>
<el-table-column v-if="columns.type.visible" prop="type" label="类型" min-width="120" align="center" show-overflow-tooltip sortable="custom">
<el-table-column v-if="columns.type.visible && hasColPermission('type')" prop="type" label="类型" min-width="120" align="center" show-overflow-tooltip sortable="custom">
<template #default="scope">{{ scope.row.type || '-' }}</template>
</el-table-column>
<el-table-column v-if="columns.spec.visible" prop="spec" label="规格型号" min-width="180" show-overflow-tooltip sortable="custom" />
<el-table-column v-if="columns.unit.visible" prop="unit" label="单位" min-width="80" align="center" sortable="custom" />
<el-table-column v-if="columns.spec.visible && hasColPermission('spec')" prop="spec" label="规格型号" min-width="180" show-overflow-tooltip sortable="custom" />
<el-table-column v-if="columns.unit.visible && hasColPermission('unit')" prop="unit" label="单位" min-width="80" align="center" sortable="custom" />
<el-table-column v-if="columns.inventory.visible" prop="inventoryCount" label="库存数" min-width="100" align="center" sortable="custom">
<el-table-column v-if="columns.inventory.visible && hasColPermission('inventory')" prop="inventoryCount" label="库存数" min-width="100" align="center" sortable="custom">
<template #default="{ row }">
<span>{{ row.inventoryCount }}</span>
</template>
</el-table-column>
<el-table-column v-if="columns.available.visible" prop="availableCount" label="可用数" min-width="100" align="center" sortable="custom">
<el-table-column v-if="columns.available.visible && hasColPermission('available')" prop="availableCount" label="可用数" min-width="100" align="center" sortable="custom">
<template #default="{ row }">
<span :style="{ fontWeight: 'bold', color: row.availableCount > 0 ? '#409EFF' : 'inherit' }">{{ row.availableCount }}</span>
</template>
</el-table-column>
<el-table-column v-if="columns.files.visible" label="资料" min-width="140" align="center">
<el-table-column v-if="columns.files.visible && hasColPermission('files')" label="资料" min-width="140" align="center">
<template #default="{ row }">
<div style="display: flex; gap: 8px; justify-content: center;">
<div v-if="getImagesOnly(row.generalImage).length > 0" class="file-preview-cell">
@ -309,7 +309,7 @@
</template>
</el-table-column>
<el-table-column v-if="columns.isEnabled.visible" prop="isEnabled" label="是否启用" min-width="100" align="center">
<el-table-column v-if="columns.isEnabled.visible && hasColPermission('isEnabled')" prop="isEnabled" label="是否启用" min-width="100" align="center">
<template #default="scope">
<el-switch
v-model="scope.row.isEnabled"
@ -321,14 +321,14 @@
/>
</template>
</el-table-column>
<el-table-column v-if="columns.isInspectionRequired.visible" prop="isInspectionRequired" label="强制质检" min-width="100" align="center">
<el-table-column v-if="columns.isInspectionRequired.visible && hasColPermission('isInspectionRequired')" prop="isInspectionRequired" label="强制质检" min-width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.isInspectionRequired ? 'danger' : 'info'" size="small">
{{ scope.row.isInspectionRequired ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column v-if="columns.isApprovalRequired.visible && userStore.hasPermission('material_list:isApprovalRequired')" label="出库/借库需审批" min-width="130" align="center">
<el-table-column v-if="columns.isApprovalRequired.visible && hasColPermission('isApprovalRequired')" label="出库/借库需审批" min-width="130" align="center">
<template #default="scope">
<el-switch
:model-value="!!scope.row.isApprovalRequired"
@ -337,13 +337,13 @@
/>
</template>
</el-table-column>
<el-table-column v-if="columns.referencePrice.visible" prop="referencePrice" label="参考价格" min-width="120" align="center" sortable="custom">
<el-table-column v-if="columns.referencePrice.visible && hasColPermission('referencePrice')" prop="referencePrice" label="参考价格" min-width="120" align="center" sortable="custom">
<template #default="scope">
<span v-if="scope.row.referencePrice != null" class="money-text">{{ scope.row.referencePrice?.toFixed(2) }}</span>
<span v-else style="color: #ccc;">-</span>
</template>
</el-table-column>
<el-table-column v-if="columns.warningStatus.visible" label="预警状态" width="120" align="center">
<el-table-column v-if="columns.warningStatus.visible && hasColPermission('warningStatus')" label="预警状态" width="120" align="center">
<template #default="{ row }">
<template v-if="row.warningStatus === 2">
<el-tag type="danger" size="small">红色预警</el-tag>
@ -993,8 +993,12 @@ const permissionMap: Record<string, string> = {
available: 'material_list:availableCount',
files: 'material_list:files',
isEnabled: 'material_list:isEnabled',
isInspectionRequired: 'material_list:operation',
isApprovalRequired: 'material_list:operation',
// ★ 这两处原先都写成 material_list:operation,与后端**读过滤实际要求的码不一致**
// (见 app/utils/field_permissions.py。前端按 operation 判断会以为「有权限」,
// 而后端其实把字段抹成了 null —— 于是渲染出一列全是「-」的空表头尸体。
// 现与后端逐一对齐:
isInspectionRequired: 'material_list:isInspectionRequired',
isApprovalRequired: 'material_list:isApprovalRequired',
referencePrice: 'material_list:referencePrice',
warningStatus: 'material_list:view_warning'
};