feat: 选择材料-子类支持多选组合筛选
子类从单选改为多选,可同时勾选多个子类(带✓标记),下方材料列表 显示所选子类的合集(客户端组合过滤,切换即时生效)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3bbafc99d7
commit
7f03b11a95
|
|
@ -33,28 +33,31 @@
|
||||||
@click="selectMajor(g.major)"
|
@click="selectMajor(g.major)"
|
||||||
>{{ g.major }}</a-tag>
|
>{{ g.major }}</a-tag>
|
||||||
</div>
|
</div>
|
||||||
<!-- 级联:子类 -->
|
<!-- 级联:子类(可多选组合) -->
|
||||||
<div class="cascade-row" v-if="currentSubs.length">
|
<div class="cascade-row" v-if="currentSubs.length">
|
||||||
<span class="cascade-lbl">子类</span>
|
<span class="cascade-lbl">子类</span>
|
||||||
<a-tag
|
<a-tag
|
||||||
:color="sub === null ? '#b4232a' : 'default'"
|
:color="!subs.length ? '#b4232a' : 'default'"
|
||||||
class="cas-tag"
|
class="cas-tag"
|
||||||
@click="selectSub(null)"
|
@click="clearSubs"
|
||||||
>全部</a-tag>
|
>全部</a-tag>
|
||||||
<a-tag
|
<a-tag
|
||||||
v-for="s in currentSubs"
|
v-for="s in currentSubs"
|
||||||
:key="s"
|
:key="s"
|
||||||
:color="sub === s ? '#b4232a' : 'default'"
|
:color="subs.includes(s) ? '#b4232a' : 'default'"
|
||||||
class="cas-tag"
|
class="cas-tag"
|
||||||
@click="selectSub(s)"
|
@click="toggleSub(s)"
|
||||||
>{{ s }}</a-tag>
|
>
|
||||||
|
<CheckOutlined v-if="subs.includes(s)" /> {{ s }}
|
||||||
|
</a-tag>
|
||||||
|
<span v-if="subs.length" class="multi-tip">已选 {{ subs.length }} 个子类(组合)</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-divider style="margin: 10px 0" />
|
<a-divider style="margin: 10px 0" />
|
||||||
|
|
||||||
<!-- 当前类别材料 -->
|
<!-- 当前类别材料 -->
|
||||||
<div class="list-head">
|
<div class="list-head">
|
||||||
<span>{{ major }}<template v-if="sub"> / {{ sub }}</template> · 共 {{ list.length }} 种</span>
|
<span>{{ major }}<template v-if="subs.length"> / {{ subs.join('、') }}</template> · 共 {{ list.length }} 种</span>
|
||||||
<a-button type="primary" size="small" :disabled="checkedCount === 0" @click="addSelected">
|
<a-button type="primary" size="small" :disabled="checkedCount === 0" @click="addSelected">
|
||||||
批量添加所选({{ checkedCount }})
|
批量添加所选({{ checkedCount }})
|
||||||
</a-button>
|
</a-button>
|
||||||
|
|
@ -103,6 +106,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, ref, watch } from 'vue';
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
|
import { CheckOutlined } from '@ant-design/icons-vue';
|
||||||
import { MATERIAL_CATEGORIES, HEALTH_GRADES, ENV_GRADES } from '@airpredict/shared';
|
import { MATERIAL_CATEGORIES, HEALTH_GRADES, ENV_GRADES } from '@airpredict/shared';
|
||||||
import { listMaterials, type Material } from '../api/materials';
|
import { listMaterials, type Material } from '../api/materials';
|
||||||
|
|
||||||
|
|
@ -119,9 +123,16 @@ const scope = ref<'public' | 'self'>('public');
|
||||||
const healthGrade = ref<string | undefined>(undefined);
|
const healthGrade = ref<string | undefined>(undefined);
|
||||||
const envGrade = ref<string | undefined>(undefined);
|
const envGrade = ref<string | undefined>(undefined);
|
||||||
const major = ref<string>('');
|
const major = ref<string>('');
|
||||||
const sub = ref<string | null>(null);
|
const subs = ref<string[]>([]); // 多选子类(组合筛选)
|
||||||
const list = ref<Material[]>([]);
|
const fullList = ref<Material[]>([]); // 当前大类全部材料(服务端按健康/环保筛过)
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 按已选子类做客户端组合过滤;未选=显示整个大类
|
||||||
|
const list = computed(() => {
|
||||||
|
if (!subs.value.length) return fullList.value;
|
||||||
|
const set = new Set(subs.value.map((s) => `${major.value}/${s}`));
|
||||||
|
return fullList.value.filter((m) => set.has(m.category));
|
||||||
|
});
|
||||||
const rowState = reactive<Record<string, { checked: boolean; area: number | null }>>({});
|
const rowState = reactive<Record<string, { checked: boolean; area: number | null }>>({});
|
||||||
|
|
||||||
const picked = computed(() => new Set(props.existingIds));
|
const picked = computed(() => new Set(props.existingIds));
|
||||||
|
|
@ -157,29 +168,33 @@ function healthColor(g: string) {
|
||||||
|
|
||||||
function selectMajor(m: string) {
|
function selectMajor(m: string) {
|
||||||
major.value = m;
|
major.value = m;
|
||||||
sub.value = null;
|
subs.value = [];
|
||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
function selectSub(s: string | null) {
|
function toggleSub(s: string) {
|
||||||
sub.value = s;
|
const i = subs.value.indexOf(s);
|
||||||
reload();
|
if (i >= 0) subs.value.splice(i, 1);
|
||||||
|
else subs.value.push(s);
|
||||||
|
}
|
||||||
|
function clearSubs() {
|
||||||
|
subs.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切大类/健康/环保/库时重新拉整个大类的材料(子类组合在客户端过滤)
|
||||||
async function reload() {
|
async function reload() {
|
||||||
if (!major.value) return;
|
if (!major.value) return;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
for (const k of Object.keys(rowState)) delete rowState[k];
|
for (const k of Object.keys(rowState)) delete rowState[k];
|
||||||
try {
|
try {
|
||||||
const category = sub.value ? `${major.value}/${sub.value}` : major.value;
|
|
||||||
const res = await listMaterials({
|
const res = await listMaterials({
|
||||||
category,
|
category: major.value,
|
||||||
healthGrade: healthGrade.value,
|
healthGrade: healthGrade.value,
|
||||||
envGrade: envGrade.value,
|
envGrade: envGrade.value,
|
||||||
scope: scope.value,
|
scope: scope.value,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 300,
|
pageSize: 300,
|
||||||
});
|
});
|
||||||
list.value = res.items;
|
fullList.value = res.items;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
@ -207,7 +222,7 @@ watch(
|
||||||
(o) => {
|
(o) => {
|
||||||
if (o) {
|
if (o) {
|
||||||
if (!major.value) major.value = tree.value[0]?.major || '';
|
if (!major.value) major.value = tree.value[0]?.major || '';
|
||||||
sub.value = null;
|
subs.value = [];
|
||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -220,6 +235,7 @@ watch(
|
||||||
.cascade-row { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
|
.cascade-row { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
|
||||||
.cascade-lbl { color: #999; font-size: 12px; width: 32px; flex-shrink: 0; }
|
.cascade-lbl { color: #999; font-size: 12px; width: 32px; flex-shrink: 0; }
|
||||||
.cas-tag { cursor: pointer; user-select: none; margin: 0; }
|
.cas-tag { cursor: pointer; user-select: none; margin: 0; }
|
||||||
|
.multi-tip { color: #b4232a; font-size: 12px; margin-left: 8px; }
|
||||||
.list-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; color: #555; }
|
.list-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; color: #555; }
|
||||||
.added { color: #999; }
|
.added { color: #999; }
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue