vue-vben-admin/packages/effects/common-ui/src/ui/spider/spider-work-view.vue
vertoryao 6679761b9e refactor(@vben/web-antd): 更新 dayjs 依赖并优化蜘蛛工作视图下载链接
- 将 dayjs 依赖从具体版本更新为目录版本,提高兼容性
- 修改蜘蛛工作视图中的下载链接,使用相对路径替代固定 IP 地址
2025-05-19 12:08:09 +08:00

526 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import type { BubbleListProps } from 'ant-design-x-vue';
import type { DrawerPlacement } from '@vben-core/popup-ui';
import type { SpiderItem } from './typing';
import { h, ref, watch } from 'vue';
import { useVbenDrawer } from '@vben-core/popup-ui';
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from '@vben-core/shadcn-ui';
import { UserOutlined } from '@ant-design/icons-vue';
import {
Button,
Flex,
message,
notification,
RangePicker,
Space,
} from 'ant-design-vue';
import { Attachments, Bubble, Welcome } from 'ant-design-x-vue';
import dayjs, { Dayjs } from 'dayjs';
import SpiderPreview from './spider-preview.vue';
interface SpiderParams {
appid: string;
}
interface SpiderContext {
userId: string;
conversationId: string;
files: [];
inputs: Record<string, unknown>;
}
interface SpiderResult {
data: {
outputs: {
result: string;
};
};
}
interface ResultItem {
key: number;
role: 'ai' | 'user';
content: string;
footer?: any;
}
interface Props {
itemMessage?: ResultItem;
item?: SpiderItem;
title: string;
runSpider?: (context: any) => Promise<SpiderResult>;
getSpiderStatus?: () => Promise<{
status: string;
}>;
stopSpider?: () => Promise<{
status: string;
}>;
sendWorkflow?: (params: SpiderParams, data: SpiderContext) => Promise<any>;
}
defineOptions({
name: 'SpiderWorkView',
});
const props = withDefaults(defineProps<Props>(), {
itemMessage: () => null,
item: () => ({
id: '',
name: '',
url: '',
}),
runSpider: () => async () => ({
msg: '',
code: '',
}),
getSpiderStatus: () => async () => ({
msg: '',
code: '',
}),
stopSpider: () => async () => ({
msg: '',
code: '',
}),
sendWorkflow: () => async () => ({
outputs: {
result: '',
files: [],
},
}),
});
const resultItems = ref<ResultItem[]>([]);
const [PreviewDrawer, previewDrawerApi] = useVbenDrawer({
// 连接抽离的组件
connectedComponent: SpiderPreview,
// placement: 'left',
});
function openPreviewDrawer(
placement: DrawerPlacement = 'right',
filename?: string,
) {
const fileData = filename.value;
previewDrawerApi.setState({ placement }).setData(fileData).open();
}
const selectedDateRange = ref<[Dayjs, Dayjs]>([
dayjs('2025-05-05'),
dayjs('2025-05-07'),
]);
// 判断是否是以 .docx 结尾的 URL
function isDocxURL(str: string): boolean {
return str.endsWith('.docx');
}
// 使用正则提取 /static/ 后面的文件名部分
function extractDocxFilename(url: string): null | string {
const match = url.match(/\/static\/([^/]+\.docx)$/i); // 匹配 /static/ 后任意非斜杠字符加 .docx
return match ? match[1] : null;
}
const startChecking = () => {
statusPollingInterval = window.setInterval(startPolling, 1000);
};
let statusPollingInterval: null | number;
let pollingCount = 0;
const maxPollingAttempts = 300; // 最多轮询300秒
const startPolling = async () => {
try {
const response = await props.getSpiderStatus();
if (!response.data.is_running) {
clearInterval(statusPollingInterval!);
statusPollingInterval = null;
if (response.data.download_url) {
const download_url = response.data.download_url;
fetchResult.value = download_url;
notification.success({ message: '获取成功', duration: 3 });
let filename = '';
if (isDocxURL(download_url)) {
filename = extractDocxFilename(download_url);
}
fetchResult.value = `/spider/${filename}`;
resultItems.value.push({
key: resultItems.value.length + 1,
role: 'ai',
content: '文档已生成',
footer: h(Flex, null, [
h(
Button,
{
size: 'normal',
type: 'primary',
onClick: () => {
openPreviewDrawer('right', fetchResult);
},
},
'文档预览',
),
h(
Button,
{
size: 'normal',
type: 'primary',
style: { marginLeft: '10px' },
onClick: () => {
const link = document.createElement('a');
link.href = `/spider${download_url}`;
link.download = fetchResult.value;
document.body.append(link);
link.click();
link.remove();
},
},
'文档下载',
),
]),
});
isFetching.value = false;
fetchStatus.value = 'completed';
} else {
isFetching.value = false;
fetchResult.value = '';
message.error('抓取无结果');
}
} else if (++pollingCount >= maxPollingAttempts) {
clearInterval(statusPollingInterval!);
statusPollingInterval = null;
props.stopSpider();
message.warn('轮询超时,请稍后再试');
}
} catch (error) {
clearInterval(statusPollingInterval!);
statusPollingInterval = null;
message.error(`状态查询失败:${error}`);
}
};
const startFetching = async () => {
// 更新状态为“抓取中”
isFetching.value = true;
fetchStatus.value = 'fetching';
let publish_start_time = ''; // 默认值
let publish_end_time = ''; // 默认值
if (selectedDateRange.value && selectedDateRange.value.length === 2) {
publish_start_time =
dayjs(selectedDateRange.value[0]).format('YYYYMMDDhhmmss') || '';
publish_end_time =
dayjs(selectedDateRange.value[1]).format('YYYYMMDDhhmmss') || '';
}
if (props.item.id === '77c068fd-d5b6-4c33-97d8-db5511a09b26') {
try {
notification.info({
message: '正在获取中...',
duration: 5,
});
const res = await props.runSpider({
publish_start_time,
publish_end_time,
llm_api_key: props.item.id,
});
if (res.code === 0) {
notification.info({
message: res.msg,
duration: 5,
});
startChecking();
} else {
isFetching.value = false;
fetchResult.value = '';
message.error('抓取无结果');
return;
}
// 组件卸载时清理定时器(需配合 onUnmounted 生命周期)
// 示例:
// onUnmounted(() => {
// if (statusPollingInterval) {
// clearInterval(statusPollingInterval);
// }
// });
} catch (error) {
isFetching.value = false;
fetchResult.value = '';
console.error(error);
}
}
if (props.item.id === 'c736edd0-925d-4877-9223-56aab7342311') {
try {
notification.info({
message: '正在获取中...',
duration: 3,
});
const res = await props.sendWorkflow(
{
appid: props.item.id,
},
{
userId: '1562',
conversationId: '',
files: [],
inputs: {},
},
);
if (res.data.outputs.files) {
// 保存抓取结果
fetchResult.value = res.data.outputs.files[0].url;
notification.success({
message: '获取成功',
duration: 3,
});
const fileUrl = ref('');
fileUrl.value = `/guangzhou${fetchResult.value}`;
resultItems.value.push({
key: resultItems.value.length + 1,
role: 'ai',
content: '文档已生成有效时间5分钟请及时查看或下载',
footer: h(Flex, null, [
h(
Button,
{
size: 'nomarl',
type: 'primary',
onClick: () => {
openPreviewDrawer('right', fileUrl);
},
},
'文档预览',
),
h(
Button,
{
size: 'normal',
type: 'primary',
style: {
marginLeft: '10px',
},
onClick: () => {
// 创建隐藏的 <a> 标签用于触发下载
const link = document.createElement('a');
link.href = fileUrl.value; // 设置下载链接
link.download = '广州公共资源交易中心数据获取'; // 设置下载文件名
document.body.append(link); // 将 <a> 标签添加到页面中
link.click(); // 触发点击事件开始下载
link.remove(); // 下载完成后移除 <a> 标签
},
},
'文档下载',
),
]),
});
fetchStatus.value = 'completed';
} else {
fetchResult.value = '';
message.error('抓取无结果');
}
} catch (error) {
message.error(`${error}`);
}
isFetching.value = false;
}
};
// 列表角色
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 isFetching = ref(false);
const fetchResult = ref('');
const fetchStatus = ref('');
// 监听 title 变化并更新 resultItems
watch(
() => props.item,
() => {
resultItems.value = [];
},
);
// 监听 itemMessage 变化并更新 resultItems
watch(
() => props.itemMessage,
(newVal) => {
resultItems.value = [];
if (newVal && newVal.length > 0) {
newVal.forEach((msg) => {
// resultItems.value.push({
// key: resultItems.value.length + 1,
// role: msg.role, // 'user' or 'ai'
// content: msg.content,
// footer: msg.footer,
// });
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,
// {
// size: 'normal',
// type: 'primary',
// onClick: () => {
// openPreviewDrawer('right', filename);
// },
// },
// '文档预览',
// ),
h(
Button,
{
size: 'normal',
type: 'primary',
style: {
marginLeft: '10px',
},
onClick: () => {
// 创建隐藏的 <a> 标签用于触发下载
const link = document.createElement('a');
link.href = msg.content; // 设置下载链接
link.download = '广州公共资源交易中心数据获取'; // 设置下载文件名
document.body.append(link); // 将 <a> 标签添加到页面中
link.click(); // 触发点击事件开始下载
link.remove(); // 下载完成后移除 <a> 标签
},
},
'文档下载',
),
]),
});
}
});
}
},
{ deep: true },
);
</script>
<template>
<!-- style="flex-direction: column"-->
<div
class="flex h-full"
style="flex-direction: column; width: 70%; padding-top: 20px"
>
<PreviewDrawer />
<!-- <PreviewDrawer />-->
<Space
v-if="resultItems.length === 0"
direction="vertical"
size:16
style="flex: 1"
>
<Welcome
variant="borderless"
icon="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*s5sNRo5LjfQAAAAAAAAAAAAADgCCAQ/fmt.webp"
title="欢迎使用AI平台信息获取"
description="请选择数据信息列表中需要获取信息的链接,选取时间后开始爬取获取信息。"
/>
</Space>
<Bubble.List
v-else
variant="shadow"
:typing="true"
:items="resultItems"
:roles="roles"
style="flex: 1"
/>
<Card class="w-full self-end" v-loading="isFetching">
<CardHeader class="py-4">
<CardTitle class="text-lg">{{ title }}</CardTitle>
</CardHeader>
<CardContent class="flex flex-wrap pt-0">
{{ item ? item.url : '请选择左侧列表' }}
</CardContent>
<CardFooter class="flex justify-end">
<RangePicker
v-if="item.id === '77c068fd-d5b6-4c33-97d8-db5511a09b26'"
class="mx-2"
v-model:value="selectedDateRange"
format="YYYY/MM/DD"
/>
<Button
:disabled="!item || selectedDateRange.length < 2"
type="primary"
@click="startFetching"
>
{{ isFetching ? '抓取中...' : '开始抓取' }}
</Button>
</CardFooter>
</Card>
</div>
</template>