Files
lucky_shop/components-diy/diy-article.vue
ZF sun f9d144159a refactor(article-component): 新增多维度CSS变量支持,对齐后端默认配置
新增标签胶囊、摘要文字、阅读区、查看更多、标题字号等多组CSS变量,将组件样式控制权交给外部配置,同时将默认值对齐后端design.js的nestedDefaults。重构列表项间距、分割线颜色等硬编码样式为变量驱动,统一组件样式管理逻辑。
2026-06-15 08:28:07 +08:00

693 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<x-skeleton data-component-name="diy-article" type="list" :loading="loading" :configs="skeletonConfig">
<view class="article-wrap" :style="warpWrapStyle">
<!-- 注: :class 用内联数组,与 ns-newform 对齐;computed listClass/itemClass 已移除(避免数组 toString 用逗号 join 的问题) -->
<view :class="['list-wrap', value.style || 'style-1']">
<!-- 自动垂直滚动(默认开启,兼容老数据:无 scrollMode/swiperConfig 字段也走这里) -->
<swiper v-if="list.length > 0 && useScroll" class="auto-scroll-swiper"
:style="swiperWrapStyle"
:vertical="swiperConfig.vertical !== false"
:autoplay="swiperConfig.autoplay !== false"
:circular="swiperConfig.circular !== false"
:interval="swiperConfig.interval || 3000"
:duration="swiperConfig.duration || 500"
:display-multiple-items="safeDisplayMultipleItems">
<swiper-item v-for="(item, index) in list" :key="index" @tap.stop="toDetail(item)">
<view class="swiper-item-content">
<view :class="['item', value.ornament && value.ornament.type]" :style="itemStyle">
<!-- 封面图:无图时隐藏图位;style-4 纯文字始终不显示 -->
<view class="article-img" v-if="item.cover_img && value.style !== 'style-4'">
<image class="cover-img" :src="$util.img(item.cover_img)" mode="aspectFill"
@error="imgError(index)" show-menu-by-longpress></image>
</view>
<view class="info-wrap">
<text class="title">{{ item.article_title }}</text>
<text class="desc"
v-if="abstractStyle.support && abstractStyle.control && item.article_abstract">
{{ item.article_abstract }}
</text>
<view class="tag-list"
v-if="tagStyle.support && tagStyle.control && item.tag_names">
<text class="tag" v-for="(tn, ti) in tagArr(item.tag_names)" :key="ti">{{ tn }}</text>
</view>
<view class="read-wrap">
<block v-if="item.category_name">
<text class="category-icon"></text>
<text class="category-name">{{ item.category_name }}</text>
</block>
<view class="num" v-if="readNumStyle.support && readNumStyle.control">
<text class="iconfont iconchakan"></text>
<text>{{ item.read_num || 0 }}</text>
</view>
<view class="num" v-if="dianzanStyle.support && dianzanStyle.control">
<text class="iconfont icondianzan"></text>
<text>{{ item.dianzan_num || 0 }}</text>
</view>
<text class="date">{{ $util.timeStampTurnTime(item.create_time, 'date') }}</text>
</view>
</view>
</view>
</view>
</swiper-item>
</swiper>
<!-- 非滚动模式(scrollMode==='none')时的普通列表 -->
<template v-if="!useScroll">
<view v-for="(item, index) in list" :key="index" :class="['item', value.ornament && value.ornament.type]" :style="itemStyle"
@tap.stop="toDetail(item)">
<view class="article-img" v-if="item.cover_img && value.style !== 'style-4'">
<image class="cover-img" :src="$util.img(item.cover_img)" mode="aspectFill"
@error="imgError(index)" show-menu-by-longpress></image>
</view>
<view class="info-wrap">
<text class="title">{{ item.article_title }}</text>
<text class="desc"
v-if="abstractStyle.support && abstractStyle.control && item.article_abstract">
{{ item.article_abstract }}
</text>
<view class="tag-list"
v-if="tagStyle.support && tagStyle.control && item.tag_names">
<text class="tag" v-for="(tn, ti) in tagArr(item.tag_names)" :key="ti">{{ tn }}</text>
</view>
<view class="read-wrap">
<block v-if="item.category_name">
<text class="category-icon"></text>
<text class="category-name">{{ item.category_name }}</text>
</block>
<view class="num" v-if="readNumStyle.support && readNumStyle.control">
<text class="iconfont iconchakan"></text>
<text>{{ item.read_num || 0 }}</text>
</view>
<view class="num" v-if="dianzanStyle.support && dianzanStyle.control">
<text class="iconfont icondianzan"></text>
<text>{{ item.dianzan_num || 0 }}</text>
</view>
<text class="date">{{ $util.timeStampTurnTime(item.create_time, 'date') }}</text>
</view>
</view>
</view>
<!-- 查看更多(仅非滚动模式显示) -->
<view class="view-more" v-if="viewMore.support && viewMore.control && list.length > 0"
:style="viewMoreStyle" @tap.stop="toListPage">
<text>{{ viewMore.text || '查看更多' }}</text>
<text class="iconfont icondiy-right-arrow"></text>
</view>
</template>
<view v-if="list.length === 0 && !loading" class="empty-placeholder">
<text class="empty-text">暂无相关文章</text>
</view>
</view>
</view>
</x-skeleton>
</template>
<script>
// 依赖 ./minx.js 提供 componentRefresh、$util 等装修通用能力
import DiyMinx from './minx.js';
// 文章
export default {
name: 'diy-article',
props: {
value: {
type: Object,
default: () => ({})
}
},
mixins: [DiyMinx],
data() {
return {
list: [],
loading: true,
skeletonConfig: {
gridRows: 1,
gridRowsGap: '40rpx',
headWidth: '160rpx',
headHeight: '160rpx',
textRows: 2
}
};
},
created() {
this.getList();
},
watch: {
// 组件刷新监听
componentRefresh() {
this.getList();
}
},
computed: {
// ========== 容器样式(与 ns-newform 对齐: $util.objToStyle + CSS 变量) ==========
// 根容器:背景 + CSS 变量(向下传递,H5/MP/Harmony 均支持继承)
// 注: 用 objToStyle 自动过滤 undefined/空值,避免输出 background-color:; 这类脏 CSS
warpWrapStyle() {
const v = this.value || {};
const r = (val, fb) => this._num(val, fb);
const isRootRound = v.componentAngle === 'round';
const isItemRound = v.elementAngle === 'round';
// 标签胶囊 3 色(默认值与后端 design.js nestedDefaults 对齐)
const tag = v.tagStyle || {};
// 摘要文字色(默认值与后端 design.js nestedDefaults 对齐)
const abs = v.abstractStyle || {};
// 查看更多(字号 + 图标大小)
const vm = v.viewMore || {};
// 阅读区(文字色 + 图标大小)
const rw = v.readWrapStyle || {};
// 标题字号(每风格独立 1/2/3/4)
const tfs1 = r(v.titleFontSize1, 30);
const tfs2 = r(v.titleFontSize2, 30);
const tfs3 = r(v.titleFontSize3, 24);
const tfs4 = r(v.titleFontSize4, 28);
return this.$util.objToStyle({
'background-color': v.componentBgColor || 'transparent',
// CSS 变量(向下传递给所有子元素,通过 var() 读取)
'--article-component-bg-color': v.componentBgColor || 'transparent',
'--article-element-bg-color': v.elementBgColor || 'transparent',
'--article-top-around-radius': isRootRound ? `${r(v.topAroundRadius, 0) * 2}rpx` : '0rpx',
'--article-bottom-around-radius': isRootRound ? `${r(v.bottomAroundRadius, 0) * 2}rpx` : '0rpx',
'--article-top-element-around-radius': isItemRound ? `${r(v.topElementAroundRadius, 0) * 2}rpx` : '0rpx',
'--article-bottom-element-around-radius': isItemRound ? `${r(v.bottomElementAroundRadius, 0) * 2}rpx` : '0rpx',
// 标签胶囊 3 色
'--article-tag-bg-color': tag.bgColor || '#f0f9ff',
'--article-tag-text-color': tag.textColor || '#1e6bb8',
'--article-tag-border-color': tag.borderColor || '#b3d8ff',
// 摘要文字色
'--article-abstract-color': abs.textColor || '#888888',
// 阅读区文字色
'--article-meta-color': rw.color || '#999ca7',
// 阅读区图标大小(rpx)
'--article-meta-icon-size': r(rw.iconSize, 24) + 'rpx',
// 查看更多字号(rpx)
'--article-view-more-font-size': (typeof vm.fontSize === 'number' ? vm.fontSize : r(vm.fontSize, 26)) + 'rpx',
// 查看更多图标大小(rpx)
'--article-view-more-icon-size': r(vm.iconSize, 24) + 'rpx',
// 列表项间距(rpx)
'--article-item-gap': r(v.itemGap, 24) + 'rpx',
// 分割线颜色(主要用于 style-4)
'--article-divider-color': v.dividerColor || '#eeeeee',
// 标题字号 4 风格独立
'--article-title-fs-1': tfs1 + 'rpx',
'--article-title-fs-2': tfs2 + 'rpx',
'--article-title-fs-3': tfs3 + 'rpx',
'--article-title-fs-4': tfs4 + 'rpx'
});
},
// 单项容器样式:背景 + 装饰(box-shadow / border)
// 注: 单项的 border-radius 由 CSS 变量向下传递(无需重复声明)
itemStyle() {
const v = this.value || {};
const ornament = v.ornament || {};
const style = {
'background-color': v.elementBgColor || 'transparent'
};
if (ornament.type === 'shadow') {
style['box-shadow'] = `0 0 10rpx ${ornament.color || 'rgba(0, 0, 0, 0.1)'}`;
}
if (ornament.type === 'stroke') {
style['border'] = `2rpx solid ${ornament.color || 'transparent'}`;
}
return this.$util.objToStyle(style);
},
// swiper 容器高度(由 swiperHeight 计算属性驱动)
swiperWrapStyle() {
return this.$util.objToStyle({
height: `${this.swiperHeight}rpx`
});
},
// 查看更多样式(颜色由 viewMore.color 控制)
viewMoreStyle() {
return this.$util.objToStyle({
color: (this.viewMore && this.viewMore.color) || '#999999'
});
},
// ========== 字段显示配置(带默认值,避免老数据缺字段时报错) ==========
abstractStyle() {
return Object.assign({ support: true, control: true }, this.value.abstractStyle || {});
},
readNumStyle() {
return Object.assign({ support: true, control: true }, this.value.readNumStyle || {});
},
dianzanStyle() {
return Object.assign({ support: true, control: false }, this.value.dianzanStyle || {});
},
tagStyle() {
return Object.assign({ support: true, control: true }, this.value.tagStyle || {});
},
viewMore() {
return Object.assign({ support: true, control: false, text: '查看更多', color: '#999999', iconSize: 24 },
this.value.viewMore || {});
},
// 阅读区样式(文字色 + 图标大小)
readWrapStyle() {
return Object.assign({ color: '#999ca7', iconSize: 24 },
this.value.readWrapStyle || {});
},
// ========== 滚动配置(同时兼容老数据 swiperConfig 和新字段 scrollMode) ==========
// 老数据 swiperConfig 字段:{ vertical, autoplay, circular, interval, duration, displayMultipleItems, swiperHeight }
// 新数据 scrollMode 字段:'none' | 'vertical'(默认 vertical,与老行为一致)
swiperConfig() {
return this.value.swiperConfig || {};
},
scrollMode() {
return this.value.scrollMode || 'vertical';
},
// 是否使用滚动模式
useScroll() {
return this.scrollMode !== 'none' && this.list.length > 1;
},
// 安全的 display-multiple-items(确保不大于实际 list 长度,避免 swiper 报错)
safeDisplayMultipleItems() {
var displayCount = this.swiperConfig.displayMultipleItems || 3;
var actualCount = this.list.length;
return actualCount > 0 ? Math.min(displayCount, actualCount) : 1;
},
// swiper 高度(rpx)优先级:
// 1. 老/新数据显式设置了 swiperConfig.swiperHeight(>0) → 用配置值
// 2. 否则按 style 算单行高度 × 实际显示行数 + 行间距
swiperHeight() {
// 1. 显式设置了 swiperHeight (>0)
var configHeight = parseInt(this.swiperConfig.swiperHeight) || 0;
if (configHeight > 0) return configHeight;
// 2. 自动算:每行高度 × 显示行数 + 间距
// 注:swiper 内 item 已去掉 margin-bottom,swiper-item 高度 = perRow
var perRow;
switch (this.value.style) {
case 'style-1': perRow = 200; break; // 图160 + padding 上下20*2 = 200
case 'style-2': perRow = 540; break; // 图320 + info 220
case 'style-3': perRow = 300; break; // 图200 + info 100
case 'style-4': perRow = 120; break; // 纯文字
default: perRow = 200; break;
}
return perRow * this.safeDisplayMultipleItems + 30; // N 行 + 30rpx 行间距总空间
}
// 注: :class 一律在模板里用 :class="['static', dynamic]" 内联数组写法
// 不再用 listClass/itemClass computed (computed 返回 array 时,uni-app WXML 编译
// 输出 {{[listClass]}},运行时 toString 用逗号 join,产生 "list-wrap,style-1" 错误 class)
},
methods: {
// 数字保底:与 diy-form._num 对齐(undefined/空串/非数字时返回兜底值)
_num(v, fb) {
if (v === '' || v === null || v === undefined) return fb;
var n = parseInt(v);
return isNaN(n) ? fb : n;
},
// 把逗号分隔的 tag_names 转为数组(过滤空)
tagArr(str) {
return (str || '').split(',').filter(Boolean);
},
getList() {
this.loading = true;
var data = {
num: this.value.count || 8,
sort_way: this.value.sortWay || 'latest'
};
var sources = this.value.sources || 'initial';
if (sources === 'diy') {
data.num = 0;
data.article_id_arr = (this.value.articleIds || []).toString();
} else if (sources === 'category' && this.value.categoryId) {
data.category_id = this.value.categoryId;
} else if (sources === 'tag' && this.value.tagId) {
data.tag_id = this.value.tagId;
}
this.$api.sendRequest({
url: '/api/article/lists',
data: data,
success: res => {
this.list = (res.code == 0 && res.data) ? res.data : [];
this.loading = false;
},
fail: () => {
this.list = [];
this.loading = false;
}
});
},
toDetail(item) {
this.$util.redirectTo('/pages_tool/article/detail', {
article_id: item.article_id
});
},
// "查看更多" 跳列表页(若按分类/按标签则带筛选参数)
toListPage() {
var query = {};
var sources = this.value.sources || 'initial';
if (sources === 'category' && this.value.categoryId) {
query.category_id = this.value.categoryId;
} else if (sources === 'tag' && this.value.tagId) {
query.tag_id = this.value.tagId;
}
this.$util.redirectTo('/pages_tool/article/list', query);
},
imgError(index) {
// 图加载失败 → 清空 cover_img 触发 v-if 隐藏图位(无图就隐藏策略)
if (!this.list[index]) return;
this.$set(this.list, index, {
...this.list[index],
cover_img: ''
});
}
}
};
</script>
<style lang="scss">
/* 与 ns-newform 对齐: 在 .article-wrap 上通过内联 style 写入 CSS 变量,
子元素通过 var() 读取。CSS 变量在 H5 / 微信小程序 WXSS / 鸿蒙元服务均支持继承。 */
.article-wrap {
/* CSS 变量默认值兜底(老数据 / 静态预览场景下用以下值;默认值与后端 design.js nestedDefaults 对齐) */
--article-component-bg-color: transparent;
--article-element-bg-color: transparent;
--article-top-around-radius: 0rpx;
--article-bottom-around-radius: 0rpx;
--article-top-element-around-radius: 0rpx;
--article-bottom-element-around-radius: 0rpx;
--article-tag-bg-color: #f0f9ff;
--article-tag-text-color: #1e6bb8;
--article-tag-border-color: #b3d8ff;
--article-abstract-color: #888888;
--article-meta-color: #999ca7;
--article-meta-icon-size: 24rpx;
--article-view-more-font-size: 26rpx;
--article-view-more-icon-size: 24rpx;
--article-item-gap: 24rpx;
--article-divider-color: #eeeeee;
--article-title-fs-1: 30rpx;
--article-title-fs-2: 30rpx;
--article-title-fs-3: 24rpx;
--article-title-fs-4: 28rpx;
/* 根容器圆角:由 --article-top/bottom-around-radius 驱动 */
border-top-left-radius: var(--article-top-around-radius, 0);
border-top-right-radius: var(--article-top-around-radius, 0);
border-bottom-left-radius: var(--article-bottom-around-radius, 0);
border-bottom-right-radius: var(--article-bottom-around-radius, 0);
.list-wrap {
// 垂直滚动 swiper
.auto-scroll-swiper {
width: 100%;
// swiper-item-content 占满 swiper-item,不另加 padding(避免与 item padding 叠加导致裁切)
.swiper-item-content {
padding: 0;
box-sizing: border-box;
}
// swiper 内 item 去掉 margin-bottom,避免被 swiper-item 高度裁切;
// 行间距由 swiperHeight 多余空间制造(perRow × 3 + 30)
.item {
margin-bottom: 0 !important;
}
}
.item {
margin-bottom: var(--article-item-gap, 24rpx);
/* 背景 / 圆角通过 CSS 变量从 .article-wrap 继承(由 warpWrapStyle 注入) */
background-color: var(--article-element-bg-color, transparent);
border-top-left-radius: var(--article-top-element-around-radius, 0);
border-top-right-radius: var(--article-top-element-around-radius, 0);
border-bottom-left-radius: var(--article-bottom-element-around-radius, 0);
border-bottom-right-radius: var(--article-bottom-element-around-radius, 0);
&:last-of-type {
margin-bottom: 0;
}
}
.style-1 .item {
padding: 20rpx;
}
.tag-list {
margin-top: 10rpx;
line-height: 1;
.tag {
display: inline-block;
/* 3 色由 CSS 变量驱动(来自 warpWrapStyle 注入;老数据走默认值) */
background: var(--article-tag-bg-color, #f0f9ff);
color: var(--article-tag-text-color, #1e6bb8);
border: 2rpx solid var(--article-tag-border-color, #b3d8ff);
border-radius: 20rpx;
padding: 2rpx 14rpx;
margin: 0 8rpx 8rpx 0;
font-size: 22rpx;
}
}
.read-wrap {
display: flex;
/* 文字色由 CSS 变量驱动(来自 warpWrapStyle 注入;老数据走默认值) */
color: var(--article-meta-color, #999ca7);
align-items: center;
margin-top: 10rpx;
line-height: 1;
flex-wrap: wrap;
font-size: $font-size-tag;
.category-icon {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
background: $base-color;
margin-right: 10rpx;
}
.category-name {
margin-right: 10rpx;
}
.num {
display: inline-flex;
align-items: center;
margin-left: 16rpx;
.iconfont {
/* 图标大小由 CSS 变量驱动 */
font-size: var(--article-meta-icon-size, 24rpx);
margin-right: 6rpx;
}
}
.date {
margin-left: 16rpx;
}
}
.view-more {
text-align: center;
padding: 20rpx 0;
/* 字号由 CSS 变量驱动;颜色由 viewMoreStyle 注入(支持点击重置) */
font-size: var(--article-view-more-font-size, 26rpx);
.iconfont {
/* 图标大小由 CSS 变量驱动 */
font-size: var(--article-view-more-icon-size, 24rpx);
margin-left: 8rpx;
vertical-align: middle;
}
}
// ========== style-1: 横向卡片(图左文右) ==========
&.style-1 {
.item {
display: flex;
padding: 20rpx;
.article-img {
margin-right: 20rpx;
width: 160rpx;
height: 160rpx;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
.cover-img {
width: 100%;
height: 100%;
}
}
.info-wrap {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
.title {
font-weight: bold;
margin-bottom: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
/* 标题字号由 CSS 变量驱动(每风格独立) */
font-size: var(--article-title-fs-1, 30rpx);
line-height: 1.4;
}
.desc {
/* 颜色由 CSS 变量驱动(来自 warpWrapStyle 注入;老数据走默认值) */
color: var(--article-abstract-color, #888888);
font-size: 24rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
}
}
// ========== style-2: 大图卡片(上图下文) ==========
&.style-2 {
.item {
padding: 0;
overflow: hidden;
.article-img {
width: 100%;
height: 320rpx;
overflow: hidden;
.cover-img {
width: 100%;
height: 100%;
}
}
.info-wrap {
padding: 20rpx;
.title {
font-weight: bold;
/* 标题字号由 CSS 变量驱动(每风格独立) */
font-size: var(--article-title-fs-2, 30rpx);
line-height: 1.4;
margin-bottom: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.desc {
/* 颜色由 CSS 变量驱动(来自 warpWrapStyle 注入;老数据走默认值) */
color: var(--article-abstract-color, #888888);
font-size: 24rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
margin-bottom: 10rpx;
}
}
}
}
// ========== style-3: 三列网格 ==========
&.style-3 {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
&::after {
content: '';
flex: auto;
}
.item {
width: 32%;
padding: 0;
overflow: hidden;
display: flex;
flex-direction: column;
.article-img {
width: 100%;
height: 200rpx;
overflow: hidden;
.cover-img {
width: 100%;
height: 100%;
}
}
.info-wrap {
padding: 10rpx 14rpx;
.title {
/* 标题字号由 CSS 变量驱动(每风格独立) */
font-size: var(--article-title-fs-3, 24rpx);
font-weight: bold;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.read-wrap {
.category-icon,
.category-name {
display: none;
}
font-size: 20rpx;
}
}
}
}
// ========== style-4: 纯文字列表(无封面) ==========
&.style-4 {
.item {
padding: 20rpx;
margin-bottom: 0;
/* 分割线颜色由 CSS 变量驱动 */
border-bottom: 2rpx dashed var(--article-divider-color, #eeeeee);
&:last-of-type {
border-bottom: 0;
}
.info-wrap {
.title {
/* 标题字号由 CSS 变量驱动(每风格独立) */
font-size: var(--article-title-fs-4, 28rpx);
font-weight: bold;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
// 空数据占位
.empty-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 0;
.empty-text {
color: #999;
font-size: 26rpx;
}
}
}
}
</style>