首次完整推送,

V:1.20240808.006
This commit is contained in:
fm453
2024-08-13 18:32:37 +08:00
parent 15be3e9373
commit c62d15b288
939 changed files with 111777 additions and 0 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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}.` : '&#8226'}}</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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,6 @@
{
"noData": "No Data",
"noNetwork": "Network error",
"toSet": "Go to settings",
"error": "error"
}

View File

@ -0,0 +1,6 @@
import en from './en.json'
import zhHans from './zh-Hans.json'
export default {
en,
'zh-Hans': zhHans
}

View File

@ -0,0 +1,6 @@
{
"noData": "暂无数据",
"noNetwork": "网络异常",
"toSet": "前往设置",
"error": "错误"
}

View File

@ -0,0 +1,3 @@
新增uni-load-state组件这是一个封装数据请求状态的组件。根据uniCloud-db组件提供的参数直接响应对应的效果。
包括加载中、当前页面为空、没有更多数据、上拉加载更多;
加载错误判断如果是断网就引导打开系统网络设置页面。恢复联网后自动触发networkResume方法。

View File

@ -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>