首次完整推送,
V:1.20240808.006
This commit is contained in:
41
uni_modules/uni-cms-article/changelog.md
Normal file
41
uni_modules/uni-cms-article/changelog.md
Normal file
@ -0,0 +1,41 @@
|
||||
## 1.0.16(2024-06-21)
|
||||
- 修复 小程序发布时无法上传的Bug
|
||||
## 1.0.15(2024-06-20)
|
||||
- 修复 小程序访问文章列表报错的Bug
|
||||
## 1.0.14(2024-06-12)
|
||||
- 修复 客户端无法显示文章详情问题的Bug
|
||||
## 1.0.13(2023-12-09)
|
||||
- 新增 支持uni-app-x,需要[uni-cms](https://ext.dcloud.net.cn/plugin?id=11700)插件版本>=1.0.17
|
||||
## 1.0.12(2023-10-17)
|
||||
- 修复 使用腾讯云服务空间时无法加载封面图及正文图片的问题
|
||||
## 1.0.11(2023-08-07)
|
||||
- 修复 Vue3下因`parse-scen-result.js`文件导出问题导致无法打包编译的bug
|
||||
## 1.0.10(2023-07-14)
|
||||
- 新增 文章预览功能,详见[文档](https://uniapp.dcloud.net.cn/uniCloud/uni-cms.html#article-preview)(需要uni-cms版本>=1.0.14)
|
||||
- 修复 文章详情页换行重复问题
|
||||
- 修复 文章列表在数据量小时下拉刷新数据会重复的问题
|
||||
## 1.0.9(2023-07-10)
|
||||
- 优化 文章详情页正文可以渲染多个换行
|
||||
## 1.0.8(2023-06-21)
|
||||
- 增加 文章列表无图、三图封面样式(需要uni-cms版本>=1.0.12)
|
||||
- 优化 文章详情页样式
|
||||
- 修复 在Vue3下出现 require is not defined 问题
|
||||
## 1.0.7(2023-06-07)
|
||||
- 新增 文章详情页面支持播放视频(发布视频需要uni-cms版本>=1.0.11)
|
||||
- 优化 文章正文渲染逻辑
|
||||
- 优化 文章详情页面样式
|
||||
- 修复 在文章详情页下点击返回按钮无响应问题
|
||||
## 1.0.6(2023-04-28)
|
||||
- 修复 文章详情只存在列表时无法渲染的问题
|
||||
## 1.0.5(2023-04-24)
|
||||
- 增加 license 文件
|
||||
## 1.0.4(2023-04-21)
|
||||
- 优化代码结构,增加代码注释,提高可读性
|
||||
## 1.0.3(2023-04-17)
|
||||
- 移除无用schema文件
|
||||
## 1.0.2(2023-04-12)
|
||||
- 优化看广告解锁文章交互
|
||||
## 1.0.1(2023-04-12)
|
||||
- 优化页面逻辑
|
||||
## 1.0.0(2023-04-11)
|
||||
- 插件发布,支持图文内容展示、广告解锁全文功能 [详见文档](https://uniapp.dcloud.net.cn/uniCloud/uni-cms.html)
|
68
uni_modules/uni-cms-article/common/parse-image-url.js
Normal file
68
uni_modules/uni-cms-article/common/parse-image-url.js
Normal file
@ -0,0 +1,68 @@
|
||||
function parseEditorImage (blocks = []) {
|
||||
const images = []
|
||||
|
||||
if (!Array.isArray(blocks)) {
|
||||
blocks = [blocks]
|
||||
}
|
||||
|
||||
for (const block of blocks) {
|
||||
const {insert = {}, attributes = {}} = block
|
||||
const {'data-custom': custom = ""} = attributes
|
||||
|
||||
let parseCustom = custom.split('&').reduce((obj, item) => {
|
||||
const [key, value] = item.split('=')
|
||||
|
||||
if (key && value) {
|
||||
obj[key] = value
|
||||
}
|
||||
return obj
|
||||
}, {})
|
||||
|
||||
images.push({
|
||||
src: insert.image,
|
||||
source: parseCustom.source ? parseCustom.source: insert.image
|
||||
})
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析媒体库/编辑器中的图片
|
||||
* @param images 图片地址
|
||||
* @param type {string} 解析类型 media: 媒体库, editor: 编辑器
|
||||
* @returns {Promise<{src: *, source: *}[]|{src, source: *}[]>}
|
||||
*/
|
||||
export async function parseImageUrl (images = [], type = "media") {
|
||||
if (type === "editor") {
|
||||
images = parseEditorImage(images).map(item => item.source)
|
||||
} else {
|
||||
if (!Array.isArray(images)) {
|
||||
images = [images]
|
||||
}
|
||||
}
|
||||
|
||||
if (!images) return null
|
||||
|
||||
const tcbFiles = images.filter(item => item.startsWith("cloud://"))
|
||||
|
||||
if (tcbFiles.length) {
|
||||
const res = await uniCloud.getTempFileURL({
|
||||
fileList: tcbFiles
|
||||
})
|
||||
|
||||
return images.map(image => {
|
||||
const file = res.fileList.find(item => item.fileID === image)
|
||||
|
||||
return {
|
||||
src: file ? file.tempFileURL : image,
|
||||
source: image
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return images.map(image => ({
|
||||
src: image,
|
||||
source: image
|
||||
}))
|
||||
}
|
||||
}
|
80
uni_modules/uni-cms-article/common/parse-image-url.uts
Normal file
80
uni_modules/uni-cms-article/common/parse-image-url.uts
Normal file
@ -0,0 +1,80 @@
|
||||
export type ParseImageUrlResult = {
|
||||
src: string
|
||||
source: string
|
||||
}
|
||||
function parseEditorImage (_blocks: any): UTSJSONObject[] {
|
||||
const images: UTSJSONObject[] = []
|
||||
let blocks: UTSJSONObject[]
|
||||
|
||||
if (!Array.isArray(_blocks)) {
|
||||
blocks = [_blocks as UTSJSONObject] as UTSJSONObject[]
|
||||
} else {
|
||||
blocks = _blocks as UTSJSONObject[]
|
||||
}
|
||||
|
||||
blocks.forEach((block: UTSJSONObject) => {
|
||||
const insert = block.getJSON('insert')
|
||||
const attributes = block.getJSON('attributes')
|
||||
const custom = attributes!.getString('data-custom')
|
||||
|
||||
let parseCustom = custom && custom.split('&') ? custom.split('&').reduce((obj: UTSJSONObject, item: string): UTSJSONObject => {
|
||||
const kv = item.split('=')
|
||||
|
||||
if (kv.length > 1) {
|
||||
obj[kv[0]] = kv[1]
|
||||
}
|
||||
|
||||
return obj
|
||||
}, {} as UTSJSONObject) : {}
|
||||
|
||||
images.push({
|
||||
src: insert!.getString('image'),
|
||||
source: parseCustom.getString('source') != null ? parseCustom.getString('source') : insert!.getString('image')
|
||||
})
|
||||
})
|
||||
|
||||
return images
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析媒体库/编辑器中的图片
|
||||
* @param images 图片地址
|
||||
* @param type {string} 解析类型 media: 媒体库, editor: 编辑器
|
||||
* @returns {Promise<{src: *, source: *}[]|{src, source: *}[]>}
|
||||
*/
|
||||
export async function parseImageUrl (images: any, type: string = "media"): Promise<ParseImageUrlResult[] | null> {
|
||||
let imagePaths: string[] = []
|
||||
if (type === "editor") {
|
||||
imagePaths = parseEditorImage(images).map((item: UTSJSONObject): string => item.getString('source')!)
|
||||
} else {
|
||||
if (!Array.isArray(images)) {
|
||||
imagePaths = [images as string] as string[]
|
||||
} else {
|
||||
imagePaths = images
|
||||
}
|
||||
}
|
||||
|
||||
if (imagePaths.length <= 0) return null
|
||||
|
||||
const tcbFiles = imagePaths.filter((item: string): boolean => item.startsWith("cloud://"))
|
||||
|
||||
if (tcbFiles.length > 0) {
|
||||
const res: UniCloudGetTempFileURLResult = await uniCloud.getTempFileURL({
|
||||
fileList: tcbFiles
|
||||
})
|
||||
|
||||
return imagePaths.map((image: string): ParseImageUrlResult => {
|
||||
const file = res.fileList.find((item: UniCloudGetTempFileURLResultItem): boolean => item.fileID === image)
|
||||
|
||||
return {
|
||||
src: file ? file.tempFileURL : image,
|
||||
source: image
|
||||
} as ParseImageUrlResult
|
||||
})
|
||||
} else {
|
||||
return imagePaths.map((image: string): ParseImageUrlResult => ({
|
||||
src: image,
|
||||
source: image
|
||||
} as ParseImageUrlResult))
|
||||
}
|
||||
}
|
28
uni_modules/uni-cms-article/common/parse-scan-result.js
Normal file
28
uni_modules/uni-cms-article/common/parse-scan-result.js
Normal file
@ -0,0 +1,28 @@
|
||||
function parseScanResult (scanText) {
|
||||
const match = scanText.match(/^(.*?):\/\/(.*)/)
|
||||
|
||||
if (!match || match.length < 1) {
|
||||
uni.showToast({
|
||||
icon: 'none',
|
||||
title: '未能识别到有效信息'
|
||||
})
|
||||
}
|
||||
|
||||
const [, protocol, path] = match
|
||||
|
||||
switch (protocol) {
|
||||
case "internallink":
|
||||
uni.navigateTo({
|
||||
url: `/${path.replace(/^\//, '')}`,
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: "访问的路径不存在"
|
||||
})
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
export default parseScanResult
|
28
uni_modules/uni-cms-article/common/parse-scan-result.uts
Normal file
28
uni_modules/uni-cms-article/common/parse-scan-result.uts
Normal file
@ -0,0 +1,28 @@
|
||||
function parseScanResult (scanText: string): void {
|
||||
const match = scanText.match(/^(.*?):\/\/(.*)/)
|
||||
|
||||
if (!match || match.length < 1) {
|
||||
uni.showToast({
|
||||
icon: 'none',
|
||||
title: '未能识别到有效信息'
|
||||
})
|
||||
}
|
||||
|
||||
const [, protocol, path] = match
|
||||
|
||||
switch (protocol) {
|
||||
case "internallink":
|
||||
uni.navigateTo({
|
||||
url: `/${path.replace(/^\//, '')}`,
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
icon: "none",
|
||||
title: "访问的路径不存在"
|
||||
})
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
export default parseScanResult
|
93
uni_modules/uni-cms-article/common/publish-time.js
Normal file
93
uni_modules/uni-cms-article/common/publish-time.js
Normal file
@ -0,0 +1,93 @@
|
||||
export default function translatePublishTime(timestamp) {
|
||||
let result = ''
|
||||
// 获取当前时间
|
||||
const currentData = new Date()
|
||||
// 获取发布时间
|
||||
const date = new Date(timestamp)
|
||||
// 获取发布年份
|
||||
const year = date.getFullYear()
|
||||
// 获取发布月份
|
||||
const mouth = date.getMonth() + 1
|
||||
// 获取发布日期
|
||||
const day = date.getDate()
|
||||
// 获取发布小时
|
||||
const hours = date.getHours()
|
||||
// 获取发布分钟
|
||||
const minute = date.getMinutes()
|
||||
// 获取发布秒数
|
||||
const second = date.getSeconds()
|
||||
// 获取发布时间戳
|
||||
const timer = date.getTime()
|
||||
// 获取当前年份
|
||||
const currentYear = currentData.getFullYear()
|
||||
// 获取当前月份
|
||||
const currentMonth = currentData.getMonth() + 1
|
||||
// 获取当前日期
|
||||
const currentDay = currentData.getDate()
|
||||
// 获取当前小时
|
||||
const currentHours = currentData.getHours()
|
||||
// 获取当前分钟
|
||||
let currentMinute = currentData.getMinutes()
|
||||
// 获取当前秒数
|
||||
const currentSecond = currentData.getSeconds()
|
||||
// 获取当前时间戳
|
||||
const currentTimer = currentData.getTime()
|
||||
|
||||
// 如果时间差小于10秒
|
||||
if ((currentTimer - timer) < 1000 * 10) {
|
||||
// 显示刚刚
|
||||
result = `刚刚`;
|
||||
// 如果时间差小于60秒
|
||||
} else if ((currentTimer - timer) < 1000 * 60) {
|
||||
// 如果当前分钟大于发布分钟
|
||||
if (currentMinute > minute) {
|
||||
// 显示秒数差
|
||||
result = `${(((currentMinute - minute) * 60) + currentSecond - second)}秒前`;
|
||||
} else {
|
||||
// 显示秒数差
|
||||
result = `${(currentSecond - second)}秒前`;
|
||||
}
|
||||
// 如果时间差小于1小时
|
||||
} else if ((currentTimer - timer) < 1000 * (60 * 60)) {
|
||||
// 如果当前小时大于发布小时
|
||||
if (currentHours > hours) {
|
||||
// 显示分钟差
|
||||
result = `${(((currentHours - hours) * 60) + currentMinute - minute)}分钟前`;
|
||||
} else {
|
||||
// 修改 昨天发布的文章时间会出现负数
|
||||
// 如果当前分钟小于发布分钟
|
||||
if (currentMinute < minute) {
|
||||
// 当前分钟加60
|
||||
currentMinute += 60
|
||||
}
|
||||
// 显示分钟差
|
||||
result = `${(currentMinute - minute)}分钟前`;
|
||||
}
|
||||
// 如果时间差小于1天
|
||||
} else if ((currentTimer - timer) < 1000 * (24 * 60 * 60)) {
|
||||
// 如果当前日期大于发布日期
|
||||
if (currentDay > day) {
|
||||
// 显示小时差
|
||||
result = `${((currentDay - day) * 24 + currentHours - hours)}小时前`;
|
||||
} else {
|
||||
// 修改 跨月-昨天发布的文章时间会出现负数
|
||||
// 如果当前月份不等于发布月份
|
||||
if (currentMonth !== mouth) {
|
||||
// 显示小时差
|
||||
result = `${(24 + currentHours - hours)}小时前`;
|
||||
} else {
|
||||
// 显示小时差
|
||||
result = `${(currentHours - hours)}小时前`;
|
||||
}
|
||||
}
|
||||
// 如果发布年份等于当前年份
|
||||
} else if (currentYear === year) {
|
||||
// 显示月份和日期
|
||||
result = `${mouth}月${day}日`;
|
||||
} else {
|
||||
// 显示年份、月份和日期
|
||||
result = `${year}年${mouth}月${day}日`;
|
||||
}
|
||||
return result // 返回结果
|
||||
}
|
||||
|
93
uni_modules/uni-cms-article/common/publish-time.uts
Normal file
93
uni_modules/uni-cms-article/common/publish-time.uts
Normal file
@ -0,0 +1,93 @@
|
||||
export default function translatePublishTime(timestamp: number): string {
|
||||
let result: string
|
||||
// 获取当前时间
|
||||
const currentData = new Date()
|
||||
// 获取发布时间
|
||||
const date = new Date(timestamp)
|
||||
// 获取发布年份
|
||||
const year = date.getFullYear()
|
||||
// 获取发布月份
|
||||
const mouth = date.getMonth() + 1
|
||||
// 获取发布日期
|
||||
const day = date.getDate()
|
||||
// 获取发布小时
|
||||
const hours = date.getHours()
|
||||
// 获取发布分钟
|
||||
const minute = date.getMinutes()
|
||||
// 获取发布秒数
|
||||
const second = date.getSeconds()
|
||||
// 获取发布时间戳
|
||||
const timer = date.getTime()
|
||||
// 获取当前年份
|
||||
const currentYear = currentData.getFullYear()
|
||||
// 获取当前月份
|
||||
const currentMonth = currentData.getMonth() + 1
|
||||
// 获取当前日期
|
||||
const currentDay = currentData.getDate()
|
||||
// 获取当前小时
|
||||
const currentHours = currentData.getHours()
|
||||
// 获取当前分钟
|
||||
let currentMinute = currentData.getMinutes()
|
||||
// 获取当前秒数
|
||||
const currentSecond = currentData.getSeconds()
|
||||
// 获取当前时间戳
|
||||
const currentTimer = currentData.getTime()
|
||||
|
||||
// 如果时间差小于10秒
|
||||
if ((currentTimer - timer) < 1000 * 10) {
|
||||
// 显示刚刚
|
||||
result = `刚刚`;
|
||||
// 如果时间差小于60秒
|
||||
} else if ((currentTimer - timer) < 1000 * 60) {
|
||||
// 如果当前分钟大于发布分钟
|
||||
if (currentMinute > minute) {
|
||||
// 显示秒数差
|
||||
result = `${(((currentMinute - minute) * 60) + currentSecond - second)}秒前`;
|
||||
} else {
|
||||
// 显示秒数差
|
||||
result = `${(currentSecond - second)}秒前`;
|
||||
}
|
||||
// 如果时间差小于1小时
|
||||
} else if ((currentTimer - timer) < 1000 * (60 * 60)) {
|
||||
// 如果当前小时大于发布小时
|
||||
if (currentHours > hours) {
|
||||
// 显示分钟差
|
||||
result = `${(((currentHours - hours) * 60) + currentMinute - minute)}分钟前`;
|
||||
} else {
|
||||
// 修改 昨天发布的文章时间会出现负数
|
||||
// 如果当前分钟小于发布分钟
|
||||
if (currentMinute < minute) {
|
||||
// 当前分钟加60
|
||||
currentMinute += 60
|
||||
}
|
||||
// 显示分钟差
|
||||
result = `${(currentMinute - minute)}分钟前`;
|
||||
}
|
||||
// 如果时间差小于1天
|
||||
} else if ((currentTimer - timer) < 1000 * (24 * 60 * 60)) {
|
||||
// 如果当前日期大于发布日期
|
||||
if (currentDay > day) {
|
||||
// 显示小时差
|
||||
result = `${((currentDay - day) * 24 + currentHours - hours)}小时前`;
|
||||
} else {
|
||||
// 修改 跨月-昨天发布的文章时间会出现负数
|
||||
// 如果当前月份不等于发布月份
|
||||
if (currentMonth !== mouth) {
|
||||
// 显示小时差
|
||||
result = `${(24 + currentHours - hours)}小时前`;
|
||||
} else {
|
||||
// 显示小时差
|
||||
result = `${(currentHours - hours)}小时前`;
|
||||
}
|
||||
}
|
||||
// 如果发布年份等于当前年份
|
||||
} else if (currentYear === year) {
|
||||
// 显示月份和日期
|
||||
result = `${mouth}月${day}日`;
|
||||
} else {
|
||||
// 显示年份、月份和日期
|
||||
result = `${year}年${mouth}月${day}日`;
|
||||
}
|
||||
return result // 返回结果
|
||||
}
|
||||
|
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<view
|
||||
:to="'/uni_modules/uni-cms-article/pages/detail/detail?id=' + data?._id"
|
||||
:key="data?._id"
|
||||
class="list-item not-cover"
|
||||
direction="column"
|
||||
>
|
||||
<view class="main">
|
||||
<view>
|
||||
<text class="title">{{ data?.title }}</text>
|
||||
</view>
|
||||
<view class="info">
|
||||
<text class="author">{{ data!.user_id!.length > 0 ? data!.user_id[0]!.nickname : '' }}</text>
|
||||
<text class="publish_date">{{ publishTime(data?.publish_date ?? 0) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import { type PropType } from 'vue'
|
||||
|
||||
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time.uts";
|
||||
|
||||
type ArticleAuthor = {
|
||||
_id: string
|
||||
nickname: string
|
||||
}
|
||||
type ArticleItem = {
|
||||
_id: string
|
||||
title: string
|
||||
publish_date: number
|
||||
thumbnail: string[]
|
||||
user_id: ArticleAuthor[]
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "not-cover",
|
||||
props: {
|
||||
data: {
|
||||
type: Object as PropType<ArticleItem>
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 格式化时间戳
|
||||
publishTime(timestamp: number): string {
|
||||
return translatePublishTime(timestamp)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./style.scss";
|
||||
</style>
|
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<uni-list-item
|
||||
:to="'/uni_modules/uni-cms-article/pages/detail/detail?id=' + data._id"
|
||||
:key="data._id"
|
||||
class="list-item not-cover"
|
||||
direction="column"
|
||||
>
|
||||
<template v-slot:body>
|
||||
<view class="main">
|
||||
<view>
|
||||
<text class="title">{{ data.title }}</text>
|
||||
</view>
|
||||
<view class="info">
|
||||
<text class="author">{{ data.user_id[0] ? data.user_id[0].nickname : '' }}</text>
|
||||
<text class="publish_date">{{ publishTime(data.publish_date) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</uni-list-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time";
|
||||
|
||||
export default {
|
||||
name: "not-cover",
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 格式化时间戳
|
||||
publishTime(timestamp) {
|
||||
return translatePublishTime(timestamp)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./style.scss";
|
||||
</style>
|
@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<view
|
||||
:to="'/uni_modules/uni-cms-article/pages/detail/detail?id=' + data?._id"
|
||||
:key="data?._id"
|
||||
class="list-item"
|
||||
>
|
||||
<view class="main">
|
||||
<text class="title">{{ data?.title }}</text>
|
||||
<view class="info">
|
||||
<text class="author">{{ data!.user_id!.length > 0 ? data!.user_id[0]!.nickname : '' }}</text>
|
||||
<text class="publish_date">{{ publishTime(data?.publish_date ?? 0) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<image class="thumbnail" :src="data!.thumbnail[0]" mode="aspectFill"></image>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import { type PropType } from 'vue'
|
||||
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time.uts";
|
||||
type ArticleAuthor = {
|
||||
_id: string
|
||||
nickname: string
|
||||
}
|
||||
type ArticleItem = {
|
||||
_id: string
|
||||
title: string
|
||||
publish_date: number
|
||||
thumbnail: string[]
|
||||
user_id: ArticleAuthor[]
|
||||
}
|
||||
export default {
|
||||
name: "right-small-cover",
|
||||
props: {
|
||||
data: {
|
||||
type: Object as PropType<ArticleItem>
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 格式化时间戳
|
||||
publishTime(timestamp: number): string {
|
||||
return translatePublishTime(timestamp)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./style.scss";
|
||||
</style>
|
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<uni-list-item
|
||||
:to="'/uni_modules/uni-cms-article/pages/detail/detail?id=' + data._id"
|
||||
:key="data._id"
|
||||
class="list-item"
|
||||
>
|
||||
<template v-slot:body>
|
||||
<view class="main">
|
||||
<text class="title">{{ data.title }}</text>
|
||||
<view class="info">
|
||||
<text class="author">{{ data.user_id[0] ? data.user_id[0].nickname : '' }}</text>
|
||||
<text class="publish_date">{{ publishTime(data.publish_date) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<image class="thumbnail" :src="data.thumbnail[0]" mode="aspectFill"></image>
|
||||
</template>
|
||||
</uni-list-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time";
|
||||
|
||||
export default {
|
||||
name: "right-small-cover",
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 格式化时间戳
|
||||
publishTime(timestamp) {
|
||||
return translatePublishTime(timestamp)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./style.scss";
|
||||
</style>
|
@ -0,0 +1,63 @@
|
||||
.list-item {
|
||||
&.not-cover {
|
||||
.main {
|
||||
.info {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
.main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
.title {
|
||||
font-size: 30rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.thumbnails {
|
||||
margin: 20rpx 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
.img {
|
||||
flex: 1;
|
||||
/* #ifndef APP-NVUE */
|
||||
width: auto;
|
||||
/* #endif */
|
||||
height: 200rpx;
|
||||
border-radius: 8rpx;
|
||||
margin: 0 10rpx;
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.author,
|
||||
.publish_date {
|
||||
font-size: 24rpx;
|
||||
color: #bbbbbb;
|
||||
}
|
||||
|
||||
.publish_date {
|
||||
margin-left: 14rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 240rpx;
|
||||
height: 160rpx;
|
||||
margin-left: 20rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<view
|
||||
:to="'/uni_modules/uni-cms-article/pages/detail/detail?id=' + data?._id"
|
||||
:key="data?._id"
|
||||
class="list-item"
|
||||
direction="column"
|
||||
>
|
||||
<view class="main">
|
||||
<text class="title">{{ data?.title }}</text>
|
||||
<view class="thumbnails">
|
||||
<image
|
||||
v-for="image in data?.thumbnail"
|
||||
:src="image"
|
||||
mode="aspectFill"
|
||||
class="img"
|
||||
></image>
|
||||
</view>
|
||||
<view class="info">
|
||||
<text class="author">{{ data!.user_id!.length > 0 ? data!.user_id[0]!.nickname : '' }}</text>
|
||||
<text class="publish_date">{{ publishTime(data?.publish_date ?? 0) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import { type PropType } from 'vue'
|
||||
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time.uts";
|
||||
type ArticleAuthor = {
|
||||
_id: string
|
||||
nickname: string
|
||||
}
|
||||
type ArticleItem = {
|
||||
_id: string
|
||||
title: string
|
||||
publish_date: number
|
||||
thumbnail: string[]
|
||||
user_id: ArticleAuthor[]
|
||||
}
|
||||
export default {
|
||||
name: "three-cover",
|
||||
props: {
|
||||
data: {
|
||||
type: Object as PropType<ArticleItem>
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 格式化时间戳
|
||||
publishTime(timestamp: number): string {
|
||||
return translatePublishTime(timestamp)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./style.scss";
|
||||
</style>
|
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<uni-list-item
|
||||
:to="'/uni_modules/uni-cms-article/pages/detail/detail?id=' + data._id"
|
||||
:key="data._id"
|
||||
class="list-item"
|
||||
direction="column"
|
||||
>
|
||||
<template v-slot:body>
|
||||
<view class="main">
|
||||
<text class="title">{{ data.title }}</text>
|
||||
<view class="thumbnails">
|
||||
<image
|
||||
v-for="image in data.thumbnail"
|
||||
:src="image"
|
||||
mode="aspectFill"
|
||||
class="img"
|
||||
></image>
|
||||
</view>
|
||||
<view class="info">
|
||||
<text class="author">{{ data.user_id[0] ? data.user_id[0].nickname : '' }}</text>
|
||||
<text class="publish_date">{{ publishTime(data.publish_date) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</uni-list-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time";
|
||||
|
||||
export default {
|
||||
name: "three-cover",
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 格式化时间戳
|
||||
publishTime(timestamp) {
|
||||
return translatePublishTime(timestamp)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./style.scss";
|
||||
</style>
|
@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<refresh @refresh="refresh" @pullingdown="onpullingdown" :display="showRefresh ? 'show' : 'hide'">
|
||||
<view class="refreshBox">
|
||||
<!-- 可以自己添加图片路径或base64实现图片 <image class="refreshImg" :src="config[state].img" mode="widthFix" resize="cover"></image> -->
|
||||
<text class="refreshText">{{config[state].text}}</text>
|
||||
</view>
|
||||
</refresh>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
showRefresh:false, // 是否显示刷新
|
||||
state:0 // 刷新状态,0:继续下拉执行刷新,1:释放立即刷新,2:正在加载中,3:加载成功
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
// 下拉刷新回调函数
|
||||
onpullingdown({pullingDistance,viewHeight}) {
|
||||
if(pullingDistance < viewHeight){
|
||||
this.state = 0 // 继续下拉执行刷新
|
||||
}else{
|
||||
this.state = 1 // 释放立即刷新
|
||||
}
|
||||
},
|
||||
// 执行刷新
|
||||
refresh(){
|
||||
// console.log('refresh');
|
||||
this.showRefresh = true // 显示刷新
|
||||
this.state = 2 // 正在加载中
|
||||
this.$emit('refresh') // 触发refresh事件
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 监听loading变化
|
||||
loading(loading, oldValue) {
|
||||
if(!loading){
|
||||
this.showRefresh = false // 隐藏刷新
|
||||
this.state = 3 // 加载成功
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
loading: {
|
||||
type:Boolean,
|
||||
default(){
|
||||
return false
|
||||
}
|
||||
},
|
||||
config: {
|
||||
type: Array,
|
||||
default(){
|
||||
return [
|
||||
{
|
||||
text:"继续下拉执行刷新",
|
||||
img:""//可以自己添加图片路径或base64实现图片
|
||||
},
|
||||
{
|
||||
text:"释放立即刷新",
|
||||
img:""//可以自己添加图片路径或base64实现图片
|
||||
},
|
||||
{
|
||||
text:"正在加载中",
|
||||
img:""//可以自己添加图片路径或base64实现图片
|
||||
},
|
||||
{
|
||||
text:"加载成功",
|
||||
img:""//可以自己添加图片路径或base64实现图片
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.refreshBox{
|
||||
width: 750rpx;
|
||||
height: 50px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
/* #ifndef APP-PLUS */
|
||||
margin-top: -50px;
|
||||
/* #endif */
|
||||
}
|
||||
.refreshImg{
|
||||
width: 55rpx;
|
||||
height: 55rpx;
|
||||
z-index: 111;
|
||||
}
|
||||
.refreshText{
|
||||
font-size: 26rpx;
|
||||
color: #999999;
|
||||
padding-left: 6rpx;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<refresh @refresh="refresh" @pullingdown="onpullingdown" :display="showRefresh ? 'show' : 'hide'">
|
||||
<view class="refreshBox">
|
||||
<!-- 可以自己添加图片路径或base64实现图片 <image class="refreshImg" :src="config[state].img" mode="widthFix" resize="cover"></image> -->
|
||||
<text class="refreshText">{{config[state].text}}</text>
|
||||
</view>
|
||||
</refresh>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
showRefresh:false, // 是否显示刷新
|
||||
state:0 // 刷新状态,0:继续下拉执行刷新,1:释放立即刷新,2:正在加载中,3:加载成功
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
// 下拉刷新回调函数
|
||||
onpullingdown({pullingDistance,viewHeight}) {
|
||||
if(pullingDistance < viewHeight){
|
||||
this.state = 0 // 继续下拉执行刷新
|
||||
}else{
|
||||
this.state = 1 // 释放立即刷新
|
||||
}
|
||||
},
|
||||
// 执行刷新
|
||||
refresh(){
|
||||
// console.log('refresh');
|
||||
this.showRefresh = true // 显示刷新
|
||||
this.state = 2 // 正在加载中
|
||||
this.$emit('refresh') // 触发refresh事件
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 监听loading变化
|
||||
loading(loading, oldValue) {
|
||||
if(!loading){
|
||||
this.showRefresh = false // 隐藏刷新
|
||||
this.state = 3 // 加载成功
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
loading: {
|
||||
type:Boolean,
|
||||
default(){
|
||||
return false
|
||||
}
|
||||
},
|
||||
config: {
|
||||
type: Array,
|
||||
default(){
|
||||
return [
|
||||
{
|
||||
text:"继续下拉执行刷新",
|
||||
img:""//可以自己添加图片路径或base64实现图片
|
||||
},
|
||||
{
|
||||
text:"释放立即刷新",
|
||||
img:""//可以自己添加图片路径或base64实现图片
|
||||
},
|
||||
{
|
||||
text:"正在加载中",
|
||||
img:""//可以自己添加图片路径或base64实现图片
|
||||
},
|
||||
{
|
||||
text:"加载成功",
|
||||
img:""//可以自己添加图片路径或base64实现图片
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.refreshBox{
|
||||
width: 750rpx;
|
||||
height: 50px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
/* #ifndef APP-PLUS */
|
||||
margin-top: -50px;
|
||||
/* #endif */
|
||||
}
|
||||
.refreshImg{
|
||||
width: 55rpx;
|
||||
height: 55rpx;
|
||||
z-index: 111;
|
||||
}
|
||||
.refreshText{
|
||||
font-size: 26rpx;
|
||||
color: #999999;
|
||||
padding-left: 6rpx;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<view
|
||||
:class="classList"
|
||||
v-if="imageData.image != ''"
|
||||
>
|
||||
<image
|
||||
:src="imagePath"
|
||||
:style="styles"
|
||||
:alt="imageData.attributes.alt"
|
||||
class="img"
|
||||
mode="aspectFill"
|
||||
@load="imageLoad"
|
||||
@click="imagePreview"
|
||||
></image>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="uts">
|
||||
import {parseImageUrl} from "@/uni_modules/uni-cms-article/common/parse-image-url.uts";
|
||||
import type {ParseImageUrlResult} from '@/uni_modules/uni-cms-article/common/parse-image-url.uts';
|
||||
type ImageAttributes = {
|
||||
customParams: string | null
|
||||
width: number | null
|
||||
height: number | null
|
||||
alt: string | null
|
||||
}
|
||||
type ImageData = {
|
||||
image: string
|
||||
attributes: ImageAttributes
|
||||
}
|
||||
type ImageCalResult = {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "render-image",
|
||||
emits: ['imagePreview'],
|
||||
props: {
|
||||
deltaOp: {
|
||||
type: Object as UTSJSONObject,
|
||||
default (): UTSJSONObject {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
reset: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
imagePath: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
imageData (): ImageData {
|
||||
const insert = this.deltaOp!.getJSON('insert')! as UTSJSONObject
|
||||
const attributes: UTSJSONObject | null = this.deltaOp!.getJSON('attributes')
|
||||
console.log(insert, attributes)
|
||||
return {
|
||||
image: insert.getString('image')!,
|
||||
attributes: {
|
||||
customParams: attributes != null ? attributes.getString('data-custom'): null,
|
||||
width: attributes != null ? attributes!.getNumber('width'): null,
|
||||
height: attributes != null ? attributes!.getNumber('height'): null,
|
||||
alt: attributes != null ? attributes!.getString('alt'): null,
|
||||
}
|
||||
} as ImageData
|
||||
},
|
||||
classList (): string[] {
|
||||
return [
|
||||
'image',
|
||||
this.reset ? 'reset': ''
|
||||
] as string[]
|
||||
},
|
||||
styles (): string {
|
||||
let style = ""
|
||||
|
||||
if (this.width != 0) {
|
||||
style += `;width:${this.width}px`
|
||||
}
|
||||
if (this.height != 0) {
|
||||
style += `;height:${this.height}px`
|
||||
}
|
||||
return style
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.loadImagePath()
|
||||
},
|
||||
methods: {
|
||||
async loadImagePath (): Promise<void> {
|
||||
const {image, attributes} = this.imageData
|
||||
const parseImages = await parseImageUrl({
|
||||
insert: {image},
|
||||
attributes: {
|
||||
'data-custom': attributes.customParams != null ? attributes.customParams : ""
|
||||
}
|
||||
}, "editor")
|
||||
|
||||
if (parseImages != null) {
|
||||
this.imagePath = parseImages[0].src
|
||||
}
|
||||
},
|
||||
imagePreview () {
|
||||
this.$emit('imagePreview', this.imageData.image)
|
||||
},
|
||||
// 图片加载完成
|
||||
imageLoad(e: ImageLoadEvent) {
|
||||
const recal = this.wxAutoImageCal(e.detail.width, e.detail.height, 15) // 计算图片宽高
|
||||
// const image = this.imageData
|
||||
|
||||
// ::TODO 关注一下在多端得表现情况
|
||||
// if (!image.data.attributes.width || Number(image.data.attributes.width) > recal.imageWidth) {
|
||||
// 如果图片宽度不存在或者图片宽度大于计算出来的宽度,则设置图片宽高
|
||||
this.width = recal.width
|
||||
this.height = recal.height
|
||||
// }
|
||||
},
|
||||
|
||||
// 计算图片宽高
|
||||
wxAutoImageCal(originalWidth: number, originalHeight: number, imagePadding: number): ImageCalResult {
|
||||
// 获取系统信息
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
let windowWidth: number;
|
||||
// let windowHeight: number;
|
||||
let autoWidth: number;
|
||||
let autoHeight: number;
|
||||
|
||||
let results: ImageCalResult = {
|
||||
width: 0,
|
||||
height: 0
|
||||
};
|
||||
// 计算图片宽度
|
||||
windowWidth = systemInfo.windowWidth - 2 * imagePadding;
|
||||
// windowHeight = systemInfo.windowHeight;
|
||||
if (originalWidth > windowWidth) {//在图片width大于手机屏幕width时候
|
||||
autoWidth = windowWidth;
|
||||
autoHeight = (autoWidth * originalHeight) / originalWidth;
|
||||
results.width = autoWidth;
|
||||
results.height = autoHeight;
|
||||
} else {//否则展示原来的数据
|
||||
results.width = originalWidth;
|
||||
results.height = originalHeight;
|
||||
}
|
||||
return results;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.image {
|
||||
margin-bottom: 40rpx;
|
||||
&.reset {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.img {
|
||||
display: flex;
|
||||
border-radius: 12rpx;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<view
|
||||
:class="classList"
|
||||
v-if="data.data"
|
||||
>
|
||||
<image
|
||||
:src="imagePath"
|
||||
:class="data.data.class"
|
||||
:style="styles"
|
||||
:alt="data.data.attributes.alt || ''"
|
||||
class="img"
|
||||
mode="aspectFill"
|
||||
@load="imageLoad"
|
||||
@click="imagePreview"
|
||||
></image>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import {parseImageUrl} from "@/uni_modules/uni-cms-article/common/parse-image-url.js";
|
||||
|
||||
export default {
|
||||
name: "render-image",
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
className: String,
|
||||
reset: false
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
imagePath: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
classList () {
|
||||
return [
|
||||
'image',
|
||||
this.reset ? 'reset': '',
|
||||
this.className
|
||||
]
|
||||
},
|
||||
styles () {
|
||||
let style = this.data.data.style
|
||||
|
||||
if (this.width) {
|
||||
style += `;width:${this.width}px`
|
||||
}
|
||||
if (this.height) {
|
||||
style += `;height:${this.height}px`
|
||||
}
|
||||
return style
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.loadImagePath()
|
||||
},
|
||||
methods: {
|
||||
async loadImagePath () {
|
||||
const parseImages = await parseImageUrl({
|
||||
insert: {image: this.data.data.value},
|
||||
attributes: this.data.data.attributes,
|
||||
}, "editor")
|
||||
|
||||
this.imagePath = parseImages[0].src
|
||||
},
|
||||
imagePreview () {
|
||||
uni.$emit('imagePreview', this.data.data.value)
|
||||
},
|
||||
// 图片加载完成
|
||||
imageLoad(e) {
|
||||
const recal = this.wxAutoImageCal(e.detail.width, e.detail.height, 15) // 计算图片宽高
|
||||
// const image = this.data
|
||||
|
||||
// ::TODO 关注一下在多端得表现情况
|
||||
// if (!image.data.attributes.width || Number(image.data.attributes.width) > recal.imageWidth) {
|
||||
// 如果图片宽度不存在或者图片宽度大于计算出来的宽度,则设置图片宽高
|
||||
this.width = recal.imageWidth
|
||||
this.height = recal.imageHeight
|
||||
// }
|
||||
},
|
||||
|
||||
// 计算图片宽高
|
||||
wxAutoImageCal(originalWidth, originalHeight, imagePadding = 0) {
|
||||
// 获取系统信息
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
let windowWidth = 0, windowHeight = 0;
|
||||
let autoWidth = 0, autoHeight = 0;
|
||||
let results = {};
|
||||
// 计算图片宽度
|
||||
windowWidth = systemInfo.windowWidth - 2 * imagePadding;
|
||||
windowHeight = systemInfo.windowHeight;
|
||||
if (originalWidth > windowWidth) {//在图片width大于手机屏幕width时候
|
||||
autoWidth = windowWidth;
|
||||
autoHeight = (autoWidth * originalHeight) / originalWidth;
|
||||
results.imageWidth = autoWidth;
|
||||
results.imageHeight = autoHeight;
|
||||
} else {//否则展示原来的数据
|
||||
results.imageWidth = originalWidth;
|
||||
results.imageHeight = originalHeight;
|
||||
}
|
||||
return results;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.image {
|
||||
margin-bottom: 40rpx;
|
||||
&.reset {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.img {
|
||||
// #ifdef APP-PLUS
|
||||
display: block;
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
display: flex;
|
||||
// #endif
|
||||
border-radius: 12rpx;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<view class="content">
|
||||
<template v-for="op in content">
|
||||
<render-text
|
||||
v-if="op.type === 'paragraph'"
|
||||
:data="op.data"
|
||||
:className="op.class"
|
||||
:style="op.style"
|
||||
></render-text>
|
||||
<render-image
|
||||
v-else-if="op.type === 'image' && op.data.length > 0"
|
||||
:data="op.data[0]"
|
||||
:className="op.class"
|
||||
:style="op.style"
|
||||
></render-image>
|
||||
<render-list
|
||||
v-else-if="op.type === 'list'"
|
||||
:data="op.data"
|
||||
:style="op.style"
|
||||
></render-list>
|
||||
<view
|
||||
v-else-if="op.type === 'divider'"
|
||||
class="divider"
|
||||
></view>
|
||||
<render-video
|
||||
v-else-if="op.type === 'mediaVideo'"
|
||||
:data="op.data"
|
||||
></render-video>
|
||||
<render-unlock-content
|
||||
v-else-if="op.type === 'unlockContent'"
|
||||
:adp-id="adConfig.adpId"
|
||||
:watch-ad-unique-type="adConfig.watchAdUniqueType"
|
||||
></render-unlock-content>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {parseImageUrl} from "@/uni_modules/uni-cms-article/common/parse-image-url"
|
||||
|
||||
import text from './text.vue'
|
||||
import image from './image.vue'
|
||||
import video from './video.vue'
|
||||
import list from './list.vue'
|
||||
import unlockContent from './unlock-content.vue'
|
||||
|
||||
export default {
|
||||
name: "render-article-detail",
|
||||
props: {
|
||||
content: {
|
||||
type: Array,
|
||||
default () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
contentImages: {
|
||||
type: Array,
|
||||
default () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
adConfig: {
|
||||
type: Object,
|
||||
default: {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
articleImages: []
|
||||
}
|
||||
},
|
||||
components: {
|
||||
renderUnlockContent: unlockContent,
|
||||
renderText: text,
|
||||
renderImage: image,
|
||||
renderList: list,
|
||||
renderVideo: video
|
||||
},
|
||||
mounted() {
|
||||
this.initImage()
|
||||
},
|
||||
beforeDestroy() {
|
||||
uni.$off('imagePreview')
|
||||
},
|
||||
methods: {
|
||||
// 初始化图片
|
||||
async initImage() {
|
||||
// 获取所有图片
|
||||
const parseImages = await parseImageUrl(this.contentImages)
|
||||
|
||||
if (parseImages != null) {
|
||||
this.articleImages = parseImages.map(image => image.src)
|
||||
}
|
||||
|
||||
// 监听图片预览
|
||||
uni.$on('imagePreview', this.imagePreview)
|
||||
},
|
||||
// 点击图片预览
|
||||
imagePreview(src) {
|
||||
if (src) {
|
||||
uni.previewImage({
|
||||
current: src.split('?')[0], // 当前显示图片的http链接
|
||||
urls: this.articleImages // 需要预览的图片http链接列表
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.content {
|
||||
line-height: 1.75;
|
||||
font-size: 32rpx;
|
||||
margin-top: 40rpx;
|
||||
padding: 0 30rpx 80rpx;
|
||||
word-break: break-word;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #d8d8d8;
|
||||
width: 100%;
|
||||
margin: 40rpx 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<view :class="['list', data.type]">
|
||||
<view class="list-item" v-for="(item, index) in data.items">
|
||||
<text class="dot">{{data.type === 'ordered' ? `${index + 1}.` : '•'}}</text>
|
||||
<render-text :data="item.data" reset class="reset-default"></render-text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import text from './text.vue'
|
||||
|
||||
export default {
|
||||
name: "render-list",
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
renderText: text
|
||||
},
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.list {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20rpx;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.dot {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
}
|
||||
.reset-default {
|
||||
text-indent: 0;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<view :class="classList">
|
||||
<template v-for="item in data">
|
||||
<text
|
||||
v-if="item.type === 'text'"
|
||||
:class="item.data.class"
|
||||
:style="item.data.style"
|
||||
class="text"
|
||||
>
|
||||
{{item.data.value}}
|
||||
</text>
|
||||
<text
|
||||
v-if="item.type === 'link'"
|
||||
:class="item.data.class"
|
||||
:style="item.data.style"
|
||||
class="link"
|
||||
@click="goLink(item.data.attributes.link)"
|
||||
>
|
||||
{{item.data.value}}
|
||||
</text>
|
||||
<image-item v-else-if="item.type === 'image'" :data="item"></image-item>
|
||||
<!-- #ifdef H5 -->
|
||||
<br v-else-if="item.type === 'br'" class="br"/>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef H5 -->
|
||||
<text v-else-if="item.type === 'br'" class="br">\n</text>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import ImageItem from './image.vue'
|
||||
|
||||
export default {
|
||||
name: "render-text",
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
className: String,
|
||||
reset: Boolean
|
||||
},
|
||||
computed: {
|
||||
classList () {
|
||||
return [
|
||||
'row-text',
|
||||
this.className,
|
||||
this.reset ? 'reset': ''
|
||||
]
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ImageItem
|
||||
},
|
||||
methods: {
|
||||
show () {
|
||||
uni.showToast({
|
||||
title: 'test',
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
// 点击链接跳转
|
||||
goLink(link) {
|
||||
// 如果链接为空,则返回
|
||||
if (!link) return
|
||||
|
||||
// #ifdef H5
|
||||
// 在新窗口中打开链接
|
||||
window.open(link, '_blank')
|
||||
// #endif
|
||||
|
||||
// #ifdef MP
|
||||
// 微信小程序不支持打开外链,复制链接到剪贴板
|
||||
uni.setClipboardData({
|
||||
data: link,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: '链接已复制',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef APP
|
||||
// 在webview中打开链接
|
||||
uni.navigateTo({
|
||||
url: `/uni_modules/uni-cms-article/pages/webview/webview?url=${encodeURIComponent(link)}`
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.row-text, .br {
|
||||
margin-bottom: 40rpx;
|
||||
&.reset {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.header-1,
|
||||
.header-2,
|
||||
.header-3,
|
||||
.header-4,
|
||||
.header-5,
|
||||
.header-6 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.header-1 {
|
||||
font-size: 44rpx;
|
||||
}
|
||||
|
||||
.header-2 {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.header-3 {
|
||||
font-size: 38rpx;
|
||||
}
|
||||
|
||||
.header-4 {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.header-5 {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.header-6 {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.strike {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #0064f9;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<view class="unlock-content">
|
||||
<!-- #ifdef H5 -->
|
||||
<!-- 等广告支持H5后优化-->
|
||||
<button class="text" @click="callAd">请观看广告后解锁全文</button>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifndef H5 -->
|
||||
<ad-rewarded-video ref="rewardedVideo" :adpid="adpId" :preload="false" :disabled="true" :loadnext="true"
|
||||
:url-callback="urlCallback" @load="onAdLoad" @close="onAdClose" @error="onAdError"
|
||||
v-slot:default="{ loading, error }">
|
||||
<text v-if="error" class="text">广告加载失败</text>
|
||||
</ad-rewarded-video>
|
||||
<button v-if="!isLoadError" class="text" @click="callAd" :loading="adLoading">请观看广告后解锁全文</button>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 实例化数据库
|
||||
const db = uniCloud.database()
|
||||
// 定义解锁记录表名
|
||||
const unlockContentDBName = 'uni-cms-unlock-record'
|
||||
|
||||
export default {
|
||||
name: "ad",
|
||||
props: {
|
||||
adpId: String,
|
||||
watchAdUniqueType: {
|
||||
type: String,
|
||||
default: 'device'
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentArticleId: '',
|
||||
currentPageRoute: '',
|
||||
adLoading: false,
|
||||
isLoadError: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 回调URL
|
||||
urlCallback() {
|
||||
return {
|
||||
extra: JSON.stringify({
|
||||
article_id: this.currentArticleId,
|
||||
unique_id: this.uniqueId,
|
||||
unique_type: this.watchAdUniqueType
|
||||
})
|
||||
}
|
||||
},
|
||||
// 是否通过设备观看
|
||||
watchByDevice() {
|
||||
return this.watchAdUniqueType === 'device'
|
||||
},
|
||||
// 是否通过用户观看
|
||||
watchByUser() {
|
||||
return this.watchAdUniqueType === 'user'
|
||||
},
|
||||
// 获取唯一ID
|
||||
uniqueId() {
|
||||
return this.watchByDevice ? uni.getSystemInfoSync().deviceId : uniCloud.getCurrentUserInfo().uid
|
||||
}
|
||||
},
|
||||
// #ifndef H5
|
||||
mounted() {
|
||||
// 获取当前页面信息
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
this.currentArticleId = currentPage.options.id
|
||||
this.currentPageRoute = currentPage.route
|
||||
|
||||
// 如果广告位ID未设置,则提示广告无法正常加载
|
||||
if (!this.adpId) {
|
||||
uni.showModal({
|
||||
content: '广告位ID未设置,广告无法正常加载',
|
||||
showCancel: false
|
||||
})
|
||||
} else {
|
||||
// 加载广告
|
||||
this.$refs.rewardedVideo.load()
|
||||
}
|
||||
},
|
||||
// #endif
|
||||
methods: {
|
||||
// 调用广告
|
||||
callAd() {
|
||||
// #ifdef H5
|
||||
// 如果在浏览器中,则提示需在App或小程序中操作
|
||||
return uni.showModal({
|
||||
content: '需观看广告解锁内容, 但浏览器不支持广告播放, 请在App或小程序中操作',
|
||||
showCancel: false
|
||||
})
|
||||
// #endif
|
||||
|
||||
if (this.watchByUser) {
|
||||
// 登录跳转URL 请根据实际情况修改
|
||||
const redirectUrl = '/uni_modules/uni-id-pages/pages/login/login-withoutpwd' + (this.currentPageRoute ? '?uniIdRedirectUrl=' + this.currentPageRoute + '?id=' + this.currentArticleId : '')
|
||||
|
||||
//::TODO 支持设备与用户
|
||||
// 如果用户未登录,则提示需要登录
|
||||
if (uniCloud.getCurrentUserInfo().tokenExpired < Date.now()) {
|
||||
uni.showModal({
|
||||
content: '请登录后操作',
|
||||
success: ({ confirm }) => {
|
||||
confirm && uni.redirectTo({
|
||||
url: redirectUrl
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 显示广告
|
||||
this.adLoading = true
|
||||
this.$refs.rewardedVideo.show()
|
||||
},
|
||||
// 广告加载成功
|
||||
onAdLoad() {
|
||||
this.adLoading && this.$refs.rewardedVideo.show()
|
||||
console.log('广告数据加载成功');
|
||||
},
|
||||
// 广告关闭
|
||||
onAdClose(e) {
|
||||
console.log('close', e)
|
||||
const detail = e.detail
|
||||
// 轮询3次,每次1秒,如果3秒内没有查询到解锁记录,就提示解锁失败
|
||||
let i = 3
|
||||
uni.hideLoading()
|
||||
this.adLoading = false
|
||||
|
||||
// detail.isEnded 为true 说明用户观看了完整视频
|
||||
if (detail && detail.isEnded) {
|
||||
uni.showLoading({
|
||||
title: '正在解锁全文',
|
||||
timeout: 7000
|
||||
})
|
||||
let queryResult = setInterval(async () => {
|
||||
i--;
|
||||
|
||||
// 查询解锁记录
|
||||
const res = await db.collection(unlockContentDBName).where({
|
||||
unique_id: this.uniqueId,
|
||||
article_id: this.currentArticleId,
|
||||
}).get()
|
||||
|
||||
// 1. result.data.length 为0 说明没有解锁记录
|
||||
// 2. i <= 0 说明已经轮询了3次,还是没有解锁记录,说明解锁失败
|
||||
// 3. result.data.length && i > 0 说明已经解锁成功
|
||||
if (i <= 0) {
|
||||
console.log('解锁失败', i)
|
||||
clearInterval(queryResult)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '解锁失败!',
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
});
|
||||
} else if (res.result && res.result.data.length) {
|
||||
console.log('解锁成功', i)
|
||||
|
||||
clearInterval(queryResult)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '解锁成功!',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
uni.$emit('onUnlockContent')
|
||||
}
|
||||
}, 1500);
|
||||
} else {
|
||||
uni.showModal({
|
||||
content: "请观看完整视频后解锁全文",
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
},
|
||||
onAdError(e) {
|
||||
// uni.hideLoading()
|
||||
// this.isLoadError = true
|
||||
console.error('onaderror: ', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.unlock-content {
|
||||
text-align: center;
|
||||
padding: 160rpx 0 60rpx;
|
||||
position: relative;
|
||||
margin-top: -140rpx;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 160rpx;
|
||||
background: linear-gradient(to bottom, transparent, #fff);
|
||||
}
|
||||
|
||||
.text {
|
||||
border: #f0f0f0 solid 1px;
|
||||
display: inline-block;
|
||||
background: #f6f6f6;
|
||||
border-radius: 10rpx;
|
||||
font-size: 34rpx;
|
||||
color: #222;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<view class="video">
|
||||
<video
|
||||
class="v"
|
||||
:src="data.attributes.src"
|
||||
:poster="data.attributes.poster"
|
||||
></video>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: "render-video",
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.video {
|
||||
margin-bottom: 40rpx;
|
||||
.v {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<view>
|
||||
<text class="uni-cms-article-icon" :style="{color, 'fontSize': size + 'px', lineHeight: size + 'px'}">{{iconCode}}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
const icons: UTSJSONObject = {
|
||||
search: "\ue654",
|
||||
back: "\ue6b9",
|
||||
scan: "\ue62a",
|
||||
closeempty: "\ue66c",
|
||||
trash: "\ue687",
|
||||
reload: "\ue6b2",
|
||||
eye: "\ue651",
|
||||
'eye-slash': '\ue6b3'
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'uni-cms-article-icons',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#333333'
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 16
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iconCode(): string {
|
||||
return icons.getString(this.type) as string
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "uni-cms-article-icons";
|
||||
src: url('/uni_modules/uni-cms-article/static/uniicons.ttf');
|
||||
}
|
||||
|
||||
.uni-cms-article-icon {
|
||||
font-family: "uni-cms-article-icons";
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
</style>
|
@ -0,0 +1,371 @@
|
||||
<template>
|
||||
<unicloud-db
|
||||
ref='udb'
|
||||
v-slot:default="{ data, pagination, hasMore, loading, error }"
|
||||
:collection="collectionList"
|
||||
:page-size="10"
|
||||
:loadtime="loadTime"
|
||||
orderby="publish_date desc"
|
||||
@load="onListLoad"
|
||||
@error="onListLoadError"
|
||||
>
|
||||
<view
|
||||
v-if="networkType == 'none'"
|
||||
class="error-box"
|
||||
@click="checkNetwork"
|
||||
>
|
||||
<image class="disconnect-icon" src="/uni_modules/uni-cms-article/static/disconnection.png" mode="widthFix"></image>
|
||||
<text class="tip-text">当前网络不可用,请点击重试</text>
|
||||
</view>
|
||||
<list-view
|
||||
v-else
|
||||
class="list-view"
|
||||
:scroll-y="true"
|
||||
:refresher-enabled="refresherEnabled"
|
||||
refresher-default-style="none"
|
||||
:refresher-triggered="refresherTriggered"
|
||||
@refresherpulling="refresherpulling"
|
||||
@refresherrefresh="refresherrefresh"
|
||||
@scrolltolower="scrolltolower"
|
||||
>
|
||||
<list-item slot="refresher" class="refresh-box">
|
||||
<text class="text">{{ refreshText[refreshState] }}</text>
|
||||
</list-item>
|
||||
|
||||
<!-- 列表渲染 -->
|
||||
<list-item
|
||||
v-for="item in articleList"
|
||||
:class="['list-item', `list-item__thumbnail-${item.thumbnail.length}`]"
|
||||
:key="item._id"
|
||||
@click="goToDetailPage(item)"
|
||||
>
|
||||
<template v-if="item.thumbnail.length == 0">
|
||||
<view class="list-item__content">
|
||||
<view class="list-item__content-title">
|
||||
<text class="text">{{ item.title }}</text>
|
||||
</view>
|
||||
<view class="list-item__content-info">
|
||||
<view class="list-item__author">
|
||||
<text class="text">{{ item!.user_id!.length > 0 ? item.user_id[0].nickname : '' }}</text>
|
||||
</view>
|
||||
<view class="list-item__publish-date">
|
||||
<text class="text">{{ publishTime(item.publish_date) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template v-if="item.thumbnail.length == 1">
|
||||
<view class="list-item__content">
|
||||
<view class="list-item__content-title">
|
||||
<text class="text">{{ item.title }}</text>
|
||||
</view>
|
||||
<view class="list-item__content-info">
|
||||
<view class="list-item__author">
|
||||
<text class="text">{{ item!.user_id!.length > 0 ? item.user_id[0].nickname : '' }}</text>
|
||||
</view>
|
||||
<view class="list-item__publish-date">
|
||||
<text class="text">{{ publishTime(item.publish_date) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="list-item__thumbnails">
|
||||
<image
|
||||
v-for="image in item.thumbnail"
|
||||
:src="image"
|
||||
mode="aspectFill"
|
||||
class="list-item__img"
|
||||
></image>
|
||||
</view>
|
||||
</template>
|
||||
<template v-if="item.thumbnail.length == 3">
|
||||
<view class="list-item__content">
|
||||
<view class="list-item__content-title">
|
||||
<text>{{ item.title }}</text>
|
||||
</view>
|
||||
<view class="list-item__thumbnails">
|
||||
<image
|
||||
v-for="image in item.thumbnail"
|
||||
:src="image"
|
||||
mode="aspectFill"
|
||||
class="list-item__img"
|
||||
></image>
|
||||
</view>
|
||||
<view class="list-item__content-info">
|
||||
<view class="list-item__author">
|
||||
<text class="text">{{ item!.user_id!.length > 0 ? item.user_id[0].nickname : '' }}</text>
|
||||
</view>
|
||||
<view class="list-item__publish-date">
|
||||
<text class="text">{{ publishTime(item.publish_date) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</list-item>
|
||||
<list-item class="load-state">
|
||||
<text class="text">{{ loading ? '加载中...' : (hasMore ? '上拉加载更多' : '没有更多数据了') }}</text>
|
||||
</list-item>
|
||||
</list-view>
|
||||
</unicloud-db>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
type ArticleAuthor = {
|
||||
_id: string
|
||||
nickname: string
|
||||
}
|
||||
type ArticleItem = {
|
||||
_id: string
|
||||
title: string
|
||||
publish_date: number
|
||||
thumbnail: string[]
|
||||
user_id: ArticleAuthor[]
|
||||
}
|
||||
|
||||
import {parseImageUrl} from "@/uni_modules/uni-cms-article/common/parse-image-url.uts";
|
||||
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time.uts";
|
||||
import type {ParseImageUrlResult} from '@/uni_modules/uni-cms-article/common/parse-image-url.uts'
|
||||
|
||||
export default {
|
||||
name: "uni-cms-article-list",
|
||||
emits: ['onRefresh', 'onLoadMore'],
|
||||
props: {
|
||||
collectionList: {
|
||||
type: Array as any[],
|
||||
default: (): any[] => []
|
||||
},
|
||||
loadTime: {
|
||||
type: String,
|
||||
default: 'auto'
|
||||
},
|
||||
refresherEnabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
articleList: [] as ArticleItem[],
|
||||
refresherTriggered: false,
|
||||
refreshState: 0,
|
||||
refreshText: [
|
||||
'继续下拉执行刷新',
|
||||
'释放立即刷新',
|
||||
'正在加载中',
|
||||
'加载成功'
|
||||
],
|
||||
networkType: ""
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.checkNetwork()
|
||||
},
|
||||
methods: {
|
||||
checkNetwork() {
|
||||
uni.getNetworkType({
|
||||
success: (res) => {
|
||||
this.networkType = res.networkType;
|
||||
}
|
||||
});
|
||||
},
|
||||
publishTime(timestamp: number): string {
|
||||
return translatePublishTime(timestamp)
|
||||
},
|
||||
async onListLoad(data: UTSJSONObject[], ended: boolean, pagination: UTSJSONObject): Promise<void> {
|
||||
const listData: ArticleItem[] = data.map((item: UTSJSONObject): ArticleItem => {
|
||||
let articleItem: ArticleItem = {
|
||||
_id: item.getString('_id')!,
|
||||
title: item.getString('title')!,
|
||||
publish_date: item.getNumber('publish_date')!,
|
||||
thumbnail: [],
|
||||
user_id: item.getArray<ArticleAuthor>('user_id')! as ArticleAuthor[]
|
||||
}
|
||||
|
||||
if (typeof item.getAny('thumbnail') === 'string') {
|
||||
articleItem.thumbnail = [item.getAny('thumbnail')! as string]
|
||||
} else {
|
||||
articleItem.thumbnail = item.getArray<string>('thumbnail')!
|
||||
}
|
||||
|
||||
return articleItem
|
||||
})
|
||||
|
||||
// 处理cloud://文件链接
|
||||
for (let i = 0; i < listData.length; i++) {
|
||||
const article = listData[i]
|
||||
const parseImages = await parseImageUrl(article.thumbnail)
|
||||
|
||||
if (parseImages != null) {
|
||||
article.thumbnail = parseImages.map((image: ParseImageUrlResult): string => image.src)
|
||||
}
|
||||
}
|
||||
|
||||
this.articleList = pagination.getNumber('current') == 1 ? listData : this.articleList.concat(listData)
|
||||
},
|
||||
refresherrefresh() {
|
||||
this.refresherTriggered = true
|
||||
this.refreshState = 2;
|
||||
|
||||
(this.$refs['udb'] as UniCloudDBElement)!.loadData({
|
||||
clear: true,
|
||||
success: (_: any) => {
|
||||
this.refresherTriggered = false
|
||||
this.refreshState = 3
|
||||
}
|
||||
})
|
||||
},
|
||||
refresherpulling(e: RefresherEvent) {
|
||||
if (e.detail.dy.toDouble() == 0.0) {
|
||||
this.refreshState = 0
|
||||
} else if (e.detail.dy > 45) {
|
||||
this.refreshState = 1
|
||||
}
|
||||
},
|
||||
scrolltolower() {
|
||||
(this.$refs['udb'] as UniCloudDBElement)!.loadMore()
|
||||
},
|
||||
reLoadList() {
|
||||
(this.$refs['udb'] as UniCloudDBElement)!.loadData({
|
||||
clear: true
|
||||
})
|
||||
},
|
||||
goToDetailPage(article: ArticleItem) {
|
||||
uni.navigateTo({
|
||||
url: `/uni_modules/uni-cms-article/pages/detail/detail?id=${article._id}&title=${article.title}`
|
||||
})
|
||||
},
|
||||
onListLoadError () {
|
||||
this.checkNetwork()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.refresh-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.text {
|
||||
padding: 30rpx 0;
|
||||
font-size: 26rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.error-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.disconnect-icon {
|
||||
width: 200rpx;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.load-state {
|
||||
height: 90rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.text {
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.list-view {
|
||||
height: 100%;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: #f5f5f5 solid 1px;
|
||||
|
||||
&__thumbnail-1 {
|
||||
.list-item__content-title {
|
||||
height: 88rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&__thumbnail-3 {
|
||||
.list-item__thumbnails {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 20rpx -10rpx;
|
||||
margin-left: -10rpx;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.list-item__img {
|
||||
margin: 0 10rpx;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__thumbnails {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
&__img {
|
||||
width: 240rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
|
||||
&-title {
|
||||
overflow: hidden;
|
||||
|
||||
.text {
|
||||
font-size: 30rpx;
|
||||
color: #333333;
|
||||
line-height: 44rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&__author,
|
||||
&__publish-date {
|
||||
.text {
|
||||
font-size: 24rpx;
|
||||
color: #bbbbbb;
|
||||
}
|
||||
}
|
||||
|
||||
&__author {
|
||||
margin-right: 14rpx;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<view class="search-bar">
|
||||
<view :style="{ height: `${navBarHeight}px` }"></view>
|
||||
<view class="search-bar__content" @click="goToSearchPage">
|
||||
<view class="search-bar__left" v-if="!showPlaceholder">
|
||||
<view class="back-icon">
|
||||
<uni-cms-article-icons type="back" :size="26" color="#333" @click="back"></uni-cms-article-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="search-bar__center">
|
||||
<uni-cms-article-icons type="search" :size="18" color="#c0c4cc"></uni-cms-article-icons>
|
||||
<text class="search-bar__placeholder" v-if="showPlaceholder">请输入搜索内容</text>
|
||||
<input
|
||||
v-else
|
||||
ref="search-input"
|
||||
class="search-bar__input"
|
||||
placeholder="请输入搜索内容"
|
||||
v-model="searchVal"
|
||||
confirm-type="search"
|
||||
:focus="focus"
|
||||
@confirm="confirm"
|
||||
/>
|
||||
<view class="clear-icon" v-if="hasSearchValue" @click="clear">
|
||||
<uni-cms-article-icons type="closeempty" :size="12" color="#fff"></uni-cms-article-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="search-bar__right" v-if="!showPlaceholder">
|
||||
<!-- <uni-cms-article-icons type="scan" :size="20" color="#c0c4cc" @click="scan"></uni-cms-article-icons>-->
|
||||
<text class="search-bar__search-text" @click="confirm">搜索</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
// import parseScanResult from "@/uni_modules/uni-cms-article/common/parse-scan-result.uts";
|
||||
|
||||
export default {
|
||||
name: 'search-bar',
|
||||
emits: ['update:modelValue', 'clear', 'confirm'],
|
||||
data() {
|
||||
return {
|
||||
navBarHeight: 44,
|
||||
searchVal: ""
|
||||
}
|
||||
},
|
||||
props: {
|
||||
showPlaceholder: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
focus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
searchVal(newValue) {
|
||||
this.$emit('update:modelValue', newValue)
|
||||
},
|
||||
modelValue: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
this.searchVal = newVal
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasSearchValue(): boolean {
|
||||
return this.searchVal != ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
back() {
|
||||
// 获取当前页面数量
|
||||
const pages = getCurrentPages()
|
||||
// 定义文章列表页的路径
|
||||
const pageUrl = '/uni_modules/uni-cms-article/pages/list/list'
|
||||
|
||||
// 如果当前页面数量大于1,返回上一页
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack({})
|
||||
} else { // 否则跳转到文章列表页
|
||||
uni.redirectTo({
|
||||
url: pageUrl,
|
||||
fail: (e: RedirectToFail) => {
|
||||
// 如果跳转失败,说明当前页面是tabbar页面,需要使用switchTab跳转
|
||||
if (e.errMsg.indexOf('tabbar') !== -1) {
|
||||
uni.switchTab({
|
||||
url: pageUrl
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
clear() {
|
||||
this.searchVal = '';
|
||||
(this.$refs['search-input'] as Element).blur()
|
||||
this.$emit('clear')
|
||||
},
|
||||
confirm() {
|
||||
(this.$refs['search-input'] as Element).blur()
|
||||
this.$emit('confirm', this.searchVal)
|
||||
},
|
||||
scan() {
|
||||
// 扫码暂不支持
|
||||
// uni.scanCode({
|
||||
// onlyFromCamera: true,
|
||||
// scanType: ["qrCode"],
|
||||
// success: (e) => parseScanResult(e.result),
|
||||
// fail: (e) => {
|
||||
// console.error(e)
|
||||
// }
|
||||
// })
|
||||
},
|
||||
goToSearchPage() {
|
||||
if (!this.showPlaceholder) return
|
||||
|
||||
uni.navigateTo({
|
||||
url: '/uni_modules/uni-cms-article/pages/search/search'
|
||||
})
|
||||
},
|
||||
hideKeyboard() {
|
||||
(this.$refs['search-input'] as Element).blur()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.search-bar {
|
||||
background: #fff;
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 10rpx 20rpx;
|
||||
}
|
||||
|
||||
&__left {
|
||||
margin-left: -20rpx;
|
||||
}
|
||||
|
||||
&__center {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #F8F8F8;
|
||||
padding: 20rpx;
|
||||
margin: 0 20rpx;
|
||||
margin-left: 0;
|
||||
border-radius: 40rpx;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
color: #c0c4cc;
|
||||
font-size: 28rpx;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
&__input {
|
||||
margin-left: 10rpx;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__search-text {
|
||||
color: #c0402b;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.clear-icon {
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
background: #c0c4cc;
|
||||
border-radius: 15rpx;
|
||||
margin-right: 0;
|
||||
margin-left: 10rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"noData": "No Data",
|
||||
"noNetwork": "Network error",
|
||||
"toSet": "Go to settings",
|
||||
"error": "error"
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import en from './en.json'
|
||||
import zhHans from './zh-Hans.json'
|
||||
export default {
|
||||
en,
|
||||
'zh-Hans': zhHans
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"noData": "暂无数据",
|
||||
"noNetwork": "网络异常",
|
||||
"toSet": "前往设置",
|
||||
"error": "错误"
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
新增uni-load-state组件,这是一个封装数据请求状态的组件。根据uniCloud-db组件提供的参数直接响应对应的效果。
|
||||
包括加载中、当前页面为空、没有更多数据、上拉加载更多;
|
||||
加载错误判断,如果是断网就引导打开系统网络设置页面。恢复联网后自动触发networkResume方法。
|
@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<view @appear="appear">
|
||||
<view v-if="state.error">
|
||||
<view class="box" v-if="networkType == 'none'">
|
||||
<image class="icon-image" src="/uni_modules/uni-cms-article/static/disconnection.png" mode="widthFix"></image>
|
||||
<text class="tip-text">{{noNetwork}}</text>
|
||||
<view class="btn btn-default" @click="openSettings">
|
||||
<text class="btn-text">{{toSet}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="error" v-else>{{error}}:{{JSON.stringify(state.error)}}</text>
|
||||
</view>
|
||||
<template v-else>
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<text class="state-text">{{state.loading?'加载中...':(state.hasMore?'上拉加载更多':'没有更多数据了')}}</text>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-NVUE -->
|
||||
<uni-load-more class="uni-load-more" :status="state.loading?'loading':(state.hasMore?'hasMore':'noMore')"></uni-load-more>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
initVueI18n
|
||||
} from '@dcloudio/uni-i18n'
|
||||
import messages from './i18n/index.js'
|
||||
const {
|
||||
t
|
||||
} = initVueI18n(messages)
|
||||
|
||||
export default {
|
||||
name: "uni-load-state",
|
||||
computed: {
|
||||
noData() {
|
||||
return t('noData')
|
||||
},
|
||||
noNetwork() {
|
||||
return t('noNetwork')
|
||||
},
|
||||
toSet() {
|
||||
return t('toSet')
|
||||
},
|
||||
error() {
|
||||
return t('error')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
"networkType": ""
|
||||
};
|
||||
},
|
||||
props: {
|
||||
state: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {
|
||||
"loading": true,
|
||||
"hasMore": false,
|
||||
"pagination": {
|
||||
"pages": 0
|
||||
},
|
||||
"data": [],
|
||||
"error": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
uni.onNetworkStatusChange(({
|
||||
networkType
|
||||
}) => {
|
||||
if (this.networkType == 'none' && networkType != 'none') { //之前没网现在有了
|
||||
this.$emit('networkResume')
|
||||
}
|
||||
this.networkType = networkType;
|
||||
});
|
||||
uni.getNetworkType({
|
||||
success: ({
|
||||
networkType
|
||||
}) => {
|
||||
this.networkType = networkType;
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
appear() {
|
||||
if (!this.state.loading && this.state.hasMore) {
|
||||
this.$emit('loadMore')
|
||||
}
|
||||
},
|
||||
openSettings() {
|
||||
if (uni.getSystemInfoSync().platform == "ios") {
|
||||
var UIApplication = plus.ios.import("UIApplication");
|
||||
var application2 = UIApplication.sharedApplication();
|
||||
var NSURL2 = plus.ios.import("NSURL");
|
||||
var setting2 = NSURL2.URLWithString("App-prefs:root=General");
|
||||
application2.openURL(setting2);
|
||||
plus.ios.deleteObject(setting2);
|
||||
plus.ios.deleteObject(NSURL2);
|
||||
plus.ios.deleteObject(application2);
|
||||
} else {
|
||||
var Intent = plus.android.importClass("android.content.Intent");
|
||||
var Settings = plus.android.importClass("android.provider.Settings");
|
||||
var mainActivity = plus.android.runtimeMainActivity();
|
||||
var intent = new Intent(Settings.ACTION_SETTINGS);
|
||||
mainActivity.startActivity(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.box {
|
||||
flex: 1;
|
||||
width: 700rpx;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.uni-load-more{
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.state-text {
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
width: 690rpx;
|
||||
padding: 10rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.icon-image {
|
||||
width: 300rpx;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
color: #999999;
|
||||
font-size: 32rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 5px 10px;
|
||||
width: 128px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
color: #999999;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
border-color: #999999;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.error {
|
||||
width: 690rpx;
|
||||
color: #DD524D;
|
||||
}
|
||||
</style>
|
35
uni_modules/uni-cms-article/license.md
Normal file
35
uni_modules/uni-cms-article/license.md
Normal file
@ -0,0 +1,35 @@
|
||||
# uni-cms源码使用许可协议
|
||||
|
||||
2022年10月
|
||||
|
||||
本许可协议,是数字天堂(北京)网络技术有限公司(以下简称DCloud)对其所拥有著作权的“DCloud uni-cms”(以下简称软件),提供的使用许可协议。
|
||||
|
||||
您对“软件”的复制、使用、修改及分发受本许可协议的条款的约束,如您不接受本协议,则不能使用、复制、修改本软件。
|
||||
|
||||
**授权许可范围**
|
||||
|
||||
a) 授予您永久性的、全球性的、免费的、非独占的、不可撤销的本软件的源码使用许可,您可以使用这些源码制作自己的应用。
|
||||
|
||||
b) 您只能在DCloud产品体系内使用本软件及其源码。您不能将源码修改后运行在DCloud产品体系之外的环境,比如客户端脱离uni-app,或服务端脱离uniCloud。
|
||||
|
||||
c) DCloud未向您授权商标使用许可。您在根据本软件源码制作自己的应用时,需以自己的名义发布软件,而不是以DCloud名义发布。
|
||||
|
||||
d) 本协议不构成代理关系。
|
||||
|
||||
DCloud的责任限制 “软件”在提供时不带任何明示或默示的担保。在任何情况下,DCloud不对任何人因使用“软件”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。
|
||||
|
||||
**您的责任限制**
|
||||
|
||||
a) 您需要在授权许可范围内使用软件。
|
||||
|
||||
b) 您在分发自己的应用时,不得侵犯DCloud商标和名誉权利。
|
||||
|
||||
c) 您不得进行破解、反编译、套壳等侵害DCloud知识产权的行为。您不得利用DCloud系统漏洞谋利或侵害DCloud利益,如您发现DCloud系统漏洞应第一时间通知DCloud。您不得进行攻击DCloud的服务器、网络等妨碍DCloud运营的行为。您不得利用DCloud的产品进行与DCloud争夺开发者的行为。
|
||||
|
||||
d) 如您违反本许可协议,需承担因此给DCloud造成的损失。
|
||||
|
||||
本协议签订地点为中华人民共和国北京市海淀区。
|
||||
|
||||
根据发展,DCloud可能会对本协议进行修改。修改时,DCloud会在产品或者网页中显著的位置发布相关信息以便及时通知到用户。如果您选择继续使用本框架,即表示您同意接受这些修改。
|
||||
|
||||
条款结束
|
90
uni_modules/uni-cms-article/package.json
Normal file
90
uni_modules/uni-cms-article/package.json
Normal file
@ -0,0 +1,90 @@
|
||||
{
|
||||
"id": "uni-cms-article",
|
||||
"displayName": "uni-cms-article",
|
||||
"version": "1.0.16",
|
||||
"description": "uni-cms的用户端,包括文章展示、搜索、看广告解锁等功能",
|
||||
"keywords": [
|
||||
"uni-cms-article",
|
||||
"cms",
|
||||
"uni-cms",
|
||||
"内容管理",
|
||||
"文章"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.1.0"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "uniapp-template-page",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [
|
||||
"uni-search-bar",
|
||||
"uni-nav-bar",
|
||||
"uni-list"
|
||||
],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "y",
|
||||
"aliyun": "y",
|
||||
"alipay": "y"
|
||||
},
|
||||
"client": {
|
||||
"Vue": {
|
||||
"vue2": "y",
|
||||
"vue3": "y"
|
||||
},
|
||||
"App": {
|
||||
"app-vue": "y",
|
||||
"app-nvue": "y"
|
||||
},
|
||||
"H5-mobile": {
|
||||
"Safari": "y",
|
||||
"Android Browser": "y",
|
||||
"微信浏览器(Android)": "y",
|
||||
"QQ浏览器(Android)": "y"
|
||||
},
|
||||
"H5-pc": {
|
||||
"Chrome": "y",
|
||||
"IE": "u",
|
||||
"Edge": "y",
|
||||
"Firefox": "y",
|
||||
"Safari": "y"
|
||||
},
|
||||
"小程序": {
|
||||
"微信": "y",
|
||||
"阿里": "u",
|
||||
"百度": "u",
|
||||
"字节跳动": "u",
|
||||
"QQ": "u",
|
||||
"钉钉": "u",
|
||||
"快手": "u",
|
||||
"飞书": "u",
|
||||
"京东": "u"
|
||||
},
|
||||
"快应用": {
|
||||
"华为": "u",
|
||||
"联盟": "u"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
223
uni_modules/uni-cms-article/pages/detail/detail.uvue
Normal file
223
uni_modules/uni-cms-article/pages/detail/detail.uvue
Normal file
@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<scroll-view :scroll-y="true" v-if="!loading">
|
||||
<view class="meta">
|
||||
<view class="title">
|
||||
<text class="text">{{ articleDetail.title }}</text>
|
||||
</view>
|
||||
<view class="excerpt">
|
||||
<text class="text">{{ articleDetail.excerpt }}</text>
|
||||
</view>
|
||||
<view class="author">
|
||||
<template v-if="articleDetail.user_id != null">
|
||||
<text class="at">{{ articleDetail.user_id?.nickname ?? '' }}</text>
|
||||
<text class="split">·</text>
|
||||
</template>
|
||||
<text class="date">{{ publishTime(articleDetail.publish_date as number) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="content" v-if="articleDetail.content != null">
|
||||
<template v-for="(block, index) in articleDetail.content" :key="index">
|
||||
<view v-if="block.type == 'mediaVideo'">
|
||||
<video
|
||||
style="width: 300px; height: 200px; margin: 0 auto 20px;"
|
||||
:src="(block.data as UTSJSONObject).getJSON('attributes')!.getString('src')!"
|
||||
:poster="(block.data as UTSJSONObject).getJSON('attributes')!.getString('poster')!"
|
||||
></video>
|
||||
</view>
|
||||
<view v-if="block.type == 'divider'" class="divider"></view>
|
||||
<view v-if="block.type == 'unlockContent'" class="unlock-content">
|
||||
<button @click="unlockContent">请观看广告后解锁全文</button>
|
||||
</view>
|
||||
<rich-text v-if="block.type == 'rich-text'" :selectable="false" :nodes="block.data" @itemclick="richTextItemClick"></rich-text>
|
||||
<render-image-component v-if="block.type == 'image'" :deltaOp="block.data" @image-preview="onImagePreview"></render-image-component>
|
||||
</template>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time";
|
||||
import RenderImageComponent from '@/uni_modules/uni-cms-article/components/render-article-detail/image.uvue'
|
||||
|
||||
type Author = {
|
||||
_id: string
|
||||
nickname: string
|
||||
}
|
||||
type Content = {
|
||||
type: string
|
||||
data: any
|
||||
}
|
||||
type Article = {
|
||||
_id: string | null
|
||||
title: string | null
|
||||
content: Content[] | null
|
||||
excerpt: string | null
|
||||
publish_date: number | null
|
||||
user_id: Author | null
|
||||
thumbnail: string[] | null
|
||||
content_images: string[] | null
|
||||
}
|
||||
|
||||
const db = uniCloud.databaseForJQL()
|
||||
const articleDBName = 'uni-cms-articles'
|
||||
const userDBName = 'uni-id-users'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RenderImageComponent
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
id: "", // 文章ID
|
||||
title: "", // 文章标题
|
||||
articleDetail: {} as Article, // 文章详情
|
||||
// 广告相关配置
|
||||
adpId: "", // TODO: 请填写广告位ID
|
||||
watchAdUniqueType: "device" // TODO: 观看广告的唯一标识类型,可选值为 user 或者 device,user 表示用户唯一,device 表示设备唯一
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
where(): string {
|
||||
//拼接where条件 查询条件 ,更多详见 :https://uniapp.dcloud.net.cn/uniCloud/unicloud-db?id=jsquery
|
||||
return `_id =="${this.id}"`
|
||||
},
|
||||
collection(): any[] {
|
||||
return [
|
||||
db.collection(articleDBName).where(this.where).field('user_id,thumbnail,excerpt,publish_date,title,content').getTemp(),
|
||||
db.collection(userDBName).field('_id, nickname').getTemp()
|
||||
]
|
||||
}
|
||||
},
|
||||
onLoad(event: OnLoadOptions) {
|
||||
if (event.has('id')) {
|
||||
this.id = event.get('id') as string
|
||||
this.load()
|
||||
}
|
||||
|
||||
if (event.has('title')) {
|
||||
uni.setNavigationBarTitle({
|
||||
title: event.get('title') as string
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async load (): Promise<void> {
|
||||
uni.showLoading({
|
||||
title: "加载中..."
|
||||
})
|
||||
const articledb = db.collection(articleDBName).where(this.where).field('user_id,thumbnail,excerpt,publish_date,title,content').getTemp()
|
||||
const userdb = db.collection(userDBName).field('_id, nickname').getTemp()
|
||||
const res = await db.collection(articledb, userdb).get()
|
||||
|
||||
this.loadData(res.data)
|
||||
this.loading = false
|
||||
uni.hideLoading()
|
||||
},
|
||||
// 格式化发布时间
|
||||
publishTime(timestamp: number): string {
|
||||
return translatePublishTime(timestamp)
|
||||
},
|
||||
loadData(data: UTSJSONObject[]) {
|
||||
if (data.length <= 0) return
|
||||
const detail = data[0]
|
||||
|
||||
const user_id = detail.getArray<Author>('user_id')!;
|
||||
|
||||
this.articleDetail = {
|
||||
title: detail.getString('title'),
|
||||
content: detail.getArray<Content>('content'),
|
||||
excerpt: detail.getString('excerpt'),
|
||||
publish_date: detail.getNumber('publish_date'),
|
||||
thumbnail: detail.getArray<string>('thumbnail'),
|
||||
user_id: user_id.length > 0 ? user_id[0]: null,
|
||||
content_images: detail.getArray<string>('content_images')
|
||||
} as Article
|
||||
|
||||
this.title = detail.getString('title')!
|
||||
|
||||
uni.setNavigationBarTitle({
|
||||
title: this.title
|
||||
})
|
||||
},
|
||||
unlockContent () {
|
||||
uni.showModal({
|
||||
content: 'uni-app-x 暂不支持观看广告解锁全文',
|
||||
showCancel: false
|
||||
})
|
||||
},
|
||||
richTextItemClick (e: RichTextItemClickEvent) {
|
||||
if (e.detail.href != null) {
|
||||
uni.navigateTo({
|
||||
url: `/uni_modules/uni-cms-article/pages/webview/webview?url=${encodeURIComponent(e.detail.href as string)}`
|
||||
})
|
||||
}
|
||||
},
|
||||
onImagePreview (url: string) {
|
||||
const contentImages = this.articleDetail.content_images != null ? this.articleDetail.content_images: [] as string[]
|
||||
|
||||
uni.previewImage({
|
||||
current: url, // 当前显示图片的http链接
|
||||
urls: contentImages as string[] // 需要预览的图片http链接列表
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.meta {
|
||||
padding: 20rpx 30rpx 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.title {
|
||||
.text {
|
||||
font-size: 40rpx;
|
||||
line-height: 66rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.excerpt {
|
||||
margin-top: 10rpx;
|
||||
|
||||
.text {
|
||||
font-size: 26rpx;
|
||||
line-height: 40rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-direction: row;
|
||||
margin-top: 20rpx;
|
||||
|
||||
.at,
|
||||
.split,
|
||||
.date {
|
||||
font-size: 26rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.split {
|
||||
margin: 0 10rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
margin-top: 40rpx;
|
||||
padding: 0 30rpx 80rpx;
|
||||
}
|
||||
.divider {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
|
217
uni_modules/uni-cms-article/pages/detail/detail.vue
Normal file
217
uni_modules/uni-cms-article/pages/detail/detail.vue
Normal file
@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<unicloud-db v-slot:default="{data, loading, error, options}" :collection="collection" :options="formData"
|
||||
:getone="true" :where="where" :manual="true" ref="detail" foreignKey="uni-cms-articles.user_id"
|
||||
@load="loadData"
|
||||
class="article">
|
||||
<template v-if="!loading && data">
|
||||
<view class="meta">
|
||||
<view class="title">
|
||||
<text class="text">{{ data.title }}</text>
|
||||
</view>
|
||||
<view class="excerpt">
|
||||
<text class="text">{{ data.excerpt }}</text>
|
||||
</view>
|
||||
<view class="author">
|
||||
<template v-if="data.user_id[0]">
|
||||
<text class="at">{{ data.user_id[0].nickname || '' }}</text>
|
||||
<text class="split">·</text>
|
||||
</template>
|
||||
<text class="date">{{ publishTime(data.publish_date) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<render-article-detail
|
||||
:content="data.content"
|
||||
:content-images="data.content_images"
|
||||
:ad-config="{ adpId, watchAdUniqueType }"
|
||||
></render-article-detail>
|
||||
</template>
|
||||
<view class="detail-loading" v-else>
|
||||
<uni-icons type="spinner-cycle" size="35px"/>
|
||||
</view>
|
||||
</unicloud-db>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import uniNavBar from '@/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-nav-bar.vue';
|
||||
import renderArticleDetail from "@/uni_modules/uni-cms-article/components/render-article-detail/index.vue";
|
||||
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time";
|
||||
|
||||
const db = uniCloud.database()
|
||||
const articleDBName = 'uni-cms-articles'
|
||||
const userDBName = 'uni-id-users'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
uniNavBar,
|
||||
renderArticleDetail
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: "", // 文章ID
|
||||
title: "", // 文章标题
|
||||
formData: {}, // 表单数据
|
||||
|
||||
// 广告相关配置
|
||||
adpId: "", // TODO: 请填写广告位ID
|
||||
watchAdUniqueType: "device" // TODO: 观看广告的唯一标识类型,可选值为 user 或者 device,user 表示用户唯一,device 表示设备唯一
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
where() {
|
||||
//拼接where条件 查询条件 ,更多详见 :https://uniapp.dcloud.net.cn/uniCloud/unicloud-db?id=jsquery
|
||||
return `_id =="${this.id}"`
|
||||
},
|
||||
collection() {
|
||||
return [
|
||||
db.collection(articleDBName).where(this.where).field('user_id,thumbnail,excerpt,publish_date,title,content').getTemp(),
|
||||
db.collection(userDBName).field('_id, nickname').getTemp()
|
||||
]
|
||||
}
|
||||
},
|
||||
onReady() {
|
||||
// 开始加载数据,修改 where 条件后才开始去加载 clinetDB 的数据 ,需要等组件渲染完毕后才开始执行 loadData,所以不能再 onLoad 中执行
|
||||
if (this.id) { // ID 不为空,则发起查询
|
||||
this.$refs.detail.loadData()
|
||||
} else {
|
||||
uni.showToast({
|
||||
icon: 'none',
|
||||
title: 'id 不能为空'
|
||||
})
|
||||
}
|
||||
},
|
||||
onLoad(event) {
|
||||
//获取文章id,通常 id 来自上一个页面
|
||||
if (event.id) {
|
||||
this.id = event.id
|
||||
}
|
||||
|
||||
// 监听解锁内容事件
|
||||
uni.$on('onUnlockContent', this.onUnlockContent)
|
||||
},
|
||||
onUnload() {
|
||||
// 页面卸载时,移除监听事件
|
||||
uni.$off('onUnlockContent', this.onUnlockContent)
|
||||
},
|
||||
onPageScroll(e) {
|
||||
// 根据滚动位置判断是否显示导航栏
|
||||
if (e.scrollTop > 100) {
|
||||
uni.setNavigationBarTitle({
|
||||
title: this.title
|
||||
})
|
||||
} else {
|
||||
uni.setNavigationBarTitle({
|
||||
title: ''
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 将时间戳转换为可读的时间格式
|
||||
publishTime(timestamp) {
|
||||
return translatePublishTime(timestamp)
|
||||
},
|
||||
// 将文章加入阅读历史
|
||||
setReadHistory() {
|
||||
// 获取阅读历史缓存,如果不存在则为空数组
|
||||
const historyCache = uni.getStorageSync('readHistory') || []
|
||||
// 过滤掉当前文章的阅读历史
|
||||
const readHistory = historyCache.filter(item => item.article_id !== this.id)
|
||||
// 将当前文章的阅读历史添加到数组最前面
|
||||
readHistory.unshift({
|
||||
article_id: this.id,
|
||||
last_time: Date.now()
|
||||
})
|
||||
// 将更新后的阅读历史缓存到本地
|
||||
uni.setStorageSync('readHistory', readHistory)
|
||||
|
||||
},
|
||||
// 加载数据
|
||||
loadData(data) {
|
||||
// 设置文章标题
|
||||
this.title = data.title
|
||||
|
||||
// 将文章添加进阅读历史
|
||||
this.setReadHistory()
|
||||
},
|
||||
// 监听解锁内容事件,解锁内容后重新加载数据
|
||||
async onUnlockContent() {
|
||||
this.$refs.detail.loadData()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* #ifdef APP-NVUE */
|
||||
.article {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
@mixin cp {
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.detail-loading {
|
||||
margin: 100rpx auto 0;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
animation: rotate360 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate360 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
transform-origin: center center;
|
||||
}
|
||||
}
|
||||
|
||||
.meta {
|
||||
@include cp;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding-top: 20rpx;
|
||||
.title {
|
||||
.text {
|
||||
font-size: 40rpx;
|
||||
line-height: 66rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.excerpt {
|
||||
margin-top: 10rpx;
|
||||
.text {
|
||||
font-size: 26rpx;
|
||||
line-height: 40rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-direction: row;
|
||||
margin-top: 20rpx;
|
||||
|
||||
.at,
|
||||
.split,
|
||||
.date {
|
||||
font-size: 26rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.split {
|
||||
margin: 0 10rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
255
uni_modules/uni-cms-article/pages/detail/preview.vue
Normal file
255
uni_modules/uni-cms-article/pages/detail/preview.vue
Normal file
@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<unicloud-db v-slot:default="{loading, error, options}" :collection="collection" :options="formData"
|
||||
:getone="true" :where="where" :manual="true" ref="detail" foreignKey="uni-cms-articles.user_id"
|
||||
@load="loadData"
|
||||
class="article">
|
||||
<template v-if="!loading && articleData">
|
||||
<view class="preview-tip">此页面仅用于临时预览文章,链接将会在短期内失效。</view>
|
||||
<view class="meta">
|
||||
<view class="title">
|
||||
<text class="text">{{ articleData.title }}</text>
|
||||
</view>
|
||||
<view class="excerpt">
|
||||
<text class="text">{{ articleData.excerpt }}</text>
|
||||
</view>
|
||||
<view class="author">
|
||||
<template v-if="articleData.user_id && articleData.user_id[0]">
|
||||
<text class="at">{{ articleData.user_id[0].nickname || '' }}</text>
|
||||
<text class="split">·</text>
|
||||
</template>
|
||||
<text class="date">{{ publishTime(articleData.publish_date) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<render-article-detail
|
||||
:content="articleData.content"
|
||||
:content-images="articleData.content_images"
|
||||
:ad-config="{ adpId, watchAdUniqueType }"
|
||||
></render-article-detail>
|
||||
</template>
|
||||
<view class="detail-loading" v-else>
|
||||
<uni-icons type="spinner-cycle" size="35px"/>
|
||||
</view>
|
||||
</unicloud-db>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import uniNavBar from '@/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-nav-bar.vue';
|
||||
import renderArticleDetail from "@/uni_modules/uni-cms-article/components/render-article-detail/index.vue";
|
||||
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time";
|
||||
|
||||
const db = uniCloud.database()
|
||||
const articleDBName = 'uni-cms-articles'
|
||||
const userDBName = 'uni-id-users'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
uniNavBar,
|
||||
renderArticleDetail
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: "", // 文章ID
|
||||
title: "", // 文章标题
|
||||
secret: "", // 文章预览密钥
|
||||
formData: {}, // 表单数据
|
||||
articleData: null, // 文章数据
|
||||
|
||||
// 广告相关配置
|
||||
adpId: "", // TODO: 请填写广告位ID
|
||||
watchAdUniqueType: "device" // TODO: 观看广告的唯一标识类型,可选值为 user 或者 device,user 表示用户唯一,device 表示设备唯一
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
where() {
|
||||
//拼接where条件 查询条件 ,更多详见 :https://uniapp.dcloud.net.cn/uniCloud/unicloud-db?id=jsquery
|
||||
return `_id =="${this.id}" && preview_secret =="${this.secret}"`
|
||||
},
|
||||
collection() {
|
||||
return [
|
||||
db.collection(articleDBName).where(this.where).field('user_id,thumbnail,excerpt,publish_date,title,content,preview_secret,preview_expired,article_status').getTemp(),
|
||||
db.collection(userDBName).field('_id, nickname').getTemp()
|
||||
]
|
||||
}
|
||||
},
|
||||
onReady() {
|
||||
// 开始加载数据,修改 where 条件后才开始去加载 clinetDB 的数据 ,需要等组件渲染完毕后才开始执行 loadData,所以不能再 onLoad 中执行
|
||||
if (this.id) { // ID 不为空,则发起查询
|
||||
this.$refs.detail.loadData()
|
||||
} else {
|
||||
uni.showToast({
|
||||
icon: 'none',
|
||||
title: 'id 不能为空'
|
||||
})
|
||||
}
|
||||
},
|
||||
onLoad(event) {
|
||||
//获取文章id,通常 id 来自上一个页面
|
||||
if (event.id) {
|
||||
this.id = event.id
|
||||
this.secret = event.secret
|
||||
}
|
||||
|
||||
// 监听解锁内容事件
|
||||
uni.$on('onUnlockContent', this.onUnlockContent)
|
||||
},
|
||||
onUnload() {
|
||||
// 页面卸载时,移除监听事件
|
||||
uni.$off('onUnlockContent', this.onUnlockContent)
|
||||
},
|
||||
onPageScroll(e) {
|
||||
// 根据滚动位置判断是否显示导航栏
|
||||
if (e.scrollTop > 100) {
|
||||
uni.setNavigationBarTitle({
|
||||
title: this.title
|
||||
})
|
||||
} else {
|
||||
uni.setNavigationBarTitle({
|
||||
title: ''
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 将时间戳转换为可读的时间格式
|
||||
publishTime(timestamp) {
|
||||
return translatePublishTime(timestamp)
|
||||
},
|
||||
// 加载数据
|
||||
loadData(data) {
|
||||
if (!data) {
|
||||
return uni.showModal({
|
||||
content: "文章不存在/预览密钥不存在",
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
// #ifdef H5
|
||||
window.close()
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
uni.navigateBack()
|
||||
// #endif
|
||||
}
|
||||
})
|
||||
}
|
||||
// 文章已发布,跳转到文章详情页
|
||||
if (data.article_status === 1) {
|
||||
uni.showToast({
|
||||
icon: 'none',
|
||||
title: '文章已发布'
|
||||
})
|
||||
uni.redirectTo({
|
||||
url: `/uni_modules/uni-cms-article/pages/detail/detail?id=${this.id}`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 预览已过期,提示用户
|
||||
if (data.preview_expired < Date.now()) {
|
||||
return uni.showModal({
|
||||
content: "预览已失效",
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
// #ifdef H5
|
||||
window.close()
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
uni.navigateBack()
|
||||
// #endif
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 设置文章标题
|
||||
this.title = data.title
|
||||
this.articleData = data
|
||||
},
|
||||
// 监听解锁内容事件,解锁内容后重新加载数据
|
||||
async onUnlockContent() {
|
||||
this.$refs.detail.loadData()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* #ifdef APP-NVUE */
|
||||
.article {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
@mixin cp {
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.detail-loading {
|
||||
margin: 100rpx auto 0;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
animation: rotate360 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate360 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
transform-origin: center center;
|
||||
}
|
||||
}
|
||||
|
||||
.meta {
|
||||
@include cp;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding-top: 20rpx;
|
||||
.title {
|
||||
.text {
|
||||
font-size: 40rpx;
|
||||
line-height: 66rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.excerpt {
|
||||
margin-top: 10rpx;
|
||||
.text {
|
||||
font-size: 26rpx;
|
||||
line-height: 40rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-direction: row;
|
||||
margin-top: 20rpx;
|
||||
|
||||
.at,
|
||||
.split,
|
||||
.date {
|
||||
font-size: 26rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.split {
|
||||
margin: 0 10rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview-tip {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
background: #fcd791;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
298
uni_modules/uni-cms-article/pages/list/list.nvue
Normal file
298
uni_modules/uni-cms-article/pages/list/list.nvue
Normal file
@ -0,0 +1,298 @@
|
||||
<template>
|
||||
<view class="pages">
|
||||
<view class="placeholder-bar">
|
||||
<statusBar></statusBar>
|
||||
<view :style="{ height: `${navBarHeight}px` }"></view>
|
||||
</view>
|
||||
|
||||
<view class="nav-box">
|
||||
<!-- #ifndef H5 -->
|
||||
<statusBar></statusBar>
|
||||
<!-- #endif -->
|
||||
<view class="nav" :style="{ height: `${navBarHeight}px` }">
|
||||
<!-- #ifdef MP -->
|
||||
<view class="mp-button-left-placeholder" :style="{ width: `${mpButtonLeftPlaceholderSize}px` }"></view>
|
||||
<!-- #endif -->
|
||||
<!-- 搜索功能 -->
|
||||
<view class="uni-search-box">
|
||||
<uni-search-bar ref="searchBar" radius="100" cancelButton="none" disabled
|
||||
:placeholder="inputPlaceholder"/>
|
||||
<view class="cover-search-bar" @click="searchClick"></view>
|
||||
</view>
|
||||
<!-- #ifdef MP -->
|
||||
<view class="mp-button-placeholder" :style="{ width: `${mpButtonPlaceholderSize}px` }"></view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<unicloud-db ref='udb' v-slot:default="{ pagination, hasMore, loading, error, options }" @error="onqueryerror"
|
||||
:collection="colList" :page-size="10" orderby="publish_date desc" @load="listLoad">
|
||||
<!-- 基于 uni-list 的页面布局 field="user_id.nickname"-->
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<list class="uni-list" :border="false" :style="{ height: listHeight }">
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-NVUE -->
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="uni-list"
|
||||
refresher-enabled
|
||||
:refresher-triggered="loadType=== 'refresh'"
|
||||
:style="{ height: listHeight }"
|
||||
@refresherrefresh="refresh"
|
||||
@scrolltolower="loadMore"
|
||||
>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<refresh-box :loading="loading" @refresh="refresh"></refresh-box>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 列表渲染 -->
|
||||
<template v-for="item in listData">
|
||||
<not-cover v-if="item.thumbnail && item.thumbnail.length === 0" :data="item"></not-cover>
|
||||
<right-small-cover v-else-if="item.thumbnail && item.thumbnail.length === 1"
|
||||
:data="item"></right-small-cover>
|
||||
<three-cover v-else-if="item.thumbnail && item.thumbnail.length === 3"
|
||||
:data="item"></three-cover>
|
||||
</template>
|
||||
|
||||
<!-- 加载状态:上拉加载更多,加载中,没有更多数据了,加载错误 -->
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<uni-list-item>
|
||||
<template v-slot:body>
|
||||
<!-- #endif -->
|
||||
<uni-load-state @networkResume="refresh"
|
||||
:state="{ data: listData, pagination, hasMore, loading, error }"
|
||||
@loadMore="loadMore">
|
||||
</uni-load-state>
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
</template>
|
||||
</uni-list-item>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-NVUE -->
|
||||
</scroll-view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
</list>
|
||||
<!-- #endif -->
|
||||
</unicloud-db>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import statusBar from "@/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-status-bar";
|
||||
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time";
|
||||
import refreshBox from "@/uni_modules/uni-cms-article/components/refresh-box/refreshBox.nvue";
|
||||
|
||||
import notCover from "@/uni_modules/uni-cms-article/components/list-template/not-cover.vue";
|
||||
import rightSmallCover from "@/uni_modules/uni-cms-article/components/list-template/right-small-cover.vue";
|
||||
import threeCover from "@/uni_modules/uni-cms-article/components/list-template/three-cover.vue";
|
||||
import {parseImageUrl} from "@/uni_modules/uni-cms-article/common/parse-image-url";
|
||||
|
||||
const db = uniCloud.database();
|
||||
const articleDBName = 'uni-cms-articles'
|
||||
const userDBName = 'uni-id-users'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
statusBar,
|
||||
refreshBox,
|
||||
notCover,
|
||||
rightSmallCover,
|
||||
threeCover
|
||||
},
|
||||
computed: {
|
||||
// 根据当前语言返回不同的搜索框占位符
|
||||
inputPlaceholder(e) {
|
||||
if (uni.getStorageSync('CURRENT_LANG') == "en") {
|
||||
return 'Please enter the search content' // 英文
|
||||
} else {
|
||||
return '请输入搜索内容' // 中文
|
||||
}
|
||||
},
|
||||
// 连表查询,返回两个集合的查询结果
|
||||
colList() {
|
||||
return [
|
||||
db.collection(articleDBName).where(this.where).field('thumbnail,title,publish_date,user_id').getTemp(), // 文章集合
|
||||
db.collection(userDBName).field('_id,nickname').getTemp() // 用户集合
|
||||
]
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
where: '"article_status" == 1', // 查询条件
|
||||
showRefresh: false, // 是否显示刷新按钮
|
||||
listHeight: 0, // 列表高度
|
||||
mpButtonLeftPlaceholderSize: 0, // 小程序左侧icon占位大小
|
||||
mpButtonPlaceholderSize: 87, // 小程序导航栏按钮占位大小
|
||||
navBarHeight: 44, // 导航栏高度
|
||||
refreshStatus: 0, // 刷新状态 0: 未刷新 1: 刷新中 2: 刷新完成
|
||||
listData: [], // 列表数据
|
||||
loadType: null
|
||||
}
|
||||
},
|
||||
async onReady() {
|
||||
// #ifdef MP
|
||||
this.initNavBarSize() // 初始化导航栏大小
|
||||
// #endif
|
||||
/* 可用窗口高度 - 搜索框高 - 状态栏高 */
|
||||
this.listHeight = uni.getSystemInfoSync().windowHeight - uni.getSystemInfoSync().statusBarHeight - this.navBarHeight + 'px'; // 计算列表高度
|
||||
},
|
||||
methods: {
|
||||
async listLoad(data) {
|
||||
const listData = data.map(item => {
|
||||
if (typeof item.thumbnail === 'string') {
|
||||
item.thumbnail = [item.thumbnail]
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
// 处理腾讯云文件链接
|
||||
for (const article of listData) {
|
||||
const parseImages = await parseImageUrl(article.thumbnail)
|
||||
|
||||
article.thumbnail = parseImages ? parseImages.map(image => image.src): []
|
||||
}
|
||||
|
||||
this.listData = this.loadType === 'loadMore' ? this.listData.concat(listData) : listData
|
||||
this.loadType = null
|
||||
},
|
||||
// 初始化导航栏大小
|
||||
initNavBarSize() {
|
||||
// 获取小程序导航栏按钮信息
|
||||
// #ifdef MP-TOUTIAO
|
||||
let menuButtonInfo = tt.getCustomButtonBoundingClientRect()
|
||||
menuButtonInfo.width = menuButtonInfo.capsule.width // 小程序按钮区域中使用的按钮宽度
|
||||
this.mpButtonLeftPlaceholderSize = menuButtonInfo.leftIcon.width + 10
|
||||
// #endif
|
||||
// #ifndef MP-TOUTIAO
|
||||
let menuButtonInfo = uni.getMenuButtonBoundingClientRect()
|
||||
// #endif
|
||||
// 计算小程序导航栏按钮占位大小
|
||||
this.mpButtonPlaceholderSize = menuButtonInfo.width + 10
|
||||
// 获取系统信息,判断是否为 iOS 系统,设置导航栏高度
|
||||
this.navBarHeight = uni.getSystemInfoSync().system.toLowerCase().includes('ios') ? 44 : 48
|
||||
},
|
||||
// 格式化时间戳
|
||||
publishTime(timestamp) {
|
||||
return translatePublishTime(timestamp)
|
||||
},
|
||||
// 点击搜索框
|
||||
searchClick(e) {
|
||||
uni.hideKeyboard();
|
||||
uni.navigateTo({
|
||||
url: '/uni_modules/uni-cms-article/pages/search/search'
|
||||
});
|
||||
},
|
||||
// 重试
|
||||
retry() {
|
||||
this.refresh()
|
||||
},
|
||||
// 刷新
|
||||
refresh() {
|
||||
this.loadType = 'refresh'
|
||||
this.$refs.udb.loadData({
|
||||
clear: true
|
||||
}, () => {
|
||||
uni.stopPullDownRefresh()
|
||||
// #ifdef APP-NVUE
|
||||
this.showRefresh = false
|
||||
// #endif
|
||||
})
|
||||
},
|
||||
// 加载更多
|
||||
loadMore() {
|
||||
this.loadType = 'loadMore'
|
||||
this.$refs.udb.loadMore()
|
||||
},
|
||||
// 查询出错
|
||||
onqueryerror(e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
// #ifdef H5
|
||||
// 下拉刷新
|
||||
onPullDownRefresh() {
|
||||
this.refresh()
|
||||
},
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* #ifndef APP-NVUE */
|
||||
.pages view {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
.pages {
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.refresh {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-box {
|
||||
background-color: #FFFFFF;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
/* #ifndef APP-PLUS */
|
||||
z-index: 9;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.pages .nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.uni-search-box {
|
||||
flex: 1;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.uni-search-box ::v-deep .uni-searchbar {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.uni-search-box ::v-deep .uni-searchbar__box {
|
||||
height: 32px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.cover-search-bar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
/* #ifndef APP-NVUE */
|
||||
z-index: 999;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.pages .uni-list ::v-deep .uni-list-item__container {
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pages .uni-list ::v-deep .uni-list-item {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.pages .uni-list ::v-deep .uni-load-more {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.pages .uni-list ::v-deep .uni-list--border:after {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
</style>
|
33
uni_modules/uni-cms-article/pages/list/list.uvue
Normal file
33
uni_modules/uni-cms-article/pages/list/list.uvue
Normal file
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<uni-cms-article-search-bar :show-placeholder="true"></uni-cms-article-search-bar>
|
||||
<uni-cms-article-list
|
||||
:collectionList="colList"
|
||||
:refresherEnabled="true"
|
||||
style="flex: 1;"
|
||||
></uni-cms-article-list>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
const db = uniCloud.databaseForJQL()
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
colList(): any[] {
|
||||
return [
|
||||
db.collection('uni-cms-articles').where("\"article_status\" == 1").field('thumbnail,title,publish_date,user_id').getTemp(), // 文章集合
|
||||
db.collection('uni-id-users').field('_id,nickname').getTemp() // 用户集合
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
752
uni_modules/uni-cms-article/pages/search/search.nvue
Normal file
752
uni_modules/uni-cms-article/pages/search/search.nvue
Normal file
@ -0,0 +1,752 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="search-container">
|
||||
<!-- 搜索框 -->
|
||||
<view class="search-container-bar">
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<uni-icons class="search-icons" :color="iconColor" size="22" type="mic-filled" @click="speech" />
|
||||
<!-- #endif -->
|
||||
<!-- :cancelText="keyBoardPopup ? '取消' : '搜索'" -->
|
||||
<uni-search-bar ref="searchBar" style="flex:1;" radius="100" v-model="searchText" :focus="focus"
|
||||
:placeholder="hotWorld" clearButton="auto" cancelButton="none" @clear="cancel" @confirm="confirm"
|
||||
@cancel="cancel" />
|
||||
<uni-icons class="scan-icons" :color="iconColor" size="22" type="scan" @click="scanEvent"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="search-body">
|
||||
<unicloud-db ref='listUdb' v-slot:default="{ pagination, hasMore, loading, error, options }"
|
||||
@error="onqueryerror" :collection="colList" :page-size="10" orderby="publish_date desc" @load="onDbLoad"
|
||||
loadtime="manual">
|
||||
<template v-if="!isLoadData">
|
||||
<!-- 搜索历史 -->
|
||||
<view class="word-container" v-if="localSearchList.length">
|
||||
<view class="word-container_header">
|
||||
<text class="word-container_header-text">搜索历史</text>
|
||||
<uni-icons v-if="!localSearchListDel" @click="localSearchListDel = true" class="search-icons"
|
||||
style="padding-right: 0;" :color="iconColor" size="18" type="trash"></uni-icons>
|
||||
<view v-else class="flex-center flex-row"
|
||||
style="font-weight: 500;justify-content: space-between;">
|
||||
<text
|
||||
style="font-size: 22rpx;color: #666;padding-top:4rpx;padding-bottom:4rpx;padding-right:20rpx;"
|
||||
@click="LocalSearchListClear">全部删除</text>
|
||||
<text
|
||||
style="font-size: 22rpx;color: #c0402b;padding-top:4rpx;padding-bottom:4rpx;padding-left:20rpx;"
|
||||
@click="localSearchListDel = false">完成</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="word-container_body">
|
||||
<view class="flex-center flex-row word-container_body-text"
|
||||
v-for="(word, index) in localSearchList" :key="index"
|
||||
@click="LocalSearchlistItemClick(word, index)">
|
||||
<text class="word-display" :key="word">{{ word }}</text>
|
||||
<uni-icons v-if="localSearchListDel" size="12" type="closeempty" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 搜索发现 -->
|
||||
<view class="word-container">
|
||||
<view class="word-container_header">
|
||||
<view class="flex-center flex-row">
|
||||
<text class="word-container_header-text">搜索发现</text>
|
||||
<uni-icons v-if="!netHotListIsHide" class="search-icons" :color="iconColor" size="14"
|
||||
type="reload" @click="searchHotRefresh"></uni-icons>
|
||||
</view>
|
||||
<uni-icons class="search-icons" style="padding-right: 0;" :color="iconColor" size="18"
|
||||
:type="netHotListIsHide ? 'eye-slash' : 'eye'"
|
||||
@click="netHotListIsHide = !netHotListIsHide"></uni-icons>
|
||||
</view>
|
||||
<unicloud-db ref="udb" #default="{ data, loading, error, options }" field="content"
|
||||
collection="opendb-search-hot" orderby="create_date desc,count desc" page-data="replace"
|
||||
:page-size="10">
|
||||
<text v-if="loading && !netHotListIsHide" class="word-container_body-info">正在加载...</text>
|
||||
<view v-else class="word-container_body">
|
||||
<template v-if="!netHotListIsHide">
|
||||
<text v-if="error" class="word-container_body-info">{{ error.message }}</text>
|
||||
<template v-else>
|
||||
<text v-for="(word, index) in data" class="word-container_body-text" :key="index"
|
||||
@click="search(word.content)">{{ word.content }}</text>
|
||||
</template>
|
||||
</template>
|
||||
<view v-else style="flex:1;">
|
||||
<text class="word-container_body-info">当前搜索发现已隐藏</text>
|
||||
</view>
|
||||
</view>
|
||||
</unicloud-db>
|
||||
</view>
|
||||
</template>
|
||||
<uni-list v-else class="uni-list" :border="false" :style="{ height: listHeight }">
|
||||
<!-- 列表渲染 -->
|
||||
<uni-list-item :to="'/uni_modules/uni-cms-article/pages/detail/detail?id=' + item._id"
|
||||
v-for="(item, index) in searchList" :key="index">
|
||||
<!-- 通过header插槽定义列表左侧图片 -->
|
||||
<template v-slot:header>
|
||||
<image class="thumbnail" :src="item.thumbnail" mode="aspectFill"></image>
|
||||
</template>
|
||||
<!-- 通过body插槽定义布局 -->
|
||||
<template v-slot:body>
|
||||
<view class="main">
|
||||
<text class="title">{{ item.title }}</text>
|
||||
<view class="info">
|
||||
<text class="author">{{ item.user_id[0] ? item.user_id[0].nickname : '' }}</text>
|
||||
<text class="publish_date">{{ publishTime(item.publish_date) }}</text>
|
||||
<!-- -->
|
||||
<!-- <uni-dateformat class="publish_date" :date="item.publish_date"-->
|
||||
<!-- format="yyyy-MM-dd" :threshold="[60000, 2592000000]"/>-->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</uni-list-item>
|
||||
<!-- 加载状态:上拉加载更多,加载中,没有更多数据了,加载错误 -->
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<uni-list-item>
|
||||
<template v-slot:body>
|
||||
<!-- #endif -->
|
||||
<uni-load-state @networkResume="refresh" :state="{ data: searchList, pagination, hasMore, loading, error }"
|
||||
@loadMore="loadMore">
|
||||
</uni-load-state>
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
</template>
|
||||
</uni-list-item>
|
||||
<!-- #endif -->
|
||||
</uni-list>
|
||||
</unicloud-db>
|
||||
</view>
|
||||
<!-- 搜索联想 -->
|
||||
<view class="search-associative" v-if="associativeShow">
|
||||
<uni-list>
|
||||
<uni-list-item v-for="(item, index) in associativeList" :key="item._id" :ellipsis="1" :title="item.title"
|
||||
@click="associativeClick(item)" show-extra-icon clickable
|
||||
:extra-icon="{ size: 18, color: iconColor, type: 'search' }">
|
||||
</uni-list-item>
|
||||
</uni-list>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 云端一体搜索模板
|
||||
* @description uniCloud云端一体搜索模板,自带下拉候选、历史搜索、热搜。无需再开发服务器代码
|
||||
*/
|
||||
|
||||
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time";
|
||||
import parseScanResult from "@/uni_modules/uni-cms-article/common/parse-scan-result";
|
||||
import {parseImageUrl} from "@/uni_modules/uni-cms-article/common/parse-image-url";
|
||||
|
||||
const searchLogDbName = 'opendb-search-log'; // 搜索记录数据库
|
||||
const articleDbName = 'uni-cms-articles'; // 文章数据库
|
||||
const associativeSearchField = 'title'; // 联想时,搜索框值检索数据库字段名
|
||||
const associativeField = '_id,title'; // 联想列表每一项携带的字段
|
||||
const localSearchListKey = '__local_search_history'; // 本地历史存储字段名
|
||||
|
||||
const db = uniCloud.database();
|
||||
const articleDBName = 'uni-cms-articles'
|
||||
const userDBName = 'uni-id-users'
|
||||
|
||||
// 数组去重
|
||||
const arrUnique = arr => {
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
const curIndex = arr.indexOf(arr[i]);
|
||||
const lastIndex = arr.lastIndexOf(arr[i])
|
||||
curIndex != lastIndex && arr.splice(lastIndex, 1)
|
||||
}
|
||||
return arr
|
||||
} // 节流
|
||||
// 防抖
|
||||
function debounce(fn, interval, isFirstAutoRun) {
|
||||
/**
|
||||
*
|
||||
* @param {要执行的函数} fn
|
||||
* @param {在操作多长时间后可再执行,第一次立即执行} interval
|
||||
*/
|
||||
var _self = fn;
|
||||
var timer = null;
|
||||
var first = true;
|
||||
|
||||
if (isFirstAutoRun) {
|
||||
_self();
|
||||
}
|
||||
|
||||
return function () {
|
||||
var args = arguments;
|
||||
var _me = this;
|
||||
if (first) {
|
||||
first = false;
|
||||
_self.apply(_me, args);
|
||||
}
|
||||
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
// return false;
|
||||
}
|
||||
|
||||
timer = setTimeout(function () {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
_self.apply(_me, args);
|
||||
}, interval || 200);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
// 组件数据
|
||||
data() {
|
||||
return {
|
||||
// 文章数据库名称
|
||||
articleDbName,
|
||||
// 搜索记录数据库名称
|
||||
searchLogDbName,
|
||||
// 状态栏高度
|
||||
statusBarHeight: '0px',
|
||||
// 本地搜索列表
|
||||
localSearchList: uni.getStorageSync(localSearchListKey),
|
||||
// 是否删除本地搜索列表
|
||||
localSearchListDel: false,
|
||||
// 是否隐藏网络热搜列表
|
||||
netHotListIsHide: false,
|
||||
// 搜索文本
|
||||
searchText: '',
|
||||
// 图标颜色
|
||||
iconColor: '#999999',
|
||||
// 联想列表
|
||||
associativeList: [],
|
||||
// 是否弹出键盘
|
||||
keyBoardPopup: false,
|
||||
// 搜索热词
|
||||
hotWorld: 'DCloud', // 搜索热词,如果没有输入即回车,则搜索热词,但是不会加入搜索记录
|
||||
// 是否自动聚焦
|
||||
focus: true,
|
||||
// 语音识别引擎
|
||||
speechEngine: 'iFly', // 语音识别引擎 iFly 讯飞 baidu 百度
|
||||
// 是否正在加载数据
|
||||
isLoadData: false,
|
||||
// 数据库查询条件
|
||||
where: '"article_status" == 1',
|
||||
// 列表高度
|
||||
listHeight: 0,
|
||||
// 是否显示联想列表
|
||||
associativeShow: false,
|
||||
// 是否显示无联想列表
|
||||
noAssociativeShow: false,
|
||||
// 搜索结果列表
|
||||
searchList: []
|
||||
}
|
||||
},
|
||||
// 组件创建时执行
|
||||
created() {
|
||||
// 初始化数据库
|
||||
this.db = uniCloud.database();
|
||||
this.searchLogDb = this.db.collection(this.searchLogDbName);
|
||||
this.articleDbName = this.db.collection(this.articleDbName);
|
||||
// #ifndef H5
|
||||
// 监听键盘高度变化
|
||||
uni.onKeyboardHeightChange((res) => {
|
||||
this.keyBoardPopup = res.height !== 0;
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
// 计算属性
|
||||
computed: {
|
||||
colList() {
|
||||
// 返回文章和用户列表
|
||||
return [
|
||||
db.collection(articleDBName).where(this.where).field('thumbnail,title,publish_date,user_id').getTemp(),
|
||||
db.collection(userDBName).field('_id,nickname').getTemp()
|
||||
]
|
||||
}
|
||||
},
|
||||
// 页面初次渲染完成时执行
|
||||
onReady() {
|
||||
// #ifdef APP-NVUE
|
||||
/* 可用窗口高度 - 搜索框高 - 状态栏高 */
|
||||
this.listHeight = uni.getSystemInfoSync().windowHeight + 'px';
|
||||
// #endif
|
||||
// #ifndef APP-NVUE
|
||||
this.listHeight = 'auto'
|
||||
// #endif
|
||||
},
|
||||
// 页面加载时执行
|
||||
onLoad() {
|
||||
//#ifdef APP-PLUS
|
||||
// 获取状态栏高度
|
||||
this.statusBarHeight = `${uni.getSystemInfoSync().statusBarHeight}px`;
|
||||
//#endif
|
||||
},
|
||||
// 组件方法
|
||||
methods: {
|
||||
// 清空搜索框
|
||||
clear(res) {
|
||||
console.log("res: ", res);
|
||||
},
|
||||
// 确认搜索
|
||||
confirm(res) {
|
||||
// 键盘确认
|
||||
this.search(res.value);
|
||||
},
|
||||
// 取消搜索
|
||||
cancel(res) {
|
||||
uni.hideKeyboard();
|
||||
this.searchText = '';
|
||||
this.isLoadData = false
|
||||
this.associativeShow = false
|
||||
// this.loadList();
|
||||
},
|
||||
// 执行搜索
|
||||
search(value) {
|
||||
if (!value && !this.hotWorld) {
|
||||
return;
|
||||
}
|
||||
if (value) {
|
||||
if (this.searchText !== value) {
|
||||
this.searchText = value
|
||||
}
|
||||
|
||||
this.localSearchListManage(value);
|
||||
|
||||
this.searchLogDbAdd(value)
|
||||
} else if (this.hotWorld) {
|
||||
this.searchText = this.hotWorld
|
||||
}
|
||||
|
||||
uni.hideKeyboard();
|
||||
this.loadList(this.searchText);
|
||||
},
|
||||
// 管理本地搜索列表
|
||||
localSearchListManage(word) {
|
||||
let list = uni.getStorageSync(localSearchListKey);
|
||||
if (list.length) {
|
||||
this.localSearchList.unshift(word);
|
||||
arrUnique(this.localSearchList);
|
||||
if (this.localSearchList.length > 10) {
|
||||
this.localSearchList.pop();
|
||||
}
|
||||
} else {
|
||||
this.localSearchList = [word];
|
||||
}
|
||||
uni.setStorageSync(localSearchListKey, this.localSearchList);
|
||||
},
|
||||
// 清空本地搜索列表
|
||||
LocalSearchListClear() {
|
||||
uni.showModal({
|
||||
content: "确认清空搜索历史吗",
|
||||
confirmText: "删除",
|
||||
confirmColor: 'red',
|
||||
cancelColor: '#808080',
|
||||
success: res => {
|
||||
if (res.confirm) {
|
||||
this.localSearchListDel = false;
|
||||
this.localSearchList = [];
|
||||
uni.removeStorageSync(localSearchListKey)
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
// 点击本地搜索列表项
|
||||
LocalSearchlistItemClick(word, index) {
|
||||
if (this.localSearchListDel) {
|
||||
this.localSearchList.splice(index, 1);
|
||||
uni.setStorageSync(localSearchListKey, this.localSearchList);
|
||||
if (!this.localSearchList.length) {
|
||||
this.localSearchListDel = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.noAssociativeShow = true;
|
||||
this.search(word);
|
||||
},
|
||||
// 刷新搜索热词
|
||||
searchHotRefresh() {
|
||||
this.$refs.udb.refresh();
|
||||
},
|
||||
// 语音搜索
|
||||
speech() {
|
||||
// #ifdef APP-PLUS
|
||||
plus.speech.startRecognize({
|
||||
engine: this.speechEngine,
|
||||
punctuation: false, // 标点符号
|
||||
timeout: 10000
|
||||
}, word => {
|
||||
word = word instanceof Array ? word[0] : word;
|
||||
this.search(word)
|
||||
}, err => {
|
||||
console.error("语音识别错误: ", err);
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
// 添加搜索记录
|
||||
searchLogDbAdd(value) {
|
||||
/*
|
||||
在此处存搜索记录,如果登录则需要存 user_id,若未登录则存device_id
|
||||
*/
|
||||
this.getDeviceId().then(device_id => {
|
||||
this.searchLogDb.add({
|
||||
// user_id: device_id,
|
||||
device_id,
|
||||
content: value,
|
||||
create_date: Date.now()
|
||||
})
|
||||
})
|
||||
},
|
||||
// 获取设备ID
|
||||
getDeviceId() {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 从本地缓存中获取uni_id
|
||||
const uniId = uni.getStorageSync('uni_id');
|
||||
// 如果uni_id不存在,则获取设备信息
|
||||
if (!uniId) {
|
||||
// #ifdef APP-PLUS
|
||||
plus.device.getInfo({
|
||||
success: (deviceInfo) => {
|
||||
resolve(deviceInfo.uuid)
|
||||
},
|
||||
fail: () => {
|
||||
// 如果获取设备信息失败,则返回一个随机字符串
|
||||
resolve(uni.getSystemInfoSync().system + '_' + Math.random().toString(36).substr(2))
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
// 如果不是APP-PLUS,则返回一个随机字符串
|
||||
resolve(uni.getSystemInfoSync().system + '_' + Math.random().toString(36).substr(2))
|
||||
// #endif
|
||||
} else {
|
||||
// 如果uni_id存在,则直接返回uni_id
|
||||
resolve(uniId)
|
||||
}
|
||||
})
|
||||
},
|
||||
// 点击联想词
|
||||
associativeClick(item) {
|
||||
/**
|
||||
* 注意:这里用户根据自己的业务需要,选择跳转的页面即可
|
||||
*/
|
||||
console.log("associativeClick: ", item, item.title);
|
||||
// 隐藏联想词
|
||||
this.noAssociativeShow = true;
|
||||
// 将搜索框的文本设置为联想词的标题
|
||||
this.searchText = item.title;
|
||||
// 加载列表
|
||||
this.loadList(item.title);
|
||||
},
|
||||
// 加载列表
|
||||
loadList(text = '') {
|
||||
// 设置查询条件
|
||||
let where = '"article_status" == 1 '
|
||||
if (text) {
|
||||
this.where = where + `&& /${text}/.test(title)`;
|
||||
} else {
|
||||
this.where = where;
|
||||
}
|
||||
|
||||
// 隐藏联想词
|
||||
this.associativeList = [];
|
||||
this.associativeShow = false;
|
||||
|
||||
this.searchList = []
|
||||
this.isLoadData = true
|
||||
|
||||
// 延迟0ms后加载数据
|
||||
setTimeout(() => {
|
||||
this.$refs.listUdb.loadData({
|
||||
clear: true
|
||||
})
|
||||
}, 0)
|
||||
},
|
||||
// 数据库加载完成
|
||||
async onDbLoad(data) {
|
||||
console.log('onDbLoad')
|
||||
// 设置数据已加载标志
|
||||
this.isLoadData = true
|
||||
// 显示联想词
|
||||
this.noAssociativeShow = false;
|
||||
|
||||
for (const article of data) {
|
||||
const parseImages = await parseImageUrl(article.thumbnail)
|
||||
|
||||
article.thumbnail = parseImages ? parseImages.map(image => image.src): []
|
||||
}
|
||||
|
||||
this.searchList = data
|
||||
},
|
||||
// 查询错误
|
||||
onqueryerror(e) {
|
||||
console.error(e);
|
||||
},
|
||||
// 刷新
|
||||
refresh() {
|
||||
// 刷新数据
|
||||
this.$refs.listUdb.loadData({
|
||||
clear: true
|
||||
}, () => {
|
||||
// 停止下拉刷新
|
||||
uni.stopPullDownRefresh()
|
||||
// #ifdef APP-NVUE
|
||||
// 隐藏刷新按钮
|
||||
this.showRefresh = false
|
||||
// #endif
|
||||
})
|
||||
},
|
||||
// 加载更多
|
||||
loadMore() {
|
||||
// 加载更多数据
|
||||
this.$refs.listUdb.loadMore()
|
||||
},
|
||||
// 格式化发布时间
|
||||
publishTime(timestamp) {
|
||||
return translatePublishTime(timestamp)
|
||||
},
|
||||
scanEvent () {
|
||||
uni.scanCode({
|
||||
onlyFromCamera: true,
|
||||
scanType: ["qrCode"],
|
||||
success: (e) => parseScanResult(e.result),
|
||||
fail: (e) => {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
onReachBottom() {
|
||||
// 当滚动到底部时,加载更多数据
|
||||
this.loadMore()
|
||||
},
|
||||
watch: {
|
||||
searchText: debounce(function (value, oldValue) {
|
||||
// 当搜索框的文本发生变化时,执行以下操作
|
||||
if (value === oldValue) return
|
||||
if (this.noAssociativeShow) return
|
||||
|
||||
if (value) {
|
||||
// 根据搜索框的文本,查询联想词
|
||||
this.articleDbName.where({
|
||||
[associativeSearchField]: new RegExp(value, 'gi'),
|
||||
}).field(associativeField).get().then(res => {
|
||||
// 将查询结果赋值给联想词列表,并显示联想词
|
||||
this.associativeList = res.result.data;
|
||||
this.associativeShow = true
|
||||
})
|
||||
} else {
|
||||
// 如果搜索框的文本为空,则清空联想词列表
|
||||
this.associativeList = [];
|
||||
}
|
||||
|
||||
}, 100)
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* #ifndef APP-NVUE */
|
||||
page {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$search-bar-height: 52px;
|
||||
$word-container_header-height: 72rpx;
|
||||
|
||||
.status-bar {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.container {
|
||||
/* #ifndef APP-NVUE */
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.search-body {
|
||||
background-color: #fff;
|
||||
border-bottom-right-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
|
||||
@mixin uni-flex {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
@mixin words-display {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
@include uni-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
@include uni-flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
/* #ifdef APP-PLUS */
|
||||
/* #ifndef APP-NVUE || VUE3*/
|
||||
::v-deep
|
||||
|
||||
/* #endif */
|
||||
.uni-searchbar {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
|
||||
/* #ifndef APP-NVUE || VUE3*/
|
||||
::v-deep
|
||||
|
||||
/* #endif */
|
||||
.uni-searchbar__box {
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
/* #ifndef APP-NVUE || VUE3 */
|
||||
::v-deep
|
||||
|
||||
/* #endif */
|
||||
.uni-input-placeholder {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
height: $search-bar-height;
|
||||
@include uni-flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
|
||||
@at-root {
|
||||
#{&}-bar {
|
||||
@include uni-flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-associative {
|
||||
/* #ifndef APP-NVUE */
|
||||
overflow-y: auto;
|
||||
/* #endif */
|
||||
position: absolute;
|
||||
top: $search-bar-height;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #fff;
|
||||
margin-top: 10rpx;
|
||||
padding-left: 10rpx;
|
||||
padding-right: 10rpx;
|
||||
}
|
||||
|
||||
.search-icons, .scan-icons {
|
||||
padding: 16rpx;
|
||||
}
|
||||
.scan-icons {
|
||||
padding-left: 0;
|
||||
}
|
||||
.word-display {
|
||||
@include words-display;
|
||||
}
|
||||
|
||||
.word-container {
|
||||
padding: 20rpx;
|
||||
|
||||
@at-root {
|
||||
#{&}_header {
|
||||
@include uni-flex;
|
||||
height: $word-container_header-height;
|
||||
line-height: $word-container_header-height;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@at-root {
|
||||
#{&}-text {
|
||||
color: #3e3e3e;
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#{&}_body {
|
||||
@include uni-flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
|
||||
@at-root {
|
||||
#{&}-text {
|
||||
@include uni-flex;
|
||||
@include words-display;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f6f6f6;
|
||||
padding: 10rpx 20rpx;
|
||||
margin: 20rpx 30rpx 0 0;
|
||||
border-radius: 30rpx;
|
||||
/* #ifndef APP-NVUE */
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#{&}-info {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: block;
|
||||
/* #endif */
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #808080;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 240rpx;
|
||||
height: 160rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.main {
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.author,
|
||||
.publish_date {
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
||||
</style>
|
363
uni_modules/uni-cms-article/pages/search/search.uvue
Normal file
363
uni_modules/uni-cms-article/pages/search/search.uvue
Normal file
@ -0,0 +1,363 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="search">
|
||||
<uni-cms-article-search-bar
|
||||
ref="searchBar"
|
||||
v-model="searchVal"
|
||||
:show-placeholder="false"
|
||||
:focus="true"
|
||||
@clear="showSearchResultPanel = false"
|
||||
@confirm="search"
|
||||
></uni-cms-article-search-bar>
|
||||
</view>
|
||||
<view class="search-result" v-if="showSearchResultPanel">
|
||||
<uni-cms-article-list
|
||||
ref="articleList"
|
||||
:collectionList="colList"
|
||||
:refresherEnabled="false"
|
||||
loadTime="manual"
|
||||
style="flex: 1;"
|
||||
></uni-cms-article-list>
|
||||
</view>
|
||||
<template v-else>
|
||||
<view class="panel history-panel" v-if="searchHistory.length > 0">
|
||||
<view class="panel__title">
|
||||
<view class="panel__title-text">
|
||||
<text class="text">搜索历史</text>
|
||||
</view>
|
||||
|
||||
<view class="delete-history-btns" v-if="deleteHistoryLoading">
|
||||
<text class="text" @click="deleteAllSearchHistory">全部删除</text>
|
||||
<text class="text danger" @click="deleteHistoryLoading = false">完成</text>
|
||||
</view>
|
||||
<uni-cms-article-icons
|
||||
class="panel__after-icon"
|
||||
type="trash"
|
||||
:size="18"
|
||||
color="#999"
|
||||
@click="deleteHistoryLoading = true"
|
||||
v-else
|
||||
></uni-cms-article-icons>
|
||||
</view>
|
||||
<view class="panel__list">
|
||||
<view class="panel__list-item" v-for="text in searchHistory">
|
||||
<text class="text" @click="search(text)">{{text}}</text>
|
||||
<uni-cms-article-icons
|
||||
class="icon"
|
||||
type="closeempty"
|
||||
:size="12"
|
||||
color="#999"
|
||||
v-if="deleteHistoryLoading"
|
||||
@click="deleteSearchHistory(text)"
|
||||
></uni-cms-article-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<unicloud-db ref="udb" #default="{ data, loading, error }" field="content"
|
||||
collection="opendb-search-hot" orderby="create_date desc,count desc" page-data="replace"
|
||||
:page-size="10">
|
||||
<view class="panel recommend-panel">
|
||||
<view class="panel__title">
|
||||
<view class="panel__title-text">
|
||||
<text class="text">搜索发现</text>
|
||||
<uni-cms-article-icons
|
||||
class="icon"
|
||||
type="reload"
|
||||
:size="14"
|
||||
color="#999"
|
||||
v-if="!hideSearchRecommend"
|
||||
@click="reLoadSearchRecommend"
|
||||
></uni-cms-article-icons>
|
||||
</view>
|
||||
<uni-cms-article-icons
|
||||
class="panel__after-icon"
|
||||
:type="hideSearchRecommend ? 'eye-slash': 'eye'"
|
||||
:size="18"
|
||||
color="#999"
|
||||
@click="hideSearchRecommend = !hideSearchRecommend"
|
||||
></uni-cms-article-icons>
|
||||
</view>
|
||||
<view class="panel__list">
|
||||
<view class="panel__list-tip" v-if="loading">
|
||||
<text class="text">正在加载...</text>
|
||||
</view>
|
||||
<view class="panel__list-tip" v-else-if="error != null">
|
||||
<text class="text">{{error.message}}</text>
|
||||
</view>
|
||||
<view class="panel__list-tip" v-else-if="hideSearchRecommend">
|
||||
<text class="text">当前搜索发现已隐藏</text>
|
||||
</view>
|
||||
<template v-else>
|
||||
<view
|
||||
class="panel__list-item"
|
||||
v-for="(word, index) in data"
|
||||
:key="index"
|
||||
@click="search(word.getString('content')!)"
|
||||
>
|
||||
<text class="text">{{ word.getString('content') }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</unicloud-db>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
type ArticleAuthor = {
|
||||
_id: string
|
||||
nickname: string
|
||||
}
|
||||
type ArticleItem = {
|
||||
_id: string
|
||||
title: string
|
||||
publish_date: number
|
||||
thumbnail: string[]
|
||||
user_id: ArticleAuthor[]
|
||||
}
|
||||
|
||||
const db = uniCloud.databaseForJQL()
|
||||
const searchLogDB = db.collection('opendb-search-log')
|
||||
const cmsArticleDB = db.collection('uni-cms-articles')
|
||||
const uniIdUsersDB = db.collection('uni-id-users')
|
||||
const localSearchHistoryKey = '__local_search_history'; // 本地历史存储字段名
|
||||
const localSearchRecommendHiddenKey = '__local_search_recommend_hidden'; // 本地搜索发现开关字段名
|
||||
const localSearchHistoryMax = 10; // 本地历史存储最大值
|
||||
export default {
|
||||
data() {
|
||||
const localSearchRecommendHidden = uni.getStorageSync(localSearchRecommendHiddenKey)
|
||||
|
||||
return {
|
||||
searchVal: "",
|
||||
searchHistory: [] as string[],
|
||||
searchRecommend: [] as string[],
|
||||
deleteHistoryLoading: false,
|
||||
hideSearchRecommend: (localSearchRecommendHidden == "" ? false : localSearchRecommendHidden) as boolean,
|
||||
showSearchResultPanel: false,
|
||||
searchResult: [] as ArticleItem[]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
hideSearchRecommend(newValue) {
|
||||
uni.setStorageSync(localSearchRecommendHiddenKey, newValue)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasSearchValue(): boolean {
|
||||
return this.searchVal != ""
|
||||
},
|
||||
where(): string {
|
||||
let where = "\"article_status\" == 1"
|
||||
|
||||
if (this.searchVal != "") {
|
||||
where += `&& /${this.searchVal}/.test(title)`
|
||||
}
|
||||
|
||||
return where
|
||||
},
|
||||
colList(): any[] {
|
||||
// 返回文章和用户列表
|
||||
return [
|
||||
cmsArticleDB.where(this.where).field('thumbnail,title,publish_date,user_id').getTemp(),
|
||||
uniIdUsersDB.field('_id,nickname').getTemp()
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 本地历史存储
|
||||
const localSearchHistory = uni.getStorageSync(localSearchHistoryKey)
|
||||
this.searchHistory = (localSearchHistory == "" ? [] as string[] : localSearchHistory) as string[]
|
||||
},
|
||||
methods: {
|
||||
deleteAllSearchHistory() {
|
||||
uni.showModal({
|
||||
title: "确定清空搜索历史吗",
|
||||
confirmText: "删除",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.deleteSearchHistory(null)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteSearchHistory(searchText: string | null) {
|
||||
let history: string[] = []
|
||||
if (searchText != null) {
|
||||
history = this.searchHistory.filter((item: string): boolean => item != searchText)
|
||||
}
|
||||
|
||||
this.searchHistory = history
|
||||
uni.setStorageSync(localSearchHistoryKey, history)
|
||||
|
||||
console.log(history.length, 'history.length')
|
||||
if (history.length <= 0) {
|
||||
this.deleteHistoryLoading = false
|
||||
}
|
||||
},
|
||||
search(searchText: string) {
|
||||
searchText = searchText.trim()
|
||||
|
||||
if (searchText == "" || this.deleteHistoryLoading) return
|
||||
// 隐藏键盘
|
||||
;(this.$refs['searchBar'] as UniCmsArticleSearchBarComponentPublicInstance)!.hideKeyboard()
|
||||
|
||||
// 保存搜索历史
|
||||
this.setLocalSearchHistory(searchText)
|
||||
// 显示搜索结果Panel
|
||||
this.showSearchResultPanel = true
|
||||
// 搜索
|
||||
this.loadSearchResult(searchText)
|
||||
// 添加搜索记录
|
||||
this.addSearchRecord(searchText)
|
||||
},
|
||||
loadSearchResult(searchText: string) {
|
||||
// 设置查询条件
|
||||
this.searchVal = searchText
|
||||
|
||||
// 延迟0ms后加载数据
|
||||
setTimeout(() => {
|
||||
(this.$refs['articleList'] as UniCmsArticleListComponentPublicInstance)!.reLoadList()
|
||||
}, 0)
|
||||
},
|
||||
addSearchRecord(searchText: string) {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
/*
|
||||
在此处存搜索记录,如果登录则需要存 user_id,若未登录则存device_id
|
||||
*/
|
||||
searchLogDB.add({
|
||||
// user_id: device_id,
|
||||
device_id: systemInfo.deviceId,
|
||||
// device_uuid: systemInfo.deviceId,
|
||||
content: searchText,
|
||||
create_date: Date.now()
|
||||
})
|
||||
},
|
||||
setLocalSearchHistory(searchText: string) {
|
||||
const history = this.searchHistory.filter((item: string): boolean => item != searchText)
|
||||
|
||||
history.unshift(searchText)
|
||||
|
||||
if (history.length > localSearchHistoryMax) {
|
||||
history.pop()
|
||||
}
|
||||
|
||||
this.searchHistory = history
|
||||
this.deleteHistoryLoading = false
|
||||
uni.setStorageSync(localSearchHistoryKey, history)
|
||||
},
|
||||
reLoadSearchRecommend() {
|
||||
(this.$refs['udb'] as UniCloudDBElement)!.loadData({
|
||||
clear: true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.search-result {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin-top: 40rpx;
|
||||
padding: 10rpx 20rpx;
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&-text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.text {
|
||||
color: #3e3e3e;
|
||||
font-size: 30rpx;
|
||||
line-height: 36rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__after-icon {
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
&__list {
|
||||
margin-top: 30rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&-item {
|
||||
background-color: #f6f6f6;
|
||||
border-radius: 30rpx;
|
||||
padding: 10rpx 20rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10rpx;
|
||||
margin-bottom: 10rpx;
|
||||
|
||||
.text {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&-tip {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
margin-top: 20rpx;
|
||||
|
||||
.text {
|
||||
font-size: 26rpx;
|
||||
color: #808080;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete-history-btns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.text {
|
||||
color: #666;
|
||||
font-size: 22rpx;
|
||||
margin-left: 20rpx;
|
||||
|
||||
&.danger {
|
||||
color: #c0402b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
48
uni_modules/uni-cms-article/pages/webview/webview.uvue
Normal file
48
uni_modules/uni-cms-article/pages/webview/webview.uvue
Normal file
@ -0,0 +1,48 @@
|
||||
<!-- 网络链接内容展示页(用于uni-cms-article展示外链内容) -->
|
||||
<template>
|
||||
<view class="web-view">
|
||||
<web-view
|
||||
class="web-view"
|
||||
v-if="url"
|
||||
:src="url"
|
||||
></web-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
export default {
|
||||
onLoad(e) {
|
||||
const url = e.get('url') as string
|
||||
let title: string | null = e.get('title')
|
||||
|
||||
if (url.substring(0, 4) != 'http') {
|
||||
uni.showModal({
|
||||
title: "错误",
|
||||
content: '不是一个有效的网站链接,' + '"' + decodeURIComponent(url) + '"',
|
||||
showCancel: false,
|
||||
confirmText: "知道了",
|
||||
complete: () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
})
|
||||
title = "页面路径错误"
|
||||
} else {
|
||||
this.url = decodeURIComponent(url)
|
||||
}
|
||||
if (title != null) {
|
||||
uni.setNavigationBarTitle({title})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
url: null as string | null,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.web-view {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
35
uni_modules/uni-cms-article/pages/webview/webview.vue
Normal file
35
uni_modules/uni-cms-article/pages/webview/webview.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<!-- 网络链接内容展示页(用于uni-cms-article展示外链内容) -->
|
||||
<template>
|
||||
<view>
|
||||
<web-view v-if="url" :src="url"></web-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
onLoad({url,title}) {
|
||||
if(url.substring(0, 4) !== 'http'){
|
||||
uni.showModal({
|
||||
title:"错误",
|
||||
content: '不是一个有效的网站链接,'+'"'+url+'"',
|
||||
showCancel: false,
|
||||
confirmText:"知道了",
|
||||
complete: () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
});
|
||||
title = "页面路径错误"
|
||||
}else{
|
||||
this.url = url;
|
||||
}
|
||||
if(title){
|
||||
uni.setNavigationBarTitle({title});
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
url:null
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
24
uni_modules/uni-cms-article/readme.md
Normal file
24
uni_modules/uni-cms-article/readme.md
Normal file
@ -0,0 +1,24 @@
|
||||
# uni-cms-article
|
||||
|
||||
uni-CMS是基于uniCloud开发的uni-admin插件,可用于快速搭建CMS内容管理系统。
|
||||
客户端可使用 uni-cms-article 插件进行内容展示,无需开发,即可在多端展示内容。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
<div style="display: flex; flex-basis: 10px">
|
||||
<div style="margin-right: 10px;">
|
||||
<img src="https://web-assets.dcloud.net.cn/unidoc/zh/202304120144625.png" width="375"/>
|
||||
</div>
|
||||
<div style="margin-right: 10px;">
|
||||
<img src="https://web-assets.dcloud.net.cn/unidoc/zh/202304120139988.png" width="375" />
|
||||
</div>
|
||||
<div style="margin-right: 10px;">
|
||||
<img src="https://web-assets.dcloud.net.cn/unidoc/zh/202304120139209.png" width="375" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 详细文档请在 [uni-cms 文档](https://uniapp.dcloud.net.cn/uniCloud/uni-cms.html) 中查看
|
BIN
uni_modules/uni-cms-article/static/disconnection.png
Normal file
BIN
uni_modules/uni-cms-article/static/disconnection.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.6 KiB |
BIN
uni_modules/uni-cms-article/static/uniicons.ttf
Normal file
BIN
uni_modules/uni-cms-article/static/uniicons.ttf
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -0,0 +1,257 @@
|
||||
const {
|
||||
QuillDeltaToHtmlConverter: QuillDeltaToHtmlConverterBase,
|
||||
BlockGroup,
|
||||
ListGroup,
|
||||
BlotBlock,
|
||||
} = require('./core')
|
||||
|
||||
const jsonRules = {
|
||||
list(list) {
|
||||
const firstItem = list.items[0];
|
||||
|
||||
return {
|
||||
type: "list",
|
||||
data: {
|
||||
type: firstItem.item.op.attributes.list,
|
||||
items: list.items.map((item) => jsonRules.listItem(item)),
|
||||
},
|
||||
attributes: firstItem.item.op.attributes,
|
||||
class: attributes2class(firstItem.item.op.attributes, 'block'),
|
||||
style: attributes2style(firstItem.item.op.attributes, 'block'),
|
||||
};
|
||||
},
|
||||
listItem(listItem) {
|
||||
// listItem.item.op.attributes.indent = 0;
|
||||
const inlines = jsonRules.inlines(listItem.item.op, listItem.item.ops, false).map((v) => v.ops)
|
||||
|
||||
return {
|
||||
data: inlines[0] || [],
|
||||
children: listItem.innerList ? jsonRules.list(listItem.innerList): [],
|
||||
}
|
||||
},
|
||||
block (op, ops) {
|
||||
const type = ops[0].insert.type
|
||||
|
||||
if (type === 'text') {
|
||||
return jsonRules.inlines(op, ops)
|
||||
}
|
||||
|
||||
return {
|
||||
type: ops[0].insert.type,
|
||||
data: ops.map(bop => jsonRules.inline(bop)),
|
||||
attributes: op.attributes,
|
||||
class: attributes2class(op.attributes, 'block'),
|
||||
style: attributes2style(op.attributes, 'block'),
|
||||
}
|
||||
},
|
||||
inlines(op = {}, ops, isInlineGroup = true) {
|
||||
const opsLen = ops.length - 1;
|
||||
const br = {
|
||||
type: 'br'
|
||||
}
|
||||
const texts = ops.reduce((acc, op, i) => {
|
||||
if (i > 0 && i === opsLen && op.isJustNewline()) {
|
||||
acc[acc.length - 1].op = op
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (!acc[acc.length - 1]) {
|
||||
acc.push({ops: [], op: {}});
|
||||
}
|
||||
|
||||
if (op.isJustNewline()) {
|
||||
const nextOp = ops[i + 1];
|
||||
acc[acc.length - 1].op = op
|
||||
if (nextOp && nextOp.isJustNewline()) {
|
||||
acc.push({ops: [br], op: {}});
|
||||
} else {
|
||||
acc.push({ops: [], op: {}});
|
||||
}
|
||||
return acc;
|
||||
} else {
|
||||
acc[acc.length - 1].ops.push(jsonRules.inline(op));
|
||||
return acc;
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!isInlineGroup) {
|
||||
return texts;
|
||||
}
|
||||
|
||||
return texts.map((v) => {
|
||||
return {
|
||||
type: "paragraph",
|
||||
data: v.ops,
|
||||
class: attributes2class(op.attributes || v.op.attributes, 'block'),
|
||||
style: attributes2style(op.attributes || v.op.attributes, 'block'),
|
||||
};
|
||||
});
|
||||
},
|
||||
inline (op) {
|
||||
const data = {
|
||||
value: op.insert.value,
|
||||
attributes: op.attributes,
|
||||
class: attributes2class(op.attributes, 'inline'),
|
||||
style: attributes2style(op.attributes, 'inline'),
|
||||
}
|
||||
|
||||
if (op.isCustomEmbed()) {
|
||||
return {
|
||||
type: op.insert.type,
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
if (op.isLink()) {
|
||||
return {
|
||||
type: 'link',
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: op.isImage() ? "image": "text",
|
||||
data
|
||||
}
|
||||
},
|
||||
blotBlock (op) {
|
||||
return {
|
||||
type: op.insert.type,
|
||||
data: {
|
||||
value: op.insert.value,
|
||||
attributes: op.attributes,
|
||||
class: attributes2class(op.attributes, 'block'),
|
||||
style: attributes2style(op.attributes, 'block'),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function attributes2style(attributes, type) {
|
||||
if (!attributes) return ''
|
||||
// 定义允许的属性
|
||||
const allowAttr = {
|
||||
inline: ['align', 'color', 'background', 'font', 'fontSize', 'fontStyle', 'fontVariant', 'fontWeight', 'fontFamily', 'lineHeight', 'letterSpacing', 'textDecoration', 'textIndent', 'wordWrap', 'wordBreak', 'whiteSpace'],
|
||||
block: ['align', 'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight', 'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight', 'lineHeight', 'textIndent']
|
||||
}[type]
|
||||
|
||||
// 定义属性适配器
|
||||
const adp = {
|
||||
align: 'text-align',
|
||||
}
|
||||
|
||||
// 如果不支持该类型的属性,则抛出错误
|
||||
if (!allowAttr) throw new Error('type not supported')
|
||||
|
||||
// 将属性转换为style
|
||||
return allowAttr.reduce((res, item) => {
|
||||
// 如果属性为空,则返回空字符串
|
||||
if (!attributes) return res
|
||||
// 如果属性不为undefined,则将属性名和属性值添加到style列表中
|
||||
if (attributes[item] !== undefined) {
|
||||
// 如果属性适配器中存在该属性,则使用适配器中的属性名
|
||||
// 否则,将属性名转换为短横线连接的形式
|
||||
res.push(`${adp[item] || item.replace(/([A-Z])/g, (s, m) => `-${m.toLowerCase()}`)}: ${attributes[item]}`)
|
||||
}
|
||||
return res
|
||||
|
||||
}, []).join(';')
|
||||
}
|
||||
function attributes2class(attributes, type) {
|
||||
if (!attributes) return ''
|
||||
// 定义允许的属性
|
||||
const allowAttr = {
|
||||
inline: ['bold', 'italic', 'underline', 'strike', 'ins', 'link'],
|
||||
block: ['header', 'list']
|
||||
}[type]
|
||||
|
||||
// 如果不支持该类型的属性,则抛出错误
|
||||
if (!allowAttr) throw new Error('type not supported')
|
||||
|
||||
// 将属性转换为class列表
|
||||
const classList = allowAttr.reduce((res, item) => {
|
||||
// 如果属性为空,则返回空字符串
|
||||
if (!attributes || item === "link") return res
|
||||
// 如果属性为true,则将属性名添加到class列表中
|
||||
if (attributes[item] === true) {
|
||||
res.push(item)
|
||||
// 如果属性不为undefined,则将属性名和属性值添加到class列表中
|
||||
} else if (attributes[item] !== undefined) {
|
||||
res.push(`${item}-${attributes[item]}`)
|
||||
}
|
||||
return res
|
||||
|
||||
}, [])
|
||||
|
||||
// 如果属性中包含link,则添加link类
|
||||
if ('link' in attributes) {
|
||||
classList.push('link')
|
||||
}
|
||||
|
||||
return classList.join(' ')
|
||||
}
|
||||
class QuillDeltaToJSONConverter {
|
||||
constructor(deltaOps) {
|
||||
|
||||
this.deltaPreHandler(deltaOps)
|
||||
|
||||
this.deltaOps = deltaOps
|
||||
this.converter = new QuillDeltaToHtmlConverterBase(this.deltaOps, {
|
||||
multiLineParagraph: false,
|
||||
});
|
||||
}
|
||||
|
||||
deltaPreHandler (deltaOps) {
|
||||
for (const op of deltaOps) {
|
||||
if (typeof op.insert === 'string') continue
|
||||
|
||||
if (!op.attributes) {
|
||||
op.attributes = {}
|
||||
}
|
||||
|
||||
const insertType = Object.keys(op.insert)
|
||||
const blockRenderList = ['divider', 'unlockContent', 'mediaVideo']
|
||||
|
||||
if (insertType && insertType.length > 0 && blockRenderList.includes(insertType[0])) {
|
||||
op.attributes.renderAsBlock = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
convert () {
|
||||
const opsGroups = this.converter.getGroupedOps();
|
||||
|
||||
return [].concat.apply(
|
||||
[],
|
||||
opsGroups.map((group) => {
|
||||
// console.log(JSON.stringify(group), '--------------------group------------------------------')
|
||||
switch (group.constructor) {
|
||||
case ListGroup:
|
||||
return jsonRules.list(group);
|
||||
case BlockGroup:
|
||||
return jsonRules.block(group.op, group.ops)
|
||||
case BlotBlock:
|
||||
return jsonRules.blotBlock(group.op, group.ops)
|
||||
default:
|
||||
return jsonRules.inlines(group.op, group.ops);
|
||||
}
|
||||
})
|
||||
).filter(op => op.data instanceof Array ? op.data.length : op.data);
|
||||
}
|
||||
}
|
||||
|
||||
class QuillDeltaToHtmlConverter extends QuillDeltaToHtmlConverterBase {
|
||||
constructor(deltaOps) {
|
||||
super(deltaOps, {
|
||||
multiLineParagraph: false,
|
||||
inlineStyles: true,
|
||||
});
|
||||
|
||||
// this.renderCustomWith(function (customOp, contextOp) {
|
||||
// console.log(customOp, contextOp)
|
||||
// })
|
||||
}
|
||||
}
|
||||
|
||||
exports.QuillDeltaToHtmlConverter = QuillDeltaToHtmlConverter
|
||||
exports.QuillDeltaToJSONConverter = QuillDeltaToJSONConverter
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "quill-delta-converter",
|
||||
"description": "quill编辑器delta格式转换器",
|
||||
"main": "index.js"
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
const crypto = require('crypto')
|
||||
const createConfig = require('uni-config-center')
|
||||
const config = createConfig({
|
||||
pluginId: 'uni-cms'
|
||||
}).config()
|
||||
|
||||
const unlockRecordDBName = 'uni-cms-unlock-record'
|
||||
|
||||
// 定义云函数
|
||||
exports.main = async function (event) {
|
||||
// 解构 event 对象
|
||||
const {trans_id, extra: _extra, sign} = event
|
||||
let extra = {}
|
||||
try {
|
||||
extra = JSON.parse(_extra)
|
||||
} catch (e) {}
|
||||
|
||||
// 如果 adConfig 或 securityKey 配置项不存在,则抛出错误引导用户配置参数
|
||||
if (!config.adConfig || !config.adConfig.securityKey) throw new Error('请先配置adConfig.securityKey')
|
||||
// 如果 extra.article_id 不存在,则返回 null
|
||||
if (!extra.article_id) return null
|
||||
|
||||
// 签名验证
|
||||
const reSign = crypto.createHash('sha256').update(`${config.adConfig.securityKey}:${trans_id}`).digest('hex')
|
||||
if (sign !== reSign) {
|
||||
console.log('签名错误', `${config.adConfig.securityKey}:${trans_id}`)
|
||||
return null
|
||||
}
|
||||
|
||||
// 获取数据库实例
|
||||
const db = uniCloud.database()
|
||||
// 查询解锁记录
|
||||
const unlockRecord = await db.collection(unlockRecordDBName).where({
|
||||
trans_id
|
||||
}).get()
|
||||
|
||||
// 如果已经解锁过了,则返回 null
|
||||
if (unlockRecord.data.length) {
|
||||
console.log('已经解锁过了')
|
||||
return null // 已经解锁过了
|
||||
}
|
||||
|
||||
// 添加解锁记录
|
||||
await db.collection(unlockRecordDBName).add({
|
||||
unique_id: extra.unique_id,
|
||||
unique_type: extra.unique_type,
|
||||
article_id: extra.article_id,
|
||||
trans_id,
|
||||
create_date: Date.now()
|
||||
})
|
||||
|
||||
console.log('解锁成功')
|
||||
|
||||
// 应广告规范,需返回 isValid 为 true
|
||||
return {
|
||||
isValid: true
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "uni-cms-unlock-callback",
|
||||
"dependencies": {
|
||||
"uni-config-center": "file:../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
|
||||
},
|
||||
"extensions": {}
|
||||
}
|
23
uni_modules/uni-cms-article/uniCloud/database/db_init.json
Normal file
23
uni_modules/uni-cms-article/uniCloud/database/db_init.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"uni-cms-unlock-record": {
|
||||
"data": [],
|
||||
"index": [
|
||||
{
|
||||
"IndexName": "unique_article_trans_",
|
||||
"MgoKeySchema": {
|
||||
"MgoIndexKeys": [{
|
||||
"Name": "unique_id",
|
||||
"Direction": "1"
|
||||
},{
|
||||
"Name": "article_id",
|
||||
"Direction": "1"
|
||||
},{
|
||||
"Name": "trans_id",
|
||||
"Direction": "1"
|
||||
}],
|
||||
"MgoIsUnique": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
{
|
||||
"bsonType": "object",
|
||||
"required": [
|
||||
"content",
|
||||
"count"
|
||||
],
|
||||
"permission": {
|
||||
"read": true,
|
||||
"create": false,
|
||||
"update": false,
|
||||
"delete": false
|
||||
},
|
||||
"properties": {
|
||||
"_id": {
|
||||
"description": "ID,系统自动生成"
|
||||
},
|
||||
"content": {
|
||||
"bsonType": "string",
|
||||
"description": "搜索内容"
|
||||
},
|
||||
"count": {
|
||||
"bsonType": "int",
|
||||
"description": "搜索次数"
|
||||
},
|
||||
"create_date": {
|
||||
"bsonType": "timestamp",
|
||||
"description": "统计时间"
|
||||
}
|
||||
},
|
||||
"version": "0.0.1"
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
{
|
||||
"bsonType": "object",
|
||||
"required": [
|
||||
"content"
|
||||
],
|
||||
"permission": {
|
||||
"read": false,
|
||||
"create": true,
|
||||
"update": false,
|
||||
"delete": false
|
||||
},
|
||||
"properties": {
|
||||
"_id": {
|
||||
"description": "ID,系统自动生成"
|
||||
},
|
||||
"user_id": {
|
||||
"bsonType": "string",
|
||||
"description": "搜索人id,参考uni-id-users表"
|
||||
},
|
||||
"device_id": {
|
||||
"bsonType": "string",
|
||||
"description": "设备id"
|
||||
},
|
||||
"platform": {
|
||||
"bsonType": "string",
|
||||
"description": "设备平台,如:mp-weixin、app-plus等"
|
||||
},
|
||||
"content": {
|
||||
"bsonType": "string",
|
||||
"description": "搜索内容"
|
||||
},
|
||||
"ip": {
|
||||
"bsonType": "string",
|
||||
"description": "客户端IP地址",
|
||||
"forceDefaultValue": {
|
||||
"$env": "clientIP"
|
||||
}
|
||||
},
|
||||
"create_date": {
|
||||
"bsonType": "timestamp",
|
||||
"description": "统计时间"
|
||||
}
|
||||
},
|
||||
"version": "0.0.1"
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
{
|
||||
"bsonType": "object",
|
||||
"required": [
|
||||
"content"
|
||||
],
|
||||
"permission": {
|
||||
"read": false,
|
||||
"create": true,
|
||||
"update": false,
|
||||
"delete": false
|
||||
},
|
||||
"properties": {
|
||||
"_id": {
|
||||
"description": "ID,系统自动生成"
|
||||
},
|
||||
"user_id": {
|
||||
"bsonType": "string",
|
||||
"description": "搜索人id,参考uni-id-users表"
|
||||
},
|
||||
"device_id": {
|
||||
"bsonType": "string",
|
||||
"description": "设备id"
|
||||
},
|
||||
"platform": {
|
||||
"bsonType": "string",
|
||||
"description": "设备平台,如:mp-weixin、app-plus等"
|
||||
},
|
||||
"content": {
|
||||
"bsonType": "string",
|
||||
"description": "搜索内容"
|
||||
},
|
||||
"ip": {
|
||||
"bsonType": "string",
|
||||
"description": "客户端IP地址",
|
||||
"forceDefaultValue": {
|
||||
"$env": "clientIP"
|
||||
}
|
||||
},
|
||||
"create_date": {
|
||||
"bsonType": "timestamp",
|
||||
"description": "统计时间"
|
||||
}
|
||||
},
|
||||
"version": "0.0.1"
|
||||
}
|
@ -0,0 +1,350 @@
|
||||
// 获取配置
|
||||
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'
|
||||
|
||||
// 安全检测文本内容
|
||||
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, '摘要存在敏感字,请修改后提交'))
|
||||
}
|
||||
// 检测内容
|
||||
if (allowCheckContent && addData.content) {
|
||||
parallel.push(checkContentSec(JSON.stringify(addData.content), clientInfo.requestId, '内容存在敏感字,请修改后提交'))
|
||||
}
|
||||
// 检测封面图
|
||||
if (allowCheckImage && addData.thumbnail) {
|
||||
parallel.push(checkImageSec(addData.thumbnail, 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, '摘要存在敏感字,请修改后提交'))
|
||||
}
|
||||
// 检测内容
|
||||
if (allowCheckContent && updateData.content) {
|
||||
parallel.push(checkContentSec(JSON.stringify(updateData.content), clientInfo.requestId, '内容存在敏感字,请修改后提交'))
|
||||
}
|
||||
// 检测封面图
|
||||
if (allowCheckImage && updateData.thumbnail) {
|
||||
parallel.push(checkImageSec(updateData.thumbnail, 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]
|
||||
|
||||
// 如果文章内容不存在,则返回
|
||||
if (!article.content) return
|
||||
|
||||
let needUnlock = false
|
||||
let unlockContent = []
|
||||
|
||||
// 获取文章内容中的图片
|
||||
article.content_images = article.content.ops.reduce((imageBlocks, block) => {
|
||||
if (!block.insert.image) return imageBlocks
|
||||
|
||||
const {attributes} = block
|
||||
const {'data-custom': custom = ""} = attributes || {}
|
||||
const parseCustom = custom.split('&').reduce((obj, item) => {
|
||||
const [key, value] = item.split('=')
|
||||
obj[key] = value
|
||||
return obj
|
||||
})
|
||||
|
||||
return imageBlocks.concat(
|
||||
parseCustom.source ||
|
||||
block.insert.image
|
||||
)
|
||||
}, [])
|
||||
|
||||
for (const op of article.content.ops) {
|
||||
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)
|
||||
}, [])
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
{
|
||||
"bsonType": "object",
|
||||
"required": ["user_id", "title", "content"],
|
||||
"permission": {
|
||||
"read": true,
|
||||
"create": "'admin' in auth.role || 'CREATE_UNI_CMS_ARTICLE' in auth.permission",
|
||||
"update": "'admin' in auth.role || 'UPDATE_UNI_CMS_ARTICLE' in auth.permission",
|
||||
"delete": "'admin' in auth.role || 'DELETE_UNI_CMS_ARTICLE' in auth.permission"
|
||||
},
|
||||
"properties": {
|
||||
"_id": {
|
||||
"description": "存储文档 ID(用户 ID),系统自动生成"
|
||||
},
|
||||
"user_id": {
|
||||
"bsonType": "string",
|
||||
"description": "文章作者ID, 参考`uni-id-users` 表",
|
||||
"foreignKey": "uni-id-users._id",
|
||||
"defaultValue": {
|
||||
"$env": "uid"
|
||||
}
|
||||
},
|
||||
"category_id": {
|
||||
"bsonType": "string",
|
||||
"title": "分类",
|
||||
"description": "分类 id,参考`uni-news-categories`表",
|
||||
"foreignKey": "uni-cms-categories._id",
|
||||
"enum": {
|
||||
"collection": "uni-cms-categories",
|
||||
"field": "name as text, _id as value"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"bsonType": "string",
|
||||
"title": "标题",
|
||||
"description": "标题",
|
||||
"label": "标题",
|
||||
"trim": "both"
|
||||
},
|
||||
"content": {
|
||||
"bsonType": "object",
|
||||
"title": "文章内容",
|
||||
"description": "文章内容; 格式为Quill编辑器的Delta格式",
|
||||
"label": "文章内容"
|
||||
},
|
||||
"excerpt": {
|
||||
"bsonType": "string",
|
||||
"title": "文章摘录",
|
||||
"description": "文章摘录",
|
||||
"label": "摘要",
|
||||
"trim": "both"
|
||||
},
|
||||
"article_status": {
|
||||
"bsonType": "int",
|
||||
"title": "文章状态",
|
||||
"description": "文章状态:0 草稿箱 1 已发布",
|
||||
"defaultValue": 0,
|
||||
"enum": [{
|
||||
"value": 0,
|
||||
"text": "草稿箱"
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"text": "已发布"
|
||||
}
|
||||
]
|
||||
},
|
||||
"view_count": {
|
||||
"bsonType": "int",
|
||||
"title": "阅读数量",
|
||||
"description": "阅读数量",
|
||||
"defaultValue": 0,
|
||||
"permission": {
|
||||
"write": false
|
||||
}
|
||||
},
|
||||
"like_count": {
|
||||
"bsonType": "int",
|
||||
"description": "喜欢数、点赞数",
|
||||
"permission": {
|
||||
"write": false
|
||||
}
|
||||
},
|
||||
"is_sticky": {
|
||||
"bsonType": "bool",
|
||||
"title": "是否置顶",
|
||||
"description": "是否置顶",
|
||||
"permission": {
|
||||
"write": false
|
||||
}
|
||||
},
|
||||
"is_essence": {
|
||||
"bsonType": "bool",
|
||||
"title": "阅读加精",
|
||||
"description": "阅读加精",
|
||||
"permission": {
|
||||
"write": false
|
||||
}
|
||||
},
|
||||
"comment_status": {
|
||||
"bsonType": "int",
|
||||
"title": "开放评论",
|
||||
"description": "评论状态:0 关闭 1 开放",
|
||||
"enum": [{
|
||||
"value": 0,
|
||||
"text": "关闭"
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"text": "开放"
|
||||
}
|
||||
]
|
||||
},
|
||||
"comment_count": {
|
||||
"bsonType": "int",
|
||||
"description": "评论数量",
|
||||
"permission": {
|
||||
"write": false
|
||||
}
|
||||
},
|
||||
"last_comment_user_id": {
|
||||
"bsonType": "string",
|
||||
"description": "最后回复用户 id,参考`uni-id-users` 表",
|
||||
"foreignKey": "uni-id-users._id"
|
||||
},
|
||||
"thumbnail": {
|
||||
"bsonType": "array",
|
||||
"title": "封面大图",
|
||||
"description": "缩略图地址",
|
||||
"label": "封面大图",
|
||||
"defaultValue": []
|
||||
},
|
||||
"publish_date": {
|
||||
"bsonType": "timestamp",
|
||||
"title": "发表时间",
|
||||
"description": "发表时间",
|
||||
"defaultValue": {
|
||||
"$env": "now"
|
||||
}
|
||||
},
|
||||
"publish_ip": {
|
||||
"bsonType": "string",
|
||||
"title": "发布文章时IP地址",
|
||||
"description": "发表时 IP 地址",
|
||||
"forceDefaultValue": {
|
||||
"$env": "clientIP"
|
||||
}
|
||||
},
|
||||
"last_modify_date": {
|
||||
"bsonType": "timestamp",
|
||||
"title": "最后修改时间",
|
||||
"description": "最后修改时间",
|
||||
"defaultValue": {
|
||||
"$env": "now"
|
||||
}
|
||||
},
|
||||
"last_modify_ip": {
|
||||
"bsonType": "string",
|
||||
"description": "最后修改时 IP 地址",
|
||||
"forceDefaultValue": {
|
||||
"$env": "clientIP"
|
||||
}
|
||||
},
|
||||
"preview_secret": {
|
||||
"bsonType": "string",
|
||||
"description": "文章预览密钥"
|
||||
},
|
||||
"preview_expired": {
|
||||
"bsonType": "timestamp",
|
||||
"description": "文章预览过期时间"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
{
|
||||
"bsonType": "object",
|
||||
"required": [
|
||||
"user_id",
|
||||
"trans_id",
|
||||
"content_id"
|
||||
],
|
||||
"permission": {
|
||||
"read": true,
|
||||
"create": true,
|
||||
"update": false,
|
||||
"delete": false
|
||||
},
|
||||
"properties": {
|
||||
"_id": {
|
||||
"description": "存储文档 ID(用户 ID),系统自动生成"
|
||||
},
|
||||
"unique_id": {
|
||||
"bsonType": "string",
|
||||
"description": "用于标识观看广告的唯一标识"
|
||||
},
|
||||
"unique_type": {
|
||||
"bsonType": "string",
|
||||
"description": "观看广告的唯一标识类型;user 用户;device 设备"
|
||||
},
|
||||
"trans_id": {
|
||||
"bsonType": "string",
|
||||
"title": "交易ID",
|
||||
"description": "广告回调传回的交易ID",
|
||||
"label": "内容id",
|
||||
"trim": "both"
|
||||
},
|
||||
"content_id": {
|
||||
"bsonType": "string",
|
||||
"title": "内容id",
|
||||
"description": "内容(文章)ID",
|
||||
"label": "内容id",
|
||||
"trim": "both"
|
||||
},
|
||||
"create_date": {
|
||||
"bsonType": "timestamp",
|
||||
"title": "创建时间",
|
||||
"description": "创建时间",
|
||||
"defaultValue": {
|
||||
"$env": "now"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user