feat: 完善视频任务与 KOC 资源库

This commit is contained in:
巫凤萍
2026-08-15 03:53:09 +08:00
parent ad3dbdcc86
commit f37d05dd88
66 changed files with 6633 additions and 558 deletions

View File

@@ -13,6 +13,9 @@ export type RecoveryWorkbookRow = {
images: Array<{
column: number;
image: RecoveryWorkbookImage;
offsetX?: number;
maxWidth?: number;
maxHeight?: number;
}>;
hyperlinks?: Array<{
column: number;
@@ -25,6 +28,7 @@ type WorkbookOptions = {
headers: string[];
columnWidths: number[];
rows: RecoveryWorkbookRow[];
hiddenColumns?: number[];
};
const XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
@@ -130,9 +134,17 @@ function imageDimensions(image: RecoveryWorkbookImage) {
return { width: 4, height: 3 };
}
function imageDisplaySize(image: RecoveryWorkbookImage) {
function imageDisplaySize(
image: RecoveryWorkbookImage,
maxWidth = 160,
maxHeight = 150,
) {
const dimensions = imageDimensions(image);
const scale = Math.min(160 / dimensions.width, 150 / dimensions.height, 1);
const scale = Math.min(
maxWidth / dimensions.width,
maxHeight / dimensions.height,
1,
);
return {
width: Math.max(28, Math.round(dimensions.width * scale)),
height: Math.max(28, Math.round(dimensions.height * scale)),
@@ -152,6 +164,14 @@ export function buildRecoveryWorkbook(options: WorkbookOptions) {
const imageEntries = options.rows.flatMap((row, rowIndex) =>
row.images.map((item) => ({ ...item, row: rowIndex + 1 })),
);
const imageCells = new Set<string>();
imageEntries.forEach((entry) => {
const key = `${entry.row}:${entry.column}`;
if (imageCells.has(key)) {
throw new Error("Excel 单元格内只能嵌入一张图片,请为每张图片分配独立列");
}
imageCells.add(key);
});
const hyperlinkEntries = options.rows.flatMap((row, rowIndex) =>
(row.hyperlinks ?? [])
.map((item) => ({
@@ -178,7 +198,7 @@ export function buildRecoveryWorkbook(options: WorkbookOptions) {
const reference = `${columnName(columnIndex)}${number}`;
const value = row.cells[columnIndex] ?? "";
if (imageColumns.has(columnIndex)) {
return inlineCell(reference, value || "见图", 4);
return inlineCell(reference, value, 4);
}
return typeof value === "number"
? numberCell(reference, value, 3)
@@ -196,17 +216,22 @@ export function buildRecoveryWorkbook(options: WorkbookOptions) {
const columns = options.headers
.map((_, index) => {
const width = Math.min(70, Math.max(8, options.columnWidths[index] ?? 14));
return `<col min="${index + 1}" max="${index + 1}" width="${width}" customWidth="1"/>`;
const hidden = options.hiddenColumns?.includes(index) ? ' hidden="1"' : "";
return `<col min="${index + 1}" max="${index + 1}" width="${width}" customWidth="1"${hidden}/>`;
})
.join("");
const drawingXml = imageEntries.length
? `${XML_HEADER}<xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">${imageEntries
.map((entry, index) => {
const size = imageDisplaySize(entry.image);
const width = size.width * 9525;
const height = size.height * 9525;
return `<xdr:oneCellAnchor><xdr:from><xdr:col>${entry.column}</xdr:col><xdr:colOff>57150</xdr:colOff><xdr:row>${entry.row}</xdr:row><xdr:rowOff>57150</xdr:rowOff></xdr:from><xdr:ext cx="${width}" cy="${height}"/><xdr:pic><xdr:nvPicPr><xdr:cNvPr id="${index + 1}" name="${cleanXmlText(entry.image.description || `图片${index + 1}`)}"/><xdr:cNvPicPr><a:picLocks noChangeAspect="1"/></xdr:cNvPicPr></xdr:nvPicPr><xdr:blipFill><a:blip r:embed="rId${index + 1}"/><a:stretch><a:fillRect/></a:stretch></xdr:blipFill><xdr:spPr><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></xdr:spPr></xdr:pic><xdr:clientData/></xdr:oneCellAnchor>`;
const size = imageDisplaySize(
entry.image,
entry.maxWidth ?? 160,
entry.maxHeight ?? 150,
);
const offsetX = entry.offsetX ?? 6;
const offsetY = 6;
return `<xdr:twoCellAnchor editAs="twoCell"><xdr:from><xdr:col>${entry.column}</xdr:col><xdr:colOff>${offsetX * 9525}</xdr:colOff><xdr:row>${entry.row}</xdr:row><xdr:rowOff>${offsetY * 9525}</xdr:rowOff></xdr:from><xdr:to><xdr:col>${entry.column}</xdr:col><xdr:colOff>${(offsetX + size.width) * 9525}</xdr:colOff><xdr:row>${entry.row}</xdr:row><xdr:rowOff>${(offsetY + size.height) * 9525}</xdr:rowOff></xdr:to><xdr:pic><xdr:nvPicPr><xdr:cNvPr id="${index + 1}" name="${cleanXmlText(entry.image.description || `图片${index + 1}`)}"/><xdr:cNvPicPr><a:picLocks noChangeAspect="1"/></xdr:cNvPicPr></xdr:nvPicPr><xdr:blipFill><a:blip r:embed="rId${index + 1}"/><a:stretch><a:fillRect/></a:stretch></xdr:blipFill><xdr:spPr><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></xdr:spPr></xdr:pic><xdr:clientData/></xdr:twoCellAnchor>`;
})
.join("")}</xdr:wsDr>`
: "";
@@ -227,7 +252,10 @@ export function buildRecoveryWorkbook(options: WorkbookOptions) {
const imageContentTypes = [...imageFormats.entries()]
.map(([extension, contentType]) => `<Default Extension="${extension}" ContentType="${contentType}"/>`)
.join("");
const contentTypes = `${XML_HEADER}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>${imageContentTypes}<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>${imageEntries.length ? '<Override PartName="/xl/drawings/drawing1.xml" ContentType="application/vnd.openxmlformats-officedocument.drawing+xml"/>' : ""}<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>`;
const drawingContentType = imageEntries.length
? '<Override PartName="/xl/drawings/drawing1.xml" ContentType="application/vnd.openxmlformats-officedocument.drawing+xml"/>'
: "";
const contentTypes = `${XML_HEADER}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>${imageContentTypes}<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>${drawingContentType}<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>`;
const hyperlinkRelationshipOffset = imageEntries.length ? 2 : 1;
const hyperlinksXml = hyperlinkEntries.length
? `<hyperlinks>${hyperlinkEntries
@@ -266,7 +294,9 @@ export function buildRecoveryWorkbook(options: WorkbookOptions) {
}
if (imageEntries.length) {
files["xl/drawings/drawing1.xml"] = strToU8(drawingXml);
files["xl/drawings/_rels/drawing1.xml.rels"] = strToU8(drawingRelationships);
files["xl/drawings/_rels/drawing1.xml.rels"] = strToU8(
drawingRelationships,
);
imageEntries.forEach((entry, index) => {
const format = imageFormat(entry.image.contentType, entry.image.bytes);
files[`xl/media/image${index + 1}.${format.extension}`] = entry.image.bytes;