mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +08:00
feat(skill-market): paginated skill list with search and frosted-glass UI
This commit is contained in:
parent
1073890e64
commit
aa6e2b6afe
@ -1,5 +1,6 @@
|
|||||||
package vip.mate.skill.controller;
|
package vip.mate.skill.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -34,10 +35,21 @@ public class SkillController {
|
|||||||
private final SkillWorkspaceManager workspaceManager;
|
private final SkillWorkspaceManager workspaceManager;
|
||||||
private final SkillSynthesisService synthesisService;
|
private final SkillSynthesisService synthesisService;
|
||||||
|
|
||||||
@Operation(summary = "获取技能列表")
|
@Operation(summary = "获取技能分页列表(RFC-042 §2.1)")
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public R<List<SkillEntity>> list() {
|
public R<IPage<SkillEntity>> list(
|
||||||
return R.ok(skillService.listSkills());
|
@RequestParam(defaultValue = "1") int page,
|
||||||
|
@RequestParam(defaultValue = "20") int size,
|
||||||
|
@RequestParam(required = false) String keyword,
|
||||||
|
@RequestParam(required = false) String skillType,
|
||||||
|
@RequestParam(required = false) Boolean enabled) {
|
||||||
|
return R.ok(skillService.pageSkills(page, size, keyword, skillType, enabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取各类型技能计数(tab 徽章用)")
|
||||||
|
@GetMapping("/counts")
|
||||||
|
public R<Map<String, Long>> counts() {
|
||||||
|
return R.ok(skillService.countByType());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "获取已启用技能列表")
|
@Operation(summary = "获取已启用技能列表")
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package vip.mate.skill.service;
|
package vip.mate.skill.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -12,6 +14,7 @@ import vip.mate.skill.workspace.SkillWorkspaceProperties;
|
|||||||
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@ -58,6 +61,53 @@ public class SkillService {
|
|||||||
.orderByDesc(SkillEntity::getCreateTime));
|
.orderByDesc(SkillEntity::getCreateTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paginated skill listing for the SkillMarket admin UI.
|
||||||
|
*
|
||||||
|
* <p>RFC-042 §2.1 — replaces the unbounded {@code /skills} list. Filters
|
||||||
|
* are all optional; empty or {@code null} means "no filter". Keyword
|
||||||
|
* searches name / description / tags with LIKE.
|
||||||
|
*/
|
||||||
|
public IPage<SkillEntity> pageSkills(int page, int size, String keyword,
|
||||||
|
String skillType, Boolean enabled) {
|
||||||
|
Page<SkillEntity> pageParam = new Page<>(Math.max(page, 1), Math.max(size, 1));
|
||||||
|
LambdaQueryWrapper<SkillEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
|
||||||
|
if (keyword != null && !keyword.isBlank()) {
|
||||||
|
String kw = keyword.trim();
|
||||||
|
wrapper.and(w -> w
|
||||||
|
.like(SkillEntity::getName, kw)
|
||||||
|
.or().like(SkillEntity::getDescription, kw)
|
||||||
|
.or().like(SkillEntity::getTags, kw));
|
||||||
|
}
|
||||||
|
if (skillType != null && !skillType.isBlank()) {
|
||||||
|
wrapper.eq(SkillEntity::getSkillType, skillType);
|
||||||
|
}
|
||||||
|
if (enabled != null) {
|
||||||
|
wrapper.eq(SkillEntity::getEnabled, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.orderByDesc(SkillEntity::getBuiltin)
|
||||||
|
.orderByDesc(SkillEntity::getCreateTime);
|
||||||
|
|
||||||
|
return skillMapper.selectPage(pageParam, wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregate skill counts per {@code skill_type}, plus an {@code all}
|
||||||
|
* rollup. Feeds the SkillMarket tab badges without pulling every row.
|
||||||
|
*/
|
||||||
|
public Map<String, Long> countByType() {
|
||||||
|
Map<String, Long> result = new LinkedHashMap<>();
|
||||||
|
result.put("all", skillMapper.selectCount(null));
|
||||||
|
for (String type : List.of("builtin", "mcp", "dynamic")) {
|
||||||
|
result.put(type, skillMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<SkillEntity>()
|
||||||
|
.eq(SkillEntity::getSkillType, type)));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取已启用的技能列表(Agent 运行时使用)
|
* 获取已启用的技能列表(Agent 运行时使用)
|
||||||
* <p>
|
* <p>
|
||||||
|
|||||||
@ -144,7 +144,16 @@ export const conversationApi = {
|
|||||||
|
|
||||||
// ==================== Skill ====================
|
// ==================== Skill ====================
|
||||||
export const skillApi = {
|
export const skillApi = {
|
||||||
list: () => http.get('/skills'),
|
/** RFC-042 §2.1 — paginated skill listing with search/type/enabled filters */
|
||||||
|
page: (params: {
|
||||||
|
page?: number
|
||||||
|
size?: number
|
||||||
|
keyword?: string
|
||||||
|
skillType?: string
|
||||||
|
enabled?: boolean
|
||||||
|
} = {}) => http.get('/skills', { params }),
|
||||||
|
/** Tab count aggregate — returns { all, builtin, mcp, dynamic } */
|
||||||
|
counts: () => http.get('/skills/counts'),
|
||||||
listEnabled: () => http.get('/skills/enabled'),
|
listEnabled: () => http.get('/skills/enabled'),
|
||||||
get: (id: string | number) => http.get(`/skills/${id}`),
|
get: (id: string | number) => http.get(`/skills/${id}`),
|
||||||
create: (data: any) => http.post('/skills', data),
|
create: (data: any) => http.post('/skills', data),
|
||||||
|
|||||||
@ -1606,6 +1606,14 @@ export default {
|
|||||||
mcp: 'MCP',
|
mcp: 'MCP',
|
||||||
dynamic: 'Dynamic',
|
dynamic: 'Dynamic',
|
||||||
},
|
},
|
||||||
|
search: {
|
||||||
|
placeholder: 'Search by name, description, or tags...',
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
all: 'All statuses',
|
||||||
|
enabled: 'Enabled',
|
||||||
|
disabled: 'Disabled',
|
||||||
|
},
|
||||||
empty: 'No skills found',
|
empty: 'No skills found',
|
||||||
emptyDesc: 'Add skills to enhance your agents\' capabilities',
|
emptyDesc: 'Add skills to enhance your agents\' capabilities',
|
||||||
noDescription: 'No description',
|
noDescription: 'No description',
|
||||||
|
|||||||
@ -1616,6 +1616,14 @@ export default {
|
|||||||
mcp: 'MCP',
|
mcp: 'MCP',
|
||||||
dynamic: '动态',
|
dynamic: '动态',
|
||||||
},
|
},
|
||||||
|
search: {
|
||||||
|
placeholder: '搜索技能名称、描述或标签...',
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
all: '全部状态',
|
||||||
|
enabled: '已启用',
|
||||||
|
disabled: '已禁用',
|
||||||
|
},
|
||||||
empty: '暂无技能',
|
empty: '暂无技能',
|
||||||
emptyDesc: '添加技能以增强 Agent 的能力',
|
emptyDesc: '添加技能以增强 Agent 的能力',
|
||||||
noDescription: '暂无描述',
|
noDescription: '暂无描述',
|
||||||
|
|||||||
@ -488,7 +488,9 @@ async function openEditModal(agent: Agent) {
|
|||||||
// Load available skills/tools/providers and current bindings in parallel
|
// Load available skills/tools/providers and current bindings in parallel
|
||||||
try {
|
try {
|
||||||
const [skillsRes, toolsRes, providersRes, boundSkillsRes, boundToolsRes, providerPrefsRes] = await Promise.all([
|
const [skillsRes, toolsRes, providersRes, boundSkillsRes, boundToolsRes, providerPrefsRes] = await Promise.all([
|
||||||
skillApi.list(),
|
// RFC-042: /skills is now paginated; binding dropdown only needs enabled skills,
|
||||||
|
// so listEnabled() is both semantically correct and shape-stable (returns array).
|
||||||
|
skillApi.listEnabled(),
|
||||||
toolApi.list(),
|
toolApi.list(),
|
||||||
modelApi.listProviders(),
|
modelApi.listProviders(),
|
||||||
agentBindingApi.listSkills(agent.id),
|
agentBindingApi.listSkills(agent.id),
|
||||||
|
|||||||
@ -34,16 +34,31 @@
|
|||||||
<!-- 分类 Tab -->
|
<!-- 分类 Tab -->
|
||||||
<div class="category-tabs mc-surface-card">
|
<div class="category-tabs mc-surface-card">
|
||||||
<button v-for="tab in categoryTabs" :key="tab.value" class="cat-tab"
|
<button v-for="tab in categoryTabs" :key="tab.value" class="cat-tab"
|
||||||
:class="{ active: activeCategory === tab.value }" @click="activeCategory = tab.value">
|
:class="{ active: query.skillType === tab.value }" @click="onTabChange(tab.value)">
|
||||||
<span class="cat-icon">{{ tab.icon }}</span>
|
<span class="cat-icon">{{ tab.icon }}</span>
|
||||||
{{ tab.label }}
|
{{ tab.label }}
|
||||||
<span class="cat-count">{{ getCategoryCount(tab.value) }}</span>
|
<span class="cat-count">{{ getCategoryCount(tab.value) }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Search + status filter (RFC-042 §2.1) -->
|
||||||
|
<div class="skill-filter-bar mc-surface-card">
|
||||||
|
<input
|
||||||
|
v-model="query.keyword"
|
||||||
|
class="skill-search-input"
|
||||||
|
type="search"
|
||||||
|
:placeholder="t('skills.search.placeholder')"
|
||||||
|
/>
|
||||||
|
<select v-model="query.enabledFilter" class="skill-status-filter" @change="onFilterChange">
|
||||||
|
<option value="">{{ t('skills.filter.all') }}</option>
|
||||||
|
<option value="true">{{ t('skills.filter.enabled') }}</option>
|
||||||
|
<option value="false">{{ t('skills.filter.disabled') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 技能列表 -->
|
<!-- 技能列表 -->
|
||||||
<div class="skill-grid" v-if="filteredSkills.length > 0">
|
<div class="skill-grid" v-if="skills.length > 0">
|
||||||
<div v-for="skill in filteredSkills" :key="skill.id" class="skill-card mc-surface-card"
|
<div v-for="skill in skills" :key="skill.id" class="skill-card mc-surface-card"
|
||||||
:class="{ disabled: !skill.enabled }">
|
:class="{ disabled: !skill.enabled }">
|
||||||
<div class="skill-header">
|
<div class="skill-header">
|
||||||
<div class="skill-icon-wrap" :class="getSkillIconBg(skill.skillType)">
|
<div class="skill-icon-wrap" :class="getSkillIconBg(skill.skillType)">
|
||||||
@ -132,6 +147,20 @@
|
|||||||
<h3>{{ t('skills.empty') }}</h3>
|
<h3>{{ t('skills.empty') }}</h3>
|
||||||
<p>{{ t('skills.emptyDesc') }}</p>
|
<p>{{ t('skills.emptyDesc') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination (RFC-042 §2.1) -->
|
||||||
|
<el-pagination
|
||||||
|
v-if="total > 0"
|
||||||
|
class="skill-pagination"
|
||||||
|
v-model:current-page="query.page"
|
||||||
|
v-model:page-size="query.size"
|
||||||
|
:page-sizes="[12, 24, 48]"
|
||||||
|
:total="total"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
background
|
||||||
|
@size-change="onPageSizeChange"
|
||||||
|
@current-change="loadSkills"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -222,7 +251,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { skillApi } from '@/api/index'
|
import { skillApi } from '@/api/index'
|
||||||
@ -231,13 +260,24 @@ import ImportHubDialog from '@/components/skill/ImportHubDialog.vue'
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const skills = ref<Skill[]>([])
|
const skills = ref<Skill[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const counts = ref<Record<string, number>>({})
|
||||||
const runtimeStatusMap = ref<Record<string, SkillRuntimeStatus>>({})
|
const runtimeStatusMap = ref<Record<string, SkillRuntimeStatus>>({})
|
||||||
const activeCategory = ref('all')
|
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const editingSkill = ref<Skill | null>(null)
|
const editingSkill = ref<Skill | null>(null)
|
||||||
const refreshing = ref(false)
|
const refreshing = ref(false)
|
||||||
const showImportDialog = ref(false)
|
const showImportDialog = ref(false)
|
||||||
|
|
||||||
|
/** Paginated query state — RFC-042 §2.1 */
|
||||||
|
const query = reactive({
|
||||||
|
page: 1,
|
||||||
|
size: 24,
|
||||||
|
keyword: '',
|
||||||
|
skillType: 'all' as string,
|
||||||
|
/** '' = all, 'true' = enabled only, 'false' = disabled only (string to avoid tri-state checkbox quirks) */
|
||||||
|
enabledFilter: '' as string,
|
||||||
|
})
|
||||||
|
|
||||||
const categoryTabs = computed(() => [
|
const categoryTabs = computed(() => [
|
||||||
{ label: t('skills.tabs.all'), value: 'all', icon: '🗂️' },
|
{ label: t('skills.tabs.all'), value: 'all', icon: '🗂️' },
|
||||||
{ label: t('skills.tabs.builtin'), value: 'builtin', icon: '🔧' },
|
{ label: t('skills.tabs.builtin'), value: 'builtin', icon: '🔧' },
|
||||||
@ -263,14 +303,8 @@ const form = ref<any>(defaultForm())
|
|||||||
/** 是否正在编辑内置技能(限制可编辑字段) */
|
/** 是否正在编辑内置技能(限制可编辑字段) */
|
||||||
const isBuiltinEditing = computed(() => editingSkill.value?.skillType === 'builtin')
|
const isBuiltinEditing = computed(() => editingSkill.value?.skillType === 'builtin')
|
||||||
|
|
||||||
const filteredSkills = computed(() => {
|
|
||||||
if (activeCategory.value === 'all') return skills.value
|
|
||||||
return skills.value.filter(s => s.skillType === activeCategory.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
function getCategoryCount(category: string) {
|
function getCategoryCount(category: string) {
|
||||||
if (category === 'all') return skills.value.length
|
return counts.value[category] ?? 0
|
||||||
return skills.value.filter(s => s.skillType === category).length
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTags(tags: string): string[] {
|
function parseTags(tags: string): string[] {
|
||||||
@ -281,20 +315,58 @@ function parseTags(tags: string): string[] {
|
|||||||
onMounted(loadAll)
|
onMounted(loadAll)
|
||||||
|
|
||||||
async function loadAll() {
|
async function loadAll() {
|
||||||
await Promise.all([loadSkills(), loadRuntimeStatus()])
|
await Promise.all([loadSkills(), loadCounts(), loadRuntimeStatus()])
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Coalesce keyword edits into one server call per 300ms so typing doesn't thrash. */
|
||||||
|
let searchDebounce: ReturnType<typeof setTimeout> | null = null
|
||||||
|
watch(() => query.keyword, () => {
|
||||||
|
if (searchDebounce) clearTimeout(searchDebounce)
|
||||||
|
searchDebounce = setTimeout(() => {
|
||||||
|
query.page = 1
|
||||||
|
loadSkills()
|
||||||
|
}, 300)
|
||||||
|
})
|
||||||
|
|
||||||
|
function onTabChange(tab: string) {
|
||||||
|
query.skillType = tab
|
||||||
|
query.page = 1
|
||||||
|
loadSkills()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFilterChange() {
|
||||||
|
query.page = 1
|
||||||
|
loadSkills()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPageSizeChange() {
|
||||||
|
query.page = 1
|
||||||
|
loadSkills()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSkills() {
|
async function loadSkills() {
|
||||||
try {
|
try {
|
||||||
const res: any = await skillApi.list()
|
const params: Record<string, unknown> = { page: query.page, size: query.size }
|
||||||
skills.value = [...(res.data || [])].sort((a: Skill, b: Skill) => {
|
if (query.keyword) params.keyword = query.keyword.trim()
|
||||||
if (Boolean(b.builtin) !== Boolean(a.builtin)) {
|
if (query.skillType && query.skillType !== 'all') params.skillType = query.skillType
|
||||||
return Number(Boolean(b.builtin)) - Number(Boolean(a.builtin))
|
if (query.enabledFilter !== '') params.enabled = query.enabledFilter === 'true'
|
||||||
}
|
|
||||||
return a.name.localeCompare(b.name)
|
const res: any = await skillApi.page(params)
|
||||||
})
|
const data = res.data || {}
|
||||||
|
skills.value = data.records || []
|
||||||
|
total.value = data.total || 0
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
skills.value = []
|
skills.value = []
|
||||||
|
total.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCounts() {
|
||||||
|
try {
|
||||||
|
const res: any = await skillApi.counts()
|
||||||
|
counts.value = res.data || {}
|
||||||
|
} catch (e) {
|
||||||
|
counts.value = {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -538,6 +610,95 @@ function getSkillTypeLabel(type: string) {
|
|||||||
.cat-count { background: var(--mc-bg-sunken); color: var(--mc-text-secondary); padding: 1px 6px; border-radius: 10px; font-size: 11px; }
|
.cat-count { background: var(--mc-bg-sunken); color: var(--mc-text-secondary); padding: 1px 6px; border-radius: 10px; font-size: 11px; }
|
||||||
.cat-tab.active .cat-count { background: rgba(217, 119, 87, 0.2); color: var(--mc-primary); }
|
.cat-tab.active .cat-count { background: rgba(217, 119, 87, 0.2); color: var(--mc-primary); }
|
||||||
|
|
||||||
|
/* RFC-042 §2.1 — search + status filter bar (frosted glass) */
|
||||||
|
.skill-filter-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
align-items: center;
|
||||||
|
backdrop-filter: blur(14px) saturate(1.1);
|
||||||
|
-webkit-backdrop-filter: blur(14px) saturate(1.1);
|
||||||
|
}
|
||||||
|
.skill-search-input,
|
||||||
|
.skill-status-filter {
|
||||||
|
height: 34px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: rgba(255, 255, 255, 0.45);
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--mc-text-primary);
|
||||||
|
outline: none;
|
||||||
|
transition: background 0.18s, border-color 0.18s, box-shadow 0.18s;
|
||||||
|
}
|
||||||
|
html.dark .skill-search-input,
|
||||||
|
html.dark .skill-status-filter {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
.skill-search-input { flex: 1; padding: 0 12px; }
|
||||||
|
.skill-status-filter { padding: 0 10px; cursor: pointer; min-width: 140px; }
|
||||||
|
.skill-search-input:hover,
|
||||||
|
.skill-status-filter:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.65);
|
||||||
|
}
|
||||||
|
html.dark .skill-search-input:hover,
|
||||||
|
html.dark .skill-status-filter:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.skill-search-input:focus,
|
||||||
|
.skill-status-filter:focus {
|
||||||
|
border-color: rgba(217, 119, 87, 0.45);
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
box-shadow: 0 0 0 3px rgba(217, 119, 87, 0.12);
|
||||||
|
}
|
||||||
|
html.dark .skill-search-input:focus,
|
||||||
|
html.dark .skill-status-filter:focus {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination — strip Element Plus's heavy boxed look, blend with the glass surface */
|
||||||
|
.skill-pagination { margin-top: 18px; display: flex; justify-content: center; }
|
||||||
|
.skill-pagination :deep(.el-pagination) {
|
||||||
|
--el-pagination-bg-color: transparent;
|
||||||
|
--el-pagination-button-bg-color: transparent;
|
||||||
|
--el-pagination-hover-color: var(--mc-primary);
|
||||||
|
background: transparent;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--mc-text-secondary);
|
||||||
|
}
|
||||||
|
.skill-pagination :deep(.el-pagination .btn-prev),
|
||||||
|
.skill-pagination :deep(.el-pagination .btn-next),
|
||||||
|
.skill-pagination :deep(.el-pagination .el-pager li),
|
||||||
|
.skill-pagination :deep(.el-pagination .el-input__wrapper),
|
||||||
|
.skill-pagination :deep(.el-pagination .el-select .el-input__wrapper) {
|
||||||
|
background: rgba(255, 255, 255, 0.45) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
html.dark .skill-pagination :deep(.el-pagination .btn-prev),
|
||||||
|
html.dark .skill-pagination :deep(.el-pagination .btn-next),
|
||||||
|
html.dark .skill-pagination :deep(.el-pagination .el-pager li),
|
||||||
|
html.dark .skill-pagination :deep(.el-pagination .el-input__wrapper),
|
||||||
|
html.dark .skill-pagination :deep(.el-pagination .el-select .el-input__wrapper) {
|
||||||
|
background: rgba(255, 255, 255, 0.06) !important;
|
||||||
|
}
|
||||||
|
.skill-pagination :deep(.el-pagination .btn-prev:hover),
|
||||||
|
.skill-pagination :deep(.el-pagination .btn-next:hover),
|
||||||
|
.skill-pagination :deep(.el-pagination .el-pager li:hover) {
|
||||||
|
background: rgba(217, 119, 87, 0.12) !important;
|
||||||
|
color: var(--mc-primary);
|
||||||
|
}
|
||||||
|
.skill-pagination :deep(.el-pagination .el-pager li.is-active) {
|
||||||
|
background: var(--mc-primary) !important;
|
||||||
|
color: #fff;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
.skill-pagination :deep(.el-pagination .el-pagination__total),
|
||||||
|
.skill-pagination :deep(.el-pagination .el-pagination__sizes) {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 技能网格 */
|
/* 技能网格 */
|
||||||
.skill-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 18px; }
|
.skill-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 18px; }
|
||||||
.skill-card { padding: 18px; transition: all 0.15s; display: flex; flex-direction: column; min-height: 280px; }
|
.skill-card { padding: 18px; transition: all 0.15s; display: flex; flex-direction: column; min-height: 280px; }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user