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通过
258 lines
8.2 KiB
JavaScript
258 lines
8.2 KiB
JavaScript
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
|
|
return JSON.parse(JSON.stringify(value))
|
|
}
|
|
|
|
function asString(value) {
|
|
if (typeof value === 'string') return value.trim()
|
|
if (value && typeof value === 'object' && typeof value.src === 'string') return value.src.trim()
|
|
return ''
|
|
}
|
|
|
|
function normalizeCmsList(list) {
|
|
if (!Array.isArray(list)) return []
|
|
|
|
return list.reduce((result, source) => {
|
|
if (!source || typeof source !== 'object') return result
|
|
const item = clone(source)
|
|
const type = typeof item.type === 'string' ? item.type : ''
|
|
if (!type) return result
|
|
|
|
if (type === 'image') {
|
|
item.image = item.image && typeof item.image === 'object' ? item.image : {}
|
|
item.image.src = asString(item.image.src)
|
|
item.image.alt = typeof item.image.alt === 'string' ? item.image.alt : ''
|
|
} else if (type === 'video') {
|
|
item.video = item.video && typeof item.video === 'object' ? item.video : {}
|
|
item.video.src = asString(item.video.src)
|
|
item.video.poster = asString(item.video.poster)
|
|
item.video.duration = Number(item.video.duration) || 0
|
|
} else if (type === 'text' || type === 'title') {
|
|
item.text = typeof item.text === 'string' ? item.text : ''
|
|
item.html = typeof item.html === 'string' ? item.html : ''
|
|
item.delta = item.delta && typeof item.delta === 'object' ? item.delta : {}
|
|
} else if (type === 'vote') {
|
|
item.vote = item.vote && typeof item.vote === 'object' ? item.vote : {}
|
|
item.vote.vote_id = typeof item.vote.vote_id === 'string' && item.vote.vote_id
|
|
? item.vote.vote_id
|
|
: createVoteId()
|
|
item.vote.voteTitle = typeof item.vote.voteTitle === 'string' ? item.vote.voteTitle.trim() : ''
|
|
item.vote.voteLst = Array.isArray(item.vote.voteLst)
|
|
? item.vote.voteLst
|
|
.filter(option => option && typeof option.value === 'string' && option.value.trim())
|
|
.map(option => ({ ...option, value: option.value.trim() }))
|
|
: []
|
|
}
|
|
|
|
if (type === 'image' && !item.image.src) return result
|
|
if (type === 'video' && !item.video.src) return result
|
|
if (type === 'text' || type === 'title') {
|
|
const hasDelta = Array.isArray(item.delta.ops) && item.delta.ops.length > 0
|
|
if (!hasDelta && !item.html && !item.text) return result
|
|
}
|
|
if (type === 'vote' && item.vote.voteLst.length < 2) return result
|
|
|
|
result.push(item)
|
|
return result
|
|
}, [])
|
|
}
|
|
|
|
function createVoteId(randomFn) {
|
|
const rand = typeof randomFn === 'function' ? randomFn(6) : randomHex(6)
|
|
return `vote_${Date.now().toString(36)}_${rand}`
|
|
}
|
|
|
|
function textOps(item) {
|
|
const delta = item && item.delta
|
|
if (delta && Array.isArray(delta.ops) && delta.ops.length) return clone(delta.ops)
|
|
if (item && item.text) return [{ insert: item.text }]
|
|
return []
|
|
}
|
|
|
|
function appendParagraphBreak(ops) {
|
|
if (!ops.length) return ops
|
|
const last = ops[ops.length - 1]
|
|
if (typeof last.insert === 'string' && last.insert.endsWith('\n')) return ops
|
|
ops.push({ insert: '\n' })
|
|
return ops
|
|
}
|
|
|
|
function cmsLstToDelta(list) {
|
|
const cmsLst = normalizeCmsList(list)
|
|
const ops = []
|
|
|
|
for (const item of cmsLst) {
|
|
if (item.type === 'text' || item.type === 'title') {
|
|
const itemOps = textOps(item)
|
|
ops.push(...itemOps)
|
|
appendParagraphBreak(ops)
|
|
continue
|
|
}
|
|
|
|
if (item.type === 'image' && item.image.src) {
|
|
ops.push({ insert: { image: item.image.src }, attributes: item.image.alt ? { alt: item.image.alt } : undefined })
|
|
ops.push({ insert: '\n' })
|
|
continue
|
|
}
|
|
|
|
if (item.type === 'video' && item.video.src) {
|
|
ops.push({
|
|
insert: { video: item.video.src },
|
|
attributes: item.video.poster ? { poster: item.video.poster } : undefined
|
|
})
|
|
ops.push({ insert: '\n' })
|
|
continue
|
|
}
|
|
|
|
if (item.type === 'vote' && item.vote.voteTitle) {
|
|
ops.push({ insert: `${item.vote.voteTitle}\n` })
|
|
}
|
|
}
|
|
|
|
return { ops: ops.map(op => {
|
|
if (op.attributes === undefined) {
|
|
const copy = { ...op }
|
|
delete copy.attributes
|
|
return copy
|
|
}
|
|
return op
|
|
}) }
|
|
}
|
|
|
|
function getArticleText(content, cmsLst) {
|
|
const values = []
|
|
if (content && Array.isArray(content.ops)) {
|
|
for (const op of content.ops) {
|
|
if (typeof op.insert === 'string') values.push(op.insert)
|
|
}
|
|
}
|
|
for (const item of normalizeCmsList(cmsLst)) {
|
|
if (item.type === 'text' || item.type === 'title') values.push(item.text || '')
|
|
if (item.type === 'vote') {
|
|
values.push(item.vote.voteTitle || '')
|
|
values.push(...item.vote.voteLst.map(option => option.value))
|
|
}
|
|
if (item.type === 'video') values.push(item.text || '')
|
|
}
|
|
return values.join('\n').trim()
|
|
}
|
|
|
|
function getArticleImages(content, cmsLst) {
|
|
const images = []
|
|
if (content && Array.isArray(content.ops)) {
|
|
for (const op of content.ops) {
|
|
if (op && op.insert && typeof op.insert.image === 'string') images.push(op.insert.image)
|
|
}
|
|
}
|
|
for (const item of normalizeCmsList(cmsLst)) {
|
|
if (item.type === 'image' && item.image.src) images.push(item.image.src)
|
|
if (item.type === 'video' && item.video.poster) images.push(item.video.poster)
|
|
}
|
|
return [...new Set(images)]
|
|
}
|
|
|
|
function hasRenderableContent(content, cmsLst) {
|
|
return Boolean(
|
|
(content && Array.isArray(content.ops) && content.ops.some(op => op && op.insert !== undefined)) ||
|
|
normalizeCmsList(cmsLst).length
|
|
)
|
|
}
|
|
|
|
/**
|
|
* 编辑器写回守卫(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,
|
|
mergeCmsItem,
|
|
normalizeThumbnail,
|
|
buildPublishPayload
|
|
}
|
|
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = adapterModule
|
|
}
|