From 39d69108f7e51211c9ea6cb7cb1be324c4daca70 Mon Sep 17 00:00:00 2001 From: cs10086 <1291454411@qq.com> Date: Sun, 13 Sep 2026 07:12:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(P1-1=E6=9C=AC=E5=9C=B0/P2-1):=20beforeUpda?= =?UTF-8?q?te=E8=8D=89=E7=A8=BF=E8=BD=AC=E5=8F=91=E5=B8=83=E5=90=88?= =?UTF-8?q?=E5=B9=B6=E6=97=A7=E6=96=87=E6=A1=A3fail-closed=20/=20es.json?= =?UTF-8?q?=20ASCII=E5=AE=89=E5=85=A8=E8=A5=BF=E8=AF=AD=EF=BC=88=E5=A4=87?= =?UTF-8?q?=E4=BB=BD.zcode/bak=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../database/uni-cms-articles.schema.ext.js | 824 +++++++++--------- .../components/uni-pagination/i18n/es.json | 10 +- 2 files changed, 426 insertions(+), 408 deletions(-) diff --git a/uniCloud-alipay/database/uni-cms-articles.schema.ext.js b/uniCloud-alipay/database/uni-cms-articles.schema.ext.js index 3b5da4e..bb5726c 100644 --- a/uniCloud-alipay/database/uni-cms-articles.schema.ext.js +++ b/uniCloud-alipay/database/uni-cms-articles.schema.ext.js @@ -1,403 +1,421 @@ -// 获取配置 -const createConfig = safeRequire('uni-config-center') -const {QuillDeltaToHtmlConverter, QuillDeltaToJSONConverter} = safeRequire('quill-delta-converter') -const config = createConfig({ - pluginId: 'uni-cms' -}).config() - -// 获取数据库实例 -const db = uniCloud.database() - -// 文章数据库名称 -const articleDBName = 'uni-cms-articles' - -// 解锁内容数据库名称 -const unlockContentDBName = 'uni-cms-unlock-record' - -function cmsLstText(list) { - if (!Array.isArray(list)) return '' - return list.map(item => { - if (!item || typeof item !== 'object') return '' - if (item.type === 'text' || item.type === 'title') return item.text || item.html || '' - if (item.type === 'video') return item.text || '' - if (item.type === 'vote' && item.vote) { - return [item.vote.voteTitle, ...(Array.isArray(item.vote.voteLst) ? item.vote.voteLst.map(option => option && option.value) : [])] - .filter(Boolean).join(' ') - } - return '' - }).filter(Boolean).join('\n') -} - -function cmsLstImages(list) { - if (!Array.isArray(list)) return [] - return list.reduce((images, item) => { - if (!item || typeof item !== 'object') return images - if (item.type === 'image' && item.image && item.image.src) images.push(item.image.src) - if (item.type === 'video' && item.video && item.video.poster) images.push(item.video.poster) - return images - }, []) -} - -function contentText(content, cmsLst) { - const deltaText = content && Array.isArray(content.ops) - ? content.ops.map(op => typeof op.insert === 'string' ? op.insert : '').join(' ') - : '' - return [deltaText, cmsLstText(cmsLst)].filter(Boolean).join('\n').trim() -} - -function contentImages(content, cmsLst) { - const deltaImages = content && Array.isArray(content.ops) - ? content.ops.filter(op => op && op.insert && typeof op.insert.image === 'string').map(op => op.insert.image) - : [] - return [...new Set([...deltaImages, ...cmsLstImages(cmsLst)])] -} - -// 安全检测文本内容 -async function checkContentSec(content, requestId, errorMsg) { - // 安全引入内容安全检测模块 - const UniSecCheck = safeRequire('uni-sec-check') - // 创建内容安全检测实例 - const uniSecCheck = new UniSecCheck({ - provider: 'mp-weixin', - requestId - }) - // 调用文本安全检测接口 - const res = await uniSecCheck.textSecCheck({ - content, // 待检测的文本内容 - scene: 1, // 表示资料类场景 - version: 1 // 调用检测API的版本号 - }) - - // 如果存在敏感词,抛出异常 - if (res.errCode === uniSecCheck.ErrorCode.RISK_CONTENT) { - throw new Error(errorMsg || '存在敏感词,请修改后提交') - } else if (res.errCode !== 0) { - console.error(res) - throw new Error('内容安全检测异常:' + res.errCode) - } -} - -// 安全检测图片内容 -async function checkImageSec(image, requestId, errorMsg) { - // 安全引入内容安全检测模块 - const UniSecCheck = safeRequire('uni-sec-check') - // 创建内容安全检测实例 - const uniSecCheck = new UniSecCheck({ - provider: 'mp-weixin', - requestId - }) - - const images = typeof image === "string" ? [image]: image - - for (let item of images) { - // 处理cloud://开头的链接 - if (item.startsWith('cloud://')) { - const res = await uniCloud.getTempFileURL({ - fileList: [item] - }) - - if (res.fileList && res.fileList.length > 0) { - item = res.fileList[0].tempFileURL - } - } - - // 调用图片安全检测接口 - const res = await uniSecCheck.imgSecCheck({ - image: item, // 待检测的图片URL - scene: 1, // 表示资料类场景 - version: 1 // 调用检测API的版本号 - }) - - // 如果存在违规内容,抛出异常 - if (res.errCode === uniSecCheck.ErrorCode.RISK_CONTENT) { - throw new Error(errorMsg || '图片违规,请修改后提交') - } else if (res.errCode !== 0) { - console.error(res) - throw new Error('内容安全检测异常:' + res.errCode) - } - } -} - -// 检测内容安全开关 -function checkContentSecurityEnable(field) { - // 1. 从配置中心获取配置 - return config.contentSecurity && config.contentSecurity.allowCheckType && config.contentSecurity.allowCheckType.includes(field) -} - -// 安全require -function safeRequire(module) { - try { - return require(module) - } catch (e) { - if (e.code === 'MODULE_NOT_FOUND') { - throw new Error(`${module} 公共模块不存在,请在 uniCloud/database 目录右击"配置schema扩展公共模块"添加 ${module} 模块`) - } - } -} - -module.exports = { - trigger: { - // 创建文章前触发 - beforeCreate: async function ({clientInfo, addDataList}) { - // addDataList 是一个数组,因为可以一次性创建多条数据 - if (addDataList.length <= 0) return - - // 检测内容安全开关 - const allowCheckContent = checkContentSecurityEnable('content') - const allowCheckImage = checkContentSecurityEnable('image') - - // 遍历数组,对每一条数据进行安全检测 - for (const addData of addDataList) { - // 如果是草稿,不检测 - if (addData.article_status !== 1) continue - - // 并行检测 - const parallel = [] - // 检测标题 - if (allowCheckContent && addData.title) { - parallel.push(checkContentSec(addData.title, clientInfo.requestId, '标题存在敏感字,请修改后提交')) - } - // 检测摘要 - if (allowCheckContent && addData.excerpt) { - parallel.push(checkContentSec(addData.excerpt, clientInfo.requestId, '摘要存在敏感字,请修改后提交')) - } - // 检测内容:兼容标准 Delta 与移动端 cmsLst - const text = contentText(addData.content, addData.cmsLst) - if (allowCheckContent && text) { - parallel.push(checkContentSec(text, clientInfo.requestId, '内容存在敏感字,请修改后提交')) - } - // 检测封面、正文图片和视频封面 - const images = [...(Array.isArray(addData.thumbnail) ? addData.thumbnail : []), ...contentImages(addData.content, addData.cmsLst)] - if (allowCheckImage && images.length) { - parallel.push(checkImageSec(images, clientInfo.requestId, '图片存在违规,请修改后提交')) - } - // 等待所有并行检测完成 - await Promise.all(parallel) - } - }, - // 更新文章前触发 - beforeUpdate: async function ({clientInfo, where, updateData}) { - const id = where && where._id - - if (!id) return - - // 如果是草稿,不检测 - if (updateData.article_status !== 1) return - - // 检测内容安全开关 - const allowCheckContent = checkContentSecurityEnable('content') - const allowCheckImage = checkContentSecurityEnable('image') - - // 并行检测 - const parallel = [] - - // 检测标题 - if (allowCheckContent && updateData.title) { - parallel.push(checkContentSec(updateData.title, clientInfo.requestId, '标题存在敏感字,请修改后提交')) - } - // 检测摘要 - if (allowCheckContent && updateData.excerpt) { - parallel.push(checkContentSec(updateData.excerpt, clientInfo.requestId, '摘要存在敏感字,请修改后提交')) - } - // 检测内容:兼容标准 Delta 与移动端 cmsLst - const text = contentText(updateData.content, updateData.cmsLst) - if (allowCheckContent && text) { - parallel.push(checkContentSec(text, clientInfo.requestId, '内容存在敏感字,请修改后提交')) - } - // 检测封面、正文图片和视频封面 - const images = [...(Array.isArray(updateData.thumbnail) ? updateData.thumbnail : []), ...contentImages(updateData.content, updateData.cmsLst)] - if (allowCheckImage && images.length) { - parallel.push(checkImageSec(images, clientInfo.requestId, '图片存在违规,请修改后提交')) - } - - // 等待所有并行检测完成 - await Promise.all(parallel) - - }, - // 读取文章后触发 - afterRead: async function ({userInfo, clientInfo, result, where, field}) { - const isAdmin = field && field.length && field.includes('is_admin') - // 检查是否配置了clientAppIds字段,如果没有则抛出错误 - if ((!config.clientAppIds || !config.clientAppIds.length) && !isAdmin) { - throw new Error('请在 uni-cms 配置文件中配置 clientAppIds 字段后访问,详见:https://uniapp.dcloud.net.cn/uniCloud/uni-cms.html#uni-cms-config') - } - - // 如果clientAppIds字段未配置或当前appId不在clientAppIds中,则返回 - if (!config.clientAppIds || !config.clientAppIds.includes(clientInfo.appId)) return - - // 获取广告配置 - const adConfig = config.adConfig || {} - - // 获取文章id - const id = where && where._id - - // 如果id不存在或者field不包含content,则返回 - if (id && field.includes('content')) { - // 读取了content字段后view_count加1 - await db.collection(articleDBName).where(where).update({ - view_count: db.command.inc(1) - }) - } - - // 如果查询结果为空,则返回 - if (!result.data || result.data.length <= 0) return - - // 获取文章 - const article = result.data[0] - - // 移动编辑器保留 cmsLst,标准渲染器使用 content;移动文章不依赖 content 才能展示 - if (article.edit_type === 'mobile') { - article.content_images = contentImages(article.content, article.cmsLst) - return - } - - // 如果文章内容不存在或格式异常,则返回空内容,避免 afterRead 直接抛异常 - if (!article.content || !Array.isArray(article.content.ops)) { - article.content_images = [] - article.content = { ops: [] } - return - } - - let needUnlock = false - let unlockContent = [] - - // 获取文章内容中的图片 - article.content_images = article.content.ops.reduce((imageBlocks, block) => { - if (!block || !block.insert || typeof block.insert.image !== 'string') return imageBlocks - - const {attributes} = block - const {'data-custom': custom = ""} = attributes || {} - const parseCustom = custom.split('&').reduce((obj, item) => { - const [key, value] = item.split('=') - if (key) obj[key] = value - return obj - }, {}) - - return imageBlocks.concat( - parseCustom.source || - block.insert.image - ) - }, []) - - for (const op of article.content.ops) { - if (!op || !op.insert) continue - unlockContent.push(op) - // 遍历文章内容,找到解锁内容 - if (op.insert.unlockContent) { - needUnlock = true - break - } - } - - // 如果文章不需要解锁,则返回 - if (!needUnlock) { - article.content = getRenderableArticleContent(article.content, clientInfo) - return - } - - // 获取唯一标识符 - const uniqueId = adConfig.watchAdUniqueType === 'user' ? userInfo.uid : clientInfo.deviceId - - // 如果未登录或者文章未解锁,则返回解锁内容 - if (!uniqueId || !article._id) { - article.content = getRenderableArticleContent({ - ops: unlockContent - }, clientInfo) - return - } - - // 查询解锁记录 - const unlockRecord = await db.collection(unlockContentDBName).where({ - unique_id: uniqueId, - article_id: article._id - }).get() - - // 如果未解锁,则返回解锁内容 - if (unlockRecord.data && unlockRecord.data.length <= 0) { - article.content = getRenderableArticleContent({ - ops: unlockContent - }, clientInfo) - return - } - - // 将文章解锁替换为行结束符 \n - article.content = getRenderableArticleContent({ - ops: article.content.ops.map(op => { - if (op.insert.unlockContent) { - op.insert = "\n" - } - return op - }) - }, clientInfo) - } - } -} - -function getRenderableArticleContent (rawArticleContent, clientInfo) { - const isUniAppX = /uni-app-x/i.test(clientInfo.userAgent) - - if (!isUniAppX) { - const quillDeltaConverter = new QuillDeltaToJSONConverter(rawArticleContent.ops) - return quillDeltaConverter.convert() - } - - const deltaOps = [] - - for (let i = 0; i < rawArticleContent.ops.length; i++) { - const op = rawArticleContent.ops[i] - - if (typeof op.insert === 'object') { - const insertType = Object.keys(op.insert) - const blockRenderList = ['image', 'divider', 'unlockContent', 'mediaVideo'] - if (insertType && insertType.length > 0 && blockRenderList.includes(insertType[0])) { - deltaOps.push({ - type: insertType[0], - ops: [op] - }) - - // 一般块级节点后面都跟一个换行,需要把这个换行给去掉 - const nextOps = rawArticleContent.ops[i + 1] - if (nextOps && nextOps.insert === '\n') { - i ++ - } - - continue - } - } - - const currentIndex = deltaOps.length > 0 ? deltaOps.length - 1: 0 - if ( - typeof deltaOps[currentIndex] !== "object" || - (deltaOps[currentIndex] && deltaOps[currentIndex].type !== 'rich-text') - ) { - deltaOps.push({ - type: 'rich-text', - ops: [] - }) - } - - deltaOps[deltaOps.length - 1].ops.push(op) - } - - return deltaOps.reduce((content, item) => { - const isRichText = item.type === 'rich-text' - let block = { - type: item.type, - data: isRichText ? item.ops: item.ops[0] - } - - if (item.type === 'rich-text') { - const lastOp = item.ops.length > 0 ? item.ops[item.ops.length - 1]: null - - if (lastOp !== null && lastOp.insert === "\n") { - item.ops.pop() - } - - const quillDeltaConverter = new QuillDeltaToHtmlConverter(item.ops) - - block.data = quillDeltaConverter.convert() - } - - return content.concat(block) - }, []) -} +// 获取配置 +const createConfig = safeRequire('uni-config-center') +const {QuillDeltaToHtmlConverter, QuillDeltaToJSONConverter} = safeRequire('quill-delta-converter') +const config = createConfig({ + pluginId: 'uni-cms' +}).config() + +// 获取数据库实例 +const db = uniCloud.database() + +// 文章数据库名称 +const articleDBName = 'uni-cms-articles' + +// 解锁内容数据库名称 +const unlockContentDBName = 'uni-cms-unlock-record' + +function cmsLstText(list) { + if (!Array.isArray(list)) return '' + return list.map(item => { + if (!item || typeof item !== 'object') return '' + if (item.type === 'text' || item.type === 'title') return item.text || item.html || '' + if (item.type === 'video') return item.text || '' + if (item.type === 'vote' && item.vote) { + return [item.vote.voteTitle, ...(Array.isArray(item.vote.voteLst) ? item.vote.voteLst.map(option => option && option.value) : [])] + .filter(Boolean).join(' ') + } + return '' + }).filter(Boolean).join('\n') +} + +function cmsLstImages(list) { + if (!Array.isArray(list)) return [] + return list.reduce((images, item) => { + if (!item || typeof item !== 'object') return images + if (item.type === 'image' && item.image && item.image.src) images.push(item.image.src) + if (item.type === 'video' && item.video && item.video.poster) images.push(item.video.poster) + return images + }, []) +} + +function contentText(content, cmsLst) { + const deltaText = content && Array.isArray(content.ops) + ? content.ops.map(op => typeof op.insert === 'string' ? op.insert : '').join(' ') + : '' + return [deltaText, cmsLstText(cmsLst)].filter(Boolean).join('\n').trim() +} + +function contentImages(content, cmsLst) { + const deltaImages = content && Array.isArray(content.ops) + ? content.ops.filter(op => op && op.insert && typeof op.insert.image === 'string').map(op => op.insert.image) + : [] + return [...new Set([...deltaImages, ...cmsLstImages(cmsLst)])] +} + +// 安全检测文本内容 +async function checkContentSec(content, requestId, errorMsg) { + // 安全引入内容安全检测模块 + const UniSecCheck = safeRequire('uni-sec-check') + // 创建内容安全检测实例 + const uniSecCheck = new UniSecCheck({ + provider: 'mp-weixin', + requestId + }) + // 调用文本安全检测接口 + const res = await uniSecCheck.textSecCheck({ + content, // 待检测的文本内容 + scene: 1, // 表示资料类场景 + version: 1 // 调用检测API的版本号 + }) + + // 如果存在敏感词,抛出异常 + if (res.errCode === uniSecCheck.ErrorCode.RISK_CONTENT) { + throw new Error(errorMsg || '存在敏感词,请修改后提交') + } else if (res.errCode !== 0) { + console.error(res) + throw new Error('内容安全检测异常:' + res.errCode) + } +} + +// 安全检测图片内容 +async function checkImageSec(image, requestId, errorMsg) { + // 安全引入内容安全检测模块 + const UniSecCheck = safeRequire('uni-sec-check') + // 创建内容安全检测实例 + const uniSecCheck = new UniSecCheck({ + provider: 'mp-weixin', + requestId + }) + + const images = typeof image === "string" ? [image]: image + + for (let item of images) { + // 处理cloud://开头的链接 + if (item.startsWith('cloud://')) { + const res = await uniCloud.getTempFileURL({ + fileList: [item] + }) + + if (res.fileList && res.fileList.length > 0) { + item = res.fileList[0].tempFileURL + } + } + + // 调用图片安全检测接口 + const res = await uniSecCheck.imgSecCheck({ + image: item, // 待检测的图片URL + scene: 1, // 表示资料类场景 + version: 1 // 调用检测API的版本号 + }) + + // 如果存在违规内容,抛出异常 + if (res.errCode === uniSecCheck.ErrorCode.RISK_CONTENT) { + throw new Error(errorMsg || '图片违规,请修改后提交') + } else if (res.errCode !== 0) { + console.error(res) + throw new Error('内容安全检测异常:' + res.errCode) + } + } +} + +// 检测内容安全开关 +function checkContentSecurityEnable(field) { + // 1. 从配置中心获取配置 + return config.contentSecurity && config.contentSecurity.allowCheckType && config.contentSecurity.allowCheckType.includes(field) +} + +// 安全require +function safeRequire(module) { + try { + return require(module) + } catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + throw new Error(`${module} 公共模块不存在,请在 uniCloud/database 目录右击"配置schema扩展公共模块"添加 ${module} 模块`) + } + } +} + +module.exports = { + trigger: { + // 创建文章前触发 + beforeCreate: async function ({clientInfo, addDataList}) { + // addDataList 是一个数组,因为可以一次性创建多条数据 + if (addDataList.length <= 0) return + + // 检测内容安全开关 + const allowCheckContent = checkContentSecurityEnable('content') + const allowCheckImage = checkContentSecurityEnable('image') + + // 遍历数组,对每一条数据进行安全检测 + for (const addData of addDataList) { + // 如果是草稿,不检测 + if (addData.article_status !== 1) continue + + // 并行检测 + const parallel = [] + // 检测标题 + if (allowCheckContent && addData.title) { + parallel.push(checkContentSec(addData.title, clientInfo.requestId, '标题存在敏感字,请修改后提交')) + } + // 检测摘要 + if (allowCheckContent && addData.excerpt) { + parallel.push(checkContentSec(addData.excerpt, clientInfo.requestId, '摘要存在敏感字,请修改后提交')) + } + // 检测内容:兼容标准 Delta 与移动端 cmsLst + const text = contentText(addData.content, addData.cmsLst) + if (allowCheckContent && text) { + parallel.push(checkContentSec(text, clientInfo.requestId, '内容存在敏感字,请修改后提交')) + } + // 检测封面、正文图片和视频封面 + const images = [...(Array.isArray(addData.thumbnail) ? addData.thumbnail : []), ...contentImages(addData.content, addData.cmsLst)] + if (allowCheckImage && images.length) { + parallel.push(checkImageSec(images, clientInfo.requestId, '图片存在违规,请修改后提交')) + } + // 等待所有并行检测完成 + await Promise.all(parallel) + } + }, + // 更新文章前触发 + beforeUpdate: async function ({clientInfo, where, updateData}) { + const id = where && where._id + + if (!id) return + + // 如果是草稿,不检测 + if (updateData.article_status !== 1) return + + // P1-1 草稿转发布合并旧文档:仅带 article_status=1 而无内容字段时, + // 直接审 updateData 会审空漏过。读库合并标题/摘要/正文/封面后再审。 + // 读库失败则拒绝放行(fail-closed),避免静默漏审。 + let auditSrc = updateData; + const hasContentField = updateData.title !== undefined || updateData.excerpt !== undefined || + updateData.content !== undefined || updateData.cmsLst !== undefined || updateData.thumbnail !== undefined; + if (updateData.article_status === 1 && !hasContentField) { + let oldDoc = null; + try { + const found = await db.collection(articleDBName).doc(id).get(); + oldDoc = found && found.data && found.data[0]; + } catch (e) { + throw new Error("发布审核需读取原文失败,请重试"); + } + if (!oldDoc) throw new Error("发布审核需读取原文失败,请重试"); + auditSrc = Object.assign({}, oldDoc, updateData); + } + + // 检测内容安全开关 + const allowCheckContent = checkContentSecurityEnable('content') + const allowCheckImage = checkContentSecurityEnable('image') + + // 并行检测 + const parallel = [] + + // 检测标题 + if (allowCheckContent && auditSrc.title) { + parallel.push(checkContentSec(auditSrc.title, clientInfo.requestId, '标题存在敏感字,请修改后提交')) + } + // 检测摘要 + if (allowCheckContent && auditSrc.excerpt) { + parallel.push(checkContentSec(auditSrc.excerpt, clientInfo.requestId, '摘要存在敏感字,请修改后提交')) + } + // 检测内容:兼容标准 Delta 与移动端 cmsLst + const text = contentText(auditSrc.content, auditSrc.cmsLst) + if (allowCheckContent && text) { + parallel.push(checkContentSec(text, clientInfo.requestId, '内容存在敏感字,请修改后提交')) + } + // 检测封面、正文图片和视频封面 + const images = [...(Array.isArray(auditSrc.thumbnail) ? auditSrc.thumbnail : []), ...contentImages(auditSrc.content, auditSrc.cmsLst)] + if (allowCheckImage && images.length) { + parallel.push(checkImageSec(images, clientInfo.requestId, '图片存在违规,请修改后提交')) + } + + // 等待所有并行检测完成 + await Promise.all(parallel) + + }, + // 读取文章后触发 + afterRead: async function ({userInfo, clientInfo, result, where, field}) { + const isAdmin = field && field.length && field.includes('is_admin') + // 检查是否配置了clientAppIds字段,如果没有则抛出错误 + if ((!config.clientAppIds || !config.clientAppIds.length) && !isAdmin) { + throw new Error('请在 uni-cms 配置文件中配置 clientAppIds 字段后访问,详见:https://uniapp.dcloud.net.cn/uniCloud/uni-cms.html#uni-cms-config') + } + + // 如果clientAppIds字段未配置或当前appId不在clientAppIds中,则返回 + if (!config.clientAppIds || !config.clientAppIds.includes(clientInfo.appId)) return + + // 获取广告配置 + const adConfig = config.adConfig || {} + + // 获取文章id + const id = where && where._id + + // 如果id不存在或者field不包含content,则返回 + if (id && field.includes('content')) { + // 读取了content字段后view_count加1 + await db.collection(articleDBName).where(where).update({ + view_count: db.command.inc(1) + }) + } + + // 如果查询结果为空,则返回 + if (!result.data || result.data.length <= 0) return + + // 获取文章 + const article = result.data[0] + + // 移动编辑器保留 cmsLst,标准渲染器使用 content;移动文章不依赖 content 才能展示 + if (article.edit_type === 'mobile') { + article.content_images = contentImages(article.content, article.cmsLst) + return + } + + // 如果文章内容不存在或格式异常,则返回空内容,避免 afterRead 直接抛异常 + if (!article.content || !Array.isArray(article.content.ops)) { + article.content_images = [] + article.content = { ops: [] } + return + } + + let needUnlock = false + let unlockContent = [] + + // 获取文章内容中的图片 + article.content_images = article.content.ops.reduce((imageBlocks, block) => { + if (!block || !block.insert || typeof block.insert.image !== 'string') return imageBlocks + + const {attributes} = block + const {'data-custom': custom = ""} = attributes || {} + const parseCustom = custom.split('&').reduce((obj, item) => { + const [key, value] = item.split('=') + if (key) obj[key] = value + return obj + }, {}) + + return imageBlocks.concat( + parseCustom.source || + block.insert.image + ) + }, []) + + for (const op of article.content.ops) { + if (!op || !op.insert) continue + unlockContent.push(op) + // 遍历文章内容,找到解锁内容 + if (op.insert.unlockContent) { + needUnlock = true + break + } + } + + // 如果文章不需要解锁,则返回 + if (!needUnlock) { + article.content = getRenderableArticleContent(article.content, clientInfo) + return + } + + // 获取唯一标识符 + const uniqueId = adConfig.watchAdUniqueType === 'user' ? userInfo.uid : clientInfo.deviceId + + // 如果未登录或者文章未解锁,则返回解锁内容 + if (!uniqueId || !article._id) { + article.content = getRenderableArticleContent({ + ops: unlockContent + }, clientInfo) + return + } + + // 查询解锁记录 + const unlockRecord = await db.collection(unlockContentDBName).where({ + unique_id: uniqueId, + article_id: article._id + }).get() + + // 如果未解锁,则返回解锁内容 + if (unlockRecord.data && unlockRecord.data.length <= 0) { + article.content = getRenderableArticleContent({ + ops: unlockContent + }, clientInfo) + return + } + + // 将文章解锁替换为行结束符 \n + article.content = getRenderableArticleContent({ + ops: article.content.ops.map(op => { + if (op.insert.unlockContent) { + op.insert = "\n" + } + return op + }) + }, clientInfo) + } + } +} + +function getRenderableArticleContent (rawArticleContent, clientInfo) { + const isUniAppX = /uni-app-x/i.test(clientInfo.userAgent) + + if (!isUniAppX) { + const quillDeltaConverter = new QuillDeltaToJSONConverter(rawArticleContent.ops) + return quillDeltaConverter.convert() + } + + const deltaOps = [] + + for (let i = 0; i < rawArticleContent.ops.length; i++) { + const op = rawArticleContent.ops[i] + + if (typeof op.insert === 'object') { + const insertType = Object.keys(op.insert) + const blockRenderList = ['image', 'divider', 'unlockContent', 'mediaVideo'] + if (insertType && insertType.length > 0 && blockRenderList.includes(insertType[0])) { + deltaOps.push({ + type: insertType[0], + ops: [op] + }) + + // 一般块级节点后面都跟一个换行,需要把这个换行给去掉 + const nextOps = rawArticleContent.ops[i + 1] + if (nextOps && nextOps.insert === '\n') { + i ++ + } + + continue + } + } + + const currentIndex = deltaOps.length > 0 ? deltaOps.length - 1: 0 + if ( + typeof deltaOps[currentIndex] !== "object" || + (deltaOps[currentIndex] && deltaOps[currentIndex].type !== 'rich-text') + ) { + deltaOps.push({ + type: 'rich-text', + ops: [] + }) + } + + deltaOps[deltaOps.length - 1].ops.push(op) + } + + return deltaOps.reduce((content, item) => { + const isRichText = item.type === 'rich-text' + let block = { + type: item.type, + data: isRichText ? item.ops: item.ops[0] + } + + if (item.type === 'rich-text') { + const lastOp = item.ops.length > 0 ? item.ops[item.ops.length - 1]: null + + if (lastOp !== null && lastOp.insert === "\n") { + item.ops.pop() + } + + const quillDeltaConverter = new QuillDeltaToHtmlConverter(item.ops) + + block.data = quillDeltaConverter.convert() + } + + return content.concat(block) + }, []) +} diff --git a/uni_modules/uni-pagination/components/uni-pagination/i18n/es.json b/uni_modules/uni-pagination/components/uni-pagination/i18n/es.json index 604a113..42c7ea7 100644 --- a/uni_modules/uni-pagination/components/uni-pagination/i18n/es.json +++ b/uni_modules/uni-pagination/components/uni-pagination/i18n/es.json @@ -1,5 +1,5 @@ -{ - "uni-pagination.prevText": "anterior", - "uni-pagination.nextText": "prxima", - "uni-pagination.piecePerPage": "Art¨ªculo/P¨¢gina" -} +{ + "uni-pagination.prevText": "anterior", + "uni-pagination.nextText": "siguiente", + "uni-pagination.piecePerPage": "elementos/pagina" +}