feat(admin): 商品管理支持多选皮肤筛选且要求包含所选全部皮肤

- 后端过滤:将 AdminListQuery.SkinName 筛选逻辑对齐前台,由 OR(交集) 改为 AND(全包含),选多款皮肤时账号必须同时包含所选的全部皮肤
- 前端交互:AdminListingsView 皮肤筛选支持多选交互,点击具体皮肤支持多选高亮,底部提供已选数量提示、清空与确定操作,外层按钮智能展示已选皮肤摘要
- 样式增强:补充管理后台皮肤筛选弹层底部的清空/确定操作栏样式
- 验证方式:新增并运行后端 TestFilterAdminListingsByMultipleSkinsRequiresAll 单元测试,运行全量 ./scripts/check.sh 门禁全绿通过
This commit is contained in:
yml2213
2026-10-10 17:14:47 +08:00
parent a93c76bcf6
commit 9d5d35c7d4
4 changed files with 121 additions and 18 deletions
@@ -107,9 +107,29 @@ const assetFilterLabel = computed(() => {
levelSlotLabel(filters.load || ''),
].join('/')
})
const skinFilterActive = computed(() => Boolean(filters.skin_group || filters.skin_name))
const selectedSkinNames = computed<string[]>({
get() {
if (!filters.skin_name) return []
return filters.skin_name
.split(',')
.map(s => s.trim())
.filter(Boolean)
},
set(val: string[]) {
filters.skin_name = val.filter(Boolean).join(',')
},
})
const skinFilterActive = computed(() =>
Boolean(filters.skin_group || selectedSkinNames.value.length)
)
const skinFilterLabel = computed(() => {
if (filters.skin_name) return filters.skin_name
const names = selectedSkinNames.value
if (names.length === 1) return names[0]
if (names.length > 1) {
const joined = names.join('、')
return joined.length <= 14 ? joined : `${names[0]} 等${names.length}款`
}
if (filters.skin_group) {
return skinFilterGroups.value.find(group => group.key === filters.skin_group)?.title || '皮肤'
}
@@ -299,17 +319,39 @@ function clearAssetFilters(shouldQuery = false) {
if (shouldQuery) void queryListings()
}
function selectSkinFilter(group: string, name = '') {
filters.skin_group = group
filters.skin_name = name
function isSkinNameActive(name: string) {
return selectedSkinNames.value.includes(name)
}
function toggleSkinNameFilter(name: string) {
const current = selectedSkinNames.value
if (current.includes(name)) {
selectedSkinNames.value = current.filter(item => item !== name)
} else {
selectedSkinNames.value = [...current, name]
}
filters.skin_group = ''
}
function selectSkinGroupFilter(groupKey: string) {
selectedSkinNames.value = []
filters.skin_group = filters.skin_group === groupKey ? '' : groupKey
closeFilterPopover()
void queryListings()
}
function confirmSkinFilters() {
closeFilterPopover()
void queryListings()
}
function clearSkinFilters(shouldQuery = false) {
filters.skin_group = ''
filters.skin_name = ''
closeFilterPopover()
if (shouldQuery) void queryListings()
if (shouldQuery) {
closeFilterPopover()
void queryListings()
}
}
function insuranceSlotLabel(value: string) {
@@ -865,8 +907,8 @@ function formatQuantity(value: number) {
<button
class="admin-filter-reset"
type="button"
:class="{ active: !filters.skin_group && !filters.skin_name }"
@click="clearSkinFilters()"
:class="{ active: !filters.skin_group && !selectedSkinNames.length }"
@click="clearSkinFilters(true)"
>
全部皮肤
</button>
@@ -880,8 +922,10 @@ function formatQuantity(value: number) {
<strong>{{ group.title }}</strong>
<button
type="button"
:class="{ active: filters.skin_group === group.key && !filters.skin_name }"
@click="selectSkinFilter(group.key)"
:class="{
active: filters.skin_group === group.key && !selectedSkinNames.length,
}"
@click="selectSkinGroupFilter(group.key)"
>
全部{{ group.title.replace('干员', '') }}
</button>
@@ -892,15 +936,24 @@ function formatQuantity(value: number) {
:key="`${group.key}-${skin}`"
type="button"
:class="{
active: filters.skin_group === group.key && filters.skin_name === skin,
active: isSkinNameActive(skin),
}"
@click="selectSkinFilter(group.key, skin)"
@click="toggleSkinNameFilter(skin)"
>
{{ skin }}
</button>
</div>
</div>
</div>
<div class="admin-skin-filter-footer">
<span class="selected-hint">已选 {{ selectedSkinNames.length }} 款皮肤</span>
<div class="footer-actions">
<el-button size="small" link @click="clearSkinFilters(false)">清空</el-button>
<el-button size="small" type="primary" @click="confirmSkinFilters"
>确定</el-button
>
</div>
</div>
</el-popover>
</el-form-item>
</el-form>