feat: 学生报告改为流式排版并隐藏空数据区块

This commit is contained in:
2026-08-05 17:20:57 +08:00
parent fd39e1686a
commit 81e622fa09
9 changed files with 269 additions and 204 deletions

View File

@@ -1,7 +1,9 @@
import { AttendanceRecord } from '../entities/attendance-record.entity';
import { esc, pageFrame, pageFooter, pageHeader } from './archive-report.helpers';
import { esc, sectionFrame, sectionHeader } from './archive-report.helpers';
export function buildAttendance(records: AttendanceRecord[], now: string): string {
if (records.length === 0) return '';
const present = records.filter((r) => r.status === 'present').length;
const absent = records.filter((r) => r.status === 'absent').length;
const late = records.filter((r) => r.status === 'late').length;
@@ -42,18 +44,11 @@ export function buildAttendance(records: AttendanceRecord[], now: string): strin
const chart = renderAttendanceBar(records);
const matrix = renderAttendanceMatrix(records);
let extraHtml = '';
if (records.length === 0) {
extraHtml = '<div class="banner-note">暂无出勤记录</div>';
}
return pageFrame(`
${pageHeader('出勤记录')}
return sectionFrame(`
${sectionHeader('出勤记录')}
${metricHtml}
${extraHtml}
${chart}
${matrix}
${pageFooter()}
`);
}

View File

@@ -1,7 +1,7 @@
import { Student } from '../entities/student.entity';
import { StudentProfile } from '../entities/student-profile.entity';
import { StudentEnrollment } from '../entities/student-enrollment.entity';
import { esc, pageFrame, pageFooter, pageHeader } from './archive-report.helpers';
import { esc, sectionFrame, sectionHeader, coverFooter } from './archive-report.helpers';
import { buildEnrollmentSection } from './archive-report.enrollment';
export function buildCover(
@@ -9,11 +9,22 @@ export function buildCover(
profile: StudentProfile | null,
enrollments: StudentEnrollment[],
now: string,
tocNames: string[],
): string {
const types = enrollments.map((e) => e.classType).filter(Boolean).join(' / ') || '-';
return pageFrame(`
${pageHeader('封面')}
const tocHtml =
tocNames.length > 0
? tocNames
.map(
(name, i) => `
<div class="toc-row"><span class="toc-index">${String(i + 1).padStart(2, '0')}</span><span class="toc-name">${esc(name)}</span></div>`,
)
.join('')
: '<div class="toc-row"><span class="toc-name muted">暂无章节</span></div>';
return sectionFrame(`
${sectionHeader('封面')}
<div class="cover-title">学生档案报告</div>
<div class="cover-subtitle">生成日期: ${esc(now)}</div>
<div class="cover-main">
@@ -40,16 +51,10 @@ export function buildCover(
</div>
</div>
</div>
<div class="toc">
<div class="toc-row"><span class="toc-index">01</span><span class="toc-name">基础信息与报读记录</span><span class="toc-page">第 2 页</span></div>
<div class="toc-row"><span class="toc-index">02</span><span class="toc-name">考试成绩总览</span><span class="toc-page">第 3 页</span></div>
<div class="toc-row"><span class="toc-index">03</span><span class="toc-name">出勤记录</span><span class="toc-page">第 4 页</span></div>
<div class="toc-row"><span class="toc-index">04</span><span class="toc-name">文化课考试成绩</span><span class="toc-page">第 5 页</span></div>
<div class="toc-row"><span class="toc-index">05</span><span class="toc-name">学情记录与录取归档</span><span class="toc-page">第 6 页</span></div>
</div>
<div class="toc">${tocHtml}</div>
<div class="watermark">恭学教育</div>
${pageFooter()}
`);
${coverFooter()}
`, true);
}
export function buildBasicInfo(
@@ -80,10 +85,9 @@ export function buildBasicInfo(
const enrollmentSection = buildEnrollmentSection(enrollments);
return pageFrame(`
${pageHeader('基础信息')}
return sectionFrame(`
${sectionHeader('基础信息')}
${infoCards}
${enrollmentSection}
${pageFooter()}
`);
}

View File

@@ -2,14 +2,10 @@ import { StudentEnrollment } from '../entities/student-enrollment.entity';
import { esc } from './archive-report.helpers';
export function buildEnrollmentSection(enrollments: StudentEnrollment[]): string {
if (enrollments.length === 0) {
return `<div class="banner-note">暂无报读记录</div>`;
}
if (enrollments.length === 0) return '';
const renderEnrollmentTable = (enrs: StudentEnrollment[]): string => {
if (enrs.length === 0) {
return `<div class="banner-note">暂无数据</div>`;
}
if (enrs.length === 0) return '';
let rows = '';
for (const e of enrs) {
@@ -46,35 +42,21 @@ export function buildEnrollmentSection(enrollments: StudentEnrollment[]): string
(!e.courseCategory.includes('文化') && !e.courseCategory.includes('专业')),
);
if (cultureEnrollments.length > 0 || profEnrollments.length > 0) {
let html =
'<div class="card" style="margin-bottom:14px;"><h3>报读记录</h3>';
const cultureTable = renderEnrollmentTable(cultureEnrollments);
const profTable = renderEnrollmentTable(profEnrollments);
const otherTable = renderEnrollmentTable(otherEnrollments);
let html = '<div class="card" style="margin-bottom:14px;"><h3>报读记录</h3>';
if (cultureTable && profTable) {
html += '<div class="grid-2" style="gap:14px;">';
html += '<div>';
html += '<h3 style="font-size:13px;margin-bottom:8px;">文化课报读</h3>';
html += renderEnrollmentTable(cultureEnrollments);
html += `<div><h3 style="font-size:13px;margin-bottom:8px;">文化课报读</h3>${cultureTable}</div>`;
html += `<div><h3 style="font-size:13px;margin-bottom:8px;">专业课报读</h3>${profTable}</div>`;
html += '</div>';
html += '<div>';
html += '<h3 style="font-size:13px;margin-bottom:8px;">专业课报读</h3>';
html += renderEnrollmentTable(profEnrollments);
html += '</div>';
html += '</div>';
if (otherEnrollments.length > 0) {
html +=
'<h3 style="font-size:13px;margin:10px 0 8px;">其他报读</h3>';
html += renderEnrollmentTable(otherEnrollments);
}
html += '</div>';
return html;
} else {
if (cultureTable) html += `<h3 style="font-size:13px;margin:0 0 8px;">文化课报读</h3>${cultureTable}`;
if (profTable) html += `<h3 style="font-size:13px;margin:0 0 8px;">专业课报读</h3>${profTable}`;
}
return `<div class="card" style="margin-bottom:14px;">
<h3>报读记录</h3>
${renderEnrollmentTable(enrollments)}
</div>`;
if (otherTable) html += `<h3 style="font-size:13px;margin:10px 0 8px;">其他报读</h3>${otherTable}`;
html += '</div>';
return html;
}

View File

@@ -1,7 +1,9 @@
import { ExamScore } from '../entities/exam-score.entity';
import { esc, pageFrame, pageFooter, pageHeader } from './archive-report.helpers';
import { esc, sectionFrame, sectionHeader } from './archive-report.helpers';
export function buildExamOverview(exams: ExamScore[], now: string): string {
if (exams.length === 0) return '';
const cultureExams = exams.filter(
(e) => e.examType && e.examType.includes('文化'),
);
@@ -64,18 +66,11 @@ export function buildExamOverview(exams: ExamScore[], now: string): string {
const scoreTable = renderScoreTable(cultureExams);
const trendChart = renderScoreTrendChart(cultureExams);
let extraHtml = '';
if (cultureExams.length === 0) {
extraHtml = '<div class="banner-note">暂无文化课考试成绩</div>';
}
return pageFrame(`
${pageHeader('考试成绩总览')}
return sectionFrame(`
${sectionHeader('考试成绩总览')}
${metricHtml}
${extraHtml}
${scoreTable}
${trendChart}
${pageFooter()}
`);
}
@@ -183,19 +178,7 @@ export function buildExamDetail(exams: ExamScore[], now: string): string {
(e) => e.examType && e.examType.includes('文化'),
);
if (cultureExams.length === 0) {
return pageFrame(`
${pageHeader('文化课考试成绩')}
<div class="title-row">
<div>
<div class="source">${esc(now)} · 系统生成</div>
<div class="section-title">文化课考试成绩</div>
</div>
</div>
<div class="banner-note">暂无文化课考试成绩</div>
${pageFooter()}
`);
}
if (cultureExams.length === 0) return '';
// Group by subject
const subjectMap = new Map<string, ExamScore[]>();
@@ -235,8 +218,8 @@ export function buildExamDetail(exams: ExamScore[], now: string): string {
</div>`;
}
return pageFrame(`
${pageHeader('文化课考试成绩')}
return sectionFrame(`
${sectionHeader('文化课考试成绩')}
<div class="title-row">
<div>
<div class="source">${esc(now)} · 系统生成</div>
@@ -244,6 +227,5 @@ export function buildExamDetail(exams: ExamScore[], now: string): string {
</div>
</div>
${subjectCards}
${pageFooter()}
`);
}

View File

@@ -7,14 +7,14 @@ export function esc(value: string): string {
.replace(/'/g, '&#39;');
}
export function pageFrame(inner: string): string {
return `<div class="page"><div class="frame"></div>${inner}</div>`;
export function sectionFrame(inner: string, cover = false): string {
return `<div class="section${cover ? ' section-cover' : ''}">${cover ? '<div class="frame"></div>' : ''}${inner}</div>`;
}
export function pageHeader(title: string): string {
export function sectionHeader(title: string): string {
return `<div class="header"><span class="logo">G</span><span class="brand">恭学教育 · 学生档案</span><span class="page-kicker">${esc(title)}</span></div>`;
}
export function pageFooter(): string {
export function coverFooter(): string {
return `<div class="footer"><span>恭学教育 · 学生档案报告</span><span>机密 · 仅限内部使用</span></div>`;
}

View File

@@ -1,85 +1,61 @@
import { LearningRecord } from '../entities/learning-record.entity';
import { ResultArchive } from '../entities/result-archive.entity';
import { esc, pageFrame, pageFooter, pageHeader } from './archive-report.helpers';
import { esc, sectionFrame, sectionHeader } from './archive-report.helpers';
export function buildLearningAndResult(
learnings: LearningRecord[],
result: ResultArchive | null,
now: string,
): string {
let learningHtml = '';
if (learnings.length === 0) {
learningHtml = `
<div class="title-row">
<div>
<div class="source">${esc(now)} · 系统生成</div>
<div class="section-title">学情记录</div>
</div>
</div>
<div class="banner-note">暂无学情记录</div>`;
} else {
const latest = learnings.slice(0, 15);
let rows = '';
for (const r of latest) {
rows += `<tr>
<td class="nowrap">${esc(r.recordDate || '-')}</td>
<td>${esc(r.recordType || '-')}</td>
<td>${esc((r.content || '-').slice(0, 200))}</td>
<td>${esc(r.followUpMethod || '-')}</td>
</tr>`;
}
export function buildLearning(learnings: LearningRecord[], now: string): string {
if (learnings.length === 0) return '';
learningHtml = `
<div class="title-row">
<div>
<div class="source">${esc(now)} · 系统生成</div>
<div class="section-title">学情记录</div>
</div>
</div>
<div class="card" style="margin-bottom:14px;">
<h3>最近学情记录</h3>
<table class="data-table">
<thead><tr>
<th style="width:90px;">日期</th><th style="width:60px;">类型</th>
<th>内容</th><th style="width:70px;">跟进方式</th>
</tr></thead>
<tbody>${rows}</tbody>
</table>
</div>`;
const latest = learnings.slice(0, 15);
let rows = '';
for (const r of latest) {
rows += `<tr>
<td class="nowrap">${esc(r.recordDate || '-')}</td>
<td>${esc(r.recordType || '-')}</td>
<td>${esc((r.content || '-').slice(0, 200))}</td>
<td>${esc(r.followUpMethod || '-')}</td>
</tr>`;
}
let resultHtml = '';
if (result) {
resultHtml = `
<div class="title-row">
<div>
<div class="section-title">录取归档</div>
</div>
return sectionFrame(`
${sectionHeader('学情记录')}
<div class="title-row">
<div>
<div class="source">${esc(now)} · 系统生成</div>
<div class="section-title">学情记录</div>
</div>
<div class="card">
<div class="grid-2">
<div class="summary-row"><span>文化课成绩</span><span><strong>${result.cultureFinalScore != null ? result.cultureFinalScore : '-'}</strong></span></div>
<div class="summary-row"><span>专业课成绩</span><span><strong>${result.professionalFinalScore != null ? result.professionalFinalScore : '-'}</strong></span></div>
<div class="summary-row"><span>录取状态</span><span><strong>${esc(result.admissionStatus || '-')}</strong></span></div>
<div class="summary-row"><span>录取院校</span><span><strong>${esc(result.admittedCollege || '-')}</strong></span></div>
<div class="summary-row"><span>录取专业</span><span><strong>${esc(result.admittedMajor || '-')}</strong></span></div>
</div>
</div>
<div class="note">录取归档信息为最终结果,如有疑问请联系教务处</div>`;
} else {
resultHtml = `
<div class="title-row">
<div>
<div class="section-title">录取归档</div>
</div>
</div>
<div class="banner-note">暂无录取归档信息</div>`;
}
return pageFrame(`
${pageHeader('学情记录与录取归档')}
${learningHtml}
${resultHtml}
${pageFooter()}
</div>
<div class="card" style="margin-bottom:14px;">
<h3>最近学情记录</h3>
<table class="data-table">
<thead><tr>
<th style="width:90px;">日期</th><th style="width:60px;">类型</th>
<th>内容</th><th style="width:70px;">跟进方式</th>
</tr></thead>
<tbody>${rows}</tbody>
</table>
</div>
`);
}
export function buildResult(result: ResultArchive | null, now: string): string {
if (!result) return '';
return sectionFrame(`
${sectionHeader('录取归档')}
<div class="title-row">
<div>
<div class="section-title">录取归档</div>
</div>
</div>
<div class="card">
<div class="grid-2">
<div class="summary-row"><span>文化课成绩</span><span><strong>${result.cultureFinalScore != null ? result.cultureFinalScore : '-'}</strong></span></div>
<div class="summary-row"><span>专业课成绩</span><span><strong>${result.professionalFinalScore != null ? result.professionalFinalScore : '-'}</strong></span></div>
<div class="summary-row"><span>录取状态</span><span><strong>${esc(result.admissionStatus || '-')}</strong></span></div>
<div class="summary-row"><span>录取院校</span><span><strong>${esc(result.admittedCollege || '-')}</strong></span></div>
<div class="summary-row"><span>录取专业</span><span><strong>${esc(result.admittedMajor || '-')}</strong></span></div>
</div>
</div>
<div class="note">录取归档信息为最终结果,如有疑问请联系教务处</div>
`);
}

View File

@@ -1,26 +1,45 @@
import { ArchiveReportService } from './archive-report.service';
interface MockData {
student?: Record<string, unknown>;
profile?: Record<string, unknown> | null;
enrollments?: Array<Record<string, unknown>>;
exams?: Array<Record<string, unknown>>;
learnings?: Array<Record<string, unknown>>;
result?: Record<string, unknown> | null;
attendances?: Array<Record<string, unknown>>;
}
function makeService(data: MockData = {}): ArchiveReportService {
return new ArchiveReportService(
{ findOne: jest.fn().mockResolvedValue(data.profile ?? null) } as never,
{ find: jest.fn().mockResolvedValue(data.enrollments ?? []) } as never,
{ find: jest.fn().mockResolvedValue(data.exams ?? []) } as never,
{ find: jest.fn().mockResolvedValue(data.learnings ?? []) } as never,
{ findOne: jest.fn().mockResolvedValue(data.result ?? null) } as never,
{ find: jest.fn().mockResolvedValue(data.attendances ?? []) } as never,
{
findOne: jest.fn().mockResolvedValue(
data.student ?? { id: 1, name: '测试学生', studentNo: 'S001' },
),
} as never,
);
}
describe('ArchiveReportService retired profile fields', () => {
it('does not render the retired campus field in a student report', async () => {
const service = new ArchiveReportService(
{ findOne: jest.fn().mockResolvedValue({ campusLocation: '旧校区', grade: '高三' }) } as never,
{ find: jest.fn().mockResolvedValue([]) } as never,
{ find: jest.fn().mockResolvedValue([]) } as never,
{ find: jest.fn().mockResolvedValue([]) } as never,
{ findOne: jest.fn().mockResolvedValue(null) } as never,
{ find: jest.fn().mockResolvedValue([]) } as never,
{
findOne: jest.fn().mockResolvedValue({
id: 1,
name: '测试学生',
gender: '男',
phone: '',
ethnicity: '',
emergencyContact: '',
emergencyPhone: '',
}),
} as never,
);
const service = makeService({
profile: { campusLocation: '旧校区', grade: '高三' },
student: {
id: 1,
name: '测试学生',
gender: '男',
phone: '',
ethnicity: '',
emergencyContact: '',
emergencyPhone: '',
},
});
const html = await service.generateReportHtml(1);
@@ -29,3 +48,73 @@ describe('ArchiveReportService retired profile fields', () => {
expect(html).toContain('高三');
});
});
describe('ArchiveReportService empty sections', () => {
it('hides empty sections and removes fixed TOC page numbers', async () => {
const service = makeService({ profile: { grade: '高三' } });
const html = await service.generateReportHtml(1);
expect(html).toContain('学生档案报告');
expect(html).toContain('基础信息');
expect(html).not.toContain('考试成绩总览');
expect(html).not.toContain('出勤记录');
expect(html).not.toContain('文化课考试成绩');
expect(html).not.toContain('学情记录');
expect(html).not.toContain('录取归档');
expect(html).not.toContain('第 2 页');
expect(html).not.toContain('暂无');
});
it('renders sections with data and lists only those sections in the TOC', async () => {
const service = makeService({
profile: { grade: '高三' },
enrollments: [{ courseCategory: '文化课', classType: '全日制' }],
exams: [
{
examType: '文化课月考',
examName: '一月月考',
subject: '数学',
score: 88,
examDate: '2026-01-10',
},
],
attendances: [
{ attendanceDate: '2026-01-12', session: '上午', status: 'present' },
],
learnings: [
{
recordDate: '2026-01-13',
recordType: '回访',
content: '状态良好',
followUpMethod: '电话',
},
],
result: {
cultureFinalScore: 90,
professionalFinalScore: 85,
admissionStatus: '录取',
admittedCollege: '示例大学',
admittedMajor: '计算机',
},
});
const html = await service.generateReportHtml(1);
expect(html).toContain('考试成绩总览');
expect(html).toContain('出勤记录');
expect(html).toContain('文化课考试成绩');
expect(html).toContain('学情记录');
expect(html).toContain('录取归档');
expect(html).toContain('<h3>报读记录</h3>');
expect(html).not.toContain('第 1 页');
expect(html).not.toContain('第 2 页');
});
it('omits the enrollment card when there are no enrollments', async () => {
const service = makeService({ profile: { grade: '高三' } });
const html = await service.generateReportHtml(1);
expect(html).not.toContain('<h3>报读记录</h3>');
expect(html).not.toContain('暂无报读记录');
});
});

View File

@@ -13,7 +13,7 @@ import { esc } from './archive-report.helpers';
import { buildCover, buildBasicInfo } from './archive-report.cover';
import { buildExamOverview, buildExamDetail } from './archive-report.exam';
import { buildAttendance } from './archive-report.attendance';
import { buildLearningAndResult } from './archive-report.learning';
import { buildLearning, buildResult } from './archive-report.learning';
interface ReportData {
student: Student;
@@ -71,17 +71,33 @@ export class ArchiveReportService {
day: 'numeric',
});
const sections = [
{
name: enrollments.length > 0 ? '基础信息与报读记录' : '基础信息',
html: buildBasicInfo(student, profile, enrollments, now),
},
{ name: '考试成绩总览', html: buildExamOverview(exams, now) },
{ name: '出勤记录', html: buildAttendance(attendances, now) },
{ name: '文化课考试成绩', html: buildExamDetail(exams, now) },
{ name: '学情记录', html: buildLearning(learnings, now) },
{ name: '录取归档', html: buildResult(result, now) },
].filter((section) => section.html.length > 0);
const cover = buildCover(
student,
profile,
enrollments,
now,
sections.map((section) => section.name),
);
return `<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>学生档案报告 - ${esc(name)}</title>
<style>${ARCHIVE_REPORT_CSS}</style></head>
<body>
${buildCover(student, profile, enrollments, now)}
${buildBasicInfo(student, profile, enrollments, now)}
${buildExamOverview(exams, now)}
${buildAttendance(attendances, now)}
${buildExamDetail(exams, now)}
${buildLearningAndResult(learnings, result, now)}
${cover}
${sections.map((section) => section.html).join('\n')}
</body></html>`;
}
}

View File

@@ -6,10 +6,14 @@ export const ARCHIVE_REPORT_CSS = `
font-family: "PingFang SC", "Microsoft YaHei", Arial, sans-serif;
-webkit-print-color-adjust: exact; print-color-adjust: exact;
}
.page {
position: relative; width: 210mm; height: 297mm;
.section {
position: relative; width: 210mm; max-width: 100%;
margin: 0 auto 18px; padding: 14mm 15mm 10mm;
overflow: hidden; background: #fff; page-break-after: always;
background: #fff;
}
.section-cover {
height: 297mm; overflow: hidden;
page-break-after: always; break-after: page;
}
.frame {
position: absolute; inset: 14mm; border: 1px solid #cfe0f2; pointer-events: none;
@@ -17,6 +21,7 @@ export const ARCHIVE_REPORT_CSS = `
.header {
position: relative; z-index: 1; display: flex; align-items: center;
height: 39px; padding-bottom: 8px; border-bottom: 1px solid #cfe0f2;
page-break-after: avoid; break-after: avoid;
}
.logo {
width: 24px; height: 24px; border-radius: 6px;
@@ -33,11 +38,13 @@ export const ARCHIVE_REPORT_CSS = `
font-size: 10px; color: #667085;
}
h1, h2, h3, p { margin: 0; }
h1, h2, h3 { page-break-after: avoid; break-after: avoid; }
.section-title { font-size: 24px; line-height: 1.24; font-weight: 800; }
.source { font-size: 12px; color: #667085; padding-bottom: 2px; }
.title-row {
display: flex; align-items: flex-end; justify-content: space-between;
margin: 26px 0 17px;
page-break-after: avoid; break-after: avoid;
}
.cover-title { margin-top: 60px; font-size: 34px; line-height: 1.22; font-weight: 800; }
.cover-subtitle { margin-top: 22px; font-size: 16px; color: #667085; }
@@ -60,7 +67,7 @@ export const ARCHIVE_REPORT_CSS = `
.value { font-size: 14px; line-height: 1.5; font-weight: 700; }
.toc { margin-top: 58px; }
.toc-row {
display: grid; grid-template-columns: 48px 1fr 72px; align-items: center;
display: grid; grid-template-columns: 48px 1fr; align-items: center;
height: 47px; border-bottom: 1px solid #cfe0f2;
}
.toc-index { color: #155aa8; font-size: 15px; font-weight: 800; }
@@ -70,9 +77,18 @@ export const ARCHIVE_REPORT_CSS = `
position: absolute; right: 36px; bottom: 82px; color: #eaf1fb;
font-size: 56px; font-weight: 900; writing-mode: vertical-rl;
}
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.card { border: 1px solid #cfe0f2; padding: 14px; background: #fff; }
.grid-2 {
display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
page-break-inside: avoid; break-inside: avoid;
}
.grid-4 {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px;
page-break-inside: avoid; break-inside: avoid;
}
.card {
border: 1px solid #cfe0f2; padding: 14px; background: #fff;
page-break-inside: avoid; break-inside: avoid;
}
.card h3 { font-size: 16px; margin-bottom: 14px; }
.data-table {
width: 100%; border-collapse: collapse; table-layout: fixed;
@@ -86,8 +102,10 @@ export const ARCHIVE_REPORT_CSS = `
}
.data-table td { overflow-wrap: anywhere; word-break: break-word; }
.data-table .nowrap { white-space: nowrap; }
.data-table tr { page-break-inside: avoid; break-inside: avoid; }
.metric {
min-height: 88px; border: 1px solid #cfe0f2; padding: 13px 14px;
page-break-inside: avoid; break-inside: avoid;
}
.metric .label { margin-bottom: 7px; }
.metric strong {
@@ -107,13 +125,16 @@ export const ARCHIVE_REPORT_CSS = `
.note {
margin-top: 14px; padding: 12px 16px; border-left: 4px solid #155aa8;
background: #eef5ff; color: #173f6f; font-size: 12px; line-height: 1.7;
page-break-inside: avoid; break-inside: avoid;
}
.banner-note {
margin-top: 12px; padding: 11px 16px; background: #eef5ff;
color: #173f6f; font-size: 12px; line-height: 1.7;
.line-chart {
width: 100%; height: 180px; display: block;
page-break-inside: avoid; break-inside: avoid;
}
.bar-chart {
width: 100%; height: 150px; display: block;
page-break-inside: avoid; break-inside: avoid;
}
.line-chart { width: 100%; height: 180px; display: block; }
.bar-chart { width: 100%; height: 150px; display: block; }
.status {
display: inline-flex; align-items: center; justify-content: center;
width: 18px; height: 18px; border-radius: 5px; margin-right: 6px;
@@ -137,6 +158,6 @@ export const ARCHIVE_REPORT_CSS = `
.muted { color: #667085; }
@media print {
body { background: #fff; }
.page { margin: 0; box-shadow: none; }
.section { margin: 0; box-shadow: none; }
}
`;