html2canvas

This commit is contained in:
mzhang93 2026-05-04 00:40:39 +08:00
parent 57eb948d37
commit 2cc477dac9
4 changed files with 215 additions and 10 deletions

50
package-lock.json generated
View File

@ -18,7 +18,9 @@
"@capgo/camera-preview": "^8.3.1",
"@ionic/vue": "^8.0.0",
"@ionic/vue-router": "^8.0.0",
"html2canvas": "^1.4.1",
"ionicons": "^7.0.0",
"silly-datetime": "^0.1.2",
"vant": "^4.9.24",
"vconsole": "^3.15.1",
"vue": "^3.3.0",
@ -35,7 +37,6 @@
"eslint-plugin-vue": "^9.9.0",
"jsdom": "^22.1.0",
"postcss-px-to-viewport": "^1.1.1",
"silly-datetime": "^0.1.2",
"terser": "^5.4.0",
"typescript": "~5.9.0",
"vite": "^5.0.0",
@ -4023,6 +4024,14 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
@ -4581,6 +4590,14 @@
"node": ">= 8"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
@ -5866,6 +5883,18 @@
"node": ">=12"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/http-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
@ -7972,8 +8001,7 @@
"node_modules/silly-datetime": {
"version": "0.1.2",
"resolved": "https://registry.npmmirror.com/silly-datetime/-/silly-datetime-0.1.2.tgz",
"integrity": "sha512-q8hnO91rRvQsYTYaZCJc6UpljzfdmWD3bNljDLKGVBT2ukj7snE+ENkVVkXfo529ABLEBeN6PHoEaT1ONEq81w==",
"dev": true
"integrity": "sha512-q8hnO91rRvQsYTYaZCJc6UpljzfdmWD3bNljDLKGVBT2ukj7snE+ENkVVkXfo529ABLEBeN6PHoEaT1ONEq81w=="
},
"node_modules/sisteransi": {
"version": "1.0.5",
@ -8261,6 +8289,14 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz",
@ -8593,6 +8629,14 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz",

View File

@ -23,12 +23,13 @@
"@capgo/camera-preview": "^8.3.1",
"@ionic/vue": "^8.0.0",
"@ionic/vue-router": "^8.0.0",
"html2canvas": "^1.4.1",
"ionicons": "^7.0.0",
"silly-datetime": "^0.1.2",
"vant": "^4.9.24",
"vconsole": "^3.15.1",
"vue": "^3.3.0",
"vue-router": "^4.2.0",
"silly-datetime": "^0.1.2"
"vue-router": "^4.2.0"
},
"devDependencies": {
"@capacitor/cli": "8.3.0",

View File

@ -5,10 +5,13 @@
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
title: string
text: string
}>();
withDefaults(
defineProps<{
title: string;
text?: string;
}>(),
{ text: '' },
);
</script>
<style scoped lang="scss">
.tips{

View File

@ -5,7 +5,7 @@
<div class="title">
我的检测报告
</div>
<div class="r" @click="router.replace('/')"><img src="@/assets/close.png" alt=""></div>
<div class="r" @click="router.replace('/')"><img ref="closeBtnEl" src="@/assets/close.png" alt=""></div>
</div>
<div class="time"> 检测时间<span>{{ creatTime }}</span></div>
@ -209,11 +209,155 @@ import Tips from '@/components/Tips/index.vue';
import { useRouter } from 'vue-router';
import { onBeforeUnmount, onMounted, ref, watch, nextTick } from 'vue';
import { format } from 'silly-datetime';
import html2canvas from 'html2canvas';
import Item from '@/components/Item/index.vue';
import content5Status1 from '@/assets/images/content5-status1.png';
import content5Status2 from '@/assets/images/content5-status2.png';
import content5Status3 from '@/assets/images/content5-status3.png';
const router = useRouter();
/** 报告页整页截图上传 */
const REPORT_UPLOAD_URL = 'https://webapi.hbnews.net:8000/gateway/coupon/common/upload';
const REPORT_IMAGE_BASE = 'https://webapi.hbnews.net:8000/gateway/coupon';
/** 最近一次截图的 blob: URL上传结束后仍会保留勿在 finally 里立刻 revoke否则新标签页打不开 */
let lastScreenshotBlobUrl: string | null = null;
function revokeLastScreenshotBlobUrl() {
if (lastScreenshotBlobUrl) {
URL.revokeObjectURL(lastScreenshotBlobUrl);
lastScreenshotBlobUrl = null;
}
}
function buildReportImageUrl(fileName: string): string {
if (!fileName) return '';
if (/^https?:\/\//i.test(fileName)) return fileName;
const base = REPORT_IMAGE_BASE.replace(/\/$/, '');
const path = fileName.replace(/^\//, '');
return `${base}/${path}`;
}
function extractUploadFileName(payload: unknown): string | null {
if (!payload || typeof payload !== 'object') return null;
const o = payload as Record<string, unknown>;
if (typeof o.fileName === 'string' && o.fileName) return o.fileName;
const data = o.data;
if (data && typeof data === 'object' && typeof (data as Record<string, unknown>).fileName === 'string') {
const fn = (data as Record<string, unknown>).fileName;
return typeof fn === 'string' && fn ? fn : null;
}
const result = o.result;
if (result && typeof result === 'object' && typeof (result as Record<string, unknown>).fileName === 'string') {
const fn = (result as Record<string, unknown>).fileName;
return typeof fn === 'string' && fn ? fn : null;
}
return null;
}
/** 截取当前报告根节点长图并 multipart 上传,返回可访问的图片完整 URL */
async function captureReportPageAndUpload(): Promise<string | null> {
console.log('[报告截图] 开始生成');
const el = rootEl.value;
if (!el) {
console.warn('[报告截图] rootEl 为空,跳过');
return null;
}
const container = scrollContainer.value;
let prevScroll = 0;
if (container === window) {
prevScroll = window.scrollY;
window.scrollTo(0, 0);
} else if (container instanceof HTMLElement) {
prevScroll = container.scrollTop;
container.scrollTop = 0;
}
await nextTick();
drawRadarChart();
await new Promise<void>((resolve) => {
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
});
const tabsNode = tabsEl.value;
const closeNode = closeBtnEl.value;
tabsNode?.classList.add('step4-capture-hide');
closeNode?.classList.add('step4-capture-hide');
await nextTick();
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
try {
const canvas = await html2canvas(el, {
scale: Math.min(window.devicePixelRatio || 1, 2),
useCORS: true,
allowTaint: false,
backgroundColor: '#F3F3F3',
width: el.scrollWidth,
height: el.scrollHeight,
windowWidth: el.scrollWidth,
windowHeight: el.scrollHeight,
scrollX: 0,
scrollY: 0,
logging: false,
});
const blob = await new Promise<Blob>((resolve, reject) => {
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('截图生成失败'))), 'image/png', 0.92);
});
revokeLastScreenshotBlobUrl();
lastScreenshotBlobUrl = URL.createObjectURL(blob);
console.log('报告截图临时:', lastScreenshotBlobUrl);
const previewWin = window.open(lastScreenshotBlobUrl, '_blank', 'noopener,noreferrer');
if (!previewWin) {
console.warn('弹窗被拦截时,请手动将控制台里的 blob: 整段地址粘贴到新标签页地址栏。');
}
if (!sessionStorage.getItem('step2_ark_result')) {
console.info('[报告截图] 无 step2_ark_result仅生成临时 blob跳过上传');
return null;
}
const fd = new FormData();
fd.append('file', blob, `health-report-${Date.now()}.png`);
const res = await fetch(REPORT_UPLOAD_URL, {
method: 'POST',
body: fd,
});
const text = await res.text();
let json: unknown;
try {
json = text ? JSON.parse(text) : null;
} catch {
json = null;
}
if (!res.ok) {
console.error('报告截图上传失败', res.status, text);
return null;
}
const fileName = extractUploadFileName(json);
if (!fileName) {
console.error('报告截图上传响应无 fileName', json);
return null;
}
return buildReportImageUrl(fileName);
} catch (e) {
console.error('报告截图或上传异常', e);
return null;
} finally {
tabsNode?.classList.remove('step4-capture-hide');
closeNode?.classList.remove('step4-capture-hide');
if (container === window) window.scrollTo(0, prevScroll);
else if (container instanceof HTMLElement) container.scrollTop = prevScroll;
}
}
const creatTime = ref(new Date().toLocaleString());
const data = ref<any>({});
const summaryText = ref('');
@ -259,6 +403,7 @@ const activeTab = ref(1);
const rootEl = ref<HTMLElement | null>(null);
const tabsEl = ref<HTMLElement | null>(null);
const closeBtnEl = ref<HTMLElement | null>(null);
const chartWrapEl = ref<HTMLElement | null>(null);
const radarCanvasRef = ref<HTMLCanvasElement | null>(null);
@ -494,6 +639,13 @@ onMounted(() => {
radarRo.observe(chartWrapEl.value);
}
window.addEventListener('resize', drawRadarChart);
// blob step2_ark_result
window.setTimeout(() => {
captureReportPageAndUpload().then((url) => {
if (url) console.log('报告截图已上传:', url);
});
}, 800);
});
watch(
@ -503,6 +655,7 @@ watch(
);
onBeforeUnmount(() => {
revokeLastScreenshotBlobUrl();
const container = scrollContainer.value;
if (container === window) window.removeEventListener('scroll', onScroll);
else container.removeEventListener('scroll', onScroll);
@ -551,6 +704,10 @@ if (arkResult) {
}
</script>
<style scoped lang="scss">
.step4-capture-hide {
display: none !important;
}
.step4 {
background: #F3F3F3;
min-height: 100vh;