添加商品管理资产皮肤筛选
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
|
||||
Status string
|
||||
ReviewStatus string
|
||||
Insurance []string
|
||||
Stamina []string
|
||||
Load []string
|
||||
SkinGroup []string
|
||||
SkinName []string
|
||||
Limit int
|
||||
Page int
|
||||
PageSize int
|
||||
|
||||
@@ -74,6 +74,11 @@ func parseAdminListQuery(c *gin.Context) (AdminListQuery, bool) {
|
||||
query.Keyword = strings.TrimSpace(c.Query("keyword"))
|
||||
query.Status = c.Query("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
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,37 @@ func (r *Repository) ListAdmin(ctx context.Context, query AdminListQuery) (*Admi
|
||||
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)
|
||||
var total int64
|
||||
if err := countDB.Count(&total).Error; err != nil {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user