feat(property-marketing): 优化投放指南下载功能并重构PDF生成逻辑

This commit is contained in:
林志军
2025-07-09 14:24:09 +08:00
parent ae8aa083de
commit 5c73fdea86
4 changed files with 123 additions and 63 deletions

View File

@ -21,7 +21,7 @@
</a-popconfirm> </a-popconfirm>
</a-space> </a-space>
<a-space> <a-space>
<a-button type="outline" @click="downloadDetailAsImage(record.id)" class="operation-btn">下载</a-button> <a-button type="outline" @click="downLoad(record.file_url)" class="operation-btn">下载</a-button>
<a-button type="outline" @click="goDetail(record.id)" class="operation-btn">详情</a-button> <a-button type="outline" @click="goDetail(record.id)" class="operation-btn">详情</a-button>
</a-space> </a-space>
</a-space> </a-space>
@ -82,32 +82,24 @@ const goDetail = async (id) => {
router.push(`/put-account/detail/${id}`); router.push(`/put-account/detail/${id}`);
}; };
const downloadDetailAsImage = (id) => { const downLoad = (fileUrl) => {
const url = `/put-account/detail/${id}`; if (isEmpty(fileUrl)) {
const win = window.open(url, '_blank'); Message.error('下载失败,文件不存在');
return;
win.onload = () => { }
setTimeout(() => { const link = document.createElement('a');
html2canvas(win.document.body, { link.href = fileUrl;
useCORS: true, link.download = '投放指南.pdf';
scale: 2, document.body.appendChild(link);
}).then((canvas) => { link.click();
const imgData = canvas.toDataURL('image/png'); document.body.removeChild(link);
const link = document.createElement('a');
link.href = imgData;
link.download = `详情页面_${Date.now()}.png`;
link.click();
win.close(); // 关闭新窗口
});
}, 2000); // 等待页面加载
};
}; };
const deleteData = async (id) => { const deleteData = async (id) => {
const { code, message } = await deleteHistorylog(id); const { code, message } = await deleteHistorylog(id);
if (code == 200) { if (code == 200) {
Message.success(message); Message.success(message);
emits('onSearch'); emits('onSearch');
console.log('onsearch') console.log('onsearch');
} }
}; };

View File

@ -50,7 +50,7 @@
</div> </div>
<div class="filter-row-item flex items-center"> <div class="filter-row-item flex items-center">
<a-button class="w-84px search-btn mr-12px" size="medium" @click="handleSearch"> <a-button class="w-84px search-btn mr-12px" :disabled="disabled" size="medium" @click="handleSearch">
<template #icon> <template #icon>
<icon-search /> <icon-search />
</template> </template>

View File

@ -54,8 +54,7 @@
<template #platform="{ record }"> <template #platform="{ record }">
<a-space size="medium" v-if="record.platform"> <a-space size="medium" v-if="record.platform">
<img :src="PLATFORM_LIST[record.platform]?.icon" width="19" class="mr-4px" />
<span>{{ PLATFORM_LIST[record.platform].label }}</span>
</a-space> </a-space>
</template> </template>

View File

@ -5,7 +5,7 @@
<a-tabs <a-tabs
v-model:activeKey="tabData" v-model:activeKey="tabData"
@tab-click="onSearch" @tab-click="onSearch"
class="a-tab-class" class="a-tab-class ignore-export"
default-active-key="placement_guide" default-active-key="placement_guide"
> >
<a-tab-pane key="placement_guide" title="投放指南"></a-tab-pane> <a-tab-pane key="placement_guide" title="投放指南"></a-tab-pane>
@ -16,6 +16,7 @@
</div> </div>
<!--表单组件搜索--> <!--表单组件搜索-->
<listSearchForm <listSearchForm
class="ignore-export"
@onReset="handleReset" @onReset="handleReset"
v-model:query="query" v-model:query="query"
@onSearch="onSearch" @onSearch="onSearch"
@ -43,18 +44,21 @@
/> />
</div> </div>
</div> </div>
<a-spin v-if="tabData === 'placement_guide'" :loading="loading" tip="AI分析中...." wrapperClassName="custom-spin-wrapper"> <a-spin
<div > v-show="tabData === 'placement_guide'"
:loading="loading"
tip="AI分析中...."
wrapperClassName="custom-spin-wrapper"
>
<div>
<MonthData :overview="aiResult.overview"></MonthData> <MonthData :overview="aiResult.overview"></MonthData>
<!-- 投放建议--> <!-- 投放建议-->
<PlacementSuggestions :optimization="aiResult.optimization"></PlacementSuggestions> <PlacementSuggestions :optimization="aiResult.optimization"></PlacementSuggestions>
<!-- 投放行动指南--> <!-- 投放行动指南-->
<ActionGuideDistribution :action_guide="aiResult.action_guide"></ActionGuideDistribution> <!-- <ActionGuideDistribution :action_guide="aiResult.action_guide"></ActionGuideDistribution>-->
</div> </div>
</a-spin> </a-spin>
<div v-if="tabData == 'placement_guide'"> <div v-if="tabData == 'placement_guide'" class="ignore-export">
<a-space class="down-btn"> <a-space class="down-btn">
<a-button type="outline" @click="downPage"> <a-button type="outline" @click="downPage">
<template #icon> <template #icon>
@ -89,7 +93,10 @@ import {
} from '@/api/all/propertyMarketing'; } from '@/api/all/propertyMarketing';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import html2canvas from 'html2canvas'; import html2canvas from 'html2canvas';
import { jsPDF } from 'jspdf';
import { AiResultStatus } from '@/views/property-marketing/put-account/investment-guidelines/constants'; import { AiResultStatus } from '@/views/property-marketing/put-account/investment-guidelines/constants';
import { fetchUploadFile } from '@/api/all';
import axios from 'axios';
const tabData = ref('placement_guide'); const tabData = ref('placement_guide');
@ -121,6 +128,7 @@ const handleUpdateQuery = (payload) => {
query.sort_column = payload.column; query.sort_column = payload.column;
query.sort_order = payload.order; query.sort_order = payload.order;
isGetAi.value = false; isGetAi.value = false;
loading.value = false;
onSearch(); onSearch();
}; };
@ -143,11 +151,10 @@ const onSearch = async () => {
guideHistoryList.value = result?.data?.data || []; guideHistoryList.value = result?.data?.data || [];
} }
listData.total = result.data.total; listData.total = result.data.total;
if (placementGuideList.value.length > 0) { if (placementGuideList.value.length > 0 && isGetAi.value) {
loading.value = true; loading.value = true;
if (isGetAi.value) { syncGetAiResult();
startTask(); startTask();
}
} }
isGetAi.value = true; isGetAi.value = true;
}; };
@ -159,15 +166,14 @@ const aiResult = reactive({
// 下载当前页面 // 下载当前页面
const downPage = async () => { const downPage = async () => {
await nextTick(); // 确保 DOM 更新完成 await nextTick();
html2canvas(document.querySelector('.guidelines-data-wrap')).then((canvas) => { const fileUrl = await uploadPdf();
const imgData = canvas.toDataURL('image/png'); const link = document.createElement('a');
const link = document.createElement('a'); link.href = fileUrl;
link.href = imgData; link.download = '投放指南.pdf';
const timestamp = new Date().getTime(); document.body.appendChild(link);
link.download = `投放指南-${timestamp}.png`; link.click();
link.click(); document.body.removeChild(link);
});
}; };
const saveForm = reactive({ const saveForm = reactive({
account: [], account: [],
@ -175,33 +181,48 @@ const saveForm = reactive({
platform: [], platform: [],
aiResult: [], aiResult: [],
code: '', code: '',
file_url: '',
}); });
const timerRef = ref<number | null>(null); const timerRef = ref<number | null>(null);
const startTask = () => { const startTask = () => {
if (timerRef.value !== null) return; if (timerRef.value !== null) return;
timerRef.value = setInterval(async () => { timerRef.value = setInterval(async () => {
try { syncGetAiResult();
const { code, data } = await getAiResult(query);
console.log('定时任务执行结果:', data);
if (data.ai_result_status === AiResultStatus.SUCCESS || data.ai_result_status === AiResultStatus.FAILED) {
stopTask();
console.log('任务已完成,定时器已关闭');
}
if (data.ai_result_status === AiResultStatus.SUCCESS) {
loading.value = false;
aiResult.optimization = data.result?.optimization?.modules || [];
aiResult.action_guide = data.result?.action_guide?.modules || [];
aiResult.overview = data.result?.overview?.content_blocks[0] || [];
}
saveForm.code = data?.code;
console.log(aiResult, 'aiResult');
} catch (error) {
console.error('定时任务执行出错:', error);
stopTask();
}
}, 5000); }, 5000);
}; };
const syncGetAiResult = async () => {
try {
const { code, data } = await getAiResult(query);
saveForm.file_url = '';
console.log('定时任务执行结果:', data);
if (data.ai_result_status === AiResultStatus.SUCCESS || data.ai_result_status === AiResultStatus.FAILED) {
stopTask();
console.log('任务已完成,定时器已关闭');
}
if (data.ai_result_status === AiResultStatus.SUCCESS) {
loading.value = false;
aiResult.optimization = data.result?.optimization?.modules || [];
aiResult.action_guide = data.result?.action_guide?.modules || [];
aiResult.overview = data.result?.overview?.content_blocks[0] || [];
} else if (data.ai_result_status === AiResultStatus.FAILED) {
Message.error('AI分析失败');
aiError();
}
saveForm.code = data?.code;
} catch (error) {
aiError();
}
};
// ai 检测失败执行方法
const aiError = () => {
aiResult.optimization = [];
aiResult.action_guide = [];
aiResult.overview = [];
stopTask();
};
const handleReset = () => { const handleReset = () => {
console.log('handleReset'); console.log('handleReset');
query.page = 1; query.page = 1;
@ -226,6 +247,7 @@ onUnmounted(() => {
stopTask(); stopTask();
}); });
const handleSave = async () => { const handleSave = async () => {
await uploadPdf();
const updatedSaveForm = { const updatedSaveForm = {
...saveForm, ...saveForm,
ai_result: aiResult, ai_result: aiResult,
@ -235,6 +257,52 @@ const handleSave = async () => {
Message.success(message); Message.success(message);
} }
}; };
// 上传pdf文件
const uploadPdf = async () => {
if (saveForm.file_url !== '') {
return saveForm.file_url;
}
const response = await fetchUploadFile({ suffix: 'pdf' });
const preSignedUrl = response?.data?.upload_url;
const fileUrl = response?.data?.file_url;
if (!preSignedUrl) {
throw new Error('未能获取有效的预签名上传地址');
}
saveForm.file_url = fileUrl;
const pdfBlob = await generatePDF(); // 生成 PDF Blob
console.log('pdfBlob', pdfBlob);
await axios.put(preSignedUrl, pdfBlob, {
headers: {
'Content-Type': pdfBlob.type,
},
});
return fileUrl;
};
const generatePDF = async (): Promise<Blob> => {
await nextTick();
const sections = document.querySelectorAll('.guidelines-data-wrap');
const pdf = new jsPDF('p', 'mm', 'a4');
let position = 0;
for (const section of sections) {
if (section.children.length === 0) continue; // 跳过空内容区域
const canvas = await html2canvas(section as HTMLElement, {
ignoreElements: (element) => {
return element.classList.contains('ignore-export'); // 例如:忽略 class 为 ignore-export 的元素
},
});
const imgData = canvas.toDataURL('image/png');
const imgProps = pdf.getImageProperties(imgData);
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width;
pdf.addImage(imgData, 'PNG', 0, position, pdfWidth, pdfHeight);
position += pdfHeight + 10;
}
return pdf.output('blob');
};
onMounted(() => { onMounted(() => {
onSearch(); onSearch();
}); });
@ -242,6 +310,7 @@ onMounted(() => {
<style lang="scss"> <style lang="scss">
@import './style.scss'; @import './style.scss';
.custom-spin-wrapper { .custom-spin-wrapper {
display: block; display: block;
width: 100%; width: 100%;