<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: () => { return null; }, 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 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: 3, }); const res = await props.runSpider({ publish_start_time, publish_end_time, llm_api_key: props.item.id, }); if (res.code !== 0) { isFetching.value = false; fetchResult.value = ''; message.error('抓取无结果'); return; } let statusPollingInterval: null | number = null; 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 = `/static/${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', safeFilename); }, }, '文档预览', ), h( Button, { size: 'normal', type: 'primary', style: { marginLeft: '10px' }, onClick: () => { const link = document.createElement('a'); link.href = download_url; link.download = safeFilename; 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}`); } }; statusPollingInterval = window.setInterval(startPolling, 1000); // 组件卸载时清理定时器(需配合 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: { publish_start_time, publish_end_time, }, }, ); if (res.data.outputs.files) { // 保存抓取结果 fetchResult.value = res.data.outputs.files[0].url; notification.success({ message: '获取成功', duration: 3, }); const fileUrl = ref(''); fileUrl.value = `http://47.112.173.8:6800${fetchResult.value}`; // 保存抓取结果 url http://47.112.173.8:6802/static/66f3cfd95e364a239d8036390db658ae.docx resultItems.value.push({ key: resultItems.value.length + 1, role: 'ai', content: '文档已生成', footer: h(Flex, null, [ // h( // Button, // { // size: 'nomarl', // type: 'primary', // onClick: () => { // openPreviewDrawer('right', fetchResult); // }, // }, // '文档预览', // ), h( Button, { size: 'normal', type: 'primary', style: { marginLeft: '10px', }, onClick: () => { // 创建隐藏的 <a> 标签用于触发下载 const link = document.createElement('a'); link.href = fileUrl; // 设置下载链接 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; } // try { // notification.info({ // message: '正在获取中...', // duration: 3, // }); // const res = await props.runSpider( // { // publish_start_time, // publish_end_time, // llm_api_key: '77c068fd-d5b6-4c33-97d8-db5511a09b26', // }, // ); // if (res.data.outputs.result) { // // 保存抓取结果 // fetchResult.value = res.data.outputs.result; // notification.success({ // message: '获取成功', // duration: 3, // }); // const { result } = res.data.outputs; // // const filename = ref(''); // // if (result && isDocxURL(result)) { // filename.value = extractDocxFilename(result); // } // // // 保存抓取结果 url http://47.112.173.8:6802/static/66f3cfd95e364a239d8036390db658ae.docx // fetchResult.value = `/static/${filename.value}`; // resultItems.value.push({ // key: resultItems.value.length + 1, // role: 'ai', // content: '文档已生成', // footer: h(Flex, null, [ // h( // Button, // { // size: 'nomarl', // 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 = filename; // 设置下载文件名 // document.body.append(link); // 将 <a> 标签添加到页面中 // link.click(); // 触发点击事件开始下载 // link.remove(); // 下载完成后移除 <a> 标签 // }, // }, // '文档下载', // ), // ]), // }); // fetchStatus.value = 'completed'; // } else { // fetchResult.value = ''; // message.error('抓取无结果'); // } // } catch (error) { // message.error(`${error}`); // } }; // 列表角色 const roles: BubbleListProps['roles'] = { user: { placement: 'end', typing: false, avatar: { icon: h(UserOutlined), style: { background: '#87d068' } }, }, ai: { placement: 'start', typing: false, style: { maxWidth: 600, marginInlineEnd: 44, }, styles: { 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(''); // 监听 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 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>