emlog前端调用函数代码
具体方法如下:
<div class="alert alert-primary">
在需要显示导航的位置添加调用代码:<pre><code><?php nav_plugin_display();></code></pre>
分类字段修改方法:<pre><code><?php echo Ixc_field_set('分类ID',1,'字段','修改的内容'); ?> </code></pre></div>
<div class="alert alert-primary">
在需要显示导航的位置添加调用代码:<pre><code><?php nav_plugin_display();></code></pre>
在需要显示导航的位置添加调用代码:<pre><code><?php echo Ixc_field_set('分类ID',1,'字段','修改的内容'); ?> </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
});
}
},