Files
KCGL/inventory-web/src/views/material/list.vue

1559 lines
56 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container">
<el-card shadow="never">
<div class="filter-wrapper">
<div class="filter-container">
<el-input
v-model="queryParams.keyword"
placeholder="请输入搜索关键字"
style="width: 320px; margin-right: 10px;"
clearable
@input="handleInputSearch"
>
<template #prepend>
<el-select v-model="queryParams.searchField" style="width: 90px" @change="handleQuery">
<el-option label="全部" value="all" />
<el-option label="名称" value="name" />
<el-option label="俗名" value="common_name" />
<el-option label="规格" value="spec" />
</el-select>
</template>
</el-input>
<el-select
v-model="queryParams.company"
placeholder="所属公司"
clearable
filterable
default-first-option
style="width: 120px; margin-right: 10px;"
@change="handleQuery"
>
<el-option v-for="item in companyOptions" :key="item" :label="item" :value="item" />
</el-select>
<el-select
v-model="queryParams.category"
placeholder="类别"
clearable
filterable
allow-create
default-first-option
style="width: 240px; margin-right: 10px;"
@change="handleQuery"
popper-class="long-dropdown"
>
<el-option v-for="item in categoryOptions" :key="item" :label="item" :value="item" />
</el-select>
<el-select
v-model="queryParams.type"
placeholder="类型"
clearable
filterable
allow-create
default-first-option
style="width: 140px; margin-right: 10px;"
@change="handleQuery"
popper-class="long-dropdown"
>
<el-option v-for="item in typeOptions" :key="item" :label="item" :value="item" />
</el-select>
<el-select
v-model="queryParams.isEnabled"
placeholder="状态"
clearable
style="width: 100px; margin-right: 10px;"
@change="handleQuery"
>
<el-option label="启用" :value="true" />
<el-option label="禁用" :value="false" />
</el-select>
<el-select
v-model="queryParams.has_stock"
placeholder="库存状态"
clearable
style="width: 120px; margin-right: 10px;"
@change="handleQuery"
>
<el-option label="全部" value="" />
<el-option label="仅看有库存" value="true" />
</el-select>
<el-button type="primary" plain @click="handleQuery">搜索</el-button>
<el-button plain @click="resetQuery">重置</el-button>
<el-popover
v-model:visible="advancedFilterVisible"
placement="bottom"
title="高级筛选"
width="600"
trigger="manual">
<template #reference>
<el-button plain @click="advancedFilterVisible = !advancedFilterVisible">高级筛选</el-button>
</template>
<div class="advanced-filter">
<div v-for="(condition, index) in advancedConditions" :key="index" class="condition-row" style="display: flex; align-items: center; margin-bottom: 10px;">
<el-select v-model="condition.field" placeholder="字段" style="width: 180px" :teleported="false">
<el-option v-for="field in fieldOptions" :key="field.value" :label="field.label" :value="field.value" />
</el-select>
<el-select v-model="condition.operator" placeholder="操作符" style="width: 120px; margin-left: 8px" :teleported="false">
<el-option v-for="op in operatorOptions" :key="op.value" :label="op.label" :value="op.value" />
</el-select>
<el-input v-model="condition.value" placeholder="值" style="width: 180px; margin-left: 8px" />
<el-button v-if="advancedConditions.length > 1" type="danger" link @click="removeCondition(index)" style="margin-left: 8px">删除</el-button>
</div>
<div style="margin-top: 12px">
<el-button type="primary" link @click="addCondition">添加条件</el-button>
<el-button @click="applyAdvancedFilter" type="primary">应用筛选</el-button>
<el-button @click="resetAdvancedFilter">重置</el-button>
</div>
</div>
</el-popover>
</div>
<div class="right-toolbar">
<el-button type="success" plain @click="handleExport" :loading="exportLoading" style="margin-right: 10px">
<el-icon style="margin-right: 5px"><Download /></el-icon>导出库存统计
</el-button>
<!-- 批量操作按钮 (需要相应权限) -->
<template v-if="!isBatchMode">
<el-button
v-if="userStore.hasPermission('material_list:edit_warning')"
type="warning"
plain
@click="enterBatchMode('warning')"
style="margin-right: 10px"
>
<el-icon style="margin-right: 5px"><Bell /></el-icon>批量设置预警
</el-button>
<el-button
v-if="userStore.hasPermission('material_list:operation')"
type="danger"
plain
@click="enterBatchMode('inspection')"
style="margin-right: 10px"
>
<el-icon style="margin-right: 5px"><CircleCheck /></el-icon>批量质检设置
</el-button>
</template>
<template v-else>
<el-button @click="cancelBatchMode">取消选择</el-button>
<el-button type="primary" @click="confirmBatchSelection">确认勾选</el-button>
</template>
<el-button v-if="userStore.hasPermission('material_list:operation')" type="primary" @click="handleAdd" style="margin-right: 10px">
<el-icon style="margin-right: 5px"><Plus /></el-icon>新增
</el-button>
<el-tooltip content="刷新" placement="top">
<el-button circle :icon="Refresh" @click="getList" />
</el-tooltip>
<el-dropdown trigger="click" @command="handleSizeChange">
<el-button circle :icon="Rank" style="margin-left: 8px" title="表格密度" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="large">宽松 (默认)</el-dropdown-item>
<el-dropdown-item command="default">中等</el-dropdown-item>
<el-dropdown-item command="small">紧凑</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-popover placement="bottom" :width="200" trigger="click">
<template #reference>
<el-button circle :icon="Setting" style="margin-left: 8px" title="列设置" />
</template>
<div class="column-setting-list">
<div style="font-weight: bold; margin-bottom: 5px; border-bottom: 1px solid #eee; padding-bottom: 5px">
列展示设置
</div>
<el-checkbox v-model="columns.id.visible" label="ID" />
<el-checkbox v-model="columns.companyName.visible" label="所属公司" />
<el-checkbox v-model="columns.name.visible" label="名称" />
<el-checkbox v-model="columns.commonName.visible" label="俗名" />
<el-checkbox v-model="columns.category.visible" label="类别" />
<el-checkbox v-model="columns.type.visible" label="类型" />
<el-checkbox v-model="columns.spec.visible" label="规格型号" />
<el-checkbox v-model="columns.unit.visible" label="单位" />
<el-checkbox v-model="columns.inventory.visible" label="库存数" />
<el-checkbox v-model="columns.available.visible" label="可用数" />
<el-checkbox v-model="columns.files.visible" label="资料" />
<el-checkbox v-model="columns.isEnabled.visible" label="状态" />
</div>
</el-popover>
</div>
</div>
<el-table
ref="tableRef"
v-loading="loading"
:data="tableData"
border
stripe
:size="tableSize"
:row-class-name="tableRowClassName"
@sort-change="handleSortChange"
@selection-change="handleSelectionChange"
style="width: 100%; margin-top: 15px"
>
<el-table-column v-if="isBatchMode" type="selection" width="55" />
<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.companyName.visible" 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" prop="name" label="名称" min-width="160" show-overflow-tooltip sortable="custom" />
<el-table-column v-if="columns.commonName.visible" 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">
<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">
<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.inventory.visible" 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">
<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">
<template #default="{ row }">
<div style="display: flex; gap: 8px; justify-content: center;">
<div v-if="getImagesOnly(row.generalImage).length > 0" class="file-preview-cell">
<el-image
style="width: 32px; height: 32px; border-radius: 4px;"
:src="getImageUrl(getImagesOnly(row.generalImage)[0])"
:preview-src-list="getImagesOnly(row.generalImage).map(u => getImageUrl(u))"
preview-teleported
fit="cover"
/>
<span v-if="getImagesOnly(row.generalImage).length > 1" class="more-badge">+{{getImagesOnly(row.generalImage).length}}</span>
</div>
<el-popover v-if="row.generalManual && row.generalManual.length > 0" placement="top" trigger="hover" width="200">
<template #reference>
<el-button link type="primary" :icon="Document" />
</template>
<div style="display: flex; flex-direction: column; gap: 5px;">
<div v-for="(link, idx) in row.generalManual" :key="idx">
<el-link v-if="isExternalLink(link)" :href="link" target="_blank" type="primary" :underline="false">
说明书 {{idx+1}} <el-icon><Link /></el-icon>
</el-link>
<el-image v-else-if="isImageFile(link)"
style="width: 100px; height: 100px"
:src="getImageUrl(link)"
:preview-src-list="[getImageUrl(link)]"
fit="cover"
preview-teleported
/>
<el-link v-else :href="getImageUrl(link)" target="_blank" type="info">PDF 文件 {{idx+1}}</el-link>
</div>
</div>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column v-if="columns.isEnabled.visible" prop="isEnabled" label="是否启用" min-width="100" align="center">
<template #default="scope">
<el-switch
v-model="scope.row.isEnabled"
:active-value="true"
:inactive-value="false"
:loading="scope.row.statusLoading"
:disabled="!userStore.hasPermission('material_list:operation')"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column v-if="columns.isInspectionRequired.visible" 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="userStore.hasPermission('material_list:operation')" label="操作" min-width="200" fixed="right" align="center">
<template #default="scope">
<el-button v-if="userStore.hasPermission('material_list:operation')" link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button v-if="userStore.hasPermission('material_list:edit_warning')" link type="warning" size="small" @click="handleSetSingleWarning(scope.row)">设置预警</el-button>
<el-button v-if="userStore.hasPermission('material_list:operation')" link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handlePageSizeChange"
@current-change="handlePageCurrentChange"
/>
</div>
<el-dialog
v-model="dialog.visible"
:title="dialog.title"
width="700px"
append-to-body
@close="cancel"
:close-on-click-modal="!isUploading"
:close-on-press-escape="!isUploading"
:show-close="!isUploading"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
<el-row>
<el-col :span="12">
<el-form-item label="名称" prop="name" v-if="hasFieldPermission('name')">
<el-input v-model="form.name" placeholder="内部名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="俗名" prop="commonName" v-if="hasFieldPermission('commonName')">
<el-input v-model="form.commonName" placeholder="标准名称" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="所属公司" prop="companyName" v-if="hasFieldPermission('companyName')">
<el-autocomplete
v-model="form.companyName"
:fetch-suggestions="querySearchCompany"
placeholder="请输入公司名称"
clearable
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="类别" prop="category" v-if="hasFieldPermission('category')">
<div style="display: flex; width: 100%; align-items: center;">
<el-cascader
v-model="tempCategoryPrefix"
:options="categoryTreeOptions"
:props="{ expandTrigger: 'hover', checkStrictly: true, emitPath: true }"
placeholder="选择前缀层级"
filterable
clearable
style="width: 50%;"
/>
<div style="padding: 0 8px; font-weight: bold; color: #909399;">/</div>
<el-input
v-model="tempCategorySuffix"
placeholder="填写具体名称"
clearable
style="width: 50%;"
/>
</div>
<div style="font-size: 12px; color: #E6A23C; margin-top: 4px; line-height: 1.2;">
* 必须构成4层结构
</div>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="类型" prop="type" v-if="hasFieldPermission('type')">
<el-autocomplete
v-model="form.type"
:fetch-suggestions="querySearchType"
placeholder="可输入或选择"
clearable
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="规格型号" prop="spec" v-if="hasFieldPermission('spec')">
<el-input v-model="form.spec" placeholder="请输入规格型号" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="计量单位" prop="unit" v-if="hasFieldPermission('unit')">
<el-input v-model="form.unit" placeholder=": , , " />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="可见等级" prop="visibilityLevel">
<el-input-number v-model="form.visibilityLevel" :min="0" :max="9" label="等级" />
<span style="margin-left: 10px; color: #999; font-size: 12px;">(0低-9高)</span>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="产品图" prop="generalImage" v-if="hasFieldPermission('files')">
<div class="upload-container">
<el-upload
v-model:file-list="fileListImage"
action="#"
list-type="picture-card"
multiple
:http-request="(opts) => customUpload(opts, 'generalImage')"
:on-preview="handlePreviewPicture"
:on-remove="(file) => handleRemoveImage(file, 'generalImage')"
:before-upload="beforeAvatarUpload"
>
<el-icon><Plus /></el-icon>
</el-upload>
<div class="camera-card" @click="triggerCamera('generalImage')">
<el-icon><Camera /></el-icon><span class="text">拍照</span>
</div>
</div>
<el-input
v-model="imageExternalUrl"
placeholder="如有外部图片链接请在此输入"
style="margin-top: 8px;"
clearable
>
<template #prefix><el-icon><Link /></el-icon></template>
</el-input>
</el-form-item>
<el-form-item label="说明书" prop="generalManual" v-if="hasFieldPermission('files')">
<div class="upload-container">
<el-upload
v-model:file-list="fileListManual"
action="#"
list-type="picture-card"
multiple
:http-request="(opts) => customUpload(opts, 'generalManual')"
:on-preview="handlePreviewPicture"
:on-remove="(file) => handleRemoveImage(file, 'generalManual')"
:before-upload="beforeAvatarUpload"
>
<el-icon><Plus /></el-icon>
</el-upload>
<div class="camera-card" @click="triggerCamera('generalManual')">
<el-icon><Camera /></el-icon><span class="text">拍照</span>
</div>
</div>
<el-input
v-model="manualExternalUrl"
placeholder="如有外部说明书链接请在此输入"
style="margin-top: 8px;"
clearable
>
<template #prefix><el-icon><Link /></el-icon></template>
</el-input>
</el-form-item>
<el-form-item label="状态" prop="isEnabled" v-if="hasFieldPermission('isEnabled')">
<el-radio-group v-model="form.isEnabled">
<el-radio :value="true">启用</el-radio>
<el-radio :value="false">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel" :disabled="isUploading">取 消</el-button>
<el-button type="primary" @click="submitForm" :loading="submitLoading || isUploading">确 定</el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="dialogVisibleImage" append-to-body width="50%">
<img style="width: 100%" :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
<el-dialog v-model="cameraDialogVisible" title="拍照上传" width="500px" append-to-body destroy-on-close :close-on-click-modal="false">
<WebRtcCamera
ref="cameraRef"
@photo-submit="handleCameraConfirm"
@cancel="cameraDialogVisible = false"
/>
</el-dialog>
<!-- 预警设置弹窗 -->
<el-dialog v-model="warningDialog.visible" :title="warningDialog.title" width="500px" append-to-body destroy-on-close>
<el-form ref="warningFormRef" :model="warningForm" :rules="warningRules" label-width="100px">
<el-alert
v-if="warningDialog.selectedCount > 1"
:title="`正在批量设置 ${warningDialog.selectedCount} 条物料的预警`"
type="info"
:closable="false"
style="margin-bottom: 15px"
/>
<el-form-item label="启用预警" prop="isEnabled">
<el-switch v-model="warningForm.isEnabled" />
</el-form-item>
<el-form-item label="红色阈值" prop="redThreshold" v-if="warningForm.isEnabled">
<el-input-number v-model="warningForm.redThreshold" :min="0" :precision="0" step="1" placeholder="库存此值为红色预警" style="width: 100%" />
<div class="form-tip">库存数量 ≤ 此值时显示红色预警</div>
</el-form-item>
<el-form-item label="黄色阈值" prop="yellowThreshold" v-if="warningForm.isEnabled">
<el-input-number v-model="warningForm.yellowThreshold" :min="0" :precision="0" step="1" placeholder="库存此值为黄色预警" style="width: 100%" />
<div class="form-tip">红色阈值 &lt; 库存 ≤ 此值时显示黄色预警</div>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="warningDialog.visible = false">取 消</el-button>
<el-button type="primary" @click="submitWarning" :loading="warningLoading">确 定</el-button>
</div>
</template>
</el-dialog>
<!-- 批量质检设置弹窗 -->
<el-dialog v-model="inspectionDialog.visible" title="批量质检设置" width="500px" append-to-body destroy-on-close>
<el-alert
:title="`已选择 ${inspectionDialog.selectedCount} 条物料进行批量质检设置`"
type="info"
:closable="false"
style="margin-bottom: 20px"
/>
<el-form label-position="top">
<el-form-item label="是否强制要求入库上传检测报告">
<el-switch
v-model="inspectionForm.isInspectionRequired"
active-text=" (强制管控)"
inactive-text=" (免检入库)"
/>
<div style="color: #909399; font-size: 12px; width: 100%; margin-top: 8px; line-height: 1.5;">
开启后,这些物料在采购入库时必须上传检测报告文件或填写外部报告链接,否则将被拦截无法入库。
</div>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="inspectionDialog.visible = false">取 消</el-button>
<el-button type="primary" @click="submitBatchInspection" :loading="inspectionLoading">确 定</el-button>
</div>
</template>
</el-dialog>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick, computed } from 'vue';
import { Plus, Document, Refresh, Setting, Rank, Camera, Link, Download, Bell, CircleCheck } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { useUserStore } from '@/stores/user';
import {
listMaterialBase,
addMaterialBase,
updateMaterialBase,
delMaterialBase,
getMaterialBaseOptions,
exportAssetStatistics,
batchSetWarning,
batchSetInspection
} from '@/api/material_base';
import { uploadFile, deleteFile } from '@/api/common/upload';
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue';
const userStore = useUserStore();
// --- 类型定义 ---
interface MaterialBaseVO {
id: number;
companyName: string;
name: string;
commonName?: string;
category: string;
type: string;
spec: string;
unit: string;
visibilityLevel: number;
generalManual: string[];
generalImage: string[];
isEnabled: boolean; // 已彻底修改为布尔值
statusLoading?: boolean;
inventoryCount?: number;
availableCount?: number;
}
interface QueryParams {
pageNum: number;
pageSize: number;
keyword: string;
searchField: string;
category: string;
type: string;
company: string;
isEnabled?: boolean;
orderByColumn: string;
isAsc: string | undefined;
advancedFilters?: any[];
has_stock?: string;
enableWarningSort?: boolean;
}
interface CascaderOption {
value: string;
label: string;
children?: CascaderOption[];
}
// --- 响应式数据 ---
const loading = ref(false);
const exportLoading = ref(false); // 导出加载状态
const total = ref(0);
const tableData = ref<MaterialBaseVO[]>([]);
const tableRef = ref<InstanceType<typeof ElTable>>();
const submitLoading = ref(false);
// 上传锁定状态
const isUploading = ref(false);
const tableSize = ref<'large' | 'default' | 'small'>('large');
const advancedFilterVisible = ref(false);
const advancedConditions = ref([{ field: '', operator: '', value: '' }]);
const fieldOptions = computed(() => {
const allFields = [
{ value: 'companyName', label: '所属公司', perm: 'material_list:companyName' },
{ value: 'name', label: '名称', perm: 'material_list:name' },
{ value: 'commonName', label: '俗名', perm: 'material_list:commonName' },
{ value: 'category', label: '类别', perm: 'material_list:category' },
{ value: 'type', label: '类型', perm: 'material_list:type' },
{ value: 'spec', label: '规格型号', perm: 'material_list:spec' },
{ value: 'unit', label: '单位', perm: 'material_list:unit' },
{ value: 'inventoryCount', label: '库存数', perm: 'material_list:inventoryCount' },
{ value: 'availableCount', label: '可用数', perm: 'material_list:availableCount' }
];
// 根据用户权限过滤
return allFields.filter(item => userStore.hasPermission(item.perm));
});
const operatorOptions = ref([
{ value: 'eq', label: '等于' },
{ value: 'ne', label: '不等于' },
{ value: 'contains', label: '包含' },
{ value: 'ge', label: '大于等于' },
{ value: 'le', label: '小于等于' }
]);
// 文件上传相关
const fileListImage = ref<any[]>([]);
const fileListManual = ref<any[]>([]);
const imageExternalUrl = ref('');
const manualExternalUrl = ref('');
const dialogVisibleImage = ref(false);
const dialogImageUrl = ref('');
const cameraDialogVisible = ref(false);
const cameraRef = ref<InstanceType<typeof WebRtcCamera> | null>(null);
const currentCameraField = ref<'generalImage' | 'generalManual'>('generalImage');
// 复选框选中数据
const selectedItems = ref<MaterialBaseVO[]>([]);
const handleSelectionChange = (selection: MaterialBaseVO[]) => {
selectedItems.value = selection;
};
// 批量操作模式
const isBatchMode = ref(false);
const batchActionType = ref(''); // 'warning' | 'inspection'
const enterBatchMode = (actionType: string) => {
batchActionType.value = actionType;
selectedItems.value = [];
isBatchMode.value = true;
tableRef.value?.clearSelection();
};
const cancelBatchMode = () => {
isBatchMode.value = false;
batchActionType.value = '';
tableRef.value?.clearSelection();
selectedItems.value = [];
};
const confirmBatchSelection = () => {
const selected = tableRef.value?.getSelectionRows() || [];
if (selected.length === 0) {
return ElMessage.warning('请先勾选需要操作的物料');
}
// 根据操作类型路由到对应的业务弹窗
if (batchActionType.value === 'inspection') {
inspectionDialog.selectedIds = selected.map((row: any) => row.id);
inspectionDialog.selectedCount = selected.length;
inspectionForm.isInspectionRequired = false;
inspectionDialog.visible = true;
} else if (batchActionType.value === 'warning') {
// 打开预警设置弹窗
warningDialog.selectedIds = selected.map((row: any) => row.id);
warningDialog.selectedCount = selected.length;
warningForm.isEnabled = false;
warningForm.redThreshold = undefined;
warningForm.yellowThreshold = undefined;
warningDialog.title = '批量设置预警';
warningDialog.visible = true;
}
// 关闭批量模式
isBatchMode.value = false;
batchActionType.value = '';
};
// 兼容旧的 exitBatchMode
const exitBatchMode = () => {
cancelBatchMode();
};
// 预警设置相关
const warningDialog = reactive({
visible: false,
title: '设置预警',
selectedCount: 0,
selectedIds: [] as number[]
});
const warningFormRef = ref<FormInstance>();
const warningLoading = ref(false);
const warningForm = reactive({
isEnabled: false,
redThreshold: undefined as number | undefined,
yellowThreshold: undefined as number | undefined
});
const warningRules = {
yellowThreshold: [
{
validator: (rule: any, value: any, callback: any) => {
if (warningForm.isEnabled && warningForm.redThreshold !== undefined && value !== undefined) {
if (value <= warningForm.redThreshold) {
callback(new Error('黄色阈值必须大于红色阈值'));
} else {
callback();
}
} else {
callback();
}
},
trigger: 'blur'
}
]
};
// 批量质检设置相关
const inspectionDialog = reactive({
visible: false,
selectedCount: 0,
selectedIds: [] as number[]
});
const inspectionLoading = ref(false);
const inspectionForm = reactive({
isInspectionRequired: false
});
const columns = reactive({
id: { visible: false },
companyName: { visible: true },
name: { visible: true },
commonName: { visible: true },
category: { visible: true },
type: { visible: true },
spec: { visible: true },
unit: { visible: true },
inventory: { visible: true },
available: { visible: true },
files: { visible: true },
isEnabled: { visible: true },
isInspectionRequired: { visible: true }
});
// 列与权限Code的映射关系数据库中的code
const permissionMap: Record<string, string> = {
id: 'material_list:id',
companyName: 'material_list:companyName',
name: 'material_list:name',
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'
};
// 根据用户权限初始化列显示状态
const initColumnPermissions = () => {
// 超级管理员跳过权限检查,显示所有列
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return;
}
// 普通用户:严格执行列级权限控制,没有权限的列必须隐藏
Object.keys(columns).forEach(key => {
const code = permissionMap[key];
if (code) {
// 如果不具备该权限,必须设为 false
columns[key].visible = !!userStore.hasPermission(code);
}
});
};
// 检查字段权限(用于表单)
const hasFieldPermission = (field: string) => {
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return true;
}
const code = permissionMap[field];
// 如果permissionMap中没有该字段默认允许
if (!code) {
return true;
}
return userStore.hasPermission(code);
};
const companyOptions = ref<string[]>([]);
const categoryOptions = ref<string[]>([]);
const typeOptions = ref<string[]>([]);
const categoryTreeOptions = ref<CascaderOption[]>([]);
const tempCategoryPrefix = ref<string[]>([]);
const tempCategorySuffix = ref<string>('');
const queryParams = reactive<QueryParams>({
pageNum: 1,
pageSize: 100,
keyword: '',
searchField: 'all',
category: '',
type: '',
company: '',
isEnabled: undefined,
orderByColumn: '',
isAsc: undefined,
advancedFilters: [],
has_stock: ''
});
// --- 弹窗与表单相关 ---
const dialog = reactive({
visible: false,
title: ''
});
const formRef = ref<FormInstance>();
const initForm = {
id: undefined,
companyName: '',
name: '',
commonName: '',
category: '',
type: '',
spec: '',
unit: '',
visibilityLevel: 0,
generalManual: [] as string[],
generalImage: [] as string[],
isEnabled: true // 已修改为默认 true
};
const form = ref({...initForm});
const validateCategoryLevel = (rule: any, value: any, callback: any) => {
const prefixStr = tempCategoryPrefix.value.join('/');
const suffixStr = tempCategorySuffix.value.trim();
if (!prefixStr && !suffixStr) {
callback(new Error('请填写或选择类别'));
return;
}
let fullPath = '';
if (prefixStr && suffixStr) fullPath = prefixStr + '/' + suffixStr;
else if (prefixStr) fullPath = prefixStr;
else fullPath = suffixStr;
const levels = fullPath.split('/').filter(p => p.trim() !== '').length;
if (levels !== 4) {
callback(new Error(`必须严格满足4层结构当前为 ${levels} 层`));
} else {
callback();
}
};
const rules = reactive<FormRules>({
name: [{ required: true, message: '请输入基础信息名称', trigger: 'blur' }],
companyName: [{ required: true, message: '请输入公司名称', trigger: 'change' }],
category: [{ required: true, validator: validateCategoryLevel, trigger: 'change' }],
type: [{ required: true, message: '请选择或输入类型', trigger: 'change' }],
spec: [{ required: true, message: '请输入规格型号', trigger: 'blur' }],
unit: [{ required: true, message: '请输入单位', trigger: 'blur' }]
});
// --- 业务逻辑方法 ---
const buildCategoryTree = (categories: string[]): CascaderOption[] => {
const root: CascaderOption[] = [];
categories.forEach(cat => {
if (!cat) return;
const parts = cat.split('/');
let currentLevel = root;
parts.forEach((part, index) => {
let existingNode = currentLevel.find(n => n.value === part);
if (!existingNode) {
existingNode = { value: part, label: part };
currentLevel.push(existingNode);
}
if (index < parts.length - 1) {
if (!existingNode.children) {
existingNode.children = [];
}
currentLevel = existingNode.children;
}
});
});
return root;
};
const getOptionsList = () => {
getMaterialBaseOptions().then((res: any) => {
if (res.code === 200) {
categoryOptions.value = res.data.categories || [];
typeOptions.value = res.data.types || [];
companyOptions.value = res.data.companies || [];
categoryTreeOptions.value = buildCategoryTree(categoryOptions.value);
}
}).catch(err => {
console.error("获取筛选项失败", err);
});
};
const querySearchCompany = (queryString: string, cb: any) => {
const results = queryString
? companyOptions.value.filter(item => item.toLowerCase().includes(queryString.toLowerCase()))
: companyOptions.value;
const formattedResults = results.map(item => ({ value: item }));
cb(formattedResults);
};
const querySearchType = (queryString: string, cb: any) => {
const results = queryString
? typeOptions.value.filter(item => item.toLowerCase().includes(queryString.toLowerCase()))
: typeOptions.value;
const formattedResults = results.map(item => ({ value: item }));
cb(formattedResults);
};
const getList = () => {
loading.value = true;
// 仅当用户没有进行手动表头排序时才开启预警排序
queryParams.enableWarningSort = userStore.hasPermission('material_list:view_warning') && !queryParams.orderByColumn;
// Stringify advancedFilters to JSON string as backend expects
const params = {
...queryParams,
advancedFilters: JSON.stringify(queryParams.advancedFilters || [])
};
listMaterialBase(params)
.then((response: any) => {
if (response && response.data) {
tableData.value = response.data.items;
total.value = response.data.total;
} else {
tableData.value = [];
total.value = 0;
}
})
.catch((err) => {
console.error(err);
tableData.value = [];
})
.finally(() => {
loading.value = false;
});
};
// [修改] 导出处理函数:修正文件名格式
const handleExport = () => {
exportLoading.value = true;
const params = {
keyword: queryParams.keyword,
company: queryParams.company,
category: queryParams.category,
type: queryParams.type,
isEnabled: queryParams.isEnabled
};
exportAssetStatistics(params)
.then((response: any) => {
const blob = new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
// 构造文件名库存统计_YYYYMMDD_HHMMSS
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hour = String(now.getHours()).padStart(2, '0');
const minute = String(now.getMinutes()).padStart(2, '0');
const second = String(now.getSeconds()).padStart(2, '0');
const filename = `库存统计_${year}${month}${day}_${hour}${minute}${second}.xlsx`;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
ElMessage.success('导出成功');
})
.catch((err) => {
console.error("导出失败", err);
ElMessage.error('导出失败');
})
.finally(() => {
exportLoading.value = false;
});
};
let searchTimer: any = null;
const handleInputSearch = () => {
if (searchTimer) clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
queryParams.pageNum = 1;
getList();
}, 500);
};
const handleSortChange = ({ column, prop, order }: any) => {
const sortableColumns = ['inventoryCount', 'availableCount', 'companyName', 'name', 'commonName', 'category', 'type', 'spec', 'unit'];
if (prop && sortableColumns.includes(prop)) {
queryParams.orderByColumn = prop;
queryParams.isAsc = order === 'ascending' ? 'asc' : order === 'descending' ? 'desc' : undefined;
} else {
queryParams.orderByColumn = '';
queryParams.isAsc = undefined;
}
queryParams.pageNum = 1;
getList();
};
const handleQuery = () => {
queryParams.pageNum = 1;
getList();
};
const resetQuery = () => {
queryParams.keyword = '';
queryParams.searchField = 'all';
queryParams.category = '';
queryParams.type = '';
queryParams.company = '';
queryParams.isEnabled = undefined;
queryParams.orderByColumn = '';
queryParams.isAsc = undefined;
queryParams.has_stock = '';
selectedItems.value = [];
tableRef.value?.clearSelection();
isBatchMode.value = false;
handleQuery();
};
const handleSizeChange = (command: 'large' | 'default' | 'small') => {
tableSize.value = command;
};
const handlePageSizeChange = (val: number) => {
queryParams.pageSize = val;
queryParams.pageNum = 1;
getList();
};
const handlePageCurrentChange = (val: number) => {
queryParams.pageNum = val;
getList();
};
const handleAdd = () => {
resetForm();
dialog.title = '新增基础信息';
dialog.visible = true;
};
const handleEdit = (row: MaterialBaseVO) => {
resetForm();
dialog.title = '编辑基础信息';
dialog.visible = true;
nextTick(() => {
const data = JSON.parse(JSON.stringify(row));
Object.assign(form.value, data);
if (data.category) {
const parts = data.category.split('/');
if (parts.length > 0) {
tempCategorySuffix.value = parts.pop() || '';
tempCategoryPrefix.value = parts;
} else {
tempCategoryPrefix.value = [];
tempCategorySuffix.value = data.category;
}
} else {
tempCategoryPrefix.value = [];
tempCategorySuffix.value = '';
}
const images = row.generalImage || [];
const manuals = row.generalManual || [];
const imgFiles = images.filter(u => !isExternalLink(u));
const imgLinks = images.filter(u => isExternalLink(u));
const manualFiles = manuals.filter(u => !isExternalLink(u));
const manualLinks = manuals.filter(u => isExternalLink(u));
fileListImage.value = imgFiles.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }));
imageExternalUrl.value = imgLinks.length > 0 ? imgLinks[0] : '';
fileListManual.value = manualFiles.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }));
manualExternalUrl.value = manualLinks.length > 0 ? manualLinks[0] : '';
});
};
const checkDuplicate = async (name: string, spec: string): Promise<boolean> => {
try {
const nameRes: any = await listMaterialBase({ pageNum: 1, pageSize: 100, keyword: name, category: '', type: '', company: '' });
if (nameRes.data?.items?.some((item: MaterialBaseVO) => item.name === name && item.id !== form.value.id)) {
ElMessage.error(`已存在名称为 "${name}" 的基础信息!`);
return true;
}
const specRes: any = await listMaterialBase({ pageNum: 1, pageSize: 100, keyword: spec, category: '', type: '', company: '' });
if (specRes.data?.items?.some((item: MaterialBaseVO) => item.spec === spec && item.id !== form.value.id)) {
ElMessage.error(`已存在规格/编号为 "${spec}" 的基础信息!`);
return true;
}
} catch (e) {
return false;
}
return false;
};
const submitForm = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true;
try {
const isDuplicate = await checkDuplicate(form.value.name, form.value.spec);
if (isDuplicate) {
submitLoading.value = false;
return;
}
const finalImageList = form.value.generalImage.filter(item => !isExternalLink(item));
if (imageExternalUrl.value) finalImageList.push(imageExternalUrl.value);
const finalManualList = form.value.generalManual.filter(item => !isExternalLink(item));
if (manualExternalUrl.value) finalManualList.push(manualExternalUrl.value);
const prefixStr = tempCategoryPrefix.value.join('/');
const suffixStr = tempCategorySuffix.value.trim();
let fullCategory = '';
if (prefixStr && suffixStr) fullCategory = prefixStr + '/' + suffixStr;
else fullCategory = prefixStr || suffixStr;
const payload = {
...form.value,
category: fullCategory,
generalImage: finalImageList,
generalManual: finalManualList
};
const requestApi = form.value.id ? updateMaterialBase : addMaterialBase;
const actionText = form.value.id ? '修改' : '新增';
await requestApi(payload);
ElMessage.success(`${actionText}成功`);
dialog.visible = false;
getList();
getOptionsList();
} catch (error: any) {
ElMessage.error(error.msg || '保存失败');
} finally {
submitLoading.value = false;
}
}
});
};
const cancel = () => {
dialog.visible = false;
resetForm();
};
const resetForm = () => {
form.value = JSON.parse(JSON.stringify(initForm));
fileListImage.value = [];
fileListManual.value = [];
tempCategoryPrefix.value = [];
tempCategorySuffix.value = '';
imageExternalUrl.value = '';
manualExternalUrl.value = '';
if (formRef.value) formRef.value.resetFields();
};
// 确保这里的布尔值切换逻辑正确匹配真伪值的切换
const handleStatusChange = (row: MaterialBaseVO) => {
row.statusLoading = true;
const text = row.isEnabled === true ? "启用" : "停用";
const updateData = { id: row.id, isEnabled: row.isEnabled };
updateMaterialBase(updateData)
.then(() => ElMessage.success(`已${text} "${row.name}"`))
.catch(() => { row.isEnabled = !row.isEnabled; }) // 回退时布尔值反转
.finally(() => { row.statusLoading = false; });
};
const handleDelete = (row: MaterialBaseVO) => {
ElMessageBox.confirm(
`是否确认删除名称为 "${row.name}" 的数据项?`,
"警告",
{ confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }
).then(() => {
delMaterialBase(row.id).then(() => {
ElMessage.success("删除成功");
if (tableData.value.length === 1 && queryParams.pageNum > 1) queryParams.pageNum--;
getList();
getOptionsList();
});
}).catch(() => {});
};
// --- 预警设置函数 ---
// 单条设置预警
const handleSetSingleWarning = (row: MaterialBaseVO) => {
warningDialog.selectedIds = [row.id];
warningDialog.selectedCount = 1;
// 如果已有预警设置则回显
warningForm.isEnabled = row.warningEnabled || false;
warningForm.redThreshold = row.warningRed;
warningForm.yellowThreshold = row.warningYellow;
warningDialog.title = '设置预警';
warningDialog.visible = true;
};
// 提交预警设置
const submitWarning = async () => {
if (!warningFormRef.value) return;
await warningFormRef.value.validate();
// 安全转换数值null/undefined 默认转为 0
const yellow = Number(warningForm.yellowThreshold) || 0;
const red = Number(warningForm.redThreshold) || 0;
// 逻辑校验:启用预警时,黄色阈值必须大于红色阈值
if (warningForm.isEnabled && yellow !== 0 && red !== 0 && yellow <= red) {
ElMessage.warning('黄色阈值必须大于红色阈值');
return;
}
warningLoading.value = true;
try {
const data = warningDialog.selectedIds.map(baseId => ({
baseId,
isEnabled: warningForm.isEnabled,
redThreshold: red,
yellowThreshold: yellow
}));
await batchSetWarning(data);
ElMessage.success('预警设置成功');
warningDialog.visible = false;
selectedItems.value = []; // 清除选中状态
tableRef.value?.clearSelection(); // 清除表格勾选状态
isBatchMode.value = false; // 退出批量模式
getList();
} catch (error: any) {
ElMessage.error(error?.msg || '设置失败');
} finally {
warningLoading.value = false;
}
};
// 提交批量质检设置
const submitBatchInspection = async () => {
if (inspectionDialog.selectedIds.length === 0) {
ElMessage.warning('请先勾选物料');
return;
}
inspectionLoading.value = true;
try {
await batchSetInspection({
ids: inspectionDialog.selectedIds,
isInspectionRequired: inspectionForm.isInspectionRequired
});
ElMessage.success('批量质检设置成功');
inspectionDialog.visible = false;
// 清理批量模式状态
isBatchMode.value = false;
batchActionType.value = '';
selectedItems.value = [];
tableRef.value?.clearSelection();
getList();
} catch (error: any) {
ElMessage.error(error?.msg || '设置失败');
} finally {
inspectionLoading.value = false;
}
};
// 表格行样式(根据预警状态)
const tableRowClassName = ({ row }: { row: MaterialBaseVO }) => {
// 只有拥有 view_warning 权限且有预警状态时才显示特殊样式
if (!userStore.hasPermission('material_list:view_warning')) return '';
const status = (row as any).warningStatus;
if (status === 2) {
return 'warning-row-red'; // 红色预警
} else if (status === 1) {
return 'warning-row-yellow'; // 黄色预警
}
return '';
};
// --- 文件上传辅助函数 ---
const getImageUrl = (url: string) => { return !url ? '' : (url.startsWith('http') ? url : url) }
const isExternalLink = (str: string) => { return str && (str.startsWith('http://') || str.startsWith('https://')) && !str.includes('/api/v1/common/files') }
const getImagesOnly = (list: string[]) => { return !list ? [] : list.filter(item => !isExternalLink(item)) }
const isImageFile = (url: string) => { return /\.(jpg|jpeg|png|gif|webp)$/i.test(url) }
const beforeAvatarUpload = (rawFile: any) => {
const isTypeValid = ['image/jpeg', 'image/png', 'application/pdf'].includes(rawFile.type);
if (!isTypeValid) {
ElMessage.error('仅支持 JPG/PNG/PDF');
return false;
}
if (rawFile.size / 1024 / 1024 > 10) {
ElMessage.error('文件不能超过 10MB');
return false;
}
return true;
}
const customUpload = async (options: any, targetField: 'generalImage' | 'generalManual') => {
const { file, onSuccess, onError } = options
const formData = new FormData()
formData.append('file', file)
isUploading.value = true
try {
const res: any = await uploadFile(formData)
if (res.code === 200) {
const newUrl = res.data.url
form.value[targetField].push(newUrl)
ElMessage.success('上传成功')
onSuccess(res)
} else {
ElMessage.error(res.msg || '上传失败');
onError(new Error(res.msg))
}
} catch (e) {
ElMessage.error('网络错误');
onError(e)
}
finally { isUploading.value = false }
}
const handleRemoveImage = async (uploadFile: any, targetField: 'generalImage' | 'generalManual') => {
try {
const urlToRemove = form.value[targetField].find(u => getImageUrl(u) === uploadFile.url) || uploadFile.url
form.value[targetField] = form.value[targetField].filter(u => u !== urlToRemove)
if (!isExternalLink(urlToRemove)) {
const filename = urlToRemove.split('/').pop();
if (filename) await deleteFile(filename)
}
ElMessage.success('已从列表移除')
} catch (e) { console.error(e) }
}
const handlePreviewPicture = (uploadFile: any) => {
dialogImageUrl.value = uploadFile.url!;
dialogVisibleImage.value = true
}
const triggerCamera = (field: 'generalImage' | 'generalManual') => {
currentCameraField.value = field;
cameraDialogVisible.value = true;
}
const handleCameraConfirm = async (file: File) => {
if (!beforeAvatarUpload(file)) {
cameraDialogVisible.value = false;
return;
}
const formData = new FormData();
formData.append('file', file);
const loadingInstance = ElLoading.service({ text: '照片上传中...', background: 'rgba(0, 0, 0, 0.7)' });
try {
const res: any = await uploadFile(formData);
if (res.code === 200) {
const newUrl = res.data.url;
const field = currentCameraField.value;
form.value[field].push(newUrl);
const fileObj = { name: newUrl.split('/').pop(), url: getImageUrl(newUrl) };
if (field === 'generalImage') {
fileListImage.value.push(fileObj);
} else {
fileListManual.value.push(fileObj);
}
ElMessage.success('拍照上传成功');
cameraDialogVisible.value = false;
} else {
ElMessage.error(res.msg || '上传失败');
}
} catch (e) {
ElMessage.error('上传过程中发生异常');
} finally {
loadingInstance.close();
}
};
const addCondition = () => {
advancedConditions.value.push({ field: '', operator: '', value: '' });
};
const removeCondition = (index: number) => {
advancedConditions.value.splice(index, 1);
};
const applyAdvancedFilter = () => {
// Filter out empty conditions
const validConditions = advancedConditions.value.filter(c => c.field && c.operator && c.value !== '');
queryParams.advancedFilters = validConditions;
advancedFilterVisible.value = false;
queryParams.pageNum = 1;
getList();
};
const resetAdvancedFilter = () => {
advancedConditions.value = [{ field: '', operator: '', value: '' }];
queryParams.advancedFilters = [];
advancedFilterVisible.value = false;
queryParams.pageNum = 1;
getList();
};
onMounted(() => {
// 先根据权限初始化列显示状态
initColumnPermissions();
getList();
getOptionsList();
});
</script>
<style scoped>
.app-container {
padding: 20px;
}
.filter-wrapper {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
flex-wrap: wrap;
}
.filter-container {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
.right-toolbar {
display: flex;
align-items: center;
}
.column-setting-list {
display: flex;
flex-direction: column;
}
.pagination-container {
margin-top: 15px;
display: flex;
justify-content: flex-start;
}
.upload-container { display: flex; flex-wrap: wrap; gap: 8px; }
:deep(.el-upload--picture-card) { width: 100px; height: 100px; line-height: 100px; }
:deep(.el-upload-list--picture-card .el-upload-list__item) { width: 100px; height: 100px; }
.camera-card { width: 100px; height: 100px; background-color: #fbfdff; border: 1px dashed #c0ccda; border-radius: 6px; box-sizing: border-box; display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer; transition: all 0.3s; color: #8c939d; }
.camera-card:hover { border-color: #409EFF; color: #409EFF; }
.camera-card .text { font-size: 12px; margin-top: 5px; }
.camera-card .el-icon { font-size: 24px; }
.file-preview-cell { display: flex; align-items: center; justify-content: center; position: relative; }
.more-badge { position: absolute; top: -5px; right: -5px; background: #909399; color: #fff; border-radius: 10px; padding: 0 4px; font-size: 10px; transform: scale(0.9); }
/* 预警行样式 - 加深颜色 */
:deep(.warning-row-red) {
--el-table-tr-bg-color: #ffcdd2 !important;
background-color: #ffcdd2 !important;
}
:deep(.warning-row-red td) {
background-color: transparent !important;
}
:deep(.warning-row-yellow) {
--el-table-tr-bg-color: #fff59d !important;
background-color: #fff59d !important;
}
:deep(.warning-row-yellow td) {
background-color: transparent !important;
}
/* 表单提示文字 */
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 4px;
line-height: 1.4;
}
</style>
<style>
/* 增加下拉框的最大高度,使其能容纳更多选项而不必频繁滚动 */
.long-dropdown .el-select-dropdown__wrap {
max-height: 600px !important; /* 可以根据屏幕大小适当调整 */
}
</style>