Files
lucky_shop/components-diy/diy-card-show.vue

448 lines
12 KiB
Vue

<template>
<view data-component-name="diy-card-show" class="diy-card-show">
<!-- 骨架屏: 首屏加载/切换名片 -->
<view v-if="loading" class="show-skeleton" :style="cardShellStyle">
<view class="sk sk-line" style="width:42%"></view>
<view class="sk sk-line" style="width:26%;margin-top:16rpx;"></view>
<view class="sk sk-line" style="width:70%;margin-top:40rpx;"></view>
<view class="sk sk-line" style="width:58%;margin-top:16rpx;"></view>
<view class="sk sk-line" style="width:40%;margin-top:16rpx;"></view>
</view>
<!-- 空态: 未拿到名片 -->
<view v-else-if="!card" class="show-empty" :style="cardShellStyle">
<text class="empty-icon">💳</text>
<text class="empty-tip">商家还未选择名片</text>
</view>
<!-- 名片: 主题色顶栏 + 白卡压边(参考图版式, 自绘不复用 mini 基件) -->
<view v-else class="show-block">
<view class="show-band" :style="bandStyle"></view>
<view class="show-card" :style="cardShellStyle">
<!-- 头部: 姓名/职位 左, 小二维码"预览名片" 右 -->
<view class="card-head">
<view class="head-info">
<text class="card-name" :style="nameStyle">{{ card.name || '未命名名片' }}</text>
<text v-if="jobLine" class="card-job" :style="subStyle">{{ jobLine }}</text>
</view>
<view v-if="card.qr_url" class="card-qr" hover-class="btn-press" @tap.stop="goDetail(card)">
<image class="qr-img" :style="qrImgStyle" :src="$util.img(card.qr_url)" mode="aspectFit" />
<text class="qr-tip" :style="subStyle">预览名片</text>
</view>
</view>
<!-- 资料行(有字形字段显图标, 无字形退化为文字标签; href 派生点击动作) -->
<view v-if="shownRows.length" class="card-rows">
<view v-for="(row, i) in shownRows" :key="i" class="card-row"
:class="{ 'row-tappable': rowTapable(row) }"
:hover-class="rowTapable(row) ? 'btn-press' : 'none'"
@tap="onRowTap(row)">
<text v-if="rowIcon(row)" class="row-icon iconfont" :class="rowIcon(row)" :style="labelStyle"></text>
<text v-else 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.stop="expanded = !expanded">
<text class="toggle-text" :style="themeTextStyle">{{ expanded ? '收起资料' : '展开全部资料(' + hiddenCount + ')' }}</text>
</view>
</view>
</view>
<!-- 发名片: 卡片外下方胶囊(详情页 send-bar 同款观感, 主题渐变色) -->
<view class="show-send" :style="sendBtnStyle" hover-class="btn-press" @tap="onSend(card)">
<text class="show-send-text">发名片</text>
</view>
</view>
</view>
</template>
<script>
// 数字名片 · 精选名片(装修组件)
// 2026-10-21 换思路: 不再经 miniConfig 复用 diy-card-mini 通用布局, 改为本组件
// 自绘参考图版式 —— 主题色顶栏衬底 + 白卡压边 + 右上小二维码(预览名片) +
// 图标资料行 + 卡外「发名片」胶囊。面板仅颜色/底色/圆角/阴影/行数继续生效。
import {
CardTokens, px2rpx, num, themeGradient, visibleRows, rowIconFor, rowActOf,
} from './diy-card-common.js';
export default {
name: 'diy-card-show',
props: {
value: { type: Object }
},
data() {
return {
card: null,
loading: false,
expanded: false,
};
},
computed: {
c() {
return this.value || {};
},
/** 白卡外壳: 面板底色/圆角/阴影可调(内边距固定对齐参考图) */
cardShellStyle() {
const c = this.c;
return this.$util.objToStyle({
background: c.bgColor || CardTokens.color.bg,
borderRadius: px2rpx(num(c.borderRadius, 16)) + 'rpx',
boxShadow: c.showShadow === false ? 'none' : CardTokens.shadow.card,
});
},
/** 主题色顶栏: 名片主题渐变, 无主题退化默认主色 */
bandStyle() {
return this.$util.objToStyle({ background: themeGradient(this.card) });
},
/** 职位行: 职位优先, 缺失退公司/名片标题(参考图只显一行) */
jobLine() {
const card = this.card || {};
return card.job_title || card.company || card.card_title || '';
},
/** 二维码边长: 面板自定义 qrSize(px) > 0 时生效, 否则固定 48px */
qrSide() {
return px2rpx(num(this.c.qrSize, 48));
},
qrImgStyle() {
return this.$util.objToStyle({
width: this.qrSide + 'rpx',
height: this.qrSide + 'rpx',
});
},
nameStyle() {
return this.$util.objToStyle({ color: this.c.nameColor || CardTokens.color.text });
},
subStyle() {
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 });
},
themeTextStyle() {
return this.$util.objToStyle({ color: (this.card && this.card.theme_color) || CardTokens.color.primary });
},
/** 发名片按钮: 名片主题渐变色(无主题退化为默认主色), 详情页 send-bar 同款胶囊 */
sendBtnStyle() {
return this.$util.objToStyle({
background: themeGradient(this.card),
height: '88rpx',
borderRadius: '44rpx',
});
},
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;
},
},
watch: {
'value.cardId'() {
this.fetchData();
},
},
mounted() {
this.fetchData();
},
methods: {
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 => {
this.loading = false;
this.card = (res.code == 0 && res.data && res.data.card) ? res.data.card : null;
},
fail: () => {
this.loading = false;
this.card = null;
},
});
},
/** 资料行图标(字段无字形返回空串, 模板退化为文字标签) */
rowIcon(row) {
return rowIconFor(row && row.field);
},
/** 资料行是否可点: 服务端 href 派生得出动作 */
rowTapable(row) {
return !!rowActOf(row);
},
onRowTap(row) {
const act = rowActOf(row);
if (act) this.doAct(act);
},
doAct(act) {
if (!act || !act.value) return;
if (act.type === 'tel') {
uni.makePhoneCall({ phoneNumber: act.value, fail: () => {} });
} else if (act.type === 'wx') {
uni.setClipboardData({
data: act.value,
success: () => uni.showToast({ title: '微信号已复制', icon: 'none' }),
});
} else if (act.type === 'mail') {
// #ifdef H5
window.location.href = 'mailto:' + act.value;
// #endif
// #ifndef H5
uni.setClipboardData({
data: act.value,
success: () => uni.showToast({ title: '邮箱已复制', icon: 'none' }),
});
// #endif
} else {
uni.setClipboardData({
data: act.value,
success: () => uni.showToast({ title: '已复制', icon: 'none' }),
});
}
},
goDetail(card) {
if (!card || !card.card_id) return;
this.$util.redirectTo('/pages_tool/card/detail', { card_id: card.card_id });
},
/**
* 发名片: 跳原生转发中转页 /pages_tool/card/forward(内含 open-type=share 按钮,
* 直接拉起微信转发面板, 接收方落地 card/share 的 web-view 名片页)。
* 依赖 diy/card 返回 wap_url; 未返回时退化进详情页(详情页里可发名片)。
*/
onSend(card) {
if (!card) return;
const wapUrl = card.wap_url || card.share_url || '';
if (!wapUrl) {
this.goDetail(card);
return;
}
const q = [
'title=' + encodeURIComponent(card.name || ''),
'avatar=' + encodeURIComponent(card.avatar ? this.$util.img(card.avatar) : ''),
'url=' + encodeURIComponent(wapUrl)
].join('&');
// navigateTo 保留返回: 转发完可回到装修页
uni.navigateTo({ url: '/pages_tool/card/forward?' + q });
},
},
};
</script>
<style lang="scss" scoped>
.diy-card-show {
width: 100%;
/* 顶栏 + 白卡压边版式 */
.show-block {
position: relative;
width: 100%;
/* 卡片上方露蓝: 用 padding 撑开(子元素 margin-top 会塌陷出容器,
把蓝色背景层一起推下去, 顶部就露出页面白底了) */
padding-top: 40rpx;
/* 主题色顶栏: 只铺上半段, 下半段回落页面底色(白卡自行压边) */
.show-band {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 300rpx;
border-radius: 24rpx 24rpx 0 0;
}
.show-card {
position: relative;
z-index: 1;
margin: 0 24rpx;
padding: 40rpx;
box-sizing: border-box;
width: auto;
overflow: hidden;
.card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
.head-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
.card-name {
font-size: 38rpx;
font-weight: 700;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-job {
font-size: 26rpx;
margin-top: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
/* 右上小二维码 + 「预览名片」: 点按进完整名片页 */
.card-qr {
flex-shrink: 0;
margin-left: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
.qr-img {
width: 96rpx;
height: 96rpx;
display: block;
}
.qr-tip {
font-size: 22rpx;
margin-top: 8rpx;
}
}
}
.card-rows {
margin-top: 36rpx;
.card-row {
display: flex;
align-items: center;
line-height: 2.1;
/* 图标/文字标签共槽, 值列左缘对齐(参考图口径) */
.row-icon {
flex-shrink: 0;
width: 88rpx;
text-align: left;
font-size: 28rpx;
}
.row-label {
flex-shrink: 0;
width: 88rpx;
font-size: 24rpx;
}
.row-value {
flex: 1;
font-size: 26rpx;
word-break: break-all;
}
&.row-tappable {
cursor: pointer;
}
}
.row-toggle {
margin-top: 8rpx;
display: flex;
justify-content: flex-end;
.toggle-text {
font-size: 22rpx;
font-weight: 500;
}
}
}
}
/* 发名片: 卡片外下方胶囊(背景色由 sendBtnStyle 内联) */
.show-send {
position: relative;
z-index: 1;
margin: 32rpx 24rpx 0;
display: flex;
align-items: center;
justify-content: center;
.show-send-text {
font-size: 30rpx;
font-weight: 600;
color: #ffffff;
}
}
}
/* 空态 */
.show-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;
color: #8a8f99;
text-align: center;
}
}
/* 骨架屏 */
.show-skeleton {
width: 100%;
box-sizing: border-box;
padding: 40rpx;
}
.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;
}
}
}
/* 按压反馈 */
.btn-press {
opacity: 0.72;
transform: scale(0.98);
}
@keyframes sk-shimmer {
0% {
background-position: -200rpx 0;
}
100% {
background-position: 200rpx 0;
}
}
</style>