feat(diy-card): 名片 DIY 组件 2.0 视觉与功能升级(原生渲染, 禁 webview)
- 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 尺寸
This commit is contained in:
130
components-diy/diy-card-common.js
Normal file
130
components-diy/diy-card-common.js
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* 名片 DIY 组件 · 共享设计系统层
|
||||
*
|
||||
* 目标(UI Designer 设计系统先行原则):
|
||||
* - 4 个组件共用同一套 token(颜色/字阶/间距/圆角/阴影/动效时长)
|
||||
* - 通用 helpers(px2rpx/头像兜底色/显隐开关解析)集中一处, 避免组件间漂移
|
||||
* - 严格遵循小程序规范: 禁 CSS 变量, 动态样式一律 $util.objToStyle, 单位 rpx
|
||||
*
|
||||
* 注: 本文件为纯 JS(无 wxss), 小程序/H5 通用。
|
||||
*/
|
||||
|
||||
export const CardTokens = {
|
||||
color: {
|
||||
primary: '#2F6BFF', // 主题默认蓝(名片 wap 公开页同色系)
|
||||
text: '#1F2329', // 主文字
|
||||
textSub: '#8A8F99', // 次文字(公司/职位)
|
||||
textLabel: '#B0B4BC', // 字段标签
|
||||
line: '#EEF0F3', // 分割线
|
||||
bg: '#FFFFFF', // 卡片底
|
||||
skeleton: '#F2F3F5', // 骨架块
|
||||
skeletonHi: '#E8EAEE', // 骨架高亮扫过
|
||||
scrim: 'rgba(15, 23, 42, 0.45)', // 封面图遮罩(对比度保障)
|
||||
},
|
||||
radius: {
|
||||
sm: 8,
|
||||
md: 16,
|
||||
lg: 24,
|
||||
pill: 999,
|
||||
},
|
||||
/** 字阶(px, 输出时 ×2 转 rpx) */
|
||||
fontSize: {
|
||||
display: 40, // 入口主标题
|
||||
title: 32, // 分组标题/名片姓名
|
||||
name: 30,
|
||||
meta: 24, // 公司/职位/字段值
|
||||
caption: 22, // 辅助说明
|
||||
},
|
||||
/** 间距(px) */
|
||||
space: {
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 12,
|
||||
lg: 16,
|
||||
xl: 20,
|
||||
xxl: 24,
|
||||
},
|
||||
/** 阴影 */
|
||||
shadow: {
|
||||
card: '0 4rpx 16rpx rgba(15, 23, 42, 0.06)',
|
||||
raised: '0 8rpx 24rpx rgba(15, 23, 42, 0.10)',
|
||||
},
|
||||
/** 动效时长(ms) */
|
||||
duration: {
|
||||
fast: 150,
|
||||
normal: 240,
|
||||
slow: 320,
|
||||
},
|
||||
/** 最小触达尺寸(rpx, 小程序可点性下限 44px≈88rpx, 芯片取 72rpx 高度) */
|
||||
touch: {
|
||||
chipHeight: 72,
|
||||
btnHeight: 80,
|
||||
avatar: 104,
|
||||
},
|
||||
};
|
||||
|
||||
/** px → rpx(组件配置字段均为 px 口径) */
|
||||
export function px2rpx(v) {
|
||||
const n = Number(v);
|
||||
return isNaN(n) ? 0 : Math.round(n * 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显隐开关解析: 兼容 true/false、1/0、"1"/"0"、"true"/"false"
|
||||
* 未下发(undefined)时返回 fallback(由调用方决定默认值)
|
||||
*/
|
||||
export function flagOn(v, fallback) {
|
||||
if (v === undefined || v === null || v === '') return fallback;
|
||||
if (v === true || v === 1 || v === '1' || v === 'true') return true;
|
||||
if (v === false || v === 0 || v === '0' || v === 'false') return false;
|
||||
return !!v;
|
||||
}
|
||||
|
||||
/** 姓名首字兜底(头像无图时) */
|
||||
export function initialOf(name) {
|
||||
const s = String(name || '').trim();
|
||||
return s ? s.charAt(0) : '名';
|
||||
}
|
||||
|
||||
/** 由字符串确定性派生色相(同名同色, 列表视觉稳定) */
|
||||
export function hueOf(str) {
|
||||
const s = String(str || '');
|
||||
let h = 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
h = (h * 31 + s.charCodeAt(i)) % 360;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/** 头像兜底底色: 主题色优先, 否则按名片确定性色相 */
|
||||
export function avatarBg(card) {
|
||||
const theme = card && card.theme_color;
|
||||
if (theme) return theme;
|
||||
const h = hueOf((card && (card.card_id || card.name)) || 'card');
|
||||
return 'hsl(' + h + ', 62%, 55%)';
|
||||
}
|
||||
|
||||
/** 主题渐变(头部饰条/主按钮): 有渐变末色用双色, 否则单色 */
|
||||
export function themeGradient(card, angle) {
|
||||
const theme = (card && card.theme_color) || CardTokens.color.primary;
|
||||
const end = card && card.gradient_end;
|
||||
const deg = (card && card.gradient_angle) || angle || 135;
|
||||
return end
|
||||
? 'linear-gradient(' + deg + 'deg, ' + theme + ' 0%, ' + end + ' 100%)'
|
||||
: 'linear-gradient(' + deg + 'deg, ' + theme + ' 20%, ' + theme + ' 100%)';
|
||||
}
|
||||
|
||||
/** 安全数字(配置字段) */
|
||||
export function num(v, dft) {
|
||||
const n = Number(v);
|
||||
return isNaN(n) ? dft : n;
|
||||
}
|
||||
|
||||
/** 字段行: 过滤空值, 限制条数(其余折叠) */
|
||||
export function visibleRows(rows, max) {
|
||||
const list = Array.isArray(rows) ? rows.filter(r => r && String(r.value || '').trim() !== '') : [];
|
||||
if (max && max > 0 && list.length > max) {
|
||||
return { shown: list.slice(0, max), rest: list.slice(max), hasMore: true };
|
||||
}
|
||||
return { shown: list, rest: [], hasMore: false };
|
||||
}
|
||||
@@ -1,52 +1,152 @@
|
||||
<template>
|
||||
<view data-component-name="diy-card-entry" class="diy-card-entry" :style="wrapStyle" @tap="goLink">
|
||||
<view class="entry-left">
|
||||
<text class="entry-title" :style="textStyle">{{ value.title || '扫名片 · 谈合作' }}</text>
|
||||
<view data-component-name="diy-card-entry" class="diy-card-entry" :class="'entry-' + stylePreset" :style="wrapStyle" hover-class="entry-press" @tap="goLink">
|
||||
<!-- 图标装饰(可选) -->
|
||||
<view v-if="showIcon" class="entry-icon" :style="iconWrapStyle">
|
||||
<image v-if="iconType === 'image' && iconImage" class="icon-img" :src="$util.img(iconImage)" mode="aspectFit" />
|
||||
<text v-else class="icon-glyph">{{ iconGlyph || '💼' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="entry-body">
|
||||
<text class="entry-title" :style="titleStyle">{{ value.title || '扫名片 · 谈合作' }}</text>
|
||||
<text v-if="value.subtitle" class="entry-sub" :style="subStyle">{{ value.subtitle }}</text>
|
||||
</view>
|
||||
<view v-if="value.btnText" class="entry-btn" :style="btnStyle">
|
||||
<text class="entry-btn-text">{{ value.btnText }}</text>
|
||||
|
||||
<!-- 按钮(可选; link 模式为文字箭头) -->
|
||||
<view v-if="buttonStyle !== 'none' && value.btnText" :class="['entry-btn', 'btn-' + buttonStyle]" :style="btnStyle" hover-class="btn-press" hover-stay-time="60">
|
||||
<text class="entry-btn-text" :style="btnTextStyle">{{ value.btnText }}</text>
|
||||
<text v-if="buttonStyle === 'link'" class="btn-arrow" :style="btnTextStyle">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 数字名片 · 名片入口横幅(装修组件, 原生渲染不依赖 webview)
|
||||
import DiyMinx from './minx.js';
|
||||
import golbalConfig from '@/common/js/golbalConfig.js';
|
||||
// 数字名片 · 名片入口横幅(装修组件)
|
||||
// 预设: gradient(渐变) / solid(纯色+主题饰条) / minimal(透明轻量)
|
||||
// 布局: left / center; 按钮: solid / ghost / link / none
|
||||
import { CardTokens, px2rpx, flagOn, num } from './diy-card-common.js';
|
||||
|
||||
export default {
|
||||
name: 'diy-card-entry',
|
||||
props: {
|
||||
value: { type: Object }
|
||||
},
|
||||
mixins: [DiyMinx, golbalConfig],
|
||||
computed: {
|
||||
v() {
|
||||
return this.value || {};
|
||||
},
|
||||
stylePreset() {
|
||||
const p = String(this.v.stylePreset || 'gradient');
|
||||
return ['gradient', 'solid', 'minimal'].includes(p) ? p : 'gradient';
|
||||
},
|
||||
isCenter() {
|
||||
return String(this.v.layout || 'left') === 'center';
|
||||
},
|
||||
showIcon() {
|
||||
return flagOn(this.v.showIcon, false);
|
||||
},
|
||||
iconType() {
|
||||
return String(this.v.iconType || 'icon') === 'image' ? 'image' : 'icon';
|
||||
},
|
||||
iconImage() {
|
||||
return this.v.iconImage || '';
|
||||
},
|
||||
iconGlyph() {
|
||||
return this.v.iconGlyph || '名';
|
||||
},
|
||||
buttonStyle() {
|
||||
const b = String(this.v.buttonStyle || 'solid');
|
||||
return ['solid', 'ghost', 'link', 'none'].includes(b) ? b : 'solid';
|
||||
},
|
||||
isGradient() {
|
||||
return this.stylePreset === 'gradient';
|
||||
},
|
||||
isMinimal() {
|
||||
return this.stylePreset === 'minimal';
|
||||
},
|
||||
themeColor() {
|
||||
return this.v.bgColorStart || CardTokens.color.primary;
|
||||
},
|
||||
wrapStyle() {
|
||||
const v = this.value || {};
|
||||
const v = this.v;
|
||||
const radius = px2rpx(num(v.borderRadius, CardTokens.radius.md)) + 'rpx';
|
||||
const pad = px2rpx(num(v.padding, CardTokens.space.lg)) + 'rpx';
|
||||
if (this.isMinimal) {
|
||||
return this.$util.objToStyle({
|
||||
background: 'transparent',
|
||||
borderRadius: radius,
|
||||
padding: pad,
|
||||
boxShadow: 'none',
|
||||
});
|
||||
}
|
||||
if (this.isGradient) {
|
||||
return this.$util.objToStyle({
|
||||
background: 'linear-gradient(135deg, ' + (v.bgColorStart || CardTokens.color.primary) + ' 0%, ' + (v.bgColorEnd || '#1E40AF') + ' 100%)',
|
||||
borderRadius: radius,
|
||||
padding: pad,
|
||||
minHeight: '120rpx',
|
||||
});
|
||||
}
|
||||
// solid: 主题色底 + 左侧亮饰条
|
||||
return this.$util.objToStyle({
|
||||
background: 'linear-gradient(135deg, ' + (v.bgColorStart || '#2F6BFF') + ' 0%, ' + (v.bgColorEnd || '#1E40AF') + ' 100%)',
|
||||
borderRadius: this.px2rpx(v.borderRadius || 12) + 'rpx',
|
||||
padding: this.px2rpx(v.padding || 16) + 'rpx ' + this.px2rpx(v.padding || 16) + 'rpx'
|
||||
background: v.bgColorStart || CardTokens.color.primary,
|
||||
borderRadius: radius,
|
||||
padding: pad,
|
||||
borderLeft: '8rpx solid ' + (v.bgColorEnd || 'rgba(255,255,255,0.35)'),
|
||||
minHeight: '120rpx',
|
||||
});
|
||||
},
|
||||
textStyle() {
|
||||
return this.$util.objToStyle({ color: (this.value || {}).textColor || '#FFFFFF' });
|
||||
iconWrapStyle() {
|
||||
return this.$util.objToStyle({
|
||||
background: this.isGradient || !this.isMinimal ? 'rgba(255,255,255,0.18)' : 'rgba(47,107,255,0.10)',
|
||||
});
|
||||
},
|
||||
titleStyle() {
|
||||
return this.$util.objToStyle({
|
||||
color: this.v.textColor || (this.isMinimal ? CardTokens.color.text : '#FFFFFF'),
|
||||
});
|
||||
},
|
||||
subStyle() {
|
||||
return this.$util.objToStyle({ color: (this.value || {}).textColor || '#FFFFFF', opacity: 0.85 });
|
||||
return this.$util.objToStyle({
|
||||
color: this.v.textColor || (this.isMinimal ? CardTokens.color.textSub : '#FFFFFF'),
|
||||
opacity: this.isMinimal ? 1 : 0.85,
|
||||
});
|
||||
},
|
||||
btnStyle() {
|
||||
return this.$util.objToStyle({ background: (this.value || {}).accentColor || '#FFEB3B' });
|
||||
}
|
||||
const v = this.v;
|
||||
const radius = px2rpx(CardTokens.radius.pill) + 'rpx';
|
||||
const pad = '0 ' + px2rpx(CardTokens.space.xl) + 'rpx';
|
||||
if (this.buttonStyle === 'ghost') {
|
||||
return this.$util.objToStyle({
|
||||
border: '2rpx solid ' + (v.accentColor || '#FFEB3B'),
|
||||
borderRadius: radius,
|
||||
padding: pad,
|
||||
height: px2rpx(CardTokens.touch.chipHeight) + 'rpx',
|
||||
});
|
||||
}
|
||||
if (this.buttonStyle === 'link') {
|
||||
return this.$util.objToStyle({ padding: '0 0 0 ' + px2rpx(CardTokens.space.sm) + 'rpx' });
|
||||
}
|
||||
return this.$util.objToStyle({
|
||||
background: v.accentColor || '#FFEB3B',
|
||||
borderRadius: radius,
|
||||
padding: pad,
|
||||
height: px2rpx(CardTokens.touch.chipHeight) + 'rpx',
|
||||
});
|
||||
},
|
||||
btnTextStyle() {
|
||||
const v = this.v;
|
||||
if (this.buttonStyle === 'ghost') {
|
||||
return this.$util.objToStyle({ color: v.accentColor || '#FFEB3B' });
|
||||
}
|
||||
if (this.buttonStyle === 'link') {
|
||||
return this.$util.objToStyle({ color: v.textColor || CardTokens.color.primary });
|
||||
}
|
||||
return this.$util.objToStyle({ color: '#333333' });
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
px2rpx(v) {
|
||||
const n = Number(v);
|
||||
return isNaN(n) ? 0 : Math.round(n * 2);
|
||||
},
|
||||
goLink() {
|
||||
const v = this.value || {};
|
||||
const v = this.v;
|
||||
let url = '';
|
||||
if (v.linkMode === 'contacts') {
|
||||
url = '/pages_tool/card/contacts';
|
||||
@@ -64,8 +164,8 @@ export default {
|
||||
return;
|
||||
}
|
||||
this.$util.redirectTo(url);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -75,10 +175,39 @@ export default {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
|
||||
.entry-left {
|
||||
&.entry-center {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
.entry-body {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.entry-icon {
|
||||
flex-shrink: 0;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
|
||||
.icon-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.icon-glyph {
|
||||
font-size: 48rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.entry-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
@@ -88,6 +217,11 @@ export default {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.entry-sub {
|
||||
@@ -105,14 +239,29 @@ export default {
|
||||
.entry-btn {
|
||||
flex-shrink: 0;
|
||||
margin-left: 20rpx;
|
||||
padding: 12rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
.entry-btn-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.btn-arrow {
|
||||
font-size: 28rpx;
|
||||
margin-left: 2rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.entry-press {
|
||||
transform: scale(0.985);
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.btn-press {
|
||||
opacity: 0.72;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,24 +1,61 @@
|
||||
<template>
|
||||
<view data-component-name="diy-card-list" class="diy-card-list">
|
||||
<view class="card-wall" :style="wallStyle">
|
||||
<text v-if="value.title" class="wall-title" :style="titleStyle">{{ value.title }}</text>
|
||||
<!-- 标题行: 标题 + 数量 + 查看全部 -->
|
||||
<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="list.length" class="wall-grid">
|
||||
<view v-for="(item, index) in list" :key="index" class="wall-item" :style="itemStyle" @tap="goDetail(item)">
|
||||
<!-- 骨架屏 -->
|
||||
<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" :style="initialStyle(item)">{{ (item.name || '名').substring(0, 1) }}</text>
|
||||
<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">
|
||||
<text class="item-name" :style="nameStyle">{{ item.name || '未命名名片' }}</text>
|
||||
<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 class="wall-empty" :style="infoStyle">
|
||||
<text>{{ emptyTip }}</text>
|
||||
<!-- 空态 -->
|
||||
<view v-else-if="!loading" class="wall-empty" :style="infoStyle">
|
||||
<text class="empty-icon">🗂️</text>
|
||||
<text class="empty-tip">{{ cardIdsConfigured ? '暂无可展示的名片' : '商家还未挑选名片' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -26,114 +63,166 @@
|
||||
|
||||
<script>
|
||||
// 数字名片 · 团队名片墙(装修组件)
|
||||
// 数据口径: 商家在设计面板手动挑选 card_ids, 走 /card/api/diy/cards 公开摘要
|
||||
import DiyMinx from './minx.js';
|
||||
import golbalConfig from '@/common/js/golbalConfig.js';
|
||||
// 布局: 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 }
|
||||
},
|
||||
mixins: [DiyMinx, golbalConfig],
|
||||
data() {
|
||||
return {
|
||||
list: []
|
||||
list: [],
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
v() {
|
||||
return this.value || {};
|
||||
},
|
||||
cardIds() {
|
||||
return String((this.value || {}).cardIds || '')
|
||||
return String(this.v.cardIds || '')
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 20)
|
||||
.join(',');
|
||||
},
|
||||
showArrow() {
|
||||
const flag = this.getDiyToggle(['showArrow']);
|
||||
return flag === undefined ? true : flag;
|
||||
cardIdsConfigured() {
|
||||
return String(this.v.cardIds || '').trim() !== '';
|
||||
},
|
||||
emptyTip() {
|
||||
const v = this.value || {};
|
||||
return String(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() {
|
||||
const v = this.value || {};
|
||||
return this.$util.objToStyle({
|
||||
background: v.bgColor || '#FFFFFF',
|
||||
borderRadius: this.px2rpx(v.borderRadius || 12) + 'rpx',
|
||||
padding: this.px2rpx(v.padding || 12) + 'rpx'
|
||||
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.value || {}).titleColor || '#333333' });
|
||||
return this.$util.objToStyle({ color: this.v.titleColor || CardTokens.color.text });
|
||||
},
|
||||
nameStyle() {
|
||||
return this.$util.objToStyle({ color: (this.value || {}).nameColor || '#1F2329' });
|
||||
return this.$util.objToStyle({ color: this.v.nameColor || CardTokens.color.text });
|
||||
},
|
||||
infoStyle() {
|
||||
return this.$util.objToStyle({ color: (this.value || {}).infoColor || '#888888' });
|
||||
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 v = this.value || {};
|
||||
const gap = Number(v.gap) || 10;
|
||||
const cols = Number(v.columns) === 1 ? 1 : 2;
|
||||
const width = cols === 1 ? '100%' : 'calc(50% - ' + this.px2rpx(gap / 2) + 'rpx)';
|
||||
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: this.px2rpx(gap) + 'rpx'
|
||||
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: {
|
||||
px2rpx(v) {
|
||||
const n = Number(v);
|
||||
return isNaN(n) ? 0 : Math.round(n * 2);
|
||||
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;
|
||||
},
|
||||
});
|
||||
},
|
||||
avatarStyle(item) {
|
||||
return this.$util.objToStyle({ background: item.theme_color || '#2F6BFF' });
|
||||
},
|
||||
initialStyle(item) {
|
||||
return this.$util.objToStyle({ color: '#FFFFFF' });
|
||||
},
|
||||
infoLine(item) {
|
||||
const v = this.value || {};
|
||||
const parts = [];
|
||||
const showJob = this.getDiyToggle(['showJob']);
|
||||
const showCompany = this.getDiyToggle(['showCompany']);
|
||||
if ((showJob === undefined || showJob) && item.job_title) parts.push(item.job_title);
|
||||
if ((showCompany === undefined || showCompany) && item.company) parts.push(item.company);
|
||||
return parts.join(' · ');
|
||||
},
|
||||
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>
|
||||
|
||||
@@ -145,86 +234,266 @@ export default {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.wall-title {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.wall-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.wall-item {
|
||||
box-sizing: border-box;
|
||||
.wall-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 2rpx solid #f0f0f0;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
overflow: hidden;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.item-avatar {
|
||||
flex-shrink: 0;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.item-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-meta {
|
||||
font-size: 22rpx;
|
||||
margin-top: 4rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.item-arrow {
|
||||
flex-shrink: 0;
|
||||
margin-left: 8rpx;
|
||||
.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 {
|
||||
padding: 30rpx 0;
|
||||
font-size: 24rpx;
|
||||
text-align: center;
|
||||
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>
|
||||
|
||||
582
components-diy/diy-card-mini.vue
Normal file
582
components-diy/diy-card-mini.vue
Normal file
@@ -0,0 +1,582 @@
|
||||
<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>
|
||||
@@ -1,65 +1,55 @@
|
||||
<template>
|
||||
<view data-component-name="diy-card-self" class="diy-card-self">
|
||||
<!-- 未登录 + 登录引导兜底 -->
|
||||
<view v-if="!hasToken && fallbackMode === 'none'" class="self-login" :style="boxStyle" @tap="goLogin">
|
||||
<view v-if="!hasToken && fallbackMode === 'none'" class="self-login" :style="boxStyle" @tap="goLogin" hover-class="btn-press">
|
||||
<text class="login-tip" :style="nameStyle">{{ value.loginTip || '登录后这里展示我的名片' }}</text>
|
||||
<view class="login-btn" :style="detailStyle">
|
||||
<view class="login-btn" :style="loginBtnStyle">
|
||||
<text class="login-btn-text">{{ value.loginBtnText || '去登录' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 名片卡(已登录=本人名片 / 未登录=兜底名片) -->
|
||||
<view v-else-if="card" class="self-box" :style="boxStyle">
|
||||
<view v-if="isSelf" class="self-badge" :style="badgeStyle">
|
||||
<text class="badge-text">我的名片</text>
|
||||
</view>
|
||||
|
||||
<view class="card-head">
|
||||
<view class="head-avatar" :style="avatarStyle">
|
||||
<image v-if="card.avatar" class="avatar-img" :src="$util.img(card.avatar)" mode="aspectFill" />
|
||||
<text v-else class="avatar-initial">{{ (card.name || '名').substring(0, 1) }}</text>
|
||||
<diy-card-mini
|
||||
v-else-if="card"
|
||||
:card="card"
|
||||
:config="miniConfig"
|
||||
:loading="loading"
|
||||
@act="doAct"
|
||||
@detail="goDetail"
|
||||
>
|
||||
<template v-if="isSelf" slot="badge">
|
||||
<view class="self-badge" :style="badgeStyle">
|
||||
<text class="badge-text">我的名片</text>
|
||||
</view>
|
||||
<view class="head-info">
|
||||
<text class="head-name" :style="nameStyle">{{ card.name || '未命名名片' }}</text>
|
||||
<text v-if="subLine" class="head-sub" :style="infoStyle">{{ subLine }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="showQuick && card.acts && card.acts.length" class="card-acts">
|
||||
<view v-for="(act, index) in card.acts" :key="index" class="act-btn" :style="actStyle" @tap.stop="doAct(act)">
|
||||
<text class="act-text" :style="actTextStyle">{{ act.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="showDetailBtn" class="detail-btn" :style="detailStyle" @tap.stop="goDetail">
|
||||
<text class="detail-text">{{ value.detailBtnText || '查看完整名片' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</diy-card-mini>
|
||||
|
||||
<!-- 已登录但没有任何名片可显示 -->
|
||||
<view v-else class="self-empty" :style="boxStyle">
|
||||
<text class="empty-tip" :style="infoStyle">登录后即可展示我的名片</text>
|
||||
<view v-else-if="!loading" class="self-empty" :style="boxStyle">
|
||||
<text class="empty-icon">🪪</text>
|
||||
<text class="empty-tip" :style="subTextStyle">{{ value.noCardTip || '登录后即可展示我的名片' }}</text>
|
||||
<view class="empty-btn" :style="loginBtnStyle" hover-class="btn-press" @tap="goCardList">
|
||||
<text class="empty-btn-text">{{ value.noCardBtnText || '去看看名片' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 数字名片 · 我的名片(装修组件)
|
||||
// 运行时判定: 操作者已登录 → 本人名片(optionalAuth); 未登录 → 配置的兜底名片
|
||||
// 运行时判定: 已登录 → 本人名片(optionalAuth); 未登录 → 配置的兜底名片
|
||||
// fallbackMode: enterprise=店铺企业名片 / card=指定 fallbackCardId / none=登录引导
|
||||
import DiyMinx from './minx.js';
|
||||
import golbalConfig from '@/common/js/golbalConfig.js';
|
||||
|
||||
// 视觉基件 diy-card-mini; 本文件管登录态/兜底/空态 CTA
|
||||
export default {
|
||||
name: 'diy-card-self',
|
||||
props: {
|
||||
value: { type: Object }
|
||||
},
|
||||
mixins: [DiyMinx, golbalConfig],
|
||||
data() {
|
||||
return {
|
||||
card: null,
|
||||
isSelf: false
|
||||
isSelf: false,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -74,48 +64,55 @@ export default {
|
||||
const m = String((this.value || {}).fallbackMode || 'enterprise');
|
||||
return ['enterprise', 'card', 'none'].includes(m) ? m : 'enterprise';
|
||||
},
|
||||
showQuick() {
|
||||
const flag = this.getDiyToggle(['showQuick']);
|
||||
return flag === undefined ? true : flag;
|
||||
},
|
||||
showDetailBtn() {
|
||||
const flag = this.getDiyToggle(['showDetailBtn']);
|
||||
return flag === undefined ? true : flag;
|
||||
},
|
||||
subLine() {
|
||||
const c = this.card || {};
|
||||
return [c.job_title, c.company].filter(Boolean).join(' · ');
|
||||
miniConfig() {
|
||||
const v = this.value || {};
|
||||
return {
|
||||
bgColor: v.bgColor,
|
||||
nameColor: v.nameColor,
|
||||
infoColor: v.infoColor,
|
||||
labelColor: v.labelColor,
|
||||
borderRadius: v.borderRadius,
|
||||
padding: v.padding,
|
||||
showQuick: v.showQuick,
|
||||
showDetailBtn: v.showDetailBtn,
|
||||
detailBtnText: v.detailBtnText,
|
||||
// 2.0 新增视觉开关
|
||||
useCover: v.useCover,
|
||||
showGradient: v.showGradient,
|
||||
showLogo: v.showLogo,
|
||||
showShadow: v.showShadow,
|
||||
showRows: v.showRows,
|
||||
maxRows: v.maxRows,
|
||||
showQr: v.showQr,
|
||||
qrTip: v.qrTip,
|
||||
};
|
||||
},
|
||||
boxStyle() {
|
||||
const v = this.value || {};
|
||||
return this.$util.objToStyle({
|
||||
background: v.bgColor || '#FFFFFF',
|
||||
borderRadius: this.px2rpx(v.borderRadius || 16) + 'rpx',
|
||||
padding: this.px2rpx(v.padding || 16) + 'rpx'
|
||||
padding: this.px2rpx(v.padding || 16) + 'rpx',
|
||||
boxShadow: '0 4rpx 16rpx rgba(15,23,42,0.06)',
|
||||
});
|
||||
},
|
||||
avatarStyle() {
|
||||
return this.$util.objToStyle({ background: (this.card || {}).theme_color || '#2F6BFF' });
|
||||
},
|
||||
nameStyle() {
|
||||
return this.$util.objToStyle({ color: (this.value || {}).nameColor || '#1F2329' });
|
||||
},
|
||||
infoStyle() {
|
||||
return this.$util.objToStyle({ color: (this.value || {}).infoColor || '#888888' });
|
||||
},
|
||||
actStyle() {
|
||||
const theme = (this.card || {}).theme_color || '#2F6BFF';
|
||||
return this.$util.objToStyle({ borderColor: theme });
|
||||
},
|
||||
actTextStyle() {
|
||||
return this.$util.objToStyle({ color: (this.card || {}).theme_color || '#2F6BFF' });
|
||||
},
|
||||
detailStyle() {
|
||||
return this.$util.objToStyle({ background: (this.card || {}).theme_color || '#2F6BFF' });
|
||||
subTextStyle() {
|
||||
return this.$util.objToStyle({ color: (this.value || {}).infoColor || '#8A8F99' });
|
||||
},
|
||||
badgeStyle() {
|
||||
return this.$util.objToStyle({ background: (this.card || {}).theme_color || '#2F6BFF' });
|
||||
}
|
||||
},
|
||||
loginBtnStyle() {
|
||||
return this.$util.objToStyle({
|
||||
background: (this.card || {}).theme_color || '#2F6BFF',
|
||||
height: '80rpx',
|
||||
borderRadius: '999rpx',
|
||||
padding: '0 48rpx',
|
||||
});
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
hasToken() {
|
||||
@@ -126,7 +123,7 @@ export default {
|
||||
},
|
||||
'value.fallbackCardId'() {
|
||||
this.fetchData();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchData();
|
||||
@@ -141,13 +138,15 @@ export default {
|
||||
this.card = null;
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
this.$api.sendRequest({
|
||||
url: '/card/api/diy/mycard',
|
||||
data: {
|
||||
fallback_mode: this.fallbackMode,
|
||||
fallback_card_id: String((this.value || {}).fallbackCardId || '').trim()
|
||||
fallback_card_id: String((this.value || {}).fallbackCardId || '').trim(),
|
||||
},
|
||||
success: res => {
|
||||
this.loading = false;
|
||||
if (res.code == 0 && res.data) {
|
||||
this.card = res.data.card || null;
|
||||
this.isSelf = res.data.is_self == 1;
|
||||
@@ -155,12 +154,20 @@ export default {
|
||||
this.card = null;
|
||||
this.isSelf = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
this.loading = false;
|
||||
this.card = null;
|
||||
this.isSelf = false;
|
||||
},
|
||||
});
|
||||
},
|
||||
goLogin() {
|
||||
this.$util.redirectTo('/pages_tool/login/login');
|
||||
},
|
||||
goCardList() {
|
||||
this.$util.redirectTo('/pages_tool/card/list');
|
||||
},
|
||||
doAct(act) {
|
||||
if (!act || !act.value) return;
|
||||
if (act.type === 'tel') {
|
||||
@@ -168,7 +175,7 @@ export default {
|
||||
} else if (act.type === 'wx') {
|
||||
uni.setClipboardData({
|
||||
data: act.value,
|
||||
success: () => uni.showToast({ title: '微信号已复制', icon: 'none' })
|
||||
success: () => uni.showToast({ title: '微信号已复制', icon: 'none' }),
|
||||
});
|
||||
} else if (act.type === 'mail') {
|
||||
// #ifdef H5
|
||||
@@ -177,21 +184,21 @@ export default {
|
||||
// #ifndef H5
|
||||
uni.setClipboardData({
|
||||
data: act.value,
|
||||
success: () => uni.showToast({ title: '邮箱已复制', icon: 'none' })
|
||||
success: () => uni.showToast({ title: '邮箱已复制', icon: 'none' }),
|
||||
});
|
||||
// #endif
|
||||
} else {
|
||||
uni.setClipboardData({
|
||||
data: act.value,
|
||||
success: () => uni.showToast({ title: '已复制', icon: 'none' })
|
||||
success: () => uni.showToast({ title: '已复制', icon: 'none' }),
|
||||
});
|
||||
}
|
||||
},
|
||||
goDetail() {
|
||||
if (!this.card || !this.card.card_id) return;
|
||||
this.$util.redirectTo('/pages_tool/card/detail', { card_id: this.card.card_id });
|
||||
}
|
||||
}
|
||||
goDetail(card) {
|
||||
if (!card || !card.card_id) return;
|
||||
this.$util.redirectTo('/pages_tool/card/detail', { card_id: card.card_id });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -200,107 +207,19 @@ export default {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
.self-box {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
/* 「我的名片」角标: 覆在基件右上(基件已留 logo 位, badge 靠左一点避免重叠) */
|
||||
.self-badge {
|
||||
position: absolute;
|
||||
top: 10rpx;
|
||||
left: 10rpx;
|
||||
z-index: 10;
|
||||
border-radius: 999rpx 0 16rpx 0;
|
||||
padding: 8rpx 20rpx;
|
||||
|
||||
.self-badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-radius: 0 16rpx 0 16rpx;
|
||||
padding: 6rpx 20rpx;
|
||||
|
||||
.badge-text {
|
||||
font-size: 20rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.head-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: 44rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.head-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.head-name {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.head-sub {
|
||||
font-size: 24rpx;
|
||||
margin-top: 6rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-acts {
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.act-btn {
|
||||
border: 2rpx solid;
|
||||
border-radius: 999rpx;
|
||||
padding: 10rpx 28rpx;
|
||||
margin: 0 16rpx 16rpx 0;
|
||||
|
||||
.act-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
margin-top: 12rpx;
|
||||
border-radius: 999rpx;
|
||||
padding: 16rpx 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.detail-text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
.badge-text {
|
||||
font-size: 20rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,6 +230,12 @@ export default {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48rpx 24rpx;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64rpx;
|
||||
}
|
||||
|
||||
.login-tip,
|
||||
.empty-tip {
|
||||
@@ -319,17 +244,25 @@ export default {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
margin-top: 24rpx;
|
||||
border-radius: 999rpx;
|
||||
padding: 14rpx 48rpx;
|
||||
.login-btn,
|
||||
.empty-btn {
|
||||
margin-top: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.login-btn-text {
|
||||
font-size: 24rpx;
|
||||
color: #ffffff;
|
||||
.login-btn-text,
|
||||
.empty-btn-text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-press {
|
||||
opacity: 0.72;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,138 +1,82 @@
|
||||
<template>
|
||||
<view data-component-name="diy-card-show" class="diy-card-show">
|
||||
<view v-if="card" class="card-box" :style="boxStyle">
|
||||
<view class="card-head">
|
||||
<view class="head-avatar" :style="avatarStyle">
|
||||
<image v-if="card.avatar" class="avatar-img" :src="$util.img(card.avatar)" mode="aspectFill" />
|
||||
<text v-else class="avatar-initial">{{ (card.name || '名').substring(0, 1) }}</text>
|
||||
</view>
|
||||
<view class="head-info">
|
||||
<text class="head-name" :style="nameStyle">{{ card.name || '未命名名片' }}</text>
|
||||
<text v-if="subLine" class="head-sub" :style="infoStyle">{{ subLine }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="showRows && card.rows && card.rows.length" class="card-rows">
|
||||
<view v-for="(row, index) in card.rows" :key="index" class="row-line">
|
||||
<text class="row-label" :style="labelStyle">{{ row.label }}</text>
|
||||
<text class="row-value" :style="infoStyle">{{ row.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="showQuick && card.acts && card.acts.length" class="card-acts">
|
||||
<view v-for="(act, index) in card.acts" :key="index" class="act-btn" :style="actStyle" @tap.stop="doAct(act)">
|
||||
<text class="act-text" :style="actTextStyle">{{ act.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="showDetailBtn" class="detail-btn" :style="detailStyle" @tap.stop="goDetail">
|
||||
<text class="detail-text">{{ value.detailBtnText || '查看完整名片' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="card-empty" :style="boxStyle">
|
||||
<text class="empty-tip" :style="infoStyle">{{ emptyTip }}</text>
|
||||
</view>
|
||||
<diy-card-mini
|
||||
:card="card"
|
||||
:config="miniConfig"
|
||||
:loading="loading"
|
||||
empty-tip="商家还未选择名片"
|
||||
@act="doAct"
|
||||
@detail="goDetail"
|
||||
></diy-card-mini>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 数字名片 · 精选名片(装修组件): 单张名片的原生迷你卡
|
||||
// 数据口径: /card/api/diy/card?card_id=.. 公开摘要(与 wap 公开页同源)
|
||||
import DiyMinx from './minx.js';
|
||||
import golbalConfig from '@/common/js/golbalConfig.js';
|
||||
|
||||
// 数字名片 · 精选名片(装修组件)
|
||||
// 视觉基件 diy-card-mini(封面/渐变/Logo/主题色/折叠/芯片/二维码), 本文件只管数据
|
||||
export default {
|
||||
name: 'diy-card-show',
|
||||
props: {
|
||||
value: { type: Object }
|
||||
},
|
||||
mixins: [DiyMinx, golbalConfig],
|
||||
data() {
|
||||
return {
|
||||
card: null
|
||||
card: null,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showRows() {
|
||||
const flag = this.getDiyToggle(['showRows']);
|
||||
return flag === undefined ? true : flag;
|
||||
},
|
||||
showQuick() {
|
||||
const flag = this.getDiyToggle(['showQuick']);
|
||||
return flag === undefined ? true : flag;
|
||||
},
|
||||
showDetailBtn() {
|
||||
const flag = this.getDiyToggle(['showDetailBtn']);
|
||||
return flag === undefined ? true : flag;
|
||||
},
|
||||
emptyTip() {
|
||||
return String((this.value || {}).cardId || '').trim() === '' ? '商家还未选择名片' : '名片不存在或已删除';
|
||||
},
|
||||
subLine() {
|
||||
const c = this.card || {};
|
||||
return [c.job_title, c.company].filter(Boolean).join(' · ');
|
||||
},
|
||||
boxStyle() {
|
||||
miniConfig() {
|
||||
const v = this.value || {};
|
||||
return this.$util.objToStyle({
|
||||
background: v.bgColor || '#FFFFFF',
|
||||
borderRadius: this.px2rpx(v.borderRadius || 16) + 'rpx',
|
||||
padding: this.px2rpx(v.padding || 16) + 'rpx'
|
||||
});
|
||||
return {
|
||||
bgColor: v.bgColor,
|
||||
nameColor: v.nameColor,
|
||||
infoColor: v.infoColor,
|
||||
labelColor: v.labelColor,
|
||||
borderRadius: v.borderRadius,
|
||||
padding: v.padding,
|
||||
showQuick: v.showQuick,
|
||||
showDetailBtn: v.showDetailBtn,
|
||||
detailBtnText: v.detailBtnText,
|
||||
// 2.0 新增视觉开关
|
||||
useCover: v.useCover,
|
||||
showGradient: v.showGradient,
|
||||
showLogo: v.showLogo,
|
||||
showShadow: v.showShadow,
|
||||
showRows: v.showRows,
|
||||
maxRows: v.maxRows,
|
||||
showQr: v.showQr,
|
||||
qrTip: v.qrTip,
|
||||
};
|
||||
},
|
||||
avatarStyle() {
|
||||
return this.$util.objToStyle({ background: (this.card || {}).theme_color || '#2F6BFF' });
|
||||
},
|
||||
nameStyle() {
|
||||
return this.$util.objToStyle({ color: (this.value || {}).nameColor || '#1F2329' });
|
||||
},
|
||||
infoStyle() {
|
||||
return this.$util.objToStyle({ color: (this.value || {}).infoColor || '#888888' });
|
||||
},
|
||||
labelStyle() {
|
||||
return this.$util.objToStyle({ color: (this.value || {}).labelColor || '#999999' });
|
||||
},
|
||||
actStyle() {
|
||||
const theme = (this.card || {}).theme_color || '#2F6BFF';
|
||||
return this.$util.objToStyle({ borderColor: theme });
|
||||
},
|
||||
actTextStyle() {
|
||||
return this.$util.objToStyle({ color: (this.card || {}).theme_color || '#2F6BFF' });
|
||||
},
|
||||
detailStyle() {
|
||||
return this.$util.objToStyle({ background: (this.card || {}).theme_color || '#2F6BFF' });
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'value.cardId'() {
|
||||
this.fetchData();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchData();
|
||||
},
|
||||
methods: {
|
||||
px2rpx(v) {
|
||||
const n = Number(v);
|
||||
return isNaN(n) ? 0 : Math.round(n * 2);
|
||||
},
|
||||
fetchData() {
|
||||
const cardId = String((this.value || {}).cardId || '').trim();
|
||||
if (cardId === '') {
|
||||
this.card = null;
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
this.$api.sendRequest({
|
||||
url: '/card/api/diy/card',
|
||||
data: { card_id: cardId },
|
||||
success: res => {
|
||||
if (res.code == 0 && res.data && res.data.card) {
|
||||
this.card = res.data.card;
|
||||
} else {
|
||||
this.card = null;
|
||||
}
|
||||
}
|
||||
this.loading = false;
|
||||
this.card = (res.code == 0 && res.data && res.data.card) ? res.data.card : null;
|
||||
},
|
||||
fail: () => {
|
||||
this.loading = false;
|
||||
this.card = null;
|
||||
},
|
||||
});
|
||||
},
|
||||
doAct(act) {
|
||||
@@ -142,7 +86,7 @@ export default {
|
||||
} else if (act.type === 'wx') {
|
||||
uni.setClipboardData({
|
||||
data: act.value,
|
||||
success: () => uni.showToast({ title: '微信号已复制', icon: 'none' })
|
||||
success: () => uni.showToast({ title: '微信号已复制', icon: 'none' }),
|
||||
});
|
||||
} else if (act.type === 'mail') {
|
||||
// #ifdef H5
|
||||
@@ -151,151 +95,26 @@ export default {
|
||||
// #ifndef H5
|
||||
uni.setClipboardData({
|
||||
data: act.value,
|
||||
success: () => uni.showToast({ title: '邮箱已复制', icon: 'none' })
|
||||
success: () => uni.showToast({ title: '邮箱已复制', icon: 'none' }),
|
||||
});
|
||||
// #endif
|
||||
} else {
|
||||
uni.setClipboardData({
|
||||
data: act.value,
|
||||
success: () => uni.showToast({ title: '已复制', icon: 'none' })
|
||||
success: () => uni.showToast({ title: '已复制', icon: 'none' }),
|
||||
});
|
||||
}
|
||||
},
|
||||
goDetail() {
|
||||
if (!this.card || !this.card.card_id) return;
|
||||
this.$util.redirectTo('/pages_tool/card/detail', { card_id: this.card.card_id });
|
||||
}
|
||||
}
|
||||
goDetail(card) {
|
||||
if (!card || !card.card_id) return;
|
||||
this.$util.redirectTo('/pages_tool/card/detail', { card_id: card.card_id });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.diy-card-show {
|
||||
width: 100%;
|
||||
|
||||
.card-box {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.head-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: 44rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.head-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.head-name {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.head-sub {
|
||||
font-size: 24rpx;
|
||||
margin-top: 6rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-rows {
|
||||
margin-top: 24rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 2rpx solid #f0f0f0;
|
||||
|
||||
.row-line {
|
||||
display: flex;
|
||||
line-height: 1.9;
|
||||
|
||||
.row-label {
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.row-value {
|
||||
flex: 1;
|
||||
font-size: 24rpx;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-acts {
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.act-btn {
|
||||
border: 2rpx solid;
|
||||
border-radius: 999rpx;
|
||||
padding: 10rpx 28rpx;
|
||||
margin: 0 16rpx 16rpx 0;
|
||||
|
||||
.act-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
margin-top: 12rpx;
|
||||
border-radius: 999rpx;
|
||||
padding: 16rpx 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.detail-text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-empty {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.empty-tip {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 30rpx 0;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user