feat:【antd】【ai】mindmap 的代码优化

This commit is contained in:
YunaiV
2025-10-26 15:16:09 +08:00
parent a35350d055
commit 4d388bdb04
8 changed files with 60 additions and 48 deletions

View File

@@ -6,6 +6,7 @@ import { requestClient } from '#/api/request';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
const accessStore = useAccessStore(); const accessStore = useAccessStore();
export namespace AiMindmapApi { export namespace AiMindmapApi {
// AI 思维导图 // AI 思维导图
export interface MindMap { export interface MindMap {
@@ -19,7 +20,7 @@ export namespace AiMindmapApi {
} }
// AI 思维导图生成 // AI 思维导图生成
export interface AiMindMapGenerateReq { export interface AiMindMapGenerateReqVO {
prompt: string; prompt: string;
} }
} }
@@ -32,7 +33,7 @@ export function generateMindMap({
ctrl, ctrl,
}: { }: {
ctrl: AbortController; ctrl: AbortController;
data: AiMindmapApi.AiMindMapGenerateReq; data: AiMindmapApi.AiMindMapGenerateReqVO;
onClose?: (...args: any[]) => void; onClose?: (...args: any[]) => void;
onError?: (...args: any[]) => void; onError?: (...args: any[]) => void;
onMessage?: (res: any) => void; onMessage?: (res: any) => void;
@@ -53,12 +54,12 @@ export function generateMindMap({
}); });
} }
// 查询思维导图分页 /** 查询思维导图分页 */
export function getMindMapPage(params: any) { export function getMindMapPage(params: any) {
return requestClient.get(`/ai/mind-map/page`, { params }); return requestClient.get(`/ai/mind-map/page`, { params });
} }
// 删除思维导图 /** 删除思维导图 */
export function deleteMindMap(id: number) { export function deleteMindMap(id: number) {
return requestClient.delete(`/ai/mind-map/delete?id=${id}`); return requestClient.delete(`/ai/mind-map/delete?id=${id}`);
} }

View File

@@ -3,13 +3,15 @@ import type { AiMindmapApi } from '#/api/ai/mindmap';
import { nextTick, onMounted, ref } from 'vue'; import { nextTick, onMounted, ref } from 'vue';
import { alert, Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { MindMapContentExample } from '@vben/constants'; import { MindMapContentExample } from '@vben/constants';
import { message } from 'ant-design-vue';
import { generateMindMap } from '#/api/ai/mindmap'; import { generateMindMap } from '#/api/ai/mindmap';
import Left from './modules/Left.vue'; import Left from './modules/left.vue';
import Right from './modules/Right.vue'; import Right from './modules/right.vue';
const ctrl = ref<AbortController>(); // 请求控制 const ctrl = ref<AbortController>(); // 请求控制
const isGenerating = ref(false); // 是否正在生成思维导图 const isGenerating = ref(false); // 是否正在生成思维导图
@@ -26,8 +28,9 @@ function directGenerate(existPrompt: string) {
generatedContent.value = existPrompt; generatedContent.value = existPrompt;
isEnd.value = true; isEnd.value = true;
} }
/** 提交生成 */ /** 提交生成 */
function submit(data: AiMindmapApi.AiMindMapGenerateReq) { function handleSubmit(data: AiMindmapApi.AiMindMapGenerateReqVO) {
isGenerating.value = true; isGenerating.value = true;
isStart.value = true; isStart.value = true;
isEnd.value = false; isEnd.value = false;
@@ -38,8 +41,8 @@ function submit(data: AiMindmapApi.AiMindMapGenerateReq) {
onMessage: async (res: any) => { onMessage: async (res: any) => {
const { code, data, msg } = JSON.parse(res.data); const { code, data, msg } = JSON.parse(res.data);
if (code !== 0) { if (code !== 0) {
alert(`生成思维导图异常! ${msg}`); message.error(`生成思维导图异常! ${msg}`);
stopStream(); handleStopStream();
return; return;
} }
generatedContent.value = generatedContent.value + data; generatedContent.value = generatedContent.value + data;
@@ -49,19 +52,20 @@ function submit(data: AiMindmapApi.AiMindMapGenerateReq) {
onClose() { onClose() {
isEnd.value = true; isEnd.value = true;
leftRef.value?.setGeneratedContent(generatedContent.value); leftRef.value?.setGeneratedContent(generatedContent.value);
stopStream(); handleStopStream();
}, },
onError(err) { onError(err) {
console.error('生成思维导图失败', err); console.error('生成思维导图失败', err);
stopStream(); handleStopStream();
// 需要抛出异常,禁止重试 // 需要抛出异常,禁止重试
throw err; throw err;
}, },
ctrl: ctrl.value, ctrl: ctrl.value,
}); });
} }
/** 停止 stream 生成 */ /** 停止 stream 生成 */
function stopStream() { function handleStopStream() {
isGenerating.value = false; isGenerating.value = false;
isStart.value = false; isStart.value = false;
ctrl.value?.abort(); ctrl.value?.abort();
@@ -80,7 +84,7 @@ onMounted(() => {
ref="leftRef" ref="leftRef"
class="mr-4" class="mr-4"
:is-generating="isGenerating" :is-generating="isGenerating"
@submit="submit" @submit="handleSubmit"
@direct-generate="directGenerate" @direct-generate="directGenerate"
/> />
<Right <Right

View File

@@ -8,7 +8,9 @@ import { Button, Textarea } from 'ant-design-vue';
defineProps<{ defineProps<{
isGenerating: boolean; isGenerating: boolean;
}>(); }>();
const emits = defineEmits(['submit', 'directGenerate']); const emits = defineEmits(['submit', 'directGenerate']);
const formData = reactive({ const formData = reactive({
prompt: '', prompt: '',
}); });

View File

@@ -18,6 +18,7 @@ const props = defineProps<{
isGenerating: boolean; // isGenerating: boolean; //
isStart: boolean; // html isStart: boolean; // html
}>(); }>();
const md = MarkdownIt(); const md = MarkdownIt();
const contentRef = ref<HTMLDivElement>(); // header const contentRef = ref<HTMLDivElement>(); // header
const mdContainerRef = ref<HTMLDivElement>(); // markdown const mdContainerRef = ref<HTMLDivElement>(); // markdown
@@ -30,12 +31,14 @@ let markMap: Markmap | null = null;
const transformer = new Transformer(); const transformer = new Transformer();
let resizeObserver: null | ResizeObserver = null; let resizeObserver: null | ResizeObserver = null;
const initialized = false; const initialized = false;
/** 初始化 */
onMounted(() => { onMounted(() => {
resizeObserver = new ResizeObserver(() => { resizeObserver = new ResizeObserver(() => {
contentAreaHeight.value = contentRef.value?.clientHeight || 0; contentAreaHeight.value = contentRef.value?.clientHeight || 0;
// //
if (contentAreaHeight.value && !initialized) { if (contentAreaHeight.value && !initialized) {
/** 初始化思维导图 */ //
try { try {
if (!markMap) { if (!markMap) {
markMap = Markmap.create(svgRef.value!); markMap = Markmap.create(svgRef.value!);
@@ -52,11 +55,15 @@ onMounted(() => {
resizeObserver.observe(contentRef.value); resizeObserver.observe(contentRef.value);
} }
}); });
/** 卸载 */
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (resizeObserver && contentRef.value) { if (resizeObserver && contentRef.value) {
resizeObserver.unobserve(contentRef.value); resizeObserver.unobserve(contentRef.value);
} }
}); });
/** 监听 props 变化 */
watch(props, ({ generatedContent, isGenerating, isEnd, isStart }) => { watch(props, ({ generatedContent, isGenerating, isEnd, isStart }) => {
// markdown // markdown
if (isStart) { if (isStart) {
@@ -84,6 +91,7 @@ function update() {
console.error(error); console.error(error);
} }
} }
/** 处理内容 */ /** 处理内容 */
function processContent(text: string) { function processContent(text: string) {
const arr: string[] = []; const arr: string[] = [];
@@ -98,6 +106,7 @@ function processContent(text: string) {
} }
return arr.join('\n'); return arr.join('\n');
} }
/** 下载图片download SVG to png file */ /** 下载图片download SVG to png file */
function downloadImage() { function downloadImage() {
const svgElement = mindMapRef.value; const svgElement = mindMapRef.value;
@@ -112,6 +121,7 @@ function downloadImage() {
drawWithImageSize: false, drawWithImageSize: false,
}); });
} }
defineExpose({ defineExpose({
scrollBottom() { scrollBottom() {
mdContainerRef.value?.scrollTo(0, mdContainerRef.value?.scrollHeight); mdContainerRef.value?.scrollTo(0, mdContainerRef.value?.scrollHeight);
@@ -135,7 +145,6 @@ defineExpose({
</div> </div>
</template> </template>
<div ref="contentRef" class="hide-scroll-bar box-border h-full"> <div ref="contentRef" class="hide-scroll-bar box-border h-full">
<!--展示 markdown 的容器最终生成的是 html 字符串直接用 v-html 嵌入-->
<div <div
v-if="isGenerating" v-if="isGenerating"
ref="mdContainerRef" ref="mdContainerRef"
@@ -146,7 +155,6 @@ defineExpose({
v-html="html" v-html="html"
></div> ></div>
</div> </div>
<div ref="mindMapRef" class="wh-full"> <div ref="mindMapRef" class="wh-full">
<svg <svg
ref="svgRef" ref="svgRef"

View File

@@ -5,12 +5,9 @@ import type { SystemUserApi } from '#/api/system/user';
import { getSimpleUserList } from '#/api/system/user'; import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils'; import { getRangePickerDefaultProps } from '#/utils';
/** 关联数据 */
let userList: SystemUserApi.User[] = []; let userList: SystemUserApi.User[] = [];
async function getUserData() { getSimpleUserList().then((data) => (userList = data));
userList = await getSimpleUserList();
}
getUserData();
/** 列表的搜索表单 */ /** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] { export function useGridFormSchema(): VbenFormSchema[] {

View File

@@ -2,8 +2,6 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AiMindmapApi } from '#/api/ai/mindmap'; import type { AiMindmapApi } from '#/api/ai/mindmap';
import { nextTick, ref } from 'vue';
import { DocAlert, Page, useVbenDrawer } from '@vben/common-ui'; import { DocAlert, Page, useVbenDrawer } from '@vben/common-ui';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
@@ -12,21 +10,21 @@ import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteMindMap, getMindMapPage } from '#/api/ai/mindmap'; import { deleteMindMap, getMindMapPage } from '#/api/ai/mindmap';
import { $t } from '#/locales'; import { $t } from '#/locales';
import Right from '../index/modules/Right.vue'; import Right from '../index/modules/right.vue';
import { useGridColumns, useGridFormSchema } from './data'; import { useGridColumns, useGridFormSchema } from './data';
const previewContent = ref('');
const [Drawer, drawerApi] = useVbenDrawer({ const [Drawer, drawerApi] = useVbenDrawer({
header: false, header: false,
footer: false, footer: false,
destroyOnClose: true, destroyOnClose: true,
}); });
/** 刷新表格 */ /** 刷新表格 */
function handleRefresh() { function handleRefresh() {
gridApi.query(); gridApi.query();
} }
/** 删除 */ /** 删除思维导图记录 */
async function handleDelete(row: AiMindmapApi.MindMap) { async function handleDelete(row: AiMindmapApi.MindMap) {
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]), content: $t('ui.actionMessage.deleting', [row.id]),
@@ -34,14 +32,18 @@ async function handleDelete(row: AiMindmapApi.MindMap) {
}); });
try { try {
await deleteMindMap(row.id as number); await deleteMindMap(row.id as number);
message.success({ message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
});
handleRefresh(); handleRefresh();
} finally { } finally {
hideLoading(); hideLoading();
} }
} }
/** 预览思维导图 */
async function openPreview(row: AiMindmapApi.MindMap) {
drawerApi.setData(row.generatedContent).open();
}
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { formOptions: {
schema: useGridFormSchema(), schema: useGridFormSchema(),
@@ -63,6 +65,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
rowConfig: { rowConfig: {
keyField: 'id', keyField: 'id',
isHover: true,
}, },
toolbarConfig: { toolbarConfig: {
refresh: true, refresh: true,
@@ -70,11 +73,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
} as VxeTableGridOptions<AiMindmapApi.MindMap>, } as VxeTableGridOptions<AiMindmapApi.MindMap>,
}); });
async function openPreview(row: AiMindmapApi.MindMap) {
previewContent.value = row.generatedContent;
drawerApi.open();
await nextTick();
}
</script> </script>
<template> <template>
@@ -82,9 +80,10 @@ async function openPreview(row: AiMindmapApi.MindMap) {
<template #doc> <template #doc>
<DocAlert title="AI 思维导图" url="https://doc.iocoder.cn/ai/mindmap/" /> <DocAlert title="AI 思维导图" url="https://doc.iocoder.cn/ai/mindmap/" />
</template> </template>
<Drawer class="w-3/5"> <Drawer class="w-3/5">
<Right <Right
:generated-content="previewContent" :generated-content="drawerApi.getData() as any"
:is-end="true" :is-end="true"
:is-generating="false" :is-generating="false"
:is-start="false" :is-start="false"
@@ -99,6 +98,7 @@ async function openPreview(row: AiMindmapApi.MindMap) {
type: 'link', type: 'link',
icon: ACTION_ICON.EDIT, icon: ACTION_ICON.EDIT,
auth: ['ai:api-key:update'], auth: ['ai:api-key:update'],
disabled: !row.generatedContent,
onClick: openPreview.bind(null, row), onClick: openPreview.bind(null, row),
}, },
{ {

View File

@@ -19,12 +19,6 @@ const abortController = ref<AbortController>(); // // 写作进行中 abort 控
const rightRef = ref<InstanceType<typeof Right>>(); // 写作面板 const rightRef = ref<InstanceType<typeof Right>>(); // 写作面板
/** 停止 stream 生成 */
function handleStopStream() {
abortController.value?.abort();
isWriting.value = false;
}
/** 提交写作 */ /** 提交写作 */
function handleSubmit(data: Partial<AiWriteApi.Write>) { function handleSubmit(data: Partial<AiWriteApi.Write>) {
abortController.value = new AbortController(); abortController.value = new AbortController();
@@ -55,6 +49,12 @@ function handleSubmit(data: Partial<AiWriteApi.Write>) {
}); });
} }
/** 停止 stream 生成 */
function handleStopStream() {
abortController.value?.abort();
isWriting.value = false;
}
/** 点击示例触发 */ /** 点击示例触发 */
function handleExampleClick(type: keyof typeof WriteExample) { function handleExampleClick(type: keyof typeof WriteExample) {
writeResult.value = WriteExample[type].data; writeResult.value = WriteExample[type].data;

View File

@@ -19,12 +19,6 @@ const abortController = ref<AbortController>(); // // 写作进行中 abort 控
const rightRef = ref<InstanceType<typeof Right>>(); // 写作面板 const rightRef = ref<InstanceType<typeof Right>>(); // 写作面板
/** 停止 stream 生成 */
function handleStopStream() {
abortController.value?.abort();
isWriting.value = false;
}
/** 提交写作 */ /** 提交写作 */
function handleSubmit(data: Partial<AiWriteApi.Write>) { function handleSubmit(data: Partial<AiWriteApi.Write>) {
abortController.value = new AbortController(); abortController.value = new AbortController();
@@ -55,6 +49,12 @@ function handleSubmit(data: Partial<AiWriteApi.Write>) {
}); });
} }
/** 停止 stream 生成 */
function handleStopStream() {
abortController.value?.abort();
isWriting.value = false;
}
/** 点击示例触发 */ /** 点击示例触发 */
function handleExampleClick(type: keyof typeof WriteExample) { function handleExampleClick(type: keyof typeof WriteExample) {
writeResult.value = WriteExample[type].data; writeResult.value = WriteExample[type].data;