浏览代码

右键菜单

kagg886 1 月之前
父节点
当前提交
c4f393d459
共有 3 个文件被更改,包括 175 次插入130 次删除
  1. 145 125
      src/components/assistant/DraggableCard.vue
  2. 2 2
      src/components/assistant/types.ts
  3. 28 3
      src/views/assistant/dashboard/edit.vue

+ 145 - 125
src/components/assistant/DraggableCard.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import { ref, computed, onUnmounted } from 'vue'
-import { Delete, MoreFilled, Edit } from '@element-plus/icons-vue'
+import { Delete, Edit } from '@element-plus/icons-vue'
 import Markdown from '/@/components/markdown/Markdown.vue'
 import type { MarkdownDashBoard, Position, Size, Content, ResizeType } from './types'
 import { MarkdownPlugin } from '/@/components/markdown/type/markdown'
@@ -36,6 +36,10 @@ const showEditDialog = ref(false)
 const editTitle = ref('')
 const editData = ref('')
 
+// 右键菜单相关状态
+const showContextMenu = ref(false)
+const contextMenuPosition = ref({ x: 0, y: 0 })
+
 // 计算卡片样式
 const cardStyle = computed(() => ({
 	position: 'absolute' as const,
@@ -174,6 +178,36 @@ const cancelEdit = () => {
 // 删除卡片
 const handleRemove = () => {
 	emit('remove', props.card.id)
+	showContextMenu.value = false
+}
+
+// 处理右键菜单
+const handleContextMenu = (event: MouseEvent) => {
+	event.preventDefault()
+	event.stopPropagation()
+
+	contextMenuPosition.value = {
+		x: event.clientX,
+		y: event.clientY
+	}
+	showContextMenu.value = true
+
+	// 点击其他地方关闭菜单
+	const closeMenu = () => {
+		showContextMenu.value = false
+		document.removeEventListener('click', closeMenu)
+	}
+
+	// 延迟添加事件监听器,避免立即触发
+	setTimeout(() => {
+		document.addEventListener('click', closeMenu)
+	}, 0)
+}
+
+// 处理右键菜单编辑
+const handleContextEdit = () => {
+	handleEdit()
+	showContextMenu.value = false
 }
 
 // 清理事件监听器
@@ -190,38 +224,13 @@ onUnmounted(() => {
 		ref="cardRef"
 		:style="cardStyle"
 		:class="['draggable-card', { 'is-dragging': isDragging, 'is-resizing': isResizing }]"
+		@contextmenu="handleContextMenu"
 	>
-		<el-card class="card-content" shadow="hover">
+		<el-card class="card-content" shadow="hover" @mousedown="startDrag">
 			<!-- 卡片标题栏 - 可拖拽区域 -->
-			<template #header>
-				<div
-					class="card-header"
-					@mousedown="startDrag"
-				>
+			<template #header v-if="card.title !== undefined">
+				<div class="card-header">
 					<span class="card-title">{{ card.title }}</span>
-					<div class="card-actions">
-						<el-dropdown trigger="click" placement="bottom-end">
-							<el-button
-								type="text"
-								:icon="MoreFilled"
-								size="small"
-								class="action-btn"
-								@click.stop
-							/>
-							<template #dropdown>
-								<el-dropdown-menu>
-									<el-dropdown-item @click="handleEdit">
-										<el-icon><Edit /></el-icon>
-										<span>编辑</span>
-									</el-dropdown-item>
-									<el-dropdown-item @click="handleRemove">
-										<el-icon><Delete /></el-icon>
-										<span>删除</span>
-									</el-dropdown-item>
-								</el-dropdown-menu>
-							</template>
-						</el-dropdown>
-					</div>
 				</div>
 			</template>
 
@@ -256,62 +265,83 @@ onUnmounted(() => {
 			></div>
 		</div>
 
-		<!-- 编辑对话框 -->
-		<el-dialog
-			v-model="showEditDialog"
-			title="编辑卡片内容"
-			width="600px"
-			:before-close="cancelEdit"
-			append-to-body
+	<!-- 右键菜单 -->
+	<Teleport to="body">
+		<div
+			v-if="showContextMenu"
+			class="context-menu"
+			:style="{
+				left: contextMenuPosition.x + 'px',
+				top: contextMenuPosition.y + 'px'
+			}"
 		>
-			<div class="edit-form">
-				<div class="form-item">
-					<label class="form-label">卡片标题</label>
-					<el-input
-						v-model="editTitle"
-						placeholder="请输入卡片标题"
-						maxlength="50"
-						show-word-limit
-					/>
-				</div>
+			<div class="context-menu-item" @click="handleContextEdit">
+				<el-icon><Edit /></el-icon>
+				<span>编辑</span>
+			</div>
+			<div class="context-menu-item context-menu-item-danger" @click="handleRemove">
+				<el-icon><Delete /></el-icon>
+				<span>删除</span>
+			</div>
+		</div>
+	</Teleport>
+
+	<!-- 编辑对话框 -->
+	<el-dialog
+		v-model="showEditDialog"
+		title="编辑卡片内容"
+		width="600px"
+		:before-close="cancelEdit"
+		append-to-body
+	>
+		<div class="edit-form">
+			<div class="form-item">
+				<label class="form-label">卡片标题</label>
+				<el-input
+					v-model="editTitle"
+					placeholder="请输入卡片标题"
+					maxlength="50"
+					show-word-limit
+				/>
+			</div>
 
-				<div class="form-item">
-					<label class="form-label">卡片内容 (支持Markdown)</label>
-					<el-input
-						v-model="editData"
-						type="textarea"
-						placeholder="请输入卡片内容,支持Markdown语法"
-						:rows="8"
-						resize="vertical"
-					/>
-				</div>
+			<div class="form-item">
+				<label class="form-label">卡片内容 (支持Markdown)</label>
+				<el-input
+					v-model="editData"
+					type="textarea"
+					placeholder="请输入卡片内容,支持Markdown语法"
+					:rows="8"
+					resize="vertical"
+				/>
+			</div>
 
-				<div class="preview-section">
-					<label class="form-label">预览效果</label>
-					<div class="preview-container">
-						<Markdown
-							:content="editData || '暂无内容'"
-							:plugins="plugin"
-							class="preview-content"
-						/>
-					</div>
+			<div class="preview-section">
+				<label class="form-label">预览效果</label>
+				<div class="preview-container">
+					<Markdown
+						:content="editData || '暂无内容'"
+						:plugins="plugin"
+						class="preview-content"
+					/>
 				</div>
 			</div>
+		</div>
 
-			<template #footer>
-				<div class="dialog-footer">
-					<el-button @click="cancelEdit">取消</el-button>
-					<el-button
-						type="primary"
-						@click="confirmEdit"
-						:disabled="!editTitle.trim() || !editData.trim()"
-					>
-						确定
-					</el-button>
-				</div>
-			</template>
-		</el-dialog>
-	</div>
+		<template #footer>
+			<div class="dialog-footer">
+				<el-button @click="cancelEdit">取消</el-button>
+				<el-button
+					type="primary"
+					@click="confirmEdit"
+					:disabled="!editTitle.trim() || !editData.trim()"
+				>
+					确定
+				</el-button>
+			</div>
+		</template>
+	</el-dialog>
+</div>
 </template>
 
 <style scoped lang="scss">
@@ -376,30 +406,6 @@ onUnmounted(() => {
 	font-size: 14px;
 }
 
-.card-actions {
-	display: flex;
-	align-items: center;
-	gap: 4px;
-	opacity: 0;
-	transition: opacity 0.2s ease;
-}
-
-.card-header:hover .card-actions {
-	opacity: 1;
-}
-
-.action-btn {
-	width: 24px !important;
-	height: 24px !important;
-	padding: 0 !important;
-	color: var(--el-text-color-regular);
-
-	&:hover {
-		color: var(--el-color-primary);
-		background: var(--el-color-primary-light-9) !important;
-	}
-}
-
 .card-body {
 	height: 100%;
 	overflow: auto;
@@ -546,33 +552,47 @@ onUnmounted(() => {
 	text-align: right;
 }
 
-/* 下拉菜单样式 */
-:deep(.el-dropdown-menu) {
-	.el-dropdown-menu__item {
-		display: flex;
-		align-items: center;
-		gap: 8px;
-		padding: 8px 16px;
+/* 右键菜单样式 */
+.context-menu {
+	position: fixed;
+	z-index: 9999;
+	background: var(--el-bg-color-overlay);
+	border: 1px solid var(--el-border-color-light);
+	border-radius: 6px;
+	box-shadow: var(--el-box-shadow-light);
+	padding: 4px 0;
+	min-width: 120px;
+	backdrop-filter: blur(12px);
+}
 
-		.el-icon {
-			font-size: 14px;
-			color: var(--el-text-color-regular);
-		}
+.context-menu-item {
+	display: flex;
+	align-items: center;
+	gap: 8px;
+	padding: 8px 16px;
+	cursor: pointer;
+	transition: all 0.2s ease;
+	font-size: 14px;
+	color: var(--el-text-color-primary);
 
-		span {
-			font-size: 14px;
-			color: var(--el-text-color-primary);
-		}
+	.el-icon {
+		font-size: 14px;
+		color: var(--el-text-color-regular);
+	}
 
-		&:hover {
-			background: var(--el-fill-color-light);
+	&:hover {
+		background: var(--el-fill-color-light);
 
-			.el-icon {
-				color: var(--el-color-primary);
-			}
+		.el-icon {
+			color: var(--el-color-primary);
 		}
+	}
+
+	&.context-menu-item-danger {
+		&:hover {
+			background: var(--el-color-danger-light-9);
+			color: var(--el-color-danger);
 
-		&:hover:has(.el-icon:contains('Delete')) {
 			.el-icon {
 				color: var(--el-color-danger);
 			}

+ 2 - 2
src/components/assistant/types.ts

@@ -17,7 +17,7 @@ export interface MarkdownDashBoard {
 	/** 层级 (z-index) */
 	z: number
 	/** 卡片标题 */
-	title: string
+	title?: string
 	/** 卡片内容 (Markdown格式) */
 	data: string
 }
@@ -45,7 +45,7 @@ export interface ComponentLibraryItem {
 	/** 组件唯一标识 */
 	id: string
 	/** 组件标题 */
-	title: string
+	title?: string
 	/** 组件图标 */
 	icon: any
 	/** 组件描述 */

+ 28 - 3
src/views/assistant/dashboard/edit.vue

@@ -104,7 +104,22 @@ const currentSelectedChat = ref<number>()
 
 watch(currentSelectedChat, (newVal) => {
 	if (newVal === undefined) {
-		library.value = []
+		//	/** 组件唯一标识 */
+		// id: string
+		// /** 组件标题 */
+		// title: string
+		// /** 组件图标 */
+		// icon: any
+		// /** 组件描述 */
+		// description: string
+		// /** 组件完整数据 */
+		// data: string
+		// /** 组件预览数据 */
+		// preview: string
+		library.value = defaultLibrary
+		return
+	}
+	if (chat.value[newVal]?.session_id === undefined) {
 		return
 	}
 	doLoadingLibrary(chat.value[newVal].session_id)
@@ -126,8 +141,18 @@ const { loading: loadingChat, doLoading: doLoadingChat } = useLoading(async () =
 	chat.value = data.list
 })
 
+const defaultLibrary: ComponentLibraryItem[] = [
+	{
+		id: Math.random().toString(),
+		icon: 'ele-PieChart',
+		description: '测试',
+		data: '测试',
+		preview: '测试',
+	},
+]
+
 // 组件库数据
-const library = ref<ComponentLibraryItem[]>([])
+const library = ref<ComponentLibraryItem[]>(defaultLibrary)
 
 const { loading: loadingLibrary, doLoading: doLoadingLibrary } = useLoading(async (id: number) => {
 	const result: {
@@ -391,7 +416,7 @@ onMounted( () => {
 
 			<!-- 右侧组件库面板 -->
 			<div class="library-panel">
-				<el-select class="library-panel-header" v-model="currentSelectedChat" v-loading="loadingChat">
+				<el-select class="library-panel-header" v-model="currentSelectedChat" v-loading="loadingChat" clearable @clear="currentSelectedChat = undefined">
 					<el-option v-for="(i, index) in chat" :key="i.session_id" :value="index" :label="i.title"></el-option>
 				</el-select>
 				<ComponentLibrary class="library-panel-content" v-loading="loadingLibrary" :library="library" @add-card="addCard" />