教程中心

APP一些代码接口数据

emlog前端调用函数代码
具体方法如下:

<div class="alert alert-primary">
    在需要显示导航的位置添加调用代码:<pre><code>&lt;?php nav_plugin_display();&gt;</code></pre>
分类字段修改方法:<pre><code>&lt;?php echo Ixc_field_set('分类ID',1,'字段','修改的内容'); ?&gt; </code></pre></div>
<div class="alert alert-primary">
在需要显示导航的位置添加调用代码:<pre><code>&lt;?php nav_plugin_display();&gt;</code></pre>
在需要显示导航的位置添加调用代码:<pre><code>&lt;?php echo Ixc_field_set('分类ID',1,'字段','修改的内容'); ?&gt; </code></pre>
</div>

 

APP

<template>
  <view class="detail-container">
    <!-- 加载中提示 -->
    <view class="loading" v-if="loading">
      <uni-loading type="circle" color="#3B82F6"></uni-loading>
    </view>

    <!-- 密码输入框(最高优先级) -->
    <view class="password-container" v-if="needPassword && !pwdVerified">
      <view class="password-tip">
        <view class="tip-title">该文章需要密码访问</view>
        
        <input 
          v-model="articlePwd" 
          type="password" 
          placeholder="请输入文章密码"
          class="pwd-input"
          @confirm="verifyPassword"
          autocomplete="new-password"
        />
        
        <button @click="verifyPassword" class="verify-btn">验证密码</button>
        <view class="pwd-error" v-if="pwdError">{{pwdErrorMsg}}</view>
      </view>
    </view>

    <!-- 文章内容(密码验证通过后显示) -->
    <view class="article-content" v-else-if="article && pwdVerified">
      <!-- 文章标题 -->
      <view class="article-title">{{article.title}}</view>

      <!-- 文章元信息(仅保留作者信息) -->
      <view class="article-meta">
        <view class="meta-left">
          <image class="author-avatar" :src="formatAvatar(article.author_avatar)" mode="widthFix"></image>
          <text class="author-name">{{article.author_name}}</text>
        </view>
      </view>

      <!-- 文章封面图 -->
      <view class="article-cover" v-if="article.cover">
        <image :src="formatImageUrl(article.cover)" mode="widthFix" class="cover-image"></image>
      </view>

      <!-- 文章详情内容容器 -->
      <view class="article-content-container">
        <view class="article-body" v-html="processedContent"></view>
      </view>

      <!-- 文章标签 -->
      <view class="article-tags" v-if="article.tags && article.tags.length > 0">
        <text class="tag-title">标签:</text>
        <view class="tag-list">
          <view class="tag-item" v-for="(tag, idx) in article.tags" :key="idx">
            {{tag.name}}
          </view>
        </view>
      </view>

      <!-- 文末添加发布时间和阅读量 -->
      <view class="article-stats">
        <text class="stat-item">发布时间:{{article.date}}</text>
        <text class="stat-item">阅读量:{{article.views}}</text>
      </view>

      <!-- 评论区 -->
      <view class="article-comments">
        <view class="comments-title">评论列表</view>
        
        <!-- 评论输入框 -->
        <view class="comment-input-area" v-if="isLogin">
          <textarea 
            class="comment-input" 
            v-model="commentContent" 
            placeholder="写下你的评论..."
            maxlength="200"
          ></textarea>
          <button class="send-comment" @click="sendArticleComment" :style="{'background-color': qcolor}">
            发送评论
          </button>
        </view>
        <isLogin v-if="!isLogin && article"></isLogin>
        
        <!-- 评论列表 -->
        <view class="comments-list">
          <commentList 
            :list="comments" 
            :gid="articleId" 
            @toComment="openReplyComment"
          ></commentList>
          <view class="load-more" @click="loadMoreComments" v-if="hasMoreComments">
            加载更多评论
          </view>
          <view class="no-comments" v-if="comments.length === 0 && !hasMoreComments">
            暂无评论,快来抢沙发吧~
          </view>
        </view>
      </view>
    </view>

    <!-- 加载失败提示(非密码问题) -->
    <view class="load-fail" v-else-if="!loading && !needPassword">
      <text class="fail-text">文章加载失败,请重试</text>
      <button @click="getArticleDetail" class="retry-btn">重新加载</button>
    </view>

    <!-- 评论弹窗 -->
    <fixedView v-if="fixedViewTwo" title="回复评论" @openFixedView="openFixedViewTwo">
      <view class="comment—list" v-if="isLogin">
        <textarea class="comment-content" v-model="replyContent" auto-height maxlength="200"
          placeholder="请输入回复内容..."></textarea>
        <view class="sendComment" @click="sendReply" :style="{'--color':qcolor}">
          提交回复
        </view>
      </view>
      <isLogin v-if="!isLogin"></isLogin>
    </fixedView>
  </view>
</template>

<script>
import { myRequest, htRequest } from '@/api.js';
import { mapState } from "vuex";
import commentList from '@/components/commentList/commentList.vue';
import isLogin from '@/components/isLogin/isLogin.vue';
import fixedView from '@/components/fixedView/fixedView.vue';

export default {
  components: {
    commentList,
    isLogin,
    fixedView
  },
  data() {
    return {
      loading: true,          // 加载状态
      article: null,          // 文章详情数据
      articleId: '',          // 文章ID
      articlePwd: '',         // 文章密码(双向绑定变量)
      pwdVerified: false,     // 密码是否验证通过
      needPassword: false,    // 是否需要密码
      pwdError: false,        // 密码错误标记
      pwdErrorMsg: '',        // 密码错误提示
      processedContent: '',   // 处理后的文章内容(含图片路径修复)
      
      // 评论相关
      comments: [],           // 评论列表
      commentPage: 1,         // 评论分页
      commentContent: '',     // 评论内容
      hasMoreComments: true,  // 是否有更多评论
      fixedViewTwo: false,    // 回复弹窗状态
      replyContent: '',       // 回复内容
      replyPid: 0             // 回复目标ID
    };
  },
  computed: {
    ...mapState(['options', 'qcolor', 'isLogin', 'user'])
  },
  onLoad(options) {
    this.articleId = options.id || '';
    this.resetPasswordState();
    
    if (this.articleId) {
      this.getArticleDetail();
    } else {
      uni.showToast({ title: '文章ID错误', icon: 'none' });
      this.loading = false;
    }
  },
  methods: {
    // 重置密码相关状态
    resetPasswordState() {
      this.articlePwd = '';
      this.pwdVerified = false;
      this.needPassword = false;
      this.pwdError = false;
      this.pwdErrorMsg = '';
    },
    
    // 格式化头像路径
    formatAvatar(avatar) {
      if (!avatar) return '/static/header.png';
      if (this.options?.blogurl && !avatar.startsWith('http')) {
        return this.options.blogurl + avatar;
      }
      return avatar;
    },
    
    // 格式化图片路径
    formatImageUrl(url) {
      if (!url) return '/static/default-img.png'; // 补充默认图
      // 处理相对路径和绝对路径
      if (!url.startsWith('http') && this.options?.blogurl) {
        // 避免双斜杠问题,统一拼接规则
        const baseUrl = this.options.blogurl.replace(/\/$/, ''); // 移除末尾斜杠
        const imgPath = url.startsWith('/') ? url.slice(1) : url; // 移除开头斜杠
        return `${baseUrl}/${imgPath}`;
      }
      // 检查路径是否有效(简单判断)
      if (url.includes('//') && !url.startsWith('http')) {
        return `https:${url}`; // 补充协议
      }
      return url;
    }, // 关键修复:添加逗号分隔符
    
    // 获取文章详情
    async getArticleDetail() {
      try {
        this.loading = true;
        this.pwdError = false;
        
        const params = { id: this.articleId };
        if (this.articlePwd) {
          params.password = this.articlePwd;
        }
        
        const res = await myRequest({
          url: '/?rest-api=article_detail',
          method: 'GET',
          data: params
        });
        
        if (res.data.code === 0) {
          this.article = res.data.data.article;
          this.needPassword = this.article.need_pwd === 'y';
          
          if (!this.needPassword || this.articlePwd) {
            this.pwdVerified = true;
            // 处理文章内容中的图片路径
            this.processedContent = this.article.content.replace(
              /<img[^>]+src="([^"]+)"/g,
              (match, src) => {
                const formattedSrc = this.formatImageUrl(src);
                return match.replace(src, formattedSrc);
              }
            );
            // 加载评论
            this.getArticleComments();
          }
        } else {
          if (res.data.msg.includes('密码') || res.data.msg.includes('password')) {
            this.needPassword = true;
            this.pwdError = true;
            this.pwdErrorMsg = res.data.msg || '密码错误,请重新输入';
          } else {
            this.needPassword = false;
            uni.showToast({ title: res.data.msg || '获取文章失败', icon: 'none' });
          }
        }
      } catch (err) {
        console.error('获取文章详情失败:', err);
        this.needPassword = false;
        uni.showToast({ title: '网络错误,请重试', icon: 'none' });
      } finally {
        this.loading = false;
      }
    },
    
    // 验证密码
    verifyPassword() {
      if (!this.articlePwd) {
        uni.showToast({ title: '请输入密码', icon: 'none' });
        return;
      }
      this.getArticleDetail();
    },
    
    // 获取文章评论
    async getArticleComments() {
      try {
        const res = await htRequest({
          url: "/content/plugins/ForumSetting/manyApi.php?route=comments",
          method: 'get',
          data: {
            gid: this.articleId,
            page: this.commentPage
          },
        })
        
        if (res.data.data.list && res.data.data.list.length > 0) {
          this.comments = this.commentPage === 1 
            ? res.data.data.list 
            : [...this.comments, ...res.data.data.list];
          this.hasMoreComments = true;
        } else {
          this.hasMoreComments = false;
        }
      } catch (err) {
        console.error('获取评论失败:', err);
      }
    },
    
    // 加载更多评论
    loadMoreComments() {
      if (!this.hasMoreComments) return;
      this.commentPage++;
      this.getArticleComments();
    },
    
    // 发送评论
    async sendArticleComment() {
      if (!this.commentContent.trim()) {
        uni.showToast({ title: '请输入评论内容', icon: 'none' });
        return;
      }
      
      uni.showLoading({ title: '提交中' });
      try {
        const res = await htRequest({
          url: "/index.php?action=addcom",
          method: 'POST',
          data: {
            gid: this.articleId,
            comname: this.user.nickname,
            comment: this.commentContent,
            commail: '',
            pid: 0,
            comurl: '',
            imgcode: '',
            resp: 'json'
          },
        })
        
        if (res.data.code == 1) {
          uni.showToast({ title: '评论成功', icon: 'success' });
          this.commentContent = '';
          this.commentPage = 1;
          this.getArticleComments(); // 重新加载评论
        } else {
          uni.showToast({ title: res.data.msg || '评论失败', icon: 'none' });
        }
      } catch (err) {
        console.error('发送评论失败:', err);
        uni.showToast({ title: '网络错误', icon: 'none' });
      } finally {
        uni.hideLoading();
      }
    },
    
    // 打开回复评论弹窗
    openReplyComment(type, id, pid) {
      if (!this.isLogin) {
        uni.showToast({ title: '请先登录', icon: 'none' });
        return;
      }
      this.replyPid = pid;
      this.replyContent = '';
      this.fixedViewTwo = true;
    },
    
    // 关闭回复弹窗
    openFixedViewTwo(type) {
      if (type === 'close') {
        this.fixedViewTwo = false;
      }
    },
    
    // 发送回复
    async sendReply() {
      if (!this.replyContent.trim()) {
        uni.showToast({ title: '请输入回复内容', icon: 'none' });
        return;
      }
      
      uni.showLoading({ title: '提交中' });
      try {
        const res = await htRequest({
          url: "/index.php?action=addcom",
          method: 'POST',
          data: {
            gid: this.articleId,
            comname: this.user.nickname,
            comment: this.replyContent,
            commail: '',
            pid: this.replyPid,
            comurl: '',
            imgcode: '',
            resp: 'json'
          },
        })
        
        if (res.data.code == 1) {
          uni.showToast({ title: '回复成功', icon: 'success' });
          this.fixedViewTwo = false;
          this.commentPage = 1;
          this.getArticleComments(); // 重新加载评论
        } else {
          uni.showToast({ title: res.data.msg || '回复失败', icon: 'none' });
        }
      } catch (err) {
        console.error('发送回复失败:', err);
        uni.showToast({ title: '网络错误', icon: 'none' });
      } finally {
        uni.hideLoading();
      }
    }
  }
};
</script>

<style scoped>
.detail-container {
  min-height: 100vh;
  background-color: #ffffff;
  padding: 20rpx;
  pointer-events: auto;
}

/* 加载中样式 */
.loading {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 500rpx;
}

/* 密码容器样式 */
.password-container {
  position: relative;
  z-index: 999;
  padding: 20rpx;
}

.password-tip {
  padding: 40rpx 30rpx;
  background-color: #fff8e6;
  border-radius: 10rpx;
  margin: 20rpx auto;
  text-align: center;
  max-width: 90%;
  box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.1);
}

.tip-title {
  font-size: 30rpx;
  color: #e6a23c;
  margin-bottom: 30rpx;
  display: block;
}

.pwd-input {
  width: 80%;
  height: 80rpx;
  line-height: 80rpx;
  margin: 0 auto 30rpx;
  padding: 0 20rpx;
  border: 1px solid #ffe58f;
  border-radius: 8rpx;
  font-size: 28rpx;
  background-color: #ffffff;
  pointer-events: auto;
  opacity: 1;
  z-index: 1000;
}

.pwd-input::placeholder {
  color: #ccc;
  font-size: 26rpx;
}

.verify-btn {
  width: 80%;
  height: 80rpx;
  line-height: 80rpx;
  font-size: 28rpx;
  margin: 0 auto 20rpx;
  background-color: #3B82F6;
  color: white;
  border: none;
  border-radius: 8rpx;
}

.verify-btn:active {
  background-color: #2c6ed6;
}

.pwd-error {
  font-size: 26rpx;
  color: #f56c6c;
  padding: 10rpx 0;
}

/* 文章内容样式 */
.article-content {
  padding: 10rpx 0;
}

.article-title {
  font-size: 36rpx;
  font-weight: bold;
  color: #333;
  line-height: 1.5;
  margin-bottom: 20rpx;
  padding: 0 10rpx;
}

.article-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15rpx 10rpx;
  border-bottom: 1px solid #f5f5f5;
  margin-bottom: 30rpx;
}

.author-avatar {
  width: 50rpx;
  height: 50rpx;
  border-radius: 50%;
  margin-right: 15rpx;
}

.author-name {
  font-size: 26rpx;
  color: #666;
}

.article-cover {
  width: 100%;
  margin: 30rpx 0;
  border-radius: 10rpx;
  overflow: hidden;
}

.cover-image {
  width: 100%;
  height: auto;
  display: block;
}

/* 文章内容容器样式 */
.article-content-container {
  padding: 20rpx 10rpx;
  margin: 20rpx 0;
  background-color: #fff;
  border-radius: 10rpx;
  box-shadow: 0 1rpx 5rpx rgba(0, 0, 0, 0.05);
}

/* 富文本内容样式 */
.article-body {
  font-size: 30rpx;
  line-height: 2;
  color: #333;
  word-break: break-word;
}

.article-body p {
  margin-bottom: 30rpx;
  text-align: justify;
}

.article-body img {
  max-width: 100% !important;
  height: auto !important;
  display: block;
  margin: 30rpx auto;
  border-radius: 8rpx;
  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}

.article-body h1,
.article-body h2,
.article-body h3,
.article-body h4,
.article-body h5,
.article-body h6 {
  font-weight: bold;
  margin: 40rpx 0 20rpx;
  color: #222;
}

.article-body h1 { font-size: 40rpx; }
.article-body h2 { font-size: 36rpx; }
.article-body h3 { font-size: 32rpx; }

.article-body code,
.article-body pre {
  font-family: monospace;
  background-color: #f8f8f8;
  border-radius: 6rpx;
  padding: 2rpx 8rpx;
  font-size: 28rpx;
  color: #e53935;
}

.article-body pre {
  padding: 20rpx;
  margin: 30rpx 0;
  overflow-x: auto;
  line-height: 1.8;
  color: #333;
  border-left: 4rpx solid #3B82F6;
}

.article-body ul,
.article-body ol {
  margin: 20rpx 0 20rpx 40rpx;
  padding-left: 20rpx;
}

.article-body li {
  margin-bottom: 15rpx;
}

.article-body a {
  color: #3B82F6;
  text-decoration: underline;
}

/* 标签样式 */
.article-tags {
  padding: 15rpx 10rpx;
  margin: 30rpx 0;
  border-top: 1px solid #f5f5f5;
}

.tag-title {
  font-size: 28rpx;
  color: #666;
  margin-right: 15rpx;
}

.tag-list {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 15rpx;
}

.tag-item {
  font-size: 26rpx;
  color: #3B82F6;
  background-color: #f0f7ff;
  padding: 5rpx 15rpx;
  border-radius: 20rpx;
}

/* 文末统计信息样式 */
.article-stats {
  padding: 20rpx 10rpx;
  margin: 20rpx 0;
  font-size: 26rpx;
  color: #666;
  border-top: 1px solid #f5f5f5;
  display: flex;
  justify-content: space-between;
}

.stat-item {
  display: flex;
  align-items: center;
}

.stat-item::before {
  content: '';
  width: 24rpx;
  height: 24rpx;
  margin-right: 8rpx;
  background-size: contain;
  background-repeat: no-repeat;
}

.stat-item:first-child::before {
  background-image: url('/static/time.png');
}

.stat-item:last-child::before {
  background-image: url('/static/view.png');
}

/* 评论区样式 */
.article-comments {
  padding: 20rpx 10rpx;
  margin-top: 30rpx;
  background-color: #fff;
  border-radius: 10rpx;
}

.comments-title {
  font-size: 32rpx;
  font-weight: bold;
  color: #333;
  margin-bottom: 20rpx;
  padding-bottom: 10rpx;
  border-bottom: 1px solid #f5f5f5;
}

.comment-input-area {
  display: flex;
  flex-direction: column;
  gap: 15rpx;
  margin-bottom: 30rpx;
}

.comment-input {
  min-height: 120rpx;
  padding: 15rpx;
  background-color: #f5f5f5;
  border-radius: 8rpx;
  font-size: 28rpx;
}

.send-comment {
  align-self: flex-end;
  padding: 10rpx 30rpx;
  color: white;
  border: none;
  border-radius: 8rpx;
  font-size: 28rpx;
}

.comments-list {
  margin-top: 20rpx;
}

.load-more {
  text-align: center;
  padding: 20rpx;
  color: #3B82F6;
  font-size: 28rpx;
  border-top: 1px solid #f5f5f5;
  margin-top: 15rpx;
}

.no-comments {
  text-align: center;
  padding: 40rpx 0;
  color: #999;
  font-size: 28rpx;
}

/* 回复弹窗样式 */
.comment-content {
  position: relative;
  z-index: 99;
  margin: auto;
  width: 90%;
  min-height: 150px;
  background-color: #eee;
  padding: 10px;
  border-radius: 10px;
}

.sendComment {
  width: 90%;
  margin: 10px auto;
  background-color: var(--color);
  text-align: center;
  border-radius: 5px;
  padding: 10px;
  color: white;
}

.load-fail {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 500rpx;
  gap: 30rpx;
}

.fail-text {
  font-size: 28rpx;
  color: #999;
}

.retry-btn {
  width: 300rpx;
  height: 80rpx;
  line-height: 80rpx;
  background-color: #f5f5f5;
  color: #333;
  border: none;
  border-radius: 8rpx;
}
</style>

APP2

<template>
  <view class="content" :style="{'--color':qcolor}">
    <!-- 头部搜索组件 -->
    <headerSearch></headerSearch>
    <!-- 轮播图组件 -->
    <swipera :lunbo="images"></swipera>
    <!-- 活跃用户 -->
    <titleUi title="活跃用户" right="true"></titleUi>
    <scroll-view class="scroll-view_H" scroll-x="true">
      <view class="horUser">
        <view class="user" v-for="item in hotUser" :key="item.id">
          <image class="user-logo" :src="reg(item.avatar)" mode=""></image>
          <view class="user-name">{{item.name}}</view>
        </view>
      </view>
    </scroll-view>

    <!-- 热门话题 -->
    <titleUi title="热门话题"></titleUi>
    <view class="tags">
      <view class="tag" v-for="(item,index) in tags" :key="index" @click="toTagInfo(item.tagname)">
        <image class="huati" src="/static/huati.png" mode=""></image>
        <view>{{item.tagname}}</view>
      </view>
    </view>

    <!-- 圈子列表(分类)+ 分类下5篇文章 -->
    <titleUi title="圈子列表"></titleUi>
    <!-- 分类列表容器 -->
    <view v-if="sorts.length > 0" class="sort-container">
      <view class="sort-item" v-for="(sort, sortIdx) in sorts" :key="sort.id">
        <!-- 分类标题和更多按钮 -->
        <view class="sort-title-bar">
          <view class="sort-title" :style="{'border-left-color':qcolor}">
            {{sort.sort_name || '未命名分类'}}
          </view>
          <view class="sort-more" @click="toCategoryAll(sort.id, sort.sort_name || '未命名分类')">
            更多 <image src="/static/more.png" class="more-icon"></image>
          </view>
        </view>
        
        <!-- 分类下文章列表 -->
        <view class="sort-article-list">
          <view class="loading" v-if="sort.loading">加载中...</view>
          <view class="no-data" v-else-if="sort.articles.length === 0">暂无文章</view>
          <view class="article-item" v-else v-for="(art, artIdx) in sort.articles" :key="art.id" @click="toArticleDetail(art.id)">
            <view class="article-title">{{art.title}}</view>
            <view class="article-meta">
              <text class="author">{{art.author_name}}</text>
              <text class="date">{{art.date}}</text>
            </view>
          </view>
        </view>
      </view>
    </view>
    
    <!-- 无分类提示 -->
    <view v-else class="no-category">
      暂无分类数据
    </view>

    <!-- 状态提示 -->
    <status :status="status"></status>
    <!-- 回到顶部按钮 -->
    <view v-if="backTopValue" class="xiaohuojian" @click="xhj">
      <image src="../../static/fanhuidingbu.png" mode=""></image>
    </view>
  </view>
</template>

<script>
import { myRequest, htRequest } from '@/api.js';
import { mapState, mapMutations } from "vuex";

export default {
  data() {
    return {
      status: 'none',
      page: 1,
      hotUser: [],
      backTopValue: false,
      images: [ 
        "http://cdn.hkiii.cn//img/_2022/06/21/09/52/42/167/6483441/13482961188039425428",
        "https://cdn.hkiii.cn/cg/11.jpeg",
        "http://cdn.hkiii.cn//img/_2022/06/21/09/52/42/172/6483441/13482961190153354896"
      ],
      tags: [], 
      sorts: [] // 分类数组(含对应文章)
    };
  },
  computed: {
    ...mapState(['isLogin', 'qcolor', 'user', 'options'])
  },
  onLoad() {
    this.getHotUser();
    this.getOpitions();
    this.getTags();
    this.getSorts(); // 核心:自动获取分类+分类下5篇文章
  },
  onShow() {
    this.updataColor();
  },
  onPullDownRefresh() {
    this.page = 1;
    this.getHotUser();
    this.getTags();
    this.getSorts();
    uni.stopPullDownRefresh();
  },
  methods: {
    ...mapMutations(['login', 'setOptions']),

    reg(e) {
      if (!e) return '/static/header.png';
      return e.replace(/content\/upload/gi, `${this.options?.blogurl || ''}content/upload`);
    },

    updataColor() {
      uni.setNavigationBarColor({
        backgroundColor: this.qcolor,
        frontColor: "#ffffff"
      });
    },

    xhj() {
      uni.pageScrollTo({ scrollTop: 0, duration: 300 });
    },

    async getHotUser() {
      try {
        const res = await htRequest({
          url: '/content/plugins/ForumSetting/manyApi.php',
          method: "GET",
          data: { route: 'hotUser' }
        });
        if (res.data.state === 1) this.hotUser = res.data.data;
      } catch (err) {
        console.error('获取活跃用户失败:', err);
      }
    },

    async getOpitions() {
      try {
        const res = await htRequest({
          url: '/content/plugins/ForumSetting/manyApi.php',
          method: "GET",
          data: { route: 'options' }
        });
        if (res.data.state === 1) this.setOptions(res.data.data);
      } catch (err) {
        console.error('获取系统设置失败:', err);
      }
    },

    async getTags() {
      try {
        const res = await htRequest({
          url: '/content/plugins/ForumSetting/manyApi.php',
          method: "GET",
          data: { route: 'tags', num: 20 }
        });
        if (res.data.state === 1) this.tags = res.data.data;
      } catch (err) {
        console.error('获取热门话题失败:', err);
      }
    },

    // 核心修改:适配article_list接口,自动获取分类及5篇最新文章
    async getSorts() {
      try {
        // 1. 先获取所有分类(通过sort_list接口)
        const sortRes = await myRequest({
          url: '/?rest-api=sort_list',
          method: 'GET',
          data: {} // 空参数=获取所有分类
        });
        
        // 校验分类接口返回(code=0为成功,适配接口格式)
        if (sortRes.data.code !== 0 || !sortRes.data.data?.sorts) {
          this.sorts = [];
          uni.showToast({ title: '获取分类失败', icon: 'none' });
          return;
        }
        
        const sorts = sortRes.data.data.sorts || [];
        this.sorts = []; // 清空旧数据,避免重复加载
        
        // 2. 遍历每个分类,调用article_list接口获取5篇最新文章
        for (const sort of sorts) {
          // 先添加分类到列表,显示加载状态
          this.sorts.push({
            id: sort.id,
            sort_name: sort.name || sort.sort_name, // 兼容分类接口可能的字段差异
            loading: true,
            articles: []
          });
          
          try {
            // 调用article_list接口(严格按你提供的接口参数配置)
            const articleRes = await myRequest({
              url: '/?rest-api=article_list',
              method: 'GET',
              data: {
                sort_id: sort.id,  // 核心参数:筛选当前分类的文章
                count: 5,         // 仅获取5篇文章
                page: 1,          // 第一页=最新文章
                order: 'new'      // 按时间倒序(最新优先,适配接口默认逻辑)
              }
            });
            
            // 适配article_list接口返回格式,提取文章数据
            if (articleRes.data.code === 0) {
              const articles = articleRes.data.data?.articles || [];
              // 更新当前分类的文章和加载状态
              const index = this.sorts.findIndex(item => item.id === sort.id);
              if (index !== -1) {
                this.sorts[index] = {
                  ...this.sorts[index],
                  loading: false,                // 结束加载
                  articles: articles,            // 赋值文章数据
                  sort_name: articles[0]?.sort_name || this.sorts[index].sort_name // 用文章返回的分类名补全(更精准)
                };
              }
            } else {
              // 文章接口返回错误(如无权限),更新状态
              const index = this.sorts.findIndex(item => item.id === sort.id);
              if (index !== -1) {
                this.sorts[index].loading = false;
                this.sorts[index].articles = [];
              }
            }
          } catch (err) {
            // 网络错误或接口异常,处理加载状态
            console.error(`获取分类【${sort.name}】文章失败:`, err);
            const index = this.sorts.findIndex(item => item.id === sort.id);
            if (index !== -1) {
              this.sorts[index].loading = false;
              this.sorts[index].articles = [];
            }
          }
        }
      } catch (err) {
        // 分类接口请求异常
        console.error('获取分类列表失败:', err);
        this.sorts = [];
      }
    },

    toTagInfo(tag) {
      uni.navigateTo({ url: "/pages/tagsInfo/tagsInfo?tag=" + tag });
    },

    toArticleDetail(articleId) {
      uni.navigateTo({
        url: `/pages/articleDetail/articleDetail?id=${articleId}`
      });
    },
    
    toCategoryAll(sortId, sortName) {
      uni.navigateTo({
        url: `/pages/categoryAll/categoryAll?sortId=${sortId}&sortName=${encodeURIComponent(sortName)}`
      });
    }
  }
};
</script>

<style scoped>
/* 页面基础样式 */
.content {
  width: 100%;
  min-height: 100vh;
  background-color: #f5f5f5;
}

/* 活跃用户样式 */
.scroll-view_H {
  width: 100%;
  white-space: nowrap;
  padding: 0 20rpx;
}
.horUser {
  display: flex;
  padding: 10rpx 0;
}
.user {
  padding: 10px;
  border-radius: 10px;
  text-align: center;
}
.user-logo {
  width: 45px;
  height: 45px;
  border-radius: 50%;
  margin: 0 auto;
}
.user-name {
  width: 50px;
  overflow: hidden;
  font-size: 12px;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: #8f8f94;
  margin-top: 5px;
}

/* 热门话题样式 */
.tags {
  display: flex;
  width: 90%;
  align-items: center;
  margin: 10px auto 0;
  flex-wrap: wrap;
}
.tag {
  border: #eee 1px solid;
  padding: 3px 5px;
  margin: 3px;
  font-size: 13px;
  border-radius: 20px;
  display: flex;
  align-items: center;
}
.huati {
  width: 13px;
  height: 13px;
  margin-right: 3px;
}

/* 分类列表样式 */
.sort-container {
  padding: 0 20rpx 20rpx;
}
.sort-item {
  margin-bottom: 30rpx;
  background-color: #fff;
  border-radius: 10rpx;
  padding: 20rpx;
}
.sort-title-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15rpx;
}
.sort-title {
  font-size: 16px;
  font-weight: bold;
  color: #333;
  border-left: 4px solid;
  padding-left: 10rpx;
}
.sort-more {
  font-size: 14px;
  color: #666;
  display: flex;
  align-items: center;
}
.more-icon {
  width: 16px;
  height: 16px;
  margin-left: 5rpx;
}

/* 无分类/无文章提示 */
.no-category {
  text-align: center;
  padding: 50rpx 0;
  color: #999;
  font-size: 28rpx;
}
.loading, .no-data {
  text-align: center;
  color: #999;
  font-size: 14px;
  padding: 20rpx 0;
}

/* 文章列表样式 */
.article-item {
  padding: 15rpx 0;
  border-bottom: 1px solid #f5f5f5;
}
.article-item:last-child {
  border-bottom: none;
}
.article-title {
  font-size: 15px;
  color: #333;
  margin-bottom: 8rpx;
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.article-meta {
  font-size: 12px;
  color: #999;
  display: flex;
  justify-content: space-between;
}

/* 回到顶部按钮 */
.xiaohuojian {
  width: 50px;
  height: 50px;
  position: fixed;
  bottom: 100px;
  right: 10px;
  z-index: 99;
}
.xiaohuojian image {
  width: 100%;
  height: 100%;
}
</style>

接口使用

数据接口:
活跃用户:route: 'hotUser', limit: 20
热门标签:route: 'hotTags'
热门文章:route: 'hotArticles', limit: 5

热门用户页面标注

<template>
  <view class="hot-users-page">
    <!-- 头部搜索组件(与首页完全对齐) -->
    <view class="search-container">
      <headerSearch></headerSearch>
    </view>
    
    <!-- 1. 活跃会员:完全同步首页“活跃用户”标题样式 -->
    <view class="hot-title-underline">活跃会员</view>
    <!-- 活跃用户容器:同步首页白色容器样式 -->
    <scroll-view class="hot-user-container">
      <view class="hot-user-list">
        <view class="hot-user-item" v-for="(user, index) in hotUser" :key="user.uid">
          <image class="hot-user-avatar" :src="reg(user.avatar)" mode="" lazy-load></image>
          <view class="hot-user-name">{{ user.name }}</view>
        </view>
      </view>
    </scroll-view>
    <view class="hot-no-data" v-if="hotUser.length === 0">暂无活跃用户</view>
    
    <!-- 2. 热门文章:同步首页“分类/昵称”标题样式 -->
    <view class="hot-title-vertical">热门文章</view>
    <view class="hot-article-container">
      <view class="hot-no-data">暂无热门文章</view>
    </view>
    
    <!-- 3. 热门标签:同步首页“标签”容器样式 -->
    <view class="hot-title-vertical">热门标签</view>
    <view class="hot-tag-container">
      <view class="hot-tag-item" v-for="(tag, index) in tags" :key="index" @click="toTagInfo(tag.name)">
        {{ tag.name }}
      </view>
      <view class="hot-no-data" v-if="tags.length === 0">暂无标签数据</view>
    </view>
  </view>
</template>

<script>
import { htRequest } from '@/api.js';
import { mapState } from 'vuex';

export default {
  data() {
    return {
      hotUser: [],
      tags: []
    };
  },
  computed: {
    ...mapState(['options', 'qcolor'])
  },
  onLoad() {
    this.getHotUser();
    this.getTags();
    // 同步首页主题色(确保颜色完全一致)
    document.documentElement.style.setProperty('--home-color', this.qcolor);
  },
  onShow() {
    this.setNavigationBarColor();
    document.documentElement.style.setProperty('--home-color', this.qcolor);
  },
  methods: {
    setNavigationBarColor() {
      uni.setNavigationBarColor({
        backgroundColor: this.qcolor,
        frontColor: "#ffffff",
        animation: { duration: 300, timingFunc: "easeInOut" }
      });
    },

    reg(e) {
      if (!e) return '/static/header.png';
      return e.replace(/content\/upload/gi, `${this.options?.blogurl || ''}content/upload`);
    },

    async getHotUser() {
      try {
        const res = await htRequest({
          url: '/content/plugins/ForumSetting/manyApi.php',
          method: "GET",
          data: { route: 'hotUser', num: 20 }
        });
        if (res.data.state === 1) this.hotUser = res.data.data;
      } catch (err) {
        console.error('获取活跃用户失败:', err);
      }
    },

    async getTags() {
      try {
        const res = await htRequest({
          url: '/content/plugins/ForumSetting/manyApi.php',
          method: "GET",
          data: { route: 'tags', num: 20 }
        });
        if (res.data.state === 1) this.tags = res.data.data;
      } catch (err) {
        console.error('获取热门标签失败:', err);
      }
    },

    toTagInfo(tag) {
      uni.navigateTo({ url: "/pages/tagsInfo/tagsInfo?tag=" + tag });
    }
  }
};
</script>

<style scoped>
/* 全局容器:同步首页body/根容器样式 */
.hot-users-page {
  padding: 0 !important; /* 替换为首页根容器的padding(如首页是10rpx就改10rpx) */
  background-color: #f5f5f5 !important; /* 替换为首页背景色 */
  min-height: 100vh !important;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; /* 同步首页字体 */
}

/* 搜索组件:完全同步首页搜索组件的外层样式 */
.search-container {
  padding-top: var(--status-bar-height) !important;
  background-color: #f5f5f5 !important; /* 替换为首页搜索区背景色 */
}
headerSearch {
  display: block !important;
  padding: 15rpx 20rpx !important; /* 替换为首页搜索组件的padding */
  margin: 0 !important; /* 替换为首页搜索组件的margin */
}

/* ———— 1. 活跃会员标题(下划线):同步首页“活跃用户”标题 ———— */
.hot-title-underline {
  /* 基础样式:复制首页活跃用户标题的font/margin */
  font-size: 32rpx !important; /* 替换为首页标题字体大小(如32rpx) */
  font-weight: 600 !important; /* 替换为首页标题字重(如bold/600) */
  color: #333 !important; /* 替换为首页标题颜色 */
  margin: 28rpx 24rpx 18rpx !important; /* 替换为首页标题的margin(上右下左) */
  padding-bottom: 10rpx !important; /* 替换为首页标题下划线的上间距 */
  
  /* 下划线:复制首页下划线样式 */
  position: relative !important;
  display: inline-block !important;
}
.hot-title-underline::after {
  content: '' !important;
  position: absolute !important;
  left: 0 !important;
  bottom: 0 !important;
  width: 100% !important; /* 替换为首页下划线宽度(如80%) */
  height: 4rpx !important; /* 替换为首页下划线厚度(如3rpx) */
  background-color: var(--home-color) !important; /* 与首页下划线颜色一致 */
  border-radius: 2rpx !important; /* 替换为首页下划线圆角(如1rpx) */
}

/* ———— 2. 热门文章/标签标题(竖杠):同步首页分类标题 ———— */
.hot-title-vertical {
  /* 基础样式:复制首页分类标题的font/margin */
  font-size: 32rpx !important; /* 替换为首页分类标题字体大小 */
  font-weight: 600 !important; /* 替换为首页分类标题字重 */
  color: #333 !important; /* 替换为首页分类标题颜色 */
  margin: 28rpx 24rpx 18rpx !important; /* 替换为首页分类标题的margin */
  
  /* 左侧竖杠:复制首页竖杠样式 */
  border-left: 4rpx solid var(--home-color) !important; /* 替换为首页竖杠厚度(如3rpx) */
  padding-left: 16rpx !important; /* 替换为首页竖杠与文字的间距(如15rpx) */
}

/* ———— 3. 活跃用户容器:同步首页白色容器 ———— */
.hot-user-container {
  width: calc(100% - 48rpx) !important; /* 替换为首页容器宽度(如calc(100% - 40rpx)) */
  white-space: nowrap !important;
  padding: 20rpx 0 !important; /* 替换为首页容器内边距 */
  background-color: #fff !important; /* 与首页容器背景色一致 */
  margin: 0 24rpx 20rpx !important; /* 替换为首页容器的margin */
  border-radius: 12rpx !important; /* 替换为首页容器圆角(如10rpx) */
  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.03) !important; /* 复制首页容器阴影(若有) */
}
.hot-user-list {
  display: flex !important;
  padding: 0 10rpx !important; /* 替换为首页用户列表内边距 */
}
.hot-user-item {
  padding: 12rpx 16rpx !important; /* 替换为首页用户卡片内边距 */
  text-align: center !important;
  flex-shrink: 0 !important;
}
.hot-user-avatar {
  width: 90rpx !important; /* 替换为首页头像宽度(如88rpx) */
  height: 90rpx !important; /* 替换为首页头像高度 */
  border-radius: 50% !important; /* 与首页头像圆角一致 */
  margin: 0 auto 10rpx !important; /* 替换为首页头像的margin(下间距) */
  display: block !important;
  border: 2rpx solid #f2f2f2 !important; /* 替换为首页头像边框(如1rpx) */
}
.hot-user-name {
  width: 80rpx !important; /* 替换为首页昵称宽度(如70rpx) */
  overflow: hidden !important;
  font-size: 24rpx !important; /* 替换为首页昵称字体大小(如22rpx) */
  white-space: nowrap !important;
  text-overflow: ellipsis !important;
  color: #888 !important; /* 替换为首页昵称颜色(如#999) */
}

/* ———— 4. 热门文章容器:同步首页文章容器 ———— */
.hot-article-container {
  width: calc(100% - 48rpx) !important; /* 替换为首页文章容器宽度 */
  background-color: #fff !important;
  padding: 40rpx 0 !important; /* 替换为首页文章容器内边距 */
  margin: 0 24rpx 20rpx !important; /* 替换为首页文章容器margin */
  border-radius: 12rpx !important; /* 替换为首页文章容器圆角 */
  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.03) !important; /* 复制首页阴影 */
}

/* ———— 5. 热门标签容器:同步首页标签容器 ———— */
.hot-tag-container {
  width: calc(100% - 48rpx) !important; /* 替换为首页标签容器宽度 */
  background-color: #fff !important;
  padding: 24rpx 20rpx !important; /* 替换为首页标签容器内边距 */
  margin: 0 24rpx 20rpx !important; /* 替换为首页标签容器margin */
  border-radius: 12rpx !important; /* 替换为首页标签容器圆角 */
  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.03) !important; /* 复制首页阴影 */
  display: flex !important;
  flex-wrap: wrap !important;
  gap: 16rpx !important; /* 替换为首页标签间距(如15rpx) */
}
.hot-tag-item {
  padding: 12rpx 24rpx !important; /* 替换为首页标签内边距 */
  background-color: #f7f7f7 !important; /* 替换为首页标签背景色 */
  border-radius: 30rpx !important; /* 替换为首页标签圆角 */
  font-size: 24rpx !important; /* 替换为首页标签字体大小 */
  color: #666 !important; /* 替换为首页标签颜色 */
  white-space: nowrap !important;
}
.hot-tag-item:active {
  background-color: #eee !important; /* 替换为首页标签点击色 */
}

/* ———— 6. 无数据提示:同步首页无数据样式 ———— */
.hot-no-data {
  text-align: center !important;
  color: #aaa !important; /* 替换为首页无数据颜色 */
  padding: 40rpx 0 !important; /* 替换为首页无数据内边距 */
  font-size: 24rpx !important; /* 替换为首页无数据字体大小 */
}
</style>

移除点击量显示,事件代码

点击:{{ article.click || 0 }}

搜索组件获取所有关键词记录20个关键词需要数据库开个表头做个存储

CREATE TABLE IF NOT EXISTS `emlog_search_history` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `keyword` varchar(100) NOT NULL COMMENT '搜索关键词',
  `search_time` int(10) NOT NULL COMMENT '最后搜索时间',
  `search_count` int(11) NOT NULL DEFAULT 1 COMMENT '搜索次数',
  PRIMARY KEY (`id`),
  UNIQUE KEY `keyword` (`keyword`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='搜索历史记录表';

“标签文章列表” 接口文档

需求 后端要求示例 你的当前配置(需核对)
1. 接口路由(route) route: 'get_tag_art' 当前用 route: 'tag_articles'
2. 标签 ID 参数名 tid: 123 当前用 tag_id: this.tagId
3. 分页参数名 page_num: 1/size:10 当前用 page: 1/count: this.pageSize

“通过 rest-api=article_list 接口,根据标签名(tag)获取对应文章”

功能模块 实现逻辑
1. 接收标签参数 onLoad(e) { this.tag = e.tag }:从热门话题页接收传递的 tag(标签名)
2. 接口请求 调用 /?rest-api=article_list 接口,传递 tag(标签名)和 page(页码)
3. 分页加载 - 下拉刷新:重置 page=1 重新加载
- 上拉加载:page += 1 加载下一页
4. 状态管理 status 控制 “加载中 / 无数据” 状态,dataa 存储文章列表数据
5. 样式适配 通过 --color 绑定全局主题色 qcolor,与其他页面视觉统一

用于存储 "文章 - 标签" 关联关系的表,缺少这个表会导致无法通过标签查询文章

-- 创建标签映射表(关联文章和标签)
CREATE TABLE IF NOT EXISTS `emlog_tag_map` (
  `mid` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `gid` int(10) unsigned NOT NULL,
  `tid` int(10) unsigned NOT NULL,
  PRIMARY KEY (`mid`),
  KEY `gid` (`gid`),
  KEY `tid` (`tid`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

把轮播图片修改该成组件

    <!-- 轮播图调试信息 -->
    <view class="lunbo-debug" v-if="showDebug">
      <view class="debug-title">轮播图调试</view>
      <view>基础URL:{{baseUrl}}</view>
      <view>请求地址:{{requestedUrl}}</view>
      <view>接口状态:{{lunboStatus}}</view>
      <view>图片来源:{{imagesFrom === 'api' ? '接口获取' : '默认图片'}}</view>
      <view>图片数量:{{images.length}} 张</view>
    </view>

创建轮播图组件引入文件(components/swipera.vue

<template>
  <swiper
    class="swiper-container"
    indicator-dots
    circular
    autoplay
    interval="3000"
    duration="500"
  >
    <swiper-item v-for="(imgUrl, index) in imgList" :key="index">
      <image :src="imgUrl" class="swiper-image" mode="widthFix"></image>
    </swiper-item>
  </swiper>
</template>

<script>
export default {
  props: {
    lunbo: {
      type: String,
      default: ''
    }
  },
  computed: {
    imgList() {
      // 将传入的逗号分隔字符串转为数组
      return this.lunbo.split(',').filter(img => img.trim() !== '');
    }
  }
};
</script>

<style scoped>
.swiper-container {
  width: 100%;
  height: 300rpx; /* 可根据需求调整轮播图高度 */
}
.swiper-image {
  width: 100%;
  height: 100%;
}
</style>

在index/index.vue加入组件

    <!-- 轮播图组件 -->
    <swipera :lunbo="images.length > 0 ? images.join(',') : defaultLunbo.join(',')"></swipera>
   <!-- 下面加入<script>内 -->
import { htRequest } from '@/api.js';
import { mapState, mapMutations } from "vuex";
// 引入配置文件中的基础URL
import setting from '@/setting.js'; // 假设setting.js在项目根目录

export default {
  data() {
    return {
      hotUser: [],
      backTopValue: false,
      images: [], // 轮播图数组
      tags: [], 
      sorts: [],
      debugInfo: '',
      defaultLunbo: [
        "https://xianyida.ysxdo.com/content/uploadfile/202508/42551755776231.png",
        "https://xianyida.ysxdo.com/content/uploadfile/202508/42551755776231.png",
        "https://xianyida.ysxdo.com/content/uploadfile/202508/42551755776231.png"
      ],
      showDebug: true,
      lunboStatus: '未开始请求',
      requestedUrl: '', // 实际请求的URL
      imagesFrom: 'default', // 图片来源
      baseUrl: setting.url, // 从配置文件获取的基础URL
    };
  },
  computed: {
    ...mapState(['isLogin', 'qcolor', 'user', 'options'])
  },
  onLoad() {
    this.getLunbo();
    setTimeout(() => {
      this.getHotUser();
      this.getOpitions();
      this.getTags();
      this.getSorts();
    }, 300);
  },
  onPullDownRefresh() {
    this.debugInfo = '';
    this.getLunbo();
    setTimeout(() => {
      this.getHotUser();
      this.getTags();
      this.getSorts();
      uni.stopPullDownRefresh();
    }, 300);
  },
  methods: {
    ...mapMutations(['login', 'setOptions']),

    // 关键:使用setting.js中的基础URL构建完整接口地址
async getLunbo() {
  try {
    this.lunboStatus = '正在请求接口...';
    const base = this.baseUrl.endsWith('/') ? this.baseUrl : this.baseUrl + '/';
    // 修复:移除路径中的content/
    const apiPath = 'content/plugins/ForumSetting/manyApi.php'; 
    this.requestedUrl = `${base}${apiPath}?route=getLunbo`;
    
    const res = await htRequest({
      url: apiPath, // 直接用修复后的路径
      method: "GET",
      data: { route: 'getLunbo' },
      contentType: 'application/json',
      timeout: 15000
    });

        // 验证接口返回
        if (!res || !res.data) {
          throw new Error('接口返回为空');
        }
        if (res.data.state !== 1) {
          throw new Error(`接口返回错误:${res.data.msg || '未知错误'}`);
        }
        if (!Array.isArray(res.data.data)) {
          throw new Error('接口返回的不是数组格式');
        }

        // 过滤无效URL
        const validImages = res.data.data.filter(img => {
          const isUrl = /^https?:\/\/.+/.test(img);
          if (!isUrl) this.lunboStatus += `| 无效URL:${img}`;
          return isUrl;
        });

        if (validImages.length > 0) {
          this.images = validImages;
          this.imagesFrom = 'api';
          this.lunboStatus = `请求成功!共${validImages.length}张有效图片`;
        } else {
          this.images = this.defaultLunbo;
          this.imagesFrom = 'default';
          this.lunboStatus = '接口返回的URL全部无效,已使用默认图';
        }

      } catch (err) {
        this.images = this.defaultLunbo;
        this.imagesFrom = 'default';
        this.lunboStatus = `请求失败:${err.message || '网络异常'},已使用默认图`;
        console.error('轮播图接口请求详情:', {
          baseUrl: this.baseUrl,
          requestedUrl: this.requestedUrl,
          error: err.message
        });
      }
    },