using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Spreadsheet; namespace Oms.Application.Services.Orders.Templates; /// /// 为标准模板 Excel 增加表头必填标黄、枚举下拉等 OpenXML 增强。 /// internal static class OrderTemplateExcelOpenXmlEnhancer { private const uint DataValidationStartRow = 2; private const uint DataValidationEndRow = 5000; private const string RequiredHeaderFillRgb = "FFFFE5E5"; private const string RequiredHeaderFontRgb = "FFC00000"; public static byte[] Enhance( byte[] workbookBytes, IReadOnlyList fields, IReadOnlyList dictOptions) { using var stream = new MemoryStream(); stream.Write(workbookBytes, 0, workbookBytes.Length); stream.Position = 0; using (var document = SpreadsheetDocument.Open(stream, true)) { var workbookPart = document.WorkbookPart ?? throw new InvalidOperationException("工作簿无效"); var requiredHeaderStyleIndex = EnsureRequiredHeaderStyle(workbookPart); var dataSheetPart = GetWorksheetPart(workbookPart, OrderTemplateExcelGenerator.DataSheetName) ?? throw new InvalidOperationException("未找到下单数据工作表"); ApplyRequiredHeaderHighlight(dataSheetPart.Worksheet, fields, requiredHeaderStyleIndex); ApplyColumnValidations(workbookPart, dataSheetPart, fields, dictOptions); dataSheetPart.Worksheet.Save(); workbookPart.Workbook.Save(); } return stream.ToArray(); } private static void ApplyRequiredHeaderHighlight( Worksheet worksheet, IReadOnlyList fields, uint styleIndex) { var sheetData = worksheet.GetFirstChild() ?? throw new InvalidOperationException("工作表数据为空"); var headerRow = sheetData.Elements().FirstOrDefault(x => x.RowIndex?.Value == 1) ?? throw new InvalidOperationException("未找到表头行"); for (var columnIndex = 0; columnIndex < fields.Count; columnIndex++) { if (!fields[columnIndex].Required) { continue; } var cellReference = $"{ToColumnName(columnIndex)}1"; var cell = headerRow.Elements().FirstOrDefault(x => x.CellReference?.Value == cellReference) ?? CreateCell(headerRow, cellReference, fields[columnIndex].Label); cell.StyleIndex = styleIndex; } } private static void ApplyColumnValidations( WorkbookPart workbookPart, WorksheetPart dataSheetPart, IReadOnlyList fields, IReadOnlyList dictOptions) { var dictRangeByType = BuildDictTypeRanges(workbookPart, dictOptions); var validations = dataSheetPart.Worksheet.GetFirstChild(); if (validations == null) { validations = new DataValidations(); dataSheetPart.Worksheet.Append(validations); } for (var columnIndex = 0; columnIndex < fields.Count; columnIndex++) { var field = fields[columnIndex]; var formula = ResolveValidationFormula(field, dictRangeByType); if (string.IsNullOrWhiteSpace(formula)) { continue; } var columnName = ToColumnName(columnIndex); var validation = new DataValidation { Type = DataValidationValues.List, AllowBlank = !field.Required, ShowInputMessage = true, ShowErrorMessage = true, ErrorTitle = "输入无效", Error = field.Required ? "请从下拉列表选择或填写有效值" : "请填写有效值", PromptTitle = field.Label, Prompt = BuildValidationPrompt(field), SequenceOfReferences = new ListValue { InnerText = $"{columnName}{DataValidationStartRow}:{columnName}{DataValidationEndRow}" } }; validation.Append(new Formula1(formula)); validations.Append(validation); } validations.Count = (uint)validations.ChildElements.Count; } private static string? ResolveValidationFormula( OrderTemplateFieldDefinition field, IReadOnlyDictionary dictRangeByType) { if (string.Equals(field.DataType, "boolean", StringComparison.OrdinalIgnoreCase)) { return "\"true,false\""; } if (string.IsNullOrWhiteSpace(field.DictType)) { return null; } if (dictRangeByType.TryGetValue(field.DictType, out var rangeFormula)) { return rangeFormula; } return null; } private static string BuildValidationPrompt(OrderTemplateFieldDefinition field) { if (!string.IsNullOrWhiteSpace(field.Hint)) { return field.Hint; } if (string.Equals(field.DataType, "boolean", StringComparison.OrdinalIgnoreCase)) { return "填写 true 或 false"; } return field.Required ? "必填" : "选填"; } private static Dictionary BuildDictTypeRanges( WorkbookPart workbookPart, IReadOnlyList dictOptions) { var result = new Dictionary(StringComparer.OrdinalIgnoreCase); if (dictOptions.Count == 0) { return result; } var dictSheetPart = GetWorksheetPart(workbookPart, OrderTemplateExcelGenerator.DictSheetName); if (dictSheetPart?.Worksheet == null) { return result; } var grouped = dictOptions .Where(x => !string.IsNullOrWhiteSpace(x.DictType) && !string.IsNullOrWhiteSpace(x.Code)) .GroupBy(x => x.DictType, StringComparer.OrdinalIgnoreCase) .OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase) .ToList(); var sheetData = dictSheetPart.Worksheet.GetFirstChild(); sheetData?.RemoveAllChildren(); sheetData ??= new SheetData(); if (dictSheetPart.Worksheet.GetFirstChild() == null) { dictSheetPart.Worksheet.Append(sheetData); } var headerRow = new Row { RowIndex = 1 }; sheetData.InsertAt(headerRow, 0); var columnIndex = 0; foreach (var group in grouped) { var columnName = ToColumnName(columnIndex); SetCellText(headerRow, $"{columnName}1", group.Key); var rowIndex = 2U; foreach (var option in group.OrderBy(x => x.Code, StringComparer.OrdinalIgnoreCase)) { var row = sheetData.Elements().FirstOrDefault(x => x.RowIndex?.Value == rowIndex) ?? CreateRow(sheetData, rowIndex); SetCellText(row, $"{columnName}{rowIndex}", option.Code); rowIndex++; } if (rowIndex == 2) { columnIndex++; continue; } var lastRow = rowIndex - 1; var escapedSheetName = OrderTemplateExcelGenerator.DictSheetName.Replace("'", "''"); result[group.Key] = $"'{escapedSheetName}'!${columnName}$2:${columnName}${lastRow}"; columnIndex++; } dictSheetPart.Worksheet.Save(); return result; } private static uint EnsureRequiredHeaderStyle(WorkbookPart workbookPart) { var stylesPart = workbookPart.WorkbookStylesPart ?? workbookPart.AddNewPart(); var stylesheet = stylesPart.Stylesheet; if (stylesheet == null) { stylesheet = new Stylesheet { Fonts = new Fonts(new Font()), Fills = new Fills( new Fill { PatternFill = new PatternFill { PatternType = PatternValues.None } }, new Fill { PatternFill = new PatternFill { PatternType = PatternValues.Gray125 } }), Borders = new Borders(new Border()), CellStyleFormats = new CellStyleFormats(new CellFormat()), CellFormats = new CellFormats(new CellFormat()) }; stylesheet.Fonts.Count = 1; stylesheet.Fills.Count = 2; stylesheet.Borders.Count = 1; stylesheet.CellStyleFormats.Count = 1; stylesheet.CellFormats.Count = 1; stylesPart.Stylesheet = stylesheet; } var fills = stylesheet.Fills ?? new Fills { Count = 0 }; stylesheet.Fills = fills; var fonts = stylesheet.Fonts ?? new Fonts { Count = 0 }; stylesheet.Fonts = fonts; var borders = stylesheet.Borders ?? new Borders { Count = 0 }; stylesheet.Borders = borders; var cellFormats = stylesheet.CellFormats ?? new CellFormats { Count = 0 }; stylesheet.CellFormats = cellFormats; if (fills.Count?.Value is not > 0) { fills.Append(new Fill { PatternFill = new PatternFill { PatternType = PatternValues.None } }); fills.Append(new Fill { PatternFill = new PatternFill { PatternType = PatternValues.Gray125 } }); fills.Count = 2U; } if (fonts.Count?.Value is not > 0) { fonts.Append(new Font()); fonts.Count = 1U; } if (borders.Count?.Value is not > 0) { borders.Append(new Border()); borders.Count = 1U; } if (cellFormats.Count?.Value is not > 0) { cellFormats.Append(new CellFormat()); cellFormats.Count = 1U; } var requiredFill = new Fill { PatternFill = new PatternFill { PatternType = PatternValues.Solid, ForegroundColor = new ForegroundColor { Rgb = RequiredHeaderFillRgb }, BackgroundColor = new BackgroundColor { Indexed = 64 } } }; fills.Append(requiredFill); var requiredFillId = (uint)fills.ChildElements.Count - 1; fills.Count = (uint)fills.ChildElements.Count; var headerFont = new Font( new Bold(), new Color { Rgb = RequiredHeaderFontRgb }); fonts.Append(headerFont); var headerFontId = (uint)fonts.ChildElements.Count - 1; fonts.Count = (uint)fonts.ChildElements.Count; var headerFormat = new CellFormat { FontId = headerFontId, FillId = requiredFillId, BorderId = 0, ApplyFont = true, ApplyFill = true }; cellFormats.Append(headerFormat); var headerFormatIndex = (uint)cellFormats.ChildElements.Count - 1; cellFormats.Count = (uint)cellFormats.ChildElements.Count; stylesheet.Save(); return headerFormatIndex; } private static WorksheetPart? GetWorksheetPart(WorkbookPart workbookPart, string sheetName) { var sheet = workbookPart.Workbook.Sheets?.Elements() .FirstOrDefault(x => string.Equals(x.Name?.Value, sheetName, StringComparison.Ordinal)); if (sheet?.Id?.Value == null) { return null; } return workbookPart.GetPartById(sheet.Id.Value) as WorksheetPart; } private static Cell CreateCell(Row row, string cellReference, string text) { var cell = new Cell { CellReference = cellReference, DataType = CellValues.String, CellValue = new CellValue(text) }; row.Append(cell); return cell; } private static Row CreateRow(SheetData sheetData, uint rowIndex) { var row = new Row { RowIndex = rowIndex }; sheetData.Append(row); return row; } private static void SetCellText(Row row, string cellReference, string text) { var cell = row.Elements().FirstOrDefault(x => x.CellReference?.Value == cellReference) ?? CreateCell(row, cellReference, text); cell.DataType = CellValues.String; cell.CellValue = new CellValue(text); } private static string ToColumnName(int columnIndex) { var dividend = columnIndex + 1; var columnName = string.Empty; while (dividend > 0) { var modulo = (dividend - 1) % 26; columnName = Convert.ToChar('A' + modulo) + columnName; dividend = (dividend - modulo) / 26; } return columnName; } }