feat(material): 调整工具栏第一行筛选项

- 材料大类、材料种类(原"细分种类",改为下拉)上移第一行
- 品牌下移到高级筛选
- 列标题"细分种类"改为"材料种类"
- 后端 material_category 改为 exact 匹配(配合下拉)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-04-24 14:22:28 +08:00
parent 34fc101856
commit 4db17db5e4
3 changed files with 36 additions and 16 deletions

View File

@ -181,10 +181,10 @@ class MaterialViewSet(ModelViewSet):
if major_category: if major_category:
queryset = queryset.filter(major_category=major_category) queryset = queryset.filter(major_category=major_category)
# 支持按材料分类模糊过滤 # 支持按材料分类过滤
material_category = self.request.query_params.get('material_category') material_category = self.request.query_params.get('material_category')
if material_category: if material_category:
queryset = queryset.filter(material_category__icontains=material_category) queryset = queryset.filter(material_category=material_category)
# 支持按材料子类过滤 # 支持按材料子类过滤
material_subcategory = self.request.query_params.get('material_subcategory') material_subcategory = self.request.query_params.get('material_subcategory')

View File

@ -7,22 +7,21 @@
<el-select v-model="filters.status" placeholder="状态" clearable style="width: 140px" @change="triggerSearch"> <el-select v-model="filters.status" placeholder="状态" clearable style="width: 140px" @change="triggerSearch">
<el-option v-for="item in statusOptions" :key="item[0]" :label="item[1]" :value="item[0]" /> <el-option v-for="item in statusOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
</el-select> </el-select>
<el-input v-model="filters.material_category" placeholder="细分种类" clearable style="width: 160px" @keyup.enter="triggerSearch" /> <el-select v-model="filters.major_category" placeholder="材料大类" clearable style="width: 140px" @change="triggerSearch">
<el-select v-model="filters.material_subcategory" placeholder="材料子类" clearable style="width: 160px" @change="triggerSearch"> <el-option v-for="item in majorCategoryOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
<el-option v-for="item in filterSubcategoryOptions" :key="item.value" :label="item.name" :value="item.value" />
</el-select> </el-select>
<el-select <el-select
v-model="filters.brand" v-model="filters.material_category"
placeholder="品牌" placeholder="材料种类"
clearable clearable
filterable filterable
remote
:remote-method="searchBrands"
:loading="brandSearchLoading"
style="width: 160px" style="width: 160px"
@change="triggerSearch" @change="triggerSearch"
> >
<el-option v-for="item in brandFilterOptions" :key="item.id" :label="item.name" :value="item.id" /> <el-option v-for="item in filterCategoryOptions" :key="item.value" :label="item.name" :value="item.value" />
</el-select>
<el-select v-model="filters.material_subcategory" placeholder="材料子类" clearable style="width: 160px" @change="triggerSearch">
<el-option v-for="item in filterSubcategoryOptions" :key="item.value" :label="item.name" :value="item.value" />
</el-select> </el-select>
<el-badge :is-dot="hasAdvancedActive" class="advanced-badge"> <el-badge :is-dot="hasAdvancedActive" class="advanced-badge">
<el-button text @click="advancedOpen = !advancedOpen"> <el-button text @click="advancedOpen = !advancedOpen">
@ -68,8 +67,18 @@
</div> </div>
<div v-show="advancedOpen" class="toolbar-row toolbar-row--advanced"> <div v-show="advancedOpen" class="toolbar-row toolbar-row--advanced">
<el-select v-model="filters.major_category" placeholder="材料大类" clearable style="width: 140px" @change="triggerSearch"> <el-select
<el-option v-for="item in majorCategoryOptions" :key="item[0]" :label="item[1]" :value="item[0]" /> v-model="filters.brand"
placeholder="品牌"
clearable
filterable
remote
:remote-method="searchBrands"
:loading="brandSearchLoading"
style="width: 160px"
@change="triggerSearch"
>
<el-option v-for="item in brandFilterOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select> </el-select>
<el-select v-model="filters.stage" placeholder="阶段" clearable style="width: 140px" @change="triggerSearch"> <el-select v-model="filters.stage" placeholder="阶段" clearable style="width: 140px" @change="triggerSearch">
<el-option v-for="item in stageOptions" :key="item[0]" :label="item[1]" :value="item[0]" /> <el-option v-for="item in stageOptions" :key="item[0]" :label="item[1]" :value="item[0]" />
@ -227,7 +236,7 @@ import {
importMaterialsExcel, importMaterialsExcel,
exportMaterialsExcel exportMaterialsExcel
} from '@/api/material' } from '@/api/material'
import { fetchSubcategories } from '@/api/category' import { fetchCategories, fetchSubcategories } from '@/api/category'
import { fetchBrands } from '@/api/brand' import { fetchBrands } from '@/api/brand'
import { fetchFactories } from '@/api/factory' import { fetchFactories } from '@/api/factory'
import MaterialForm from '@/views/material/MaterialForm.vue' import MaterialForm from '@/views/material/MaterialForm.vue'
@ -275,7 +284,7 @@ const filters = reactive({
}) })
const advancedKeys = [ const advancedKeys = [
'major_category', 'material_category', 'stage', 'importance_level', 'brand', 'stage', 'importance_level',
'factory', 'factory__cooperation_mode', 'landing_project', 'factory', 'factory__cooperation_mode', 'landing_project',
'cost_compare__gte', 'cost_compare__lte', 'score_level__gte', 'cost_compare__gte', 'cost_compare__lte', 'score_level__gte',
'contact_person', 'handler', 'contact_person', 'handler',
@ -299,6 +308,7 @@ const importanceLevelOptions = ref([])
const cooperationModeOptions = ref([]) const cooperationModeOptions = ref([])
const applicationSceneChoices = ref([]) const applicationSceneChoices = ref([])
const advantageChoices = ref([]) const advantageChoices = ref([])
const filterCategoryOptions = ref([])
const filterSubcategoryOptions = ref([]) const filterSubcategoryOptions = ref([])
// //
@ -410,6 +420,15 @@ const loadChoices = async () => {
advantageChoices.value = data.advantage || [] advantageChoices.value = data.advantage || []
} }
const loadFilterCategories = async () => {
const data = await fetchCategories()
filterCategoryOptions.value = (data.results || data).map((item) => ({
id: item.id,
name: item.name,
value: item.value
}))
}
const loadFilterSubcategories = async () => { const loadFilterSubcategories = async () => {
const data = await fetchSubcategories({}) const data = await fetchSubcategories({})
filterSubcategoryOptions.value = (data.results || data).map((item) => ({ filterSubcategoryOptions.value = (data.results || data).map((item) => ({
@ -607,6 +626,7 @@ const onPageSizeChange = (size) => {
onMounted(() => { onMounted(() => {
loadChoices() loadChoices()
loadFilterCategories()
loadFilterSubcategories() loadFilterSubcategories()
loadBrandFilterOptions() loadBrandFilterOptions()
loadFactoryFilterOptions() loadFactoryFilterOptions()

View File

@ -10,7 +10,7 @@ export const MATERIAL_COLUMNS = [
// ==== A. 材料信息 ==== // ==== A. 材料信息 ====
{ group: 'material', key: 'name', label: '材料名称', minWidth: 180, showOverflowTooltip: true }, { group: 'material', key: 'name', label: '材料名称', minWidth: 180, showOverflowTooltip: true },
{ group: 'material', key: 'major_category_display', label: '材料大类', width: 100 }, { group: 'material', key: 'major_category_display', label: '材料大类', width: 100 },
{ group: 'material', key: 'material_category', label: '细分种类', minWidth: 140, showOverflowTooltip: true }, { group: 'material', key: 'material_category', label: '材料种类', minWidth: 140, showOverflowTooltip: true },
{ group: 'material', key: 'material_subcategory', label: '材料子类', minWidth: 140, showOverflowTooltip: true }, { group: 'material', key: 'material_subcategory', label: '材料子类', minWidth: 140, showOverflowTooltip: true },
{ group: 'material', key: 'stage_display', label: '阶段', width: 130, showOverflowTooltip: true }, { group: 'material', key: 'stage_display', label: '阶段', width: 130, showOverflowTooltip: true },
{ group: 'material', key: 'importance_level_display', label: '重要等级', width: 110 }, { group: 'material', key: 'importance_level_display', label: '重要等级', width: 110 },