diff --git a/src/components/page/GenerateCharts.vue b/src/components/page/GenerateCharts.vue index 48b9f85..66ce818 100644 --- a/src/components/page/GenerateCharts.vue +++ b/src/components/page/GenerateCharts.vue @@ -737,6 +737,10 @@ import bottomTinymce from '@/components/page/components/Tinymce'; import catalogue from '@/components/page/components/table/catalogue.vue'; import editPublicRefTableOnly from './editPublicRefTableOnly.vue'; import { extractAutociteIdsFromHtmlString } from '@/utils/autociteHtml.js'; +import { + collectCiteHtmlSourcesForMainListItem, + extractAutociteOrderFromMainList +} from '@/utils/autociteMainListOrder.js'; export default { data() { return { @@ -1030,7 +1034,7 @@ export default { if (draft == null || draft === '') { return this.articleCiteIdOrder; } - const order = this.extractAutociteOrderFromMainList(this.Main_List, this.currentContent.am_id, draft); + const order = extractAutociteOrderFromMainList(this.Main_List, this.currentContent.am_id, draft); return order.length > 0 ? order : this.articleCiteIdOrder; }, /** @@ -1045,7 +1049,7 @@ export default { const citeMap = this.buildDisplayCiteMap(order || []); list.forEach((p) => { if (!p || Number(p.type) !== 2) return; - const candidates = this.collectCiteHtmlSourcesForMainListItem(p); + const candidates = collectCiteHtmlSourcesForMainListItem(p); const ids = []; candidates.forEach((raw) => { extractAutociteIdsFromHtmlString(raw).forEach((id) => { @@ -1073,7 +1077,6 @@ export default { if (p.table.table_id != null) out[String(p.table.table_id)] = max; } }); - console.log('[tableLinkCiteMaxMap]', out); return out; }, /** 表格抽屉内角标:与 Edit Content 同源,按 Main_List + Title/表/Note 合并稿做全文首次出现排序,避免仍用 articleCiteIdOrder 列表下标 [38] */ @@ -1085,7 +1088,12 @@ export default { if (draft == null || draft === '') { return this.articleCiteIdOrder; } - const order = this.extractAutociteOrderFromMainList(this.Main_List, this.lineStyle.am_id, draft); + /** 正文里 指向本表时,展开用当前抽屉合并稿,角标与「全文该 mytable 之前」顺序一致 */ + const citeOpts = { + tableDraftAmId: this.lineStyle.am_id, + tableDraftHtml: draft + }; + const order = extractAutociteOrderFromMainList(this.Main_List, this.lineStyle.am_id, draft, citeOpts); return order.length > 0 ? order : this.articleCiteIdOrder; } }, @@ -1535,9 +1543,10 @@ export default { * 按 Main_List 各段正文中 mycite 首次出现顺序重排参考文献。 * draftAmId + draftHtml:合并「当前将保存的这一段」后再扫全文(点 Save 时与保存后一致); * 传 (null, null) 表示完全以 Main_List 已存 content 为准。 + * citeOptions:Edit Table 时传入 { tableDraftAmId, tableDraftHtml },使正文 mytable 展开与抽屉稿一致。 */ - reorderReferencesFromMainListBody(draftAmId, draftHtml) { - const order = this.extractAutociteOrderFromMainList(this.Main_List, draftAmId, draftHtml); + reorderReferencesFromMainListBody(draftAmId, draftHtml, citeOptions) { + const order = extractAutociteOrderFromMainList(this.Main_List, draftAmId, draftHtml, citeOptions || {}); if (order.length > 0) { this.handleReorderReferencesByBody(order); } @@ -1587,7 +1596,11 @@ export default { this.reorderReferencesFromMainListBody(null, null); return; } - this.reorderReferencesFromMainListBody(this.lineStyle.am_id, html); + const citeOpts = { + tableDraftAmId: this.lineStyle.am_id, + tableDraftHtml: html + }; + this.reorderReferencesFromMainListBody(this.lineStyle.am_id, html, citeOpts); }, scheduleFlushReorderFromTableModal() { if (!this.threeVisible || !this.lineStyle || this.lineStyle.am_id == null) return; @@ -1743,77 +1756,6 @@ export default { if (sig(next) === sig(refs)) return; this.chanFerForm = next; }, - /** - * 单条 Main_List 段内、与稿面阅读顺序一致的 HTML 片段(用于统计 mycite 顺序)。 - * 表格段:标题 → 表体(html_data 或逐单元格 text)→ Note;图段:标题 → 说明。 - */ - collectCiteHtmlSourcesForMainListItem(p, draftAmId, draftHtml) { - if (draftAmId != null && p && p.am_id == draftAmId && draftHtml != null) { - return [draftHtml]; - } - const out = []; - const typ = p != null && p.type != null ? Number(p.type) : NaN; - if (typ === 2 && p.table) { - const t = p.table; - if (typeof t.title === 'string' && t.title.trim()) out.push(t.title); - if (typeof t.html_data === 'string' && t.html_data.trim()) { - out.push(t.html_data); - } else { - const pushRows = (rows) => { - if (!Array.isArray(rows)) return; - rows.forEach((row) => { - if (!Array.isArray(row)) return; - row.forEach((cell) => { - if (cell && typeof cell.text === 'string' && cell.text) out.push(cell.text); - }); - }); - }; - pushRows(t.tableHeader); - pushRows(t.tableContent); - } - if (typeof t.note === 'string' && t.note.trim()) out.push(t.note); - // 兜底:不同接口版本的表体字段名不一致,深度扫描 table 对象里所有可能含引用的字符串字段。 - this.collectCiteStringsDeep(t, out); - return out; - } - if (typ === 1 && p.image) { - const img = p.image; - if (typeof img.title === 'string' && img.title.trim()) out.push(img.title); - if (typeof img.note === 'string' && img.note.trim()) out.push(img.note); - if (out.length === 0) { - if (p && typeof p.content === 'string' && p.content.trim()) out.push(p.content); - if (p && typeof p.text === 'string' && p.text.trim()) out.push(p.text); - } - return out; - } - if (p && typeof p.text === 'string' && p.text.trim()) out.push(p.text); - if (p && typeof p.content === 'string' && p.content.trim()) out.push(p.content); - return out; - }, - /** - * 递归收集对象中的字符串字段,覆盖 html/text/content 等异构字段名。 - * 仅保留看起来可能包含引用信息的片段,避免噪音过大。 - */ - collectCiteStringsDeep(source, out, depth = 0) { - if (!source || depth > 6) return; - if (typeof source === 'string') { - const s = source.trim(); - if (!s) return; - const maybeCite = /<(?:mycite|autocite)\b/i.test(s) || /\[[\d\s,,\-–—]+\]/.test(s); - if (maybeCite) out.push(s); - return; - } - if (Array.isArray(source)) { - source.forEach((item) => this.collectCiteStringsDeep(item, out, depth + 1)); - return; - } - if (typeof source === 'object') { - Object.keys(source).forEach((k) => { - const v = source[k]; - this.collectCiteStringsDeep(v, out, depth + 1); - }); - } - }, extractBracketCiteNumbersFromText(raw) { const out = []; if (!raw || typeof raw !== 'string') return out; @@ -1842,20 +1784,6 @@ export default { } return out; }, - /** 与 word.vue 一致:从各段 HTML 中按出现顺序收集 autocite(含表格单元格、表题、表注、图题图注) */ - extractAutociteOrderFromMainList(mainList, draftAmId, draftHtml) { - const list = Array.isArray(mainList) ? mainList : []; - const order = []; - list.forEach((p) => { - const candidates = this.collectCiteHtmlSourcesForMainListItem(p, draftAmId, draftHtml); - candidates.forEach((raw) => { - extractAutociteIdsFromHtmlString(raw).forEach((id) => { - if (!order.includes(id)) order.push(id); - }); - }); - }); - return order; - }, /** 编辑弹窗内正文一变立即按全文合并稿重排参考文献(不再防抖,选中/输入后列表马上跟正文一致) */ scheduleReorderFromEditModal(html) { if (!this.editVisible || !this.currentContent || this.currentContent.am_id == null) return; @@ -1866,7 +1794,7 @@ export default { /** 立即按全文合并稿重排参考文献顺序(插入引用后调用,避免编号长期停留在列表序号 [8]) */ flushReorderFromEditModal(html) { if (!this.editVisible || !this.currentContent || this.currentContent.am_id == null) return; - let order = this.extractAutociteOrderFromMainList(this.Main_List, this.currentContent.am_id, html); + let order = extractAutociteOrderFromMainList(this.Main_List, this.currentContent.am_id, html); if (order.length === 0 && html && /(autocite|mycite)/i.test(html)) { order = extractAutociteIdsFromHtmlString(html); } diff --git a/src/components/page/comArtHtmlCreatNew.vue b/src/components/page/comArtHtmlCreatNew.vue index 7ea11a6..0d923d8 100644 --- a/src/components/page/comArtHtmlCreatNew.vue +++ b/src/components/page/comArtHtmlCreatNew.vue @@ -82,6 +82,7 @@ import bottomTinymce from '@/components/page/components/Tinymce'; import catalogue from '@/components/page/components/table/catalogue.vue'; import editPublicRefTableOnly from './editPublicRefTableOnly.vue'; import { extractAutociteIdsFromHtmlString } from '@/utils/autociteHtml.js'; +import { extractAutociteOrderFromMainList } from '@/utils/autociteMainListOrder.js'; export default { data() { return { @@ -327,7 +328,7 @@ export default { if (draft == null || draft === '') { return this.articleCiteIdOrder; } - const order = this.extractAutociteOrderFromMainList(this.Main_List, this.currentContent.am_id, draft); + const order = extractAutociteOrderFromMainList(this.Main_List, this.currentContent.am_id, draft); return order.length > 0 ? order : this.articleCiteIdOrder; } }, @@ -508,7 +509,7 @@ export default { * 传 (null, null) 表示完全以 Main_List 已存 content 为准。 */ reorderReferencesFromMainListBody(draftAmId, draftHtml) { - const order = this.extractAutociteOrderFromMainList(this.Main_List, draftAmId, draftHtml); + const order = extractAutociteOrderFromMainList(this.Main_List, draftAmId, draftHtml); if (order.length > 0) { this.handleReorderReferencesByBody(order); } @@ -646,65 +647,6 @@ export default { if (sig(next) === sig(refs)) return; this.chanFerForm = next; }, - /** - * 单条 Main_List 段内、与稿面阅读顺序一致的 HTML 片段(用于统计 mycite 顺序)。 - * 表格段:标题 → 表体(html_data 或逐单元格 text)→ Note;图段:标题 → 说明。 - */ - collectCiteHtmlSourcesForMainListItem(p, draftAmId, draftHtml) { - if (draftAmId != null && p && p.am_id == draftAmId && draftHtml != null) { - return [draftHtml]; - } - const out = []; - const typ = p != null && p.type != null ? Number(p.type) : NaN; - if (typ === 2 && p.table) { - const t = p.table; - if (typeof t.title === 'string' && t.title.trim()) out.push(t.title); - if (typeof t.html_data === 'string' && t.html_data.trim()) { - out.push(t.html_data); - } else { - const pushRows = (rows) => { - if (!Array.isArray(rows)) return; - rows.forEach((row) => { - if (!Array.isArray(row)) return; - row.forEach((cell) => { - if (cell && typeof cell.text === 'string' && cell.text) out.push(cell.text); - }); - }); - }; - pushRows(t.tableHeader); - pushRows(t.tableContent); - } - if (typeof t.note === 'string' && t.note.trim()) out.push(t.note); - return out; - } - if (typ === 1 && p.image) { - const img = p.image; - if (typeof img.title === 'string' && img.title.trim()) out.push(img.title); - if (typeof img.note === 'string' && img.note.trim()) out.push(img.note); - if (out.length === 0) { - if (p && typeof p.content === 'string' && p.content.trim()) out.push(p.content); - if (p && typeof p.text === 'string' && p.text.trim()) out.push(p.text); - } - return out; - } - if (p && typeof p.text === 'string' && p.text.trim()) out.push(p.text); - if (p && typeof p.content === 'string' && p.content.trim()) out.push(p.content); - return out; - }, - /** 与 word.vue 一致:从各段 HTML 中按出现顺序收集 autocite(含表格单元格、表题、表注、图题图注) */ - extractAutociteOrderFromMainList(mainList, draftAmId, draftHtml) { - const list = Array.isArray(mainList) ? mainList : []; - const order = []; - list.forEach((p) => { - const candidates = this.collectCiteHtmlSourcesForMainListItem(p, draftAmId, draftHtml); - candidates.forEach((raw) => { - extractAutociteIdsFromHtmlString(raw).forEach((id) => { - if (!order.includes(id)) order.push(id); - }); - }); - }); - return order; - }, /** 编辑弹窗内正文一变立即按全文合并稿重排参考文献(不再防抖,选中/输入后列表马上跟正文一致) */ scheduleReorderFromEditModal(html) { if (!this.editVisible || !this.currentContent || this.currentContent.am_id == null) return; @@ -715,7 +657,7 @@ export default { /** 立即按全文合并稿重排参考文献顺序(插入引用后调用,避免编号长期停留在列表序号 [8]) */ flushReorderFromEditModal(html) { if (!this.editVisible || !this.currentContent || this.currentContent.am_id == null) return; - let order = this.extractAutociteOrderFromMainList(this.Main_List, this.currentContent.am_id, html); + let order = extractAutociteOrderFromMainList(this.Main_List, this.currentContent.am_id, html); if (order.length === 0 && html && /mycite/i.test(html)) { order = extractAutociteIdsFromHtmlString(html); } diff --git a/src/components/page/components/Tinymce/index.vue b/src/components/page/components/Tinymce/index.vue index 96382c7..04d29fa 100644 --- a/src/components/page/components/Tinymce/index.vue +++ b/src/components/page/components/Tinymce/index.vue @@ -436,7 +436,6 @@ export default { const tid = (node.getAttribute && node.getAttribute('data-id')) || ''; const map = this.tableLinkCiteMaxMap || {}; const tableMax = Number(map[String(tid)]); - console.log('[autolink/mytable]', { tid, tableMax, map }); if (!Number.isNaN(tableMax) && tableMax > 0) { state.tableOffset = tableMax; } @@ -472,13 +471,6 @@ export default { // 表格链接后的局部序号从 tableMax+1 开始接续:1->max+1, 2->max+2 ... const mappedNo = n > 0 && tableOffset > 0 ? n + tableOffset - 1 : n; const id = numToId[mappedNo]; - console.log('[autolink/map]', { - rawBracket: m[0], - n, - tableOffset, - mappedNo, - id - }); if (id && !seen.has(id)) { seen.add(id); ids.push(id); @@ -545,14 +537,12 @@ export default { const sortedAll = this.sortAutociteIdsByCiteNumber( this.parseAutociteDataIds(el.getAttribute('data-id')) ); - /** 已删除的文献 id 从角标中忽略,并写回 data-id,仅保留仍存在于列表中的 id */ const validIds = sortedAll.filter((id) => refMap[String(id)]); if (validIds.length < sortedAll.length) { ed.dom.setAttrib(el, 'data-id', validIds.length ? validIds.join(',') : ''); } const sortedIds = validIds.length ? this.sortAutociteIdsByCiteNumber(validIds) : []; - /** 角标只展示仍绑定且已有全局序号的文献 */ const parts = sortedIds.map((id) => { const ref = refMap[String(id)]; const no = ref ? citeMap[String(id)] : null; @@ -564,8 +554,7 @@ export default { .map((p) => p.num) .filter((n) => n != null && n !== '') .map(Number); - const label = - numsForLabel.length > 0 ? this.formatCiteNumbers(numsForLabel) : ''; + const label = numsForLabel.length > 0 ? this.formatCiteNumbers(numsForLabel) : ''; const lines = parts .filter((p) => p.ref) @@ -583,7 +572,6 @@ export default { return `${content}\nDOI: ${doi}`; }); - /** 无可展示序号或全部 id 已失效时,直接移除节点,不保留空标签 */ if (!label) { ed.dom.remove(el); return; @@ -592,7 +580,6 @@ export default { el.textContent = `[${label}]`; el.style.display = ''; ed.dom.setAttrib(el, 'title', lines.join('\n')); - /** 不再使用 data-cite-missing 红底提示,角标样式与普通引用一致 */ ed.dom.setAttrib(el, 'data-cite-missing', null); }); this.padAutociteCaretPlaceholder(ed); diff --git a/src/components/page/components/table/word.vue b/src/components/page/components/table/word.vue index 74d01ef..d974283 100644 --- a/src/components/page/components/table/word.vue +++ b/src/components/page/components/table/word.vue @@ -1076,6 +1076,7 @@ import { TableUtils } from '@/common/js/TableUtils'; import { debounce, throttle } from '@/common/js/debounce'; import { tableStyle, commonWordStyle } from '@/utils/tinymceStyles'; import { extractAutociteIdsFromHtmlString } from '@/utils/autociteHtml.js'; +import { extractAutociteOrderFromMainList } from '@/utils/autociteMainListOrder.js'; export default { name: 'tinymce', components: {}, @@ -2531,22 +2532,9 @@ renderCiteLabels(html) { }, 0); }, syncRefOrder() { - const root = this.$refs.scrollDiv; - if (!root) return; - const nodes = root.querySelectorAll('mycite'); - const seen = new Set(); - const order = []; - nodes.forEach((el) => { - const raw = el.getAttribute('data-id') || ''; - raw.split(',').forEach((part) => { - const id = part.trim(); - if (!id) return; - if (!seen.has(id)) { - seen.add(id); - order.push(id); - } - }); - }); + /** 与 GenerateCharts extractAutociteOrderFromMainList 一致:Main_List 顺序 + 正文内 mytable 处插入表内引用后再计后续角标(勿用 DOM 中 mycite 顺序,否则稿面与弹窗不一致) */ + const list = this.contentList || []; + const order = extractAutociteOrderFromMainList(list, null, null); const prev = this.bodyCiteIdOrder || []; const sameBody = order.length === prev.length && order.every((id, i) => String(id) === String(prev[i])); diff --git a/src/utils/autociteMainListOrder.js b/src/utils/autociteMainListOrder.js new file mode 100644 index 0000000..ac02cd1 --- /dev/null +++ b/src/utils/autociteMainListOrder.js @@ -0,0 +1,161 @@ +/** + * Main_List 全文首次出现顺序(与 Edit Content / 稿面 / 底部参考文献列表共用)。 + * 正文中 占位处需先插入对应表格段在 Main_List 中的表题/表体/表注引用,再计其后的 mycite。 + */ +import { extractAutociteIdsFromHtmlString } from '@/utils/autociteHtml.js'; + +export function collectCiteStringsDeep(source, out, depth = 0) { + if (!source || depth > 6) return; + if (typeof source === 'string') { + const s = source.trim(); + if (!s) return; + const maybeCite = /<(?:mycite|autocite)\b/i.test(s) || /\[[\d\s,,\-–—]+\]/.test(s); + if (maybeCite) out.push(s); + return; + } + if (Array.isArray(source)) { + source.forEach((item) => collectCiteStringsDeep(item, out, depth + 1)); + return; + } + if (typeof source === 'object') { + Object.keys(source).forEach((k) => { + const v = source[k]; + collectCiteStringsDeep(v, out, depth + 1); + }); + } +} + +export function collectCiteHtmlSourcesForMainListItem(p, draftAmId, draftHtml) { + if (draftAmId != null && p && p.am_id == draftAmId && draftHtml != null) { + return [draftHtml]; + } + const out = []; + const typ = p != null && p.type != null ? Number(p.type) : NaN; + if (typ === 2 && p.table) { + const t = p.table; + if (typeof t.title === 'string' && t.title.trim()) out.push(t.title); + if (typeof t.html_data === 'string' && t.html_data.trim()) { + out.push(t.html_data); + } else { + const pushRows = (rows) => { + if (!Array.isArray(rows)) return; + rows.forEach((row) => { + if (!Array.isArray(row)) return; + row.forEach((cell) => { + if (cell && typeof cell.text === 'string' && cell.text) out.push(cell.text); + }); + }); + }; + pushRows(t.tableHeader); + pushRows(t.tableContent); + } + if (typeof t.note === 'string' && t.note.trim()) out.push(t.note); + collectCiteStringsDeep(t, out); + return out; + } + if (typ === 1 && p.image) { + const img = p.image; + if (typeof img.title === 'string' && img.title.trim()) out.push(img.title); + if (typeof img.note === 'string' && img.note.trim()) out.push(img.note); + if (out.length === 0) { + if (p && typeof p.content === 'string' && p.content.trim()) out.push(p.content); + if (p && typeof p.text === 'string' && p.text.trim()) out.push(p.text); + } + return out; + } + if (p && typeof p.text === 'string' && p.text.trim()) out.push(p.text); + if (p && typeof p.content === 'string' && p.content.trim()) out.push(p.content); + return out; +} + +export function findMainListTableByLinkId(mainList, rawId) { + if (rawId == null || String(rawId).trim() === '') return null; + const sid = String(rawId).trim(); + const list = Array.isArray(mainList) ? mainList : []; + return ( + list.find((p) => { + if (!p || Number(p.type) !== 2) return false; + if (String(p.amt_id) === sid) return true; + if (String(p.am_id) === sid) return true; + if (p.p_main_table_id != null && String(p.p_main_table_id) === sid) return true; + if (p.table && p.table.amt_id != null && String(p.table.amt_id) === sid) return true; + if (p.table && p.table.am_id != null && String(p.table.am_id) === sid) return true; + if (p.table && p.table.p_main_table_id != null && String(p.table.p_main_table_id) === sid) + return true; + if (p.table && p.table.table_id != null && String(p.table.table_id) === sid) return true; + return false; + }) || null + ); +} + +/** + * @param {object} [options] + * @param {number|string|null} [options.tableDraftAmId] 正在编辑的表格段 am_id(Edit Table 抽屉) + * @param {string|null} [options.tableDraftHtml] 与该表对应的 Title+表体+Note 合并稿;正文里 mytable 展开到该表时用此稿替代 Main_List 已存内容,使「全文 mytable 之前」的序号与抽屉内一致 + */ +export function extractAutociteOrderFromDraftHtmlWithInlineTables(html, mainList, options = {}) { + const order = []; + const pushUnique = (id) => { + if (id && !order.includes(id)) order.push(id); + }; + const pushFromRaw = (raw) => { + extractAutociteIdsFromHtmlString(raw).forEach((id) => pushUnique(id)); + }; + if (!html || typeof html !== 'string') return order; + + const re = /]*>[\s\S]*?<\/mytable>/gi; + let last = 0; + let m; + while ((m = re.exec(html)) !== null) { + const before = html.slice(last, m.index); + if (before) pushFromRaw(before); + + const openEnd = html.indexOf('>', m.index) + 1; + const openTag = html.slice(m.index, openEnd); + const dm = openTag.match(/\bdata-id\s*=\s*["']([^"']*)["']/i); + const tid = dm ? String(dm[1]).trim() : ''; + const tableP = findMainListTableByLinkId(mainList, tid); + if (tableP) { + const useDraft = + options.tableDraftAmId != null && + options.tableDraftHtml != null && + String(tableP.am_id) === String(options.tableDraftAmId); + const sources = useDraft + ? [options.tableDraftHtml] + : collectCiteHtmlSourcesForMainListItem(tableP, null, null); + sources.forEach((raw) => pushFromRaw(raw)); + } + + last = m.index + m[0].length; + } + const after = html.slice(last); + if (after) pushFromRaw(after); + + return order; +} + +export function extractAutociteOrderFromMainList(mainList, draftAmId, draftHtml, options = {}) { + const list = Array.isArray(mainList) ? mainList : []; + const order = []; + list.forEach((p) => { + if (draftAmId != null && p && p.am_id == draftAmId && draftHtml != null) { + extractAutociteOrderFromDraftHtmlWithInlineTables(draftHtml, list, options).forEach((id) => { + if (!order.includes(id)) order.push(id); + }); + return; + } + const candidates = collectCiteHtmlSourcesForMainListItem(p, draftAmId, draftHtml); + candidates.forEach((raw) => { + if (typeof raw === 'string' && / { + if (!order.includes(id)) order.push(id); + }); + } else { + extractAutociteIdsFromHtmlString(raw).forEach((id) => { + if (!order.includes(id)) order.push(id); + }); + } + }); + }); + return order; +}