diff --git a/src/api/cms/manuscript/model/index.ts b/src/api/cms/manuscript/model/index.ts index a3ef05a..6eee659 100644 --- a/src/api/cms/manuscript/model/index.ts +++ b/src/api/cms/manuscript/model/index.ts @@ -9,8 +9,10 @@ export interface Manuscript { id?: number; // title?: string; - // + // 正文(HTML,历史上可能是 markdown / 纯文本) content?: string; + // 正文格式:html / markdown / text + contentFormat?: string; // cover?: string; // 是否删除, 0否, 1是 diff --git a/src/utils/manuscript-content.ts b/src/utils/manuscript-content.ts new file mode 100644 index 0000000..fd53d42 --- /dev/null +++ b/src/utils/manuscript-content.ts @@ -0,0 +1,91 @@ +/** + * 稿件正文的格式判定与转换(后台)。 + * + * `cms_manuscript.content` 历史上混着两种来源: + * 1) wangEditor 保存的 HTML; + * 2) 从 AI 结果直接粘进后台的 markdown(`## 标题`、`**加粗**`、`- 列表`)。 + * 后台编辑器只认 HTML:把 markdown 原样塞进 wangEditor,它会当成普通文字, + * 保存时再包一层 `
`,于是 `## 标题` 变成正文里的一行字——打开编辑即损坏。
+ *
+ * 所以打开编辑弹窗时先把非 HTML 的正文转成 HTML,保存时统一写 `contentFormat: 'html'`。
+ */
+import { getProcessor } from 'bytemd';
+
+const HTML_TAG_RE = /<\/?([a-z][a-z0-9]*)\b[^>]*>/i;
+const MARKDOWN_LINE_RE =
+ /(^|\n)[ \t]{0,3}(#{1,6}[ \t]|[-*+][ \t]|\d+\.[ \t]|>[ \t])/;
+const MARKDOWN_EMPHASIS_RE =
+ /(\*\*[^*\n]+\*\*)|(__[^_\n]+__)|(`[^`\n]+`)|(!\[[^\]]*\]\()/;
+
+export type ContentFormat = 'html' | 'markdown' | 'text';
+
+/** 后端 `contentFormat` 的规范值;认不出来返回空串,交给内容判断兜底。 */
+export function normalizeFormat(format?: string | null): ContentFormat | '' {
+ const value = String(format ?? '')
+ .trim()
+ .toLowerCase();
+ if (value === 'html' || value === 'text') {
+ return value;
+ }
+ if (value === 'markdown' || value === 'md') {
+ return 'markdown';
+ }
+ return '';
+}
+
+export function looksLikeHtml(content?: string | null): boolean {
+ return HTML_TAG_RE.test(String(content ?? ''));
+}
+
+export function looksLikeMarkdown(content?: string | null): boolean {
+ const text = String(content ?? '');
+ if (!text.trim() || looksLikeHtml(text)) {
+ return false;
+ }
+ return MARKDOWN_LINE_RE.test(text) || MARKDOWN_EMPHASIS_RE.test(text);
+}
+
+/** 没有格式标记时的兜底判断(老接口、或写库前漏填的行)。 */
+export function detectFormat(content?: string | null): ContentFormat {
+ const text = String(content ?? '');
+ if (!text.trim()) {
+ return 'html';
+ }
+ if (looksLikeHtml(text)) {
+ return 'html';
+ }
+ return looksLikeMarkdown(text) ? 'markdown' : 'text';
+}
+
+/** markdown → HTML,用的是后台已有的 bytemd(和 ByteMdViewer 同一套解析)。 */
+export function markdownToHtml(markdown: string): string {
+ try {
+ // processSync 返回的是 vfile(`toString()` 即 HTML),不是裸字符串
+ const result = getProcessor({}).processSync(String(markdown ?? ''));
+ return String(result);
+ } catch (e) {
+ console.error('markdown 转换失败', e);
+ return plainTextToHtml(markdown);
+ }
+}
+
+function escapeHtml(text: string): string {
+ return String(text ?? '')
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+}
+
+/** 纯文本 → HTML:空行分段,段内换行转 `
`,否则编辑器里会挤成一段。 */
+export function plainTextToHtml(text: string): string {
+ return String(text ?? '')
+ .replace(/\r\n?/g, '\n')
+ .split(/\n{2,}/)
+ .map((paragraph) => paragraph.trim())
+ .filter((paragraph) => paragraph !== '')
+ .map(
+ (paragraph) => `
${escapeHtml(paragraph).replace(/\n/g, '
')}
,等于把正文改坏。 + */ + const toEditorHtml = (data: Manuscript): string => { + const content = data.content || ''; + const format = normalizeFormat(data.contentFormat) || detectFormat(content); + if (format === 'markdown') { + message.info('这篇稿件存的是 Markdown,已转换为富文本,请确认排版后保存'); + return markdownToHtml(content); + } + if (format === 'text') { + message.info( + '这篇稿件存的是纯文本,已按段落转换为富文本,请确认排版后保存' + ); + return plainTextToHtml(content); + } + return content; + }; + const hasEditorContent = (html?: string) => { const text = String(html || '') .replace(/