- 添加文章相关API配置和模型 - 创建文章列表页和详情页组件 - 实现文章列表展示和详情查看功能 - 在底部导航栏添加文章入口 - 新增文章列表项组件master
@ -0,0 +1,31 @@ | |||||
/** | |||||
* 文章相关接口配置 | |||||
* 对应后端 YaoDuArticleController | |||||
*/ | |||||
const api = { | |||||
/** | |||||
* 获取文章列表 | |||||
* 对应后端: GET /city/article/list | |||||
* @param {String} title - 文章标题(搜索关键词) | |||||
* @param {Object} bean - 分页排序参数 (包含 current, size 等分页信息) | |||||
*/ | |||||
articleList: { | |||||
url: '/city/article/list', | |||||
method: 'GET', | |||||
auth: false, | |||||
showLoading: false | |||||
}, | |||||
/** | |||||
* 根据ID获取文章详情 | |||||
* 对应后端: GET /city/article/queryById | |||||
* @param {String} id - 文章ID | |||||
*/ | |||||
articleDetail: { | |||||
url: '/city/article/queryById', | |||||
method: 'GET', | |||||
auth: false, | |||||
} | |||||
} | |||||
export default api |
@ -0,0 +1,76 @@ | |||||
<template> | |||||
<view class="article-item" @click="handleClick"> | |||||
<view class="image-container"> | |||||
<image :src="item.image" mode="aspectFill" class="article-image" /> | |||||
</view> | |||||
<view class="content-container"> | |||||
<text class="article-title">{{ item.title }}</text> | |||||
</view> | |||||
</view> | |||||
</template> | |||||
<script> | |||||
export default { | |||||
name: 'ArticleItem', | |||||
props: { | |||||
item: { | |||||
type: Object, | |||||
required: true, | |||||
default: () => ({ | |||||
title: '', | |||||
image: '' | |||||
}) | |||||
} | |||||
}, | |||||
methods: { | |||||
handleClick() { | |||||
this.$emit('click', this.item); | |||||
} | |||||
} | |||||
} | |||||
</script> | |||||
<style scoped lang="scss"> | |||||
.article-item { | |||||
display: flex; | |||||
flex-direction: column; | |||||
background: #fff; | |||||
border-radius: 12rpx; | |||||
overflow: hidden; | |||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); | |||||
margin-bottom: 20rpx; | |||||
transition: transform 0.2s ease; | |||||
&:active { | |||||
transform: scale(0.98); | |||||
} | |||||
} | |||||
.image-container { | |||||
width: 100%; | |||||
height: 300rpx; | |||||
overflow: hidden; | |||||
.article-image { | |||||
width: 100%; | |||||
height: 100%; | |||||
object-fit: cover; | |||||
} | |||||
} | |||||
.content-container { | |||||
padding: 20rpx; | |||||
.article-title { | |||||
font-size: 30rpx; | |||||
font-weight: 500; | |||||
color: #333; | |||||
line-height: 1.6; | |||||
display: -webkit-box; | |||||
-webkit-box-orient: vertical; | |||||
-webkit-line-clamp: 2; | |||||
overflow: hidden; | |||||
text-overflow: ellipsis; | |||||
} | |||||
} | |||||
</style> |
@ -0,0 +1,99 @@ | |||||
<template> | |||||
<view class="page"> | |||||
<navbar title="江华"/> | |||||
<view class="content"> | |||||
<view class="article-list" v-if="!loading"> | |||||
<article-item | |||||
v-for="(item, index) in List" | |||||
:key="item.id || index" | |||||
:item="item" | |||||
@click="handleItemClick" | |||||
/> | |||||
</view> | |||||
<!-- 加载状态 --> | |||||
<view class="loading-container" v-if="loading"> | |||||
<text class="loading-text">加载中...</text> | |||||
</view> | |||||
<!-- 空状态 --> | |||||
<view class="empty-container" v-if="!loading && List.length === 0"> | |||||
<text class="empty-text">暂无文章</text> | |||||
</view> | |||||
</view> | |||||
<tabber select="1" /> | |||||
</view> | |||||
</template> | |||||
<script> | |||||
import tabber from '@/components/base/tabbar.vue' | |||||
import articleItem from '@/components/list/articleItem.vue' | |||||
import loadList from '@/mixins/loadList.js' | |||||
export default { | |||||
mixins: [loadList], | |||||
components : { | |||||
tabber, | |||||
articleItem | |||||
}, | |||||
data() { | |||||
return { | |||||
mixinsListApi: 'articleList' | |||||
} | |||||
}, | |||||
methods: { | |||||
// 处理文章点击事件 | |||||
handleItemClick(item) { | |||||
console.log('点击了文章:', item); | |||||
// 跳转到文章详情页 | |||||
uni.navigateTo({ | |||||
url: `/pages_order/article/index?id=${item.id}` | |||||
}) | |||||
} | |||||
} | |||||
} | |||||
</script> | |||||
<style scoped lang="scss"> | |||||
.page { | |||||
background-color: #f5f5f5; | |||||
min-height: 100vh; | |||||
} | |||||
.content { | |||||
padding: 20rpx; | |||||
padding-bottom: 120rpx; // 为底部tabbar留出空间 | |||||
} | |||||
.article-list { | |||||
display: flex; | |||||
flex-direction: column; | |||||
gap: 20rpx; | |||||
} | |||||
.loading-container { | |||||
display: flex; | |||||
justify-content: center; | |||||
align-items: center; | |||||
padding: 100rpx 0; | |||||
.loading-text { | |||||
font-size: 28rpx; | |||||
color: #999; | |||||
} | |||||
} | |||||
.empty-container { | |||||
display: flex; | |||||
justify-content: center; | |||||
align-items: center; | |||||
padding: 100rpx 0; | |||||
.empty-text { | |||||
font-size: 28rpx; | |||||
color: #999; | |||||
} | |||||
} | |||||
</style> |
@ -0,0 +1,224 @@ | |||||
<template> | |||||
<view class="page"> | |||||
<navbar title="文章详情" /> | |||||
<view class="content"> | |||||
<!-- 加载状态 --> | |||||
<view class="loading-container" v-if="loading"> | |||||
<text class="loading-text">加载中...</text> | |||||
</view> | |||||
<!-- 文章详情 --> | |||||
<view class="article-detail" v-if="!loading && articleDetail"> | |||||
<!-- 创建时间 --> | |||||
<view class="article-meta"> | |||||
<text class="create-time">{{ formatTime(articleDetail.createTime) }}</text> | |||||
</view> | |||||
<!-- 富文本内容 --> | |||||
<view class="article-content"> | |||||
<rich-text :nodes="articleDetail.content"></rich-text> | |||||
</view> | |||||
</view> | |||||
<!-- 错误状态 --> | |||||
<view class="error-container" v-if="!loading && !articleDetail"> | |||||
<text class="error-text">文章不存在或已被删除</text> | |||||
</view> | |||||
</view> | |||||
</view> | |||||
</template> | |||||
<script> | |||||
export default { | |||||
data() { | |||||
return { | |||||
articleId: '', | |||||
articleDetail: null, | |||||
loading: false | |||||
} | |||||
}, | |||||
onLoad(options) { | |||||
if (options.id) { | |||||
this.articleId = options.id; | |||||
this.loadArticleDetail(); | |||||
} | |||||
}, | |||||
onShareAppMessage(res) { | |||||
return { | |||||
title: this.articleDetail ? this.articleDetail.title || '文章详情' : '文章详情', | |||||
imageUrl: this.articleDetail ? this.articleDetail.image : '', | |||||
path: '/pages_order/article/index?id=' + this.articleId | |||||
} | |||||
}, | |||||
methods: { | |||||
// 加载文章详情 | |||||
loadArticleDetail() { | |||||
if (!this.articleId) return; | |||||
this.loading = true; | |||||
const params = { | |||||
id: this.articleId | |||||
}; | |||||
this.$api('articleDetail', params, (res) => { | |||||
this.loading = false; | |||||
if (res.code === 200 && res.result) { | |||||
this.articleDetail = res.result; | |||||
} else { | |||||
uni.showToast({ | |||||
title: res.message || '加载失败', | |||||
icon: 'none' | |||||
}); | |||||
} | |||||
}); | |||||
}, | |||||
// 格式化时间 | |||||
formatTime(time) { | |||||
if (!time) return ''; | |||||
const date = new Date(time); | |||||
const year = date.getFullYear(); | |||||
const month = String(date.getMonth() + 1).padStart(2, '0'); | |||||
const day = String(date.getDate()).padStart(2, '0'); | |||||
const hours = String(date.getHours()).padStart(2, '0'); | |||||
const minutes = String(date.getMinutes()).padStart(2, '0'); | |||||
return `${year}-${month}-${day} ${hours}:${minutes}`; | |||||
} | |||||
} | |||||
} | |||||
</script> | |||||
<style scoped lang="scss"> | |||||
.page { | |||||
background-color: #f5f5f5; | |||||
min-height: 100vh; | |||||
} | |||||
.content { | |||||
padding: 20rpx; | |||||
} | |||||
.loading-container { | |||||
display: flex; | |||||
justify-content: center; | |||||
align-items: center; | |||||
padding: 200rpx 0; | |||||
.loading-text { | |||||
font-size: 28rpx; | |||||
color: #999; | |||||
} | |||||
} | |||||
.error-container { | |||||
display: flex; | |||||
justify-content: center; | |||||
align-items: center; | |||||
padding: 200rpx 0; | |||||
.error-text { | |||||
font-size: 28rpx; | |||||
color: #999; | |||||
} | |||||
} | |||||
.article-detail { | |||||
background-color: #fff; | |||||
border-radius: 16rpx; | |||||
padding: 30rpx; | |||||
margin-bottom: 20rpx; | |||||
} | |||||
.article-meta { | |||||
padding-bottom: 20rpx; | |||||
border-bottom: 1px solid #f0f0f0; | |||||
margin-bottom: 30rpx; | |||||
.create-time { | |||||
font-size: 24rpx; | |||||
color: #999; | |||||
} | |||||
} | |||||
.article-content { | |||||
line-height: 1.6; | |||||
// 富文本内容样式 | |||||
:deep(rich-text) { | |||||
font-size: 30rpx; | |||||
color: #333; | |||||
// 图片样式 | |||||
img { | |||||
max-width: 100%; | |||||
height: auto; | |||||
border-radius: 8rpx; | |||||
margin: 20rpx 0; | |||||
} | |||||
// 段落样式 | |||||
p { | |||||
margin: 20rpx 0; | |||||
line-height: 1.8; | |||||
} | |||||
// 标题样式 | |||||
h1, h2, h3, h4, h5, h6 { | |||||
margin: 30rpx 0 20rpx 0; | |||||
font-weight: bold; | |||||
} | |||||
h1 { font-size: 36rpx; } | |||||
h2 { font-size: 34rpx; } | |||||
h3 { font-size: 32rpx; } | |||||
// 列表样式 | |||||
ul, ol { | |||||
padding-left: 40rpx; | |||||
margin: 20rpx 0; | |||||
} | |||||
li { | |||||
margin: 10rpx 0; | |||||
line-height: 1.6; | |||||
} | |||||
// 引用样式 | |||||
blockquote { | |||||
border-left: 4rpx solid #ddd; | |||||
padding-left: 20rpx; | |||||
margin: 20rpx 0; | |||||
color: #666; | |||||
font-style: italic; | |||||
} | |||||
// 代码样式 | |||||
code { | |||||
background-color: #f5f5f5; | |||||
padding: 4rpx 8rpx; | |||||
border-radius: 4rpx; | |||||
font-family: monospace; | |||||
font-size: 26rpx; | |||||
} | |||||
pre { | |||||
background-color: #f5f5f5; | |||||
padding: 20rpx; | |||||
border-radius: 8rpx; | |||||
overflow-x: auto; | |||||
margin: 20rpx 0; | |||||
code { | |||||
background: none; | |||||
padding: 0; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
</style> |