Files
lucky_shop/components-diy/diy-card-self.vue
ZF sun f1bd606e0b fix(diy-card): 审计修复——corner 档头部水平让位动态化 + self 首载骨架屏
- 修复 P1: corner 档自定义 qrSize>120 时静态 padding-right:180rpx
  让位不足, 浮层压住头部姓名区 → 新增 cornerHeadStyle 按浮层实际
  宽度(尺寸+衬底+右偏移20+呼吸20)动态反算, 与 cornerClearStyle
  垂直让位同口径; 废弃静态 CSS 值(负断言锁定)
- 修复: self.vue 已登录首次加载三分支都不渲染(loading 且无 card)
  → 白屏闪烁; 改 v-else-if="loading || card" 挂上 mini 骨架屏
- 契约测试: 新增 cornerHeadStyle 断言 + resolveQrSize 三处口径 +
  静态让位废弃负断言; 3 项变异验证全抓住; 四文件 67 项全绿
2026-10-21 14:09:56 +08:00

281 lines
6.9 KiB
Vue

<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" hover-class="btn-press">
<text class="login-tip" :style="nameStyle">{{ value.loginTip || '登录后这里展示我的名片' }}</text>
<view class="login-btn" :style="loginBtnStyle">
<text class="login-btn-text">{{ value.loginBtnText || '去登录' }}</text>
</view>
</view>
<!-- 名片卡(已登录=本人名片 / 未登录=兜底名片); loading 时基件出骨架屏 -->
<diy-card-mini
v-else-if="loading || card"
:card="card"
:config="miniConfig"
:loading="loading"
@act="doAct"
@row-act="doAct"
@detail="goDetail"
>
<template v-if="isSelf" slot="badge">
<view class="self-badge" :style="badgeStyle">
<text class="badge-text">我的名片</text>
</view>
</template>
</diy-card-mini>
<!-- 已登录但没有任何名片可显示 -->
<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); 未登录 → 配置的兜底名片
// fallbackMode: enterprise=店铺企业名片 / card=指定 fallbackCardId / none=登录引导
// 视觉基件 diy-card-mini; 本文件管登录态/兜底/空态 CTA
export default {
name: 'diy-card-self',
props: {
value: { type: Object }
},
data() {
return {
card: null,
isSelf: false,
loading: false,
};
},
computed: {
hasToken() {
try {
return !!(this.$store && this.$store.state && this.$store.state.token);
} catch (e) {
return false;
}
},
fallbackMode() {
const m = String((this.value || {}).fallbackMode || 'enterprise');
return ['enterprise', 'card', 'none'].includes(m) ? m : 'enterprise';
},
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,
// 3.0 A+B 视觉升级
showRowIcons: v.showRowIcons,
rowTap: v.rowTap,
qrPosition: v.qrPosition,
// 3.1 头像开关 + 二维码样式/微调
showAvatar: v.showAvatar,
qrStyle: v.qrStyle,
qrSize: v.qrSize,
qrOffsetX: v.qrOffsetX,
qrOffsetY: v.qrOffsetY,
qrTipShow: v.qrTipShow,
};
},
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',
boxShadow: '0 4rpx 16rpx rgba(15,23,42,0.06)',
});
},
nameStyle() {
return this.$util.objToStyle({ color: (this.value || {}).nameColor || '#1F2329' });
},
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() {
this.fetchData();
},
'value.fallbackMode'() {
this.fetchData();
},
'value.fallbackCardId'() {
this.fetchData();
},
},
mounted() {
this.fetchData();
},
methods: {
px2rpx(v) {
const n = Number(v);
return isNaN(n) ? 0 : Math.round(n * 2);
},
fetchData() {
if (!this.hasToken && this.fallbackMode === 'none') {
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(),
},
success: res => {
this.loading = false;
if (res.code == 0 && res.data) {
this.card = res.data.card || null;
this.isSelf = res.data.is_self == 1;
} else {
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') {
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 });
},
},
};
</script>
<style lang="scss" scoped>
.diy-card-self {
width: 100%;
position: relative;
/* 「我的名片」角标: 覆在基件右上(基件已留 logo 位, badge 靠左一点避免重叠) */
.self-badge {
position: absolute;
top: 10rpx;
left: 10rpx;
z-index: 10;
border-radius: 999rpx 0 16rpx 0;
padding: 8rpx 20rpx;
.badge-text {
font-size: 20rpx;
color: #ffffff;
font-weight: 600;
}
}
.self-login,
.self-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;
}
.login-tip,
.empty-tip {
font-size: 28rpx;
font-weight: 600;
text-align: center;
}
.login-btn,
.empty-btn {
margin-top: 28rpx;
display: flex;
align-items: center;
justify-content: center;
.login-btn-text,
.empty-btn-text {
font-size: 26rpx;
font-weight: 600;
color: #ffffff;
}
}
}
}
.btn-press {
opacity: 0.72;
transform: scale(0.98);
}
</style>