|
|
@@ -227,7 +227,7 @@ public class WordReportService {
|
|
|
|
|
|
/**
|
|
|
* 替换段落中的普通 {{placeholder}} 占位符和 {{CB_xxx}} checkbox 占位符。
|
|
|
- * CB_ 占位符会被转换为 ☑/☐ 纯文本内联替换,支持与普通文字在同一段落中。
|
|
|
+ * CB_ 占位符会为每个选项创建独立的 run:checkbox 字符用 Wingdings 字体,标签用普通字体。
|
|
|
*/
|
|
|
private void replacePlaceholdersInParagraph(XWPFParagraph paragraph, Map<String, String> placeholders) {
|
|
|
String fullText = paragraph.getText();
|
|
|
@@ -249,52 +249,142 @@ public class WordReportService {
|
|
|
}
|
|
|
|
|
|
String concatenated = sb.toString();
|
|
|
- // 先检查是否有普通占位符或 CB_ 占位符
|
|
|
Matcher placeholderMatcher = PLACEHOLDER_PATTERN.matcher(concatenated);
|
|
|
if (!placeholderMatcher.find()) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 替换所有占位符(包括 CB_ 前缀的),CB_ 的值转换为 ☑/☐ 纯文本
|
|
|
- String replaced = PLACEHOLDER_PATTERN.matcher(concatenated).replaceAll(mr -> {
|
|
|
- String key = mr.group(1).trim();
|
|
|
- String value = placeholders.getOrDefault(key, " ");
|
|
|
- if (value.isEmpty()) {
|
|
|
- return " ";
|
|
|
- }
|
|
|
- // 如果是 CB_ 前缀的占位符,将 checkbox 格式转为 ☑/☐ 纯文本
|
|
|
- if (key.startsWith("CB_")) {
|
|
|
- return Matcher.quoteReplacement(convertCheckboxToPlainText(value));
|
|
|
+ // 检查是否包含 CB_ 占位符
|
|
|
+ boolean hasCheckboxPlaceholder = false;
|
|
|
+ Matcher cbCheck = PLACEHOLDER_PATTERN.matcher(concatenated);
|
|
|
+ while (cbCheck.find()) {
|
|
|
+ if (cbCheck.group(1).trim().startsWith("CB_")) {
|
|
|
+ hasCheckboxPlaceholder = true;
|
|
|
+ break;
|
|
|
}
|
|
|
- return Matcher.quoteReplacement(value);
|
|
|
- });
|
|
|
-
|
|
|
- if (replaced.contains("\n")) {
|
|
|
- String[] lines = replaced.split("\n", -1);
|
|
|
- setRunTextSafely(runs.get(0), lines[0]);
|
|
|
- for (int i = 1; i < runs.size(); i++) {
|
|
|
- setRunTextSafely(runs.get(i), "");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!hasCheckboxPlaceholder) {
|
|
|
+ // 纯文本替换,无 checkbox
|
|
|
+ String replaced = PLACEHOLDER_PATTERN.matcher(concatenated).replaceAll(mr -> {
|
|
|
+ String key = mr.group(1).trim();
|
|
|
+ String value = placeholders.getOrDefault(key, " ");
|
|
|
+ if (value.isEmpty()) return " ";
|
|
|
+ return Matcher.quoteReplacement(value);
|
|
|
+ });
|
|
|
+
|
|
|
+ if (replaced.contains("\n")) {
|
|
|
+ String[] lines = replaced.split("\n", -1);
|
|
|
+ setRunTextSafely(runs.get(0), lines[0]);
|
|
|
+ for (int i = 1; i < runs.size(); i++) {
|
|
|
+ setRunTextSafely(runs.get(i), "");
|
|
|
+ }
|
|
|
+ for (int i = 1; i < lines.length; i++) {
|
|
|
+ runs.get(0).addBreak();
|
|
|
+ runs.get(0).setText(lines[i]);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ setRunTextSafely(runs.get(0), replaced);
|
|
|
+ for (int i = 1; i < runs.size(); i++) {
|
|
|
+ setRunTextSafely(runs.get(i), "");
|
|
|
+ }
|
|
|
}
|
|
|
- for (int i = 1; i < lines.length; i++) {
|
|
|
- runs.get(0).addBreak();
|
|
|
- runs.get(0).setText(lines[i]);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 包含 CB_ 占位符:需要按片段拆分,每个片段用独立 run
|
|
|
+ // 先清空所有现有 run 的文本
|
|
|
+ for (XWPFRun run : runs) {
|
|
|
+ setRunTextSafely(run, "");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取第一个 run 的格式信息作为参考
|
|
|
+ XWPFRun firstRun = runs.get(0);
|
|
|
+ Double fontSize = firstRun.getFontSizeAsDouble();
|
|
|
+ String fontFamily = firstRun.getFontFamily();
|
|
|
+
|
|
|
+ // 逐段解析拼接文本,将 CB_ 和非 CB_ 部分分别处理
|
|
|
+ Matcher matcher = PLACEHOLDER_PATTERN.matcher(concatenated);
|
|
|
+ int lastEnd = 0;
|
|
|
+ List<XWPFRun> newRuns = new ArrayList<>();
|
|
|
+ // 复用第一个 run
|
|
|
+ newRuns.add(firstRun);
|
|
|
+ int currentRunIndex = 0;
|
|
|
+ boolean isFirstText = true;
|
|
|
+
|
|
|
+ while (matcher.find()) {
|
|
|
+ // 占位符之前的普通文本
|
|
|
+ if (matcher.start() > lastEnd) {
|
|
|
+ String beforeText = concatenated.substring(lastEnd, matcher.start());
|
|
|
+ XWPFRun textRun = getOrCreateRun(paragraph, newRuns, currentRunIndex++, isFirstText);
|
|
|
+ isFirstText = false;
|
|
|
+ textRun.setFontFamily(fontFamily);
|
|
|
+ if (fontSize != null) textRun.setFontSize(fontSize);
|
|
|
+ setRunTextSafely(textRun, beforeText);
|
|
|
}
|
|
|
- } else {
|
|
|
- setRunTextSafely(runs.get(0), replaced);
|
|
|
- for (int i = 1; i < runs.size(); i++) {
|
|
|
- setRunTextSafely(runs.get(i), "");
|
|
|
+
|
|
|
+ String key = matcher.group(1).trim();
|
|
|
+ String value = placeholders.getOrDefault(key, " ");
|
|
|
+
|
|
|
+ if (key.startsWith("CB_")) {
|
|
|
+ // CB_ 占位符:为每个选项创建 Wingdings checkbox run + 普通标签 run
|
|
|
+ List<RunFragment> fragments = buildCheckboxFragments(value);
|
|
|
+ for (RunFragment frag : fragments) {
|
|
|
+ XWPFRun run = getOrCreateRun(paragraph, newRuns, currentRunIndex++, isFirstText);
|
|
|
+ isFirstText = false;
|
|
|
+ if (frag.isCheckbox) {
|
|
|
+ setWingdingsFont(run);
|
|
|
+ if (fontSize != null) run.setFontSize(fontSize);
|
|
|
+ } else {
|
|
|
+ run.setFontFamily(fontFamily);
|
|
|
+ if (fontSize != null) run.setFontSize(fontSize);
|
|
|
+ }
|
|
|
+ setRunTextSafely(run, frag.text);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 普通占位符
|
|
|
+ XWPFRun textRun = getOrCreateRun(paragraph, newRuns, currentRunIndex++, isFirstText);
|
|
|
+ isFirstText = false;
|
|
|
+ textRun.setFontFamily(fontFamily);
|
|
|
+ if (fontSize != null) textRun.setFontSize(fontSize);
|
|
|
+ setRunTextSafely(textRun, value.isEmpty() ? " " : value);
|
|
|
}
|
|
|
+
|
|
|
+ lastEnd = matcher.end();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 剩余普通文本
|
|
|
+ if (lastEnd < concatenated.length()) {
|
|
|
+ String remaining = concatenated.substring(lastEnd);
|
|
|
+ XWPFRun textRun = getOrCreateRun(paragraph, newRuns, currentRunIndex++, isFirstText);
|
|
|
+ textRun.setFontFamily(fontFamily);
|
|
|
+ if (fontSize != null) textRun.setFontSize(fontSize);
|
|
|
+ setRunTextSafely(textRun, remaining);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 文本片段,标记是否为 checkbox(需要 Wingdings 字体) */
|
|
|
+ private static class RunFragment {
|
|
|
+ final String text;
|
|
|
+ final boolean isCheckbox;
|
|
|
+ RunFragment(String text, boolean isCheckbox) {
|
|
|
+ this.text = text;
|
|
|
+ this.isCheckbox = isCheckbox;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 将 CB_ checkbox 格式的值转换为纯文本。
|
|
|
+ * 将 CB_ checkbox 格式的值拆分为 RunFragment 列表。
|
|
|
+ * 每个 checkbox 符号(☑/☐)和标签文字分别作为独立片段,
|
|
|
+ * checkbox 用 Wingdings 字体,标签用普通字体。
|
|
|
* 输入格式: "选项1,选项2,选项3|SELECTED:选中项"
|
|
|
- * 输出格式: "☑ 选项1 ☐ 选项2 ☐ 选项3"
|
|
|
*/
|
|
|
- private String convertCheckboxToPlainText(String value) {
|
|
|
+ private List<RunFragment> buildCheckboxFragments(String value) {
|
|
|
+ List<RunFragment> fragments = new ArrayList<>();
|
|
|
+
|
|
|
if (value == null || value.isEmpty()) {
|
|
|
- return " ";
|
|
|
+ fragments.add(new RunFragment(" ", false));
|
|
|
+ return fragments;
|
|
|
}
|
|
|
|
|
|
String optionsPart = value;
|
|
|
@@ -316,20 +406,33 @@ public class WordReportService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- StringBuilder result = new StringBuilder();
|
|
|
- for (String option : options) {
|
|
|
- option = option.trim();
|
|
|
- if (result.length() > 0) {
|
|
|
- result.append(" ");
|
|
|
- }
|
|
|
- if (selectedSet.contains(option)) {
|
|
|
- result.append("☑ ");
|
|
|
- } else {
|
|
|
- result.append("☐ ");
|
|
|
+ for (int i = 0; i < options.length; i++) {
|
|
|
+ String option = options[i].trim();
|
|
|
+ if (i > 0) {
|
|
|
+ // 选项间加空格(用普通字体)
|
|
|
+ fragments.add(new RunFragment(" ", false));
|
|
|
}
|
|
|
- result.append(option);
|
|
|
+ // checkbox 符号用 Wingdings 字体
|
|
|
+ char boxChar = selectedSet.contains(option) ? WINGDINGS_CHECKED : WINGDINGS_UNCHECKED;
|
|
|
+ fragments.add(new RunFragment(String.valueOf(boxChar), true));
|
|
|
+ // 标签用普通字体
|
|
|
+ fragments.add(new RunFragment(option, false));
|
|
|
+ }
|
|
|
+
|
|
|
+ return fragments;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取或创建 run。
|
|
|
+ * 如果 newRuns 列表中有足够的 run 则复用,否则新建。
|
|
|
+ */
|
|
|
+ private XWPFRun getOrCreateRun(XWPFParagraph paragraph, List<XWPFRun> newRuns, int index, boolean isFirst) {
|
|
|
+ if (index < newRuns.size()) {
|
|
|
+ return newRuns.get(index);
|
|
|
}
|
|
|
- return result.toString();
|
|
|
+ XWPFRun newRun = paragraph.createRun();
|
|
|
+ newRuns.add(newRun);
|
|
|
+ return newRun;
|
|
|
}
|
|
|
|
|
|
private void setRunTextSafely(XWPFRun run, String text) {
|