feat:1.历史记录重构UI重构2.置顶功能实现
This commit is contained in:
@@ -66,10 +66,10 @@ class MultiAgentTaskCRUD:
|
||||
def get_recent(
|
||||
db: Session, limit: int = 20, offset: int = 0
|
||||
) -> List[MultiAgentTask]:
|
||||
"""获取最近的任务记录"""
|
||||
"""获取最近的任务记录,置顶的排在最前面"""
|
||||
return (
|
||||
db.query(MultiAgentTask)
|
||||
.order_by(MultiAgentTask.created_at.desc())
|
||||
.order_by(MultiAgentTask.is_pinned.desc(), MultiAgentTask.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.all()
|
||||
@@ -184,6 +184,18 @@ class MultiAgentTaskCRUD:
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def update_is_pinned(
|
||||
db: Session, task_id: str, is_pinned: bool
|
||||
) -> Optional[MultiAgentTask]:
|
||||
"""更新任务置顶状态"""
|
||||
task = db.query(MultiAgentTask).filter(MultiAgentTask.task_id == task_id).first()
|
||||
if task:
|
||||
task.is_pinned = is_pinned
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def append_rehearsal_log(
|
||||
db: Session, task_id: str, log_entry: dict
|
||||
|
||||
@@ -6,7 +6,7 @@ SQLAlchemy ORM 数据模型
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum as PyEnum
|
||||
from sqlalchemy import Column, String, Text, DateTime, Integer, Enum, Index, ForeignKey
|
||||
from sqlalchemy import Column, String, Text, DateTime, Integer, Enum, Index, ForeignKey, Boolean
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
@@ -48,6 +48,7 @@ class MultiAgentTask(Base):
|
||||
execution_id = Column(String(64))
|
||||
rehearsal_log = Column(JSONB)
|
||||
branches = Column(JSONB) # 任务大纲探索分支数据
|
||||
is_pinned = Column(Boolean, default=False, nullable=False) # 置顶标志
|
||||
created_at = Column(DateTime(timezone=True), default=utc_now)
|
||||
updated_at = Column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
|
||||
|
||||
@@ -74,6 +75,7 @@ class MultiAgentTask(Base):
|
||||
"execution_id": self.execution_id,
|
||||
"rehearsal_log": self.rehearsal_log,
|
||||
"branches": self.branches,
|
||||
"is_pinned": self.is_pinned,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
|
||||
@@ -331,10 +331,11 @@ def handle_execute_plan_optimized_ws(data):
|
||||
MultiAgentTaskCRUD.update_status(db, task_id, TaskStatus.STOPPED)
|
||||
print(f"[execute_plan_optimized] 用户停止执行,跳过保存执行数据,已完成 {completed_steps_count}/{plan_steps_count} 步骤,task_id={task_id}")
|
||||
|
||||
# 任务大纲(用户可能编辑了)仍然保存
|
||||
if plan:
|
||||
MultiAgentTaskCRUD.update_task_outline(db, task_id, plan)
|
||||
print(f"[execute_plan_optimized] 已保存 task_outline 到数据库,task_id={task_id}")
|
||||
# # 任务大纲(用户可能编辑了)仍然保存
|
||||
# # 注释原因:执行任务时不保存 task_outline,避免覆盖导致步骤 ID 变化与 agent_scores 不匹配
|
||||
# if plan:
|
||||
# MultiAgentTaskCRUD.update_task_outline(db, task_id, plan)
|
||||
# print(f"[execute_plan_optimized] 已保存 task_outline 到数据库,task_id={task_id}")
|
||||
|
||||
# # 保存 assigned_agents(每个步骤使用的 agent)
|
||||
# # 注释原因:assigned_agents 只在生成阶段由用户手动选择写入,执行时不覆盖
|
||||
@@ -1849,6 +1850,7 @@ def handle_get_plans(data):
|
||||
"status": task.status.value if task.status else 'unknown',
|
||||
"execution_count": task.execution_count or 0,
|
||||
"created_at": task.created_at.isoformat() if task.created_at else None,
|
||||
"is_pinned": task.is_pinned or False, # 置顶标志
|
||||
# 完整数据用于恢复
|
||||
"task_outline": task.task_outline,
|
||||
"assigned_agents": task.assigned_agents,
|
||||
@@ -2060,6 +2062,54 @@ def handle_delete_plan(data):
|
||||
})
|
||||
|
||||
|
||||
@socketio.on('pin_plan')
|
||||
def handle_pin_plan(data):
|
||||
"""
|
||||
WebSocket版本:置顶/取消置顶历史任务
|
||||
"""
|
||||
# socketio 包装: data = { id: 'pin_plan-xxx', action: 'pin_plan', data: { id: 'ws_req_xxx', data: {...} } }
|
||||
request_id = data.get('id') # socketio 包装的 id
|
||||
incoming_data = data.get('data', {}).get('data', {}) # 真正的请求数据
|
||||
plan_id = incoming_data.get('plan_id')
|
||||
is_pinned = incoming_data.get('is_pinned', True) # 默认为置顶
|
||||
|
||||
if not plan_id:
|
||||
emit('response', {
|
||||
'id': request_id,
|
||||
'status': 'error',
|
||||
'error': '缺少 plan_id(task_id)'
|
||||
})
|
||||
return
|
||||
|
||||
try:
|
||||
with get_db_context() as db:
|
||||
task = MultiAgentTaskCRUD.update_is_pinned(db, plan_id, is_pinned)
|
||||
|
||||
if not task:
|
||||
emit('response', {
|
||||
'id': request_id,
|
||||
'status': 'error',
|
||||
'error': f'任务不存在: {plan_id}'
|
||||
})
|
||||
return
|
||||
|
||||
# 通知所有客户端刷新历史列表
|
||||
socketio.emit('history_updated', {'task_id': plan_id})
|
||||
|
||||
emit('response', {
|
||||
'id': request_id,
|
||||
'status': 'success',
|
||||
'data': {"message": "置顶成功" if is_pinned else "取消置顶成功"}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
emit('response', {
|
||||
'id': request_id,
|
||||
'status': 'error',
|
||||
'error': str(e)
|
||||
})
|
||||
|
||||
|
||||
@socketio.on('save_branches')
|
||||
def handle_save_branches(data):
|
||||
"""
|
||||
|
||||
157
frontend/src/components/DeleteConfirmDialog/index.vue
Normal file
157
frontend/src/components/DeleteConfirmDialog/index.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:width="width"
|
||||
:modal="false"
|
||||
:close-on-click-modal="false"
|
||||
:center="true"
|
||||
:show-close="false"
|
||||
top="20vh"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<template #header>
|
||||
<div class="dialog-header">
|
||||
<SvgIcon icon-class="JingGao" size="20px" color="#ff6712" />
|
||||
<span>{{ title }}</span>
|
||||
<button class="dialog-close-btn" @click="handleCancel">
|
||||
<SvgIcon icon-class="close" size="18px" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<slot>
|
||||
<span>{{ content }}</span>
|
||||
</slot>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleCancel">{{ cancelText }}</el-button>
|
||||
<el-button class="confirm-btn" type="danger" :loading="loading" @click="handleConfirm">
|
||||
{{ confirmText }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
title?: string
|
||||
content?: string
|
||||
width?: string | number
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '确认删除',
|
||||
content: '删除后,该操作无法恢复!',
|
||||
width: '400px',
|
||||
confirmText: '删除',
|
||||
cancelText: '取消'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'confirm'): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(props.modelValue)
|
||||
|
||||
// 监听 v-model 变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
val => {
|
||||
dialogVisible.value = val
|
||||
}
|
||||
)
|
||||
|
||||
// 监听对话框显示状态变化
|
||||
watch(dialogVisible, val => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 确认删除
|
||||
const handleConfirm = async () => {
|
||||
loading.value = true
|
||||
emit('confirm')
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('cancel')
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
// 对话框关闭后重置 loading 状态
|
||||
const handleClosed = () => {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 暴露方法供外部调用
|
||||
defineExpose({
|
||||
setLoading: (val: boolean) => {
|
||||
loading.value = val
|
||||
},
|
||||
close: () => {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
|
||||
.dialog-close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
margin-left: auto;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
|
||||
.el-button:not(.confirm-btn) {
|
||||
background: #ffffff;
|
||||
border-color: #dcdfe6;
|
||||
color: #000000;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #c0c0c0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
--el-button-bg-color: #ff6712;
|
||||
--el-button-border-color: #ff6712;
|
||||
--el-button-hover-bg-color: #e55a0f;
|
||||
--el-button-hover-border-color: #e55a0f;
|
||||
}
|
||||
</style>
|
||||
@@ -18,7 +18,7 @@ const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg aria-hidden="true" class="svg-icon" :style="`color:${props.color}`">
|
||||
<svg aria-hidden="true" class="svg-icon" :style="props.color ? `color:${props.color}` : ''">
|
||||
<use :xlink:href="symbolId" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
<template>
|
||||
<div class="history-list">
|
||||
<div class="header">
|
||||
<h3>📋 历史任务</h3>
|
||||
<el-button type="primary" link @click="fetchPlans">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
@@ -24,66 +16,61 @@
|
||||
:key="plan.id"
|
||||
class="plan-item"
|
||||
:class="{ active: selectedPlanId === plan.id }"
|
||||
@click="selectPlan(plan)"
|
||||
@click="restorePlan(plan)"
|
||||
>
|
||||
<div class="plan-info">
|
||||
<div class="plan-goal">{{ plan.general_goal || '未知任务' }}</div>
|
||||
<div class="plan-meta">
|
||||
<el-tag size="small" :type="getStatusType(plan.status)">
|
||||
{{ getStatusText(plan.status) }}
|
||||
</el-tag>
|
||||
<span class="plan-time">{{ formatTime(plan.created_at) }}</span>
|
||||
</div>
|
||||
<div class="plan-stats">
|
||||
<span>执行次数: {{ plan.execution_count }}</span>
|
||||
<div class="plan-goal">
|
||||
<SvgIcon icon-class="XiaoXi" size="20px" />
|
||||
{{ plan.general_goal || '未知任务' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="plan-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click.stop="restorePlan(plan)"
|
||||
:disabled="restoring"
|
||||
<el-popover
|
||||
placement="bottom-end"
|
||||
:show-arrow="false"
|
||||
trigger="click"
|
||||
popper-class="action-popover"
|
||||
>
|
||||
恢复
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
link
|
||||
@click.stop="deletePlan(plan)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
<template #reference>
|
||||
<button class="more-btn" @click.stop>
|
||||
<SvgIcon icon-class="more" class="more-icon" size="16px" />
|
||||
</button>
|
||||
</template>
|
||||
<div class="action-menu">
|
||||
<div class="action-item" @click="pinPlan(plan)">
|
||||
<SvgIcon icon-class="ZhiDing" size="14px" />
|
||||
<span>{{ plan.is_pinned ? '取消置顶' : '置顶' }}</span>
|
||||
</div>
|
||||
<div class="action-item">
|
||||
<SvgIcon icon-class="FenXiang" size="14px" />
|
||||
<span>分享</span>
|
||||
</div>
|
||||
<div class="action-item" @click="deletePlan(plan)">
|
||||
<SvgIcon icon-class="ShanChu" size="14px" />
|
||||
<span>删除</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="plans.length > 0" class="pagination">
|
||||
<span>共 {{ total }} 个任务</span>
|
||||
</div>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<el-dialog
|
||||
<DeleteConfirmDialog
|
||||
v-model="dialogVisible"
|
||||
title="删除确认"
|
||||
width="400px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<span>确定要删除任务 "{{ planToDelete?.general_goal }}" 吗?此操作不可恢复。</span>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="danger" @click="confirmDelete" :loading="deleting">确定删除</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
title="确认删除该任务 ?"
|
||||
content="删除后,该任务无法恢复 !"
|
||||
@confirm="confirmDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Refresh, Loading, Delete } from '@element-plus/icons-vue'
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog/index.vue'
|
||||
import websocket from '@/utils/websocket'
|
||||
|
||||
// 事件定义
|
||||
@@ -94,12 +81,9 @@ const emit = defineEmits<{
|
||||
|
||||
// 数据类型
|
||||
interface PlanInfo {
|
||||
id: string // 对应数据库 task_id
|
||||
general_goal: string // 对应数据库 query
|
||||
status: string
|
||||
execution_count: number
|
||||
created_at: string
|
||||
// 完整恢复数据
|
||||
id: string
|
||||
general_goal: string
|
||||
is_pinned?: boolean
|
||||
task_outline?: any
|
||||
assigned_agents?: any
|
||||
agent_scores?: any
|
||||
@@ -111,8 +95,6 @@ const plans = ref<PlanInfo[]>([])
|
||||
const loading = ref(false)
|
||||
const restoring = ref(false)
|
||||
const selectedPlanId = ref<string | null>(null)
|
||||
const total = ref(0)
|
||||
const isConnected = ref(false)
|
||||
|
||||
// 删除对话框相关
|
||||
const dialogVisible = ref(false)
|
||||
@@ -137,22 +119,14 @@ const fetchPlans = async () => {
|
||||
try {
|
||||
const result = await websocket.send('get_plans', { id: reqId })
|
||||
plans.value = (result.data || []) as PlanInfo[]
|
||||
total.value = plans.value.length
|
||||
isConnected.value = true
|
||||
} catch (error) {
|
||||
console.error('获取任务列表失败:', error)
|
||||
ElMessage.error('获取任务列表失败')
|
||||
isConnected.value = false
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 选择任务
|
||||
const selectPlan = (plan: PlanInfo) => {
|
||||
selectedPlanId.value = plan.id
|
||||
}
|
||||
|
||||
// 恢复任务
|
||||
const restorePlan = async (plan: PlanInfo) => {
|
||||
if (restoring.value) return
|
||||
@@ -192,6 +166,25 @@ const restorePlan = async (plan: PlanInfo) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 置顶/取消置顶任务
|
||||
const pinPlan = async (plan: PlanInfo) => {
|
||||
const newPinnedState = !plan.is_pinned
|
||||
const reqId = generateRequestId()
|
||||
|
||||
try {
|
||||
await websocket.send('pin_plan', {
|
||||
id: reqId,
|
||||
data: { plan_id: plan.id, is_pinned: newPinnedState }
|
||||
})
|
||||
|
||||
ElMessage.success(newPinnedState ? '置顶成功' : '取消置顶成功')
|
||||
// 成功后会自动通过 history_updated 事件刷新列表
|
||||
} catch (error) {
|
||||
console.error('置顶任务失败:', error)
|
||||
ElMessage.error('置顶失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
const deletePlan = (plan: PlanInfo) => {
|
||||
planToDelete.value = plan
|
||||
@@ -222,44 +215,6 @@ const confirmDelete = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr: string | undefined) => {
|
||||
if (!timeStr) return ''
|
||||
try {
|
||||
const date = new Date(timeStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
} catch {
|
||||
return timeStr
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status: string): string => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'generating': 'warning',
|
||||
'executing': 'warning',
|
||||
'completed': 'success',
|
||||
'stopped': 'danger'
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string): string => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'generating': '生成中',
|
||||
'executing': '执行中',
|
||||
'completed': '已完成',
|
||||
'stopped': '已停止'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
fetchPlans()
|
||||
@@ -281,19 +236,6 @@ onUnmounted(() => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -315,17 +257,21 @@ onUnmounted(() => {
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 8px;
|
||||
background: #f5f7fa;
|
||||
background: var(--color-bg-three);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #ecf5ff;
|
||||
background: var(--color-bg-hover);
|
||||
|
||||
.plan-goal {
|
||||
color: var(--color-text-plan-item-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #ecf5ff;
|
||||
border-left: 3px solid #409eff;
|
||||
background: var(--color-bg-hover);
|
||||
border-left: 3px solid var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,29 +281,17 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.plan-goal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.plan-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.plan-time {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.plan-stats {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
color: var(--color-text-plan-item);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.plan-actions {
|
||||
@@ -366,11 +300,83 @@ onUnmounted(() => {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
// 更多按钮
|
||||
.more-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 16px;
|
||||
color: #909399;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
.more-icon {
|
||||
color: var(--color-text-plan-item);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-card-border-three);
|
||||
border-radius: 50%;
|
||||
|
||||
.more-icon {
|
||||
color: var(--color-text-plan-item-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 操作菜单
|
||||
.action-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.el-popover__content) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-plan-item);
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
|
||||
.svg-icon {
|
||||
color: var(--color-text-plan-item);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-card-border-three);
|
||||
color: var(--color-text-plan-item-hover);
|
||||
|
||||
.svg-icon {
|
||||
color: var(--color-text-plan-item-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.action-popover {
|
||||
padding: 0 !important;
|
||||
border-radius: 8px !important;
|
||||
width: 120px !important;
|
||||
min-width: 120px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user