446 lines
10 KiB
Vue
446 lines
10 KiB
Vue
<template>
|
|
<!-- Workflow panel | 工作流浮动面板 -->
|
|
<Transition name="panel-slide">
|
|
<div v-if="visible" class="workflow-panel" v-click-outside="handleClickOutside">
|
|
<!-- Header | 头部 -->
|
|
<div class="panel-header">
|
|
<div class="panel-tabs">
|
|
<span
|
|
class="tab-item"
|
|
:class="{ active: activeTab === 'public' }"
|
|
@click="activeTab = 'public'"
|
|
>公共工作流</span>
|
|
<span
|
|
class="tab-item"
|
|
:class="{ active: activeTab === 'my' }"
|
|
@click="activeTab = 'my'"
|
|
>我的工作流</span>
|
|
</div>
|
|
<button class="expand-btn" @click="visible = false">
|
|
<n-icon :size="16"><CloseOutline /></n-icon>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Content | 内容 -->
|
|
<div class="panel-content">
|
|
<!-- Public workflows | 公共工作流 -->
|
|
<div v-if="activeTab === 'public'" class="workflow-grid">
|
|
<div
|
|
v-for="workflow in publicWorkflows"
|
|
:key="workflow.id"
|
|
class="workflow-card"
|
|
@click="handleAddWorkflow(workflow)"
|
|
>
|
|
<div class="card-cover">
|
|
<img v-if="workflow.cover" :src="workflow.cover" :alt="workflow.name" class="cover-img" />
|
|
<n-icon v-else :size="36" class="cover-icon">
|
|
<component :is="getIcon(workflow.icon)" />
|
|
</n-icon>
|
|
</div>
|
|
<div class="card-title">{{ workflow.name }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- My workflows | 我的工作流 -->
|
|
<div v-else class="my-workflows">
|
|
<div class="my-toolbar">
|
|
<button class="save-current-btn" @click="$emit('save-current')" title="保存当前工作流">
|
|
<n-icon :size="15"><BookmarkOutline /></n-icon>
|
|
<span>保存当前</span>
|
|
</button>
|
|
<button class="refresh-btn" @click="$emit('refresh-workflows')" title="刷新我的工作流">
|
|
<n-icon :size="16"><RefreshOutline /></n-icon>
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="loadingMyWorkflows" class="empty-state">
|
|
<n-icon :size="30" class="text-gray-500">
|
|
<RefreshOutline />
|
|
</n-icon>
|
|
<p class="text-gray-500 text-sm mt-2">正在加载...</p>
|
|
</div>
|
|
|
|
<div v-else-if="myWorkflows.length" class="workflow-grid">
|
|
<div
|
|
v-for="workflow in myWorkflows"
|
|
:key="workflow.id"
|
|
class="workflow-card my-workflow-card"
|
|
@click="handleAddWorkflow(workflow)"
|
|
>
|
|
<button
|
|
class="delete-workflow-btn"
|
|
title="删除工作流"
|
|
@click.stop="$emit('delete-workflow', workflow)"
|
|
>
|
|
<n-icon :size="13"><TrashOutline /></n-icon>
|
|
</button>
|
|
<div class="card-cover">
|
|
<img v-if="workflow.thumbnail" :src="workflow.thumbnail" :alt="workflow.name" class="cover-img" />
|
|
<n-icon v-else :size="34" class="cover-icon">
|
|
<BookmarkOutline />
|
|
</n-icon>
|
|
</div>
|
|
<div class="card-title">{{ workflow.name }}</div>
|
|
<div class="card-meta">{{ formatWorkflowMeta(workflow) }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="empty-state">
|
|
<n-icon :size="36" class="text-gray-500">
|
|
<FolderOpenOutline />
|
|
</n-icon>
|
|
<p class="text-gray-500 text-sm mt-2">暂无自定义工作流</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</template>
|
|
|
|
<script setup>
|
|
/**
|
|
* Workflow Panel Component | 工作流面板组件
|
|
* 显示工作流模板列表,支持一键添加到画布
|
|
*/
|
|
import { computed, ref } from 'vue'
|
|
import { NIcon } from 'naive-ui'
|
|
import {
|
|
CloseOutline,
|
|
GridOutline,
|
|
ImageOutline,
|
|
VideocamOutline,
|
|
FolderOpenOutline,
|
|
BookOutline,
|
|
PersonOutline,
|
|
CartOutline,
|
|
ChatbubbleOutline,
|
|
BookmarkOutline,
|
|
RefreshOutline,
|
|
TrashOutline
|
|
} from '@vicons/ionicons5'
|
|
import { WORKFLOW_TEMPLATES } from '../config/workflows'
|
|
|
|
const props = defineProps({
|
|
show: Boolean,
|
|
myWorkflows: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
loadingMyWorkflows: Boolean
|
|
})
|
|
|
|
const emit = defineEmits(['update:show', 'add-workflow', 'save-current', 'delete-workflow', 'refresh-workflows'])
|
|
|
|
// Active tab | 当前标签
|
|
const activeTab = ref('public')
|
|
|
|
// Visible state | 显示状态
|
|
const visible = computed({
|
|
get: () => props.show,
|
|
set: (val) => emit('update:show', val)
|
|
})
|
|
|
|
// Public workflows | 公共工作流
|
|
const publicWorkflows = computed(() => WORKFLOW_TEMPLATES)
|
|
|
|
// Icon mapping | 图标映射
|
|
const iconMap = {
|
|
GridOutline,
|
|
ImageOutline,
|
|
VideocamOutline,
|
|
BookOutline,
|
|
PersonOutline,
|
|
ShoppingOutline: CartOutline,
|
|
ChatbubbleOutline
|
|
}
|
|
|
|
const getIcon = (iconName) => {
|
|
return iconMap[iconName] || GridOutline
|
|
}
|
|
|
|
// Handle add workflow | 处理添加工作流
|
|
const handleAddWorkflow = (workflow) => {
|
|
// 直接添加工作流,节点内容由用户自己填写
|
|
emit('add-workflow', { workflow, options: {} })
|
|
visible.value = false
|
|
}
|
|
|
|
const formatWorkflowMeta = (workflow) => {
|
|
const count = workflow.workflowData?.nodes?.length || 0
|
|
return `${count} 个节点`
|
|
}
|
|
|
|
// Handle click outside | 点击外部关闭
|
|
const handleClickOutside = () => {
|
|
visible.value = false
|
|
}
|
|
|
|
// Custom directive | 自定义指令
|
|
const vClickOutside = {
|
|
mounted(el, binding) {
|
|
el._clickOutside = (e) => {
|
|
if (!el.contains(e.target)) {
|
|
binding.value()
|
|
}
|
|
}
|
|
setTimeout(() => {
|
|
document.addEventListener('click', el._clickOutside)
|
|
}, 0)
|
|
},
|
|
unmounted(el) {
|
|
document.removeEventListener('click', el._clickOutside)
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Panel container | 面板容器 */
|
|
.workflow-panel {
|
|
position: fixed;
|
|
left: 72px;
|
|
top: 100px;
|
|
width: 520px;
|
|
max-height: 70vh;
|
|
background: var(--bg-secondary);
|
|
backdrop-filter: blur(12px);
|
|
border-radius: 16px;
|
|
border: 1px solid var(--border-color);
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
|
z-index: 100;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
:global(.dark) .workflow-panel {
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
}
|
|
|
|
/* Header | 头部 */
|
|
.panel-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 16px 20px 12px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
.panel-tabs {
|
|
display: flex;
|
|
gap: 24px;
|
|
}
|
|
|
|
.tab-item {
|
|
font-size: 15px;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
transition: color 0.2s;
|
|
padding-bottom: 4px;
|
|
}
|
|
|
|
.tab-item:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.tab-item.active {
|
|
color: var(--text-primary);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.expand-btn {
|
|
width: 28px;
|
|
height: 28px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--bg-tertiary);
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.expand-btn:hover {
|
|
background: var(--border-color);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
/* Content | 内容区 */
|
|
.panel-content {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 16px;
|
|
}
|
|
|
|
.my-workflows {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
}
|
|
|
|
.my-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
}
|
|
|
|
.save-current-btn,
|
|
.refresh-btn,
|
|
.delete-workflow-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 1px solid var(--border-color);
|
|
background: var(--bg-tertiary);
|
|
color: var(--text-primary);
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.save-current-btn {
|
|
gap: 6px;
|
|
height: 30px;
|
|
padding: 0 10px;
|
|
border-radius: 8px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.refresh-btn {
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.save-current-btn:hover,
|
|
.refresh-btn:hover,
|
|
.delete-workflow-btn:hover {
|
|
border-color: var(--accent-color);
|
|
color: var(--accent-color);
|
|
}
|
|
|
|
/* Workflow grid | 工作流网格 */
|
|
.workflow-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 16px;
|
|
}
|
|
|
|
/* Workflow card | 工作流卡片 */
|
|
.workflow-card {
|
|
cursor: pointer;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.my-workflow-card {
|
|
position: relative;
|
|
}
|
|
|
|
.workflow-card:hover {
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.workflow-card:hover .card-cover {
|
|
border-color: var(--accent-color);
|
|
}
|
|
|
|
.card-cover {
|
|
aspect-ratio: 1;
|
|
border-radius: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--bg-tertiary);
|
|
border: 1px solid var(--border-color);
|
|
transition: border-color 0.2s;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.cover-img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.cover-icon {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.card-title {
|
|
margin-top: 10px;
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
text-align: center;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.card-meta {
|
|
margin-top: 3px;
|
|
font-size: 11px;
|
|
color: var(--text-secondary);
|
|
text-align: center;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.delete-workflow-btn {
|
|
position: absolute;
|
|
right: 6px;
|
|
top: 6px;
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 7px;
|
|
opacity: 0;
|
|
z-index: 2;
|
|
}
|
|
|
|
.my-workflow-card:hover .delete-workflow-btn {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Empty state | 空状态 */
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 60px 24px;
|
|
text-align: center;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* Transition | 过渡动画 */
|
|
.panel-slide-enter-active,
|
|
.panel-slide-leave-active {
|
|
transition: all 0.25s ease;
|
|
}
|
|
|
|
.panel-slide-enter-from,
|
|
.panel-slide-leave-to {
|
|
opacity: 0;
|
|
transform: translateX(-12px);
|
|
}
|
|
|
|
/* Scrollbar | 滚动条 */
|
|
.panel-content::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.panel-content::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.panel-content::-webkit-scrollbar-thumb {
|
|
background: var(--border-color);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.panel-content::-webkit-scrollbar-thumb:hover {
|
|
background: var(--text-secondary);
|
|
}
|
|
</style>
|