瀏覽代碼

接入仪表板设计器

kagg886 1 月之前
父節點
當前提交
5e85130141
共有 4 個文件被更改,包括 329 次插入24 次删除
  1. 14 1
      src/api/assist/index.ts
  2. 10 0
      src/api/assist/type.ts
  3. 25 20
      src/views/assistant/dashboard/edit.vue
  4. 280 3
      src/views/assistant/dashboard/index.vue

+ 14 - 1
src/api/assist/index.ts

@@ -9,7 +9,7 @@ import {
 	LmConfigDeleteParams,
 	LmConfigGetParams,
 	SessionMessagesListParams,
-	SessionMessagesSaveReq,
+	SessionMessagesSaveReq, LmDashboard,
 } from '/@/api/assist/type'
 import { get, post, del, put } from '/@/utils/request'
 import getOrigin from '/@/utils/origin'
@@ -31,6 +31,19 @@ export default {
 		setStatus: (data: LmConfigStatusReq) => put('/system/lmconfig/status', data),
 	},
 
+	dashboard: {
+		// 获取仪表盘列表
+		list: (params: any) => get('/system/lmdashboards/list', params),
+		// 获取仪表盘详情
+		detail: (id: number) => get('/system/lmdashboards/get', { id }),
+		// 添加仪表盘
+		add: (data: Omit<LmDashboard, 'id'>) => post('/system/lmdashboards/add', data),
+		// 编辑仪表盘
+		edit: (data: LmDashboard) => put('/system/lmdashboards/edit', data),
+		// 删除仪表盘
+		del: (ids: number[]) => del('/system/lmdashboards/delete', { ids }),
+	},
+
 	session: {
 		list: (params: any) => get('/system/lmsessions/list', params),
 		// 添加大模型会话

+ 10 - 0
src/api/assist/type.ts

@@ -165,3 +165,13 @@ export type SessionMessagesSaveReq = {
 	sessionId: number
 	messages: Message[]
 }
+
+export type LmDashboard = {
+	id: number
+	title: string
+	data: string
+	createdAt?: string // 创建时间
+	updatedAt?: string // 更新时间
+	createdBy?: number // 创建者ID
+	updatedBy?: number // 更新者ID
+}

+ 25 - 20
src/views/assistant/dashboard/edit.vue

@@ -8,22 +8,24 @@ import type { MarkdownDashBoard, Position, Size, Content, AddCardData, Component
 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 } from '@element-plus/icons-vue'
+import { useRoute } from 'vue-router'
 
 // 预留props,暂时不使用
-const props = defineProps<{
-	id?: number
-}>()
+const route = useRoute()
+
+const id = computed(() => route.query.id as unknown as number)
 
 const cards = ref<MarkdownDashBoard[]>([])
+const title = ref<string>('新建仪表板')
 
-// 预留加载功能,暂时不使用
 const { loading: loadingDashboard, doLoading: doLoadingDashBoard } = useLoading(async () => {
-	if (props.id === undefined) {
+	if (id.value === undefined) {
 		return
 	}
-	await new Promise((resolve) => setTimeout(resolve, 1000))
-	//TODO fetch remote
+	const res = await assist.dashboard.detail(id.value)
+	title.value = res.title
+	cards.value = JSON.parse(res.data)
 })
 
 const renderer = computed<MarkdownDashBoard[]>(() => [...cards.value].sort((a, b) => a.z - b.z))
@@ -35,9 +37,11 @@ const generateId = () => {
 
 const { loading: loadingDashboardSubmit, doLoading: doLoadingDashboardSubmit } = useLoading(async () => {
 	try {
-		await new Promise((resolve) => setTimeout(resolve, 1000))
-		// TODO: 实际的保存逻辑
-		// 这里应该调用API保存data.value到后端
+		assist.dashboard.edit({
+			id: id.value,
+			title: title.value,
+			data: JSON.stringify(cards.value),
+		})
 		ElMessage.success('仪表板保存成功')
 	} catch (error) {
 		ElMessage.error('保存失败')
@@ -148,7 +152,7 @@ const parseMessagesForLibrary = (messages: Message[]): ComponentLibraryItem[] =>
 	const md = new MarkdownIt({
 		html: true,
 		linkify: true,
-		typographer: true
+		typographer: true,
 	})
 
 	messages.forEach((message, messageIndex) => {
@@ -174,7 +178,7 @@ const isValidEchartsJSON = (str: string): boolean => {
 	}
 	try {
 		const expr = JSON.parse(str)
-		return expr["series"] !== undefined
+		return expr['series'] !== undefined
 	} catch {
 		return false
 	}
@@ -202,7 +206,7 @@ ${content}
 \`\`\``,
 						preview: `\`\`\`echarts
 ${content}
-\`\`\``
+\`\`\``,
 					})
 				} catch (error) {
 					console.warn('解析echarts配置失败:', error)
@@ -228,7 +232,7 @@ const extractTablesFromTokens = (tokens: any[], messageIndex: number, libraryIte
 				const title = `表格 ${libraryItems.length + 1}`
 				const previewData = {
 					headers: tableData.headers.slice(0, 3), // 只显示前3列
-					rows: tableData.rows.slice(0, 3) // 只显示前3行
+					rows: tableData.rows.slice(0, 3), // 只显示前3行
 				}
 
 				libraryItems.push({
@@ -237,7 +241,7 @@ const extractTablesFromTokens = (tokens: any[], messageIndex: number, libraryIte
 					icon: 'ele-Table',
 					description: `来自消息的数据表格 (${tableData.rows.length}行 x ${tableData.headers.length}列)`,
 					data: generateMarkdownTable(tableData),
-					preview: generateMarkdownTable(previewData)
+					preview: generateMarkdownTable(previewData),
 				})
 			}
 		}
@@ -344,15 +348,16 @@ const generateMarkdownTable = (tableData: TableData): string => {
 	}
 
 	// 生成表格行
-	tableData.rows.forEach(row => {
+	tableData.rows.forEach((row) => {
 		markdown += '| ' + row.join(' | ') + ' |\n'
 	})
 
 	return markdown
 }
 
-onMounted(async () => {
-	await Promise.all([doLoadingChat(), doLoadingDashBoard()])
+onMounted( () => {
+	doLoadingChat()
+	doLoadingDashBoard()
 })
 </script>
 
@@ -361,7 +366,7 @@ onMounted(async () => {
 		<!-- 顶部工具栏 -->
 		<div class="toolbar">
 			<div class="toolbar-left">
-				<h2>仪表板设计器</h2>
+				<h2>仪表板设计器 | {{ title }}</h2>
 			</div>
 			<div class="toolbar-right">
 				<el-button type="primary" @click="doLoadingDashboardSubmit" :loading="loadingDashboardSubmit" :disabled="loadingDashboard">

+ 280 - 3
src/views/assistant/dashboard/index.vue

@@ -1,9 +1,286 @@
-<script setup lang="ts"></script>
+<script setup lang="ts">
+import { onMounted, reactive, ref } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Search as EleSearch, Refresh as EleRefresh, Plus as ElePlus, Delete as EleDelete, Edit as EleEdit, Setting as EleSetting } from '@element-plus/icons-vue'
+import { useLoading } from '/@/utils/loading-util'
+import { useRouter } from 'vue-router'
+import api from '/@/api/assist'
+import type { LmDashboard } from '/@/api/assist/type'
+
+const router = useRouter()
+
+// 数据搜索部分
+const searchParam = reactive({
+	pageNum: 1,
+	pageSize: 10,
+	keyWord: '',
+	dateRange: [],
+})
+
+const total = ref<number>(0)
+const data = ref<Array<LmDashboard>>([])
+const ids = ref<number[]>([])
+
+// 加载列表数据
+const { loading, doLoading: doListLoad } = useLoading(async () => {
+	try {
+		const res: {
+			list: LmDashboard[]
+			total: number
+		} = await api.dashboard.list(searchParam)
+		total.value = res.total
+		data.value = res.list
+	} catch (error) {
+		console.error('获取仪表盘列表失败:', error)
+		data.value = []
+		total.value = 0
+	}
+})
+
+// 重置搜索条件
+const reset = () => {
+	Object.assign(searchParam, {
+		pageNum: 1,
+		pageSize: 10,
+		keyWord: '',
+		dateRange: [],
+	})
+	doListLoad()
+}
+
+// 选择删除项
+const onDeleteItemSelected = (selection: LmDashboard[]) => {
+	ids.value = selection.map((item) => item.id!).filter(Boolean)
+}
+
+// 批量删除
+const del = async () => {
+	if (ids.value.length === 0) {
+		ElMessage.error('请选择要删除的数据')
+		return
+	}
+
+	try {
+		await ElMessageBox.confirm('您确定要删除所选数据吗?', '提示', {
+			confirmButtonText: '确认',
+			cancelButtonText: '取消',
+			type: 'warning',
+		})
+
+		await api.dashboard.del(ids.value)
+		ElMessage.success('删除成功')
+		await doListLoad()
+	} catch (error) {
+		if (error !== 'cancel') {
+			console.error('删除失败:', error)
+			ElMessage.error('删除失败')
+		}
+	}
+}
+
+// 单个删除
+const delSingle = async (id: number) => {
+	try {
+		await ElMessageBox.confirm('您确定要删除这个仪表盘吗?', '提示', {
+			confirmButtonText: '确认',
+			cancelButtonText: '取消',
+			type: 'warning',
+		})
+
+		await api.dashboard.del([id])
+		ElMessage.success('删除成功')
+		await doListLoad()
+	} catch (error) {
+		if (error !== 'cancel') {
+			console.error('删除失败:', error)
+			ElMessage.error('删除失败')
+		}
+	}
+}
+
+// 编辑/新增对话框相关
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const isEdit = ref(false)
+const formRef = ref()
+
+const formData = reactive<LmDashboard>({
+	id: 0,
+	title: '',
+	data: '',
+})
+
+// 表单验证规则
+const formRules = {
+	title: [{ required: true, message: '请输入仪表盘标题', trigger: 'blur' }],
+}
+
+// 重置表单
+const resetForm = () => {
+	Object.assign(formData, {
+		id: 0,
+		title: '',
+		data: '',
+	})
+	formRef.value?.clearValidate()
+}
+
+// 打开新增对话框
+const openAddDialog = () => {
+	resetForm()
+	dialogTitle.value = '新增仪表盘'
+	isEdit.value = false
+	dialogVisible.value = true
+}
+
+// 打开编辑对话框
+const openEditDialog = async (row: LmDashboard) => {
+	resetForm()
+	Object.assign(formData, row)
+	dialogTitle.value = '编辑仪表盘'
+	isEdit.value = true
+	dialogVisible.value = true
+}
+
+// 保存表单
+const { loading: saveLoading, doLoading: doSave } = useLoading(async () => {
+	try {
+		await formRef.value?.validate()
+
+		if (isEdit.value) {
+			await api.dashboard.edit(formData as any)
+			ElMessage.success('编辑成功')
+		} else {
+			await api.dashboard.add({...formData,data: '[]'} as any)
+			ElMessage.success('新增成功')
+		}
+
+		dialogVisible.value = false
+		await doListLoad()
+	} catch (error) {
+		console.error('保存失败:', error)
+		if (error !== false) {
+			// 表单验证失败时不显示错误消息
+			ElMessage.error('保存失败')
+		}
+	}
+})
+
+// 跳转到设计页面
+const goToDesign = (id?: number) => {
+	if (id) {
+		router.push(`/assistant/dashboard/edit?id=${id}`)
+	} else {
+		router.push('/assistant/dashboard/edit')
+	}
+}
+
+// 组件挂载时加载数据
+onMounted(() => {
+	doListLoad()
+})
+</script>
 
 <template>
-	<div>仪表盘</div>
+	<div class="system-model-container layout-padding">
+		<el-card shadow="hover" class="layout-padding-auto">
+			<!-- 搜索区域 -->
+			<el-form :model="searchParam" ref="queryRef" :inline="true" label-width="68px">
+				<el-form-item label="关键词" prop="keyWord">
+					<el-input v-model="searchParam.keyWord" placeholder="请输入标题关键词" clearable style="width: 200px" />
+				</el-form-item>
+				<el-form-item label="" prop="dateRange">
+					<el-date-picker
+						v-model="searchParam.dateRange"
+						style="width: 220px"
+						value-format="YYYY-MM-DD"
+						type="daterange"
+						range-separator="-"
+						start-placeholder="开始时间"
+						end-placeholder="结束时间"
+					/>
+				</el-form-item>
+				<el-form-item>
+					<el-button type="primary" @click="doListLoad">
+						<el-icon>
+							<EleSearch />
+						</el-icon>
+						查询
+					</el-button>
+					<el-button @click="reset">
+						<el-icon>
+							<EleRefresh />
+						</el-icon>
+						重置
+					</el-button>
+					<el-button type="primary" @click="openAddDialog">
+						<el-icon>
+							<ElePlus />
+						</el-icon>
+						新增仪表盘
+					</el-button>
+					<el-button type="danger" @click="del" :disabled="ids.length === 0">
+						<el-icon>
+							<EleDelete />
+						</el-icon>
+						批量删除
+					</el-button>
+				</el-form-item>
+			</el-form>
+
+			<!-- 表格区域 -->
+			<el-table :data="data" v-loading="loading" style="width: 100%" @selection-change="onDeleteItemSelected">
+				<el-table-column type="selection" width="55" align="center" />
+				<el-table-column label="ID" prop="id" width="80" align="center" />
+				<el-table-column label="标题" prop="title" min-width="200" align="center" />
+				<el-table-column label="创建时间" prop="createdAt" width="180" align="center" />
+				<el-table-column label="更新时间" prop="updatedAt" width="180" align="center" />
+				<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="success" size="small" @click="goToDesign(scope.row.id)">
+							<el-icon><EleSetting /></el-icon>
+							设计
+						</el-button>
+						<el-button text type="danger" size="small" @click="delSingle(scope.row.id)"> 删除 </el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+
+			<!-- 分页 -->
+			<el-pagination
+				v-show="total > 0"
+				:current-page="searchParam.pageNum"
+				:page-size="searchParam.pageSize"
+				:page-sizes="[10, 20, 50, 100]"
+				:total="total"
+				layout="total, sizes, prev, pager, next, jumper"
+				@size-change="(val: number) => { searchParam.pageSize = val; doListLoad() }"
+				@current-change="(val: number) => { searchParam.pageNum = val; doListLoad() }"
+			/>
+		</el-card>
+
+		<!-- 新增/编辑对话框 -->
+		<el-dialog :title="dialogTitle" v-model="dialogVisible" width="600px" destroy-on-close>
+			<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
+				<el-form-item label="仪表盘标题" prop="title">
+					<el-input v-model="formData.title" placeholder="请输入仪表盘标题" clearable />
+				</el-form-item>
+			</el-form>
+			<template #footer>
+				<div class="dialog-footer">
+					<el-button @click="dialogVisible = false">取消</el-button>
+					<el-button type="primary" @click="doSave" :loading="saveLoading">确定</el-button>
+				</div>
+			</template>
+		</el-dialog>
+	</div>
 </template>
 
 <style scoped lang="scss">
-
+.system-model-container {
+	.layout-padding-auto {
+		padding: 15px;
+	}
+}
 </style>