1
0
spogulis no https://github.com/ialley-workshop-open/uni-halo.git synced 2026-06-10 11:59:28 +08:00
Files
uni-halo/pagesA/article-detail/article-detail.vue
T

1590 rindas
42 KiB
Vue
Neapstrādāts Vainot Vēsture

Šis fails satur neviennozīmīgus unikoda simbolus
Šis fails satur unikoda simbolus, kas var tikt sajauktas ar citām rakstzīmēm. Ja šķiet, ka tas ir ar nolūku, šo brīdinājumu var droši neņemt vērā. Jāizmanto atsoļa taustiņš (Esc), lai atklātu tās.
<template>
<view class="app-page">
<view v-if="loading !== 'success'" class="loading-wrap">
<tm-skeleton model="card"></tm-skeleton>
<tm-skeleton model="card"></tm-skeleton>
<tm-skeleton model="card"></tm-skeleton>
</view>
<block v-else>
<!-- 顶部信息 -->
<view class="head ma-24">
<view class="title">{{ result.spec.title }}</view>
<view class="detail">
<view class="author">
<text class="author-name">作者{{ result.owner.displayName }}</text>
<text class="author-time">时间{{ { d: result.spec.publishTime, f: 'yyyy年MM月dd日 星期w' } | formatTime }}</text>
</view>
<view class="cover" v-if="result.spec.cover">
<image class="cover-img" mode="aspectFill" :src="calcUrl(result.spec.cover)" @click="handlePreview(0, [{ url: calcUrl(result.spec.cover) }])"></image>
</view>
<view class="count" :class="{ 'no-thumbnail': !result.spec.cover }">
<view class="count-item">
<text class="value">{{ result.stats.visit }}</text>
<text class="label">阅读</text>
</view>
<view class="count-item">
<text class="value">{{ result.stats.upvote }}</text>
<text class="label">喜欢</text>
</view>
<view v-if="postDetailConfig && postDetailConfig.showComment" class="count-item">
<text class="value">{{ result.stats.comment }}</text>
<text class="label">评论</text>
</view>
<view class="count-item">
<text class="value">{{ result.content.raw.length }}</text>
<text class="label">字数</text>
</view>
</view>
</view>
</view>
<!-- 分类 -->
<view class="category">
<view class="category-type">
<text class="text-weight-b">分类</text>
<text v-if="result.categories.length === 0" class="category-tag is-empty">未选择分类</text>
<block v-else>
<text class="category-tag" v-for="(item, index) in result.categories" :key="index" @click="fnToCate(item)">
{{ item.spec.displayName }}
</text>
</block>
</view>
<view class="mt-18 category-type">
<text class="text-weight-b">标签</text>
<text v-if="result.tags.length === 0" class="category-tag is-empty">未选择标签</text>
<block v-else>
<text class="category-tag" :style="{ backgroundColor: item.color }" v-for="(item, index) in result.tags" :key="index" @click="fnToTag(item)">
{{ item.spec.displayName }}
</text>
</block>
</view>
<view v-if="originalURL" class="mt-18 category-type original-url">
<view class="original-url_left text-weight-b">原文</view>
<view class="original-url_right text-grey">
<text class="original-url_right__link" @click.stop="fnToOriginal(originalURL)">{{ originalURL }}</text>
<text class="original-url_right__btn" @click.stop="fnToOriginal(originalURL)">
阅读原文
<text class="iconfont icon-angle-right ml-5 text-size-s"></text>
</text>
</view>
</view>
</view>
<view v-if="!calcAuditModeEnabled && result._doubanUrls.length !== 0 && doubanPluginConfig.position === 'top'" class="pa-24 pb-0">
<view class="vote-wrap copyright-wrap bg-white pa-24 round-4">
<view class="copyright-title text-weight-b flex items-center justify-between">
<text>豆瓣资源</text>
<text class="vote-opra" @click="doubanIsOpen = !doubanIsOpen">
{{ doubanIsOpen ? '收起' : '展开' }}
</text>
</view>
<view v-show="doubanIsOpen" class="flex flex-col uh-gap-8 uh-mt-8">
<ArticleDouban
v-for="(doubanUrl, doubanIndex) in result._doubanUrls"
:key="doubanIndex"
:url="doubanUrl"
:article="result"
:index="doubanIndex"
></ArticleDouban>
</view>
<view v-show="uniHaloPluginAvailable && !doubanIsOpen" class="vote-tip" @click="doubanIsOpen = !doubanIsOpen">豆瓣内容已收起点击展开</view>
<view v-show="!uniHaloPluginAvailable && !doubanIsOpen" class="vote-tip" @click="doubanIsOpen = !doubanIsOpen">提示区域已收起点击显示</view>
</view>
</view>
<!-- 内容区域 -->
<view class="content ml-24 mr-24">
<!-- markdown渲染 -->
<view class="markdown-wrap">
<view v-if="checkPostRestrictRead(result)">
<view v-if="showContentArr.length == 0">
<restrict-read-skeleton
:loading="true"
:lines="3"
:tip-text="`此处内容已隐藏,「${getRestrictReadTypeName(result)}可见」`"
:button-text="`${getRestrictReadTypeName(result)}`"
button-color="#1890ff"
skeleton-color="#f0f0f0"
skeleton-highlight="#e0e0e0"
animation-duration="2"
@refresh="readMore"
/>
</view>
<view v-else v-for="showContent in showContentArr">
<mp-html
class="evan-markdown"
lazy-load
:domain="markdownConfig.domain"
:loading-img="markdownConfig.loadingGif"
:scroll-table="true"
:selectable="true"
:tag-style="markdownConfig.tagStyle"
:container-style="markdownConfig.containStyle"
:content="showContent"
:markdown="true"
:showLineNumber="true"
:showLanguageName="true"
:copyByLongPress="true"
/>
<restrict-read-skeleton
:loading="true"
:lines="3"
:tip-text="`此处内容已隐藏,「${getRestrictReadTypeName(result)}可见」`"
:button-text="`${getRestrictReadTypeName(result)}`"
button-color="#1890ff"
skeleton-color="#f0f0f0"
skeleton-highlight="#e0e0e0"
animation-duration="2"
@refresh="readMore"
/>
</view>
</view>
<view v-else>
<mp-html
class="evan-markdown"
lazy-load
:domain="markdownConfig.domain"
:loading-img="markdownConfig.loadingGif"
:scroll-table="true"
:selectable="true"
:tag-style="markdownConfig.tagStyle"
:container-style="markdownConfig.containStyle"
:content="result.content.raw"
:markdown="true"
:showLineNumber="true"
:showLanguageName="true"
:copyByLongPress="true"
/>
</view>
</view>
<!-- 豆瓣 -->
<view
v-if="!calcAuditModeEnabled && result._doubanUrls.length !== 0 && doubanPluginConfig.position === 'bottom'"
class="vote-wrap copyright-wrap bg-white mt-24 pa-24 round-4"
>
<view class="copyright-title text-weight-b flex items-center justify-between">
<text>豆瓣资源</text>
<text class="vote-opra" @click="doubanIsOpen = !doubanIsOpen">
{{ doubanIsOpen ? '收起' : '展开' }}
</text>
</view>
<view v-show="doubanIsOpen" class="flex flex-col uh-gap-8 uh-mt-8">
<ArticleDouban
v-for="(doubanUrl, doubanIndex) in result._doubanUrls"
:key="doubanIndex"
:url="doubanUrl"
:article="result"
:index="doubanIndex"
></ArticleDouban>
</view>
<view v-show="uniHaloPluginAvailable && !doubanIsOpen" class="vote-tip" @click="doubanIsOpen = !doubanIsOpen">豆瓣内容已收起点击展开</view>
<view v-show="!uniHaloPluginAvailable && !doubanIsOpen" class="vote-tip" @click="doubanIsOpen = !doubanIsOpen">提示区域已收起点击显示</view>
</view>
<!-- 投票 -->
<view v-if="!calcAuditModeEnabled && result._voteIds.length !== 0" class="vote-wrap copyright-wrap bg-white mt-24 pa-24 round-4">
<view class="copyright-title text-weight-b flex items-center justify-between">
<text>相关投票</text>
<text class="vote-opra" @click="voteIsOpen = !voteIsOpen">
{{ voteIsOpen ? '收起' : '展开' }}
</text>
</view>
<template v-if="reloadVote">
<view v-show="voteIsOpen" class="flex flex-col uh-gap-8 uh-mt-8">
<PluginUnavailable
v-if="!uniHaloPluginAvailable"
:pluginId="uniHaloPluginId"
:error-text="uniHaloPluginAvailableError"
:useDecoration="false"
:customStyle="{
width: '100%',
borderRadius: '16rpx'
}"
/>
<template v-else>
<ArticleVote v-for="(voteId, voteIdIndex) in result._voteIds" :key="voteId" :voteId="voteId" :article="result" :index="voteIdIndex"></ArticleVote>
</template>
</view>
<view v-show="uniHaloPluginAvailable && !voteIsOpen" class="vote-tip" @click="voteIsOpen = !voteIsOpen">
投票已收起点击展开 {{ result._voteIds.length }} 个投票项
</view>
<view v-show="!uniHaloPluginAvailable && !voteIsOpen" class="vote-tip" @click="voteIsOpen = !voteIsOpen">提示区域已收起点击显示</view>
</template>
</view>
<!-- 版权声明 -->
<view v-if="postDetailConfig && postDetailConfig.copyrightEnabled" class="copyright-wrap bg-white mt-24 pa-24 round-4">
<view class="copyright-title text-weight-b">版权声明</view>
<view class="copyright-content mt-12 grey-lighten-5 text-grey-darken-2 round-4 pt-12 pb-12 pl-24 pr-24">
<view v-if="postDetailConfig.copyrightAuthor" class="copyright-text text-size-s">版权归属{{ postDetailConfig.copyrightAuthor }}</view>
<view v-if="postDetailConfig.copyrightDesc" class="copyright-text text-size-s mt-12">版权说明{{ postDetailConfig.copyrightDesc }}</view>
<view v-if="postDetailConfig.copyrightViolation" class="copyright-text text-size-s mt-12 text-red">
侵权处理{{ postDetailConfig.copyrightViolation }}
</view>
</view>
</view>
<!-- 评论展示区域 -->
<block v-if="postDetailConfig && postDetailConfig.showComment">
<view v-if="result" id="CommentList" class="comment-wrap bg-white mt-24 pa-24 round-4">
<commentList
ref="commentListRef"
:disallowComment="!result.spec.allowComment"
:postName="result.metadata.name"
:post="result"
@on-comment="fnOnComment"
@on-comment-detail="fnOnShowCommentDetail"
@on-loaded="fnOnCommentLoaded"
></commentList>
</view>
</block>
</view>
<!-- 弹幕效果 -->
<barrage ref="barrage" :maxTop="240" :type="globalAppSettings.barrage.type"></barrage>
<!-- 返回顶部 -->
<tm-flotbutton :width="90" :offset="[16, 80]" icon="icon-angle-up" color="bg-gradient-light-blue-accent" @click="fnToTopPage()"></tm-flotbutton>
<tm-flotbutton
:width="90"
:actions="flotButtonActions"
:click-actions-hiden="false"
actions-pos="left"
:show-text="true"
color="bg-gradient-orange-accent"
@change="fnOnFlotButtonChange"
></tm-flotbutton>
</block>
<!-- 评论详情 -->
<tm-poup v-model="commentDetail.show" height="auto" :round="6" :over-close="true" position="bottom">
<view v-if="commentDetail.show && result" class="pa-24">
<view class="poup-head pb-24">
<view class="poup-title text-align-center text-size-g text-weight-b mb-32">评论详情</view>
<comment-item :useContentBg="false" :useActions="false" :isChild="false" :comment="commentDetail.comment" :postName="result.metadata.name"></comment-item>
</view>
<scroll-view :scroll-y="true" class="poup-body">
<view v-if="commentDetail.loading !== 'success'" class="poup-loading-wrap flex flex-center">
<view v-if="commentDetail.loading === 'loading'" class="loading flex flex-center flex-col">
<text class="e-loading-icon iconfont icon-loading text-blue"></text>
<view class="text-size-n text-grey-lighten-1 py-12 mt-12">加载中请稍等...</view>
</view>
<view v-else-if="commentDetail.loading === 'error'" class="error">
<tm-empty icon="icon-wind-cry" label="加载失败">
<tm-button theme="bg-gradient-light-blue-accent" size="m" @click="fnGetChildComments()">刷新试试</tm-button>
</tm-empty>
</view>
</view>
<block v-else>
<view v-if="commentDetail.list.length === 0" class="poup-empty flex flex-center">
<tm-empty icon="icon-shiliangzhinengduixiang-" label="没有更多评论啦~"></tm-empty>
</view>
<block v-else>
<comment-item
v-for="(comment, index) in commentDetail.list"
:useContentBg="false"
:useSolid="false"
:useActions="false"
:key="index"
:isChild="false"
:comment="comment"
:postName="result.metadata.name"
></comment-item>
</block>
</block>
</scroll-view>
</view>
</tm-poup>
<tm-poup v-model="poster.show" width="90vw" height="auto" :round="6" :over-close="true" position="center">
<view class="poster-content pt-12 bg-white">
<liu-poster ref="liuPoster" :width="674" :height="940" @change="handleOnPosterChange"></liu-poster>
<view v-if="poster.loading" class="poster-loading flex flex-center text-grey-darken-1">
<text class="e-loading-icon iconfont icon-loading"></text>
<text class="ml-6">海报正在生成...</text>
</view>
<block v-if="!poster.loading">
<image
:style="{
width: '100%',
height: '940rpx'
}"
:src="poster.url"
></image>
<view class="poster-save ma-24 mt-0 pt-20 flex flex-center">
<tm-button theme="bg-gradient-light-blue-accent" size="m" @click="fnSavePoster()">保存到相册</tm-button>
<tm-button v-if="false" theme="bg-gradient-orange-accent" size="m" @click="fnShareTo()">分享给好友</tm-button>
<tm-button theme="bg-gradient-blue-grey-accent" size="m" @click="fnOnPosterClose()"> </tm-button>
</view>
</block>
</view>
</tm-poup>
<!-- 验证码弹窗 -->
<tm-dialog
v-model="verificationCodeModal.show"
:disabled="true"
title="验证提示"
:confirmText="verificationCodeModal.confirmText"
:showCancel="true"
model="verificationCodeModal.model"
theme="split"
confirmColor="blue shadow-blue-0"
@cancel="verificationCodeModal.show = false"
@confirm="restrictReadCheckOrViewVideo"
>
<template #default>
<view class="pa-20">
<!-- 扫码验证模式 -->
<view v-if="verificationCodeModal.type === 'scan'" class="flex flex-col flex-center">
<text class="mb-20">请扫描下方二维码获取验证码</text>
<tm-images :width="180" :height="180" :src="verificationCodeModal.imgUrl" preview round="5"></tm-images>
<tm-input bg-color="grey-lighten-5" required v-model="restrictReadInputCode" placeholder="请输入验证码" :border-bottom="false" :flat="true"></tm-input>
</view>
<!-- 观看视频模式 -->
<view v-else class="flex flex-col flex-center">
<text class="mb-20">点击观看视频之后可访问</text>
</view>
</view>
</template>
</tm-dialog>
<!-- 密码弹窗 -->
<tm-dialog
v-model="passwordModal.show"
:disabled="true"
title="验证提示"
confirmText="确定"
content="请输入密码"
:showCancel="true"
model="confirm"
theme="split"
confirmColor="blue shadow-blue-0"
:input-val.sync="restrictReadInputCode"
@cancel="passwordModal.show = false"
@confirm="restrictReadCheck"
></tm-dialog>
<!-- 评论弹窗 -->
<block v-if="calcIsShowComment">
<comment-modal
:show="commentModal.show"
:title="commentModal.title"
:postName="commentModal.postName"
:isComment="commentModal.isComment"
@on-close="fnOnCommentModalClose"
></comment-modal>
</block>
</view>
</template>
<script>
import MarkdownConfig from '@/common/markdown/markdown.config.js';
import tmSkeleton from '@/tm-vuetify/components/tm-skeleton/tm-skeleton.vue';
import tmPoup from '@/tm-vuetify/components/tm-poup/tm-poup.vue';
import tmFlotbutton from '@/tm-vuetify/components/tm-flotbutton/tm-flotbutton.vue';
import tmButton from '@/tm-vuetify/components/tm-button/tm-button.vue';
import tmEmpty from '@/tm-vuetify/components/tm-empty/tm-empty.vue';
import tmDialog from '@/tm-vuetify/components/tm-dialog/tm-dialog.vue';
import tmMore from '@/tm-vuetify/components/tm-more/tm-more.vue';
import mpHtml from '@/components/mp-html/components/mp-html/mp-html.vue';
import commentList from '@/components/comment-list/comment-list.vue';
import commentItem from '@/components/comment-item/comment-item.vue';
import commentModal from '@/components/comment-modal/comment-modal.vue';
import ArticleVote from '@/components/article-vote/article-vote.vue';
import ArticleDouban from '@/components/article-douban/article-douban.vue';
import barrage from '@/components/barrage/barrage.vue';
import { getAppConfigs } from '@/config/index.js';
import { upvote } from '@/utils/upvote.js';
import { checkPostRestrictRead, copyToClipboard, getRestrictReadTypeName, getShowableContent } from '@/utils/restrictRead';
import HaloTokenConfig from '@/config/uhalo.config';
import RestrictReadSkeleton from '@/components/restrict-read-skeleton/restrict-read-skeleton.vue';
import TmImages from '@/tm-vuetify/components/tm-images/tm-images.vue';
import TmInput from '@/tm-vuetify/components/tm-input/tm-input.vue';
import pluginAvailableMixin from '@/common/mixins/pluginAvailable.js';
import PluginUnavailable from '@/components/plugin-unavailable/plugin-unavailable.vue';
let videoAd = null;
export default {
mixins: [pluginAvailableMixin],
components: {
TmInput,
TmImages,
RestrictReadSkeleton,
tmSkeleton,
tmPoup,
tmFlotbutton,
tmButton,
tmEmpty,
tmDialog,
tmMore,
mpHtml,
commentList,
commentItem,
barrage,
commentModal,
ArticleVote,
PluginUnavailable,
ArticleDouban
},
data() {
return {
loading: 'loading',
markdownConfig: MarkdownConfig,
flotButtonActions: [],
queryParams: {
name: null
},
result: null,
commentDetail: {
loading: 'loading',
show: false,
comment: {},
postName: undefined,
list: []
},
poster: {
show: false,
showCanvas: false,
loading: true,
res: null,
url: '',
configs: []
},
metas: [], // 自定义元数据
showContentArr: [],
restrictReadInputCode: '',
verificationCodeModal: {
show: false,
model: 'confirm',
confirmText: '确定',
type: '',
imgUrl: '',
adId: ''
},
passwordModal: {
show: false
},
commentModal: {
show: false,
isComment: false,
postName: '',
title: ''
},
commentListScrollTop: 0,
voteIsOpen: true,
reloadVote: false,
doubanIsOpen: true,
appAuthorize: {
writePhotosAlbum: ''
}
};
},
computed: {
haloConfigs() {
return this.$tm.vx.getters().getConfigs;
},
postDetailConfig() {
return this.haloConfigs.basicConfig.postDetailConfig;
},
calcUrl() {
return (url) => {
if (this.$utils.checkIsUrl(url)) {
return url;
}
return getApp().globalData.baseApiUrl + url;
};
},
// 获取博主信息
bloggerInfo() {
const blogger = this.$tm.vx.getters().getConfigs.authorConfig.blogger;
blogger.avatar = this.$utils.checkAvatarUrl(blogger.avatar, true);
return blogger;
},
// 原文链接:个人资质=可以打开公众号文章;非个人:任意链接地址(需在小程序后台配置)
originalURL() {
return this.result?.metadata?.annotations?.unihalo_originalURL || '';
},
calcIsShowComment() {
return this.postDetailConfig.showComment;
},
calcUpvoted() {
return upvote.has('post', this.result?.metadata?.name);
},
calcAuditModeEnabled() {
return this.haloConfigs.auditConfig.auditModeEnabled;
},
pluginsConfig() {
return this.haloConfigs.pluginConfig;
},
doubanPluginConfig() {
return this.pluginsConfig.doubanPlugin;
}
},
watch: {
haloConfigs: {
deep: true,
immediate: true,
handler: function (newVal) {
if (!newVal) return;
this.fnHandleSetFlotButtonItems(newVal);
}
}
},
async onLoad(e) {
// 检查插件
this.setPluginId(this.NeedPluginIds.PluginVote);
this.setPluginError('阿偶,检测到当前插件没有安装或者启用,无法使用投票功能,请联系管理员');
await this.checkPluginAvailable();
this.fnSetPageTitle('文章加载中...');
this.queryParams.name = e.name;
this.fnGetData();
},
onShow() {
if (this.reloadVote) {
this.reloadVote = false;
setTimeout(() => {
this.reloadVote = true;
}, 100);
}
},
onPullDownRefresh() {
this.fnGetData();
},
onShareAppMessage() {
const cover = this.result.spec.cover ? this.calcUrl(this.result.spec.cover) : '';
return {
path: '/pagesA/article-detail/article-detail?name=' + this.result.metadata.name,
title: this.result.spec.title,
imageUrl: cover
};
},
onShareTimeline() {
const cover = this.result.spec.cover ? this.calcUrl(this.result.spec.cover) : '';
return {
title: this.result.spec.title,
query: {
name: this.result.metadata.name
},
imageUrl: cover
};
},
methods: {
getRestrictReadTypeName,
checkPostRestrictRead,
fnGetData() {
this.loading = 'loading';
this.$httpApi.v2
.getPostByName(this.queryParams.name)
.then((res) => {
console.log('详情', res);
const tempResult = res;
tempResult._voteIds = this.extractVoteBlockIds(res.content?.raw || res.content?.content || '');
tempResult._doubanUrls = this.extractDoubanBlockUrls(res.content?.raw || res.content?.content || '');
const openid = uni.getStorageSync('openid');
if (openid === '' || openid === null) {
this.fnGetOpenid();
}
const toolsPluginEnabled = getAppConfigs().pluginConfig.toolsPlugin?.enabled;
const restrictRead = checkPostRestrictRead(tempResult);
if (restrictRead && toolsPluginEnabled) {
let verifyCodeType = getAppConfigs().pluginConfig.toolsPlugin?.verifyCodeType;
const postVerifyCodeType = tempResult?.metadata?.annotations?.verifyCodeType;
if (postVerifyCodeType) {
verifyCodeType = postVerifyCodeType;
}
if (verifyCodeType === 'scan') {
const scanCodeUrl = getAppConfigs().pluginConfig.toolsPlugin?.scanCodeUrl;
this.verificationCodeModal.type = 'scan';
this.verificationCodeModal.imgUrl = this.$utils.checkImageUrl(scanCodeUrl);
this.verificationCodeModal.model = 'confirm';
this.verificationCodeModal.confirmText = '立即验证';
} else if (verifyCodeType === 'advert') {
const rewardedVideoAdId = getAppConfigs().pluginConfig.toolsPlugin?.rewardedVideoAdId;
this.verificationCodeModal.type = 'advert';
this.verificationCodeModal.adId = rewardedVideoAdId;
this.verificationCodeModal.model = 'dialog';
this.verificationCodeModal.confirmText = '观看视频';
// #ifdef MP-WEIXIN
this.adLoad();
// #endif
}
const showableContentArr = getShowableContent(tempResult);
this.showContentArr = showableContentArr;
}
this.result = tempResult;
this.fnSetPageTitle('文章详情');
this.loading = 'success';
this.reloadVote = true;
this.fnHandleSetFlotButtonItems(this.haloConfigs);
this.handleQueryCommentListScrollTop();
})
.catch((err) => {
console.log('错误', err);
this.loading = 'error';
})
.finally(() => {
uni.hideLoading();
uni.stopPullDownRefresh();
});
},
fnHandleSetFlotButtonItems(configs) {
const actions = [
{
icon: 'icon-share1',
color: 'bg-gradient-blue-accent',
use: true
},
{
icon: upvote.has('post', this.result?.metadata?.name) ? 'icon-heart-fill' : 'icon-like',
color: upvote.has('post', this.result?.metadata?.name) ? 'bg-gradient-red-accent' : 'bg-gradient-orange-accent',
use: true
},
{
icon: 'icon-commentdots-fill',
color: 'bg-gradient-green-accent',
use: configs?.basicConfig?.postDetailConfig?.showComment
}
];
this.flotButtonActions = actions.filter((x) => x.use === true);
},
// 浮动按钮点击
fnOnFlotButtonChange(index) {
switch (index) {
case 0:
this.fnShowShare();
break;
case 1:
this.fnDoLikes();
break;
case 2:
this.fnToComment();
break;
}
},
fnToComment() {
if (!this.calcIsShowComment) {
return;
}
if (!this.result.spec.allowComment) {
return uni.$tm.toast('文章已开启禁止评论!');
}
this.commentModal.isComment = true;
this.commentModal.postName = this.result.metadata.name;
this.commentModal.title = '新增评论';
this.commentModal.show = true;
setTimeout(() => {
uni.pageScrollTo({
scrollTop: this.commentListScrollTop,
duration: 100
});
}, 300);
},
fnOnComment(data) {
this.commentModal.isComment = data.isComment;
this.commentModal.postName = data.postName;
this.commentModal.title = data.title;
this.commentModal.show = true;
},
fnOnCommentModalClose({ refresh, isSubmit }) {
// 评论后自动刷新
if (this.result?.metadata?.annotations?.restrictReadEnable === 'comment') {
this.fnGetData();
}
if (refresh && isSubmit && this.$refs.commentListRef) {
this.$refs.commentListRef.fnGetData();
}
this.commentModal.show = false;
this.commentModal.isComment = false;
this.commentModal.postName = '';
this.commentModal.title = '';
},
fnDoLikes() {
if (upvote.has('post', this.result?.metadata?.name)) {
uni.$tm.toast('已经点过赞啦!');
return;
}
this.$httpApi.v2
.submitUpvote({
group: 'content.halo.run',
plural: 'posts',
name: this.result?.metadata?.name
})
.then((res) => {
uni.$tm.toast('点赞成功!');
upvote.set('post', this.result?.metadata?.name);
this.fnHandleSetFlotButtonItems(this.haloConfigs);
})
.catch((err) => {
uni.$tm.toast('点赞失败');
});
},
async fnShowShare() {
this.poster.show = true;
await this.handleCreatePoster();
setTimeout(() => {
this.poster.showCanvas = true;
this.$nextTick(() => {
this.$refs.liuPoster.init(this.poster.configs);
});
}, 500);
},
handleOnPosterChange(url) {
this.poster.url = url;
this.poster.loading = false;
},
async handleCreatePoster() {
const systemInfo = await uni.getSystemInfoSync();
const _bloggerAvatar = this.$utils.checkAvatarUrl(this.bloggerInfo.avatar, true);
const _articleCover = this.$utils.checkThumbnailUrl(this.result.spec.cover, true);
const _qrCodeImageUrl = await this.qrCodeImageUrl();
this.poster.configs = [
{
type: 'color',
width: 674,
height: 940,
x: 0,
y: 0,
radius: 24,
lineWidth: 0,
lineColor: '#ffffff',
colorObj: {
colorList: ['#FFFFFF'],
direction: 2
}
},
{
type: 'image',
width: 96,
height: 96,
x: 24,
y: 24,
radius: 48,
lineWidth: 2,
lineColor: '#FFFFFF',
path: _bloggerAvatar
},
{
type: 'text',
width: 400,
height: 40,
x: 140,
y: 42,
color: '#000000',
fontSize: 30,
lineHeight: 30,
bold: true,
content: this.bloggerInfo.nickname
},
{
type: 'text',
width: 400,
height: 40,
x: 140,
y: 90,
color: '#666666',
fontSize: 24,
lineHeight: 24,
bold: false,
content: this.bloggerInfo.description
},
{
type: 'image',
width: 624,
height: 360,
x: 24,
y: 152,
radius: 12,
lineWidth: 0,
lineColor: '#FFFFFF',
path: _articleCover
},
{
type: 'text',
width: 626,
height: 40,
x: 24,
y: 562,
color: '#333333',
fontSize: 28,
lineHeight: 28,
bold: true,
content: this.result.spec.title
},
{
type: 'text',
width: 626,
height: 80,
x: 24,
y: 612,
color: '#333333',
fontSize: 24,
lineHeight: 40,
bold: false,
content: this.result?.status?.excerpt || '文章暂无摘要信息'
},
{
type: 'line',
width: 2,
color: '#999999',
startX: 24,
startY: 722,
endX: 646,
endY: 722,
lineType: 'dash'
},
{
type: 'image',
width: 160,
height: 160,
x: 24,
y: 752,
radius: 12,
lineWidth: 0,
lineColor: '#FFFFFF',
path: this.$utils.checkImageUrl(_qrCodeImageUrl)
},
{
type: 'text',
width: 300,
height: 44,
x: 320,
y: 772,
color: '#333333',
fontSize: 32,
lineHeight: 44,
bold: true,
content: '长按识别小程序'
},
{
type: 'text',
width: 442,
height: 24,
x: 234,
y: 872,
color: '#333333',
fontSize: 24,
lineHeight: 24,
bold: false,
content: '关注我,给你分享更多有趣的知识'
}
];
},
// 绘制虚线:https://blog.csdn.net/a460550542/article/details/124821248
drawDashedLine(ctx, x, y, w, h, pattern, color) {
ctx.lineWidth = h;
ctx.strokeStyle = color;
ctx.beginPath();
ctx.setLineDash(pattern);
ctx.moveTo(x, y);
ctx.lineTo(w, y);
ctx.stroke();
y += 20;
},
fnOnPosterClose() {
this.poster.show = false;
this.poster.showCanvas = false;
this.poster.loading = true;
},
fnGetAlbumAuthorize() {
return new Promise((resolve) => {
uni.showModal({
title: '提示',
content: '保存到相册未授权,是否去授权?',
success: (res) => {
if (res.confirm) {
uni.openSetting({
success: (res) => {
if (res.authSetting['scope.writePhotosAlbum'] === true) {
uni.showToast({
icon: 'none',
title: '授权成功,请重新保存'
});
resolve(true);
} else {
uni.showToast({
icon: 'none',
title: '未进行授权操作'
});
resolve(false);
}
},
fail: (err) => {
uni.showToast({
icon: 'none',
title: '授权失败'
});
resolve(false);
}
});
}
},
fail: () => {
resolve(false);
}
});
});
},
fnCheckAlbumAppAuthorize() {
return new Promise((resolve) => {
// #ifdef MP-WEIXIN
uni.getSetting({
success: async (res) => {
if (res.authSetting['scope.writePhotosAlbum'] === false) {
const authorize = await this.fnGetAlbumAuthorize();
console.log('authorize1', authorize);
if (!authorize) {
uni.openSetting({
success: (res) => {
console.log('授权1', res);
}
});
console.log('打开授权页面1');
}
resolve(false);
} else {
resolve(true);
}
},
fail: async () => {
const authorize = await this.fnGetAlbumAuthorize();
console.log('authorize2', authorize);
if (!authorize) {
uni.openSetting({
success: (res) => {
console.log('授权2', res);
}
});
console.log('打开授权页面2');
}
resolve(false);
}
});
// #endif
// #ifndef MP-WEIXIN
resolve(true);
// #endif
});
},
async fnSavePoster() {
// 检查授权
const authorize = await this.fnCheckAlbumAppAuthorize();
if (!authorize) return;
uni.saveImageToPhotosAlbum({
filePath: this.poster.url,
success: () => {
uni.$tm.toast('保存成功');
},
fail: (e) => {
if(!e?.errMsg.includes('cancel')){
uni.$tm.toast('保存失败,请重试');
}
}
});
},
fnShareTo() {
// #ifdef MP-WEIXIN
uni.$tm.toast('点击右上角分享给好友!');
// #endif
},
fnOnShowCommentDetail(data) {
const { postName, comment } = data;
this.commentDetail.comment = comment;
this.commentDetail.postName = postName;
this.commentDetail.list = [];
this.commentDetail.show = true;
this.fnGetChildComments();
},
fnGetChildComments() {
this.commentDetail.loading = 'loading';
this.$httpApi.v2
.getPostCommentReplyList(this.commentDetail.postName, {
page: 1,
size: 100
})
.then((res) => {
console.log('getPostChildrenCommentList res', res);
this.commentDetail.loading = 'success';
this.commentDetail.list = res.items;
})
.catch((err) => {
console.log('getPostChildrenCommentList err', err);
this.commentDetail.loading = 'error';
});
},
fnToCate(category) {
uni.navigateTo({
url: `/pagesA/category-detail/category-detail?name=${category.metadata.name}&title=${category.spec.displayName}`
});
},
fnToTag(tag) {
uni.navigateTo({
url: `/pagesA/tag-detail/tag-detail?name=${tag.metadata.name}&title=${tag.spec.displayName}`
});
},
async fnOnCommentLoaded(data) {
const _list = [];
const _handleData = (list) => {
return new Promise((resolve) => {
if (list.length === 0) {
resolve();
} else {
list.forEach((item) => {
_list.push(item);
if (item.replies && item.replies.length !== 0) {
_handleData(item.replies.items);
}
resolve();
});
}
});
};
await _handleData(data);
// if (this.globalAppSettings.barrage.use) {
// this.$nextTick(() => {
// if (_list.length != 0) {
// _handleAddBarrage();
// }
// });
// }
// const _handleRemove = () => {
// this.$refs['barrage'] && this.$refs['barrage'].remove({
// duration: 5000, // 延迟关闭的时间
// speed: 600 // 弹幕消失的速度
// });
// };
// let index = 0;
// const _handleAddBarrage = () => {
// setTimeout(() => {
// this.$refs['barrage'] && this.$refs['barrage'].add(_list[index]);
// index += 1;
// if (index < _list.length - 1) {
// _handleAddBarrage();
// } else {
// _handleRemove();
// }
// }, 1000);
// };
},
fnToWebview(data) {
uni.navigateTo({
url:
'/pagesC/website/website?data=' +
JSON.stringify({
title: data.title,
url: encodeURIComponent(data.url)
})
});
},
fnToOriginal(originalURL) {
this.fnToWebview({
title: this.result.title,
url: originalURL
});
},
readMore() {
const annotations = this.result?.metadata?.annotations;
const restrictReadEnable = annotations?.restrictReadEnable;
if (restrictReadEnable === 'password') {
this.passwordModal.show = true;
return;
} else if (restrictReadEnable === 'code') {
this.verificationCodeModal.show = true;
return;
} else if (restrictReadEnable === 'comment') {
this.fnToComment();
return;
} else if (restrictReadEnable === 'login') {
uni.showToast({
title: '前往web端登录后访问',
icon: 'none'
});
} else if (restrictReadEnable === 'pay') {
uni.showToast({
title: '前往web端支付后访问',
icon: 'none'
});
}
// 两秒后执行
setTimeout(() => {
copyToClipboard(`${HaloTokenConfig.BASE_API + this.result.status.permalink}`);
}, 2000);
},
// 获取openid
fnGetOpenid() {
// #ifdef MP-WEIXIN
uni.login({
provider: 'weixin',
success: function (loginRes) {
try {
// todo 因为没有获取openid,所以先使用code代替
uni.setStorageSync('openid', loginRes.code);
} catch (error) {
console.log(error);
}
}
});
// #endif
},
restrictReadCheckOrViewVideo() {
console.log('restrictReadCheckOrViewVideo', this.verificationCodeModal.type);
if (this.verificationCodeModal.type === 'advert') {
this.openVideoAd();
} else {
this.restrictReadCheck();
}
},
// 校验密码
restrictReadCheck() {
if (!this.restrictReadInputCode) {
uni.showToast({
title: '请输入内容',
icon: 'none'
});
return;
}
this.$httpApi.v2
.requestRestrictReadCheck(this.result?.metadata?.annotations?.restrictReadEnable, this.restrictReadInputCode, this.result?.metadata?.name)
.then((res) => {
if (res.code === 200) {
this.passwordModal.show = false;
this.verificationCodeModal.show = false;
this.fnGetData();
} else {
uni.showToast({
title: '密码错误',
icon: 'none'
});
}
})
.catch((err) => {
console.error(err);
});
},
adLoad() {
if (wx.createRewardedVideoAd) {
videoAd = wx.createRewardedVideoAd({
adUnitId: this.verificationCodeModal.adId
});
videoAd.onError((err) => {});
videoAd.onClose((status) => {
if ((status && status.isEnded) || status === undefined) {
//这里写广告播放完成后的事件
this.getVerificationCode();
} else {
// 广告播放未完成
}
});
}
},
openVideoAd: function () {
if (videoAd && this.verificationCodeModal.adId !== '') {
videoAd.show().catch((err) => {
// 失败重试
console.log('广告拉取失败');
videoAd.load().then(() => videoAd.show());
});
} else {
this.getVerificationCode();
}
},
getVerificationCode() {
uni.showLoading({
title: '正在获取...'
});
this.$httpApi.v2
.createVerificationCode()
.then((res) => {
if (res.code === 200) {
this.verificationCodeModal.show = false;
this.restrictReadInputCode = res.data;
this.restrictReadCheck();
} else {
uni.$tm.toast('操作失败,请重试!');
}
})
.catch((err) => {
uni.$tm.toast(err.message);
});
},
async qrCodeImageUrl() {
const useDynamicQRCode = this.haloConfigs?.appConfig?.appInfo?.useDynamicQRCode;
if (useDynamicQRCode) {
const qrCodeImg = await this.$httpApi.v2.getQRCodeImg(this.result.metadata.name);
return qrCodeImg;
} else {
return this.haloConfigs?.appConfig?.appInfo?.qrCodeImageUrl;
}
},
handleQueryCommentListScrollTop() {
if (!this.postDetailConfig) return;
if (!this.postDetailConfig.showComment) return;
this.$nextTick(() => {
setTimeout(() => {
uni.createSelectorQuery()
.in(this)
.select('#CommentList')
.boundingClientRect((res) => {
this.commentListScrollTop = res.top - 12;
})
.exec();
}, 2 * 1000);
});
},
/**
* 从HTML字符串中提取所有vote-block的id
* @param {string} html - 包含vote-block的HTML字符串
* @returns {string[]} 提取到的id数组
*/
extractVoteBlockIds(html) {
// 正则表达式匹配<vote-block id="xxx">格式
// 捕获组提取id值
const regex = /<vote-block\s+id="(vote-\w+)"\s*\/?>/g;
const ids = [];
let match;
// 循环匹配所有符合条件的id
while ((match = regex.exec(html)) !== null) {
// 第一个捕获组是id值
ids.push(match[1]);
}
return ids;
},
extractDoubanBlockUrls(html) {
// 匹配<douban src="xxx">格式,仅关注标签和src属性,不限制域名
const regex = /<douban\s+src="([^"]+)"\s*\/?>/g;
const urls = [];
let match;
while ((match = regex.exec(html)) !== null) {
// 捕获src属性中双引号内的所有内容(不限制域名)
urls.push(match[1]);
}
return urls;
},
handlePreview(index, list) {
uni.previewImage({
current: index,
urls: list.map((item) => item.url)
});
}
}
};
</script>
<style lang="scss" scoped>
.app-page {
width: 100vw;
min-height: 100vh;
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #fafafd;
padding-bottom: 24rpx;
}
.bg-image {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: 100vw;
height: 100vh;
z-index: 0;
}
.loading-wrap {
padding: 0 24rpx;
height: inherit;
background-color: #fff;
}
.head {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 36rpx 24rpx;
background-color: #ffffff;
box-shadow: 0rpx 4rpx 24rpx rgba(0, 0, 0, 0.03);
// box-shadow: 0rpx 6rpx 30rpx rgba(182, 223, 255, 0.3);
border-radius: 12rpx;
.title {
font-size: 36rpx;
font-weight: 600;
text-align: center;
}
.detail {
width: 100%;
margin-top: 24rpx;
font-size: 26rpx;
.author {
text-align: center;
font-size: 24rpx;
color: #666;
&-name {
}
&-time {
margin-left: 36rpx;
}
}
.cover {
margin-top: 24rpx;
width: 100%;
height: 280rpx;
&-img {
width: 100%;
height: 100%;
border-radius: 12rpx;
}
}
.count {
margin-top: 24rpx;
display: flex;
justify-content: space-between;
&.no-thumbnail {
border-top: 2rpx solid #f2f2f2;
padding-top: 12rpx;
.count-item {
position: relative;
color: #666;
&::after {
content: '';
position: absolute;
right: 0;
background-color: #eee;
width: 2rpx;
height: 32rpx;
}
&:last-child {
&::after {
display: none;
}
}
}
}
&-item {
flex: 1;
display: flex;
align-items: flex-end;
justify-content: center;
color: #666;
.label {
font-size: 24rpx;
padding-left: 8rpx;
}
.value {
font-size: 32rpx;
}
}
}
}
}
.category {
margin: 0 24rpx;
padding: 24rpx;
background-color: #ffffff;
border-radius: 12rpx;
// box-shadow: 0rpx 0rpx 24rpx rgba(182, 223, 255, 0.3);
box-shadow: 0rpx 4rpx 24rpx rgba(0, 0, 0, 0.03);
// border: 2rpx solid #f8f8f8;
font-size: 28rpx;
&-type {
line-height: 55rpx;
}
&-tag {
background-color: #5bb8fa;
color: #fff;
padding: 6rpx 12rpx;
border-radius: 6rpx;
font-size: 24rpx;
&.is-empty {
background-color: #607d8b;
}
}
}
.category-tag + .category-tag {
margin-left: 12rpx;
}
.content {
margin-top: 24rpx;
}
.markdown-wrap,
.evan-markdown,
.ad-wrap,
.copyright-wrap,
.comment-wrap {
box-shadow: 0rpx 0rpx 24rpx rgba(0, 0, 0, 0.03);
}
.vote-wrap {
position: relative;
}
.vote-opra {
font-size: 24rpx;
font-weight: normal;
color: #999;
}
.vote-tip {
margin-top: 12rpx;
padding: 50rpx 24rpx;
background: #f1f5f9;
color: #666;
font-size: 24rpx;
border-radius: 12rpx;
text-align: center;
}
.markdown-wrap {
overflow: hidden;
border-radius: 12rpx;
}
.copyright-title {
position: relative;
box-sizing: border-box;
padding-left: 24rpx;
font-size: 30rpx;
&:before {
content: '';
position: absolute;
left: 0rpx;
top: 8rpx;
width: 8rpx;
height: 26rpx;
background-color: rgb(3, 174, 252);
border-radius: 6rpx;
}
}
.poup-head {
border-bottom: 2rpx solid #f5f5f5;
}
.poup-body {
height: 50vh;
}
.poup-empty {
width: 100%;
height: 40vh;
}
.poup-loading-wrap {
width: 100%;
height: 40vh;
.e-loading-icon {
font-size: 80rpx;
}
}
.poster-content {
width: 100%;
min-height: 60vh;
overflow: hidden;
}
.copyright-text {
line-height: 1.7;
}
.poster-loading {
width: 100%;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
// background-color: rgba(0, 0, 0, 0.1);
z-index: 666;
}
.poster-save {
box-sizing: border-box;
border-top: 2rpx dashed #eee;
}
.original-url {
display: flex;
&_left {
flex-shrink: 0;
width: 84rpx;
}
&_right {
flex-grow: 1;
display: inline-flex;
align-items: center;
&__link {
width: 410rpx;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__btn {
flex-grow: 1;
flex-shrink: 0;
display: block;
text-align: right;
}
}
}
</style>