添加商品管理资产皮肤筛选

This commit is contained in:
yml2213
2026-07-05 20:21:18 +08:00
parent 090a90a268
commit 1984e07e91
8 changed files with 612 additions and 2 deletions
@@ -1,10 +1,12 @@
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue'
import { ArrowDown, Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { computed, reactive, ref } from 'vue'
import { computed, onMounted, reactive, ref } from 'vue'
import {
emptyListingPublishOptions,
fetchAdminListings,
fetchListingPublishOptions,
type AdminListingPage,
type AdminListingQuery,
type Listing,
@@ -29,10 +31,23 @@ const filters = reactive<AdminListingQuery>({
owner_id: '',
status: '',
review_status: '',
insurance: '',
stamina: '',
load: '',
skin_group: '',
skin_name: '',
})
type FilterPopover = 'asset' | 'skin'
type AssetFilterKey = 'insurance' | 'stamina' | 'load'
const fallbackInsuranceOptions = ['2*1', '2*2', '2*3', '3*3']
const fallbackLevelOptions = ['1级', '2级', '3级', '4级', '5级', '6级', '7级']
const pageSize = ref(10)
const currentPage = ref(1)
const activeFilterPopover = ref<FilterPopover | ''>('')
const publishOptions = ref(emptyListingPublishOptions)
const {
loading,
@@ -63,6 +78,36 @@ const pendingCount = computed(
() => listings.value.filter(item => item.review_status === 'pending').length
)
const totalPages = computed(() => Math.max(1, Math.ceil(totalListings.value / pageSize.value)))
const insuranceOptions = computed(() =>
publishOptions.value.insurance_options.length
? publishOptions.value.insurance_options
: fallbackInsuranceOptions
)
const levelOptions = computed(() =>
publishOptions.value.level_options.length
? publishOptions.value.level_options
: fallbackLevelOptions
)
const skinFilterGroups = computed(() => publishOptions.value.skin_groups)
const assetFilterActive = computed(() =>
Boolean(filters.insurance || filters.stamina || filters.load)
)
const assetFilterLabel = computed(() => {
if (!assetFilterActive.value) return '全部资产'
return [
insuranceSlotLabel(filters.insurance || ''),
levelSlotLabel(filters.stamina || ''),
levelSlotLabel(filters.load || ''),
].join('/')
})
const skinFilterActive = computed(() => Boolean(filters.skin_group || filters.skin_name))
const skinFilterLabel = computed(() => {
if (filters.skin_name) return filters.skin_name
if (filters.skin_group) {
return skinFilterGroups.value.find(group => group.key === filters.skin_group)?.title || '皮肤'
}
return '全部皮肤'
})
const screenshotColumns = [
{ label: '商品编码', width: 88, read: (row: Listing) => formatListingCode(row) },
@@ -93,6 +138,7 @@ const screenshotColumns = [
] as const
async function queryListings() {
closeFilterPopover()
currentPage.value = 1
await loadListings()
}
@@ -102,9 +148,71 @@ function resetFilters() {
filters.owner_id = ''
filters.status = ''
filters.review_status = ''
clearAssetFilters(false)
clearSkinFilters(false)
void queryListings()
}
onMounted(loadPublishOptions)
async function loadPublishOptions() {
try {
publishOptions.value = await fetchListingPublishOptions()
} catch {
publishOptions.value = emptyListingPublishOptions
ElMessage.warning('商品筛选选项加载失败,已使用默认资产选项')
}
}
function setFilterPopover(key: FilterPopover, visible: boolean) {
activeFilterPopover.value = visible
? key
: activeFilterPopover.value === key
? ''
: activeFilterPopover.value
}
function closeFilterPopover() {
activeFilterPopover.value = ''
}
function setAssetFilter(key: AssetFilterKey, value: string) {
filters[key] = filters[key] === value ? '' : value
}
function clearAssetFilters(shouldQuery = false) {
filters.insurance = ''
filters.stamina = ''
filters.load = ''
if (shouldQuery) void queryListings()
}
function selectSkinFilter(group: string, name = '') {
filters.skin_group = group
filters.skin_name = name
closeFilterPopover()
}
function clearSkinFilters(shouldQuery = false) {
filters.skin_group = ''
filters.skin_name = ''
closeFilterPopover()
if (shouldQuery) void queryListings()
}
function insuranceSlotLabel(value: string) {
const parts = value.split('*').map(item => Number(item))
if (parts.length === 2 && parts.every(item => Number.isFinite(item) && item > 0)) {
const [rows = 0, columns = 0] = parts
return `${rows * columns}`
}
return value || '-'
}
function levelSlotLabel(value: string) {
return value ? value.replace(/级$/, '') : '-'
}
function setStatusFilter(status: string) {
filters.status = (filters.status === status ? '' : status) as AdminListingQuery['status']
filters.review_status = ''
@@ -490,6 +598,161 @@ function formatQuantity(value: number) {
<el-option label="已拒绝" value="rejected" />
</el-select>
</el-form-item>
<el-form-item label="资产">
<el-popover
:visible="activeFilterPopover === 'asset'"
trigger="click"
placement="bottom-start"
popper-class="admin-listing-filter-popover"
:width="360"
@update:visible="setFilterPopover('asset', $event)"
>
<template #reference>
<button
class="admin-filter-chip"
:class="{ active: assetFilterActive }"
type="button"
>
<span>{{ assetFilterLabel }}</span>
<el-icon><ArrowDown /></el-icon>
</button>
</template>
<div class="admin-asset-filter-menu">
<div class="admin-filter-section">
<span class="admin-filter-title">保险</span>
<div class="admin-option-grid compact">
<button
type="button"
:class="{ active: !filters.insurance }"
@click="setAssetFilter('insurance', '')"
>
全部
</button>
<button
v-for="item in insuranceOptions"
:key="item"
type="button"
:class="{ active: filters.insurance === item }"
@click="setAssetFilter('insurance', item)"
>
{{ insuranceSlotLabel(item) }}
</button>
</div>
</div>
<div class="admin-filter-section">
<span class="admin-filter-title">体力</span>
<div class="admin-option-grid">
<button
type="button"
:class="{ active: !filters.stamina }"
@click="setAssetFilter('stamina', '')"
>
全部
</button>
<button
v-for="item in levelOptions"
:key="`stamina-${item}`"
type="button"
:class="{ active: filters.stamina === item }"
@click="setAssetFilter('stamina', item)"
>
{{ levelSlotLabel(item) }}
</button>
</div>
</div>
<div class="admin-filter-section">
<span class="admin-filter-title">负重</span>
<div class="admin-option-grid">
<button
type="button"
:class="{ active: !filters.load }"
@click="setAssetFilter('load', '')"
>
全部
</button>
<button
v-for="item in levelOptions"
:key="`load-${item}`"
type="button"
:class="{ active: filters.load === item }"
@click="setAssetFilter('load', item)"
>
{{ levelSlotLabel(item) }}
</button>
</div>
</div>
<button
v-if="assetFilterActive"
class="admin-filter-reset"
type="button"
@click="clearAssetFilters()"
>
清空资产筛选
</button>
</div>
</el-popover>
</el-form-item>
<el-form-item label="皮肤">
<el-popover
:visible="activeFilterPopover === 'skin'"
trigger="click"
placement="bottom-start"
popper-class="admin-listing-filter-popover"
:width="420"
@update:visible="setFilterPopover('skin', $event)"
>
<template #reference>
<button
class="admin-filter-chip wide"
:class="{ active: skinFilterActive }"
type="button"
>
<span>{{ skinFilterLabel }}</span>
<el-icon><ArrowDown /></el-icon>
</button>
</template>
<div class="admin-skin-filter-menu">
<button
class="admin-filter-reset"
type="button"
:class="{ active: !filters.skin_group && !filters.skin_name }"
@click="clearSkinFilters()"
>
全部皮肤
</button>
<div v-if="!skinFilterGroups.length" class="admin-filter-empty">暂无皮肤选项</div>
<div
v-for="group in skinFilterGroups"
:key="group.key"
class="admin-filter-section"
>
<div class="admin-skin-filter-title">
<strong>{{ group.title }}</strong>
<button
type="button"
:class="{ active: filters.skin_group === group.key && !filters.skin_name }"
@click="selectSkinFilter(group.key)"
>
全部{{ group.title.replace('干员', '') }}
</button>
</div>
<div class="admin-skin-option-grid">
<button
v-for="skin in group.options"
:key="`${group.key}-${skin}`"
type="button"
:class="{
active: filters.skin_group === group.key && filters.skin_name === skin,
}"
@click="selectSkinFilter(group.key, skin)"
>
{{ skin }}
</button>
</div>
</div>
</div>
</el-popover>
</el-form-item>
</el-form>
<div class="listing-action-strip">
<el-button @click="resetFilters">重置</el-button>
@@ -166,6 +166,11 @@ export interface AdminListingQuery {
owner_id?: string
status?: ListingStatus | ''
review_status?: ListingReviewStatus | ''
insurance?: string
stamina?: string
load?: string
skin_group?: string
skin_name?: string
limit?: number
page?: number
page_size?: number
+162
View File
@@ -533,9 +533,11 @@ h1 {
.listing-filter-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0;
margin: 0;
row-gap: 10px;
}
.listing-filter-bar .el-form-item {
@@ -564,6 +566,166 @@ h1 {
white-space: nowrap;
}
.admin-filter-chip {
display: inline-flex;
width: 104px;
height: 32px;
align-items: center;
justify-content: space-between;
gap: 6px;
padding: 0 10px;
border: 1px solid #dcdfe6;
border-radius: 6px;
background: #ffffff;
color: #606266;
cursor: pointer;
font-size: 13px;
line-height: 1;
transition:
border-color 0.15s ease,
color 0.15s ease,
background 0.15s ease;
}
.admin-filter-chip.wide {
width: 150px;
}
.admin-filter-chip span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.admin-filter-chip:hover,
.admin-filter-chip.active {
border-color: #409eff;
background: #ecf5ff;
color: #1677ff;
}
.admin-filter-chip .el-icon {
flex-shrink: 0;
}
.admin-listing-filter-popover {
padding: 10px !important;
border-radius: 8px !important;
}
.admin-asset-filter-menu,
.admin-skin-filter-menu {
display: flex;
max-height: 420px;
flex-direction: column;
gap: 12px;
overflow-y: auto;
}
.admin-filter-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.admin-filter-title {
color: #6b7785;
font-size: 12px;
font-weight: 700;
}
.admin-option-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 6px;
}
.admin-option-grid.compact {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.admin-option-grid button,
.admin-skin-option-grid button,
.admin-filter-reset,
.admin-skin-filter-title button {
border: none;
border-radius: 6px;
background: transparent;
color: #334155;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition:
background 0.15s ease,
color 0.15s ease;
}
.admin-option-grid button {
min-height: 30px;
padding: 7px 8px;
}
.admin-option-grid button:hover,
.admin-skin-option-grid button:hover,
.admin-filter-reset:hover,
.admin-skin-filter-title button:hover {
background: #f1f5f9;
}
.admin-option-grid button.active,
.admin-skin-option-grid button.active,
.admin-filter-reset.active,
.admin-skin-filter-title button.active {
background: #ecf5ff;
color: #1677ff;
font-weight: 700;
}
.admin-filter-reset {
min-height: 32px;
padding: 8px 10px;
text-align: left;
}
.admin-filter-empty {
padding: 10px;
color: #8f9bba;
font-size: 12px;
}
.admin-skin-filter-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.admin-skin-filter-title strong {
color: #17233d;
font-size: 13px;
}
.admin-skin-filter-title button {
flex-shrink: 0;
padding: 5px 8px;
}
.admin-skin-option-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
}
.admin-skin-option-grid button {
min-height: 30px;
padding: 8px 10px;
overflow: hidden;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ========== Dashboard Panels ========== */
.dashboard-panels {
display: grid;