添加商品管理资产皮肤筛选
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
package listing
|
||||||
|
|
||||||
|
func hasAdminAssetFilters(query AdminListQuery) bool {
|
||||||
|
return len(query.Insurance) > 0 ||
|
||||||
|
len(query.Stamina) > 0 ||
|
||||||
|
len(query.Load) > 0 ||
|
||||||
|
len(query.SkinGroup) > 0 ||
|
||||||
|
len(query.SkinName) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterAdminListings(items []ListingDTO, query AdminListQuery) []ListingDTO {
|
||||||
|
if !hasAdminAssetFilters(query) {
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
filtered := make([]ListingDTO, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
if !matchesAdminAssetQuery(item, query) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, item)
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesAdminAssetQuery(item ListingDTO, query AdminListQuery) bool {
|
||||||
|
if !matchesAny(query.Insurance, readAssetString(item.AssetSummary, "season_insurance")) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !matchesAny(query.Stamina, readAssetString(item.AssetSummary, "stamina_level")) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !matchesAny(query.Load, readAssetString(item.AssetSummary, "load_level")) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(query.SkinName) > 0 {
|
||||||
|
if len(query.SkinGroup) > 0 {
|
||||||
|
return skinGroupsContainAny(item.AssetSummary, query.SkinGroup, query.SkinName)
|
||||||
|
}
|
||||||
|
return intersects(query.SkinName, skinNamesFromSummary(item.AssetSummary))
|
||||||
|
}
|
||||||
|
if len(query.SkinGroup) > 0 {
|
||||||
|
return skinGroupsHaveAny(item.AssetSummary, query.SkinGroup)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package listing
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestFilterAdminListingsByAssetLevels(t *testing.T) {
|
||||||
|
items := []ListingDTO{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
AssetSummary: map[string]any{
|
||||||
|
"season_insurance": "3*3",
|
||||||
|
"stamina_level": "7级",
|
||||||
|
"load_level": "7级",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
AssetSummary: map[string]any{
|
||||||
|
"season_insurance": "2*3",
|
||||||
|
"stamina_level": "7级",
|
||||||
|
"load_level": "6级",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := filterAdminListings(items, AdminListQuery{
|
||||||
|
Insurance: []string{"3*3"},
|
||||||
|
Stamina: []string{"7级"},
|
||||||
|
Load: []string{"7级"},
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(filtered) != 1 || filtered[0].ID != 1 {
|
||||||
|
t.Fatalf("expected only listing 1 in 9/7/7 asset filter, got %#v", filtered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterAdminListingsBySkinGroupAndName(t *testing.T) {
|
||||||
|
items := []ListingDTO{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
AssetSummary: map[string]any{
|
||||||
|
"skin_groups": map[string]any{
|
||||||
|
"operatorRed": []any{"凌霄成卫"},
|
||||||
|
"weapon": []any{"K416命运"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
AssetSummary: map[string]any{
|
||||||
|
"skin_groups": map[string]any{
|
||||||
|
"operatorGold": []any{"露娜-金牌射手"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := filterAdminListings(items, AdminListQuery{
|
||||||
|
SkinGroup: []string{"operatorRed"},
|
||||||
|
SkinName: []string{"凌霄成卫"},
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(filtered) != 1 || filtered[0].ID != 1 {
|
||||||
|
t.Fatalf("expected only listing 1 by red skin name, got %#v", filtered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterAdminListingsBySkinGroupOnly(t *testing.T) {
|
||||||
|
items := []ListingDTO{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
AssetSummary: map[string]any{
|
||||||
|
"skin_groups": map[string]any{
|
||||||
|
"operatorRed": []any{"凌霄成卫"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
AssetSummary: map[string]any{
|
||||||
|
"skin_groups": map[string]any{
|
||||||
|
"operatorGold": []any{"露娜-金牌射手"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := filterAdminListings(items, AdminListQuery{
|
||||||
|
SkinGroup: []string{"operatorGold"},
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(filtered) != 1 || filtered[0].ID != 2 {
|
||||||
|
t.Fatalf("expected only listing 2 by gold skin group, got %#v", filtered)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,6 +76,11 @@ type AdminListQuery struct {
|
|||||||
Keyword string
|
Keyword string
|
||||||
Status string
|
Status string
|
||||||
ReviewStatus string
|
ReviewStatus string
|
||||||
|
Insurance []string
|
||||||
|
Stamina []string
|
||||||
|
Load []string
|
||||||
|
SkinGroup []string
|
||||||
|
SkinName []string
|
||||||
Limit int
|
Limit int
|
||||||
Page int
|
Page int
|
||||||
PageSize int
|
PageSize int
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ func parseAdminListQuery(c *gin.Context) (AdminListQuery, bool) {
|
|||||||
query.Keyword = strings.TrimSpace(c.Query("keyword"))
|
query.Keyword = strings.TrimSpace(c.Query("keyword"))
|
||||||
query.Status = c.Query("status")
|
query.Status = c.Query("status")
|
||||||
query.ReviewStatus = c.Query("review_status")
|
query.ReviewStatus = c.Query("review_status")
|
||||||
|
query.Insurance = parseCSVQuery(c.Query("insurance"))
|
||||||
|
query.Stamina = parseCSVQuery(c.Query("stamina"))
|
||||||
|
query.Load = parseCSVQuery(c.Query("load"))
|
||||||
|
query.SkinGroup = parseCSVQuery(c.Query("skin_group"))
|
||||||
|
query.SkinName = parseCSVQuery(firstNonEmpty(c.Query("skin_name"), c.Query("skin")))
|
||||||
return query, true
|
return query, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,37 @@ func (r *Repository) ListAdmin(ctx context.Context, query AdminListQuery) (*Admi
|
|||||||
pageSize = 100
|
pageSize = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hasAdminAssetFilters(query) {
|
||||||
|
var rows []listingRow
|
||||||
|
err := r.applyAdminListFilters(r.baseQuery(ctx), query).
|
||||||
|
Order("l.id DESC").
|
||||||
|
Scan(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items := filterAdminListings(rowsToDTO(rows), query)
|
||||||
|
total := int64(len(items))
|
||||||
|
start := (page - 1) * pageSize
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
if start >= len(items) {
|
||||||
|
items = []ListingDTO{}
|
||||||
|
} else {
|
||||||
|
end := start + pageSize
|
||||||
|
if end > len(items) {
|
||||||
|
end = len(items)
|
||||||
|
}
|
||||||
|
items = items[start:end]
|
||||||
|
}
|
||||||
|
return &AdminListResult{
|
||||||
|
Items: items,
|
||||||
|
Total: total,
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
countDB := r.applyAdminListFilters(r.db.WithContext(ctx).Table("rental_listings AS l"), query)
|
countDB := r.applyAdminListFilters(r.db.WithContext(ctx).Table("rental_listings AS l"), query)
|
||||||
var total int64
|
var total int64
|
||||||
if err := countDB.Count(&total).Error; err != nil {
|
if err := countDB.Count(&total).Error; err != nil {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<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 { ElMessage } from 'element-plus'
|
||||||
import { computed, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
emptyListingPublishOptions,
|
||||||
fetchAdminListings,
|
fetchAdminListings,
|
||||||
|
fetchListingPublishOptions,
|
||||||
type AdminListingPage,
|
type AdminListingPage,
|
||||||
type AdminListingQuery,
|
type AdminListingQuery,
|
||||||
type Listing,
|
type Listing,
|
||||||
@@ -29,10 +31,23 @@ const filters = reactive<AdminListingQuery>({
|
|||||||
owner_id: '',
|
owner_id: '',
|
||||||
status: '',
|
status: '',
|
||||||
review_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 pageSize = ref(10)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
|
const activeFilterPopover = ref<FilterPopover | ''>('')
|
||||||
|
const publishOptions = ref(emptyListingPublishOptions)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loading,
|
loading,
|
||||||
@@ -63,6 +78,36 @@ const pendingCount = computed(
|
|||||||
() => listings.value.filter(item => item.review_status === 'pending').length
|
() => listings.value.filter(item => item.review_status === 'pending').length
|
||||||
)
|
)
|
||||||
const totalPages = computed(() => Math.max(1, Math.ceil(totalListings.value / pageSize.value)))
|
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 = [
|
const screenshotColumns = [
|
||||||
{ label: '商品编码', width: 88, read: (row: Listing) => formatListingCode(row) },
|
{ label: '商品编码', width: 88, read: (row: Listing) => formatListingCode(row) },
|
||||||
@@ -93,6 +138,7 @@ const screenshotColumns = [
|
|||||||
] as const
|
] as const
|
||||||
|
|
||||||
async function queryListings() {
|
async function queryListings() {
|
||||||
|
closeFilterPopover()
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
await loadListings()
|
await loadListings()
|
||||||
}
|
}
|
||||||
@@ -102,9 +148,71 @@ function resetFilters() {
|
|||||||
filters.owner_id = ''
|
filters.owner_id = ''
|
||||||
filters.status = ''
|
filters.status = ''
|
||||||
filters.review_status = ''
|
filters.review_status = ''
|
||||||
|
clearAssetFilters(false)
|
||||||
|
clearSkinFilters(false)
|
||||||
void queryListings()
|
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) {
|
function setStatusFilter(status: string) {
|
||||||
filters.status = (filters.status === status ? '' : status) as AdminListingQuery['status']
|
filters.status = (filters.status === status ? '' : status) as AdminListingQuery['status']
|
||||||
filters.review_status = ''
|
filters.review_status = ''
|
||||||
@@ -490,6 +598,161 @@ function formatQuantity(value: number) {
|
|||||||
<el-option label="已拒绝" value="rejected" />
|
<el-option label="已拒绝" value="rejected" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</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>
|
</el-form>
|
||||||
<div class="listing-action-strip">
|
<div class="listing-action-strip">
|
||||||
<el-button @click="resetFilters">重置</el-button>
|
<el-button @click="resetFilters">重置</el-button>
|
||||||
|
|||||||
@@ -166,6 +166,11 @@ export interface AdminListingQuery {
|
|||||||
owner_id?: string
|
owner_id?: string
|
||||||
status?: ListingStatus | ''
|
status?: ListingStatus | ''
|
||||||
review_status?: ListingReviewStatus | ''
|
review_status?: ListingReviewStatus | ''
|
||||||
|
insurance?: string
|
||||||
|
stamina?: string
|
||||||
|
load?: string
|
||||||
|
skin_group?: string
|
||||||
|
skin_name?: string
|
||||||
limit?: number
|
limit?: number
|
||||||
page?: number
|
page?: number
|
||||||
page_size?: number
|
page_size?: number
|
||||||
|
|||||||
@@ -533,9 +533,11 @@ h1 {
|
|||||||
|
|
||||||
.listing-filter-bar {
|
.listing-filter-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
row-gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.listing-filter-bar .el-form-item {
|
.listing-filter-bar .el-form-item {
|
||||||
@@ -564,6 +566,166 @@ h1 {
|
|||||||
white-space: nowrap;
|
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 ========== */
|
||||||
.dashboard-panels {
|
.dashboard-panels {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
Reference in New Issue
Block a user