feat: P0-2抽纯函数+补a-g断言13组+P1-2编辑器空覆盖守卫

P0-2:
- content-adapter.js: 解耦Node crypto(客户端直引),
  新增mergeCmsItem/normalizeThumbnail/buildPublishPayload
- buildPublishPayload补normalizeCmsList归一(抓到vote_id漏生成真bug)
- check-article-flow.js: 4组->13组断言,覆盖a-g

P1-2:
- editText.vue/smallTitle.vue getContents成功回调加空覆盖守卫
- editSetting封面已统一字符串数组,无需改动

验证: verify.js全绿,h5构建90 JS通过
This commit is contained in:
2026-09-12 01:33:35 +08:00
parent 3117146281
commit 25307a755e
4 changed files with 262 additions and 29 deletions
@@ -1,4 +1,13 @@
const crypto = require('crypto')
function randomHex(bytes) {
try {
if (typeof require === 'function') {
return require('crypto').randomBytes(bytes).toString('hex')
}
} catch (e) { /* fall through to Math.random */ }
let out = ''
while (out.length < bytes * 2) out += Math.random().toString(16).slice(2)
return out.slice(0, bytes * 2)
}
function clone(value) {
if (value === undefined || value === null) return value
@@ -59,8 +68,9 @@ function normalizeCmsList(list) {
}, [])
}
function createVoteId() {
return `vote_${Date.now().toString(36)}_${crypto.randomBytes(6).toString('hex')}`
function createVoteId(randomFn) {
const rand = typeof randomFn === 'function' ? randomFn(6) : randomHex(6)
return `vote_${Date.now().toString(36)}_${rand}`
}
function textOps(item) {
@@ -159,12 +169,89 @@ function hasRenderableContent(content, cmsLst) {
)
}
module.exports = {
/**
* 编辑器写回守卫(P1-2 同源逻辑)。
* 有旧内容但新内容为空时返回 null(调用方跳过覆盖并返回上一页),否则返回合并后的条目。
*/
function mergeCmsItem(oldItem, incoming) {
const next = incoming || {}
const nextText = next.text || ''
const nextHtml = next.html || ''
const nextDelta = next.delta
const hadContent = Boolean(oldItem && (oldItem.text || oldItem.html))
const gotContent = Boolean(
nextText || nextHtml || (nextDelta && Array.isArray(nextDelta.ops) && nextDelta.ops.length)
)
if (hadContent && !gotContent) return null
return {
...(oldItem || {}),
text: nextText,
html: nextHtml,
delta: nextDelta && typeof nextDelta === 'object' ? nextDelta : { ops: [] }
}
}
/**
* 封面归一:始终返回字符串数组(d. editSetting 同源逻辑)。
* mode: 0 无封面 / 1 单图 / 3 三图。
*/
function normalizeThumbnail(raw, mode) {
const srcOf = (v) => (typeof v === 'string' ? v : (v && typeof v.src === 'string' ? v.src : ''))
const list = Array.isArray(raw) ? raw.map(srcOf).filter(Boolean) : []
if (mode === 0) return []
if (mode === 1) return list[0] ? [list[0]] : []
if (mode === 3) return list.slice(0, 3)
return list
}
/**
* 发布 payload 构造(e. preview 同源逻辑)。
* 返回 { query } 或 { error },调用方按 error 弹 toast。
*/
function buildPublishPayload(stored, extra) {
const s = stored || {}
const x = extra || {}
if (!s.title || !String(s.title).trim()) return { error: '请先填写标题' }
const cmsLst = normalizeCmsList(Array.isArray(s.cmsLst) ? s.cmsLst : [])
const articleStatus = x.articleStatus === 1 ? 1 : 0
if (articleStatus === 1 && !hasRenderableContent(s.content, cmsLst)) {
return { error: '正文不能为空' }
}
const content = (s.content && Array.isArray(s.content.ops))
? s.content
: cmsLstToDelta(cmsLst)
return {
query: {
title: s.title,
title_html: s.title_html,
title_delta: s.title_delta,
thumbnail: s.thumbnail,
p_type: s.p_type,
category_id: s.category_id,
temp_id: x.tempId || s.temp_id || '',
music_url_id: s.music_url_id || {},
edit_type: 'mobile',
cmsLst,
content,
excerpt: s.excerpt || '',
article_status: articleStatus
}
}
}
const adapterModule = {
clone,
normalizeCmsList,
cmsLstToDelta,
createVoteId,
getArticleText,
getArticleImages,
hasRenderableContent
hasRenderableContent,
mergeCmsItem,
normalizeThumbnail,
buildPublishPayload
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = adapterModule
}