From 9d5d35c7d42f9adc71c18029570537f92c916788 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 10 Oct 2026 17:14:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E5=95=86=E5=93=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=94=AF=E6=8C=81=E5=A4=9A=E9=80=89=E7=9A=AE=E8=82=A4?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E4=B8=94=E8=A6=81=E6=B1=82=E5=8C=85=E5=90=AB?= =?UTF-8?q?=E6=89=80=E9=80=89=E5=85=A8=E9=83=A8=E7=9A=AE=E8=82=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端过滤:将 AdminListQuery.SkinName 筛选逻辑对齐前台,由 OR(交集) 改为 AND(全包含),选多款皮肤时账号必须同时包含所选的全部皮肤 - 前端交互:AdminListingsView 皮肤筛选支持多选交互,点击具体皮肤支持多选高亮,底部提供已选数量提示、清空与确定操作,外层按钮智能展示已选皮肤摘要 - 样式增强:补充管理后台皮肤筛选弹层底部的清空/确定操作栏样式 - 验证方式:新增并运行后端 TestFilterAdminListingsByMultipleSkinsRequiresAll 单元测试,运行全量 ./scripts/check.sh 门禁全绿通过 --- .../internal/modules/listing/admin_filter.go | 9 +-- .../modules/listing/admin_filter_test.go | 31 ++++++++ .../admin/views/AdminListingsView.vue | 79 ++++++++++++++++--- frontend/src/styles/admin.css | 20 +++++ 4 files changed, 121 insertions(+), 18 deletions(-) diff --git a/backend/internal/modules/listing/admin_filter.go b/backend/internal/modules/listing/admin_filter.go index 7a6d065..a312f1f 100644 --- a/backend/internal/modules/listing/admin_filter.go +++ b/backend/internal/modules/listing/admin_filter.go @@ -33,12 +33,11 @@ func matchesAdminAssetQuery(item ListingDTO, query AdminListQuery) bool { return false } if len(query.SkinName) > 0 { - if len(query.SkinGroup) > 0 { - return skinGroupsContainAny(item.AssetSummary, query.SkinGroup, query.SkinName) + // 皮肤筛选:多选时要求账号必须包含所有选中项(AND),与前台首页一致 + if !containsAll(query.SkinName, skinNamesFromSummary(item.AssetSummary)) { + return false } - return intersects(query.SkinName, skinNamesFromSummary(item.AssetSummary)) - } - if len(query.SkinGroup) > 0 { + } else if len(query.SkinGroup) > 0 { return skinGroupsHaveAny(item.AssetSummary, query.SkinGroup) } return true diff --git a/backend/internal/modules/listing/admin_filter_test.go b/backend/internal/modules/listing/admin_filter_test.go index e27da87..8fe65cd 100644 --- a/backend/internal/modules/listing/admin_filter_test.go +++ b/backend/internal/modules/listing/admin_filter_test.go @@ -98,6 +98,37 @@ func TestFilterAdminListingsBySkinGroupOnly(t *testing.T) { } } +func TestFilterAdminListingsByMultipleSkinsRequiresAll(t *testing.T) { + items := []ListingDTO{ + { + ID: 1, + AssetSummary: map[string]any{ + "skin_groups": map[string]any{ + "melee": []any{"坠星者", "暗星"}, + "weapon": []any{"M7棱镜攻势S2"}, + }, + }, + }, + { + ID: 2, + AssetSummary: map[string]any{ + "skin_groups": map[string]any{ + "melee": []any{"坠星者"}, + }, + }, + }, + } + + // 选了 "坠星者" 和 "暗星",只有同时具备两者的 ID=1 才能被筛选出,ID=2 仅有坠星者应被排除 + filtered := filterAdminListings(items, AdminListQuery{ + SkinName: []string{"坠星者", "暗星"}, + }) + + if len(filtered) != 1 || filtered[0].ID != 1 { + t.Fatalf("expected only listing 1 having both skins, got %#v", filtered) + } +} + func TestApplyAdminListFiltersSmartRouting(t *testing.T) { db := database.NewDryRunDB() repo := &Repository{} diff --git a/frontend/src/features/admin/views/AdminListingsView.vue b/frontend/src/features/admin/views/AdminListingsView.vue index 64271b4..380ad62 100644 --- a/frontend/src/features/admin/views/AdminListingsView.vue +++ b/frontend/src/features/admin/views/AdminListingsView.vue @@ -107,9 +107,29 @@ const assetFilterLabel = computed(() => { levelSlotLabel(filters.load || ''), ].join('/') }) -const skinFilterActive = computed(() => Boolean(filters.skin_group || filters.skin_name)) +const selectedSkinNames = computed({ + 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) { @@ -880,8 +922,10 @@ function formatQuantity(value: number) { {{ group.title }} @@ -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 }} + diff --git a/frontend/src/styles/admin.css b/frontend/src/styles/admin.css index 78b3395..142df6d 100644 --- a/frontend/src/styles/admin.css +++ b/frontend/src/styles/admin.css @@ -1042,6 +1042,26 @@ h1 { white-space: nowrap; } +.admin-skin-filter-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 10px; + margin-top: 4px; + border-top: 1px solid #f1f5f9; +} + +.admin-skin-filter-footer .selected-hint { + color: #64748b; + font-size: 12px; +} + +.admin-skin-filter-footer .footer-actions { + display: flex; + align-items: center; + gap: 8px; +} + /* ========== Dashboard Panels ========== */ .dashboard-panels { display: grid;