feat: add Claude AI skills for shadcn theming and image generation tools

This commit introduces comprehensive Claude AI skill configurations for:
- shadcn/ui theming with OKLCH color space support
- Gemini API integration for image generation and chat capabilities
- Batch processing and multi-turn conversation features
- File handling utilities for image processing workflows
This commit is contained in:
2026-03-18 02:56:05 +08:00
parent 69e96412ff
commit 791a523101
16 changed files with 1967 additions and 796 deletions

View File

@@ -3,7 +3,6 @@ import { ref, reactive, computed, onMounted } from 'vue'
import { toast } from 'vue-sonner'
import { Icon } from '@iconify/vue'
import dayjs from 'dayjs'
import BasicLayout from '@/layouts/components/BasicLayout.vue'
import { MaterialService } from '@/api/material'
import { VoiceService } from '@/api/voice'
import { useUpload } from '@/composables/useUpload'
@@ -42,6 +41,7 @@ import {
} from '@/components/ui/alert-dialog'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { Progress } from '@/components/ui/progress'
import TaskPageLayout from '@/views/system/task-management/components/TaskPageLayout.vue'
// ========== 常量 ==========
const MAX_FILE_SIZE = 5 * 1024 * 1024
@@ -369,32 +369,41 @@ onMounted(() => loadVoiceList())
</script>
<template>
<BasicLayout :show-back="false" :show-title="false">
<!-- 工具栏 -->
<div class="toolbar">
<div class="toolbar-left">
<Button @click="handleCreate">
新建配音
</Button>
<div class="p-4">
<TaskPageLayout
:loading="loading"
:current="pagination.current"
:page-size="pagination.pageSize"
:total="pagination.total"
@page-change="handlePageChange"
>
<!-- 筛选条件 -->
<template #filters>
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="flex items-center gap-3">
<Button @click="handleCreate">
新建配音
</Button>
</div>
<div class="flex items-center gap-3">
<Input
v-model="searchParams.name"
placeholder="搜索配音名称..."
class="w-64"
@keypress.enter="handleSearch"
>
<template #prefix>
<Icon icon="lucide:search" class="size-4 text-muted-foreground" />
</template>
</Input>
<Button variant="outline" @click="handleSearch">搜索</Button>
<Button variant="ghost" @click="handleReset">重置</Button>
</div>
</div>
<div class="toolbar-right">
<Input
v-model="searchParams.name"
placeholder="搜索配音名称..."
class="w-64"
@keypress.enter="handleSearch"
>
<template #prefix>
<Icon icon="lucide:search" class="size-4 text-muted-foreground" />
</template>
</Input>
<Button variant="outline" @click="handleSearch">搜索</Button>
<Button variant="ghost" @click="handleReset">重置</Button>
</div>
</div>
</template>
<!-- 表格区域 -->
<div class="table-wrapper">
<!-- 表格 -->
<template #table>
<Table>
<TableHeader>
<TableRow class="hover:bg-transparent">
@@ -405,16 +414,9 @@ onMounted(() => loadVoiceList())
</TableRow>
</TableHeader>
<TableBody>
<!-- 加载状态 -->
<TableRow v-if="loading">
<TableCell :col-span="4" class="h-48 text-center">
<Spinner class="mx-auto size-6" />
</TableCell>
</TableRow>
<!-- 空状态 -->
<TableRow v-else-if="voiceList.length === 0">
<TableCell :col-span="4" class="h-48 text-center">
<TableRow v-if="voiceList.length === 0 && !loading">
<TableCell :colspan="4" class="h-48 text-center">
<div class="flex flex-col items-center gap-3 text-muted-foreground">
<Icon icon="lucide:mic-off" class="size-10 opacity-50" />
<span>暂无配音数据</span>
@@ -460,190 +462,137 @@ onMounted(() => loadVoiceList())
</TableRow>
</TableBody>
</Table>
</template>
<!-- 分页 -->
<div v-if="pagination.total > pagination.pageSize" class="pagination-bar">
<span class="text-sm text-muted-foreground">
{{ pagination.total }}
</span>
<div class="flex gap-1">
<Button
variant="outline"
size="sm"
:disabled="pagination.current === 1"
@click="handlePageChange(pagination.current - 1)"
>
上一页
</Button>
<Button
variant="outline"
size="sm"
:disabled="pagination.current * pagination.pageSize >= pagination.total"
@click="handlePageChange(pagination.current + 1)"
>
下一页
</Button>
</div>
</div>
</div>
<!-- 弹窗 -->
<template #modals>
<!-- 新建/编辑弹窗 -->
<Dialog :open="modalVisible" @update:open="(v) => modalVisible = v">
<DialogContent class="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{{ isCreateMode ? '新建配音' : '编辑配音' }}</DialogTitle>
</DialogHeader>
<!-- 新建/编辑弹窗 -->
<Dialog :open="modalVisible" @update:open="(v) => modalVisible = v">
<DialogContent class="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{{ isCreateMode ? '新建配音' : '编辑配音' }}</DialogTitle>
</DialogHeader>
<div class="space-y-5 py-2">
<!-- 名称 -->
<div class="space-y-2">
<Label for="name">配音名称 <span class="text-destructive">*</span></Label>
<Input id="name" v-model="formData.name" placeholder="请输入配音名称" />
</div>
<div class="space-y-5 py-2">
<!-- 名称 -->
<div class="space-y-2">
<Label for="name">配音名称 <span class="text-destructive">*</span></Label>
<Input id="name" v-model="formData.name" placeholder="请输入配音名称" />
<!-- 上传区域 -->
<div v-if="isCreateMode" class="space-y-2">
<Label>音频文件 <span class="text-destructive">*</span></Label>
<!-- 未上传状态 -->
<div
v-if="fileList.length === 0 && !uploadState.uploading && !extractingText"
class="upload-zone"
:class="{ 'upload-zone--dragging': isDragging }"
@click="triggerFileInput"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
@drop="handleDrop"
>
<div class="upload-zone__icon">
<Icon icon="lucide:cloud-upload" />
</div>
<p class="upload-zone__title">点击或拖拽音频文件到此区域</p>
<p class="upload-zone__hint">支持 MP3WAVAACM4AFLACOGG最大 5MB</p>
</div>
<!-- 上传中 -->
<div v-else-if="uploadState.uploading" class="upload-status">
<Progress :value="50" class="w-16" />
<p class="mt-3 text-sm text-muted-foreground">正在上传...</p>
</div>
<!-- 识别中 -->
<div v-else-if="extractingText" class="upload-status">
<Progress :value="50" class="w-16" />
<p class="mt-3 text-sm text-muted-foreground">正在识别语音...</p>
</div>
<!-- 已上传 -->
<div v-else class="upload-preview">
<div class="upload-preview__icon">
<Icon icon="lucide:file-audio" />
</div>
<div class="upload-preview__info">
<span class="upload-preview__name">{{ fileList[0]?.name || '音频文件' }}</span>
<Badge v-if="formData.text" variant="secondary" class="gap-1">
<Icon icon="lucide:check-circle" class="size-3" />
已识别语音
</Badge>
<Badge v-else variant="outline" class="gap-1 text-amber-600">
<Icon icon="lucide:alert-circle" class="size-3" />
未识别到语音
</Badge>
</div>
<Button variant="ghost" size="sm" class="text-destructive" @click="handleRemoveFile">
<Icon icon="lucide:x" class="size-4" />
</Button>
</div>
<input
ref="fileInputRef"
type="file"
accept="audio/*,.mp3,.wav,.aac,.m4a,.flac,.ogg"
class="hidden"
@change="handleFileSelect"
/>
</div>
<!-- 备注 -->
<div class="space-y-2">
<Label for="note">备注</Label>
<Textarea
id="note"
v-model="formData.note"
placeholder="备注信息(选填)"
:rows="2"
/>
</div>
</div>
<!-- 上传区域 -->
<div v-if="isCreateMode" class="space-y-2">
<Label>音频文件 <span class="text-destructive">*</span></Label>
<DialogFooter>
<Button variant="outline" @click="handleCancel">取消</Button>
<Button :disabled="isSubmitDisabled" :loading="submitting" @click="handleSubmit">
保存
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- 未上传状态 -->
<div
v-if="fileList.length === 0 && !uploadState.uploading && !extractingText"
class="upload-zone"
:class="{ 'upload-zone--dragging': isDragging }"
@click="triggerFileInput"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
@drop="handleDrop"
<!-- 删除确认 -->
<AlertDialog :open="deleteDialogVisible" @update:open="(v) => deleteDialogVisible = v">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认删除</AlertDialogTitle>
<AlertDialogDescription>
确定要删除配音{{ deleteTarget?.name }}此操作不可恢复
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
@click="confirmDelete"
>
<div class="upload-zone__icon">
<Icon icon="lucide:cloud-upload" />
</div>
<p class="upload-zone__title">点击或拖拽音频文件到此区域</p>
<p class="upload-zone__hint">支持 MP3WAVAACM4AFLACOGG最大 5MB</p>
</div>
删除
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</template>
</TaskPageLayout>
</div>
<!-- 上传中 -->
<div v-else-if="uploadState.uploading" class="upload-status">
<Progress :value="50" class="w-16" />
<p class="mt-3 text-sm text-muted-foreground">正在上传...</p>
</div>
<!-- 识别中 -->
<div v-else-if="extractingText" class="upload-status">
<Progress :value="50" class="w-16" />
<p class="mt-3 text-sm text-muted-foreground">正在识别语音...</p>
</div>
<!-- 已上传 -->
<div v-else class="upload-preview">
<div class="upload-preview__icon">
<Icon icon="lucide:file-audio" />
</div>
<div class="upload-preview__info">
<span class="upload-preview__name">{{ fileList[0]?.name || '音频文件' }}</span>
<Badge v-if="formData.text" variant="secondary" class="gap-1">
<Icon icon="lucide:check-circle" class="size-3" />
已识别语音
</Badge>
<Badge v-else variant="outline" class="gap-1 text-amber-600">
<Icon icon="lucide:alert-circle" class="size-3" />
未识别到语音
</Badge>
</div>
<Button variant="ghost" size="sm" class="text-destructive" @click="handleRemoveFile">
<Icon icon="lucide:x" class="size-4" />
</Button>
</div>
<input
ref="fileInputRef"
type="file"
accept="audio/*,.mp3,.wav,.aac,.m4a,.flac,.ogg"
class="hidden"
@change="handleFileSelect"
/>
</div>
<!-- 备注 -->
<div class="space-y-2">
<Label for="note">备注</Label>
<Textarea
id="note"
v-model="formData.note"
placeholder="备注信息(选填)"
:rows="2"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="handleCancel">取消</Button>
<Button :disabled="isSubmitDisabled" :loading="submitting" @click="handleSubmit">
保存
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- 删除确认 -->
<AlertDialog :open="deleteDialogVisible" @update:open="(v) => deleteDialogVisible = v">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认删除</AlertDialogTitle>
<AlertDialogDescription>
确定要删除配音{{ deleteTarget?.name }}此操作不可恢复
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
@click="confirmDelete"
>
删除
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<audio ref="audioPlayer" class="hidden" />
</BasicLayout>
<audio ref="audioPlayer" class="hidden" />
</template>
<style scoped lang="less">
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-4);
background: var(--card);
border-radius: var(--radius-lg);
margin-bottom: var(--space-4);
box-shadow: var(--shadow-sm);
}
.toolbar-right {
display: flex;
align-items: center;
gap: var(--space-2);
}
.table-wrapper {
background: var(--card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
overflow: hidden;
}
.pagination-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-3) var(--space-4);
border-top: 1px solid var(--border);
background: var(--card);
}
// 上传区域
.upload-zone {
display: flex;