refactor(diy-article): 优化文章组件,完善空状态和鲁棒性

1. 新增空数据占位提示
2. 给props添加默认值避免空指针
3. 完善样式和判断逻辑的默认值处理
4. 增加请求失败兜底逻辑
5. 优化响应式更新图片错误的处理
6. 抽离class计算逻辑提升代码可读性
This commit is contained in:
2026-06-13 14:26:52 +08:00
parent ebe2963466
commit 01e7c3e981

View File

@@ -1,265 +1,302 @@
<template> <template>
<x-skeleton data-component-name="diy-article" type="list" :loading="loading" :configs="skeletonConfig"> <x-skeleton data-component-name="diy-article" type="list" :loading="loading" :configs="skeletonConfig">
<view class="article-wrap" :style="warpCss"> <view class="article-wrap" :style="warpCss">
<view :class="['list-wrap', value.style]" :style="warpCss"> <view :class="listClass" :style="warpCss">
<!-- 自动垂直滚动 --> <!-- 自动垂直滚动 -->
<swiper v-if="list.length > 0" class="auto-scroll-swiper" :style="[swiperStyle]" :vertical="swiperConfig.vertical !== false" <swiper v-if="list.length > 0" class="auto-scroll-swiper" :style="swiperStyle"
:autoplay="swiperConfig.autoplay !== false" :circular="swiperConfig.circular !== false" :vertical="swiperConfig.vertical !== false"
:interval="swiperConfig.interval || 3000" :duration="swiperConfig.duration || 500" :autoplay="swiperConfig.autoplay !== false" :circular="swiperConfig.circular !== false"
:display-multiple-items="safeDisplayMultipleItems"> :interval="swiperConfig.interval || 3000" :duration="swiperConfig.duration || 500"
<swiper-item v-for="(item, index) in list" :key="index" @tap.stop="toDetail(item)"> :display-multiple-items="safeDisplayMultipleItems">
<view class="swiper-item-content"> <swiper-item v-for="(item, index) in list" :key="index" @tap.stop="toDetail(item)">
<view :class="['item', value.ornament.type]" :style="itemCss"> <view class="swiper-item-content">
<view class="article-img"> <view :class="itemClass" :style="itemCss">
<image class="cover-img" :src="$util.img(item.cover_img)" mode="widthFix" <view class="article-img">
@error="imgError(index)" show-menu-by-longpress/> <image class="cover-img" :src="$util.img(item.cover_img)" mode="widthFix"
</view> @error="imgError(index)" show-menu-by-longpress></image>
<view class="info-wrap"> </view>
<text class="title">{{ item.article_title }}</text> <view class="info-wrap">
<text class="desc" <text class="title">{{ item.article_title }}</text>
style="color:#888;font-size: 24rpx; display: -webkit-box;-webkit-box-orient: vertical;-webkit-line-clamp: 2;overflow: hidden;text-overflow: ellipsis;">{{ <text class="desc">{{ item.article_abstract }}</text>
item.article_abstract }}</text> <view class="read-wrap">
<view class="read-wrap"> <block v-if="item.category_name">
<block v-if="item.category_name"> <text class="category-icon"></text>
<text class="category-icon"></text> <text class="category-name">{{ item.category_name }}</text>
<text>{{ item.category_name }}</text> </block>
</block> <text class="date">{{ $util.timeStampTurnTime(item.create_time, 'date') }}</text>
<text class="date">{{ $util.timeStampTurnTime(item.create_time, 'date') </view>
}}</text> </view>
</view> </view>
</view> </view>
</view> </swiper-item>
</view> </swiper>
</swiper-item> <view v-if="list.length === 0 && !loading" class="empty-placeholder">
</swiper> <text class="empty-text">暂无相关文章</text>
</view> </view>
</view> </view>
</x-skeleton> </view>
</template> </x-skeleton>
</template>
<script>
import DiyMinx from './minx.js' <script>
// 文章 // 依赖 ./minx.js 提供 componentRefresh、$util 等装修通用能力
export default { import DiyMinx from './minx.js';
name: 'diy-article', // 文章
props: { export default {
value: { name: 'diy-article',
type: Object props: {
} value: {
}, type: Object,
mixins: [DiyMinx], default: () => ({})
data() { }
return { },
list: [], mixins: [DiyMinx],
loading: true, data() {
skeletonConfig: { return {
gridRows: 1, list: [],
gridRowsGap: '40rpx', loading: true,
headWidth: '160rpx', skeletonConfig: {
headHeight: '160rpx', gridRows: 1,
textRows: 2 gridRowsGap: '40rpx',
} headWidth: '160rpx',
}; headHeight: '160rpx',
}, textRows: 2
created() { }
this.getList(); };
}, },
watch: { created() {
// 组件刷新监听 this.getList();
componentRefresh: function (nval) { },
this.getList(); watch: {
} // 组件刷新监听(来自 DiyMinx mixin)
}, componentRefresh() {
computed: { this.getList();
// 获取swiper配置,默认空对象 }
swiperConfig() { },
return this.value?.swiperConfig || {}; computed: {
}, // 获取swiper配置,默认空对象
// 安全的display-multiple-items值,确保不大于swiper-item数量 swiperConfig() {
safeDisplayMultipleItems() { return this.value.swiperConfig || {};
const displayCount = this.swiperConfig.displayMultipleItems || 3; },
const actualCount = this.list.length; // 安全的display-multiple-items值,确保不大于swiper-item数量
return actualCount > 0 ? Math.min(displayCount, actualCount) : 1; safeDisplayMultipleItems() {
}, const displayCount = this.swiperConfig.displayMultipleItems || 3;
warpCss() { const actualCount = this.list.length;
var obj = ''; return actualCount > 0 ? Math.min(displayCount, actualCount) : 1;
obj += 'background-color:' + this.value.componentBgColor + ';'; },
if (this.value.componentAngle == 'round') { // 列表容器 class
obj += 'border-top-left-radius:' + this.value.topAroundRadius * 2 + 'rpx;'; listClass() {
obj += 'border-top-right-radius:' + this.value.topAroundRadius * 2 + 'rpx;'; return ['list-wrap', this.value.style];
obj += 'border-bottom-left-radius:' + this.value.bottomAroundRadius * 2 + 'rpx;'; },
obj += 'border-bottom-right-radius:' + this.value.bottomAroundRadius * 2 + 'rpx;'; // 单项 class
} itemClass() {
return obj; return ['item', (this.value.ornament || {}).type];
}, },
// swiper容器样式 warpCss() {
swiperStyle() { var obj = '';
// 从swiperConfig获取高度,默认600rpx obj += 'background-color:' + (this.value.componentBgColor || 'transparent') + ';';
const height = this.swiperConfig.swiperHeight || 600; if (this.value.componentAngle == 'round') {
return { obj += 'border-top-left-radius:' + (this.value.topAroundRadius || 0) * 2 + 'rpx;';
height: height + 'rpx', obj += 'border-top-right-radius:' + (this.value.topAroundRadius || 0) * 2 + 'rpx;';
width: '100%' obj += 'border-bottom-left-radius:' + (this.value.bottomAroundRadius || 0) * 2 + 'rpx;';
}; obj += 'border-bottom-right-radius:' + (this.value.bottomAroundRadius || 0) * 2 + 'rpx;';
}, }
// 子项样式 return obj;
itemCss() { },
var obj = ''; // swiper容器样式
obj += 'background-color:' + this.value.elementBgColor + ';'; swiperStyle() {
if (this.value.elementAngle == 'round') { var height = parseInt(this.swiperConfig.swiperHeight) || 600;
obj += 'border-top-left-radius:' + this.value.topElementAroundRadius * 2 + 'rpx;'; return {
obj += 'border-top-right-radius:' + this.value.topElementAroundRadius * 2 + 'rpx;'; height: height + 'rpx',
obj += 'border-bottom-left-radius:' + this.value.bottomElementAroundRadius * 2 + 'rpx;'; width: '100%'
obj += 'border-bottom-right-radius:' + this.value.bottomElementAroundRadius * 2 + 'rpx;'; };
} },
if (this.value.ornament.type == 'shadow') { // 子项样式
obj += 'box-shadow:' + '0 0 10rpx ' + this.value.ornament.color; itemCss() {
} var obj = '';
if (this.value.ornament.type == 'stroke') { obj += 'background-color:' + (this.value.elementBgColor || 'transparent') + ';';
obj += 'border:' + '2rpx solid ' + this.value.ornament.color; if (this.value.elementAngle == 'round') {
} obj += 'border-top-left-radius:' + (this.value.topElementAroundRadius || 0) * 2 + 'rpx;';
return obj; obj += 'border-top-right-radius:' + (this.value.topElementAroundRadius || 0) * 2 + 'rpx;';
} obj += 'border-bottom-left-radius:' + (this.value.bottomElementAroundRadius || 0) * 2 + 'rpx;';
}, obj += 'border-bottom-right-radius:' + (this.value.bottomElementAroundRadius || 0) * 2 + 'rpx;';
methods: { }
getList() { var ornament = this.value.ornament || {};
var data = { if (ornament.type == 'shadow') {
num: this.value.count obj += 'box-shadow:0 0 10rpx ' + (ornament.color || 'rgba(0, 0, 0, 0.1)') + ';';
}; }
if (this.value.sources == 'diy') { if (ornament.type == 'stroke') {
data.num = 0; obj += 'border:2rpx solid ' + (ornament.color || 'transparent') + ';';
data.article_id_arr = this.value.articleIds.toString(); }
} return obj;
}
this.$api.sendRequest({ },
url: '/api/article/lists', methods: {
data: data, getList() {
success: res => { this.loading = true;
if (res.code == 0 && res.data) { var data = {
let data = res.data; num: this.value.count || 0
this.list = data; };
} if (this.value.sources == 'diy') {
this.loading = false; data.num = 0;
} data.article_id_arr = (this.value.articleIds || []).toString();
}); }
},
toDetail(item) { this.$api.sendRequest({
this.$util.redirectTo('/pages_tool/article/detail', { url: '/api/article/lists',
article_id: item.article_id data: data,
}); success: res => {
}, this.list = (res.code == 0 && res.data) ? res.data : [];
imgError(index) { this.loading = false;
if (this.list[index]) this.list[index].cover_img = this.$util.getDefaultImage().article; },
} fail: () => {
} this.list = [];
}; this.loading = false;
</script> }
});
<style lang="scss"> },
.article-wrap { toDetail(item) {
.list-wrap { this.$util.redirectTo('/pages_tool/article/detail', {
.auto-scroll-swiper { article_id: item.article_id
height: 600rpx; });
/* 可以根据需要调整高度 */ },
width: 100%; imgError(index) {
if (!this.list[index]) return;
.swiper-item-content { var defaultImg = (this.$util.getDefaultImage() || {}).article;
padding: 10rpx 20rpx; // 使用 $set 保证响应式更新
box-sizing: border-box; this.$set(this.list, index, {
} ...this.list[index],
} cover_img: defaultImg
});
.item { }
margin: 0; }
margin-bottom: 24rpx; };
} </script>
.style-1 .item { <style lang="scss">
padding: 20rpx; .article-wrap {
} .list-wrap {
.auto-scroll-swiper {
&.style-1 { height: 600rpx;
.item { width: 100%;
display: flex;
margin-top: 0; .swiper-item-content {
padding: 10rpx 20rpx;
&:first-of-type { box-sizing: border-box;
margin-top: 0; }
} }
.article-img { .item {
margin-right: 20rpx; margin: 0;
width: 160rpx; margin-bottom: 24rpx;
height: 160rpx; }
overflow: hidden;
display: flex; .style-1 .item {
align-items: center; padding: 20rpx;
justify-content: center; }
image { &.style-1 {
width: 100%; .item {
} display: flex;
} margin-top: 0;
.info-wrap { &:first-of-type {
flex: 1; margin-top: 0;
display: flex; }
flex-direction: column;
justify-content: space-between; .article-img {
margin-right: 20rpx;
.title { width: 160rpx;
font-weight: bold; height: 160rpx;
margin-bottom: 10rpx; overflow: hidden;
overflow: hidden; display: flex;
text-overflow: ellipsis; align-items: center;
display: -webkit-box; justify-content: center;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical; image {
font-size: 30rpx; width: 100%;
line-height: 1.5; }
} }
.abstract { .info-wrap {
overflow: hidden; flex: 1;
text-overflow: ellipsis; display: flex;
display: -webkit-box; flex-direction: column;
-webkit-line-clamp: 2; justify-content: space-between;
-webkit-box-orient: vertical;
font-size: $font-size-tag; .title {
} font-weight: bold;
margin-bottom: 10rpx;
.read-wrap { overflow: hidden;
display: flex; text-overflow: ellipsis;
color: #999ca7; display: -webkit-box;
justify-content: flex-start; -webkit-line-clamp: 2;
align-items: center; -webkit-box-orient: vertical;
margin-top: 10rpx; font-size: 30rpx;
line-height: 1; line-height: 1.5;
}
text {
font-size: $font-size-tag; .desc {
} color: #888;
font-size: 24rpx;
.iconfont { overflow: hidden;
font-size: 36rpx; text-overflow: ellipsis;
vertical-align: bottom; display: -webkit-box;
margin-right: 10rpx; -webkit-line-clamp: 2;
} -webkit-box-orient: vertical;
}
.category-icon {
width: 8rpx; .read-wrap {
height: 8rpx; display: flex;
border-radius: 50%; color: #999ca7;
background: $base-color; justify-content: flex-start;
margin-right: 10rpx; align-items: center;
} margin-top: 10rpx;
line-height: 1;
.date {
margin-left: 20rpx; text {
} font-size: $font-size-tag;
} }
}
} .iconfont {
} font-size: 36rpx;
} vertical-align: bottom;
} margin-right: 10rpx;
}
.category-icon {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
background: $base-color;
margin-right: 10rpx;
}
.category-name {
margin-right: 10rpx;
}
.date {
margin-left: 20rpx;
}
}
}
}
}
// 空数据占位
.empty-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 0;
.empty-text {
color: #999;
font-size: 26rpx;
}
}
}
}
</style> </style>