Files
t/uni_modules/uni-cms-article/pages/detail/detail.vue
T
root 4f5893f87a fix: 修复首页空白/失效外链/云函数越权,补齐缺失页面与种子数据
## 阻断性缺陷
- list.vue 是 0 字节空文件、slist.vue 与 search/search.vue 从未存在,
  而前者是 tabBar 首页、后者是 tabBar「搜索」页 —— 开屏即白屏。
  按 .nvue 原型与详情页契约重建三页(CSS 渐变主视觉、分类筛选、搜索历史/热搜/联想)。
- parse-image-url.js 对空封面调 undefined.startsWith 直接抛错,列表页整页崩。
- 云函数目录缺 uni-cms-articles / uni-cms-categories / uni-cms-unlock-record
  schema 与 schema.ext.js,线上内容渲染与解锁逻辑无配置可用。

## 越权与数据一致性
- uni-cms-articles:del/update/add 全部无鉴权,未登录即可删任意文章、
  改他人文章作者与阅读量。补 login + 作者归属校验,作者与计数改为服务端取值。
- comments.likeComment/relikeComment:直接采信客户端传入的 user_id,
  可冒名点赞刷计数。改为以令牌为准,并纳入事务。
- comments.updateComment:对数组取 .author_id,权限判断恒失败;
  字段名 updateTime 与 ip_location 类型与 schema 不符。
- comments.deleteComment:`!root_id === 0` 优先级错误导致计数恒不减;
  且误更新 uni-cms-articles、按不存在的 type 字段删点赞明细产生孤儿数据。
- cms-articles-like/collect:查重条件混入本次请求时间戳,防重永远失效,
  可无限重复刷计数;补唯一索引并事务化。
- cms-vote:读-改-写票数导致并发丢票,记录与统计非原子;改为事务 + 原子自增。
- cms-articles-log:忽略传入 user_id 直接返回全表,泄露全站浏览记录。
- article_info:get() 使用未定义变量必崩;读接口全部无鉴权。
- user-info:公开资料接口可查任意用户 last_login_ip。

## 资源与数据
- 全项目清空失效的签名外链(expire_at 均为 2025-03,必然 403),
  改为本地生成资源:6 套文章模板、8 个编辑器图标、2 张文章配图。
- 新增分类 / 模板 / 礼物 / 热搜词种子数据,并在 db_init.json 登记,
  同时补上点赞、收藏、投票、浏览日志的唯一索引。

## 功能
- 草稿箱:预览页拆出「发布」与「存为草稿」,作品列表按状态筛选并显示徽标。
  原实现有 4 个 tab 但只有 1 个有内容,且 article_status 在 UI 上无体现。
- 编辑中断恢复:接上原本空实现的「编辑草稿」回调,区分新建与编辑已有文章。

## 工具
- tools/audit-project.js:编码 / 页面路由 / 云调用 / 云函数鉴权 / 敏感信息检查
- tools/check-vue.js:SFC 脚本语法(词法扫描处理 import·export 与条件编译)
- tools/verify.js:一键验证;两个检查器各带自测,防止"永远通过"
- tools/gen-*.py:模板与图标资源生成脚本
2026-09-11 17:48:27 +08:00

2597 lines
64 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="" :style="backgrond_color + color">
<view>
<uni-popup ref="inputDialog" type="dialog">
<uni-popup-dialog type="info" cancelText="关闭" confirmText="继续" title="提示" content="该功能还在开发中!"
@confirm="dialogConfirm" @close="dialogClose">
</uni-popup-dialog>
</uni-popup>
</view>
<!-- <view class="v-preview">
<image :src="templateSrc" alt="" class="img" mode="widthFix" />
<view class="btn-area">
<view class="red-btn-area">
<button class="red-btn" @click="btn(0)">老兵直播</button>
<button class="red-btn" @click="btn(1)">老兵故事</button>
<button class="red-btn" @click="btn(2)">在线注册</button>
</view>
<view class="red-btn-plain-area">
<view class="red-btn-plain" @click="btn(3)">新四军</view>
<view class="red-btn-plain" @click="btn(4)">抗美援朝</view>
<view class="red-btn-plain" @click="btn(5)">抗美援越</view>
<view class="red-btn-plain" @click="btn(6)">对越自卫反击战</view>
<view class="red-btn-plain" @click="btn(7)">和平时期退役兵</view>
</view>
</view>
</view> -->
<unicloud-db v-slot:default="{data, loading, error, options}" :collection="collection" :options="formData"
:getone="true" :where="where" :manual="true" ref="detail" foreignKey="uni-cms-articles.user_id"
@load="loadData" class="article">
<template v-if="!loading && data">
<view class="v-preview" v-if="temp">
<image :src="temp.temptele_src" alt="" class="img" mode="widthFix" />
</view>
<view class="meta">
<view class="author">
<cloud-image height="64rpx" width="64rpx" borderRadius="50%"
:src="userInfo.avatar_file.url"></cloud-image>
<text class="at" :style="color">{{ userInfo.nickname || '' }}</text>
<text class="split">·</text>
<text class="date" :style="color">{{ publishTime( data.publish_date) }}</text>
</view>
</view>
<view class="" v-if="data.edit_type === 'mobile'">
<view class="" v-for="(item, index) in data.cmsLst" :key="index">
<u-parse :content="item.html" v-if="item.type === 'image' || item.type === 'video'">
</u-parse>
<view class="" v-if="item.type === 'image'"
style="width: 100wv;margin-top: 30rpx;margin-bottom: 30rpx;">
<cloud-image width="100%" height="500rpx" mode="aspectFit" :src="item.image.src">
</cloud-image>
</view>
<view class="" v-if="item.type === 'video'"
style="width: 100wv;margin-top: 30rpx;margin-bottom: 30rpx;">
<!-- <cloud-image
width="100%"
height="500rpx"
mode="aspectFit"
:src="item.video.poster">
</cloud-image> -->
<video style="width: 100%; height: 500rpx;" :src="item.video.src" enable-danmu danmu-btn
controls>
</video>
</view>
<view class="" v-if="item.type === 'vote'">
<uni-card>
<view class="vote-container">
<text class="vote-title">{{ item.vote.voteTitle }}</text>
<text class="deadline">截止时间:{{ item.vote.datetime }}</text>
<view class="" v-if="item.vote.type === voteType[0]">
<view class="">
<view class=""
v-if="isSelectVoteValue[item.vote.vote_id] === true || isExpired(item.vote.datetime)"
v-for="(radio, radioIndex) in item.vote.voteLst">
<text>
{{ radio.value }}
</text>
<view class="progress-container">
<u-line-progress :percentage="(selectVoteValue[item.vote.vote_id].all_data.selectValue[radio.value] || 0) /
selectVoteValue[item.vote.vote_id].all_data.count * 100"
:activeColor="selectVoteValue[item.vote.vote_id].user_data.selectValue === radio.value ? '#3366ff' : '#66ff66'"
class="progress">
</u-line-progress>
<text class="count-text">
{{
(selectVoteValue[item.vote.vote_id].
all_data.selectValue[radio.value] || 0)
}}
</text>
</view>
</view>
</view>
<view class="" v-if="isSelectVoteValue[item.vote.vote_id] === false">
<u-radio-group v-model="selectVoteValue[item.vote.vote_id].val"
placement="column" @change="groupChange" wrap>
<u-radio :customStyle="{marginBottom: '8px'}"
v-for="(radio, radioIndex) in item.vote.voteLst" :key="radioIndex"
:label="radio.value" :name="radio.value"
@change="() => {this.setVoteValue(item.vote.vote_id, radio.value)}"
style="width: 100%;">
</u-radio>
</u-radio-group>
</view>
</view>
<view class="" v-if="item.vote.type === voteType[1]">
<view class="" v-if="isSelectVoteValue[item.vote.vote_id] === true"
v-for="(radio, radioIndex) in item.vote.voteLst">
<text>
{{ radio.value }}
</text>
<view class="progress-container">
<u-line-progress :percentage="(selectVoteValue[item.vote.vote_id].all_data.selectValue[radio.value] || 0) /
selectVoteValue[item.vote.vote_id].all_data.count * 100"
:activeColor="selectVoteValue[item.vote.vote_id].
user_data.selectValue === radio.value ? '#3366ff' : '#66ff66'" :showText="true"
class="progress"></u-line-progress>
<text class="count-text">
{{
(selectVoteValue[item.vote.vote_id].
all_data.selectValue[radio.value] || 0)
}}
</text>
</view>
</view>
<view v-if="isSelectVoteValue[item.vote.vote_id] === false">
<u-checkbox-group v-model="selectVoteValue[item.vote.vote_id].val"
placement="column" @change="checkboxChange">
<u-checkbox :customStyle="{marginBottom: '8px'}"
v-for="(checkbox, checkboxIndex) in item.vote.voteLst"
:key="checkboxIndex" :label="checkbox.value" :name="checkbox.value">
</u-checkbox>
</u-checkbox-group>
</view>
</view>
<view class="" v-if="item.vote.type === voteType[2]">
<u-checkbox-group v-model="item.vote.selectValue" placement="column"
@change="checkboxChange">
<view class="">
<u-checkbox :customStyle="{marginBottom: '8px'}"
v-for="(checkbox, checkboxIndex) in item.vote.voteLst"
:key="checkboxIndex" :label="checkbox.value" :name="checkbox.value"
:disabled="isCheckboxDisabled(item.vote.selectValue, checkbox.value)">
</u-checkbox>
</view>
</u-checkbox-group>
</view>
<view class="" v-if="isLogin && isSelectVoteValue[item.vote.vote_id] === true"
style="border-top: 3rpx solid #ccc;padding-top: 20rpx;margin-top: 20rpx;
text-align: center;">
你已投票
</view>
<view class="" v-else-if="isExpired(item.vote.datetime)" style="border-top: 3rpx solid #ccc;padding-top: 20rpx;margin-top: 20rpx;
text-align: center;;">
已截止
</view>
<view class="" v-else-if="!isLogin" @click="toLogin()" style="border-top: 3rpx solid #ccc;padding-top: 20rpx;margin-top: 20rpx;
text-align: center;;">
点击登录后投票
</view>
<view class="" v-else @click="selectVote(item)" style="border-top: 3rpx solid #ccc;padding-top: 20rpx;margin-top: 20rpx;
text-align: center;">
点击投票
</view>
</view>
</uni-card>
</view>
</view>
</view>
<view v-else>
<view>
<view class="meta">
<view class="title">
<text class="text" :style="color">{{ data.title }}</text>
</view>
<!-- <view class="author" @click="toPersonalpage()">
<template>
<cloud-image v-if="checkUrlProtocol(data.user_id[0].avatar_file.url) === 'cloud'"
height="64rpx" width="64rpx" borderRadius="50%"
:src="data.user_id[0].avatar_file.url"></cloud-image>
<image
v-else-if="checkUrlProtocol(data.user_id[0].avatar_file.url) === 'http/https'"
:src="data.user_id[0].avatar_file.url"
style="width: 64rpx;height: 64rpx;border-radius: 50%;"></image>
<image v-else src="https://qnycdn.mymoyu.top/static/avatar.jpg"
style="width: 64rpx;height: 64rpx;border-radius: 50%;"></image>
<text class="at" :style="color">{{ data.user_id[0].nickname || '' }}</text>
<text class="split">·</text>
</template>
<text class="date" :style="color">{{ publishTime( data.publish_date) }}</text>
</view> -->
</view>
</view>
<render-article-detail :content="data.content" :color="color" :content-images="data.content_images"
:ad-config="{ adpId, watchAdUniqueType }"></render-article-detail>
</view>
</template>
<view class="detail-loading" v-else>
<uni-icons type="spinner-cycle" size="35px" />
</view>
<!-- <view>
<view class="" v-for="(item, index) in gridLst" :index="index" :key="index">
<view class="grid-item-box" style="background-color: #fff;">
<uni-icons :type="item.icon" :size="30" :color="item.select_flag?'#e66':'#777'" />
<text class="text">{{item.value?item.value:0}}</text>
</view>
</view>
</view> -->
<uni-grid :column="3" :highlight="true" @change="change" :show-border="false">
<uni-grid-item v-for="(item, index) in gridLst" :index="index" :key="index">
<view class="grid-item-box">
<uni-icons :type="item.icon" :size="30"
:color="item.select_flag?select_icon_color:icon_color" />
<text class="text" :style="item.select_flag?select_font_color:icon_font_color">
{{item.value?item.value:0}}
</text>
</view>
</uni-grid-item>
</uni-grid>
<view class="">
<text class="date" @click="complaint">投诉</text> | 阅读 {{ data.view_count }}
</view>
<view class="example-body" style="justify-content:center" v-for="(item, index) in cmsArticleLogList"
:key="index">
<!-- <view class="" v-if="item" v-for="(item1, index1) in item" :key="index1">
<cloud-image v-if="checkUrlProtocol(item1.user_id[0].avatar_file.url) === 'cloud'" height="64rpx"
width="64rpx" borderRadius="50%" :src="item1.user_id[0].avatar_file.url"></cloud-image>
<image v-else-if="checkUrlProtocol(item1.user_id[0].avatar_file.url) === 'http/https'"
:src="item1.user_id[0].avatar_file.url" style="width: 64rpx;height: 64rpx;border-radius: 50%;">
</image>
<image v-else src="https://qnycdn.mymoyu.top/static/avatar.jpg"
style="width: 64rpx;height: 64rpx;border-radius: 50%;"></image>
</view> -->
</view>
<view class="btn-gift-charts " @click="openGiftCharts()">
{{ "送礼榜单 >" }}
</view>
<!-- comment_count -->
<view class="comment-eg">
<hb-comment ref="hbComment" @add="add" @del="del" @like="like" @relike="relike" @focusOn="focusOn"
@replyAdd="replyAdd" @getTreeComment="getTreeComment" :deleteTip="'确认删除?'" :cmData="commentData"
v-if="commentData" :user_name_color="temp.user_name_color" :btn_color="btn_color"
:select_icon_color="select_icon_color" :icon_color="icon_color"
:btn_send_commends_color="btn_send_commends_color"
:btn_send_commends_font_color="btn_send_commends_font_color" :icon_font_color="icon_font_color"
:select_font_color="select_font_color">
</hb-comment>
</view>
</unicloud-db>
<uni-popup ref="giftCharts" type="bottom" background-color="#fff">
<view class="tab-container">
<view class="tab-item" :class="{active: activeTab === 'monthly'}" @click="switchTab('monthly')">
月度榜单
</view>
<view class="tab-item" :class="{active: activeTab === 'total'}" @click="switchTab('total')">
总榜单
</view>
</view>
<view class="totalGiftCharts" v-if="activeTab === 'total'">
<unicloud-db ref='totalGiftCharts' v-slot:default="{ pagination, hasMore, loading, error, options }"
@error="onqueryerror" @onPullDownRefresh="onPullDownRefresh"
:collection="totalGiftChartsCollecttion" :page-size="20" orderby="publish_date desc"
@load="totalGiftChartsListLoad">
<!-- 基于 uni-list 的页面布局 field="user_id.nickname"-->
<!-- #ifdef APP-NVUE -->
<list class="uni-list giftCharts" :border="false">
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
<scroll-view scroll-y class="uni-list" refresher-enabled
:refresher-triggered="loadType=== 'refresh'" @refresherrefresh="totalGiftChar_refresh"
@scrolltolower="totalGiftChar_loadMore">
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<refresh-box :loading="loading" @refresh="totalGiftChar_refresh"></refresh-box>
<!-- #endif -->
<!-- 列表渲染 -->
<template v-for="(item, index) in totalGiftChartsList">
<view class="giftCharts-item">
<!-- 序号列 -->
<view class="order-index">
{{ index + 1 }}
</view>
<!-- 头像列 -->
<view class="avatar-column">
<cloud-image
v-if="item.user_id[0].avatar_file && checkUrlProtocol(item.user_id[0].avatar_file.url) === 'cloud'"
borderRadius="50%" width="64rpx" height="64rpx"
:src="item.user_id[0].avatar_file.url"></cloud-image>
<image class="avatar" :src="emptyAvatar"
v-else-if="item.user_id[0].avatar_file && checkUrlProtocol(item.user_id[0].avatar_file.url) === 'http/https'">
</image>
<image class="avatar" :src="emptyAvatar" v-else></image>
</view>
<!-- 昵称列 -->
<view class="nickname-column">
{{ item.user_id[0].nickname || '匿名用户' }}
</view>
<!-- 礼物值列 -->
<view class="gift-value">
{{ item.gift_value }} 礼物值
</view>
</view>
</template>
<!-- 加载状态:上拉加载更多,加载中,没有更多数据了,加载错误 -->
<!-- #ifdef APP-PLUS -->
<uni-list-item>
<template v-slot:body>
<!-- #endif -->
<uni-load-state @networkResume="totalGiftChar_refresh"
:state="{ data: totalGiftChartsList, pagination, hasMore, loading, error }"
@loadMore="totalGiftChar_loadMore">
</uni-load-state>
<!-- #ifdef APP-PLUS -->
</template>
</uni-list-item>
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
</scroll-view>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
</list>
<!-- #endif -->
</unicloud-db>
</view>
<view class="monthlyGiftCharts" v-if="activeTab === 'monthly'">
<unicloud-db ref='monthlyGiftCharts' v-slot:default="{ pagination, hasMore, loading, error, options }"
@error="onqueryerror" @onPullDownRefresh="onPullDownRefresh"
:collection="monthlyGiftChartsCollecttion" :page-size="20" orderby="publish_date desc"
@load="monthlyGiftChartsListLoad">
<!-- 基于 uni-list 的页面布局 field="user_id.nickname"-->
<!-- #ifdef APP-NVUE -->
<list class="uni-list giftCharts" :border="false">
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
<scroll-view scroll-y class="uni-list" refresher-enabled
:refresher-triggered="loadType=== 'refresh'" @refresherrefresh="monthlyGiftCharts_refresh"
@scrolltolower="monthlyGiftCharts_loadMore">
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<refresh-box :loading="loading" @refresh="monthlyGiftCharts_refresh"></refresh-box>
<!-- #endif -->
<!-- 列表渲染 -->
<template v-for="(item, index) in monthlyGiftChartsList">
<view class="giftCharts-item">
<!-- 序号列 -->
<view class="order-index">
{{ index + 1 }}
</view>
<!-- 头像列 -->
<view class="avatar-column">
<cloud-image
v-if="item.user_id[0].avatar_file && checkUrlProtocol(item.user_id[0].avatar_file.url) === 'cloud'"
borderRadius="50%" width="64rpx" height="64rpx"
:src="item.user_id[0].avatar_file.url"></cloud-image>
<image class="avatar" :src="emptyAvatar"
v-else-if="item.user_id[0].avatar_file && checkUrlProtocol(item.user_id[0].avatar_file.url) === 'http/https'">
</image>
<image class="avatar" :src="emptyAvatar" v-else></image>
</view>
<!-- 昵称列 -->
<view class="nickname-column">
{{ item.user_id[0].nickname || '匿名用户' }}
</view>
<!-- 礼物值列 -->
<view class="gift-value">
{{ item.gift_value }} 礼物值
</view>
</view>
</template>
<!-- 加载状态:上拉加载更多,加载中,没有更多数据了,加载错误 -->
<!-- #ifdef APP-PLUS -->
<uni-list-item>
<template v-slot:body>
<!-- #endif -->
<uni-load-state @networkResume="monthlyGiftCharts_refresh"
:state="{ data: monthlyGiftChartsList, pagination, hasMore, loading, error }"
@loadMore="monthlyGiftCharts_loadMore">
</uni-load-state>
<!-- #ifdef APP-PLUS -->
</template>
</uni-list-item>
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
</scroll-view>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
</list>
<!-- #endif -->
</unicloud-db>
</view>
</uni-popup>
<uni-popup ref="popup" background-color="#fff" type="bottom">
<view class="">
<uni-notice-bar text="本月送出 0 礼物值,快来支持作者" moreText="规则" show-get-more @getmore="getMore" />
<uni-swiper-dot class="uni-swiper-dot-box" @clickItem=clickItem :info="info" :current="current"
:mode="mode" field="content">
<swiper class="swiper" :indicator-dots="true">
<swiper-item class="swiper-box">
<view class="grid-container">
<view v-for="(item, index) in giftList" :key="index" :class="{
'grid-item': true,
'select-gift': item.is_select_gift
}" @click="setGiftItem(item, index)">
<view class="grid-item-box">
<image :src="item.url" class="image" mode="aspectFill" />
<text class="text">{{"¥ " + item.price }}</text>
</view>
</view>
</view>
</swiper-item>
</swiper>
</uni-swiper-dot>
<view class="wallet-container">
<view class="uni-px-5 uni-pb-5">
<uni-data-checkbox mode="list" multiple v-model="isBalance"
:localdata="hobby"></uni-data-checkbox>
</view>
<view class=""></view>
<view class="wallet-info">
钱包余额:{{ 0 }} 元
</view>
<button class="gift-btn" @tap="handleGift()">
赠送
</button>
</view>
</view>
</uni-popup>
<view class="fixed-music-area" @click="onPlay">
<block v-if="!isPlay">
<image style="width: 40rpx;height: 40rpx;" src="/static/music-play.png" mode=""></image>
</block>
<block v-else>
<image style="width: 40rpx;height: 40rpx;" src="/static/music-close.png" mode=""></image>
</block>
<!-- <button @click="onPlay">{{isPlay?'暂停':'播放'}}</button> -->
</view>
</view>
</template>
<script>
import uLineProgress from "@/uni_modules/uview-ui/components/u-line-progress/u-line-progress.vue"
import uRadioGroup from "@/uni_modules/uview-ui/components/u-radio-group/u-radio-group.vue";
import uRadio from "@/uni_modules/uview-ui/components/u-radio/u-radio.vue";
import uParse from "@/uni_modules/uview-ui/components/u-parse/u-parse.vue";
import uTabbar from "@/uni_modules/uview-ui/components/u-tabbar/u-tabbar.vue";
import uTabbarItem from "@/uni_modules/uview-ui/components/u-tabbar-item/u-tabbar-item.vue";
import mediaList from "@/components/image-grid/media-list.vue";
import uCheckboxGroup from "@/uni_modules/uview-ui/components/u-checkbox-group/u-checkbox-group.vue";
import uCheckbox from "@/uni_modules/uview-ui/components/u-checkbox/u-checkbox.vue";
import uniNavBar from '@/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-nav-bar.vue';
import renderArticleDetail from "@/uni_modules/uni-cms-article/components/render-article-detail/index.vue";
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time";
import cloudImage from "@/components/cloud-image.vue"
const uniIdCo = uniCloud.importObject("uni-id-co")
import {
store,
mutations
} from '@/pages3/uni_modules/uni-id-pages/common/store.js'
const db = uniCloud.database()
const _ = db.command;
const articleDBName = 'uni-cms-articles'
const userDBName = 'uni-id-users'
const date = new Date();
const nowMonths = (date.getUTCFullYear() - 1970) * 12 + date.getUTCMonth() + 1;
console.log(nowMonths);
const audioContext = wx.createInnerAudioContext()
export default {
components: {
uniNavBar,
renderArticleDetail,
cloudImage,
mediaList,
uParse,
uRadioGroup,
uRadio,
uCheckboxGroup,
uCheckbox,
uLineProgress
},
data() {
return {
monthlyGiftChartsList: [],
totalGiftChartsList: [],
emptyAvatar: "https://qnycdn.mymoyu.top/static/avatar.jpg",
showRefresh: false, // 是否显示刷新按钮
listHeight: 0, // 列表高度
mpButtonLeftPlaceholderSize: 0, // 小程序左侧icon占位大小
mpButtonPlaceholderSize: 87, // 小程序导航栏按钮占位大小
navBarHeight: 44, // 导航栏高度
refreshStatus: 0, // 刷新状态 0: 未刷新 1: 刷新中 2: 刷新完成
listData: [], // 列表数据
loadType: null,
activeTab: 'monthly',
selectGift: {},
isBalance: [],
hobby: [{
text: '余额抵扣',
value: 0
}],
swiperDotIndex: 0,
giftList: [],
voteType: [
"单选(默认)",
"多选,无限制",
"多选, 最多两项"
],
gridLst: [{
text: "点赞",
icon: "heart",
value: 0,
select_flag: false
},
{
text: "收藏",
icon: "folder-add",
value: 0,
select_flag: false
},
{
text: "打赏",
icon: "gift",
value: 0,
select_flag: false
}
],
cmsArticleLogList: [],
templateSrc: "https://qnycdn.mymoyu.top/temp/抗战胜利纪念日.jpg",
"commentData": null,
"reqFlag": false,
id: "", // 文章ID
title: "", // 文章标题
formData: {}, // 表单数据
user_id: {},
// 广告相关配置
adpId: "", // TODO: 请填写广告位ID
watchAdUniqueType: "device", // TODO: 观看广告的唯一标识类型,可选值为 user 或者 device,user 表示用户唯一,device 表示设备唯一
detail_music: {},
temp_id: "",
temp: {},
isPlay: true,
isSelectVoteValue: {
},
selectVoteValue: {
}
}
},
onShow() {
let uid = this.userInfo._id;
console.log(uid);
if (this.id) {
this.detailLog(this.id, uid);
this.getComment();
}
},
computed: {
btn_send_commends_color() {
if (!this.temp) {
return "";
}
return `background-color: ${this.temp.btn_send_commends_color};`
},
btn_send_commends_font_color() {
if (!this.temp) {
return "";
}
return `color: ${this.temp.btn_send_commends_font_color};`
},
btn_color() {
if (!this.temp) {
return "";
}
return `color: ${this.temp.btn_color}`
},
select_icon_color() {
if (!this.temp) {
return "";
}
return `${this.temp.select_icon_color};`
},
icon_color() {
if (!this.temp) {
return "";
}
return `${this.temp.icon_color};`
},
icon_font_color() {
if (!this.temp) {
return "";
}
return `color:${this.temp.icon_color};`
},
select_font_color() {
if (!this.temp) {
return "";
}
return `color: ${this.temp.select_icon_color};`
},
backgrond_color() {
if (!this.temp) {
return "";
}
return `background-color: ${this.temp.backgrond_color};`
},
color() {
if (!this.temp) {
return "";
}
return `color: ${this.temp.font_color};`
},
userInfo() {
return store.userInfo
},
realNameStatus() {
if (!this.userInfo.realNameAuth) {
return 0
}
return this.userInfo.realNameAuth.authStatus
},
isLogin() {
return this.userInfo._id ? true : false;
},
where() {
return {
_id: this.id
}
},
collection() {
return [
db.collection("uni-cms-articles").where({
_id: this.id
}).getTemp(),
db.collection("uni-id-users").field('_id, nickname,avatar_file').getTemp()
]
},
// 总榜单为0 当前榜单为 时间撮对应的总月份数
monthlyGiftChartsCollecttion() {
return [
db.collection("users-give-gift")
.where({
months: nowMonths
})
.field(
'_id, gift_value,user_id,article_id,months'
)
.orderBy('gift_value', 'desc')
.getTemp(),
db.collection("uni-id-users").field('_id, nickname,avatar_file').getTemp()
]
},
totalGiftChartsCollecttion() {
return [
db.collection("users-give-gift")
.where({
months: 0
})
.field(
'_id, gift_value,user_id,article_id,months'
)
.orderBy('gift_value', 'desc')
.getTemp(),
db.collection("uni-id-users").field('_id, nickname,avatar_file').getTemp()
]
}
},
onReady() {
// 开始加载数据,修改 where 条件后才开始去加载 clinetDB 的数据 ,需要等组件渲染完毕后才开始执行 loadData,所以不能再 onLoad 中执行
if (this.id) { // ID 不为空,则发起查询
this.$refs.detail.loadData()
this.$refs.monthlyGiftCharts.loadData();
} else {
uni.showToast({
icon: 'none',
title: 'id 不能为空'
})
}
},
async onLoad(event) {
//获取文章id,通常 id 来自上一个页面
if (event.id) {
this.id = event.id
}
await this.getArticleInfo();
await this.getCmsArticleLog(this.id);
// 监听解锁内容事件
uni.$on('onUnlockContent', this.onUnlockContent)
console.log(audioContext, 'this.audioContext');
audioContext && audioContext.onError((res) => {
console.log('播放音频错误');
})
},
onUnload() {
// 页面卸载时,移除监听事件
uni.$off('onUnlockContent', this.onUnlockContent)
},
onPageScroll(e) {
// 根据滚动位置判断是否显示导航栏
if (e.scrollTop > 100) {
uni.setNavigationBarTitle({
title: this.title
})
} else {
uni.setNavigationBarTitle({
title: ''
})
}
},
async created() {
await this.getGiftList()
},
methods: {
setVoteValue(vote_id, value) {
this.selectVoteValue[vote_id].val = value;
},
async isSelectVote(vote_id) {
console.log(vote_id)
let user_id = this.userInfo._id;
let query = {
vote_id: vote_id,
user_id: user_id
}
const res = await uniCloud.importObject("article_info").isSelectVote(query);
console.log(res);
if (res.code === 200) {
this.$set(this.isSelectVoteValue, vote_id, res.isSelectVote);
}
},
isExpired(voteEndTime) {
if (!voteEndTime) return false; // 处理 null/undefined
const endTime = new Date(voteEndTime);
if (isNaN(endTime.getTime())) {
console.warn('Invalid voteEndTime:', voteEndTime);
return false; // 或根据业务需求返回 true
}
return new Date() >= endTime;
},
async safeToFixed(value, digits = 2) {
// 确保 value 是数字
const num = Number(value);
// 检查是否是有效数字
if (typeof num !== 'number' || isNaN(num) || !isFinite(num)) {
return '0'.padEnd(digits + 1, '.0'); // 返回 "0.00"
}
return num.toFixed(digits);
},
isCheckboxDisabled(arr, name) {
console.log(arr, name)
if (arr.length < 2 || arr.includes(name)) {
return false;
}
return true;
},
checkboxChange(e) {
console.log(this.selectVoteValue);
console.log(e)
},
groupChange(n) {
console.log('groupChange', n);
},
radioChange(n) {
console.log('radioChange', n);
},
formatDate(date, format) {
const map = {
'yyyy': date.getFullYear(),
'MM': String(date.getMonth() + 1).padStart(2, '0'),
'dd': String(date.getDate()).padStart(2, '0'),
'HH': String(date.getHours()).padStart(2, '0'),
'mm': String(date.getMinutes()).padStart(2, '0'),
'ss': String(date.getSeconds()).padStart(2, '0')
};
return format.replace(
/yyyy|MM|dd|HH|mm|ss/g,
matched => map[matched]
);
},
// 时间格式化
formatTime() {
return this.formatDate(new Date(), 'yyyy-MM-dd HH:mm');
},
async ttt(item) {
console.log(item)
console.log(this.isExpired(item.vote.datetime))
},
async monthlyGiftChartsListLoad(data) {
console.log(data);
this.monthlyGiftChartsList = this.loadType === 'loadMore' ? this.monthlyGiftChartsList.concat(data) :
data
this.loadType = null
},
async totalGiftChartsListLoad(data) {
console.log(data);
this.totalGiftChartsList = this.loadType === 'loadMore' ? this.totalGiftChartsList.concat(data) : data
this.loadType = null
},
dialogConfirm() {
console.log('点击确认')
},
dialogClose() {
console.log('点击关闭')
},
inputDialogToggle() {
this.$refs.inputDialog.open()
},
btn(item) {
if (item === 0) {
this.inputDialogToggle();
}
if ([1, 3, 4, 5, 6, 7].indexOf(item) !== -1) {
uni.switchTab({
url: "/uni_modules/uni-cms-article/pages/list/slist"
})
} else if (item === 2) {
let token = uni.getStorageSync("uni_id_token");
console.log(token);
if (!token) {
uni.navigateTo({
url: "/pages3/uni_modules/uni-id-pages/pages/login/login-withoutpwd?type=weixin"
})
} else {
uni.navigateTo({
url: "/pages3/uni_modules/uni-id-pages/pages/userinfo/userinfo?showLoginManage=true"
})
}
}
},
async listLoad(data) {
console.log(data);
const listData = data.map(item => {
if (typeof item.thumbnail === 'string') {
item.thumbnail = [item.thumbnail]
}
return item
})
// 处理腾讯云文件链接
for (const article of listData) {
const parseImages = await parseImageUrl(article.thumbnail)
article.thumbnail = parseImages ? parseImages.map(image => image.src) : []
}
this.listData = this.loadType === 'loadMore' ? this.listData.concat(listData) : listData
this.loadType = null
},
// 初始化导航栏大小
initNavBarSize() {
// 获取小程序导航栏按钮信息
// #ifdef MP-TOUTIAO
let menuButtonInfo = tt.getCustomButtonBoundingClientRect()
menuButtonInfo.width = menuButtonInfo.capsule.width // 小程序按钮区域中使用的按钮宽度
this.mpButtonLeftPlaceholderSize = menuButtonInfo.leftIcon.width + 10
// #endif
// #ifndef MP-TOUTIAO
let menuButtonInfo = uni.getMenuButtonBoundingClientRect()
// #endif
// 计算小程序导航栏按钮占位大小
this.mpButtonPlaceholderSize = menuButtonInfo.width + 10
// 获取系统信息,判断是否为 iOS 系统,设置导航栏高度
this.navBarHeight = uni.getSystemInfoSync().system.toLowerCase().includes('ios') ? 44 : 48
},
// 重试
retry() {
this.refresh()
},
// 刷新
async totalGiftChar_refresh() {
this.loadType = 'refresh'
console.log(this.$refs.totalGiftCharts);
this.$refs.totalGiftCharts.loadData({
clear: true
}, () => {
uni.stopPullDownRefresh()
// #ifdef APP-NVUE
this.showRefresh = false
// #endif
})
},
async monthlyGiftCharts_refresh() {
this.loadType = 'refresh'
this.$refs.monthlyGiftCharts.loadData({
clear: true
}, () => {
uni.stopPullDownRefresh()
// #ifdef APP-NVUE
this.showRefresh = false
// #endif
})
},
// 加载更多
totalGiftChar_loadMore() {
this.loadType = 'loadMore'
this.$refs.totalGiftChar.loadMore();
},
monthlyGiftCharts_loadMore() {
this.loadType = 'loadMore'
this.$refs.monthlyGiftCharts.loadMore();
},
monthlyGiftCharts_data_clear() {
this.loadType = 'clear'
this.$refs.monthlyGiftCharts.clear();
},
totalGiftChar__data_clear() {
this.loadType = 'clear'
this.$refs.totalGiftChar.clear();
},
// 查询出错
onqueryerror(e) {
console.error(e);
},
switchTab(tab) {
this.activeTab = tab
// 动态加载榜单数据...
},
openGiftCharts() {
this.$refs.giftCharts.open();
},
async getGiftList() {
try {
const res = await uniCloud.importObject("gift_info").getGiftList();
console.log(res);
this.giftList = res.data;
} catch (e) {
console.log(e);
}
},
async handleGift() {
try {
if (this.selectGift && this.selectGift !== {}) {
console.log(this.selectGift);
}
} catch (e) {
console.log(e)
}
},
setGiftItem(item, index) {
for (let i = 0; i < this.giftList.length; i++) {
this.giftList[i].is_select_gift = false;
}
this.giftList[index].is_select_gift = true;
this.selectGift = item;
console.log(this.selectGift)
console.log(this.selectGift._id === item._id);
},
getMore() {
console.log("规则");
},
toPersonalpage() {
console.log(this.user_id);
uni.navigateTo({
url: "/uni_modules/zetank-personalpage/pages/personalpage/personalpage?uid=" + this.user_id._id
})
},
giftChange(e) {
console.log(e)
},
chunkDataIntoGroups(originalArray) {
const MAX_PER_GROUP = 7;
const MAX_TOTAL = 21;
const result = [];
const workingArray = originalArray.slice(0, MAX_TOTAL);
for (let i = 0; i < workingArray.length; i += MAX_PER_GROUP) {
const group = workingArray.slice(i, i + MAX_PER_GROUP);
result.push(group);
}
return result;
},
async getCmsArticleLog(id) {
try {
if (this.userInfo._id) {
let res = await uniCloud.importObject("article_info").getCmsArticleLog({
article_id: id
});
console.log(res);
if (res.code === 200) {
if (res.cmsArticleLog.length > 0) {
this.cmsArticleLogList = this.chunkDataIntoGroups(res.cmsArticleLog);
}
}
console.log(this.cmsArticleLogList);
}
} catch (e) {
console.log(e.message);
}
},
async getArticleInfo() {
try {
if (this.userInfo._id && this.id) {
let res = await uniCloud.importObject("article_info").getInfo({
user_id: this.userInfo._id,
article_id: this.id
});
console.log(res);
if (res.code === 200) {
this.gridLst[0].select_flag = res.isLike;
this.gridLst[1].select_flag = res.isCollect;
}
} else {
console.log("未登录");
}
} catch (e) {
console.log(e.message);
}
},
async change(e) {
console.log(e);
let index = e.detail.index;
console.log(index);
if (this.gridLst[index].text === "点赞") {
if (!this.gridLst[index].select_flag) {
let res = await uniCloud.importObject("cms-articles-like").likeArticle({
user_id: this.userInfo._id,
article_id: this.id
});
console.log(res);
this.gridLst[index].value++;
} else {
let res = await uniCloud.importObject("cms-articles-like").relikeArticle({
user_id: this.userInfo._id,
article_id: this.id
});
console.log(res);
this.gridLst[index].value--;
}
} else if (this.gridLst[index].text === "收藏") {
if (!this.gridLst[index].select_flag) {
let res = await uniCloud.importObject("cms-articles-collect").collectArticle({
user_id: this.userInfo._id,
article_id: this.id
});
console.log(res);
this.gridLst[index].value++;
} else {
let res = await uniCloud.importObject("cms-articles-collect").recollectArticle({
user_id: this.userInfo._id,
article_id: this.id
});
console.log(res);
this.gridLst[index].value--;
}
} else if (this.gridLst[index].text === "打赏") {
console.log("点击了打赏");
this.$refs.popup.open("bottom")
}
this.gridLst[index].select_flag = !this.gridLst[index].select_flag;
},
async detailLog(id, uid) {
console.log(uid);
if (uid) {
let res = await uniCloud.importObject("uni-cms-articles").detailLog({
articleId: id,
userId: uid,
});
}
},
publishTime(timestamp) {
// console.log(translatePublishTime(timestamp))
return translatePublishTime(timestamp)
},
setReadHistory() {
const historyCache = uni.getStorageSync('readHistory') || []
const readHistory = historyCache.filter(item => item.article_id !== this.id)
readHistory.unshift({
article_id: this.id,
last_time: Date.now()
})
uni.setStorageSync('readHistory', readHistory)
},
async getVoteInfo(vote_id) {
let user_id = this.userInfo._id;
let query = {
user_id: user_id,
vote_id: vote_id
};
const res = await uniCloud.importObject("cms-vote").getVoteInfo(query);
if (res.code === 200) {
this.selectVoteValue[vote_id] = {
user_data: res.user_data,
all_data: res.all_data
};
}
},
async loadData(data) {
console.log(data);
this.gridLst[0].value = data.like_count;
this.gridLst[1].value = data.collect_count
this.gridLst[2].value = data.gift_count
this.title = data.title
this.user_id = data.user_id[0]
this.detail_music = data.music_url_id
this.temp_id = data.temp_id
if (data.edit_type === "mobile") {
for (let i = 0; i < data.cmsLst.length; i++) {
if (data.cmsLst[i].type === "vote") {
let vote_id = data.cmsLst[i].vote.vote_id;
await this.isSelectVote(vote_id);
if (data.cmsLst[i].vote.type === "单选(默认)") {
this.selectVoteValue[vote_id] = {
val: data.cmsLst[i].vote.voteLst[0].value
}
} else {
this.selectVoteValue[vote_id] = {
val: []
}
}
// console.log(vote_id, this.isSelectVoteValue[vote_id], this.isSelectVoteValue);
if (this.isSelectVoteValue[vote_id] === true) {
console.log("success");
await this.getVoteInfo(vote_id);
}
}
}
}
console.log(data.music_url_id, 'data-----------------------', this.detail_music);
this.setReadHistory()
await this.getMusicUrl()
await this.getTemp()
},
async getTemp() {
console.log(this.temp_id)
if (!this.temp_id) {
return
}
const temp = await uniCloud.importObject("uni-cms-articles").getTemp({
temp_id: this.temp_id
})
console.log(temp);
if (temp.code === 200) {
this.temp = temp.data;
}
},
async getMusicUrl() {
console.log(this.detail_music);
if (!this.detail_music) {
return
}
const url = await uniCloud.getTempFileURL({
fileList: [this.detail_music.music_url_id]
});
const musicPath = url.fileList[0].tempFileURL
audioContext['src'] = musicPath
if (this.isPlay && audioContext['src']) {
audioContext.play()
}
},
onPlay() {
if (this.isPlay) {
this.onStop()
return
}
this.isPlay = true
console.log(audioContext);
audioContext.play()
},
onStop() {
audioContext.pause();
audioContext.stop()
this.isPlay = false
},
async onUnlockContent() {
this.$refs.detail.loadData()
},
check() {
const token = uni.getStorageSync('uni_id_token');
const tokenExpired = uni.getStorageSync('uni_id_token_expired');
const now = Date.now();
// 检查 Token 存在性 + 有效期
if (token && now < tokenExpired) {
return true;
} else if (token && now >= tokenExpired) {
uni.showToast({
title: '登录已过期',
icon: 'none'
});
this.clearLoginState(); // 清理过期凭证
return false;
}
},
checkLogin() {
// 防抖处理(防止连续触发)
if (this._lastCheckTime && Date.now() - this._lastCheckTime < 1500) return false;
this._lastCheckTime = Date.now();
try {
// 增强校验逻辑(结合本地存储和服务端验证)
const token = uni.getStorageSync('uni_id_token');
const isLogin = !!token && this.verifyTokenExpiration(token);
if (isLogin) return true;
// 优化提示交互(带图标和动画)
uni.showModal({
title: '?? 登录提示',
content: '体验完整功能需要先登录哦~',
confirmText: '立即登录',
cancelText: '稍后再说',
confirmColor: '#007AFF',
animationType: 'slide-in-bottom',
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: "/pages/login/login"
})
}
}
});
return false;
} catch (error) {
uni.showToast({
title: '登录状态异常,请重新登录',
icon: 'error'
});
this.clearInvalidSession();
return false;
}
},
// 新增 Token 校验方法
verifyTokenExpiration(token) {
return true;
},
// 新增会话清理
clearInvalidSession() {
uni.removeStorageSync('uni_id_token');
},
// 输入框聚焦
focusOn() {
this.checkLogin();
},
async getTreeComment(root_id) {
try {
let query = {
root_id: root_id
};
const res = await uniCloud.importObject("comments").getReplyComment(query);
console.log(res.data.commentList)
const index = this.commentData.comment.findIndex(
item => item._id === root_id
)
console.log(1, this.commentData, index)
if (index !== -1) {
console.log(this.commentData.comment);
this.commentData.comment[index].children = [...this.commentData.comment[index].children, ...res
.data.commentList
];
this.commentData.comment[index].replyCount -= res.data.commentList.length;
console.log(this.commentData.comment[index])
}
} catch (error) {
console.error('获取评论失败:', error);
// 设置默认值或提示用户
this.commentData = {
readNumer: 0,
commentSize: 0,
comment: []
};
uni.showToast({
title: '评论加载失败,请重试',
icon: 'none'
});
}
},
async getComment() {
try {
console.log(this.id);
console.log('开始获取评论');
let query = {
article_id: this.id
};
const res = await uniCloud.importObject("comments").getComments(query);
const readNumer_res = await uniCloud.importObject("comments").getReadCount(query);
// 处理正常响应
this.commentData = {
readNumer: readNumer_res.data.view_count,
commentSize: res.data.commentList.length,
comment: res.data.commentList
};
} catch (error) {
console.error('获取评论失败:', error);
// 设置默认值或提示用户
this.commentData = {
readNumer: 0,
commentSize: 0,
comment: []
};
uni.showToast({
title: '评论加载失败,请重试',
icon: 'none'
});
}
},
// 新增评论
async add(content) {
try {
// 获取当前用户信息(需确保已登录)
const uniIdToken = uni.getStorageSync('uni_id_token');
if (!uniIdToken) {
uni.showToast({
title: '请先登录',
icon: 'none'
});
return;
}
console.log(this.id);
// 构建符合后端接口规范的评论对象
const comment = {
article_id: this.id,
content: content,
parent_id: 0,
};
// 调用云对象方法
const commentObj = uniCloud.importObject("comments");
const res = await commentObj.addComment(comment);
// 处理响应结果
if (res.code === 200) {
uni.showToast({
title: '评论成功'
});
this.commentData.comment.splice(0, 0, res.data);
this.commentData.commentSize++;
}
} catch (e) {
console.error('评论失败:', e);
// 错误分类处理(根据后端错误码映射)
const errorHandler = {
'PERMISSION_DENIED': '登录状态失效,请重新登录',
'INVALID_PARAMS': '评论内容不能为空',
'CONTENT_BLOCKED': '包含违禁词汇: ' + e.message.split(':')[1],
'PARENT_COMMENT_NOT_FOUND': '回复的评论不存在'
};
uni.showToast({
title: errorHandler[e.errorType] || '评论失败,请稍后重试',
icon: 'none'
});
}
},
async selectVote(item) {
console.log(item);
let user_id = this.userInfo._id;
console.log(user_id);
let vote_id = item.vote.vote_id;
let query = {
vote_id: vote_id,
user_id: user_id,
selectValue: this.selectVoteValue[vote_id].val,
type: item.vote.type
}
const res = await uniCloud.importObject("cms-vote").add_vote(query);
if (res.code === 200) {
await this.getVoteInfo(vote_id);
this.isSelectVoteValue[vote_id] = true;
}
},
checkUrlProtocol(url) {
try {
const protocolSeparatorIndex = url.indexOf('://');
if (protocolSeparatorIndex === -1) {
return 'invalid';
}
const protocol = url.substring(0, protocolSeparatorIndex).toLowerCase();
if (protocol === 'http' || protocol === 'https') {
return 'http/https';
} else if (protocol === 'cloud') {
return 'cloud';
} else {
return 'other';
}
} catch (error) {
return 'invalid';
}
},
async like(comment) {
if (!this.checkLogin()) {
return;
}
// 防抖处理(500ms间隔)
if (this._lastClickTime && Date.now() - this._lastClickTime < 500) {
uni.vibrateShort();
return;
}
this._lastClickTime = Date.now();
try {
// 显示加载状态
let commentId = comment._id;
let userInfo = uniCloud.getCurrentUserInfo();
console.log(userInfo);
let uid = userInfo.uid;
const commentObj = uniCloud.importObject("comments");
const res = await commentObj.likeComment(uid, commentId);
// 更新UI状态(带动画效果)
this.$refs.hbComment.likeComplete(comment);
// 视觉反馈
} catch (error) {
console.error(error);
// 错误处理
uni.showToast({
title: error.message || '操作失败',
icon: 'error'
});
} finally {
this.reqFlag = false;
}
},
async relike(comment) {
if (!this.checkLogin()) {
return;
}
// 防抖处理(500ms间隔)
if (this._lastClickTime && Date.now() - this._lastClickTime < 500) {
uni.vibrateShort();
return;
}
this._lastClickTime = Date.now();
try {
// 显示加载状态
let commentId = comment._id;
let userInfo = uniCloud.getCurrentUserInfo();
console.log(userInfo);
let uid = userInfo.uid;
const commentObj = uniCloud.importObject("comments");
const res = await commentObj.relikeComment(uid, commentId);
// 更新UI状态(带动画效果)
this.$refs.hbComment.likeComplete(comment);
// 视觉反馈
} catch (error) {
console.error(error);
// 错误处理
uni.showToast({
title: error.message || '操作失败',
icon: 'error'
});
} finally {
this.reqFlag = false;
}
},
async replyAdd(commentReq) {
try {
let content = commentReq.content;
// 获取当前用户信息(需确保已登录)
const uniIdToken = uni.getStorageSync('uni_id_token');
if (!uniIdToken) {
uni.showToast({
title: '请先登录',
icon: 'none'
});
return;
}
// return;
// 构建符合后端接口规范的评论对象
const comment = {
article_id: this.id,
root_id: commentReq.root_id,
content: commentReq.content,
parent_id: commentReq.pId
};
console.log(comment)
// 调用云对象方法
const commentObj = uniCloud.importObject("comments");
const res = await commentObj.replyComment(comment);
// 处理响应结果
if (res.code === 200) {
uni.showToast({
title: '评论成功'
});
// this.commentData.comment.splice(0, 0, res.data);
const index = this.commentData.comment.findIndex(
item => item._id === commentReq.root_id
);
console.log(this.commentData);
console.log(index);
if (index !== -1) {
this.commentData.comment[index].children.splice(0, 0, res.data)
this.commentData.commentSize++;
}
console.log(this.commentData.comment);
}
} catch (e) {
console.error('评论失败:', e);
const errorHandler = {
'PERMISSION_DENIED': '登录状态失效,请重新登录',
'INVALID_PARAMS': '评论内容不能为空',
'CONTENT_BLOCKED': '包含违禁词汇: ' + e.message.split(':')[1],
'PARENT_COMMENT_NOT_FOUND': '回复的评论不存在'
};
uni.showToast({
title: errorHandler[e.errorType] || '评论失败,请稍后重试',
icon: 'none'
});
}
},
async del(item) {
try {
if (this.reqFlag) {
uni.showToast({
title: '操作受限',
icon: 'none'
});
return;
}
console.log(item);
this.reqFlag = true;
uni.hideToast();
uni.showLoading({
title: '安全检查中...',
mask: true
});
const {
confirm
} = await uni.showModal({
title: '删除确认',
content: '确定删除这条评论?删除后不可恢复',
confirmText: '确认删除',
confirmColor: '#dd514d'
});
if (!confirm) {
this._resetOperationState();
return;
}
let commentId = item.id;
uni.showLoading({
title: '数据删除中...',
mask: true
});
const commentObj = uniCloud.importObject("comments");
const res = await commentObj.deleteComment(commentId, item.root_id);
console.log(res);
console.log(this.commentData.comment);
if (item.root_id === 0) {
const index = this.commentData.comment.findIndex(
item => item._id === commentId
);
if (index !== -1) {
this.commentData.comment.splice(index, 1);
}
this.commentData.commentSize--;
uni.showLoading({
title: '删除成功',
mask: true
});
} else {
const index = this.commentData.comment.findIndex(
i => i._id === item.root_id
);
if (index !== -1) {
let comment = this.commentData.comment[index].children;
const index2 = comment.findIndex(
i => i._id === item.root_id
);
this.commentData.comment[index].children.splice(index2, 1);
uni.showLoading({
title: '删除成功',
mask: true
});
}
}
} catch (error) {
this._handleDeleteError(error);
this.$refs.commentList?.refresh();
} finally {
this._resetOperationState();
}
},
toLogin() {
uni.navigateTo({
url: "/pages3/uni_modules/uni-id-pages/pages/login/login-withoutpwd?type=weixin"
})
},
_updateLocalData(commentId, serverData) {
this.$store.commit('comments/REMOVE', commentId);
this.$store.commit('articles/UPDATE_STATS', {
id: serverData.article_id,
type: 'comment',
value: -1
});
},
// 错误处理(网页4/6)
_handleDeleteError(error) {
const errorConfig = {
'PERMISSION_DENIED': {
title: '操作权限不足',
icon: 'none'
},
'COMMENT_NOT_FOUND': {
title: '内容已删除',
icon: 'none'
},
'NETWORK_ERROR': {
title: '网络不稳定',
icon: 'none'
},
'default': {
title: error.errMsg || '操作失败',
icon: 'none'
}
};
console.log(error);
uni.showToast(errorConfig[error.code] || errorConfig.default);
},
inputDialogToggle() {
this.$refs.inputDialog.open()
},
btn(item) {
if (item === 0) {
this.inputDialogToggle();
} else if ([1, 3, 4, 5, 6, 7].indexOf(item)) {
uni.switchTab({
url: "/uni_modules/uni-cms-article/pages/list/slist"
})
} else if (item === 2) {
let token = uni.getStorageSync("uni_id_token");
console.log(token);
if (!token) {
uni.navigateTo({
url: "/pages3/uni_modules/uni-id-pages/pages/login/login-withoutpwd?type=weixin"
})
} else {
uni.navigateTo({
url: "/pages3/uni_modules/uni-id-pages/pages/userinfo/userinfo?showLoginManage=true"
})
}
}
},
_resetOperationState() {
try {
uni.hideLoading();
} catch (e) {}
this.reqFlag = false;
},
getTree(data) {
let result = [];
let map = {};
data.forEach(item => {
map[item.id] = item;
});
data.forEach(item => {
let parent = map[item.parentId];
if (parent) {
(parent.children || (parent.children = [])).push(item);
} else {
result.push(item);
}
});
return result;
},
complaint() {
}
}
}
</script>
<style scoped lang="scss">
page {
position: relative;
}
.fixed-music-area {
position: fixed;
top: 24rpx;
right: 24rpx;
display: flex;
align-items: center;
justify-content: center;
width: 50rpx;
height: 50rpx;
border-radius: 50%;
background-color: #c44;
}
/* #ifdef APP-NVUE */
.article {
background-color: #fff;
}
/* #endif */
@mixin cp {
padding: 0 30rpx;
}
.detail-loading {
margin: 100rpx auto 0;
width: 35px;
height: 35px;
animation: rotate360 2s linear infinite;
}
@keyframes rotate360 {
0% {
transform: rotate(0deg);
transform-origin: center center;
}
100% {
transform: rotate(360deg);
transform-origin: center center;
}
}
.meta {
@include cp;
position: relative;
z-index: 1;
padding-top: 20rpx;
.title {
.text {
font-size: 40rpx;
line-height: 66rpx;
font-weight: bold;
color: #333;
}
}
.excerpt {
margin-top: 10rpx;
.text {
font-size: 26rpx;
line-height: 40rpx;
color: #999;
}
}
.author {
display: flex;
align-items: center;
justify-content: flex-start;
flex-direction: row;
margin-top: 20rpx;
.at,
.split,
.date {
font-size: 26rpx;
color: #ccc;
}
.split {
margin: 0 10rpx;
}
}
}
.example-body {
/* #ifndef APP-PLUS-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: flex-start;
/* align-items: flex-end; */
/* flex-wrap: wrap; */
padding: 10rpx;
}
.btn {
text-align: center;
color: #fff;
padding: 20rpx;
margin: 20rpx;
border-radius: 20rpx;
background-color: #2979ff;
}
.tab {
text-indent: 2rem;
}
.v-preview {
.img {
width: 100%;
}
.btn-area {
position: relative;
top: -50rpx;
}
.red-btn-area {
display: flex;
justify-content: center;
padding: 10rpx 0;
margin: 0 40rpx 20rpx 40rpx;
font-size: 20rpx;
.red-btn {
position: relative;
color: #fff;
font-weight: 700;
font-size: 24rpx;
height: 40rpx;
line-height: 40rpx;
margin: 0;
background-image: linear-gradient(to bottom, #c11c1f, #5c0201);
+.red-btn {
margin-left: 20rpx;
}
}
}
.red-btn-plain-area {
display: flex;
justify-content: center;
padding: 10rpx 0;
margin: 0 40rpx;
font-size: 21rpx;
border-top: 2px solid;
border-bottom: 2px solid;
border-image: linear-gradient(to right,
#fff,
#f44336bf,
#b13636,
#f44336bf,
#fff) 1;
.red-btn-plain {
position: relative;
color: #b13636;
font-weight: 700;
padding-left: 15rpx;
+.red-btn-plain {
margin-left: 10rpx;
&::before {
// border-left: 2px solid #f44336;
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 2px;
height: 20rpx;
background-color: #b13636;
}
}
}
}
}
.game_content .person_info:nth-child(1) .person_detail .user_avatar_box .rank_box {
background-image: url();
}
.game_content .person_info:nth-child(2) .person_detail .user_avatar_box .rank_box {
background-image: url();
}
.game_content .person_info:nth-child(3) .person_detail .user_avatar_box .rank_box {
background-image: url();
}
.image {
width: 150rpx;
height: 150rpx;
}
.text {
font-size: 14px;
margin-top: 5px;
}
.example-body {
/* #ifndef APP-NVUE */
// display: block;
/* #endif */
}
.grid-dynamic-box {
margin-bottom: 15px;
}
.grid-item-box {
flex: 1;
// position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
align-items: center;
justify-content: center;
padding: 15px 0;
}
.grid-item-box-row {
flex: 1;
// position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: center;
padding: 15px 0;
}
.grid-dot {
position: absolute;
top: 5px;
right: 15px;
}
.swiper {
height: 600rpx;
}
.gifImage {
width: 100rpx;
height: 100rpx;
}
.swiper-box {
height: 800rpx;
}
.swiper-item {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
align-items: center;
height: 200px;
color: #fff;
}
.grid-container {
.grid-item {
width: calc(98% / 3);
}
display: flex;
/* 启用 Flex 布局 */
flex-wrap: wrap;
/* 允许换行 */
box-sizing: border-box;
/* 包含 padding 计算 */
padding: 10rpx;
/* 容器内边距(控制边缘间距) */
width: 100%;
/* 确保容器宽度占满父元素 */
}
/* 外层容器 */
.wallet-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 30rpx;
background: #ffffff;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
border-radius: 16rpx;
margin: 20rpx;
}
/* 钱包信息 */
.wallet-info {
font-size: 28rpx;
color: #333;
font-weight: 500;
flex: 1;
/* 允许内容区域自动扩展 */
}
/* 按钮样式 */
.gift-btn {
background: #2979ff;
color: #fff;
border-radius: 40rpx;
padding: 12rpx 32rpx;
font-size: 26rpx;
margin-left: 20rpx;
border: none;
transition: all 0.2s;
}
.select-gift {
border: #e80 solid 1rpx;
}
/* 处理iOS弹性盒bug */
.ios-fix {
flex-shrink: 0;
}
/* 微信小程序特殊适配 */
:deep(.wx-comp) {
display: flex;
box-sizing: border-box;
}
/* 点击态反馈 */
.btn:active {
transform: scale(0.95);
opacity: 0.8;
transition: all 0.2s ease;
}
.active {
border-bottom: #3cc solid 5rpx;
}
.tab-item {
text-align: center;
font-size: 32rpx;
line-height: 100rpx;
color: #c44;
}
/* 弹窗容器 */
.giftCharts {
min-height: 800rpx;
height: 100%;
}
/* 弹性容器 */
.tab-container {
display: flex;
justify-content: space-around;
position: relative;
padding-bottom: 8rpx;
border-bottom: 2rpx solid #eee;
}
.btn-gift-charts {
text-align: center;
font-size: 32rpx;
line-height: 100rpx;
}
/* #ifndef APP-NVUE */
.pages view {
display: flex;
box-sizing: border-box;
flex-direction: column;
}
/* #endif */
.pages {
background-color: #FFFFFF;
}
.refresh {
text-align: center;
}
.nav-box {
background-color: #FFFFFF;
position: fixed;
top: 0;
left: 0;
right: 0;
/* #ifndef APP-PLUS */
z-index: 9;
/* #endif */
}
.pages .nav {
display: flex;
align-items: center;
flex-direction: row;
}
.uni-search-box {
flex: 1;
padding: 0 10px;
}
.uni-search-box :deep(.uni-searchbar) {
padding: 0;
}
.uni-search-box :deep(.uni-searchbar__box) {
height: 32px;
flex-direction: row;
}
.cover-search-bar {
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
/* #ifndef APP-NVUE */
z-index: 999;
/* #endif */
}
.pages .uni-list :deep(.uni-list-item__container) {
flex-direction: row;
width: 100%;
}
.pages .uni-list :deep(.uni-list-item) {
align-items: flex-start;
}
.pages .uni-list :deep(.uni-load-more) {
display: flex;
}
.pages .uni-list :deep(.uni-list--border:after) {
background-color: #f5f5f5;
}
.giftCharts-item {
display: flex;
align-items: center;
padding: 20rpx;
margin: 15rpx 0;
background: #fff;
border-radius: 12rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
}
[class$="-column"],
.order-index,
.gift-value {
padding: 0 15rpx;
font-size: 28rpx;
color: #333;
}
.order-index {
flex: 1;
text-align: center;
font-weight: bold;
color: #666;
}
.avatar-column {
flex: 1;
display: flex;
justify-content: center;
}
.avatar-img {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
border: 2rpx solid #f0f0f0;
}
.nickname-column {
flex: 4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-left: 20rpx;
}
.gift-value {
flex: 2;
text-align: right;
color: #e6a23c;
font-weight: 500;
}
@media (max-width: 480px) {
.nickname-column {
font-size: 24rpx;
}
.gift-value {
font-size: 26rpx;
}
}
.avatar {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
}
.grid-container {
display: flex;
gap: 10px;
/* 列间距 */
padding: 15px;
background: #f5f5f5;
}
.grid-item {
flex: 1;
/* 等分容器宽度 */
min-width: 0;
/* 防止内容溢出 */
aspect-ratio: 1;
/* 默认正方形 */
background: white;
border-radius: 8px;
overflow: hidden;
}
.page-container {
padding: 20rpx 0;
}
.rectangle-img {
width: 100%;
height: 100%;
object-fit: cover;
aspect-ratio: 16/9;
/* 设置长方形比例 */
}
.meta {
position: relative;
z-index: 1;
padding-top: 20rpx;
.title {
.text {
font-size: 40rpx;
line-height: 66rpx;
font-weight: bold;
color: #333;
}
}
.excerpt {
margin-top: 10rpx;
.text {
font-size: 26rpx;
line-height: 40rpx;
color: #999;
}
}
.author {
display: flex;
align-items: center;
justify-content: flex-start;
flex-direction: row;
margin-top: 20rpx;
.at,
.split,
.date {
font-size: 26rpx;
color: #ccc;
}
.split {
margin: 0 10rpx;
}
}
}
.body {
padding: 20rpx;
.cms-body {
display: flex;
min-height: 60hv;
width: 100wv;
}
}
.v-preview {
.img {
width: 100%;
}
.btn-area {
position: relative;
top: -50rpx;
}
.red-btn-area {
display: flex;
justify-content: center;
padding: 10rpx 0;
margin: 0 40rpx 20rpx 40rpx;
font-size: 20rpx;
.red-btn {
position: relative;
color: #fff;
font-weight: 700;
font-size: 24rpx;
height: 40rpx;
line-height: 40rpx;
margin: 0;
background-image: linear-gradient(to bottom, #c11c1f, #5c0201);
+.red-btn {
margin-left: 20rpx;
}
}
}
.red-btn-plain-area {
display: flex;
justify-content: center;
padding: 10rpx 0;
margin: 0 40rpx;
font-size: 21rpx;
border-top: 2px solid;
border-bottom: 2px solid;
border-image: linear-gradient(to right,
#fff,
#f44336bf,
#b13636,
#f44336bf,
#fff) 1;
.red-btn-plain {
position: relative;
color: #b13636;
font-weight: 700;
padding-left: 15rpx;
+.red-btn-plain {
margin-left: 10rpx;
&::before {
// border-left: 2px solid #f44336;
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 2px;
height: 20rpx;
background-color: #b13636;
}
}
}
}
}
.vote-container {
padding: 20rpx;
}
.vote-title {
font-size: 16px;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 10rpx;
}
.deadline {
font-size: 12px;
color: #999;
margin-bottom: 30rpx;
display: block;
}
.radio-group {
margin-bottom: 40rpx;
}
.option-item {
display: flex;
align-items: center;
margin: 15rpx 0;
}
.option-label {
margin-left: 20rpx;
font-size: 14px;
}
.vote-btn {
background-color: #2979FF;
color: white;
border-radius: 8rpx;
font-size: 14px;
margin-top: 30rpx;
&[disabled] {
background-color: #CCCCCC;
}
}
.progress-container {
display: flex;
align-items: center;
/* 垂直居中 */
width: 100%;
/* 父容器宽度 */
}
.progress {
flex: 1;
/* 占据剩余空间 */
margin-right: 16rpx;
/* 与右侧计数间隔 */
}
.count-text {
min-width: 80rpx;
/* 防止数字换行 */
text-align: right;
color: #666;
/* 文字颜色 */
font-size: 24rpx;
}
</style>