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:
@@ -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">支持 MP3、WAV、AAC、M4A、FLAC、OGG,最大 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">支持 MP3、WAV、AAC、M4A、FLAC、OGG,最大 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;
|
||||
|
||||
Reference in New Issue
Block a user