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 }