feat(bid): 完成物料分类体系全功能开发

1. 新增物料分类删除校验,支持校验子分类和关联物料
2. 优化物料列表查询,支持按分类及其后代分类筛选
3. 重构物料详情和编辑页面,使用级联选择器选择分类
4. 新增分类管理页面,支持增删改查分类树形结构
5. 新增物料分类初始化SQL脚本,导入标准分类体系
This commit is contained in:
2026-06-17 10:12:34 +08:00
parent 38f6246090
commit c7d4c2b0ad
9 changed files with 621 additions and 99 deletions

View File

@@ -33,6 +33,10 @@ public class BizMaterialCategoryController extends BaseController {
@DeleteMapping("/{categoryId}")
public AjaxResult remove(@PathVariable Long categoryId) {
return toAjax(service.deleteCategory(categoryId));
try {
return toAjax(service.deleteCategory(categoryId));
} catch (Exception e) {
return AjaxResult.error(e.getMessage());
}
}
}

View File

@@ -7,6 +7,12 @@ import java.util.List;
public interface BizMaterialCategoryMapper {
List<BizMaterialCategory> selectCategoryList();
BizMaterialCategory selectCategoryById(@Param("categoryId") Long categoryId);
/** 查询子分类数量 */
int countChildren(@Param("parentId") Long parentId);
/** 查询关联该分类的物料数量 */
int countMaterials(@Param("categoryId") Long categoryId);
/** 查询所有后代分类ID通过ancestors LIKE */
List<Long> selectDescendantIds(@Param("ancestors") String ancestors);
int insertCategory(BizMaterialCategory category);
int updateCategory(BizMaterialCategory category);
int deleteCategory(@Param("categoryId") Long categoryId);

View File

@@ -7,5 +7,6 @@ public interface IBizMaterialCategoryService {
List<BizMaterialCategory> selectCategoryList();
int insertCategory(BizMaterialCategory category);
int updateCategory(BizMaterialCategory category);
int deleteCategory(Long categoryId);
/** 删除分类(含子分类和物料校验) */
int deleteCategory(Long categoryId) throws Exception;
}

View File

@@ -36,7 +36,19 @@ public class BizMaterialCategoryServiceImpl implements IBizMaterialCategoryServi
public int updateCategory(BizMaterialCategory cat) { return mapper.updateCategory(cat); }
@Override
public int deleteCategory(Long categoryId) { return mapper.deleteCategory(categoryId); }
public int deleteCategory(Long categoryId) throws Exception {
// 校验子分类
int childCount = mapper.countChildren(categoryId);
if (childCount > 0) {
throw new Exception("该分类下存在子分类,无法删除");
}
// 校验关联物料
int materialCount = mapper.countMaterials(categoryId);
if (materialCount > 0) {
throw new Exception("该分类下存在物料,无法删除");
}
return mapper.deleteCategory(categoryId);
}
private List<BizMaterialCategory> buildTree(List<BizMaterialCategory> all) {
Map<Long, BizMaterialCategory> map = new LinkedHashMap<>();

View File

@@ -16,6 +16,20 @@
FROM biz_material_category WHERE category_id = #{categoryId}
</select>
<select id="countChildren" resultType="int">
SELECT COUNT(*) FROM biz_material_category WHERE parent_id = #{parentId}
</select>
<select id="countMaterials" resultType="int">
SELECT COUNT(*) FROM biz_material WHERE category_id = #{categoryId}
</select>
<select id="selectDescendantIds" resultType="java.lang.Long">
SELECT category_id FROM biz_material_category
WHERE ancestors LIKE CONCAT(#{ancestors}, ',%')
OR ancestors = #{ancestors}
</select>
<insert id="insertCategory" useGeneratedKeys="true" keyProperty="categoryId">
INSERT INTO biz_material_category (tenant_id,category_name,parent_id,ancestors,sort,status,create_by,create_time)
VALUES (1,#{categoryName},#{parentId},#{ancestors},#{sort},#{status},#{createBy},NOW())

View File

@@ -29,7 +29,16 @@
LEFT JOIN biz_material_category c ON m.category_id = c.category_id
<where>
<if test="tenantId != null"> AND m.tenant_id = #{tenantId}</if>
<if test="categoryId != null"> AND m.category_id = #{categoryId}</if>
<if test="categoryId != null"> AND (
m.category_id = #{categoryId}
OR m.category_id IN (
SELECT category_id FROM biz_material_category
WHERE ancestors LIKE CONCAT(
(SELECT ancestors FROM biz_material_category WHERE category_id = #{categoryId}),
',%'
)
)
)</if>
<if test="materialCode != null and materialCode != ''"> AND m.material_code LIKE CONCAT('%',#{materialCode},'%')</if>
<if test="materialName != null and materialName != ''"> AND m.material_name LIKE CONCAT('%',#{materialName},'%')</if>
<if test="brand != null and brand != ''"> AND m.brand LIKE CONCAT('%',#{brand},'%')</if>

View File

@@ -75,7 +75,16 @@
<div class="grid-item">
<span class="grid-label">所属分类</span>
<div class="grid-value-wrap">
<el-input v-if="isEditing" v-model="form.categoryName" size="small" disabled />
<el-cascader
v-if="isEditing"
v-model="form.categoryId"
:options="categoryTreeData"
:props="{ label: 'categoryName', value: 'categoryId', children: 'children', checkStrictly: true, emitPath: false }"
placeholder="请选择分类"
clearable
filterable
size="small"
style="width:100%" />
<span v-else class="grid-value" :class="{ empty: !material.categoryName }">{{ material.categoryName || '' }}</span>
</div>
</div>
@@ -142,6 +151,7 @@
<script>
import { getMaterial, updateMaterial } from "@/api/bid/material";
import { getCategoryList } from "@/api/bid/category";
import SupplierQuoteTab from "./components/SupplierQuoteTab";
import ClientQuoteTab from "./components/ClientQuoteTab";
import CompareSection from "./components/CompareSection";
@@ -156,14 +166,21 @@ export default {
form: {},
perfParams: [],
activeTab: "supplier",
isEditing: false
isEditing: false,
categoryTreeData: []
};
},
created() {
this.materialId = this.$route.query && this.$route.query.id;
this.loadMaterial();
this.loadCategories();
},
methods: {
loadCategories() {
getCategoryList().then(res => {
this.categoryTreeData = res.data || [];
});
},
loadMaterial() {
if (!this.materialId) return;
getMaterial(this.materialId).then(res => {
@@ -201,14 +218,31 @@ export default {
this.$message.success("保存成功");
this.isEditing = false;
// 更新本地数据
this.material = { ...this.material, ...saveData };
// 更新本地数据,同步分类名称显示
const categoryName = this.findCategoryName(this.form.categoryId);
this.material = { ...this.material, ...saveData, categoryName };
this.material.perfArray = this.perfParams;
} catch (error) {
this.$message.error("保存失败:" + error.message);
}
},
// 根据categoryId从分类树中查找分类名称
findCategoryName(categoryId) {
if (!categoryId) return '';
const search = (nodes) => {
for (const n of nodes) {
if (n.categoryId === categoryId) return n.categoryName;
if (n.children && n.children.length) {
const found = search(n.children);
if (found) return found;
}
}
return '';
};
return search(this.categoryTreeData);
},
// 取消编辑
handleCancel() {
this.isEditing = false;

View File

@@ -2,75 +2,107 @@
<div class="app-container">
<!-- 标签页 -->
<el-tabs v-model="activeTab">
<!-- Tab 1: 物料列表原有内容 -->
<!-- Tab 1: 物料列表左右布局 -->
<el-tab-pane label="物料列表" name="list">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true">
<el-form-item label="物料名称" prop="materialName">
<el-input v-model="queryParams.materialName" placeholder="请输入物料名称" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="物料编码" prop="materialCode">
<el-input v-model="queryParams.materialCode" placeholder="请输入物料编码" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="厂家/品牌" prop="brand">
<el-select v-model="queryParams.brand" placeholder="全部品牌" clearable style="width:160px" filterable>
<el-option v-for="b in brandList" :key="b" :label="b" :value="b" />
</el-select>
</el-form-item>
<el-form-item label="所属分类">
<el-select v-model="queryParams.categoryId" placeholder="全部分类" clearable style="width:160px">
<el-option v-for="c in flatCategories" :key="c.categoryId"
:label="c.categoryName" :value="c.categoryId" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="material-layout">
<!-- 左侧分类树 -->
<div class="category-tree-panel">
<div class="tree-header">
<span class="tree-title">物料分类</span>
<el-input
v-model="treeFilter"
prefix-icon="el-icon-search"
size="mini"
placeholder="搜索分类"
clearable
style="width:140px" />
</div>
<el-tree
ref="categoryTree"
:data="categoryTreeData"
:props="{ label: 'categoryName', children: 'children' }"
node-key="categoryId"
:filter-node-method="filterNode"
:expand-on-click-node="false"
:default-expand-all="true"
highlight-current
@node-click="handleCategoryClick"
class="category-tree" />
<div class="tree-footer">
<el-button size="mini" type="text" icon="el-icon-setting" @click="activeTab='category'">管理分类</el-button>
</div>
</div>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
</el-col>
</el-row>
<!-- 右侧物料表格 -->
<div class="material-table-panel">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true">
<el-form-item label="物料名称" prop="materialName">
<el-input v-model="queryParams.materialName" placeholder="请输入物料名称" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="物料编码" prop="materialCode">
<el-input v-model="queryParams.materialCode" placeholder="请输入物料编码" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="厂家/品牌" prop="brand">
<el-select v-model="queryParams.brand" placeholder="全部品牌" clearable style="width:160px" filterable>
<el-option v-for="b in brandList" :key="b" :label="b" :value="b" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table
v-loading="loading"
:data="materialList"
@selection-change="handleSelectionChange"
border stripe style="width:100%"
:header-cell-style="{ background: '#ffffff', color: '#666', fontWeight: 500, fontSize: '12px' }"
:cell-style="{ fontSize: '12px', color: '#333' }"
size="small">
<el-table-column type="selection" width="44" align="center" />
<el-table-column label="物料编码" prop="materialCode" width="120" header-align="center" align="center" />
<el-table-column label="物料名称" prop="materialName" min-width="130" :show-overflow-tooltip="true" />
<el-table-column label="分类" prop="categoryName" width="100" :show-overflow-tooltip="true" />
<el-table-column label="品牌" prop="brand" width="120" :show-overflow-tooltip="true" />
<el-table-column label="规格型号" prop="spec" min-width="140" :show-overflow-tooltip="true" />
<el-table-column label="材质" prop="material" width="80" :show-overflow-tooltip="true" />
<el-table-column label="用途" prop="purpose" min-width="100" :show-overflow-tooltip="true" />
<el-table-column label="状态" align="center" width="80">
<template slot-scope="scope">
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="180" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="handleDetail(scope.row)">详情</el-button>
<el-button size="mini" type="text" @click="handleUpdate(scope.row)">修改</el-button>
<el-button size="mini" type="text" style="color:#f56c6c" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!loading && total === 0" description="暂无物料数据(请检查后端接口是否正常)" />
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
</el-col>
<el-col :span="1.5" v-if="currentCategoryName">
<el-tag size="medium" closable @close="clearCategoryFilter" type="warning">
当前分类: {{ currentCategoryName }}
</el-tag>
</el-col>
</el-row>
<el-table
v-loading="loading"
:data="materialList"
@selection-change="handleSelectionChange"
border stripe style="width:100%"
:header-cell-style="{ background: '#ffffff', color: '#666', fontWeight: 500, fontSize: '12px' }"
:cell-style="{ fontSize: '12px', color: '#333' }"
size="small">
<el-table-column type="selection" width="44" align="center" />
<el-table-column label="物料编码" prop="materialCode" width="110" header-align="center" align="center" />
<el-table-column label="物料名称" prop="materialName" min-width="120" :show-overflow-tooltip="true" />
<el-table-column label="分类" prop="categoryName" width="90" :show-overflow-tooltip="true" />
<el-table-column label="品牌" prop="brand" width="100" :show-overflow-tooltip="true" />
<el-table-column label="规格型号" prop="spec" min-width="120" :show-overflow-tooltip="true" />
<el-table-column label="材质" prop="material" width="70" :show-overflow-tooltip="true" />
<el-table-column label="用途" prop="purpose" min-width="80" :show-overflow-tooltip="true" />
<el-table-column label="状态" align="center" width="70">
<template slot-scope="scope">
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="160" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="handleDetail(scope.row)">详情</el-button>
<el-button size="mini" type="text" @click="handleUpdate(scope.row)">修改</el-button>
<el-button size="mini" type="text" style="color:#f56c6c" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!loading && total === 0" description="暂无物料数据" />
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
</div>
</div>
</el-tab-pane>
<!-- Tab 2: 发货记录新增 -->
<!-- Tab 2: 发货记录 -->
<el-tab-pane label="发货记录" name="records">
<el-select v-model="recordMaterialId" filterable placeholder="选择物料" style="width:400px" @change="loadRecords" clearable>
<el-option v-for="m in materialOptions" :key="m.materialId"
@@ -104,9 +136,49 @@
<el-empty v-if="!recordMaterialId && !recordLoading" description="请先选择物料" />
<el-empty v-if="recordMaterialId && !recordList.length && !recordLoading" description="该物料暂无发货记录" />
</el-tab-pane>
<!-- Tab 3: 分类管理 -->
<el-tab-pane label="分类管理" name="category">
<div class="category-mgmt">
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAddCategory(0)">新增根分类</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="el-icon-refresh" size="mini" @click="loadCategories">刷新</el-button>
</el-col>
</el-row>
<el-table
:data="categoryTreeData"
row-key="categoryId"
border
size="small"
:tree-props="{ children: 'children' }"
default-expand-all
style="width:100%">
<el-table-column label="分类名称" prop="categoryName" min-width="200" />
<el-table-column label="排序" prop="sort" width="80" align="center" />
<el-table-column label="状态" width="80" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.status === '0' ? 'success' : 'danger'" size="mini">
{{ scope.row.status === '0' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="240" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-plus" @click="handleAddCategory(scope.row.categoryId)">添加子级</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEditCategory(scope.row)">编辑</el-button>
<el-button size="mini" type="text" style="color:#f56c6c" icon="el-icon-delete" @click="handleDeleteCategory(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
</el-tabs>
<!-- 新增/修改对话框 -->
<!-- 新增/修改物料对话框 -->
<el-dialog :title="title" :visible.sync="open" width="760px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="90px">
<el-row>
@@ -117,10 +189,14 @@
</el-col>
<el-col :span="12">
<el-form-item label="所属分类">
<el-select v-model="form.categoryId" placeholder="请选择分类" clearable style="width:100%">
<el-option v-for="c in flatCategories" :key="c.categoryId"
:label="c.indentName" :value="c.categoryId" />
</el-select>
<el-cascader
v-model="form.categoryId"
:options="categoryTreeData"
:props="{ label: 'categoryName', value: 'categoryId', children: 'children', checkStrictly: true, emitPath: false }"
placeholder="请选择分类"
clearable
filterable
style="width:100%" />
</el-form-item>
</el-col>
</el-row>
@@ -195,12 +271,44 @@
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</el-dialog>
<!-- 新增/修改分类对话框 -->
<el-dialog :title="categoryTitle" :visible.sync="categoryOpen" width="500px" append-to-body>
<el-form ref="categoryForm" :model="categoryForm" :rules="categoryRules" label-width="90px">
<el-form-item label="上级分类">
<el-cascader
v-model="categoryForm.parentId"
:options="categoryTreeData"
:props="{ label: 'categoryName', value: 'categoryId', children: 'children', checkStrictly: true, emitPath: false }"
placeholder="不选则为根分类"
clearable
filterable
style="width:100%" />
</el-form-item>
<el-form-item label="分类名称" prop="categoryName">
<el-input v-model="categoryForm.categoryName" placeholder="请输入分类名称" />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="categoryForm.sort" :min="0" :max="999" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="categoryForm.status">
<el-radio label="0">正常</el-radio>
<el-radio label="1">停用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="categoryOpen = false">取消</el-button>
<el-button type="primary" @click="submitCategoryForm">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listMaterial, getMaterial, addMaterial, updateMaterial, delMaterial, listManufacturer } from "@/api/bid/material";
import { getCategoryList } from "@/api/bid/category";
import { getCategoryList, addCategory, updateCategory, delCategory } from "@/api/bid/category";
import request from '@/utils/request'
export default {
@@ -212,20 +320,36 @@ export default {
queryParams: { pageNum: 1, pageSize: 10, materialName: null, materialCode: null, categoryId: null, brand: null, spec: null },
form: {},
flatCategories: [],
categoryTreeData: [],
brandList: [],
perfParams: [],
// 分类树筛选
treeFilter: "",
currentCategoryName: "",
// 发货记录
activeTab: "list",
recordMaterialId: null,
recordLoading: false,
recordList: [],
materialOptions: [],
// 分类管理
categoryOpen: false,
categoryTitle: "",
categoryForm: {},
categoryRules: {
categoryName: [{ required: true, message: "分类名称不能为空", trigger: "blur" }]
},
rules: {
materialCode: [{ required: true, message: "物料编码不能为空", trigger: "blur" }],
materialName: [{ required: true, message: "物料名称不能为空", trigger: "blur" }],
}
};
},
watch: {
treeFilter(val) {
this.$refs.categoryTree.filter(val);
}
},
created() {
this.getList();
this.loadCategories();
@@ -235,7 +359,8 @@ export default {
methods: {
loadCategories() {
getCategoryList().then(res => {
this.flatCategories = this.flattenTree(res.data || [], 0, "");
this.categoryTreeData = res.data || [];
this.flatCategories = this.flattenTree(this.categoryTreeData, 0, "");
});
},
loadBrands() {
@@ -251,6 +376,21 @@ export default {
}
return result;
},
filterNode(value, data) {
if (!value) return true;
return data.categoryName.indexOf(value) !== -1;
},
handleCategoryClick(data) {
this.queryParams.categoryId = data.categoryId;
this.currentCategoryName = data.categoryName;
this.queryParams.pageNum = 1;
this.getList();
},
clearCategoryFilter() {
this.queryParams.categoryId = null;
this.currentCategoryName = "";
this.handleQuery();
},
getList() {
this.loading = true;
listMaterial(this.queryParams).then(res => {
@@ -264,7 +404,7 @@ export default {
});
},
handleQuery() { this.queryParams.pageNum = 1; this.getList(); },
resetQuery() { this.resetForm("queryForm"); this.handleQuery(); },
resetQuery() { this.resetForm("queryForm"); this.clearCategoryFilter(); },
handleSelectionChange(sel) { this.multiple = !sel.length; this.ids = sel.map(s => s.materialId); },
handleAdd() {
this.reset();
@@ -276,7 +416,6 @@ export default {
this.reset();
getMaterial(row.materialId).then(res => {
this.form = res.data;
// 解析性能参数JSON → table
this.perfParams = this.parsePerfParamsToArray(this.form.performanceParams);
this.open = true;
this.title = "修改物料";
@@ -297,21 +436,11 @@ export default {
removePerfRow(index) {
this.perfParams.splice(index, 1);
},
parsePerfParams(jsonStr) {
if (!jsonStr) return '-';
try {
const obj = JSON.parse(jsonStr);
const arr = Object.keys(obj).map(k => ({ name: k, value: obj[k] }));
return arr.map(p => p.name + ': ' + p.value).join('; ');
} catch { return jsonStr; }
},
parsePerfParamsToArray(jsonStr) {
if (!jsonStr) return [];
try {
// Try [{name,value,unit}] format first
const arr = JSON.parse(jsonStr);
if (Array.isArray(arr)) return arr;
// Try {key: value} format
return Object.keys(arr).map(k => ({ name: k, value: arr[k], unit: '' }));
} catch { return []; }
},
@@ -324,7 +453,6 @@ export default {
submitForm() {
this.$refs["form"].validate(valid => {
if (!valid) return;
// 性能参数数组 → JSON string
if (this.perfParams.length) {
this.form.performanceParams = JSON.stringify(this.perfParams);
} else {
@@ -334,6 +462,38 @@ export default {
action(this.form).then(() => { this.$modal.msgSuccess("操作成功"); this.open = false; this.getList(); });
});
},
// ═══════════ 分类管理 ═══════════
handleAddCategory(parentId) {
this.categoryForm = { parentId: parentId || 0, sort: 0, status: "0" };
this.categoryTitle = "新增分类";
this.categoryOpen = true;
},
handleEditCategory(row) {
this.categoryForm = { ...row };
this.categoryTitle = "编辑分类";
this.categoryOpen = true;
},
handleDeleteCategory(row) {
this.$modal.confirm("确认删除分类「" + row.categoryName + "」?").then(() => {
return delCategory(row.categoryId);
}).then(() => {
this.$modal.msgSuccess("删除成功");
this.loadCategories();
}).catch(err => {
if (err && err.message) this.$modal.msgError(err.message);
});
},
submitCategoryForm() {
this.$refs["categoryForm"].validate(valid => {
if (!valid) return;
const action = this.categoryForm.categoryId ? updateCategory : addCategory;
action(this.categoryForm).then(() => {
this.$modal.msgSuccess("操作成功");
this.categoryOpen = false;
this.loadCategories();
});
});
},
// ═══════════ 发货记录 ═══════════
loadMaterialOptions() {
listMaterial({ pageSize: 999 }).then(r => { this.materialOptions = r.rows || [] }).catch(() => {})
@@ -350,20 +510,79 @@ export default {
},
recordStatusType(s) { return { pending: "warning", transit: "primary", history: "success" }[s] || "" },
recordStatusLabel(s) { return { pending: "待发", transit: "在途", history: "已收货" }[s] || s || "-" },
// 兼容 snake_case 和 camelCase
fmtRow(r) { return r }
}
};
</script>
<style scoped>
/* ═══ 京东主题 — 页面级变量覆盖 ═══ */
.app-container {
background: var(--bg-page);
padding: 16px 20px;
border-radius: var(--radius-base);
}
/* ═══ 左右布局 ═══ */
.material-layout {
display: flex;
gap: 12px;
align-items: flex-start;
}
.category-tree-panel {
width: 200px;
flex-shrink: 0;
background: #fff;
border: 1px solid #e5e5e5;
border-radius: 4px;
display: flex;
flex-direction: column;
max-height: calc(100vh - 220px);
position: sticky;
top: 0;
}
.tree-header {
padding: 10px 12px;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
justify-content: space-between;
}
.tree-title {
font-size: 13px;
font-weight: 500;
color: #333;
}
.category-tree {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.category-tree >>> .el-tree-node__content {
height: 32px;
font-size: 13px;
}
.category-tree >>> .el-tree-node.is-current > .el-tree-node__content {
background: #fef0f0;
color: #e4393c;
}
.tree-footer {
padding: 8px 12px;
border-top: 1px solid #f0f0f0;
text-align: center;
}
.material-table-panel {
flex: 1;
min-width: 0;
overflow-x: hidden;
}
/* 紧凑表格行 */
.el-table td { padding: 4px 4px !important; }
.el-table th { padding: 6px 4px !important; }
@@ -371,10 +590,6 @@ export default {
/* 圆角按钮 */
.el-button--mini { border-radius: var(--radius-base) !important; }
/* 搜索按钮(适配京东红) */
.search-btn { background: var(--brand-primary); color: #fff; border: none; border-radius: var(--radius-base); }
.search-btn:hover { background: var(--brand-primary-hover); }
/* 搜索表单样式 */
.el-form--inline .el-form-item {
margin-bottom: 16px;
@@ -422,4 +637,12 @@ export default {
.el-button + .el-button {
margin-left: 8px;
}
/* 分类管理 */
.category-mgmt {
background: #fff;
padding: 16px;
border: 1px solid #e5e5e5;
border-radius: 4px;
}
</style>

View File

@@ -0,0 +1,219 @@
-- ============================================================
-- 物料分类体系初始化脚本
-- 1. 插入标准分类数据(三级层级)
-- 2. 批量更新现有物料的分类关联
-- ============================================================
SET NAMES utf8mb4;
-- ============================================================
-- 第一部分:插入标准分类数据
-- ============================================================
-- 一级分类
INSERT INTO biz_material_category (tenant_id, category_name, parent_id, ancestors, sort, status, create_by, create_time)
VALUES
(1, '电气控制', 0, '0', 1, '0', 'admin', NOW()),
(1, '传感器', 0, '0', 2, '0', 'admin', NOW()),
(1, '机械传动', 0, '0', 3, '0', 'admin', NOW()),
(1, '气动液压', 0, '0', 4, '0', 'admin', NOW()),
(1, '辅材耗材', 0, '0', 5, '0', 'admin', NOW());
-- 获取一级分类ID并插入二级分类
-- 电气控制 -> PLC控制器, 变频器, 低压电器, 人机界面
INSERT INTO biz_material_category (tenant_id, category_name, parent_id, ancestors, sort, status, create_by, create_time)
SELECT 1, t.sub_name, t.pid, CONCAT(p.ancestors, ',', p.category_id), t.sort, '0', 'admin', NOW()
FROM (
SELECT 'PLC控制器' AS sub_name, category_id AS pid, 1 AS sort FROM biz_material_category WHERE category_name='电气控制' AND parent_id=0
UNION ALL
SELECT '变频器', category_id, 2 FROM biz_material_category WHERE category_name='电气控制' AND parent_id=0
UNION ALL
SELECT '低压电器', category_id, 3 FROM biz_material_category WHERE category_name='电气控制' AND parent_id=0
UNION ALL
SELECT '人机界面', category_id, 4 FROM biz_material_category WHERE category_name='电气控制' AND parent_id=0
) t
JOIN biz_material_category p ON t.pid = p.category_id;
-- 传感器 -> 接近开关, 光电传感器, 温度传感器
INSERT INTO biz_material_category (tenant_id, category_name, parent_id, ancestors, sort, status, create_by, create_time)
SELECT 1, t.sub_name, t.pid, CONCAT(p.ancestors, ',', p.category_id), t.sort, '0', 'admin', NOW()
FROM (
SELECT '接近开关' AS sub_name, category_id AS pid, 1 AS sort FROM biz_material_category WHERE category_name='传感器' AND parent_id=0
UNION ALL
SELECT '光电传感器', category_id, 2 FROM biz_material_category WHERE category_name='传感器' AND parent_id=0
UNION ALL
SELECT '温度传感器', category_id, 3 FROM biz_material_category WHERE category_name='传感器' AND parent_id=0
) t
JOIN biz_material_category p ON t.pid = p.category_id;
-- 机械传动 -> 电机, 减速机, 联轴器
INSERT INTO biz_material_category (tenant_id, category_name, parent_id, ancestors, sort, status, create_by, create_time)
SELECT 1, t.sub_name, t.pid, CONCAT(p.ancestors, ',', p.category_id), t.sort, '0', 'admin', NOW()
FROM (
SELECT '电机' AS sub_name, category_id AS pid, 1 AS sort FROM biz_material_category WHERE category_name='机械传动' AND parent_id=0
UNION ALL
SELECT '减速机', category_id, 2 FROM biz_material_category WHERE category_name='机械传动' AND parent_id=0
UNION ALL
SELECT '联轴器', category_id, 3 FROM biz_material_category WHERE category_name='机械传动' AND parent_id=0
) t
JOIN biz_material_category p ON t.pid = p.category_id;
-- 气动液压 -> 气缸, 阀岛, 管接头
INSERT INTO biz_material_category (tenant_id, category_name, parent_id, ancestors, sort, status, create_by, create_time)
SELECT 1, t.sub_name, t.pid, CONCAT(p.ancestors, ',', p.category_id), t.sort, '0', 'admin', NOW()
FROM (
SELECT '气缸' AS sub_name, category_id AS pid, 1 AS sort FROM biz_material_category WHERE category_name='气动液压' AND parent_id=0
UNION ALL
SELECT '阀岛', category_id, 2 FROM biz_material_category WHERE category_name='气动液压' AND parent_id=0
UNION ALL
SELECT '管接头', category_id, 3 FROM biz_material_category WHERE category_name='气动液压' AND parent_id=0
) t
JOIN biz_material_category p ON t.pid = p.category_id;
-- 辅材耗材 -> 线缆, 紧固件, 标签标识
INSERT INTO biz_material_category (tenant_id, category_name, parent_id, ancestors, sort, status, create_by, create_time)
SELECT 1, t.sub_name, t.pid, CONCAT(p.ancestors, ',', p.category_id), t.sort, '0', 'admin', NOW()
FROM (
SELECT '线缆' AS sub_name, category_id AS pid, 1 AS sort FROM biz_material_category WHERE category_name='辅材耗材' AND parent_id=0
UNION ALL
SELECT '紧固件', category_id, 2 FROM biz_material_category WHERE category_name='辅材耗材' AND parent_id=0
UNION ALL
SELECT '标签标识', category_id, 3 FROM biz_material_category WHERE category_name='辅材耗材' AND parent_id=0
) t
JOIN biz_material_category p ON t.pid = p.category_id;
-- 三级分类PLC控制器 -> 西门子系列, 三菱系列, 欧姆龙系列
INSERT INTO biz_material_category (tenant_id, category_name, parent_id, ancestors, sort, status, create_by, create_time)
SELECT 1, t.sub_name, t.pid, CONCAT(p.ancestors, ',', p.category_id), t.sort, '0', 'admin', NOW()
FROM (
SELECT '西门子系列' AS sub_name, category_id AS pid, 1 AS sort FROM biz_material_category WHERE category_name='PLC控制器' AND parent_id!=0
UNION ALL
SELECT '三菱系列', category_id, 2 FROM biz_material_category WHERE category_name='PLC控制器' AND parent_id!=0
UNION ALL
SELECT '欧姆龙系列', category_id, 3 FROM biz_material_category WHERE category_name='PLC控制器' AND parent_id!=0
) t
JOIN biz_material_category p ON t.pid = p.category_id;
-- 三级分类:低压电器 -> 断路器, 接触器, 继电器
INSERT INTO biz_material_category (tenant_id, category_name, parent_id, ancestors, sort, status, create_by, create_time)
SELECT 1, t.sub_name, t.pid, CONCAT(p.ancestors, ',', p.category_id), t.sort, '0', 'admin', NOW()
FROM (
SELECT '断路器' AS sub_name, category_id AS pid, 1 AS sort FROM biz_material_category WHERE category_name='低压电器' AND parent_id!=0
UNION ALL
SELECT '接触器', category_id, 2 FROM biz_material_category WHERE category_name='低压电器' AND parent_id!=0
UNION ALL
SELECT '继电器', category_id, 3 FROM biz_material_category WHERE category_name='低压电器' AND parent_id!=0
) t
JOIN biz_material_category p ON t.pid = p.category_id;
-- ============================================================
-- 第二部分:批量更新现有物料的分类关联
-- 根据物料名称关键词自动匹配分类
-- ============================================================
-- PLC相关 -> PLC控制器
UPDATE biz_material SET category_id = (
SELECT category_id FROM biz_material_category WHERE category_name='PLC控制器' AND parent_id!=0 LIMIT 1
) WHERE (material_name LIKE '%PLC%' OR material_name LIKE '%可编程%') AND (category_id = 0 OR category_id IS NULL);
-- 变频器相关
UPDATE biz_material SET category_id = (
SELECT category_id FROM biz_material_category WHERE category_name='变频器' AND parent_id!=0 LIMIT 1
) WHERE (material_name LIKE '%变频%' OR material_name LIKE '%VFD%' OR material_name LIKE '%inverter%')
AND (category_id = 0 OR category_id IS NULL);
-- 断路器
UPDATE biz_material SET category_id = (
SELECT category_id FROM biz_material_category WHERE category_name='断路器' LIMIT 1
) WHERE (material_name LIKE '%断路%' OR material_name LIKE '%空开%')
AND (category_id = 0 OR category_id IS NULL);
-- 接触器
UPDATE biz_material SET category_id = (
SELECT category_id FROM biz_material_category WHERE category_name='接触器' LIMIT 1
) WHERE material_name LIKE '%接触%' AND (category_id = 0 OR category_id IS NULL);
-- 继电器
UPDATE biz_material SET category_id = (
SELECT category_id FROM biz_material_category WHERE category_name='继电器' LIMIT 1
) WHERE material_name LIKE '%继电%' AND (category_id = 0 OR category_id IS NULL);
-- 接近开关
UPDATE biz_material SET category_id = (
SELECT category_id FROM biz_material_category WHERE category_name='接近开关' LIMIT 1
) WHERE material_name LIKE '%接近开关%' AND (category_id = 0 OR category_id IS NULL);
-- 光电传感器
UPDATE biz_material SET category_id = (
SELECT category_id FROM biz_material_category WHERE category_name='光电传感器' LIMIT 1
) WHERE (material_name LIKE '%光电%' OR material_name LIKE '%光电传感%')
AND (category_id = 0 OR category_id IS NULL);
-- 温度传感器
UPDATE biz_material SET category_id = (
SELECT category_id FROM biz_material_category WHERE category_name='温度传感器' LIMIT 1
) WHERE (material_name LIKE '%温度传感%' OR material_name LIKE '%热电偶%' OR material_name LIKE '%热电阻%')
AND (category_id = 0 OR category_id IS NULL);
-- 电机
UPDATE biz_material SET category_id = (
SELECT category_id FROM biz_material_category WHERE category_name='电机' LIMIT 1
) WHERE (material_name LIKE '%电机%' OR material_name LIKE '%马达%')
AND (category_id = 0 OR category_id IS NULL);
-- 减速机
UPDATE biz_material SET category_id = (
SELECT category_id FROM biz_material_category WHERE category_name='减速机' LIMIT 1
) WHERE material_name LIKE '%减速%' AND (category_id = 0 OR category_id IS NULL);
-- 联轴器
UPDATE biz_material SET category_id = (
SELECT category_id FROM biz_material_category WHERE category_name='联轴器' LIMIT 1
) WHERE material_name LIKE '%联轴%' AND (category_id = 0 OR category_id IS NULL);
-- 气缸
UPDATE biz_material SET category_id = (
SELECT category_id FROM biz_material_category WHERE category_name='气缸' LIMIT 1
) WHERE material_name LIKE '%气缸%' AND (category_id = 0 OR category_id IS NULL);
-- 阀岛/电磁阀
UPDATE biz_material SET category_id = (
SELECT category_id FROM biz_material_category WHERE category_name='阀岛' LIMIT 1
) WHERE (material_name LIKE '%阀岛%' OR material_name LIKE '%电磁阀%')
AND (category_id = 0 OR category_id IS NULL);
-- 线缆
UPDATE biz_material SET category_id = (
SELECT category_id FROM biz_material_category WHERE category_name='线缆' LIMIT 1
) WHERE (material_name LIKE '%线缆%' OR material_name LIKE '%电缆%' OR material_name LIKE '%导线%')
AND (category_id = 0 OR category_id IS NULL);
-- 人机界面/触摸屏
UPDATE biz_material SET category_id = (
SELECT category_id FROM biz_material_category WHERE category_name='人机界面' LIMIT 1
) WHERE (material_name LIKE '%触摸屏%' OR material_name LIKE '%HMI%' OR material_name LIKE '%人机%')
AND (category_id = 0 OR category_id IS NULL);
-- 未匹配的物料归入"辅材耗材"
UPDATE biz_material SET category_id = (
SELECT category_id FROM biz_material_category WHERE category_name='辅材耗材' AND parent_id=0 LIMIT 1
) WHERE (category_id = 0 OR category_id IS NULL);
-- ============================================================
-- 验证结果
-- ============================================================
SELECT '=== 分类数据统计 ===' AS info;
SELECT COUNT(*) AS total_categories FROM biz_material_category;
SELECT '=== 各分类物料数量 ===' AS info;
SELECT
c.category_name,
COUNT(m.material_id) AS material_count
FROM biz_material_category c
LEFT JOIN biz_material m ON c.category_id = m.category_id
GROUP BY c.category_id, c.category_name
ORDER BY c.sort;
SELECT '=== 未分类物料数量 ===' AS info;
SELECT COUNT(*) AS uncategorized FROM biz_material WHERE category_id = 0 OR category_id IS NULL;