首次完整推送,

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