基础信息修改,新增所属公司,同时修正类别排序以及新增时候类别选择的功能

This commit is contained in:
dxc
2026-02-24 15:13:37 +08:00
parent d1ab5f1100
commit 7e2fa8db8e
4 changed files with 214 additions and 98 deletions

View File

@ -11,6 +11,18 @@
@input="handleInputSearch"
/>
<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="类别"
@ -81,6 +93,7 @@
列展示设置
</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="类别" />
@ -105,6 +118,13 @@
style="width: 100%; margin-top: 15px"
>
<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>
<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 />
<el-table-column v-if="columns.commonName.visible" prop="commonName" label="俗名" min-width="140" show-overflow-tooltip>
@ -114,7 +134,7 @@
</template>
</el-table-column>
<el-table-column v-if="columns.category.visible" prop="category" label="类别" min-width="120" align="center" show-overflow-tooltip>
<el-table-column v-if="columns.category.visible" prop="category" label="类别" min-width="140" show-overflow-tooltip>
<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>
@ -196,19 +216,6 @@
</el-table-column>
</el-table>
<div style="margin-top: 20px; text-align: right;">
<el-pagination
background
layout="total, sizes, prev, pager, next, jumper"
:total="total"
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[100, 200, 500, 1000]"
@size-change="getList"
@current-change="getList"
/>
</div>
<el-dialog
v-model="dialog.visible"
:title="dialog.title"
@ -233,16 +240,44 @@
<el-row>
<el-col :span="12">
<el-form-item label="类别" prop="category">
<el-form-item label="所属公司" prop="companyName">
<el-autocomplete
v-model="form.category"
:fetch-suggestions="querySearchCategory"
placeholder="输入或选择"
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">
<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">
<el-autocomplete
@ -254,26 +289,27 @@
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="规格型号" prop="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">
<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="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-form-item label="产品图" prop="generalImage">
<div class="upload-container">
<el-upload
@ -361,9 +397,8 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue';
import { ref, reactive, onMounted, nextTick, computed } from 'vue';
import { Plus, Picture, Document, Refresh, Setting, Rank, Camera, Link } from '@element-plus/icons-vue';
// 修复:引入 ElLoading
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
@ -372,7 +407,7 @@ import {
addMaterialBase,
updateMaterialBase,
delMaterialBase,
getMaterialBaseOptions // 新增引入
getMaterialBaseOptions
} from '@/api/material_base';
import { uploadFile, deleteFile } from '@/api/common/upload';
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue';
@ -380,6 +415,7 @@ import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue';
// --- 类型定义 ---
interface MaterialBaseVO {
id: number;
companyName: string;
name: string;
commonName?: string;
category: string;
@ -391,7 +427,6 @@ interface MaterialBaseVO {
generalImage: string[];
isEnabled: number;
statusLoading?: boolean;
// 新增字段
inventoryCount?: number;
availableCount?: number;
}
@ -402,9 +437,16 @@ interface QueryParams {
keyword: string;
category: string;
type: string;
company: string;
isEnabled?: number;
}
interface CascaderOption {
value: string;
label: string;
children?: CascaderOption[];
}
// --- 响应式数据 ---
const loading = ref(false);
const total = ref(0);
@ -425,28 +467,35 @@ const currentCameraField = ref<'generalImage' | 'generalManual'>('generalImage')
const columns = reactive({
id: { visible: true },
companyName: { visible: true },
name: { visible: true },
commonName: { visible: true },
category: { visible: true },
type: { visible: true },
spec: { visible: true },
unit: { visible: true },
// visibilityLevel: { visible: false }, // 不再使用
inventory: { visible: true }, // 新增
available: { visible: true }, // 新增
inventory: { visible: true },
available: { visible: true },
files: { visible: true },
isEnabled: { visible: true }
});
const companyOptions = ref<string[]>([]);
const categoryOptions = ref<string[]>([]);
const typeOptions = ref<string[]>([]);
const categoryTreeOptions = ref<CascaderOption[]>([]);
// [修改] 将类别拆分为前后两部分进行绑定
const tempCategoryPrefix = ref<string[]>([]); // 前缀部分 (Cascader)
const tempCategorySuffix = ref<string>(''); // 后缀部分 (Input)
const queryParams = reactive<QueryParams>({
pageNum: 1,
pageSize: 100, // 修改默认显示条数为 100
pageSize: 100,
keyword: '',
category: '',
type: '',
company: '',
isEnabled: undefined
});
@ -460,6 +509,7 @@ const formRef = ref<FormInstance>();
const initForm = {
id: undefined,
companyName: '',
name: '',
commonName: '',
category: '',
@ -474,9 +524,40 @@ const initForm = {
const form = ref({...initForm});
// [新增] 自定义验证规则确保拼合后的类别符合4层结构
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;
// 检查层级数量 (以 / 分割后的数组长度)
// 4层结构意味着有3个斜杠例如 A/B/C/D => length 4
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' }],
category: [{ required: true, message: '请选择或输入类别', trigger: 'change' }],
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' }]
@ -484,22 +565,46 @@ const rules = reactive<FormRules>({
// --- 业务逻辑方法 ---
// 获取所有选项(不再依赖当前页数据)
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 querySearchCategory = (queryString: string, cb: any) => {
const querySearchCompany = (queryString: string, cb: any) => {
const results = queryString
? categoryOptions.value.filter(item => item.toLowerCase().includes(queryString.toLowerCase()))
: categoryOptions.value;
? companyOptions.value.filter(item => item.toLowerCase().includes(queryString.toLowerCase()))
: companyOptions.value;
const formattedResults = results.map(item => ({ value: item }));
cb(formattedResults);
};
@ -519,7 +624,6 @@ const getList = () => {
if (response && response.data) {
tableData.value = response.data.items;
total.value = response.data.total;
// 移除 extractDynamicOptions 调用
} else {
tableData.value = [];
total.value = 0;
@ -552,6 +656,7 @@ const resetQuery = () => {
queryParams.keyword = '';
queryParams.category = '';
queryParams.type = '';
queryParams.company = '';
queryParams.isEnabled = undefined;
handleQuery();
};
@ -572,17 +677,31 @@ const handleEdit = (row: MaterialBaseVO) => {
dialog.visible = true;
nextTick(() => {
// 基础字段赋值 - 深拷贝防止引用
Object.assign(form.value, JSON.parse(JSON.stringify(row)));
const data = JSON.parse(JSON.stringify(row));
Object.assign(form.value, data);
// [修改] 解析已有 category拆分为 Prefix 和 Suffix
if (data.category) {
const parts = data.category.split('/');
if (parts.length > 0) {
// 取最后一部分作为 Suffix其余作为 Prefix
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));
@ -619,23 +738,28 @@ const submitForm = async () => {
if (valid) {
submitLoading.value = true;
try {
// 重复校验
const isDuplicate = await checkDuplicate(form.value.name, form.value.spec);
if (isDuplicate) {
submitLoading.value = false;
return;
}
// 整理文件数据:本地上传的路径已经在 form.value 中
// 我们需要重新合并:已有的非外链 + 新的输入框外链
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);
// [修改] 提交前组合字符串Prefix + / + Suffix
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
};
@ -647,7 +771,6 @@ const submitForm = async () => {
ElMessage.success(`${actionText}成功`);
dialog.visible = false;
getList();
// 提交成功后,刷新选项,以防有新的类别/类型被创建
getOptionsList();
} catch (error: any) {
ElMessage.error(error.msg || '保存失败');
@ -667,6 +790,11 @@ 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();
@ -692,7 +820,6 @@ const handleDelete = (row: MaterialBaseVO) => {
ElMessage.success("删除成功");
if (tableData.value.length === 1 && queryParams.pageNum > 1) queryParams.pageNum--;
getList();
// 删除后也刷新选项
getOptionsList();
});
}).catch(() => {});
@ -762,7 +889,6 @@ const triggerCamera = (field: 'generalImage' | 'generalManual') => {
}
const handleCameraConfirm = async (file: File) => {
console.log('✅ 父组件收到照片:', file.name)
if (!beforeAvatarUpload(file)) {
cameraDialogVisible.value = false;
return;
@ -770,7 +896,6 @@ const handleCameraConfirm = async (file: File) => {
const formData = new FormData();
formData.append('file', file);
// 修复点:使用 ElLoading
const loadingInstance = ElLoading.service({ text: '照片上传中...', background: 'rgba(0, 0, 0, 0.7)' });
try {
@ -801,7 +926,7 @@ const handleCameraConfirm = async (file: File) => {
onMounted(() => {
getList();
getOptionsList(); // 初始化下拉选项
getOptionsList();
});
</script>