feat: 选择材料-子类支持多选组合筛选

子类从单选改为多选,可同时勾选多个子类(带✓标记),下方材料列表
显示所选子类的合集(客户端组合过滤,切换即时生效)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
zty 2026-06-11 15:04:40 +08:00
parent 3bbafc99d7
commit 7f03b11a95
1 changed files with 33 additions and 17 deletions

View File

@ -33,28 +33,31 @@
@click="selectMajor(g.major)"
>{{ g.major }}</a-tag>
</div>
<!-- 级联子类 -->
<!-- 级联子类可多选组合 -->
<div class="cascade-row" v-if="currentSubs.length">
<span class="cascade-lbl">子类</span>
<a-tag
:color="sub === null ? '#b4232a' : 'default'"
:color="!subs.length ? '#b4232a' : 'default'"
class="cas-tag"
@click="selectSub(null)"
@click="clearSubs"
>全部</a-tag>
<a-tag
v-for="s in currentSubs"
:key="s"
:color="sub === s ? '#b4232a' : 'default'"
:color="subs.includes(s) ? '#b4232a' : 'default'"
class="cas-tag"
@click="selectSub(s)"
>{{ s }}</a-tag>
@click="toggleSub(s)"
>
<CheckOutlined v-if="subs.includes(s)" /> {{ s }}
</a-tag>
<span v-if="subs.length" class="multi-tip">已选 {{ subs.length }} 个子类组合</span>
</div>
<a-divider style="margin: 10px 0" />
<!-- 当前类别材料 -->
<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">
批量添加所选{{ checkedCount }}
</a-button>
@ -103,6 +106,7 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from '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 { listMaterials, type Material } from '../api/materials';
@ -119,9 +123,16 @@ const scope = ref<'public' | 'self'>('public');
const healthGrade = ref<string | undefined>(undefined);
const envGrade = ref<string | undefined>(undefined);
const major = ref<string>('');
const sub = ref<string | null>(null);
const list = ref<Material[]>([]);
const subs = ref<string[]>([]); //
const fullList = ref<Material[]>([]); // /
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 picked = computed(() => new Set(props.existingIds));
@ -157,29 +168,33 @@ function healthColor(g: string) {
function selectMajor(m: string) {
major.value = m;
sub.value = null;
subs.value = [];
reload();
}
function selectSub(s: string | null) {
sub.value = s;
reload();
function toggleSub(s: string) {
const i = subs.value.indexOf(s);
if (i >= 0) subs.value.splice(i, 1);
else subs.value.push(s);
}
function clearSubs() {
subs.value = [];
}
// ///
async function reload() {
if (!major.value) return;
loading.value = true;
for (const k of Object.keys(rowState)) delete rowState[k];
try {
const category = sub.value ? `${major.value}/${sub.value}` : major.value;
const res = await listMaterials({
category,
category: major.value,
healthGrade: healthGrade.value,
envGrade: envGrade.value,
scope: scope.value,
page: 1,
pageSize: 300,
});
list.value = res.items;
fullList.value = res.items;
} finally {
loading.value = false;
}
@ -207,7 +222,7 @@ watch(
(o) => {
if (o) {
if (!major.value) major.value = tree.value[0]?.major || '';
sub.value = null;
subs.value = [];
reload();
}
},
@ -220,6 +235,7 @@ watch(
.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; }
.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; }
.added { color: #999; }
</style>