feat:【antd】【ai】mindmap 的代码优化
This commit is contained in:
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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: '',
|
||||||
});
|
});
|
||||||
@@ -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"
|
||||||
@@ -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[] {
|
||||||
|
|||||||
@@ -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),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user