Sfoglia il codice sorgente

添加预览功能

kagg886 1 mese fa
parent
commit
30e4d5fabf

+ 118 - 0
src/components/assistant/DashboardViewer.vue

@@ -0,0 +1,118 @@
+<script setup lang="ts">
+import ViewerCard from './ViewerCard.vue'
+import { Document } from '@element-plus/icons-vue'
+import type { MarkdownDashBoard } from './types'
+
+defineProps<{
+	cards: MarkdownDashBoard[]
+}>()
+</script>
+
+<template>
+	<div class="dashboard-viewer">
+		<div class="viewer-canvas">
+			<!-- 网格背景 -->
+			<div class="grid-background"></div>
+
+			<!-- 渲染所有卡片 -->
+			<ViewerCard
+				v-for="card in cards"
+				:key="card.id"
+				:card="card"
+			/>
+
+			<!-- 空状态提示 -->
+			<div v-if="cards.length === 0" class="empty-canvas">
+				<div class="empty-icon">
+					<el-icon :size="60" color="#d1d5db">
+						<Document />
+					</el-icon>
+				</div>
+				<div class="empty-text">
+					<h3>暂无内容</h3>
+					<p>仪表板中还没有任何组件</p>
+				</div>
+			</div>
+		</div>
+	</div>
+</template>
+
+<style scoped lang="scss">
+.dashboard-viewer {
+	height: 100%;
+	display: flex;
+	flex-direction: column;
+}
+
+.viewer-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	padding: 16px 20px;
+	border-bottom: 1px solid var(--el-border-color-light);
+	background: var(--el-fill-color-extra-light);
+
+	h3 {
+		margin: 0;
+		color: var(--el-text-color-primary);
+		font-size: 16px;
+		font-weight: 600;
+	}
+
+	.viewer-info {
+		font-size: 14px;
+		color: var(--el-text-color-regular);
+	}
+}
+
+.viewer-canvas {
+	flex: 1;
+	position: relative;
+	overflow: hidden;
+	background: var(--el-bg-color-page);
+	min-height: 500px;
+}
+
+.grid-background {
+	position: absolute;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	background-image:
+		linear-gradient(to right, var(--el-border-color-extra-light) 1px, transparent 1px),
+		linear-gradient(to bottom, var(--el-border-color-extra-light) 1px, transparent 1px);
+	background-size: 20px 20px;
+	pointer-events: none;
+	opacity: 0.5;
+}
+
+.empty-canvas {
+	position: absolute;
+	top: 50%;
+	left: 50%;
+	transform: translate(-50%, -50%);
+	text-align: center;
+	color: var(--el-text-color-secondary);
+
+	.empty-icon {
+		margin-bottom: 16px;
+		opacity: 0.6;
+	}
+
+	.empty-text {
+		h3 {
+			margin: 0 0 8px 0;
+			font-size: 18px;
+			font-weight: 500;
+			color: var(--el-text-color-regular);
+		}
+
+		p {
+			margin: 0;
+			font-size: 14px;
+			color: var(--el-text-color-secondary);
+		}
+	}
+}
+</style>

+ 132 - 0
src/components/assistant/ViewerCard.vue

@@ -0,0 +1,132 @@
+<script setup lang="ts">
+import { computed, ref, watch } from 'vue'
+import Markdown from '/@/components/markdown/Markdown.vue'
+import type { MarkdownDashBoard } from './types'
+import { MarkdownPlugin } from '/@/components/markdown/type/markdown'
+import EChartsPlugin from '/@/components/markdown/plugins/echarts'
+import VueCharts from '/@/components/markdown/plugins/impl/VueCharts.vue'
+
+const plugin: Array<MarkdownPlugin<any>> = [EChartsPlugin()]
+
+const props = defineProps<{
+	card: MarkdownDashBoard
+}>()
+
+// 计算卡片样式
+const cardStyle = computed(() => ({
+	position: 'absolute' as const,
+	left: `${props.card.x}%`,
+	top: `${props.card.y}%`,
+	width: `${props.card.w}%`,
+	height: `${props.card.h}%`,
+	zIndex: props.card.z + 100
+}))
+
+const charts = ref()
+
+watch(()=> props.card.w, () => {
+	charts.value?.resize()
+})
+watch(()=> props.card.h, () => {
+	charts.value?.resize()
+})
+</script>
+
+<template>
+	<div
+		:style="cardStyle"
+		class="viewer-card"
+	>
+		<el-card class="card-content" shadow="hover">
+			<!-- 卡片标题栏 -->
+			<template #header v-if="card.title !== undefined">
+				<div class="card-header">
+					<span class="card-title">{{ card.title }}</span>
+				</div>
+			</template>
+
+			<!-- 卡片内容 -->
+			<div class="card-body">
+				<Markdown
+					v-if="card.type === 'markdown'"
+					:content="card.data"
+					:plugins="plugin"
+					class="markdown-content"
+				/>
+				<vue-charts ref="charts" style="width: 100%;height: 100%" :data="card.data" v-if="card.type === 'echarts'"/>
+			</div>
+		</el-card>
+	</div>
+</template>
+
+<style scoped lang="scss">
+.viewer-card {
+	transition: none;
+}
+
+.card-content {
+	height: 100%;
+	display: flex;
+	flex-direction: column;
+
+	:deep(.el-card__header) {
+		padding: 12px 16px;
+		border-bottom: 1px solid var(--el-border-color-lighter);
+		background: var(--el-fill-color-extra-light);
+	}
+
+	:deep(.el-card__body) {
+		flex: 1;
+		padding: 16px;
+		overflow: auto;
+	}
+}
+
+.card-header {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	user-select: none;
+}
+
+.card-title {
+	font-weight: 600;
+	color: var(--el-text-color-primary);
+	font-size: 14px;
+}
+
+.card-body {
+	height: 100%;
+	overflow: auto;
+}
+
+.markdown-content {
+	font-size: 13px;
+	line-height: 1.5;
+
+	:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
+		margin-top: 12px;
+		margin-bottom: 8px;
+		font-size: 14px;
+	}
+
+	:deep(p) {
+		margin-bottom: 8px;
+	}
+
+	:deep(blockquote) {
+		margin: 8px 0;
+		padding: 8px 12px;
+		font-size: 12px;
+	}
+
+	:deep(table) {
+		font-size: 12px;
+		margin: 8px 0;
+	}
+
+	:deep(th), :deep(td) {
+		padding: 4px 8px;
+	}
+}
+</style>

+ 47 - 2
src/views/assistant/dashboard/edit.vue

@@ -2,16 +2,16 @@
 import { useLoading } from '/@/utils/loading-util'
 import { computed, onMounted, ref, watch } from 'vue'
 import DashboardDesigner from '/@/components/assistant/DashboardDesigner.vue'
+import DashboardViewer from '/@/components/assistant/DashboardViewer.vue'
 import ComponentLibrary from '/@/components/assistant/ComponentLibrary.vue'
 import { ElMessage } from 'element-plus'
 import type { MarkdownDashBoard, Position, Size, Content, AddCardData, ComponentLibraryItem } from '/@/components/assistant/types'
 import { LmSession, Message } from '/@/api/assist/type'
 import assist from '/@/api/assist'
 import MarkdownIt from 'markdown-it'
-import { PieChart } from '@element-plus/icons-vue'
+import { PieChart, View } from '@element-plus/icons-vue'
 import { useRoute } from 'vue-router'
 
-// 预留props,暂时不使用
 const route = useRoute()
 
 const id = computed(() => route.query.id as unknown as number)
@@ -19,6 +19,9 @@ const id = computed(() => route.query.id as unknown as number)
 const cards = ref<MarkdownDashBoard[]>([])
 const title = ref<string>('新建仪表板')
 
+// 预览相关状态
+const showPreviewDialog = ref(false)
+
 const { loading: loadingDashboard, doLoading: doLoadingDashBoard } = useLoading(async () => {
 	if (id.value === undefined) {
 		return
@@ -380,6 +383,16 @@ const generateMarkdownTable = (tableData: TableData): string => {
 	return markdown
 }
 
+// 打开预览对话框
+const openPreview = () => {
+	showPreviewDialog.value = true
+}
+
+// 关闭预览对话框
+const closePreview = () => {
+	showPreviewDialog.value = false
+}
+
 onMounted( () => {
 	doLoadingChat()
 	doLoadingDashBoard()
@@ -394,6 +407,9 @@ onMounted( () => {
 				<h2>仪表板设计器 | {{ title }}</h2>
 			</div>
 			<div class="toolbar-right">
+				<el-button :icon="View" @click="openPreview" :disabled="loadingDashboard">
+					预览
+				</el-button>
 				<el-button type="primary" @click="doLoadingDashboardSubmit" :loading="loadingDashboardSubmit" :disabled="loadingDashboard">
 					保存仪表板
 				</el-button>
@@ -422,6 +438,20 @@ onMounted( () => {
 				<ComponentLibrary class="library-panel-content" v-loading="loadingLibrary" :library="library" @add-card="addCard" />
 			</div>
 		</div>
+
+		<!-- 预览对话框 -->
+		<el-dialog
+			v-model="showPreviewDialog"
+			title="仪表板预览"
+			width="90%"
+			:before-close="closePreview"
+			append-to-body
+			class="preview-dialog"
+		>
+			<div class="preview-container">
+				<DashboardViewer :cards="renderer" />
+			</div>
+		</el-dialog>
 	</div>
 </template>
 
@@ -487,4 +517,19 @@ onMounted( () => {
 		width: 100%;
 	}
 }
+
+/* 预览对话框样式 */
+:deep(.preview-dialog) {
+	.el-dialog__body {
+		padding: 0;
+		height: 70vh;
+	}
+}
+
+.preview-container {
+	height: 100%;
+	border: 1px solid var(--el-border-color-light);
+	border-radius: 6px;
+	overflow: hidden;
+}
 </style>

+ 9 - 0
src/views/assistant/dashboard/index.vue

@@ -6,6 +6,7 @@ import { useLoading } from '/@/utils/loading-util'
 import { useRouter } from 'vue-router'
 import api from '/@/api/assist'
 import type { LmDashboard } from '/@/api/assist/type'
+import View from '/@/views/assistant/dashboard/view.vue'
 
 const router = useRouter()
 
@@ -175,6 +176,10 @@ const goToDesign = (id?: number) => {
 	}
 }
 
+const goToView = (id: number) => {
+	router.push(`/assistant/dashboard/view?id=${id}`)
+}
+
 // 组件挂载时加载数据
 onMounted(() => {
 	doListLoad()
@@ -238,6 +243,10 @@ onMounted(() => {
 				<el-table-column label="操作" width="250" align="center" fixed="right">
 					<template #default="scope">
 						<el-button text type="primary" size="small" @click="openEditDialog(scope.row)"> 编辑 </el-button>
+						<el-button text type="primary" size="small" @click="goToView(scope.row.id)">
+							<el-icon><ele-search /></el-icon>
+							预览
+						</el-button>
 						<el-button text type="success" size="small" @click="goToDesign(scope.row.id)">
 							<el-icon><EleSetting /></el-icon>
 							设计

+ 27 - 0
src/views/assistant/dashboard/view.vue

@@ -0,0 +1,27 @@
+<script setup lang="ts">
+import { useRoute } from 'vue-router'
+import { computed, onMounted, ref } from 'vue'
+import DashboardViewer from '/@/components/assistant/DashboardViewer.vue'
+import { MarkdownDashBoard } from '/@/components/assistant/types'
+import { useLoading } from '/@/utils/loading-util'
+import assist from '/@/api/assist'
+
+const route = useRoute()
+const id = computed(() => parseInt(route.query['id']?.toString() ?? '-1'))
+
+const cards = ref<MarkdownDashBoard[]>([])
+
+const { loading: loadingCards, doLoading: executeCards } = useLoading(async () => {
+	assist.dashboard.detail(id.value).then((res) => {
+		cards.value = JSON.parse(res.data)
+	})
+})
+
+onMounted(executeCards)
+</script>
+
+<template>
+	<dashboard-viewer :cards="cards" style="width: 100%;height: 100%"/>
+</template>
+
+<style scoped lang="scss"></style>