- 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 尺寸
583 lines
14 KiB
Vue
583 lines
14 KiB
Vue
<template>
|
|
<view data-component-name="diy-card-mini" class="card-mini">
|
|
<!-- 骨架屏: 首屏加载/切换名片 -->
|
|
<view v-if="loading" class="mini-skeleton" :style="wrapStyle">
|
|
<view class="sk-row">
|
|
<view class="sk sk-avatar"></view>
|
|
<view class="sk-col">
|
|
<view class="sk sk-line" style="width:55%"></view>
|
|
<view class="sk sk-line" style="width:35%;margin-top:16rpx;"></view>
|
|
</view>
|
|
</view>
|
|
<view class="sk sk-line" style="width:100%;margin-top:28rpx;"></view>
|
|
<view class="sk sk-line" style="width:72%;margin-top:16rpx;"></view>
|
|
<view class="sk sk-btn" style="margin-top:28rpx;"></view>
|
|
</view>
|
|
|
|
<!-- 空态: 未拿到名片 -->
|
|
<view v-else-if="!card" class="mini-empty" :style="wrapStyle">
|
|
<text class="empty-icon">💳</text>
|
|
<text class="empty-tip" :style="subTextStyle">{{ emptyTip || '暂无名片数据' }}</text>
|
|
<view v-if="emptyActionText" class="empty-btn" :style="emptyBtnStyle" hover-class="btn-press" @tap="onEmptyAction">
|
|
<text class="empty-btn-text">{{ emptyActionText }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 名片卡 -->
|
|
<view v-else class="mini-card fade-up" :style="wrapStyle">
|
|
<!-- 角标插槽(如 self 组件的「我的名片」) -->
|
|
<view class="mini-badge-slot">
|
|
<slot name="badge"></slot>
|
|
</view>
|
|
<!-- 封面图(可选) + 遮罩 -->
|
|
<image v-if="showCover" class="mini-cover" :src="$util.img(card.cover)" mode="aspectFill" />
|
|
<view v-if="showCover" class="mini-cover-scrim"></view>
|
|
|
|
<!-- 主题渐变饰条(可选) -->
|
|
<view v-if="showGradient" class="mini-gradient" :style="gradientStyle"></view>
|
|
|
|
<!-- Logo(可选, 右上) -->
|
|
<image v-if="showLogo && card.logo" class="mini-logo" :src="$util.img(card.logo)" mode="aspectFit" />
|
|
|
|
<view class="mini-head">
|
|
<view class="mini-avatar" :style="avatarStyle">
|
|
<image v-if="card.avatar" class="avatar-img" :src="$util.img(card.avatar)" mode="aspectFill" />
|
|
<text v-else class="avatar-initial">{{ initialOf(card.name) }}</text>
|
|
</view>
|
|
<view class="mini-head-info">
|
|
<view class="name-row">
|
|
<text class="mini-name" :style="nameStyle">{{ card.name || '未命名名片' }}</text>
|
|
<text v-if="isEnterprise" class="mini-badge" :style="badgeStyle">企业</text>
|
|
</view>
|
|
<text v-if="card.card_title" class="mini-card-title" :style="subTextStyle">{{ card.card_title }}</text>
|
|
<text v-if="subLine" class="mini-sub" :style="subTextStyle">{{ subLine }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 资料行(可折叠) -->
|
|
<view v-if="showRowsBlock && shownRows.length" class="mini-rows">
|
|
<view v-for="(row, i) in shownRows" :key="i" class="mini-row">
|
|
<text class="row-label" :style="labelStyle">{{ row.label }}</text>
|
|
<text class="row-value" :style="valueStyle">{{ row.value }}</text>
|
|
</view>
|
|
<view v-if="hasMoreRows" class="row-toggle" hover-class="btn-press" @tap="expanded = !expanded">
|
|
<text class="toggle-text" :style="toggleStyle">{{ expanded ? '收起资料' : '展开全部资料(' + hiddenCount + ')' }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 快捷操作(芯片, 最小触达 72rpx; 首个为主动作) -->
|
|
<view v-if="showActs && acts.length" class="mini-acts">
|
|
<view v-for="(act, i) in acts" :key="i" class="act-chip"
|
|
:style="i === 0 ? actPrimaryStyle : actGhostStyle" hover-class="btn-press" @tap.stop="onAct(act)">
|
|
<text class="act-text" :style="i === 0 ? actPrimaryTextStyle : actGhostTextStyle">{{ act.label }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 二维码(可选) -->
|
|
<view v-if="showQr && card.qr_url" class="mini-qr">
|
|
<image class="qr-img" :src="$util.img(card.qr_url)" mode="aspectFit" />
|
|
<text class="qr-tip" :style="labelStyle">{{ qrTip || '扫码查看完整名片' }}</text>
|
|
</view>
|
|
|
|
<!-- 查看完整名片 -->
|
|
<view v-if="showDetailBtn" class="mini-detail" :style="detailBtnStyle" hover-class="btn-press" @tap.stop="onDetail">
|
|
<text class="detail-text">{{ detailBtnText || '查看完整名片' }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
// 数字名片 · 名片迷你卡基件(show/self 共用, 设计系统单一事实来源)
|
|
// 消费名片自身设计资产: cover 封面 / logo / theme_color / 渐变末色与角度 / card_title
|
|
import {
|
|
CardTokens, px2rpx, flagOn, num, initialOf, avatarBg, themeGradient, visibleRows,
|
|
} from './diy-card-common.js';
|
|
|
|
export default {
|
|
name: 'diy-card-mini',
|
|
props: {
|
|
card: { type: Object, default: null },
|
|
config: { type: Object, default: () => ({}) },
|
|
loading: { type: Boolean, default: false },
|
|
emptyTip: { type: String, default: '' },
|
|
emptyActionText: { type: String, default: '' },
|
|
},
|
|
data() {
|
|
return { expanded: false };
|
|
},
|
|
computed: {
|
|
c() {
|
|
return this.config || {};
|
|
},
|
|
wrapStyle() {
|
|
const c = this.c;
|
|
return this.$util.objToStyle({
|
|
background: flagOn(c.useCover, false) && this.card && this.card.cover ? 'transparent' : (c.bgColor || CardTokens.color.bg),
|
|
borderRadius: px2rpx(num(c.borderRadius, CardTokens.radius.md)) + 'rpx',
|
|
padding: px2rpx(num(c.padding, CardTokens.space.lg)) + 'rpx',
|
|
boxShadow: flagOn(c.showShadow, true) ? CardTokens.shadow.card : 'none',
|
|
});
|
|
},
|
|
showCover() {
|
|
return flagOn(this.c.useCover, false) && !!(this.card && this.card.cover);
|
|
},
|
|
showGradient() {
|
|
return flagOn(this.c.showGradient, false);
|
|
},
|
|
showLogo() {
|
|
return flagOn(this.c.showLogo, false);
|
|
},
|
|
showActs() {
|
|
return flagOn(this.c.showQuick, true);
|
|
},
|
|
showRowsBlock() {
|
|
// 资料行总开关(面板「显示资料行」); 历史配置无此字段时默认显示, 兼容 1.0
|
|
return flagOn(this.c.showRows, true);
|
|
},
|
|
showQr() {
|
|
return flagOn(this.c.showQr, false);
|
|
},
|
|
showDetailBtn() {
|
|
return flagOn(this.c.showDetailBtn, true);
|
|
},
|
|
qrTip() {
|
|
return this.c.qrTip || '';
|
|
},
|
|
detailBtnText() {
|
|
return this.c.detailBtnText || '';
|
|
},
|
|
maxRows() {
|
|
return num(this.c.maxRows, 4);
|
|
},
|
|
rowsInfo() {
|
|
if (!this.card) return { shown: [], hasMore: false };
|
|
return visibleRows(this.card.rows, this.expanded ? 0 : this.maxRows);
|
|
},
|
|
shownRows() {
|
|
return this.rowsInfo.shown;
|
|
},
|
|
hasMoreRows() {
|
|
return !this.expanded && this.rowsInfo.hasMore;
|
|
},
|
|
hiddenCount() {
|
|
return this.rowsInfo.rest.length;
|
|
},
|
|
acts() {
|
|
return this.card && Array.isArray(this.card.acts) ? this.card.acts : [];
|
|
},
|
|
isEnterprise() {
|
|
return this.card && this.card.card_type === 'enterprise';
|
|
},
|
|
subLine() {
|
|
const c = this.card || {};
|
|
return [c.job_title, c.company].filter(Boolean).join(' · ');
|
|
},
|
|
gradientStyle() {
|
|
return this.$util.objToStyle({ background: themeGradient(this.card) });
|
|
},
|
|
avatarStyle() {
|
|
return this.$util.objToStyle({ background: avatarBg(this.card) });
|
|
},
|
|
nameStyle() {
|
|
return this.$util.objToStyle({ color: this.c.nameColor || CardTokens.color.text });
|
|
},
|
|
subTextStyle() {
|
|
return this.$util.objToStyle({ color: this.c.infoColor || CardTokens.color.textSub });
|
|
},
|
|
labelStyle() {
|
|
return this.$util.objToStyle({ color: this.c.labelColor || CardTokens.color.textLabel });
|
|
},
|
|
valueStyle() {
|
|
return this.$util.objToStyle({ color: this.c.infoColor || CardTokens.color.textSub });
|
|
},
|
|
badgeStyle() {
|
|
return this.$util.objToStyle({ background: avatarBg(this.card) });
|
|
},
|
|
toggleStyle() {
|
|
return this.$util.objToStyle({ color: (this.card && this.card.theme_color) || CardTokens.color.primary });
|
|
},
|
|
actPrimaryStyle() {
|
|
const theme = (this.card && this.card.theme_color) || CardTokens.color.primary;
|
|
return this.$util.objToStyle({
|
|
background: theme,
|
|
height: px2rpx(CardTokens.touch.chipHeight) + 'rpx',
|
|
padding: '0 ' + px2rpx(CardTokens.space.xl) + 'rpx',
|
|
borderRadius: px2rpx(CardTokens.radius.pill) + 'rpx',
|
|
});
|
|
},
|
|
actPrimaryTextStyle() {
|
|
return this.$util.objToStyle({ color: '#FFFFFF' });
|
|
},
|
|
actGhostStyle() {
|
|
const theme = (this.card && this.card.theme_color) || CardTokens.color.primary;
|
|
return this.$util.objToStyle({
|
|
background: 'transparent',
|
|
border: '2rpx solid ' + theme,
|
|
height: px2rpx(CardTokens.touch.chipHeight) + 'rpx',
|
|
padding: '0 ' + px2rpx(CardTokens.space.lg) + 'rpx',
|
|
borderRadius: px2rpx(CardTokens.radius.pill) + 'rpx',
|
|
});
|
|
},
|
|
actGhostTextStyle() {
|
|
return this.$util.objToStyle({ color: (this.card && this.card.theme_color) || CardTokens.color.primary });
|
|
},
|
|
detailBtnStyle() {
|
|
return this.$util.objToStyle({
|
|
background: themeGradient(this.card),
|
|
height: px2rpx(CardTokens.touch.btnHeight) + 'rpx',
|
|
borderRadius: px2rpx(CardTokens.radius.pill) + 'rpx',
|
|
});
|
|
},
|
|
emptyBtnStyle() {
|
|
return this.$util.objToStyle({
|
|
background: CardTokens.color.primary,
|
|
height: px2rpx(CardTokens.touch.btnHeight) + 'rpx',
|
|
borderRadius: px2rpx(CardTokens.radius.pill) + 'rpx',
|
|
padding: '0 ' + px2rpx(CardTokens.space.xxl) + 'rpx',
|
|
});
|
|
},
|
|
},
|
|
methods: {
|
|
onAct(act) {
|
|
this.$emit('act', act);
|
|
},
|
|
onDetail() {
|
|
this.$emit('detail', this.card);
|
|
},
|
|
onEmptyAction() {
|
|
this.$emit('empty-action');
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.card-mini {
|
|
width: 100%;
|
|
|
|
.mini-card {
|
|
position: relative;
|
|
overflow: hidden;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
|
|
.mini-badge-slot {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
z-index: 4;
|
|
}
|
|
|
|
.mini-cover {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 0;
|
|
}
|
|
|
|
.mini-cover-scrim {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(15, 23, 42, 0.45);
|
|
z-index: 1;
|
|
}
|
|
|
|
.mini-gradient {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
height: 10rpx;
|
|
z-index: 2;
|
|
}
|
|
|
|
.mini-logo {
|
|
position: absolute;
|
|
right: 24rpx;
|
|
top: 24rpx;
|
|
width: 64rpx;
|
|
height: 64rpx;
|
|
z-index: 3;
|
|
}
|
|
|
|
.mini-head {
|
|
position: relative;
|
|
z-index: 2;
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
.mini-avatar {
|
|
flex-shrink: 0;
|
|
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;
|
|
}
|
|
}
|
|
|
|
.mini-head-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
margin-left: 24rpx;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
.name-row {
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
.mini-name {
|
|
font-size: 34rpx;
|
|
font-weight: 700;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.mini-badge {
|
|
flex-shrink: 0;
|
|
margin-left: 12rpx;
|
|
font-size: 18rpx;
|
|
color: #ffffff;
|
|
padding: 2rpx 12rpx;
|
|
border-radius: 8rpx;
|
|
}
|
|
}
|
|
|
|
.mini-card-title {
|
|
font-size: 26rpx;
|
|
margin-top: 6rpx;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.mini-sub {
|
|
font-size: 24rpx;
|
|
margin-top: 6rpx;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
}
|
|
}
|
|
|
|
.mini-rows {
|
|
position: relative;
|
|
z-index: 2;
|
|
margin-top: 24rpx;
|
|
|
|
.mini-row {
|
|
display: flex;
|
|
line-height: 2;
|
|
|
|
.row-label {
|
|
flex-shrink: 0;
|
|
width: 96rpx;
|
|
font-size: 24rpx;
|
|
}
|
|
|
|
.row-value {
|
|
flex: 1;
|
|
font-size: 24rpx;
|
|
word-break: break-all;
|
|
}
|
|
}
|
|
|
|
.row-toggle {
|
|
margin-top: 8rpx;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
|
|
.toggle-text {
|
|
font-size: 22rpx;
|
|
font-weight: 500;
|
|
}
|
|
}
|
|
}
|
|
|
|
.mini-acts {
|
|
position: relative;
|
|
z-index: 2;
|
|
margin-top: 24rpx;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
|
|
.act-chip {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-sizing: border-box;
|
|
margin: 0 16rpx 16rpx 0;
|
|
|
|
.act-text {
|
|
font-size: 24rpx;
|
|
font-weight: 500;
|
|
}
|
|
}
|
|
}
|
|
|
|
.mini-qr {
|
|
position: relative;
|
|
z-index: 2;
|
|
margin-top: 24rpx;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
|
|
.qr-img {
|
|
width: 180rpx;
|
|
height: 180rpx;
|
|
display: block;
|
|
}
|
|
|
|
.qr-tip {
|
|
font-size: 22rpx;
|
|
margin-top: 12rpx;
|
|
}
|
|
}
|
|
|
|
.mini-detail {
|
|
position: relative;
|
|
z-index: 2;
|
|
margin-top: 24rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
|
|
.detail-text {
|
|
font-size: 26rpx;
|
|
font-weight: 600;
|
|
color: #ffffff;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 空态 */
|
|
.mini-empty {
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 48rpx 24rpx;
|
|
|
|
.empty-icon {
|
|
font-size: 64rpx;
|
|
}
|
|
|
|
.empty-tip {
|
|
font-size: 24rpx;
|
|
margin-top: 16rpx;
|
|
text-align: center;
|
|
}
|
|
|
|
.empty-btn {
|
|
margin-top: 28rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
|
|
.empty-btn-text {
|
|
font-size: 26rpx;
|
|
font-weight: 600;
|
|
color: #ffffff;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 骨架屏 */
|
|
.mini-skeleton {
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
|
|
.sk-row {
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
.sk-avatar {
|
|
width: 104rpx;
|
|
height: 104rpx;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sk-col {
|
|
flex: 1;
|
|
margin-left: 24rpx;
|
|
}
|
|
}
|
|
|
|
.sk-btn {
|
|
height: 80rpx;
|
|
border-radius: 999rpx;
|
|
}
|
|
}
|
|
|
|
.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: 28rpx;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 入场动效(逐项延迟由父组件传 animation-delay) */
|
|
.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>
|