浏览代码

添加拖拽放大缩小功能

kagg886 1 月之前
父节点
当前提交
d9a3983fc3

+ 14 - 0
src/components/assistant/DashboardDesigner.vue

@@ -19,6 +19,8 @@ const props = defineProps<{
 
 const emit = defineEmits<{
 	updatePosition: [index: number, position: { x: number; y: number }]
+	updateSize: [index: number, size: { w: number; h: number }]
+	updateContent: [index: number, content: { title: string; data: string }]
 	removeCard: [index: number]
 	addCard: [cardData: { title: string; data: string; x: number; y: number }]
 }>()
@@ -30,6 +32,16 @@ const handleCardPositionUpdate = (index: number, position: { x: number; y: numbe
 	emit('updatePosition', index, position)
 }
 
+// 处理卡片大小更新
+const handleCardSizeUpdate = (index: number, size: { w: number; h: number }) => {
+	emit('updateSize', index, size)
+}
+
+// 处理卡片内容更新
+const handleCardContentUpdate = (index: number, content: { title: string; data: string }) => {
+	emit('updateContent', index, content)
+}
+
 // 处理卡片删除
 const handleCardRemove = (index: number) => {
 	emit('removeCard', index)
@@ -95,6 +107,8 @@ const handleDragOver = (event: DragEvent) => {
 				:card="card"
 				:index="index"
 				@update-position="handleCardPositionUpdate"
+				@update-size="handleCardSizeUpdate"
+				@update-content="handleCardContentUpdate"
 				@remove="handleCardRemove"
 			/>
 

+ 308 - 7
src/components/assistant/DraggableCard.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
-import { ref, computed, onMounted, onUnmounted } from 'vue'
-import { Delete, MoreFilled } from '@element-plus/icons-vue'
+import { ref, computed, onUnmounted } from 'vue'
+import { Delete, MoreFilled, Edit } from '@element-plus/icons-vue'
 import Markdown from '/@/components/markdown/Markdown.vue'
 
 type MarkdownDashBoard = {
@@ -20,12 +20,23 @@ const props = defineProps<{
 
 const emit = defineEmits<{
 	updatePosition: [index: number, position: { x: number; y: number }]
+	updateSize: [index: number, size: { w: number; h: number }]
+	updateContent: [index: number, content: { title: string; data: string }]
 	remove: [index: number]
 }>()
 
 const cardRef = ref<HTMLElement>()
 const isDragging = ref(false)
+const isResizing = ref(false)
 const dragOffset = ref({ x: 0, y: 0 })
+const resizeType = ref<'se' | 'e' | 's' | ''>('')
+const initialSize = ref({ w: 0, h: 0 })
+const initialPosition = ref({ x: 0, y: 0 })
+
+// 编辑相关状态
+const showEditDialog = ref(false)
+const editTitle = ref('')
+const editData = ref('')
 
 // 计算卡片样式
 const cardStyle = computed(() => ({
@@ -37,9 +48,9 @@ const cardStyle = computed(() => ({
 	zIndex: props.card.z + 100
 }))
 
-// 开始拖拽
+// 开始拖拽移动
 const startDrag = (event: MouseEvent) => {
-	if (!cardRef.value) return
+	if (!cardRef.value || isResizing.value) return
 
 	isDragging.value = true
 
@@ -60,7 +71,25 @@ const startDrag = (event: MouseEvent) => {
 	event.preventDefault()
 }
 
-// 处理拖拽
+// 开始拖拽调整大小
+const startResize = (event: MouseEvent, type: 'se' | 'e' | 's') => {
+	if (!cardRef.value) return
+
+	isResizing.value = true
+	resizeType.value = type
+
+	initialSize.value = { w: props.card.w, h: props.card.h }
+	initialPosition.value = { x: event.clientX, y: event.clientY }
+
+	document.addEventListener('mousemove', handleResize)
+	document.addEventListener('mouseup', stopResize)
+
+	// 防止文本选择和事件冒泡
+	event.preventDefault()
+	event.stopPropagation()
+}
+
+// 处理拖拽移动
 const handleDrag = (event: MouseEvent) => {
 	if (!isDragging.value || !cardRef.value) return
 
@@ -77,13 +106,73 @@ const handleDrag = (event: MouseEvent) => {
 	emit('updatePosition', props.index, { x: constrainedX, y: constrainedY })
 }
 
-// 停止拖拽
+// 处理拖拽调整大小
+const handleResize = (event: MouseEvent) => {
+	if (!isResizing.value || !cardRef.value) return
+
+	const parentRect = cardRef.value.parentElement?.getBoundingClientRect()
+	if (!parentRect) return
+
+	const deltaX = event.clientX - initialPosition.value.x
+	const deltaY = event.clientY - initialPosition.value.y
+
+	const deltaXPercent = (deltaX / parentRect.width) * 100
+	const deltaYPercent = (deltaY / parentRect.height) * 100
+
+	let newW = initialSize.value.w
+	let newH = initialSize.value.h
+
+	if (resizeType.value === 'se' || resizeType.value === 'e') {
+		newW = Math.max(15, Math.min(100 - props.card.x, initialSize.value.w + deltaXPercent))
+	}
+
+	if (resizeType.value === 'se' || resizeType.value === 's') {
+		newH = Math.max(10, Math.min(100 - props.card.y, initialSize.value.h + deltaYPercent))
+	}
+
+	emit('updateSize', props.index, { w: newW, h: newH })
+}
+
+// 停止拖拽移动
 const stopDrag = () => {
 	isDragging.value = false
 	document.removeEventListener('mousemove', handleDrag)
 	document.removeEventListener('mouseup', stopDrag)
 }
 
+// 停止拖拽调整大小
+const stopResize = () => {
+	isResizing.value = false
+	resizeType.value = ''
+	document.removeEventListener('mousemove', handleResize)
+	document.removeEventListener('mouseup', stopResize)
+}
+
+// 编辑卡片
+const handleEdit = () => {
+	editTitle.value = props.card.title
+	editData.value = props.card.data
+	showEditDialog.value = true
+}
+
+// 确认编辑
+const confirmEdit = () => {
+	if (editTitle.value.trim() && editData.value.trim()) {
+		emit('updateContent', props.index, {
+			title: editTitle.value.trim(),
+			data: editData.value.trim()
+		})
+		showEditDialog.value = false
+	}
+}
+
+// 取消编辑
+const cancelEdit = () => {
+	showEditDialog.value = false
+	editTitle.value = ''
+	editData.value = ''
+}
+
 // 删除卡片
 const handleRemove = () => {
 	emit('remove', props.index)
@@ -93,6 +182,8 @@ const handleRemove = () => {
 onUnmounted(() => {
 	document.removeEventListener('mousemove', handleDrag)
 	document.removeEventListener('mouseup', stopDrag)
+	document.removeEventListener('mousemove', handleResize)
+	document.removeEventListener('mouseup', stopResize)
 })
 </script>
 
@@ -100,7 +191,7 @@ onUnmounted(() => {
 	<div
 		ref="cardRef"
 		:style="cardStyle"
-		:class="['draggable-card', { 'is-dragging': isDragging }]"
+		:class="['draggable-card', { 'is-dragging': isDragging, 'is-resizing': isResizing }]"
 	>
 		<el-card class="card-content" shadow="hover">
 			<!-- 卡片标题栏 - 可拖拽区域 -->
@@ -121,6 +212,10 @@ onUnmounted(() => {
 							/>
 							<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>
@@ -141,6 +236,83 @@ onUnmounted(() => {
 				/>
 			</div>
 		</el-card>
+
+		<!-- 调整大小的拖拽手柄 -->
+		<div class="resize-handles">
+			<!-- 右边缘 -->
+			<div
+				class="resize-handle resize-handle-e"
+				@mousedown="startResize($event, 'e')"
+			></div>
+
+			<!-- 底边缘 -->
+			<div
+				class="resize-handle resize-handle-s"
+				@mousedown="startResize($event, 's')"
+			></div>
+
+			<!-- 右下角 -->
+			<div
+				class="resize-handle resize-handle-se"
+				@mousedown="startResize($event, 'se')"
+			></div>
+		</div>
+
+		<!-- 编辑对话框 -->
+		<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="preview-section">
+					<label class="form-label">预览效果</label>
+					<div class="preview-container">
+						<Markdown
+							:content="editData || '暂无内容'"
+							:plugins="[]"
+							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>
 
@@ -153,6 +325,18 @@ onUnmounted(() => {
 			box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
 		}
 	}
+
+	&.is-resizing {
+		.card-content {
+			box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+		}
+	}
+
+	&:hover {
+		.resize-handles {
+			opacity: 1;
+		}
+	}
 }
 
 .card-content {
@@ -253,6 +437,117 @@ onUnmounted(() => {
 	}
 }
 
+/* 调整大小手柄样式 */
+.resize-handles {
+	position: absolute;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	pointer-events: none;
+	opacity: 0;
+	transition: opacity 0.2s ease;
+}
+
+.resize-handle {
+	position: absolute;
+	pointer-events: all;
+	background: var(--el-color-primary);
+	transition: all 0.2s ease;
+
+	&:hover {
+		background: var(--el-color-primary-light-3);
+	}
+}
+
+.resize-handle-e {
+	top: 0;
+	right: -2px;
+	bottom: 0;
+	width: 4px;
+	cursor: e-resize;
+}
+
+.resize-handle-s {
+	left: 0;
+	right: 0;
+	bottom: -2px;
+	height: 4px;
+	cursor: s-resize;
+}
+
+.resize-handle-se {
+	right: -2px;
+	bottom: -2px;
+	width: 8px;
+	height: 8px;
+	cursor: se-resize;
+	border-radius: 0 0 4px 0;
+}
+
+/* 编辑对话框样式 */
+.edit-form {
+	.form-item {
+		margin-bottom: 20px;
+	}
+
+	.form-label {
+		display: block;
+		margin-bottom: 8px;
+		font-size: 14px;
+		font-weight: 600;
+		color: var(--el-text-color-primary);
+	}
+
+	.preview-section {
+		margin-top: 24px;
+	}
+
+	.preview-container {
+		border: 1px solid var(--el-border-color-light);
+		border-radius: 6px;
+		padding: 16px;
+		background: var(--el-fill-color-extra-light);
+		max-height: 200px;
+		overflow-y: auto;
+	}
+
+	.preview-content {
+		font-size: 14px;
+		line-height: 1.6;
+
+		:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
+			margin-top: 16px;
+			margin-bottom: 12px;
+
+			&:first-child {
+				margin-top: 0;
+			}
+		}
+
+		:deep(p) {
+			margin-bottom: 12px;
+
+			&:last-child {
+				margin-bottom: 0;
+			}
+		}
+
+		:deep(blockquote) {
+			margin: 12px 0;
+			padding: 12px 16px;
+		}
+
+		:deep(table) {
+			margin: 12px 0;
+		}
+	}
+}
+
+.dialog-footer {
+	text-align: right;
+}
+
 /* 下拉菜单样式 */
 :deep(.el-dropdown-menu) {
 	.el-dropdown-menu__item {
@@ -275,6 +570,12 @@ onUnmounted(() => {
 			background: var(--el-fill-color-light);
 
 			.el-icon {
+				color: var(--el-color-primary);
+			}
+		}
+
+		&:hover:has(.el-icon:contains('Delete')) {
+			.el-icon {
 				color: var(--el-color-danger);
 			}
 		}

+ 18 - 0
src/views/assistant/dashboard/edit.vue

@@ -86,6 +86,22 @@ const updateCardPosition = (index: number, position: { x: number; y: number }) =
 	}
 }
 
+// 更新卡片大小
+const updateCardSize = (index: number, size: { w: number; h: number }) => {
+	if (data.value[index]) {
+		data.value[index].w = size.w
+		data.value[index].h = size.h
+	}
+}
+
+// 更新卡片内容
+const updateCardContent = (index: number, content: { title: string; data: string }) => {
+	if (data.value[index]) {
+		data.value[index].title = content.title
+		data.value[index].data = content.data
+	}
+}
+
 // 删除卡片
 const removeCard = (index: number) => {
 	data.value.splice(index, 1)
@@ -117,6 +133,8 @@ const removeCard = (index: number) => {
 				<DashboardDesigner
 					:cards="renderer"
 					@update-position="updateCardPosition"
+					@update-size="updateCardSize"
+					@update-content="updateCardContent"
 					@remove-card="removeCard"
 					@add-card="addCard"
 				/>