Files
zn-ai/src/pages/skills/components/SkillDetailDrawer.vue
duanshuwen 364db041eb feat: enhance dark mode support across various components
- Updated styles in AccountSetting, SystemConfig, Version, and other components to improve dark mode visibility.
- Added dark mode classes for text and background colors to ensure better contrast and readability.
- Modified CSS variables in dark.css for consistent theming.
- Improved accessibility by ensuring all text elements are legible in dark mode.
2026-04-15 21:17:08 +08:00

251 lines
9.4 KiB
Vue

<template>
<el-drawer
:model-value="isOpen"
@update:model-value="(v: boolean) => !v && $emit('close')"
size="450px"
direction="rtl"
:with-header="false"
class="skill-detail-drawer"
>
<div v-if="skill" class="flex flex-col h-full bg-[#f3f1e9] dark:bg-[#1b1b1d]">
<div class="flex-1 overflow-y-auto px-8 py-10">
<div class="flex flex-col items-center mb-8">
<div class="w-16 h-16 flex items-center justify-center rounded-full bg-white dark:bg-[#222225] border border-black/5 dark:border-gray-700 shrink-0 mb-4 relative shadow-sm">
<span class="text-3xl">{{ skill.icon || '🔧' }}</span>
<div v-if="skill.isCore" class="absolute -bottom-1 -right-1 bg-[#f3f1e9] dark:bg-[#1b1b1d] rounded-full p-1 shadow-sm border border-black/5 dark:border-gray-700">
<RiLockLine class="h-3 w-3 text-[#525866] dark:text-gray-400 shrink-0" />
</div>
</div>
<h2 class="text-[28px] font-serif text-[#171717] dark:text-[#f3f4f6] font-normal mb-3 text-center tracking-tight">
{{ skill.name }}
</h2>
<div class="flex items-center justify-center gap-2.5 mb-6 opacity-80">
<el-tag type="info" effect="plain" class="!rounded-full !bg-black/[0.04] dark:!bg-white/[0.04] !border-0 !text-[#171717]/70 dark:!text-[#f3f4f6]/70 !px-3 !py-0.5 !text-[11px] !font-mono">
v{{ skill.version || '1.0.0' }}
</el-tag>
<el-tag type="info" effect="plain" class="!rounded-full !bg-black/[0.04] dark:!bg-white/[0.04] !border-0 !text-[#171717]/70 dark:!text-[#f3f4f6]/70 !px-3 !py-0.5 !text-[11px]">
{{ skill.isCore ? t('skills.detail.coreSystem') : skill.isBundled ? t('skills.detail.bundled') : t('skills.detail.userInstalled') }}
</el-tag>
</div>
<p v-if="skill.description" class="text-[14px] text-[#171717]/70 dark:text-gray-400 font-medium leading-[1.6] text-center px-4">
{{ skill.description }}
</p>
</div>
<div class="space-y-7 px-1">
<div class="space-y-2">
<h3 class="text-[13px] font-bold text-[#171717]/80 dark:text-gray-300">{{ t('skills.detail.source') }}</h3>
<div class="flex items-center gap-2 flex-wrap">
<el-tag type="info" effect="plain" class="!rounded-full !bg-black/[0.04] dark:!bg-white/[0.04] !border-0 !text-[#171717]/70 dark:!text-[#f3f4f6]/70 !px-3 !py-0.5 !text-[11px]">
{{ resolvedSource }}
</el-tag>
</div>
<div class="flex items-center gap-2">
<el-input
:model-value="skill.baseDir || t('skills.detail.pathUnavailable')"
readonly
class="flex-1 !h-[38px] font-mono text-[12px] bg-[#eeece3] dark:bg-[#222225] border-black/10 dark:border-gray-700 rounded-xl text-[#171717]/70 dark:text-gray-300"
/>
<el-button
class="!h-[38px] !w-[38px] !px-0"
@click="handleCopyPath"
:disabled="!skill.baseDir"
:title="t('skills.detail.copyPath')"
>
<RiFileCopyLine class="h-3.5 w-3.5" />
</el-button>
<el-button
class="!h-[38px] !w-[38px] !px-0"
@click="handleOpenFolder"
:disabled="!skill.baseDir"
:title="t('skills.detail.openActualFolder')"
>
<RiFolderOpenLine class="h-3.5 w-3.5" />
</el-button>
</div>
</div>
<div v-if="!skill.isCore" class="space-y-2">
<h3 class="text-[13px] font-bold flex items-center gap-2 text-[#171717]/80 dark:text-gray-300">
<RiKeyLine class="h-3.5 w-3.5 text-blue-500" />
{{ t('skills.detail.apiKey') }}
</h3>
<el-input
v-model="apiKey"
:placeholder="t('skills.detail.apiKeyPlaceholder', 'Enter API Key (optional)')"
type="password"
show-password
class="!h-[44px]"
/>
<p class="text-[12px] text-[#171717]/50 dark:text-gray-500 mt-2 font-medium">
{{ t('skills.detail.apiKeyDesc', 'The primary API key for this skill.') }}
</p>
</div>
<EnvVarManager v-if="!skill.isCore" v-model="envVars" />
<div v-if="skill.slug && !skill.isBundled && !skill.isCore" class="flex gap-2 justify-center pt-8">
<el-button size="small" class="!h-[28px] !text-[11px] !font-medium !px-3 !gap-1.5 !rounded-full !border-black/10 dark:!border-gray-700 !bg-transparent hover:!bg-black/5 dark:hover:!bg-white/5" @click="handleOpenClawhub">
<RiGlobalLine class="h-3 w-3" />
ClawHub
</el-button>
<el-button size="small" class="!h-[28px] !text-[11px] !font-medium !px-3 !gap-1.5 !rounded-full !border-black/10 dark:!border-gray-700 !bg-transparent hover:!bg-black/5 dark:hover:!bg-white/5" @click="handleOpenEditor">
<RiFileCodeLine class="h-3 w-3" />
{{ t('skills.detail.openManual') }}
</el-button>
</div>
</div>
<div class="pt-8 pb-4 flex items-center justify-center gap-4 w-full px-2 max-w-[340px] mx-auto">
<el-button
v-if="!skill.isCore"
type="primary"
class="flex-1 !h-[42px] !text-[13px] !rounded-full !font-semibold"
:loading="isSaving"
@click="handleSaveConfig"
>
{{ isSaving ? t('skills.detail.saving') : t('skills.detail.saveConfig') }}
</el-button>
<el-button
v-if="!skill.isCore"
class="flex-1 !h-[42px] !text-[13px] !rounded-full !font-semibold !border-black/20 dark:!border-gray-700 hover:!bg-black/5 dark:hover:!bg-white/5"
@click="handleSecondaryAction"
>
{{ secondaryLabel }}
</el-button>
</div>
</div>
</div>
</el-drawer>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import type { Skill } from '@src/lib/skills-types'
import { apiUpdateSkillConfig, apiOpenSkillPath, apiOpenSkillReadme } from '@src/lib/skills-api'
import { resolveSkillSourceLabel } from '@src/lib/skills-utils'
import { ElMessage } from 'element-plus'
import { RiLockLine, RiKeyLine, RiFileCopyLine, RiFolderOpenLine, RiGlobalLine, RiFileCodeLine } from '@remixicon/vue'
import EnvVarManager from './EnvVarManager.vue'
import { useSkillsStore } from '@src/store/skills'
const props = defineProps<{
skill: Skill | null
isOpen: boolean
}>()
const emit = defineEmits<{
close: []
toggle: [enabled: boolean]
uninstall: [slug: string]
}>()
const { t } = useI18n()
const store = useSkillsStore()
const apiKey = ref('')
const envVars = ref<Array<{ key: string; value: string }>>([])
const isSaving = ref(false)
watch(() => props.skill, (s) => {
if (!s) return
apiKey.value = String(s.config?.apiKey || '')
if (s.config?.env && typeof s.config.env === 'object') {
envVars.value = Object.entries(s.config.env).map(([k, v]) => ({ key: k, value: String(v) }))
} else {
envVars.value = []
}
}, { immediate: true })
const resolvedSource = computed(() => {
if (!props.skill) return ''
return resolveSkillSourceLabel(props.skill, t)
})
const secondaryLabel = computed(() => {
const s = props.skill
if (!s) return ''
if (!s.isBundled) {
return t('skills.detail.uninstall', 'Uninstall')
}
return s.enabled ? t('skills.detail.disable', 'Disable') : t('skills.detail.enable', 'Enable')
})
function handleOpenClawhub() {
if (!props.skill?.slug) return
window.api.external.open(`https://clawhub.ai/s/${props.skill.slug}`)
}
async function handleOpenEditor() {
if (!props.skill) return
try {
await apiOpenSkillReadme(props.skill.id, props.skill.slug, props.skill.baseDir)
ElMessage.success(t('skills.toast.openedEditor'))
} catch (err) {
ElMessage.error(t('skills.toast.failedEditor') + ': ' + String(err))
}
}
async function handleCopyPath() {
if (!props.skill?.baseDir) return
try {
await navigator.clipboard.writeText(props.skill.baseDir)
ElMessage.success(t('skills.toast.copiedPath'))
} catch (err) {
ElMessage.error(t('skills.toast.failedCopyPath') + ': ' + String(err))
}
}
async function handleOpenFolder() {
if (!props.skill) return
try {
await apiOpenSkillPath(props.skill.id, props.skill.slug, props.skill.baseDir)
} catch (err) {
ElMessage.error(t('skills.toast.failedOpenActualFolder') + ': ' + String(err))
}
}
async function handleSaveConfig() {
if (isSaving.value || !props.skill) return
isSaving.value = true
try {
const envObj = envVars.value.reduce((acc, curr) => {
const k = curr.key.trim()
if (k) acc[k] = curr.value.trim()
return acc
}, {} as Record<string, string>)
await apiUpdateSkillConfig(props.skill.id, {
apiKey: apiKey.value || '',
env: envObj,
})
await store.fetchSkills()
ElMessage.success(t('skills.detail.configSaved'))
} catch (err) {
ElMessage.error(t('skills.toast.failedSave') + ': ' + String(err))
} finally {
isSaving.value = false
}
}
function handleSecondaryAction() {
const s = props.skill
if (!s) return
if (!s.isBundled) {
emit('uninstall', s.slug || s.id)
emit('close')
} else {
emit('toggle', !s.enabled)
}
}
</script>
<style scoped>
:deep(.skill-detail-drawer .el-drawer__body) {
padding: 0;
}
</style>