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, '
')}

` + ) + .join(''); +} diff --git a/src/views/cms/manuscript/components/manuscriptEdit.vue b/src/views/cms/manuscript/components/manuscriptEdit.vue index 57db34e..1844498 100644 --- a/src/views/cms/manuscript/components/manuscriptEdit.vue +++ b/src/views/cms/manuscript/components/manuscriptEdit.vue @@ -79,6 +79,12 @@ import { shallowRef, ref, reactive, watch, onBeforeUnmount } from 'vue'; import { Form, message } from 'ant-design-vue'; import { assignObject } from 'ele-admin-pro'; + import { + detectFormat, + markdownToHtml, + normalizeFormat, + plainTextToHtml + } from '@/utils/manuscript-content'; import { addManuscript, updateManuscript } from '@/api/cms/manuscript'; import { Manuscript } from '@/api/cms/manuscript/model'; import { useThemeStore } from '@/store/modules/theme'; @@ -147,6 +153,7 @@ id: undefined, title: undefined, content: undefined, + contentFormat: 'html', cover: undefined, deleted: undefined, tenantId: undefined, @@ -161,6 +168,27 @@ emit('update:visible', value); }; + /** + * 入库格式 → 编辑器 HTML。 + * 库里可能是 markdown 或纯文本(早期从 AI 结果粘进来的),wangEditor 只认 HTML, + * 原样塞进去会在保存时被当成普通文字包进

,等于把正文改坏。 + */ + 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(/]*>[\s\S]*?<\/style>/gi, '') @@ -257,7 +285,8 @@ loading.value = true; const formData = { ...form, - content: contentHtml.value + content: contentHtml.value, + contentFormat: 'html' }; const saveOrUpdate = isUpdate.value ? updateManuscript : addManuscript; const msg = await saveOrUpdate(formData); @@ -294,7 +323,7 @@ if (visible) { if (props.data) { assignObject(form, props.data); - contentHtml.value = props.data.content || ''; + contentHtml.value = toEditorHtml(props.data); coverList.value = props.data.cover ? [ { diff --git a/src/views/cms/manuscript/index.vue b/src/views/cms/manuscript/index.vue index 1817d16..9d059d8 100644 --- a/src/views/cms/manuscript/index.vue +++ b/src/views/cms/manuscript/index.vue @@ -58,7 +58,11 @@ - + import { createVNode, ref } from 'vue'; - import { message, Modal } from 'ant-design-vue'; + import { message, Modal, Tag } from 'ant-design-vue'; import { ExclamationCircleOutlined } from '@ant-design/icons-vue'; import type { EleProTable } from 'ele-admin-pro'; import { toDateString } from 'ele-admin-pro'; @@ -82,9 +86,14 @@ import Search from './components/search.vue'; import ManuscriptEdit from './components/manuscriptEdit.vue'; import ReviewRecordModal from './components/reviewRecordModal.vue'; - import { pageManuscript, removeManuscript, removeBatchManuscript } from '@/api/cms/manuscript'; + import { + pageManuscript, + removeManuscript, + removeBatchManuscript + } from '@/api/cms/manuscript'; import type { Manuscript, ManuscriptParam } from '@/api/cms/manuscript/model'; import type { ReviewList } from '@/api/gxmu/reviewList/model'; + import { detectFormat, normalizeFormat } from '@/utils/manuscript-content'; // 表格实例 const tableRef = ref | null>(null); @@ -130,19 +139,36 @@ dataIndex: 'id', key: 'id', align: 'center', - width: 90, + width: 90 }, { title: '标题', dataIndex: 'title', key: 'title', - align: 'center', + align: 'center' }, { title: '封面', dataIndex: 'cover', key: 'cover', + align: 'center' + }, + { + title: '正文格式', + dataIndex: 'contentFormat', + key: 'contentFormat', align: 'center', + width: 110, + customRender: ({ record }: { record: Manuscript }) => { + const format = + normalizeFormat(record.contentFormat) || detectFormat(record.content); + if (format === 'html') { + return createVNode(Tag, { color: 'default' }, () => 'HTML'); + } + return createVNode(Tag, { color: 'orange' }, () => + format === 'markdown' ? 'Markdown' : '纯文本' + ); + } }, { title: '创建时间', @@ -157,7 +183,7 @@ title: '状态', dataIndex: 'status', key: 'status', - align: 'center', + align: 'center' }, { title: '当前审核状态',