fix(skills): P3 frontend polish for skill/agent category split
Deploy to Production / deploy (push) Waiting to run Details

- skills.ts: make category/agent_type/execution_mode/task_mode optional
  in ISkillInfo and ISkillDetail for backward compat during rollout
- SkillCard.vue: remove dead size="small" on a-tag, add title attr for
  a11y, add isEngine computed, CSS fallback cleanup, category fallback
  in class binding
- SkillsView.vue: fix a-empty condition to use grouped counts so orphan
  skills (category mismatch) don't render a blank page
- SkillsTab.vue: add type tag (引擎/技能) and category-based icon
  (thunderbolt for engine, appstore for business), remove size="small",
  add engine icon color variant
This commit is contained in:
chiguyong 2026-06-23 19:41:54 +08:00
parent a672dddc9a
commit 3d108dd08e
4 changed files with 51 additions and 30 deletions

View File

@ -13,11 +13,11 @@ export interface ISkillInfo {
capabilities: string[]
dependencies: string[]
status: string
/** "agent_template" = 通用执行引擎 (react/direct/rewoo/...); "business_skill" = 业务领域技能 */
category: SkillCategory
agent_type: string
execution_mode: string
task_mode: string
/** "agent_template" = 通用执行引擎 (react/direct/rewoo/...); "business_skill" = 业务领域技能. Optional for backward compat during rollout. */
category?: SkillCategory
agent_type?: string
execution_mode?: string
task_mode?: string
}
export interface ISkillDetail {
@ -28,10 +28,10 @@ export interface ISkillDetail {
dependencies: string[]
config: Record<string, unknown>
health_status: string
category: SkillCategory
agent_type: string
execution_mode: string
task_mode: string
category?: SkillCategory
agent_type?: string
execution_mode?: string
task_mode?: string
}
export interface ICapabilityInfo {

View File

@ -20,11 +20,19 @@
v-for="skill in skillsStore.skills"
:key="skill.name"
class="skills-tab__item"
:class="`skills-tab__item--${skill.category || 'business_skill'}`"
@click="openSkill(skill.name)"
>
<div class="skills-tab__item-header">
<AppstoreOutlined class="skills-tab__item-icon" />
<component :is="iconFor(skill)" class="skills-tab__item-icon" />
<span class="skills-tab__item-name">{{ skill.name }}</span>
<a-tag
class="skills-tab__item-type"
:color="isEngine(skill) ? 'purple' : 'blue'"
:title="isEngine(skill) ? '执行引擎模板' : '业务领域技能'"
>
{{ isEngine(skill) ? '引擎' : '技能' }}
</a-tag>
<a-badge
class="skills-tab__item-status"
:status="skill.status === 'active' ? 'success' : 'default'"
@ -33,7 +41,7 @@
</div>
<p class="skills-tab__item-desc">{{ skill.description || '暂无描述' }}</p>
<div class="skills-tab__item-tags">
<a-tag v-for="cap in skill.capabilities.slice(0, 3)" :key="cap" size="small">
<a-tag v-for="cap in skill.capabilities.slice(0, 3)" :key="cap">
{{ cap }}
</a-tag>
<span v-if="skill.capabilities.length > 3" class="skills-tab__item-more">
@ -53,13 +61,22 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { AppstoreOutlined, WarningOutlined } from '@ant-design/icons-vue'
import { AppstoreOutlined, ThunderboltOutlined, WarningOutlined } from '@ant-design/icons-vue'
import { useSkillsStore } from '@/stores/skills'
import type { ISkillInfo } from '@/api/skills'
import SkillDetail from '@/components/skills/SkillDetail.vue'
const skillsStore = useSkillsStore()
const detailVisible = ref(false)
function isEngine(skill: ISkillInfo): boolean {
return skill.category === 'agent_template'
}
function iconFor(skill: ISkillInfo) {
return isEngine(skill) ? ThunderboltOutlined : AppstoreOutlined
}
async function openSkill(name: string): Promise<void> {
await skillsStore.fetchSkillDetail(name)
detailVisible.value = true
@ -130,6 +147,14 @@ onMounted(() => {
font-size: var(--font-sm);
}
.skills-tab__item--agent_template .skills-tab__item-icon {
color: var(--accent-board);
}
.skills-tab__item-type {
margin-right: 0;
}
.skills-tab__item-name {
font-weight: var(--font-weight-medium);
color: var(--text-primary);

View File

@ -1,5 +1,5 @@
<template>
<div class="skill-card" :class="`skill-card--${skill.category}`" @click="$emit('click')">
<div class="skill-card" :class="`skill-card--${skill.category || 'business_skill'}`" @click="$emit('click')">
<a-card hoverable size="small">
<template #title>
<div class="skill-card__title">
@ -10,11 +10,11 @@
<template #extra>
<div class="skill-card__badges">
<a-tag
:color="skill.category === 'agent_template' ? 'purple' : 'blue'"
size="small"
:color="isEngine ? 'purple' : 'blue'"
class="skill-card__type-tag"
:title="isEngine ? '执行引擎模板' : '业务领域技能'"
>
{{ skill.category === 'agent_template' ? '引擎' : '技能' }}
{{ isEngine ? '引擎' : '技能' }}
</a-tag>
<a-badge
:status="skill.status === 'active' ? 'success' : 'default'"
@ -28,13 +28,13 @@
<code class="skill-card__mode-value">{{ modeText }}</code>
</div>
<div class="skill-card__tags">
<a-tag v-for="cap in skill.capabilities" :key="cap" size="small" color="blue">
<a-tag v-for="cap in skill.capabilities" :key="cap" color="blue">
{{ cap }}
</a-tag>
</div>
<div v-if="skill.dependencies.length > 0" class="skill-card__deps">
<span class="skill-card__deps-label">依赖:</span>
<a-tag v-for="dep in skill.dependencies.slice(0, 3)" :key="dep" size="small">
<a-tag v-for="dep in skill.dependencies.slice(0, 3)" :key="dep">
{{ dep }}
</a-tag>
<span v-if="skill.dependencies.length > 3" class="skill-card__more">
@ -61,22 +61,18 @@ defineEmits<{
click: []
}>()
const isEngine = computed(() => props.skill.category === 'agent_template')
// Differentiate icon by category: engine = thunderbolt, business = appstore
const titleIcon = computed(() =>
props.skill.category === 'agent_template' ? ThunderboltOutlined : AppstoreOutlined
)
const titleIcon = computed(() => (isEngine.value ? ThunderboltOutlined : AppstoreOutlined))
// Show the most relevant mode field.
// Agent templates: execution_mode (react/direct/rewoo/...).
// Business skills: agent_type (the domain identifier).
const modeText = computed(() =>
props.skill.category === 'agent_template'
? props.skill.execution_mode
: props.skill.agent_type
)
const modeLabel = computed(() =>
props.skill.category === 'agent_template' ? '执行模式' : '领域类型'
isEngine.value ? props.skill.execution_mode : props.skill.agent_type
)
const modeLabel = computed(() => (isEngine.value ? '执行模式' : '领域类型'))
</script>
<style scoped>
@ -118,7 +114,7 @@ const modeLabel = computed(() =>
align-items: center;
gap: 4px;
margin-bottom: var(--space-2);
font-size: var(--font-xs, 12px);
font-size: var(--font-xs);
}
.skill-card__mode-label {
@ -128,7 +124,7 @@ const modeLabel = computed(() =>
.skill-card__mode-value {
font-family: 'SF Mono', 'Fira Code', Menlo, Consolas, monospace;
color: var(--text-secondary);
background: var(--bg-secondary, rgba(0, 0, 0, 0.04));
background: var(--bg-secondary);
padding: 1px 4px;
border-radius: 3px;
}

View File

@ -62,7 +62,7 @@
</section>
<a-empty
v-if="!skillsStore.isLoading && skillsStore.skills.length === 0"
v-if="!skillsStore.isLoading && skillsStore.agentTemplates.length + skillsStore.businessSkills.length === 0"
description="暂无已注册技能"
/>
</a-spin>