refactor(@vben/web-antd): 重构 PPT 生成页面

- 更新基本表单的验证逻辑
- 优化 PPT 生成和预览流程
- 添加思维链展示功能
- 改进错误处理和用户提示
- 调整页面布局和样式
This commit is contained in:
Kven 2025-07-10 20:51:34 +08:00
parent b2b6a202ea
commit 74da2052e1
4 changed files with 292 additions and 186 deletions

View File

@ -176,7 +176,7 @@ export function updateUser(id: any, data: UserApi.UserRecord) {
}
export function selfUpdate(data: UserApi.UserUpdateRecord) {
return requestClient.patch<UserApi.Res>(`/rest/user/self`, data);
return requestClient.patch<boolean>(`/rest/user/self`, data);
}
// 获取个人用户信息

View File

@ -27,7 +27,7 @@ const handleSubmit = async () => {
email: userInfo.value.email,
address: userInfo.value.address,
});
if (res.code === 200) {
if (res) {
notification.success({
message: '修改成功',
description: '用户信息已更新',
@ -36,7 +36,7 @@ const handleSubmit = async () => {
} else {
notification.error({
message: '修改失败',
description: res.msg,
description: res,
});
}
};

View File

@ -1,18 +1,42 @@
<script setup lang="ts">
import type { BubbleListProps } from 'ant-design-x-vue';
import type { ThoughtChainItem } from 'ant-design-x-vue';
import type { DrawerPlacement } from '@vben/common-ui';
import type { PropsWork, ResultItem } from '../typing';
import type { WorkflowResult } from '#/views/word/typing';
import { h, ref, watch } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import { SvgPPT } from '@vben/icons';
import { useAccessStore, useUserStore } from '@vben/stores';
import { UserOutlined } from '@ant-design/icons-vue';
import { Button, Flex, Space } from 'ant-design-vue';
import { Attachments, Bubble, Sender, Welcome } from 'ant-design-x-vue';
import {
Card,
CardContent,
CardHeader,
CardTitle,
VbenIcon,
} from '@vben-core/shadcn-ui';
import {
CheckCircleOutlined,
InfoCircleOutlined,
LoadingOutlined,
// UserOutlined,
} from '@ant-design/icons-vue';
import { Button, notification, Space } from 'ant-design-vue';
import {
// Attachments,
Sender,
ThoughtChain,
Welcome,
XStream,
} from 'ant-design-x-vue';
import { getToken } from '#/api/csrf';
import PptPreview from './ppt-preview.vue';
@ -41,145 +65,202 @@ const props = withDefaults(defineProps<PropsWork>(), {
});
const userStore = useUserStore();
const roles: BubbleListProps['roles'] = {
user: {
placement: 'end',
typing: false,
styles: {
content: {
background: '#ffffff',
},
},
avatar: { icon: h(UserOutlined), style: { background: '#87d068' } },
},
ai: {
placement: 'start',
typing: false,
style: {
maxWidth: 600,
marginInlineEnd: 44,
},
styles: {
content: {
background: '#ffffff',
},
footer: {
width: '100%',
},
},
avatar: { icon: h(UserOutlined), style: { background: '#fde3cf' } },
},
file: {
placement: 'start',
avatar: { icon: h(UserOutlined), style: { visibility: 'hidden' } },
variant: 'borderless',
messageRender: (items: any) =>
h(
Flex,
{ vertical: true, gap: 'middle' },
items.map((item: any) =>
h(Attachments.FileCard, { key: item.uid, item }),
),
),
},
};
// const roles: BubbleListProps['roles'] = {
// user: {
// placement: 'end',
// typing: false,
// styles: {
// content: {
// background: '#ffffff',
// },
// },
// avatar: { icon: h(UserOutlined), style: { background: '#87d068' } },
// },
// ai: {
// placement: 'start',
// typing: false,
// style: {
// maxWidth: 600,
// marginInlineEnd: 44,
// },
// styles: {
// content: {
// background: '#ffffff',
// },
// footer: {
// width: '100%',
// },
// },
// avatar: { icon: h(UserOutlined), style: { background: '#fde3cf' } },
// },
// file: {
// placement: 'start',
// avatar: { icon: h(UserOutlined), style: { visibility: 'hidden' } },
// variant: 'borderless',
// messageRender: (items: any) =>
// h(
// Flex,
// { vertical: true, gap: 'middle' },
// items.map((item: any) =>
// h(Attachments.FileCard, { key: item.uid, item }),
// ),
// ),
// },
// };
// ==================== State ====================
const content = ref('');
const agentRequestLoading = ref(false);
const fetchStatus = ref('');
const resultItems = ref<ResultItem[]>([]);
const fetchResult = ref('');
const accessStore = useAccessStore();
const mockServerResponseData: ThoughtChainItem[] = [];
const chainItems = ref<ThoughtChainItem[]>(mockServerResponseData);
// const fetchResult = ref('');
const pptMessage = ref('');
const [PreviewDrawer, previewDrawerApi] = useVbenDrawer({
//
connectedComponent: PptPreview,
// placement: 'left',
});
function openPreviewDrawer(
placement: DrawerPlacement = 'right',
filename?: string,
) {
previewDrawerApi.setState({ placement }).setData(filename).open();
function openPreviewDrawer(placement: DrawerPlacement = 'right', file?: any) {
previewDrawerApi.setState({ placement }).setData(file.url).open();
}
// ==================== Event ====================
// .pptx URL
// function isPptxURL(str: string): boolean {
// return str.endsWith('.pptx');
// }
function getStatusIcon(status: ThoughtChainItem['status']) {
switch (status) {
case 'error': {
return h(InfoCircleOutlined);
}
case 'pending': {
return h(LoadingOutlined);
}
case 'success': {
return h(CheckCircleOutlined);
}
default: {
return undefined;
}
}
}
function downloadPpt(files: any) {
// <a>
const link = document.createElement('a');
link.href = files.url; //
link.download = files.filename; //
document.body.append(link); // <a>
link.click(); //
link.remove(); // <a>
}
function updateChainItem(status: ThoughtChainItem['status']) {
if (mockServerResponseData.length > 0) {
mockServerResponseData[mockServerResponseData.length - 1].status = status;
mockServerResponseData[mockServerResponseData.length - 1].icon =
getStatusIcon(status);
mockServerResponseData[mockServerResponseData.length - 1].description =
`status: ${status}`;
}
}
function parseAndRenderStream(text: WorkflowResult) {
let parsedData;
try {
parsedData = JSON.parse(text?.data);
} catch (error) {
console.error('Failed to parse event data:', error);
chainItems.value = [];
notification.error({
message: '流式数据解析失败',
description: '无法解析来自服务器的数据,请检查网络连接或稍后重试。',
});
return;
}
const { event, data } = parsedData;
if (event === 'node_started' && data) {
mockServerResponseData.push({
key: data.nodeId,
title: data.title,
description: data.nodeType,
icon: getStatusIcon('pending'),
status: 'pending',
});
chainItems.value = [...mockServerResponseData];
} else if (
event === 'node_finished' &&
data &&
mockServerResponseData.length > 0
) {
updateChainItem('success');
chainItems.value = [...mockServerResponseData];
if (data.outputs.files?.length > 0) {
pptMessage.value = data.outputs.files[0];
}
}
}
const startFetching = async () => {
resultItems.value = [];
pptMessage.value = '';
agentRequestLoading.value = true;
fetchStatus.value = 'fetching';
resultItems.value.push({
key: resultItems.value.length + 1,
role: 'user',
content: content.value,
});
// resultItems.value.push({
// key: resultItems.value.length + 1,
// role: 'user',
// content: content.value,
// });
try {
const res = await props.runWorkflow(props.item.id, {
userId: userStore.userInfo?.userId || '',
conversationId: '',
files: [],
inputs: {
declarationDoc: content.value,
const res = await fetch(`/api/v1/workflow/run/stream/${props.item.id}`, {
method: 'POST',
body: JSON.stringify({
userId: userStore.userInfo?.userId || '',
conversationId: '',
files: [],
inputs: {
declarationDoc: content.value,
},
}),
headers: {
'Accept-Language': 'zh-CN',
'Content-Type': 'application/json',
'X-CSRF-TOKEN': getToken() || '',
Authorization: `Bearer ${accessStore.accessToken}`,
},
});
const files = res.data.outputs.files[0];
content.value = '';
// const filename = ref('');
// if (result && isPptxURL(result)) {
// filename.value = extractPptxFilename(result);
// }
// url http://47.112.173.8:6802/static/66f3cfd95e364a239d8036390db658ae.pptx
fetchResult.value = files.url;
resultItems.value.push({
key: resultItems.value.length + 1,
role: 'ai',
content: '文档已生成',
footer: h(Flex, null, [
h(
Button,
{
type: 'primary',
onClick: () => {
openPreviewDrawer('right', files.url);
},
},
'文档预览',
),
h(
Button,
{
type: 'primary',
style: {
marginLeft: '10px',
},
onClick: () => {
// <a>
const link = document.createElement('a');
link.href = files.url; //
link.download = files.filename; //
document.body.append(link); // <a>
link.click(); //
link.remove(); // <a>
},
},
'文档下载',
),
]),
});
// AI
for await (const chunk of XStream({ readableStream: res.body })) {
// console.log('chunk', chunk.data);
// const text = new TextDecoder().decode(chunk.data);
parseAndRenderStream(chunk);
}
} catch (error) {
console.error(error);
console.error('Stream processing failed:', error);
notification.error({
message: '请求失败',
description: 'AI 回答获取失败,请稍后再试',
});
}
// try {
// const res = await props.runWorkflow(props.item.id, {
// userId: userStore.userInfo?.userId || '',
// conversationId: '',
// files: [],
// inputs: {
// declarationDoc: content.value,
// },
// });
// } catch (error) {
// console.error(error);
// }
//
content.value = '';
agentRequestLoading.value = false;
fetchStatus.value = 'completed';
};
@ -191,56 +272,58 @@ watch(
() => props.itemMessage,
(newVal) => {
resultItems.value = [];
chainItems.value = [];
content.value = '';
if (newVal && newVal.length > 0) {
newVal.forEach((msg) => {
if (msg.role === 'user') {
resultItems.value.push({
key: resultItems.value.length + 1,
role: msg.role, // 'user' or 'ai'
content: msg.content,
footer: msg.footer,
});
} else {
resultItems.value.push({
key: resultItems.value.length + 1,
role: msg.role, // 'user' or 'ai'
content: '文档已生成',
footer: h(Flex, null, [
h(
Button,
{
type: 'primary',
onClick: () => {
openPreviewDrawer('right', msg.content.url);
},
},
'文档预览',
),
h(
Button,
{
type: 'primary',
style: {
marginLeft: '10px',
},
onClick: () => {
// <a>
const link = document.createElement('a');
link.href = msg.content.url; //
link.download = msg.content.filename; //
document.body.append(link); // <a>
link.click(); //
link.remove(); // <a>
},
},
'文档下载',
),
]),
});
}
});
}
pptMessage.value = newVal[1]?.content;
// if (newVal && newVal.length > 0) {
// newVal.forEach((msg) => {
// if (msg.role === 'user') {
// resultItems.value.push({
// key: resultItems.value.length + 1,
// role: msg.role, // 'user' or 'ai'
// content: msg.content,
// footer: msg.footer,
// });
// } else {
// resultItems.value.push({
// key: resultItems.value.length + 1,
// role: msg.role, // 'user' or 'ai'
// content: '',
// footer: h(Flex, null, [
// h(
// Button,
// {
// type: 'primary',
// onClick: () => {
// openPreviewDrawer('right', msg.content.url);
// },
// },
// '',
// ),
// h(
// Button,
// {
// type: 'primary',
// style: {
// marginLeft: '10px',
// },
// onClick: () => {
// // <a>
// const link = document.createElement('a');
// link.href = msg.content.url; //
// link.download = msg.content.filename; //
// document.body.append(link); // <a>
// link.click(); //
// link.remove(); // <a>
// },
// },
// '',
// ),
// ]),
// });
// }
// });
// }
},
{ deep: true },
);
@ -267,29 +350,44 @@ watch(
</template>
</Sender>
</div>
<Space
v-if="resultItems.length === 0"
direction="vertical"
size:16
style="width: 60%; padding: 20px 0 0 12px"
>
<Space direction="vertical" size:16 class="chat-right">
<!-- 思维链 -->
<ThoughtChain size="large" :items="chainItems">
<ThoughtChainItem
v-for="item in chainItems"
:key="item.key"
style="background: #0d0d10"
/>
</ThoughtChain>
<Welcome
v-if="chainItems.length <= 0 && !pptMessage"
variant="borderless"
icon="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*s5sNRo5LjfQAAAAAAAAAAAAADgCCAQ/fmt.webp"
title="欢迎使用PPT自动生成"
description="请选择模板列表中需要生成文件的模板,输入参数后开始生成生成文件。"
/>
<ADivider v-if="pptMessage && chainItems.length > 0" />
<Card
v-if="pptMessage"
title="PPT"
class="w-5/12 cursor-pointer hover:shadow-xl"
style="background: transparent"
>
<CardHeader>
<CardTitle class="flex-start flex text-xl">
<VbenIcon :icon="SvgPPT" class="size-8 flex-shrink-0" />
PPT已生成,有效时间五分钟
</CardTitle>
</CardHeader>
<CardContent class="flex items-center justify-around">
<Button @click="() => openPreviewDrawer('right', pptMessage)">
预览
</Button>
<Button @click="downloadPpt(pptMessage)">下载</Button>
</CardContent>
</Card>
</Space>
<!-- 🌟 消息列表 -->
<Bubble.List
v-else
style="width: 60%; padding: 12px"
variant="shadow"
:typing="true"
:items="resultItems"
:roles="roles"
/>
</div>
</div>
</template>
@ -303,7 +401,6 @@ watch(
flex-direction: column;
padding: 0;
margin: 0 auto;
background: hsl(216deg 21.74% 95.49%);
font-family: AlibabaPuHuiTi, v-deep(var(--ant-font-family)), sans-serif;
border-radius: v-deep(var(--ant-border-radius));
}
@ -327,6 +424,14 @@ watch(
padding: 12px 12px 0 12px;
}
.chat-right {
width: 60%;
height: auto;
margin: 0 0 0 10px;
padding: 20px;
background: rgb(255, 255, 255);
}
.placeholder {
padding-top: 32px;
text-align: left;

View File

@ -292,7 +292,8 @@ watch(
<CardTitle class="text-lg">{{ title }}</CardTitle>
</CardHeader>
<CardContent class="flex flex-wrap pt-0">
{{ props.item ? props.item.description : '请选择左侧列表' }}
<!-- {{ props.item ? props.item.description : 'https://www.gzggzy.cn/' }}-->
https://www.gzggzy.cn/
</CardContent>
<CardFooter class="flex justify-end">
<Button :disabled="!item" type="primary" @click="startFetching">