748 lines
17 KiB
Vue
748 lines
17 KiB
Vue
<template>
|
|
<div
|
|
:class="[
|
|
'agent-card',
|
|
`agent-card--${agent.status}`,
|
|
{ 'agent-card--hovered': isHovered }
|
|
]"
|
|
@mouseenter="handleMouseEnter"
|
|
@mouseleave="handleMouseLeave"
|
|
ref="cardRef"
|
|
>
|
|
<!-- 卡片头部 -->
|
|
<div class="agent-card__header">
|
|
<div class="agent-avatar-section">
|
|
<div class="agent-avatar">
|
|
<img
|
|
:src="agent.avatar || defaultAvatar"
|
|
:alt="agent.name"
|
|
class="avatar-image"
|
|
@error="handleImageError"
|
|
/>
|
|
<div
|
|
:class="[
|
|
'status-indicator',
|
|
{ 'status-indicator--online': agent.status === AGENT_STATUS.ACTIVE }
|
|
]"
|
|
></div>
|
|
</div>
|
|
<div class="agent-basic-info">
|
|
<h3 class="agent-name">{{ agent.name }}</h3>
|
|
<div class="agent-role">
|
|
<el-tag :type="roleType" size="small">
|
|
<el-icon v-if="roleIcon" class="role-icon"><component :is="roleIcon" /></el-icon>
|
|
{{ roleLabel }}
|
|
</el-tag>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="agent-menu">
|
|
<el-dropdown trigger="click" @command="handleMenuCommand">
|
|
<el-button size="small" text>
|
|
<el-icon><MoreFilled /></el-icon>
|
|
</el-button>
|
|
<template #dropdown>
|
|
<el-dropdown-menu>
|
|
<el-dropdown-item command="edit">
|
|
<el-icon><Edit /></el-icon>
|
|
{{ $t('agents.card.actions.edit') }}
|
|
</el-dropdown-item>
|
|
<el-dropdown-item command="duplicate" :disabled="isProcessing">
|
|
<el-icon><CopyDocument /></el-icon>
|
|
{{ $t('agents.card.actions.duplicate') }}
|
|
</el-dropdown-item>
|
|
<el-dropdown-item
|
|
command="device"
|
|
:disabled="isProcessing"
|
|
>
|
|
<el-icon><Connection /></el-icon>
|
|
{{ $t('agents.card.actions.device') }}
|
|
</el-dropdown-item>
|
|
<el-dropdown-item
|
|
command="export"
|
|
:disabled="isProcessing"
|
|
>
|
|
<el-icon><Download /></el-icon>
|
|
{{ $t('agents.card.actions.export') }}
|
|
</el-dropdown-item>
|
|
<el-dropdown-item divided command="delete" class="text-danger">
|
|
<el-icon><Delete /></el-icon>
|
|
{{ $t('agents.card.actions.delete') }}
|
|
</el-dropdown-item>
|
|
</el-dropdown-menu>
|
|
</template>
|
|
</el-dropdown>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 智能体描述 -->
|
|
<div class="agent-card__description">
|
|
<p class="description-text" :title="agent.description">
|
|
{{ truncatedDescription }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- 设备绑定状态 -->
|
|
<div class="device-binding-section">
|
|
<div class="device-status">
|
|
<el-icon
|
|
:class="[
|
|
'device-icon',
|
|
{ 'device-icon--bound': agent.device.bound }
|
|
]"
|
|
>
|
|
<Phone v-if="agent.device.bound" />
|
|
<Monitor v-else />
|
|
</el-icon>
|
|
<div class="device-info">
|
|
<span class="device-status-text">
|
|
{{ agent.device.bound ? $t('agents.card.device.bound') : $t('agents.card.device.unbound') }}
|
|
</span>
|
|
<span v-if="agent.device.bound && agent.device.deviceName" class="device-name">
|
|
{{ agent.device.deviceName }}
|
|
</span>
|
|
</div>
|
|
<el-button
|
|
v-if="!agent.device.bound"
|
|
type="primary"
|
|
size="small"
|
|
@click="handleBindDevice"
|
|
:loading="bindingState.loading"
|
|
:disabled="isProcessing"
|
|
>
|
|
{{ $t('agents.card.device.bind') }}
|
|
</el-button>
|
|
<el-dropdown v-else trigger="click" @command="handleDeviceMenuCommand">
|
|
<el-button size="small" text>
|
|
<el-icon><MoreFilled /></el-icon>
|
|
</el-button>
|
|
<template #dropdown>
|
|
<el-dropdown-menu>
|
|
<el-dropdown-item command="view-device">
|
|
<el-icon><View /></el-icon>
|
|
{{ $t('agents.card.device.viewDetails') }}
|
|
</el-dropdown-item>
|
|
<el-dropdown-item command="unbind-device">
|
|
<el-icon><CloseBold /></el-icon>
|
|
{{ $t('agents.card.device.unbind') }}
|
|
</el-dropdown-item>
|
|
</el-dropdown-menu>
|
|
</template>
|
|
</el-dropdown>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 最新活动/对话预览 -->
|
|
<div v-if="lastActivity" class="activity-preview">
|
|
<div class="activity-header">
|
|
<el-icon class="activity-icon"><ChatDotRound /></el-icon>
|
|
<span class="activity-title">{{ $t('agents.card.activity.title') }}</span>
|
|
</div>
|
|
<div class="activity-content">
|
|
<p class="activity-text">{{ lastActivity.message }}</p>
|
|
<span class="activity-time">{{ formatRelativeTime(lastActivity.timestamp) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 统计信息 -->
|
|
<div class="stats-section">
|
|
<div class="stats-grid">
|
|
<div class="stat-item">
|
|
<span class="stat-value">{{ agent.settings?.temperature || 0.7 }}</span>
|
|
<span class="stat-label">{{ $t('agents.card.stats.temperature') }}</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-value">{{ formatNumber(agent.settings?.maxTokens || 0) }}</span>
|
|
<span class="stat-label">{{ $t('agents.card.stats.tokens') }}</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-value">{{ agent.voice }}</span>
|
|
<span class="stat-label">{{ $t('agents.card.stats.voice') }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 错误状态显示 -->
|
|
<div v-if="error" class="error-section">
|
|
<el-alert
|
|
:title="error"
|
|
type="error"
|
|
:closable="false"
|
|
show-icon
|
|
class="error-alert"
|
|
/>
|
|
</div>
|
|
|
|
<!-- 加载状态覆盖 -->
|
|
<div v-if="bindingState.loading || isProcessing" class="loading-overlay">
|
|
<el-icon class="loading-icon"><Loading /></el-icon>
|
|
<span class="loading-text">
|
|
{{ bindingState.verifying ? $t('agents.card.device.verifying') : $t('agents.card.loading') }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import {
|
|
MoreFilled,
|
|
Edit,
|
|
CopyDocument,
|
|
Connection,
|
|
Download,
|
|
Delete,
|
|
Phone,
|
|
Monitor,
|
|
ChatDotRound,
|
|
View,
|
|
CloseBold,
|
|
Loading,
|
|
User,
|
|
Briefcase,
|
|
MagicStick,
|
|
Reading,
|
|
} from '@element-plus/icons-vue'
|
|
import { AGENT_STATUS, AGENT_ROLES, VOICE_MODELS } from '../stores/agents.js'
|
|
|
|
// 默认头像
|
|
const defaultAvatar = '/images/default-avatar.png'
|
|
|
|
export default {
|
|
name: 'AgentCard',
|
|
components: {
|
|
MoreFilled,
|
|
Edit,
|
|
CopyDocument,
|
|
Connection,
|
|
Download,
|
|
Delete,
|
|
Phone,
|
|
Monitor,
|
|
ChatDotRound,
|
|
View,
|
|
CloseBold,
|
|
Loading,
|
|
User,
|
|
Briefcase,
|
|
MagicStick,
|
|
Reading,
|
|
},
|
|
props: {
|
|
agent: {
|
|
type: Object,
|
|
required: true,
|
|
validator: (agent) => {
|
|
return agent && typeof agent.id === 'string' && typeof agent.name === 'string'
|
|
}
|
|
},
|
|
isProcessing: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
},
|
|
emits: [
|
|
'edit',
|
|
'duplicate',
|
|
'delete',
|
|
'bind-device',
|
|
'unbind-device',
|
|
'view-details',
|
|
'export'
|
|
],
|
|
setup(props, { emit, expose }) {
|
|
const { t } = useI18n()
|
|
const cardRef = ref(null)
|
|
const isHovered = ref(false)
|
|
const error = ref(null)
|
|
|
|
// 绑定状态
|
|
const bindingState = ref({
|
|
loading: false,
|
|
error: null,
|
|
verifying: false
|
|
})
|
|
|
|
// ==================== 计算属性 ====================
|
|
|
|
const truncatedDescription = computed(() => {
|
|
if (!props.agent.description) return ''
|
|
const maxLength = 80
|
|
if (props.agent.description.length <= maxLength) {
|
|
return props.agent.description
|
|
}
|
|
return props.agent.description.substring(0, maxLength) + '...'
|
|
})
|
|
|
|
const roleType = computed(() => {
|
|
const typeMap = {
|
|
[AGENT_ROLES.ASSISTANT]: 'success',
|
|
[AGENT_ROLES.CONSULTANT]: 'warning',
|
|
[AGENT_ROLES.CREATIVE]: 'info',
|
|
[AGENT_ROLES.EDUCATOR]: 'primary',
|
|
[AGENT_ROLES.CUSTOM]: 'default'
|
|
}
|
|
return typeMap[props.agent.role] || 'default'
|
|
})
|
|
|
|
const roleLabel = computed(() => {
|
|
const labelMap = {
|
|
[AGENT_ROLES.ASSISTANT]: t('agents.card.role.assistant'),
|
|
[AGENT_ROLES.CONSULTANT]: t('agents.card.role.consultant'),
|
|
[AGENT_ROLES.CREATIVE]: t('agents.card.role.creative'),
|
|
[AGENT_ROLES.EDUCATOR]: t('agents.card.role.educator'),
|
|
[AGENT_ROLES.CUSTOM]: t('agents.card.role.custom')
|
|
}
|
|
return labelMap[props.agent.role] || props.agent.role
|
|
})
|
|
|
|
const roleIcon = computed(() => {
|
|
const iconMap = {
|
|
[AGENT_ROLES.ASSISTANT]: 'User',
|
|
[AGENT_ROLES.CONSULTANT]: 'Briefcase',
|
|
[AGENT_ROLES.CREATIVE]: 'MagicStick',
|
|
[AGENT_ROLES.EDUCATOR]: 'Reading',
|
|
[AGENT_ROLES.CUSTOM]: 'Setting'
|
|
}
|
|
return iconMap[props.agent.role]
|
|
})
|
|
|
|
const lastActivity = computed(() => {
|
|
if (!props.agent.lastActive) return null
|
|
|
|
// 模拟最新对话数据(实际中可能从其他地方获取)
|
|
const sampleMessages = [
|
|
t('agents.card.activity.messages.greeting'),
|
|
t('agents.card.activity.messages.help'),
|
|
t('agents.card.activity.messages.conversation'),
|
|
t('agents.card.activity.messages.task')
|
|
]
|
|
|
|
return {
|
|
message: sampleMessages[Math.floor(Math.random() * sampleMessages.length)],
|
|
timestamp: props.agent.lastActive
|
|
}
|
|
})
|
|
|
|
// ==================== 事件处理 ====================
|
|
|
|
const handleMouseEnter = () => {
|
|
isHovered.value = true
|
|
}
|
|
|
|
const handleMouseLeave = () => {
|
|
isHovered.value = false
|
|
}
|
|
|
|
const handleMenuCommand = (command) => {
|
|
error.value = null
|
|
|
|
switch (command) {
|
|
case 'edit':
|
|
emit('edit', props.agent)
|
|
break
|
|
case 'duplicate':
|
|
emit('duplicate', props.agent)
|
|
break
|
|
case 'delete':
|
|
emit('delete', props.agent)
|
|
break
|
|
case 'device':
|
|
if (props.agent.device.bound) {
|
|
emit('view-details', props.agent)
|
|
} else {
|
|
emit('bind-device', props.agent)
|
|
}
|
|
break
|
|
case 'export':
|
|
emit('export', props.agent)
|
|
break
|
|
default:
|
|
console.warn('Unknown menu command:', command)
|
|
}
|
|
}
|
|
|
|
const handleDeviceMenuCommand = (command) => {
|
|
error.value = null
|
|
|
|
switch (command) {
|
|
case 'view-device':
|
|
emit('view-details', props.agent)
|
|
break
|
|
case 'unbind-device':
|
|
emit('unbind-device', props.agent)
|
|
break
|
|
default:
|
|
console.warn('Unknown device menu command:', command)
|
|
}
|
|
}
|
|
|
|
const handleBindDevice = () => {
|
|
emit('bind-device', props.agent)
|
|
}
|
|
|
|
// ==================== 工具函数 ====================
|
|
|
|
const handleImageError = (event) => {
|
|
event.target.src = defaultAvatar
|
|
}
|
|
|
|
const formatRelativeTime = (timestamp) => {
|
|
const now = new Date()
|
|
const time = new Date(timestamp)
|
|
const diffMs = now - time
|
|
const diffMins = Math.floor(diffMs / (1000 * 60))
|
|
const diffHours = Math.floor(diffMins / 60)
|
|
const diffDays = Math.floor(diffHours / 24)
|
|
|
|
if (diffMins < 1) return t('agents.card.time.justNow')
|
|
if (diffMins < 60) return `${diffMins}${t('agents.card.time.minutesAgo')}`
|
|
if (diffHours < 24) return `${diffHours}${t('agents.card.time.hoursAgo')}`
|
|
if (diffDays < 7) return `${diffDays}${t('agents.card.time.daysAgo')}`
|
|
|
|
return time.toLocaleDateString()
|
|
}
|
|
|
|
const formatNumber = (num) => {
|
|
if (num >= 1000) {
|
|
return `${(num / 1000).toFixed(1)}k`
|
|
}
|
|
return num.toString()
|
|
}
|
|
|
|
// ==================== 暴露方法 ====================
|
|
|
|
const updateBindingState = (newState) => {
|
|
bindingState.value = { ...bindingState.value, ...newState }
|
|
}
|
|
|
|
const setError = (errorMessage) => {
|
|
error.value = errorMessage
|
|
}
|
|
|
|
const clearError = () => {
|
|
error.value = null
|
|
}
|
|
|
|
expose({
|
|
updateBindingState,
|
|
setError,
|
|
clearError
|
|
})
|
|
|
|
return {
|
|
// 常量
|
|
AGENT_STATUS,
|
|
defaultAvatar,
|
|
|
|
// 响应式数据
|
|
cardRef,
|
|
isHovered,
|
|
error,
|
|
bindingState,
|
|
|
|
// 计算属性
|
|
truncatedDescription,
|
|
roleType,
|
|
roleLabel,
|
|
roleIcon,
|
|
lastActivity,
|
|
|
|
// 方法
|
|
handleMouseEnter,
|
|
handleMouseLeave,
|
|
handleMenuCommand,
|
|
handleDeviceMenuCommand,
|
|
handleBindDevice,
|
|
handleImageError,
|
|
formatRelativeTime,
|
|
formatNumber
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* 基础样式 */
|
|
.agent-card {
|
|
@apply relative bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden;
|
|
@apply transition-all duration-300 ease-in-out;
|
|
min-height: 280px;
|
|
}
|
|
|
|
.agent-card:hover {
|
|
@apply shadow-md border-gray-300;
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.agent-card--hovered {
|
|
@apply border-blue-300;
|
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.1);
|
|
}
|
|
|
|
/* 卡片头部 */
|
|
.agent-card__header {
|
|
@apply flex items-start justify-between p-4 border-b border-gray-100;
|
|
}
|
|
|
|
.agent-avatar-section {
|
|
@apply flex items-center space-x-3;
|
|
}
|
|
|
|
.agent-avatar {
|
|
@apply relative;
|
|
}
|
|
|
|
.avatar-image {
|
|
@apply w-12 h-12 rounded-full object-cover;
|
|
@apply border-2 border-gray-200;
|
|
}
|
|
|
|
.status-indicator {
|
|
@apply absolute -bottom-1 -right-1 w-4 h-4 rounded-full border-2 border-white;
|
|
@apply bg-gray-400;
|
|
}
|
|
|
|
.status-indicator--online {
|
|
@apply bg-green-500;
|
|
}
|
|
|
|
.agent-basic-info {
|
|
@apply flex-1 min-w-0;
|
|
}
|
|
|
|
.agent-name {
|
|
@apply text-lg font-semibold text-gray-900 mb-1;
|
|
@apply truncate;
|
|
}
|
|
|
|
.agent-role {
|
|
@apply flex items-center;
|
|
}
|
|
|
|
.role-icon {
|
|
@apply mr-1;
|
|
}
|
|
|
|
.agent-menu {
|
|
@apply flex-shrink-0;
|
|
}
|
|
|
|
/* 描述区域 */
|
|
.agent-card__description {
|
|
@apply p-4 border-b border-gray-100;
|
|
}
|
|
|
|
.description-text {
|
|
@apply text-sm text-gray-600 leading-relaxed;
|
|
@apply line-clamp-2;
|
|
}
|
|
|
|
/* 设备绑定状态 */
|
|
.device-binding-section {
|
|
@apply p-4 border-b border-gray-100;
|
|
}
|
|
|
|
.device-status {
|
|
@apply flex items-center space-x-3;
|
|
}
|
|
|
|
.device-icon {
|
|
@apply text-gray-400;
|
|
@apply transition-colors duration-200;
|
|
}
|
|
|
|
.device-icon--bound {
|
|
@apply text-green-500;
|
|
}
|
|
|
|
.device-info {
|
|
@apply flex-1 min-w-0;
|
|
}
|
|
|
|
.device-status-text {
|
|
@apply text-sm font-medium text-gray-900;
|
|
}
|
|
|
|
.device-name {
|
|
@apply text-xs text-gray-500 block;
|
|
@apply truncate;
|
|
}
|
|
|
|
/* 活动预览 */
|
|
.activity-preview {
|
|
@apply p-4 border-b border-gray-100;
|
|
}
|
|
|
|
.activity-header {
|
|
@apply flex items-center space-x-2 mb-2;
|
|
}
|
|
|
|
.activity-icon {
|
|
@apply text-blue-500 text-sm;
|
|
}
|
|
|
|
.activity-title {
|
|
@apply text-xs font-medium text-gray-500 uppercase tracking-wide;
|
|
}
|
|
|
|
.activity-content {
|
|
@apply flex items-center justify-between;
|
|
}
|
|
|
|
.activity-text {
|
|
@apply text-sm text-gray-600;
|
|
@apply truncate flex-1 mr-2;
|
|
}
|
|
|
|
.activity-time {
|
|
@apply text-xs text-gray-400 flex-shrink-0;
|
|
}
|
|
|
|
/* 统计信息 */
|
|
.stats-section {
|
|
@apply p-4;
|
|
}
|
|
|
|
.stats-grid {
|
|
@apply grid grid-cols-3 gap-4;
|
|
}
|
|
|
|
.stat-item {
|
|
@apply text-center;
|
|
}
|
|
|
|
.stat-value {
|
|
@apply block text-sm font-semibold text-gray-900;
|
|
}
|
|
|
|
.stat-label {
|
|
@apply block text-xs text-gray-500 mt-1;
|
|
}
|
|
|
|
/* 错误状态 */
|
|
.error-section {
|
|
@apply p-2;
|
|
}
|
|
|
|
.error-alert {
|
|
@apply mb-0;
|
|
}
|
|
|
|
/* 加载状态 */
|
|
.loading-overlay {
|
|
@apply absolute inset-0 bg-white bg-opacity-80 flex flex-col items-center justify-center;
|
|
@apply space-y-2;
|
|
}
|
|
|
|
.loading-icon {
|
|
@apply text-blue-500 text-lg animate-spin;
|
|
}
|
|
|
|
.loading-text {
|
|
@apply text-sm text-gray-600;
|
|
}
|
|
|
|
/* 文本截断 */
|
|
.line-clamp-2 {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* 危险操作样式 */
|
|
.text-danger {
|
|
@apply text-red-600;
|
|
}
|
|
|
|
/* 响应式设计 */
|
|
@media (max-width: 768px) {
|
|
.agent-card__header {
|
|
@apply p-3;
|
|
}
|
|
|
|
.agent-avatar-section {
|
|
@apply space-x-2;
|
|
}
|
|
|
|
.avatar-image {
|
|
@apply w-10 h-10;
|
|
}
|
|
|
|
.agent-name {
|
|
@apply text-base;
|
|
}
|
|
|
|
.device-binding-section,
|
|
.activity-preview,
|
|
.stats-section {
|
|
@apply p-3;
|
|
}
|
|
|
|
.description-text {
|
|
@apply text-xs;
|
|
}
|
|
|
|
.stats-grid {
|
|
@apply gap-3;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.agent-card {
|
|
@apply rounded-md;
|
|
}
|
|
|
|
.stats-grid {
|
|
@apply grid-cols-2;
|
|
}
|
|
|
|
.device-status {
|
|
@apply flex-col items-start space-y-2;
|
|
}
|
|
}
|
|
|
|
/* 深色模式支持 */
|
|
@media (prefers-color-scheme: dark) {
|
|
.agent-card {
|
|
@apply bg-gray-800 border-gray-700;
|
|
}
|
|
|
|
.agent-card:hover {
|
|
@apply border-gray-600;
|
|
}
|
|
|
|
.agent-card__header {
|
|
@apply border-gray-700;
|
|
}
|
|
|
|
.agent-name {
|
|
@apply text-gray-100;
|
|
}
|
|
|
|
.description-text,
|
|
.activity-text,
|
|
.device-status-text {
|
|
@apply text-gray-300;
|
|
}
|
|
|
|
.stat-value {
|
|
@apply text-gray-100;
|
|
}
|
|
|
|
.activity-icon {
|
|
@apply text-blue-400;
|
|
}
|
|
|
|
.loading-overlay {
|
|
@apply bg-gray-800 bg-opacity-80;
|
|
}
|
|
|
|
.loading-text {
|
|
@apply text-gray-300;
|
|
}
|
|
}
|
|
</style> |