- diy-card-common.js(新): 设计 token 层 —— 颜色/字阶/间距/圆角/阴影/动效/ 触达尺寸 + px2rpx/flagOn/initialOf/avatarBg/themeGradient/visibleRows 助手 - diy-card-mini.vue(新): 共享名片卡基件 —— 封面+遮罩/渐变饰条/Logo/主题色 头像(首字兜底)/资料行折叠(showRows 总开关+maxRows 上限+展开收起)/快捷操作 芯片(首个主动作实心, 72rpx 触达)/二维码/详情按钮/骨架屏 shimmer/badge 插槽 - diy-card-entry.vue: 三预设(gradient/solid/minimal)×两布局(left/center)× 图标装饰(内置字形/图片)×按钮四变体(solid/ghost/link/none) - diy-card-list.vue: 三布局(grid 双列/list/scroll 横滑)+骨架屏+逐项 fade-up 入场+标题行(计数/更多)+企业徽章+空态 - diy-card-show.vue / diy-card-self.vue: 改为基件包装; self 管登录态 (hasToken watch 刷新)+兜底(fallbackMode)+空态 CTA+「我的名片」角标 - 修复死配置: 面板「显示资料行」showRows 此前被 C 端丢弃, 现由基件消费 (历史配置无此字段时默认显示, 兼容 1.0) - 规范: 禁 CSS 变量, 动态样式走 $util.objToStyle, rpx 尺寸
500 lines
12 KiB
Vue
500 lines
12 KiB
Vue
<template>
|
||
<view data-component-name="diy-card-list" class="diy-card-list">
|
||
<view class="card-wall" :style="wallStyle">
|
||
<!-- 标题行: 标题 + 数量 + 查看全部 -->
|
||
<view v-if="showTitle || showCount || showMore" class="wall-head">
|
||
<text v-if="showTitle" class="wall-title" :style="titleStyle">{{ value.title || '团队名片' }}</text>
|
||
<text v-if="showCount && list.length" class="wall-count" :style="countStyle">{{ list.length }} 人</text>
|
||
<view class="head-spacer"></view>
|
||
<text v-if="showMore" class="wall-more" :style="moreStyle" hover-class="btn-press" @tap="goMore">{{ moreText || '查看全部' }} ›</text>
|
||
</view>
|
||
|
||
<!-- 骨架屏 -->
|
||
<view v-if="loading && !list.length" class="wall-skeleton">
|
||
<view v-for="n in skeletonCount" :key="'sk' + n" class="sk-item">
|
||
<view class="sk sk-avatar"></view>
|
||
<view class="sk-col">
|
||
<view class="sk sk-line" style="width:50%"></view>
|
||
<view class="sk sk-line" style="width:70%;margin-top:14rpx;"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 横滑布局 -->
|
||
<scroll-view v-else-if="layout === 'scroll' && list.length" class="wall-scroll" scroll-x show-scrollbar="false">
|
||
<view v-for="(item, index) in list" :key="'s' + index" class="scroll-item fade-up" :style="scrollItemStyle" hover-class="btn-press" @tap="goDetail(item)">
|
||
<view class="item-avatar" :style="avatarStyle(item)">
|
||
<image v-if="item.avatar" class="avatar-img" :src="$util.img(item.avatar)" mode="aspectFill" />
|
||
<text v-else class="avatar-initial">{{ initialOf(item.name) }}</text>
|
||
</view>
|
||
<text class="scroll-name" :style="nameStyle">{{ item.name || '未命名名片' }}</text>
|
||
<text v-if="infoLine(item)" class="scroll-meta" :style="infoStyle">{{ infoLine(item) }}</text>
|
||
<text v-if="isEnterprise(item)" class="item-badge" :style="badgeStyle(item)">企业</text>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 网格/列表布局 -->
|
||
<view v-else-if="list.length" :class="['wall-grid', layout === 'list' ? 'is-list' : 'is-grid']">
|
||
<view v-for="(item, index) in list" :key="'g' + index" class="wall-item fade-up"
|
||
:style="[itemStyle, { animationDelay: (index * 60) + 'ms' }]" hover-class="btn-press" @tap="goDetail(item)">
|
||
<view class="item-avatar" :style="avatarStyle(item)">
|
||
<image v-if="item.avatar" class="avatar-img" :src="$util.img(item.avatar)" mode="aspectFill" />
|
||
<text v-else class="avatar-initial">{{ initialOf(item.name) }}</text>
|
||
</view>
|
||
<view class="item-info">
|
||
<view class="name-row">
|
||
<text class="item-name" :style="nameStyle">{{ item.name || '未命名名片' }}</text>
|
||
<text v-if="isEnterprise(item)" class="item-badge" :style="badgeStyle(item)">企业</text>
|
||
</view>
|
||
<text v-if="infoLine(item)" class="item-meta" :style="infoStyle">{{ infoLine(item) }}</text>
|
||
</view>
|
||
<text v-if="showArrow" class="item-arrow" :style="infoStyle">›</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 空态 -->
|
||
<view v-else-if="!loading" class="wall-empty" :style="infoStyle">
|
||
<text class="empty-icon">🗂️</text>
|
||
<text class="empty-tip">{{ cardIdsConfigured ? '暂无可展示的名片' : '商家还未挑选名片' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
// 数字名片 · 团队名片墙(装修组件)
|
||
// 布局: grid(1/2 列) / list(整行) / scroll(横滑); 骨架屏 + 逐项入场 + 按压反馈
|
||
import { CardTokens, px2rpx, flagOn, num, initialOf, avatarBg } from './diy-card-common.js';
|
||
|
||
export default {
|
||
name: 'diy-card-list',
|
||
props: {
|
||
value: { type: Object }
|
||
},
|
||
data() {
|
||
return {
|
||
list: [],
|
||
loading: false,
|
||
};
|
||
},
|
||
computed: {
|
||
v() {
|
||
return this.value || {};
|
||
},
|
||
cardIds() {
|
||
return String(this.v.cardIds || '')
|
||
.split(',')
|
||
.map(s => s.trim())
|
||
.filter(Boolean)
|
||
.slice(0, 20)
|
||
.join(',');
|
||
},
|
||
cardIdsConfigured() {
|
||
return String(this.v.cardIds || '').trim() !== '';
|
||
},
|
||
layout() {
|
||
const l = String(this.v.layout || 'grid');
|
||
return ['grid', 'list', 'scroll'].includes(l) ? l : 'grid';
|
||
},
|
||
columns() {
|
||
return Number(this.v.columns) === 1 ? 1 : 2;
|
||
},
|
||
showTitle() {
|
||
return String(this.v.title || '').trim() !== '';
|
||
},
|
||
showCount() {
|
||
return flagOn(this.v.showCount, false);
|
||
},
|
||
showMore() {
|
||
return flagOn(this.v.showMore, false);
|
||
},
|
||
moreText() {
|
||
return this.v.moreText || '';
|
||
},
|
||
showArrow() {
|
||
return flagOn(this.v.showArrow, true);
|
||
},
|
||
skeletonCount() {
|
||
return this.layout === 'list' ? 3 : this.columns * 2;
|
||
},
|
||
wallStyle() {
|
||
return this.$util.objToStyle({
|
||
background: this.v.bgColor || CardTokens.color.bg,
|
||
borderRadius: px2rpx(num(this.v.borderRadius, CardTokens.radius.md)) + 'rpx',
|
||
padding: px2rpx(num(this.v.padding, CardTokens.space.md)) + 'rpx',
|
||
});
|
||
},
|
||
titleStyle() {
|
||
return this.$util.objToStyle({ color: this.v.titleColor || CardTokens.color.text });
|
||
},
|
||
nameStyle() {
|
||
return this.$util.objToStyle({ color: this.v.nameColor || CardTokens.color.text });
|
||
},
|
||
infoStyle() {
|
||
return this.$util.objToStyle({ color: this.v.infoColor || CardTokens.color.textSub });
|
||
},
|
||
countStyle() {
|
||
return this.$util.objToStyle({
|
||
color: CardTokens.color.primary,
|
||
background: 'rgba(47,107,255,0.08)',
|
||
});
|
||
},
|
||
moreStyle() {
|
||
return this.$util.objToStyle({ color: CardTokens.color.textLabel });
|
||
},
|
||
itemStyle() {
|
||
const gap = num(this.v.gap, 10);
|
||
const width = this.layout === 'list' || this.columns === 1 ? '100%' : 'calc(50% - ' + px2rpx(gap / 2) + 'rpx)';
|
||
return this.$util.objToStyle({
|
||
width: width,
|
||
marginBottom: px2rpx(gap) + 'rpx',
|
||
});
|
||
},
|
||
scrollItemStyle() {
|
||
return this.$util.objToStyle({
|
||
width: '260rpx',
|
||
marginRight: px2rpx(num(this.v.gap, 10)) + 'rpx',
|
||
});
|
||
},
|
||
},
|
||
watch: {
|
||
'value.cardIds'() {
|
||
this.fetchData();
|
||
},
|
||
},
|
||
mounted() {
|
||
this.fetchData();
|
||
},
|
||
methods: {
|
||
initialOf,
|
||
avatarStyle(item) {
|
||
return this.$util.objToStyle({ background: avatarBg(item) });
|
||
},
|
||
badgeStyle(item) {
|
||
return this.$util.objToStyle({ background: avatarBg(item) });
|
||
},
|
||
isEnterprise(item) {
|
||
return item && item.card_type === 'enterprise';
|
||
},
|
||
infoLine(item) {
|
||
const showJob = flagOn(this.v.showJob, true);
|
||
const showCompany = flagOn(this.v.showCompany, true);
|
||
const parts = [];
|
||
if (showJob && item.job_title) parts.push(item.job_title);
|
||
if (showCompany && item.company) parts.push(item.company);
|
||
return parts.join(' · ');
|
||
},
|
||
fetchData() {
|
||
if (this.cardIds === '') {
|
||
this.list = [];
|
||
return;
|
||
}
|
||
this.loading = true;
|
||
this.$api.sendRequest({
|
||
url: '/card/api/diy/cards',
|
||
data: { card_ids: this.cardIds },
|
||
success: res => {
|
||
this.loading = false;
|
||
if (res.code == 0 && res.data) {
|
||
this.list = res.data.list || [];
|
||
}
|
||
},
|
||
fail: () => {
|
||
this.loading = false;
|
||
},
|
||
});
|
||
},
|
||
goDetail(item) {
|
||
if (!item || !item.card_id) return;
|
||
this.$util.redirectTo('/pages_tool/card/detail', { card_id: item.card_id });
|
||
},
|
||
goMore() {
|
||
const mode = String(this.v.moreLinkMode || 'list');
|
||
let url = '/pages_tool/card/list';
|
||
if (mode === 'contacts') {
|
||
url = '/pages_tool/card/contacts';
|
||
} else if (mode === 'custom') {
|
||
url = String(this.v.moreLinkUrl || '').trim();
|
||
if (url === '' || url.indexOf('/') !== 0) {
|
||
uni.showToast({ title: '自定义路径需以 / 开头', icon: 'none' });
|
||
return;
|
||
}
|
||
}
|
||
this.$util.redirectTo(url);
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.diy-card-list {
|
||
width: 100%;
|
||
|
||
.card-wall {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
|
||
.wall-head {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
|
||
.wall-title {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.wall-count {
|
||
font-size: 20rpx;
|
||
margin-left: 12rpx;
|
||
padding: 2rpx 12rpx;
|
||
border-radius: 999rpx;
|
||
}
|
||
|
||
.head-spacer {
|
||
flex: 1;
|
||
}
|
||
|
||
.wall-more {
|
||
font-size: 22rpx;
|
||
padding: 8rpx 0 8rpx 16rpx;
|
||
}
|
||
}
|
||
|
||
/* 网格/列表 */
|
||
.wall-grid {
|
||
&.is-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.wall-item {
|
||
box-sizing: border-box;
|
||
display: flex;
|
||
align-items: center;
|
||
border: 2rpx solid #f0f0f0;
|
||
border-radius: 16rpx;
|
||
padding: 20rpx;
|
||
overflow: hidden;
|
||
|
||
.item-avatar {
|
||
flex-shrink: 0;
|
||
width: 88rpx;
|
||
height: 88rpx;
|
||
border-radius: 50%;
|
||
overflow: hidden;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
.avatar-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
}
|
||
|
||
.avatar-initial {
|
||
font-size: 36rpx;
|
||
font-weight: 700;
|
||
color: #ffffff;
|
||
}
|
||
}
|
||
|
||
.item-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
margin-left: 16rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.name-row {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.item-name {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.item-badge {
|
||
flex-shrink: 0;
|
||
margin-left: 10rpx;
|
||
font-size: 18rpx;
|
||
color: #ffffff;
|
||
padding: 2rpx 10rpx;
|
||
border-radius: 6rpx;
|
||
}
|
||
}
|
||
|
||
.item-meta {
|
||
font-size: 22rpx;
|
||
margin-top: 4rpx;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
.item-arrow {
|
||
flex-shrink: 0;
|
||
margin-left: 8rpx;
|
||
font-size: 32rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 横滑 */
|
||
.wall-scroll {
|
||
width: 100%;
|
||
white-space: nowrap;
|
||
|
||
.scroll-item {
|
||
display: inline-flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
box-sizing: border-box;
|
||
border: 2rpx solid #f0f0f0;
|
||
border-radius: 16rpx;
|
||
padding: 24rpx 16rpx;
|
||
position: relative;
|
||
|
||
.item-avatar {
|
||
width: 104rpx;
|
||
height: 104rpx;
|
||
border-radius: 50%;
|
||
overflow: hidden;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
.avatar-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
}
|
||
|
||
.avatar-initial {
|
||
font-size: 42rpx;
|
||
font-weight: 700;
|
||
color: #ffffff;
|
||
}
|
||
}
|
||
|
||
.scroll-name {
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
margin-top: 12rpx;
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.scroll-meta {
|
||
font-size: 20rpx;
|
||
margin-top: 4rpx;
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.item-badge {
|
||
position: absolute;
|
||
top: 12rpx;
|
||
right: 12rpx;
|
||
font-size: 18rpx;
|
||
color: #ffffff;
|
||
padding: 2rpx 10rpx;
|
||
border-radius: 6rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 空态 */
|
||
.wall-empty {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 40rpx 0;
|
||
|
||
.empty-icon {
|
||
font-size: 56rpx;
|
||
}
|
||
|
||
.empty-tip {
|
||
font-size: 24rpx;
|
||
margin-top: 12rpx;
|
||
}
|
||
}
|
||
|
||
/* 骨架屏 */
|
||
.wall-skeleton {
|
||
.sk-item {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
|
||
.sk-avatar {
|
||
width: 88rpx;
|
||
height: 88rpx;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.sk-col {
|
||
flex: 1;
|
||
margin-left: 16rpx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.sk {
|
||
background: linear-gradient(90deg, #f2f3f5 25%, #e8eaee 37%, #f2f3f5 63%);
|
||
background-size: 400rpx 100%;
|
||
animation: sk-shimmer 1.2s linear infinite;
|
||
border-radius: 8rpx;
|
||
|
||
&.sk-line {
|
||
height: 24rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.fade-up {
|
||
animation: card-fade-up 240ms ease both;
|
||
}
|
||
|
||
.btn-press {
|
||
opacity: 0.72;
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
@keyframes sk-shimmer {
|
||
0% {
|
||
background-position: -200rpx 0;
|
||
}
|
||
|
||
100% {
|
||
background-position: 200rpx 0;
|
||
}
|
||
}
|
||
|
||
@keyframes card-fade-up {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(12rpx);
|
||
}
|
||
|
||
to {
|
||
opacity: 1;
|
||
transform: none;
|
||
}
|
||
}
|
||
</style>
|