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

View File

@ -20,11 +20,19 @@
v-for="skill in skillsStore.skills" v-for="skill in skillsStore.skills"
:key="skill.name" :key="skill.name"
class="skills-tab__item" class="skills-tab__item"
:class="`skills-tab__item--${skill.category || 'business_skill'}`"
@click="openSkill(skill.name)" @click="openSkill(skill.name)"
> >
<div class="skills-tab__item-header"> <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> <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 <a-badge
class="skills-tab__item-status" class="skills-tab__item-status"
:status="skill.status === 'active' ? 'success' : 'default'" :status="skill.status === 'active' ? 'success' : 'default'"
@ -33,7 +41,7 @@
</div> </div>
<p class="skills-tab__item-desc">{{ skill.description || '暂无描述' }}</p> <p class="skills-tab__item-desc">{{ skill.description || '暂无描述' }}</p>
<div class="skills-tab__item-tags"> <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 }} {{ cap }}
</a-tag> </a-tag>
<span v-if="skill.capabilities.length > 3" class="skills-tab__item-more"> <span v-if="skill.capabilities.length > 3" class="skills-tab__item-more">
@ -53,13 +61,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' 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 { useSkillsStore } from '@/stores/skills'
import type { ISkillInfo } from '@/api/skills'
import SkillDetail from '@/components/skills/SkillDetail.vue' import SkillDetail from '@/components/skills/SkillDetail.vue'
const skillsStore = useSkillsStore() const skillsStore = useSkillsStore()
const detailVisible = ref(false) 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> { async function openSkill(name: string): Promise<void> {
await skillsStore.fetchSkillDetail(name) await skillsStore.fetchSkillDetail(name)
detailVisible.value = true detailVisible.value = true
@ -130,6 +147,14 @@ onMounted(() => {
font-size: var(--font-sm); 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 { .skills-tab__item-name {
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
color: var(--text-primary); color: var(--text-primary);

View File

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

View File

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