f
This commit is contained in:
@@ -1,5 +1,17 @@
|
||||
<template>
|
||||
<div class="hy-report">
|
||||
<button
|
||||
v-if="showToolbar"
|
||||
type="button"
|
||||
class="export-fab no-print"
|
||||
:disabled="exporting"
|
||||
:aria-busy="exporting"
|
||||
@click="handleExport"
|
||||
>
|
||||
<span class="export-fab__icon">{{ exporting ? '…' : '↓' }}</span>
|
||||
<span class="export-fab__text">{{ exporting ? '生成中' : '下载 PDF' }}</span>
|
||||
</button>
|
||||
|
||||
<div ref="reportRef" class="hy-container">
|
||||
<ReportHeaderSection :report-time="reportTime" />
|
||||
|
||||
@@ -25,8 +37,8 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { parseRoot, extractReportUrl, REPORT_USAGE_NOTICE } from './reportHelper';
|
||||
import { printReportAsPdf } from './reportExport';
|
||||
import { parseRoot, REPORT_USAGE_NOTICE } from './reportHelper';
|
||||
import { downloadReportAsPdf } from './reportExport';
|
||||
import ReportHeaderSection from './components/ReportHeaderSection.vue';
|
||||
import RiskRatingSection from './components/RiskRatingSection.vue';
|
||||
import RiskOverviewSection from './components/RiskOverviewSection.vue';
|
||||
@@ -44,10 +56,10 @@ const props = defineProps({
|
||||
index: { type: Number, default: 0 },
|
||||
notifyRiskStatus: { type: Function, default: () => {} },
|
||||
reportDateTime: { type: String, default: '' },
|
||||
showToolbar: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const root = computed(() => parseRoot(props.data));
|
||||
const reportUrl = computed(() => extractReportUrl(props.data));
|
||||
const reportRef = ref(null);
|
||||
const exporting = ref(false);
|
||||
|
||||
@@ -58,12 +70,26 @@ const reportTime = computed(
|
||||
async function handleExport() {
|
||||
exporting.value = true;
|
||||
try {
|
||||
await printReportAsPdf(reportRef.value);
|
||||
await downloadReportAsPdf(reportRef.value, {
|
||||
filename: `${props.apiId}_海宇个人风险报告_${formatFileDate(new Date())}.pdf`,
|
||||
});
|
||||
} finally {
|
||||
exporting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatFileDate(date) {
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
return [
|
||||
date.getFullYear(),
|
||||
pad(date.getMonth() + 1),
|
||||
pad(date.getDate()),
|
||||
pad(date.getHours()),
|
||||
pad(date.getMinutes()),
|
||||
pad(date.getSeconds()),
|
||||
].join('');
|
||||
}
|
||||
|
||||
defineExpose({ reportRef, handleExport });
|
||||
</script>
|
||||
|
||||
@@ -85,12 +111,70 @@ defineExpose({ reportRef, handleExport });
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.export-fab {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 32px;
|
||||
z-index: 1000;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 18px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: #0a6e8e;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 16px rgba(10, 110, 142, 0.35);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(10, 110, 142, 0.45);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.72;
|
||||
cursor: wait;
|
||||
}
|
||||
}
|
||||
|
||||
.export-fab__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.export-fab__text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.main-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.export-fab {
|
||||
right: 16px;
|
||||
bottom: 20px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.export-fab__text {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
|
||||
@@ -1,18 +1,109 @@
|
||||
import html2canvas from 'html2canvas';
|
||||
import { jsPDF } from 'jspdf';
|
||||
|
||||
const A4_WIDTH_MM = 210;
|
||||
const A4_HEIGHT_MM = 297;
|
||||
const PAGE_MARGIN_MM = 8;
|
||||
|
||||
/**
|
||||
* 通过浏览器打印对话框导出 PDF(目标打印机选「另存为 PDF」)。
|
||||
* 将报告 DOM 渲染为 PDF 文件并触发浏览器下载。
|
||||
*
|
||||
* @param {HTMLElement} reportElement
|
||||
* @param {{
|
||||
* filename?: string,
|
||||
* beforeCapture?: () => void | Promise<void>,
|
||||
* afterCapture?: () => void,
|
||||
* }} [options]
|
||||
*/
|
||||
export async function printReportAsPdf(reportElement) {
|
||||
export async function downloadReportAsPdf(reportElement, options = {}) {
|
||||
if (!reportElement) return;
|
||||
|
||||
document.body.classList.add('dwbg9fb2-printing');
|
||||
const {
|
||||
filename = `报告_${formatFileDate(new Date())}.pdf`,
|
||||
beforeCapture,
|
||||
afterCapture,
|
||||
} = options;
|
||||
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
if (beforeCapture) {
|
||||
await beforeCapture();
|
||||
}
|
||||
|
||||
const scrollLeft = window.scrollX;
|
||||
const scrollTop = window.scrollY;
|
||||
window.scrollTo(0, 0);
|
||||
await waitForPaint();
|
||||
|
||||
try {
|
||||
window.print();
|
||||
const canvas = await html2canvas(reportElement, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
logging: false,
|
||||
backgroundColor: '#ffffff',
|
||||
scrollX: 0,
|
||||
scrollY: -window.scrollY,
|
||||
windowWidth: document.documentElement.clientWidth,
|
||||
});
|
||||
|
||||
const pdf = new jsPDF({
|
||||
orientation: 'portrait',
|
||||
unit: 'mm',
|
||||
format: 'a4',
|
||||
});
|
||||
|
||||
const contentWidth = A4_WIDTH_MM - PAGE_MARGIN_MM * 2;
|
||||
const contentHeight = A4_HEIGHT_MM - PAGE_MARGIN_MM * 2;
|
||||
const imgWidth = contentWidth;
|
||||
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
||||
const imgData = canvas.toDataURL('image/jpeg', 0.92);
|
||||
|
||||
let heightLeft = imgHeight;
|
||||
let position = 0;
|
||||
|
||||
pdf.addImage(
|
||||
imgData,
|
||||
'JPEG',
|
||||
PAGE_MARGIN_MM,
|
||||
PAGE_MARGIN_MM + position,
|
||||
imgWidth,
|
||||
imgHeight,
|
||||
);
|
||||
heightLeft -= contentHeight;
|
||||
|
||||
while (heightLeft > 0) {
|
||||
position = heightLeft - imgHeight;
|
||||
pdf.addPage();
|
||||
pdf.addImage(
|
||||
imgData,
|
||||
'JPEG',
|
||||
PAGE_MARGIN_MM,
|
||||
PAGE_MARGIN_MM + position,
|
||||
imgWidth,
|
||||
imgHeight,
|
||||
);
|
||||
heightLeft -= contentHeight;
|
||||
}
|
||||
|
||||
pdf.save(filename);
|
||||
} finally {
|
||||
window.setTimeout(() => {
|
||||
document.body.classList.remove('dwbg9fb2-printing');
|
||||
}, 500);
|
||||
window.scrollTo(scrollLeft, scrollTop);
|
||||
if (afterCapture) afterCapture();
|
||||
}
|
||||
}
|
||||
|
||||
function formatFileDate(date) {
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
return [
|
||||
date.getFullYear(),
|
||||
pad(date.getMonth() + 1),
|
||||
pad(date.getDate()),
|
||||
pad(date.getHours()),
|
||||
pad(date.getMinutes()),
|
||||
pad(date.getSeconds()),
|
||||
].join('');
|
||||
}
|
||||
|
||||
function waitForPaint() {
|
||||
return new Promise((resolve) => {
|
||||
requestAnimationFrame(() => requestAnimationFrame(resolve));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
<span class="case-text">{{ caseListText(c) }}</span>
|
||||
<span class="case-arrow">{{ expandedIndex === i ? '∨' : '>' }}</span>
|
||||
</div>
|
||||
<div v-if="expandedIndex === i" class="case-block">
|
||||
<div v-if="isCaseExpanded(i)" class="case-block">
|
||||
<div class="case-info">
|
||||
<div><span>案件类型:</span><span>{{ c.sectionLabel }}</span></div>
|
||||
<div><span>诉讼地位:</span><span>{{ c.n_ssdw }}</span></div>
|
||||
@@ -361,11 +361,23 @@ const allCases = computed(() => {
|
||||
});
|
||||
|
||||
const expandedIndex = ref(null);
|
||||
const expandAllForPrint = ref(false);
|
||||
|
||||
function isCaseExpanded(index) {
|
||||
return expandAllForPrint.value || expandedIndex.value === index;
|
||||
}
|
||||
|
||||
function toggleCase(index) {
|
||||
if (expandAllForPrint.value) return;
|
||||
expandedIndex.value = expandedIndex.value === index ? null : index;
|
||||
}
|
||||
|
||||
function setExpandAllForPrint(expand) {
|
||||
expandAllForPrint.value = expand;
|
||||
}
|
||||
|
||||
defineExpose({ setExpandAllForPrint });
|
||||
|
||||
const dishonestList = computed(() =>
|
||||
extractJudicialList(props.data, [
|
||||
'dishonest',
|
||||
@@ -985,4 +997,16 @@ function partiesText(dsrxx) {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.case-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.case-list-group,
|
||||
.case-block,
|
||||
.module-card {
|
||||
break-inside: avoid-page;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
<template>
|
||||
<div class="gamma-report">
|
||||
<button
|
||||
v-if="showToolbar"
|
||||
type="button"
|
||||
class="export-fab no-print"
|
||||
:disabled="exporting"
|
||||
:aria-busy="exporting"
|
||||
@click="handleExport"
|
||||
>
|
||||
<span class="export-fab__icon">{{ exporting ? '…' : '↓' }}</span>
|
||||
<span class="export-fab__text">{{ exporting ? '生成中' : '下载 PDF' }}</span>
|
||||
</button>
|
||||
|
||||
<div ref="reportRef" class="gamma-container">
|
||||
<ReportHeaderSection :report-time="reportTime" />
|
||||
|
||||
@@ -26,15 +38,15 @@
|
||||
<OverdueSurveySection :data="root.loanRiskTagV10" />
|
||||
<CreditPanoramaSection :data="root.loanRiskTagV21" />
|
||||
<LoanIntentSection :data="root.loanRiskTagV11" />
|
||||
<JudicialCaseSection :data="root.personalLawsuit" />
|
||||
<JudicialCaseSection ref="judicialCaseRef" :data="root.personalLawsuit" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { parseRoot, extractReportUrl } from './reportHelper';
|
||||
import { printReportAsPdf } from './reportExport';
|
||||
import { parseRoot } from './reportHelper';
|
||||
import { downloadReportAsPdf } from './reportExport';
|
||||
import RiskAssessmentSection from './components/RiskAssessmentSection.vue';
|
||||
import RiskSummarySection from './components/RiskSummarySection.vue';
|
||||
import BasicInfoSection from './components/BasicInfoSection.vue';
|
||||
@@ -59,13 +71,40 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const root = computed(() => parseRoot(props.data));
|
||||
const reportUrl = computed(() => extractReportUrl(props.data));
|
||||
const reportRef = ref(null);
|
||||
const judicialCaseRef = ref(null);
|
||||
const exporting = ref(false);
|
||||
|
||||
const reportTime = computed(
|
||||
() => props.reportDateTime || new Date().toLocaleString('zh-CN'),
|
||||
);
|
||||
|
||||
async function handleExport() {
|
||||
exporting.value = true;
|
||||
try {
|
||||
await downloadReportAsPdf(reportRef.value, {
|
||||
filename: `${props.apiId}_海宇贷前风险档案_${formatFileDate(new Date())}.pdf`,
|
||||
beforeCapture: () => judicialCaseRef.value?.setExpandAllForPrint(true),
|
||||
afterCapture: () => judicialCaseRef.value?.setExpandAllForPrint(false),
|
||||
});
|
||||
} finally {
|
||||
exporting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatFileDate(date) {
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
return [
|
||||
date.getFullYear(),
|
||||
pad(date.getMonth() + 1),
|
||||
pad(date.getDate()),
|
||||
pad(date.getHours()),
|
||||
pad(date.getMinutes()),
|
||||
pad(date.getSeconds()),
|
||||
].join('');
|
||||
}
|
||||
|
||||
defineExpose({ reportRef, handleExport });
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -73,58 +112,61 @@ const reportTime = computed(
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.report-toolbar {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto 16px;
|
||||
padding: 12px 20px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.export-fab {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 32px;
|
||||
z-index: 1000;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #8b4513;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
padding: 8px 18px;
|
||||
gap: 8px;
|
||||
padding: 12px 18px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
border-radius: 999px;
|
||||
background: #8b4513;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 16px rgba(139, 69, 19, 0.35);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
&:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(139, 69, 19, 0.45);
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: #fff;
|
||||
color: #8b4513;
|
||||
border: 1px solid #d4af37;
|
||||
&:disabled {
|
||||
opacity: 0.72;
|
||||
cursor: wait;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.report-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
.export-fab__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.export-fab__text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.export-fab {
|
||||
right: 16px;
|
||||
bottom: 20px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.export-fab__text {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,109 @@
|
||||
import html2canvas from 'html2canvas';
|
||||
import { jsPDF } from 'jspdf';
|
||||
|
||||
const A4_WIDTH_MM = 210;
|
||||
const A4_HEIGHT_MM = 297;
|
||||
const PAGE_MARGIN_MM = 8;
|
||||
|
||||
/**
|
||||
* 通过浏览器打印对话框导出 PDF(目标打印机选「另存为 PDF」)。
|
||||
* 零依赖,适合当前纯前端报告查看器;复杂图表场景可改用服务端渲染 PDF。
|
||||
* 将报告 DOM 渲染为 PDF 文件并触发浏览器下载。
|
||||
*
|
||||
* @param {HTMLElement} reportElement
|
||||
* @param {{
|
||||
* filename?: string,
|
||||
* beforeCapture?: () => void | Promise<void>,
|
||||
* afterCapture?: () => void,
|
||||
* }} [options]
|
||||
*/
|
||||
export async function printReportAsPdf(reportElement) {
|
||||
export async function downloadReportAsPdf(reportElement, options = {}) {
|
||||
if (!reportElement) return;
|
||||
|
||||
document.body.classList.add('dwbg9fb3-printing');
|
||||
const {
|
||||
filename = `报告_${formatFileDate(new Date())}.pdf`,
|
||||
beforeCapture,
|
||||
afterCapture,
|
||||
} = options;
|
||||
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
if (beforeCapture) {
|
||||
await beforeCapture();
|
||||
}
|
||||
|
||||
const scrollLeft = window.scrollX;
|
||||
const scrollTop = window.scrollY;
|
||||
window.scrollTo(0, 0);
|
||||
await waitForPaint();
|
||||
|
||||
try {
|
||||
window.print();
|
||||
const canvas = await html2canvas(reportElement, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
logging: false,
|
||||
backgroundColor: '#ffffff',
|
||||
scrollX: 0,
|
||||
scrollY: -window.scrollY,
|
||||
windowWidth: document.documentElement.clientWidth,
|
||||
});
|
||||
|
||||
const pdf = new jsPDF({
|
||||
orientation: 'portrait',
|
||||
unit: 'mm',
|
||||
format: 'a4',
|
||||
});
|
||||
|
||||
const contentWidth = A4_WIDTH_MM - PAGE_MARGIN_MM * 2;
|
||||
const contentHeight = A4_HEIGHT_MM - PAGE_MARGIN_MM * 2;
|
||||
const imgWidth = contentWidth;
|
||||
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
||||
const imgData = canvas.toDataURL('image/jpeg', 0.92);
|
||||
|
||||
let heightLeft = imgHeight;
|
||||
let position = 0;
|
||||
|
||||
pdf.addImage(
|
||||
imgData,
|
||||
'JPEG',
|
||||
PAGE_MARGIN_MM,
|
||||
PAGE_MARGIN_MM + position,
|
||||
imgWidth,
|
||||
imgHeight,
|
||||
);
|
||||
heightLeft -= contentHeight;
|
||||
|
||||
while (heightLeft > 0) {
|
||||
position = heightLeft - imgHeight;
|
||||
pdf.addPage();
|
||||
pdf.addImage(
|
||||
imgData,
|
||||
'JPEG',
|
||||
PAGE_MARGIN_MM,
|
||||
PAGE_MARGIN_MM + position,
|
||||
imgWidth,
|
||||
imgHeight,
|
||||
);
|
||||
heightLeft -= contentHeight;
|
||||
}
|
||||
|
||||
pdf.save(filename);
|
||||
} finally {
|
||||
window.setTimeout(() => {
|
||||
document.body.classList.remove('dwbg9fb3-printing');
|
||||
}, 500);
|
||||
window.scrollTo(scrollLeft, scrollTop);
|
||||
if (afterCapture) afterCapture();
|
||||
}
|
||||
}
|
||||
|
||||
function formatFileDate(date) {
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
return [
|
||||
date.getFullYear(),
|
||||
pad(date.getMonth() + 1),
|
||||
pad(date.getDate()),
|
||||
pad(date.getHours()),
|
||||
pad(date.getMinutes()),
|
||||
pad(date.getSeconds()),
|
||||
].join('');
|
||||
}
|
||||
|
||||
function waitForPaint() {
|
||||
return new Promise((resolve) => {
|
||||
requestAnimationFrame(() => requestAnimationFrame(resolve));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ export const RISK_LEVEL_DESC = [
|
||||
export const RISK_SUMMARY_CATEGORIES = [
|
||||
{ key: 'mobile4Verify', title: '基本信息', icon: '📛' },
|
||||
{ key: 'personalLawsuit', title: '司法案件', icon: '📑' },
|
||||
{ key: 'courtRiskTagV31', title: '限高被执行人', icon: '⚖️' },
|
||||
{ key: 'courtRiskTagV41', title: '失信被执行人', icon: '⚖️' },
|
||||
{ key: 'loanRiskTagV11', title: '借贷意向', icon: '📈' },
|
||||
{ key: 'loanRiskTagV10', title: '逾期勘测V3', icon: '📑' },
|
||||
{ key: 'loanRiskTagV12', title: '借贷行为验证', icon: '📊' },
|
||||
@@ -35,7 +37,7 @@ export function buildRiskSummaryCards(risks = {}) {
|
||||
return {
|
||||
key,
|
||||
title: meta?.title ?? key,
|
||||
icon: meta?.icon ?? '⚠️',
|
||||
icon: meta?.icon ?? '📋',
|
||||
items: Array.isArray(risks[key]) ? risks[key] : [],
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user